Skip to content

Commit

Permalink
[#9] Add POST /api/post API
Browse files Browse the repository at this point in the history
  • Loading branch information
3jins committed Jul 23, 2021
1 parent 82c3231 commit f897171
Show file tree
Hide file tree
Showing 19 changed files with 626 additions and 207 deletions.
25 changes: 25 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@koa/router": "^9.4.0",
"@types/pino": "^6.3.4",
"ajv": "^8.4.0",
"ajv-formats": "^2.1.0",
"axios": "^0.21.1",
"config": "^3.3.2",
"file-type": "^15.0.1",
Expand Down
6 changes: 5 additions & 1 deletion backend/src/common/constant/URL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,8 @@ const ENDPOINT = {
TAG: '/tag',
};

export { PREFIX, ENDPOINT };
const BEHAVIOR = {
NEW: '/new',
};

export { PREFIX, ENDPOINT, BEHAVIOR };
7 changes: 7 additions & 0 deletions backend/src/common/error/BlogErrorCode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ const BlogErrorCode: { [key: string]: BlogErrorCodeFormat } = {
httpErrorCode: http2.constants.HTTP_STATUS_NOT_FOUND,
logLevel: LogLevel.ERROR,
},
TAGS_NOT_FOUND: {
code: 'TAGS_NOT_FOUND',
errorMessage: '존재하는 tag가 아닙니다.',
loggingMessage: '주어진 {0} 정보로는 tag를 찾을 수 없습니다. (찾지 못한 tag 리스트: {1})',
httpErrorCode: http2.constants.HTTP_STATUS_NOT_FOUND,
logLevel: LogLevel.ERROR,
},
INVALID_REQUEST_PARAMETER: {
code: 'INVALID_REQUEST_PARAMETER',
errorMessage: '형식에 맞지 않는 요청 파라미터가 시스템에 인입되었습니다.',
Expand Down
2 changes: 2 additions & 0 deletions backend/src/common/validation/DtoValidationUtil.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { Schema } from 'ajv/lib/types/index';
import { JSONSchemaType } from 'ajv/lib/types/json-schema';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
import BlogError from '@src/common/error/BlogError';
import { BlogErrorCode } from '@src/common/error/BlogErrorCode';

export const getValidatedRequestDtoOf = <T>(schema: Schema | JSONSchemaType<T>, requestBody: T): T => {
const ajv = new Ajv({ coerceTypes: true });
addFormats(ajv);
const validate = ajv.compile(schema);
if (!validate(requestBody)) {
throw new BlogError(BlogErrorCode.INVALID_REQUEST_PARAMETER, [JSON.stringify(requestBody)], JSON.stringify(validate.errors));
Expand Down
11 changes: 11 additions & 0 deletions backend/src/common/validation/ObjectTypeSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const fileTypeSchema = ({
type: 'object',
additionalProperties: true,
properties: {
size: { type: 'number', nullable: false },
path: { type: 'string', format: 'uri-template', nullable: false },
name: { type: 'string', nullable: false },
type: { type: 'string', pattern: 'application/octet-stream', nullable: false },
},
nullable: false,
});
64 changes: 36 additions & 28 deletions backend/src/post/PostRouter.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,36 @@
// import Router from '@koa/router';
// import koaBody from 'koa-body';
// import { Context } from 'koa';
// import Container from 'typedi';
// import _ from 'lodash';
// import { BlogErrorCode } from '@src/common/error/BlogErrorCode';
// import * as URL from '../common/constant/URL';
// import PostService from './PostService';
//
// const postRouter = new Router();
// const koaBodyOptions = {
// multipart: true,
// };
// const postService: PostService = Container.get(PostService);
//
// postRouter.post(`${URL.PREFIX.API}${URL.ENDPOINT.POST}`, koaBody(koaBodyOptions), (ctx: Context) => {
// if (_.isEmpty(ctx.request.files) || _.has(ctx.request.files!.post)) {
// throw new Error(BlogErrorCode.FILE_NOT_UPLOADED.code);
// }
//
// postService.createPost({
// post: ctx.request.files!.post,
// ...ctx.request.body,
// });
// ctx.status = 200;
// });
//
// export default postRouter;
import _ from 'lodash';
import Router from '@koa/router';
import koaBody from 'koa-body';
import { Context } from 'koa';
import Container from 'typedi';
import * as http2 from 'http2';
import { File } from 'formidable';
import * as URL from '@src/common/constant/URL';
import { getValidatedRequestDtoOf } from '@src/common/validation/DtoValidationUtil';
import PostService from '@src/post/PostService';
import BlogError from '@src/common/error/BlogError';
import { CreateNewPostRequestDto, CreateNewPostRequestSchema } from '@src/post/dto/PostRequestDto';
import { fileTypeSchema } from '@src/common/validation/ObjectTypeSchema';
import { BlogErrorCode } from '@src/common/error/BlogErrorCode';

const postRouter = new Router();
const koaBodyOptions = {
multipart: true,
};
const postService: PostService = Container.get(PostService);

postRouter.post(`${URL.PREFIX.API}${URL.ENDPOINT.POST}${URL.BEHAVIOR.NEW}`, koaBody(koaBodyOptions), (ctx: Context) => {
if (_.isEmpty(ctx.request.files)) {
throw new BlogError(BlogErrorCode.FILE_NOT_UPLOADED);
}

const requestDto: CreateNewPostRequestDto = getValidatedRequestDtoOf(CreateNewPostRequestSchema, ctx.request.body);
const post: File = getValidatedRequestDtoOf(fileTypeSchema, ctx.request.files!.post) as File;

postService.createNewPost({ ...requestDto, post })
.then(() => {
ctx.status = http2.constants.HTTP_STATUS_CREATED;
});
});

export default postRouter;
132 changes: 100 additions & 32 deletions backend/src/post/PostService.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,100 @@
// import { Service } from 'typedi';
// import fs from 'fs';
// import { CreatePostParamDto } from '@src/post/dto/PostParamDto';
// import PostRepository from '@src/post/repository/PostRepository';
//
// @Service()
// export default class PostService {
// public constructor(private readonly postRepository: PostRepository) {
// }
//
// public createPost(paramDto: CreatePostParamDto): void {
// const { post } = paramDto;
// const rawContent: string = this.readPostContent(post.path);
// const renderedContent = this.renderContent(rawContent);
// this.postRepository.createPost({
// ...paramDto,
// title: post.name,
// rawContent,
// renderedContent,
// createdDate: new Date(),
// });
// }
//
// private readPostContent(path: string): string {
// return fs.readFileSync(path).toString();
// }
//
// private renderContent(rawContent: string): string {
// // TODO: 렌더링 로직 구현
// return rawContent;
// }
// }
import fs from 'fs';
import _ from 'lodash';
import { Service } from 'typedi';
import { Types } from 'mongoose';
import PostMetaRepository from '@src/post/repository/PostMetaRepository';
import PostRepository from '@src/post/repository/PostRepository';
import CategoryRepository from '@src/category/CategoryRepository';
import SeriesRepository from '@src/series/SeriesRepository';
import TagRepository from '@src/tag/TagRepository';
import { CreatePostMetaRepoParamDto } from '@src/post/dto/PostMetaRepoParamDto';
import { CreateNewPostParamDto } from '@src/post/dto/PostParamDto';
import { CreatePostRepoParamDto } from '@src/post/dto/PostRepoParamDto';
import { CategoryDoc } from '@src/category/Category';
import { SeriesDoc } from '@src/series/Series';
import { TagDoc } from '@src/tag/Tag';
import BlogError from '@src/common/error/BlogError';
import { BlogErrorCode } from '@src/common/error/BlogErrorCode';

@Service()
export default class PostService {
public constructor(
private readonly postMetaRepository: PostMetaRepository,
private readonly postRepository: PostRepository,
private readonly categoryRepository: CategoryRepository,
private readonly seriesRepository: SeriesRepository,
private readonly tagRepository: TagRepository,
) {}

public async createNewPost(paramDto: CreateNewPostParamDto): Promise<void> {
const currentDate = new Date();
const createPostMetaRepoParamDto: CreatePostMetaRepoParamDto = await this.makeCreatePostMetaRepoParamDto(paramDto, currentDate);
const postNo = await this.postMetaRepository.createPostMeta(createPostMetaRepoParamDto);
const createPostRepoParamDto: CreatePostRepoParamDto = this.makeCreatePostRepoParamDto(postNo, paramDto, currentDate);
await this.postRepository.createPost(createPostRepoParamDto);
}

private async makeCreatePostMetaRepoParamDto(paramDto: CreateNewPostParamDto, currentDate: Date): Promise<CreatePostMetaRepoParamDto> {
const createPostMetaRepoParamDto: CreatePostMetaRepoParamDto = {
createdDate: currentDate,
isPrivate: _.isNil(paramDto.isPrivate) ? false : paramDto.isPrivate,
};
if (!_.isNil(paramDto.categoryName)) {
const categoryList: CategoryDoc[] = await this.categoryRepository.findCategory({ name: paramDto.categoryName });
if (_.isEmpty(categoryList)) {
throw new BlogError(BlogErrorCode.CATEGORY_NOT_FOUND, [paramDto.categoryName, 'name']);
}
Object.assign(createPostMetaRepoParamDto, { categoryId: categoryList[0]._id });
}
if (!_.isNil(paramDto.seriesName)) {
const seriesList: SeriesDoc[] = await this.seriesRepository.findSeries({ name: paramDto.seriesName });
if (_.isEmpty(seriesList)) {
throw new BlogError(BlogErrorCode.SERIES_NOT_FOUND, [paramDto.seriesName, 'name']);
}
Object.assign(createPostMetaRepoParamDto, { seriesId: seriesList[0]._id });
}
if (!_.isNil(paramDto.tagNameList)) {
const tagList: TagDoc[] = await this.tagRepository.findTag({
findTagByNameDto: {
nameList: paramDto.tagNameList,
isOnlyExactNameFound: true,
},
});
if (tagList.length !== paramDto.tagNameList.length) {
const failedToFindTagNameList = _.difference(paramDto.tagNameList, tagList.map((tag) => tag.name));
throw new BlogError(BlogErrorCode.TAG_NOT_FOUND, ['name', failedToFindTagNameList.join(', ')]);
}
Object.assign(createPostMetaRepoParamDto, { tagIdList: tagList.map((tag) => tag._id) });
}
return createPostMetaRepoParamDto;
}

private makeCreatePostRepoParamDto(postNo: number, paramDto: CreateNewPostParamDto, currentDate: Date): CreatePostRepoParamDto {
const { post } = paramDto;
const rawContent: string = this.readPostContent(post.path);
const renderedContent = this.renderContent(rawContent);
const createPostRepoParamDto: CreatePostRepoParamDto = {
postNo,
title: post.name as string,
rawContent,
renderedContent,
language: paramDto.language,
thumbnailContent: paramDto.thumbnailContent,
lastUpdatedDate: currentDate,
isLatestVersion: true,
};
if (!_.isNil(paramDto.thumbnailImageId)) {
Object.assign(createPostRepoParamDto, { thumbnailImageId: Types.ObjectId(paramDto.thumbnailImageId) });
}
return createPostRepoParamDto;
}

private readPostContent(path: string): string {
return fs.readFileSync(path).toString();
}

private renderContent(rawContent: string): string {
// TODO: 렌더링 로직 구현
return rawContent;
}
}
3 changes: 1 addition & 2 deletions backend/src/post/dto/PostMetaRepoParamDto.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { Types } from 'mongoose';

export interface CreatePostMetaRepoParamDto {
postNo: number;
categoryId?: Types.ObjectId;
tagIdList?: Types.ObjectId[];
seriesId?: Types.ObjectId;
tagIdList?: Types.ObjectId[];
createdDate: Date;
isPrivate?: boolean;
}
18 changes: 12 additions & 6 deletions backend/src/post/dto/PostParamDto.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { Types } from 'mongoose';
import { File } from 'formidable';
import Language from '@src/common/constant/Language';

export interface CreatePostParamDto {
export interface CreateNewPostParamDto {
// post meta
categoryName?: string;
tagNameList?: string[];
seriesName?: string;
isPrivate?: boolean;

// post
post: File;
title: string;
rawContent: string;
renderedContent: string;
isLatestVersion: boolean;
lastVersionPost?: Types.ObjectId;
language: Language;
thumbnailContent: string;
thumbnailImageId?: string;
}

export interface AddPostParamDto {
Expand Down
1 change: 1 addition & 0 deletions backend/src/post/dto/PostRepoParamDto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Types } from 'mongoose';
import Language from '@src/common/constant/Language';

export interface CreatePostRepoParamDto {
postNo: number;
title: string;
rawContent: string;
renderedContent: string;
Expand Down
31 changes: 31 additions & 0 deletions backend/src/post/dto/PostRequestDto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { JSONSchemaType } from 'ajv';
import Language from '@src/common/constant/Language';

export interface CreateNewPostRequestDto {
// post meta
categoryName?: string;
tagNameList?: string[];
seriesName?: string;
isPrivate?: boolean;

// post
language: Language;
thumbnailContent: string;
thumbnailImageId?: string;
}

export const CreateNewPostRequestSchema: JSONSchemaType<CreateNewPostRequestDto> = {
type: 'object',
additionalProperties: false,
properties: {
categoryName: { type: 'string', nullable: true },
tagNameList: { type: 'array', nullable: true, items: { type: 'string' } },
seriesName: { type: 'string', nullable: true },
isPrivate: { type: 'boolean', nullable: true },

language: { type: 'string', nullable: false },
thumbnailContent: { type: 'string', nullable: false },
thumbnailImageId: { type: 'string', nullable: true },
},
required: ['language', 'thumbnailContent'],
};
Loading

0 comments on commit f897171

Please sign in to comment.