Skip to content

Commit

Permalink
feat: books api 스펙 정리 (#676)
Browse files Browse the repository at this point in the history
* feat: books contract 작성

* feat: 400, 500 에러 스키마 추가

* feat: 기부자 수정 api contract 추가

* refactor: 기부자 수정 api 404 에러 스키마 추가

* refactor: zod.extend 활용해 querySchema 중복 제거

* refactor: book status, z.enum 적용
  • Loading branch information
JeongJiHwan authored Aug 22, 2023
1 parent dff0587 commit 32547b9
Show file tree
Hide file tree
Showing 3 changed files with 331 additions and 0 deletions.
150 changes: 150 additions & 0 deletions contracts/src/books/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { initContract } from "@ts-rest/core";
import {
searchAllBooksQuerySchema,
searchAllBooksResponseSchema,
searchBookByIdResponseSchema,
searchBookInfoCreateQuerySchema,
searchBookInfoCreateResponseSchema,
createBookBodySchema,
createBookResponseSchema,
categoryNotFoundSchema,
formatErrorSchema,
insertionFailureSchema,
isbnNotFoundSchema,
naverBookNotFoundSchema,
updateBookBodySchema,
updateBookResponseSchema,
unknownPatchErrorSchema,
nonDataErrorSchema,
searchBookInfosQuerySchema,
searchBookInfosResponseSchema,
searchBookInfosSortedQuerySchema,
searchBookInfosSortedResponseSchema,
searchBookInfoByIdQuerySchema,
searchBookInfoByIdResponseSchema,
updateDonatorBodySchema,
updateDonatorResponseSchema
} from "./schema";
import { badRequestSchema, bookInfoNotFoundSchema, bookNotFoundSchema, serverErrorSchema } from "../shared";

const c = initContract();

export const booksContract = c.router(
{
searchAllBookInfos: {
method: 'GET',
path: '/info/search',
description: '책 정보(book_info)를 검색하여 가져온다.',
query: searchBookInfosQuerySchema,
responses: {
200: searchBookInfosResponseSchema,
400: badRequestSchema,
500: serverErrorSchema,
},
},
searchBookInfosByTag: {
method: 'GET',
path: '/info/tag',
description: '똑같은 내용의 태그가 달린 책의 정보를 검색하여 가져온다.',
query: searchBookInfosQuerySchema,
responses: {
200: searchBookInfosResponseSchema,
400: badRequestSchema,
500: serverErrorSchema,
},
},
searchBookInfosSorted: {
method: 'GET',
path: '/info/sorted',
description: '책 정보를 기준에 따라 정렬한다. 정렬기준이 popular일 경우 당일으로부터 42일간 인기순으로 한다.',
query: searchBookInfosSortedQuerySchema,
responses: {
200: searchBookInfosSortedResponseSchema,
400: badRequestSchema,
500: serverErrorSchema,
},
},
searchBookInfoById: {
method: 'GET',
path: '/info/:id',
description: 'book_info테이블의 ID기준으로 책 한 종류의 정보를 가져온다.',
query: searchBookInfoByIdQuerySchema,
responses: {
200: searchBookInfoByIdResponseSchema,
404: bookInfoNotFoundSchema,
500: serverErrorSchema
}
},
searchAllBooks: {
method: 'GET',
path: '/search',
description: '개별 책 정보(book)를 검색하여 가져온다. 책이 대출할 수 있는지 확인 할 수 있음',
query: searchAllBooksQuerySchema,
responses: {
200: searchAllBooksResponseSchema,
400: badRequestSchema,
500: serverErrorSchema,
},
},
searchBookInfoForCreate: {
method: 'GET',
path: '/create',
description: '책 생성을 위해 국립중앙도서관에서 ISBN으로 검색한 뒤에 책정보를 반환',
query: searchBookInfoCreateQuerySchema,
responses: {
200: searchBookInfoCreateResponseSchema,
303: isbnNotFoundSchema,
310: naverBookNotFoundSchema,
500: serverErrorSchema,
}
},
searchBookById: {
method: 'GET',
path: '/:bookId',
description: 'book테이블의 ID기준으로 책 한 종류의 정보를 가져온다.',
responses: {
200: searchBookByIdResponseSchema,
404: bookNotFoundSchema,
500: serverErrorSchema,
}
},
createBook: {
method: 'POST',
path: '/create',
description: '책 정보를 생성한다. bookInfo가 있으면 book에만 insert한다.',
body: createBookBodySchema,
responses: {
200: createBookResponseSchema,
308: insertionFailureSchema,
309: categoryNotFoundSchema,
311: formatErrorSchema,
500: serverErrorSchema,
},
},
updateBook: {
method: 'PATCH',
path: '/update',
description: '책 정보를 수정합니다. book_info table or book table',
body: updateBookBodySchema,
responses: {
204: updateBookResponseSchema,
312: unknownPatchErrorSchema,
313: nonDataErrorSchema,
311: formatErrorSchema,
500: serverErrorSchema,
},
},
updateDonator: {
method: 'PATCH',
path: '/donator',
description: '기부자 정보를 수정합니다.',
body: updateDonatorBodySchema,
responses: {
204: updateDonatorResponseSchema,
404: bookNotFoundSchema,
500: serverErrorSchema,
},
},
},
{ pathPrefix: '/books' },
)
179 changes: 179 additions & 0 deletions contracts/src/books/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import { metaSchema, positiveInt, mkErrorMessageSchema, statusSchema } from "../shared";
import { z } from "../zodWithOpenapi";

const commonQuerySchema = z.object({
query: z.string().optional(),
page: positiveInt.default(0),
limit: positiveInt.default(10),
});

export const searchBookInfosQuerySchema = commonQuerySchema.extend({
sort: z.string(),
category: z.string(),
});

export const searchBookInfosSortedQuerySchema = z.object({
sort: z.string(),
limit: positiveInt.default(10),
});

export const searchBookInfoByIdQuerySchema = z.object({
id: positiveInt,
});

export const searchAllBooksQuerySchema = commonQuerySchema;

export const searchBookInfoCreateQuerySchema = z.object({
isbnQuery: z.string(),
});

export const createBookBodySchema = z.object({
title: z.string(),
isbn: z.string(),
author: z.string(),
publisher: z.string(),
image: z.string(),
categoryId: z.string(),
pubdate: z.string(),
donator: z.string(),
});

export const updateBookBodySchema = z.object({
bookInfoId: positiveInt,
categoryId: positiveInt,
title: z.string(),
author: z.string(),
publisher: z.string(),
publishedAt: z.string(),
image: z.string(),
bookId: positiveInt,
callSign: z.string(),
status: statusSchema,
});

export const updateDonatorBodySchema = z.object({
bookId: positiveInt,
nickname: z.string(),
});

export const bookInfoSchema = z.object({
id: positiveInt,
title: z.string(),
author: z.string(),
publisher: z.string(),
isbn: z.string(),
image: z.string(),
category: z.string(),
publishedAt: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
lendingCnt: positiveInt,
});

export const searchBookInfosResponseSchema = z.object({
items: z.array(
bookInfoSchema,
),
categories: z.array(
z.object({
name: z.string(),
count: positiveInt,
}),
),
meta: metaSchema,
});

export const searchBookInfosSortedResponseSchema = z.object({
items: z.array(
bookInfoSchema,
)
});

export const searchBookInfoByIdResponseSchema = z.object({
bookInfoSchema,
books: z.array(
z.object({
id: positiveInt,
callSign: z.string(),
donator: z.string(),
status: statusSchema,
dueDate: z.string(),
isLendable: positiveInt,
isReserved: positiveInt,
}),
),
});

export const searchAllBooksResponseSchema = z.object({
items: z.array(
z.object({
bookId: positiveInt.openapi({ example: 1 }),
bookInfoId: positiveInt.openapi({ example: 1 }),
title: z.string().openapi({ example: '모두의 데이터 과학 with 파이썬' }),
author: z.string().openapi({ example: '드미트리 지노비에프' }),
donator: z.string().openapi({ example: 'mingkang' }),
publisher: z.string().openapi({ example: '길벗' }),
publishedAt: z.string().openapi({ example: '20170714' }),
isbn: z.string().openapi({ example: '9791160502152' }),
image: z.string().openapi({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/152/x9791160502152.jpg' }),
status: statusSchema.openapi({ example: 3 }),
categoryId: positiveInt.openapi({ example: 8 }),
callSign: z.string().openapi({ example: 'K23.17.v1.c1' }),
category: z.string().openapi({ example: '데이터 분석/AI/ML' }),
isLendable: positiveInt.openapi({ example: 0 }),
})
),
meta: metaSchema,
});

export const searchBookInfoCreateResponseSchema = z.object({
bookInfo: z.object({
title: z.string().openapi({ example: '작별인사' }),
image: z.string().openapi({ example: 'http://image.kyobobook.co.kr/images/book/xlarge/225/x9791191114225.jpg' }),
author: z.string().openapi({ example: '지은이: 김영하' }),
category: z.string().openapi({ example: '8' }),
isbn: z.string().openapi({ example: '9791191114225' }),
publisher: z.string().openapi({ example: '복복서가' }),
pubdate: z.string().openapi({ example: '20220502' }),
}),
})

export const searchBookByIdResponseSchema = z.object({
id: positiveInt.openapi({ example: 3 }),
bookId: positiveInt.openapi({ example: 3 }),
bookInfoId: positiveInt.openapi({ example: 2}),
title: z.string().openapi({ example: 'TCP IP 윈도우 소켓 프로그래밍(IT Cookbook 한빛 교재 시리즈 124)' }),
author: z.string().openapi({ example: '김선우' }),
donator: z.string().openapi({ example: 'mingkang' }),
publisher: z.string().openapi({ example: '한빛아카데미' }),
publishedAt: z.string().openapi({ example: '20130730' }),
isbn: z.string().openapi({ example: '9788998756444' }),
image: z.string().openapi({ example: 'https://image.kyobobook.co.kr/images/book/xlarge/444/x9788998756444.jpg' }),
status: statusSchema.openapi({ example: 0 }),
categoryId: positiveInt.openapi({ example: 2}),
callsign: z.string().openapi({ example: 'C5.13.v1.c2' }),
category: z.string().openapi({ example: '네트워크' }),
isLendable: positiveInt.openapi({ example: 1 }),
});

export const updateBookResponseSchema = z.literal('책 정보가 수정되었습니다.');

export const updateDonatorResponseSchema = z.literal('기부자 정보가 수정되었습니다.');

export const createBookResponseSchema = z.object({
callSign: z.string().openapi({ example: 'K23.17.v1.c1' }),
});

export const isbnNotFoundSchema = mkErrorMessageSchema('ISBN_NOT_FOUND').describe('국립중앙도서관 API에서 ISBN 검색이 실패하였습니다.');

export const naverBookNotFoundSchema = mkErrorMessageSchema('NAVER_BOOK_NOT_FOUND').describe('네이버 책검색 API에서 ISBN 검색이 실패');

export const insertionFailureSchema = mkErrorMessageSchema('INSERT_FAILURE').describe('예상치 못한 에러로 책 정보 insert에 실패함.');

export const categoryNotFoundSchema = mkErrorMessageSchema('CATEGORY_NOT_FOUND').describe('보내준 카테고리 ID에 해당하는 callsign을 찾을 수 없음');

export const formatErrorSchema = mkErrorMessageSchema('FORMAT_ERROR').describe('입력한 pubdate가 알맞은 형식이 아님. 기대하는 형식 "20220807"');

export const unknownPatchErrorSchema = mkErrorMessageSchema('PATCH_ERROR').describe('예상치 못한 에러로 patch에 실패.');

export const nonDataErrorSchema = mkErrorMessageSchema('NO_DATA_ERROR').describe('DATA가 적어도 한 개는 필요.');
2 changes: 2 additions & 0 deletions contracts/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export const dateLike = z.union([z.date(), z.string()]).transform(String)

export const bookInfoIdSchema = positiveInt.describe('개별 도서 ID');

export const statusSchema = z.enum(["ok", "lost", "damaged"]);

type ErrorMessage = { code: string; description: string };

/**
Expand Down

0 comments on commit 32547b9

Please sign in to comment.