Skip to content

Commit

Permalink
feat: v2 리팩토링 stock api (#663)
Browse files Browse the repository at this point in the history
* feat: stock api contract 작성

* feat: search & update stock api refactoring

* fix: update 시 VStock 테이블은 not updatable 이기 때문에 Book 레포 추가

* fix: Update contracts/src/stock/schema.ts

updatedAt 필드의 값을 Date->string 으로 처리

Co-authored-by: scarf <[email protected]>
  • Loading branch information
JeongJiHwan and scarf005 authored Aug 13, 2023
1 parent 69e1bc5 commit 80f6953
Show file tree
Hide file tree
Showing 16 changed files with 268 additions and 3 deletions.
4 changes: 2 additions & 2 deletions backend/src/entity/entities/VStock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Reservation } from './Reservation';
.addSelect('book.id', 'bookId')
.addSelect('book.status', 'status')
.addSelect('book.donator', 'donator')
.addSelect("date_format(book.updatedAt, '%Y-%m-%d-%T')", 'updatedAt')
.addSelect("date_format(book.updatedAt, '%Y-%m-%d %T')", 'updatedAt')
.addSelect('book_info.categoryId', 'categoryId')
.addSelect('category.name', 'category')
.from(Book, 'book')
Expand Down Expand Up @@ -62,7 +62,7 @@ export class VStock {
status: number;

@ViewColumn()
categoryId: string;
categoryId: number;

@ViewColumn()
callSign: string;
Expand Down
2 changes: 2 additions & 0 deletions backend/src/v2/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { contract } from '@jiphyeonjeon-42/contracts';
import { initServer } from '@ts-rest/express';

import { reviews } from './reviews/impl';
import { stock } from './stock/impl';

const s = initServer();
export default s.router(contract, {
reviews,
stock,
});
8 changes: 8 additions & 0 deletions backend/src/v2/shared/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,11 @@ export class BookInfoNotFoundError extends Error {
super(`개별 도서 정보 (id: ${bookInfoId})를 찾을 수 없습니다`);
}
}

export class BookNotFoundError extends Error {
declare readonly _tag: 'BookNotFoundError';

constructor(bookId: number) {
super(`도서 정보 (id: ${bookId})를 찾을 수 없습니다`);
}
}
10 changes: 9 additions & 1 deletion backend/src/v2/shared/responses.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { bookInfoNotFoundSchema, reviewNotFoundSchema } from '@jiphyeonjeon-42/contracts';
import { bookNotFoundSchema, bookInfoNotFoundSchema, reviewNotFoundSchema } from '@jiphyeonjeon-42/contracts';
import { z } from 'zod';

export const reviewNotFound = {
Expand All @@ -16,3 +16,11 @@ export const bookInfoNotFound = {
description: '검색한 책이 존재하지 않습니다.',
} as z.infer<typeof bookInfoNotFoundSchema>,
} as const;

export const bookNotFound = {
status: 404,
body: {
code: 'BOOK_NOT_FOUND',
description: '검색한 책이 존재하지 않습니다.',
} as z.infer<typeof bookNotFoundSchema>,
} as const;
10 changes: 10 additions & 0 deletions backend/src/v2/shared/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { AppRoute } from '@ts-rest/core';
import { AppRouteOptions } from '@ts-rest/express';
import { z } from 'zod';

export type HandlerFor<T extends AppRoute> = AppRouteOptions<T>['handler'];

export type Meta = z.infer<typeof meta>;
export const meta = z.object({
totalItems: z.number().nonnegative(),
itemCount: z.number().nonnegative(),
itemsPerPage: z.number().nonnegative(),
totalPages: z.number().nonnegative(),
currentPage: z.number().nonnegative(),
});
37 changes: 37 additions & 0 deletions backend/src/v2/stock/controller/controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { contract } from '@jiphyeonjeon-42/contracts';
import { P, match } from 'ts-pattern';
import { UpdateResult } from 'typeorm';
import {
bookNotFound,
HandlerFor,
} from '../../shared';
import { StockService } from '../service';

