URL 단축기 만들기 - 5. URL 서비스 만들기

URL 단축기 2022년 12월 9일

이번에는 URL 관련 서비스를 만들어 볼거에요. 이번에토 테스트 코드까지 써보는 걸로 할게요.

시작하기 전에, db 스키마에 정의했던 regex에 문제가 있는걸 확인했어요. 이건 일단 임시로 제거해두기로 할게요. dbschema/default.esdl 에서 regex 부분을 지워주세요.

-    required property url -> str {
-      constraint regexp(r'https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)'); # https://stackoverflow.com/a/3809435
-    }
+    required property url -> str;
default.esdl

그런 다음 다시 마이그레이션을 실행해 줍시다.

$ edgedb migration create
did you drop constraint 'std::regexp' of property 'url'? [y,n,l,c,b,s,q,?]
> y
Created dbschema/migrations/00002.edgeql, id: m1wwv3yepicvicyyvlfkssileenqfodhecelvkbjprlqqapcg6j6xa

$ edgedb migrate
Applied m1wwv3yepicvicyyvlfkssileenqfodhecelvkbjprlqqapcg6j6xa (00002.edgeql)

일단 regex constraint를 제거해줬어요. 이제 서비스 코드를 써볼게요!

그 전에, urls.module.ts 에도 DatabaseModuleimports 에 추가해 주세요.

import { Module } from '@nestjs/common'
import { DatabaseModule } from 'src/database/database.module'
import { UrlsService } from './urls.service'

@Module({
  providers: [UrlsService],
  imports: [DatabaseModule],
})
export class UrlsModule {}

그리고 오늘도 쿼리를 써줍시다.

지금 필요한 쿼리는 3가지입니다.

  • URL 생성
  • URL 찾기
  • URL 삭제

수정 기능은 딱히 만들 생각이 없네요. 그럼 이거에 대한 쿼리를 써봅시다.

select (
  insert Url {
    slug := <str>$slug,
    url := <str>$url
  }
) {
  id,
  slug,
  url
}
createUrl
select Url {
  slug, url
} filter .slug = <str>$slug;
findUrlBySlug
delete Url filter .slug = <str>$slug;
deleteUrl

이제 서비스 코드를 써봅시다.

import {
  BadRequestException,
  Inject,
  Injectable,
  NotFoundException,
} from '@nestjs/common'
import { EdgeDBClient } from 'src/database/client'
import { DI } from 'src/utils/DI'
import { createUrl } from './queries/createUrl.edgeql'
import { deleteUrl } from './queries/deleteUrl.edgeql'
import { findUrlBySlug } from './queries/findUrlBySlug.edgeql'
import { Url } from './types'

@Injectable()
export class UrlsService {
  constructor(@Inject(DI.EdgeDB) private db: EdgeDBClient) {}

  /**
   * Finds a url from database by slug
   * @param slug The slug for link
   * @returns The Url fetched from database
   */
  async findUrlBySlug(slug: string): Promise<Url> {
    const url = await findUrlBySlug(this.db, { slug })

    if (!url)
      throw new NotFoundException(
        `Cannot find URL with following slug: ${slug}`,
      )

    return { slug: url.slug, url: url.url }
  }

  /**
   * Creates a url with slug
   * @param slug The slug for link
   * @returns The Url fetched from database
   */
  async createUrl(
    url: string,
    slug: string = Date.now().toString(36),
  ): Promise<Url> {
    const existing = await findUrlBySlug(this.db, { slug })

    if (existing) throw new BadRequestException('URL already exists')

    const item = await createUrl(this.db, { slug, url })

    return {
      slug: item.slug,
      url: item.url,
    }
  }

  /**
   * Deletes a shortened url
   * @param slug The slug of link to delete
   */
  async deleteUrl(slug: string): Promise<void> {
    const deleted = await deleteUrl(this.db, { slug })

    if (!deleted) throw new NotFoundException('URL not found')
  }
}

이제 테스트 코드를 써줍시다.

import { Test, TestingModule } from '@nestjs/testing'
import { edgedbClient } from 'src/database/client'
import { DatabaseModule } from 'src/database/database.module'
import { UrlsService } from './urls.service'

