URL 단축기 만들기 - 5. URL 서비스 만들기
이번에는 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;
그런 다음 다시 마이그레이션을 실행해 줍시다.
$ 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
에도 DatabaseModule
을 imports
에 추가해 주세요.
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
}
select Url {
slug, url
} filter .slug = <str>$slug;
delete Url filter .slug = <str>$slug;
이제 서비스 코드를 써봅시다.
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.