type GetDeps = Pick<StockService, 'searchStock'>;
type MkGet = (services: GetDeps) => HandlerFor<typeof contract.stock.get>;
export const mkGetStock: MkGet = ({ searchStock }) =>
async ({ query: { page, limit } }) => {
contract.stock.get.query.safeParse({ page, limit });
const result = await searchStock({ page, limit });

return match(result)
.otherwise(() => ({
status: 200,
body: result,
} as const));
};

type PatchDeps = Pick<StockService, 'updateStock'>;
type MkPatch = (services: PatchDeps) => HandlerFor<typeof contract.stock.patch>;
export const mkPatchStock: MkPatch = ({ updateStock }) =>
async ({ body: { id } }) => {
contract.stock.patch.body.safeParse({ id });
const result = await updateStock({ id });

return match(result)
.with(
P.instanceOf(UpdateResult),
() => ({ status: 200, body: '재고 상태가 업데이트되었습니다.' } as const),
)
.otherwise(() => bookNotFound);
};
9 changes: 9 additions & 0 deletions backend/src/v2/stock/controller/impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { mkGetStock, mkPatchStock } from './controller';
import {
StockService,
} from '../service';

export const implStockController = (service: StockService) => ({
getStock: mkGetStock(service),
patchStock: mkPatchStock(service),
});
1 change: 1 addition & 0 deletions backend/src/v2/stock/controller/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './controller';
23 changes: 23 additions & 0 deletions backend/src/v2/stock/impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { contract } from '@jiphyeonjeon-42/contracts';
import { initServer } from '@ts-rest/express';
import jipDataSource from '~/app-data-source';
import { VStock, Book } from '~/entity/entities';
import { implStockService } from '~/v2/stock/service/impl';
import { implStockController } from '~/v2/stock/controller/impl';

const service = implStockService({
stockRepo: jipDataSource.getRepository(VStock),
bookRepo: jipDataSource.getRepository(Book),
});

const handler = implStockController(service);

const s = initServer();
export const stock = s.router(contract.stock, {
get: {
handler: handler.getStock,
},
patch: {
handler: handler.patchStock,
},
});
12 changes: 12 additions & 0 deletions backend/src/v2/stock/service/impl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Repository } from 'typeorm';
import { VStock, Book } from '~/entity/entities';

import { mkSearchStock, mkUpdateStock } from './service';

export const implStockService = (repos: {
stockRepo: Repository<VStock>;
bookRepo: Repository<Book>;
}) => ({
searchStock: mkSearchStock(repos),
updateStock: mkUpdateStock(repos),
});
23 changes: 23 additions & 0 deletions backend/src/v2/stock/service/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { UpdateResult } from 'typeorm';
import { VStock } from '~/entity/entities';
import { Meta, BookNotFoundError } from '~/v2/shared';

type SearchArgs = {
page: number,
limit: number,
};

type UpdateArgs = {
id: number,
};

export type StockService = {
searchStock: (
args: SearchArgs,
) => Promise<{ items: VStock[], meta: Meta }>;
updateStock: (
args: UpdateArgs,
) => Promise<BookNotFoundError | UpdateResult>;
};

export * from './service';
54 changes: 54 additions & 0 deletions backend/src/v2/stock/service/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { match } from 'ts-pattern';

import { VStock, Book } from '~/entity/entities';
import { type Repository, LessThan } from 'typeorm';

import { startOfDay, addDays } from 'date-fns';
import { Meta } from '~/v2/shared';
import { BookNotFoundError } from '~/v2/shared/errors';
import type { StockService } from '.';

type Repos = { stockRepo: Repository<VStock> };

type MkSearchStock = (
repos: Repos
) => StockService['searchStock'];