describe('UrlsService', () => {
  let service: UrlsService
  let slug: string = ''

  beforeEach(async () => {
    const module: TestingModule = await Test.createTestingModule({
      providers: [UrlsService],
      imports: [DatabaseModule],
    }).compile()

    service = module.get<UrlsService>(UrlsService)
  })

  afterAll(async () => {
    if (slug) {
      await edgedbClient.execute(`delete Url filter .slug = <str>$slug;`, {
        slug,
      })
    }
  })

  it('should be defined', () => {
    expect(service).toBeDefined()
  })

  it('should throw not found exception', async () => {
    await expect(service.findUrlBySlug('wowthisistest')).rejects.toThrow(
      'Cannot find URL with following slug: wowthisistest',
    )
  })

  it('should create url', async () => {
    const url = await service.createUrl('https://google.com')

    slug = url.slug

    expect(url).toEqual({ url: 'https://google.com', slug: url.slug })
  })

  it('should throw error on create url(already exists)', async () => {
    await expect(service.createUrl('https://google.com', slug)).rejects.toThrow(
      'URL already exists',
    )
  })

  it('should find url', async () => {
    await expect(service.findUrlBySlug(slug)).resolves.toEqual({
      slug,
      url: 'https://google.com',
    })
  })

  it('should delete url', async () => {
    await expect(service.deleteUrl(slug)).resolves
  })

  it('should throw not found error(already deleted)', async () => {
    await expect(service.deleteUrl(slug)).rejects.toThrow('URL not found')
  })
})

afterAll에서 삭제를 한번더 해주는건 혹시 테스트가 실패해도 삭제는 되게 하려는 그런거에요..? 네 그렇다고요

$ yarn test:cov
 PASS  src/app.controller.spec.ts (7.952 s)
  AppController
    root
      ✓ should return hello world (22 ms)

 PASS  src/users/users.service.spec.ts (9.477 s)
  UsersService
    ✓ should be defined (37 ms)
    ✓ find an undefined user (180 ms)
    ✓ create a user (31 ms)
    ✓ userId should be defined (6 ms)
    ✓ find an undefined user (8 ms)
    ✓ delete a user (8 ms)
    ✓ delete a non-existing user (10 ms)

 PASS  src/urls/urls.service.spec.ts (9.501 s)
  UrlsService
    ✓ should be defined (25 ms)
    ✓ should throw not found exception (131 ms)
    ✓ should create url (20 ms)
    ✓ should throw error on create url(already exists) (8 ms)
    ✓ should find url (10 ms)
    ✓ should delete url (4 ms)
    ✓ should throw not found error(already deleted) (113 ms)

--------------------------|---------|----------|---------|---------|-------------------
File                      | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
--------------------------|---------|----------|---------|---------|-------------------
All files                 |    74.5 |      100 |   94.44 |    75.6 |                   
 src                      |   48.14 |      100 |      75 |   42.85 |                   
  app.controller.ts       |     100 |      100 |     100 |     100 |                   
  app.module.ts           |       0 |      100 |     100 |       0 | 1-13              
  app.service.ts          |     100 |      100 |     100 |     100 |                   
  main.ts                 |       0 |      100 |       0 |       0 | 1-8               
 src/database             |     100 |      100 |     100 |     100 |                   
  client.ts               |     100 |      100 |     100 |     100 |                   
  database.module.ts      |     100 |      100 |     100 |     100 |                   
 src/urls                 |   77.77 |      100 |     100 |   80.95 |                   
  urls.module.ts          |       0 |      100 |     100 |       0 | 1-9               
  urls.service.ts         |     100 |      100 |     100 |     100 |                   
 src/urls/queries         |     100 |      100 |     100 |     100 |                   
  createUrl.edgeql.ts     |     100 |      100 |     100 |     100 |                   
  deleteUrl.edgeql.ts     |     100 |      100 |     100 |     100 |                   
  findUrlBySlug.edgeql.ts |     100 |      100 |     100 |     100 |                   
 src/users                |      75 |      100 |     100 |   77.77 |                   
  users.module.ts         |       0 |      100 |     100 |       0 | 1-9               
  users.service.ts        |     100 |      100 |     100 |     100 |                   
 src/users/queries        |     100 |      100 |     100 |     100 |                   
  createUser.edgeql.ts    |     100 |      100 |     100 |     100 |                   
  deleteUser.edgeql.ts    |     100 |      100 |     100 |     100 |                   
  findUserById.edgeql.ts  |     100 |      100 |     100 |     100 |                   
 src/utils                |     100 |      100 |     100 |     100 |                   
  DI.ts                   |     100 |      100 |     100 |     100 |                   
  config.ts               |     100 |      100 |     100 |     100 |                   
--------------------------|---------|----------|---------|---------|-------------------
Test Suites: 3 passed, 3 total
Tests:       15 passed, 15 total
Snapshots:   0 total
Time:        19.451 s
Ran all test suites.

커버리지 100%를 보니 기부니가 조아요!히히

일단 url 서비스 완성했으니 여기까지 할게요! 읽어주셔서 감사합니당!

GitHub - paringparing/url-shortener: something
something. Contribute to paringparing/url-shortener development by creating an account on GitHub.

태그

파링

바보