export const mkSearchStock: MkSearchStock = ({ stockRepo }) =>
async ({ page, limit }) => {
const today = startOfDay(new Date());
const [items, totalItems] = await stockRepo.findAndCount({
where: {
updatedAt: LessThan(addDays(today, -15)),
},
take: limit,
skip: limit * page,
});

const meta: Meta = {
totalItems,
itemCount: items.length,
itemsPerPage: limit,
totalPages: Math.ceil(totalItems / limit),
currentPage: page + 1,
};

const returnObject = {
items,
meta,
};
console.log(returnObject);
return returnObject;
};

type MkUpdateStock = (
repos: Repos & { bookRepo: Repository<Book> })
=> StockService['updateStock'];
export const mkUpdateStock: MkUpdateStock = ({ stockRepo, bookRepo }) =>
async ({ id }) => {
const stock = await stockRepo.findOneBy({ bookId: id });

return match(stock)
.with(null, () => new BookNotFoundError(id))
.otherwise(() => bookRepo.update({ id }, { updatedAt: new Date() }));
};
2 changes: 2 additions & 0 deletions contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { initContract } from '@ts-rest/core';
import { reviewsContract } from './reviews';
import { usersContract } from './users';
import { likesContract } from './likes';
import { stockContract } from './stock';

export * from './reviews';
export * from './shared';
Expand All @@ -13,6 +14,7 @@ export const contract = c.router(
{
// likes: likesContract,
reviews: reviewsContract,
stock: stockContract,
users: usersContract,
},
{
Expand Down
3 changes: 3 additions & 0 deletions contracts/src/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ type ErrorMessage = { code: string; description: string };
export const mkErrorMessageSchema = <const T extends string>(code: T) =>
z.object({ code: z.literal(code) as z.ZodLiteral<T> });

export const bookNotFoundSchema =
mkErrorMessageSchema('BOOK_NOT_FOUND').describe('해당 도서가 존재하지 않습니다');

export const bookInfoNotFoundSchema = mkErrorMessageSchema('BOOK_INFO_NOT_FOUND').describe('해당 도서 연관 정보가 존재하지 않습니다');

export const serverErrorSchema = mkErrorMessageSchema('SERVER_ERROR').describe('서버에서 오류가 발생했습니다.');
Expand Down
36 changes: 36 additions & 0 deletions contracts/src/stock/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { initContract } from '@ts-rest/core';
import {
stockGetQuerySchema,
stockGetResponseSchema,
stockPatchBodySchema,
stockPatchResponseSchema
} from './schema';
import { bookNotFoundSchema } from '../shared';

const c = initContract();

export const stockContract = c.router(
{
get: {
method: 'GET',
path: '/search',
description: '책 재고 정보를 검색해 온다.',
query: stockGetQuerySchema,
responses: {
200: stockGetResponseSchema,
// 특정한 에러케이스가 생각나지 않습니다.
},
},
patch: {
method: 'PATCH',
path: '/update',
description: '책 재고를 확인하고 수정일시를 업데이트한다.',
body: stockPatchBodySchema,
responses: {
200: stockPatchResponseSchema,
404: bookNotFoundSchema,
},
},
},
{ pathPrefix: '/stock'},
);
37 changes: 37 additions & 0 deletions contracts/src/stock/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { metaSchema, positiveInt } from '../shared';
import { z } from '../zodWithOpenapi';

export const bookIdSchema = positiveInt.describe('업데이트 할 도서 ID');

export const stockPatchBodySchema = z.object({
id: bookIdSchema.openapi({ example: 0 }),
});

export const stockPatchResponseSchema = z.literal('재고 상태가 업데이트되었습니다.');

export const stockGetQuerySchema = z.object({
page: positiveInt.default(0),
limit: positiveInt.default(10),
});

export const stockGetResponseSchema = z.object({
items: z.array(
z.object({
bookId: positiveInt,
bookInfoId: positiveInt,
title: z.string(),
author: z.string(),
donator: z.string(),
publisher: z.string(),
publishedAt: z.string(),
isbn: z.string(),
image: z.string(),
status: positiveInt,
categoryId: positiveInt,
callSign: z.string(),
category: z.string(),
updatedAt: z.string(),
}),
),
meta: metaSchema,
});

0 comments on commit 80f6953

Please sign in to comment.