Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: server-common 리드미, jsdoc 작성 #4

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/server/common/README.md
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
# @awesome=dev/server-common

공용 Decorator와 BaseEntity, BaseService, BaseRepository, BaseException과 각종 필터와 데이터를 가공하는데 필요한 매서드를 제공합니다.

## Installation

```bash
$ npm i @awesome-dev/server-common
```

## Dependencies

@awesome-dev/server-typeorm
@awesome-dev/typings

## Support

Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).

## License

Nest is [MIT licensed](LICENSE).
15 changes: 15 additions & 0 deletions packages/server/common/src/base.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,20 @@ import { HttpStatus } from '@nestjs/common';
import { BaseIdEntity } from './entities';
import { BaseException } from './exceptions';

/**
* BaseRepository는 TypeORM의 Repository를 상속받아 BaseIdEntity를 사용하는 Repository의 기본 메서드를 제공합니다.
*/
export abstract class BaseRepository<Entity extends BaseIdEntity> extends Repository<Entity> {
protected isEntity(obj: unknown): obj is Entity {
return obj !== undefined && (obj as Entity).id !== undefined;
}

/**
* 해당 ID를 가진 Entity를 가져옵니다.
* id: Entity ID
* relations: 연결된 Entity를 가져올 때 사용할 Relation
* 해당 ID를 가진 Entity가 존재하면 Entity, 존재하지 않을 때는 MODEL_NOT_FOUND 예외를 발생시킵니다.
*/
async get(id: number, relations: string[] = []): Promise<Entity | null> {
return await this.findOne({
where: { id } as FindOptionsWhere<Entity>,
Expand All @@ -28,6 +37,12 @@ export abstract class BaseRepository<Entity extends BaseIdEntity> extends Reposi
.catch(error => Promise.reject(error));
}

/**
* 해당 ID들을 가진 Entity가 특정 사용자에게 속하는지 확인합니다.
* ids: Entity ID 배열
* userId: 사용자 ID
* 해당 ID들이 모두 특정 사용자에게 속하면 true, 아니면 false
*/
async checkBelongsTo(ids: number[], userId: number): Promise<boolean> {
const result = await this.createQueryBuilder()
.select('1')
Expand Down
43 changes: 43 additions & 0 deletions packages/server/common/src/base.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,21 @@ type EntityClass<Entity> = {
new (partial: PartialDeep<Entity>): Entity;
};

/**
* BaseService는 BaseIdEntity를 상속받아 Entity를 사용하는 Service의 기본 메서드를 제공합니다.
*/
export abstract class BaseService<
Entity extends BaseIdEntity,
Relations = EntityRelations<Entity>,
> {
abstract repository: BaseRepository<Entity>;

/**
* 해당 ID를 가진 Entity를 가져옵니다.
* id: Entity ID
* relations: 연결된 Entity를 가져올 때 사용할 Relation
* 해당 ID를 가진 Entity가 존재하면 Entity, 존재하지 않을 때는 null을 반환합니다.
*/
get(id: number, relations?: Relations): Promise<Entity | null> {
const _relations = Array.isArray(relations) ? relations : [relations];

Expand All @@ -30,10 +39,20 @@ export abstract class BaseService<
);
}

/**
* Entity 목록을 가져옵니다.
* options: ListOptions
* ListOptions에 따라 Entity 목록을 가져옵니다.
*/
async list(options?: ListOptions<Entity>): Promise<Entity[]> {
return await this.repository.find(options?.toFindOptions());
}

/**
* Entity를 하나 가져옵니다.
* partial: FindOneOptions
* relations: 연결된 Entity를 가져올 때 사용할 Relation
*/
findOne(partial: FindOneOptions<Entity>, relations?: Relations): Promise<Entity | null> {
return this.repository.findOne({
relations: relations as unknown as string[],
Expand All @@ -42,10 +61,21 @@ export abstract class BaseService<
});
}

/**
* Entity의 개수를 가져옵니다.
* options: ListOptions
* ListOptions에 따라 Entity의 개수를 가져옵니다.
*/
async count(options?: ListOptions<Entity>): Promise<number> {
return await this.repository.count(options?.toFindOptions());
}

/**
* 해당 ID들을 가진 Entity가 특정 사용자에게 속하는지 확인합니다.
* ids: Entity ID 배열
* userId: 사용자 ID
* 해당 ID들이 모두 특정 사용자에게 속하지 않으면 FORBIDDEN 예외를 발생시킵니다.
*/
async assertBelongsTo(ids: number[], userId: number): Promise<void> {
const isMine = await this.repository.checkBelongsTo(ids, userId);
if (!isMine) {
Expand All @@ -57,6 +87,10 @@ export abstract class BaseService<
}
}

/**
* 해당 ID를 가진 Entity를 삭제합니다.
* id: Entity ID
*/
async remove(id: number) {
const entity = await this.get(id);

Expand All @@ -65,10 +99,19 @@ export abstract class BaseService<
}
}

/**
* Entity를 생성합니다.
* partial: PartialDeep<Entity>
*/
create(partial: PartialDeep<Entity>) {
return this.repository.save(new (this.repository.target as EntityClass<Entity>)(partial));
}

/**
* 해당 ID를 가진 Entity를 수정합니다.
* id: Entity ID
* partial: PartialDeep<Entity>
*/
async update(id: number, partial: PartialDeep<Entity>) {
const existing = await this.get(id);

Expand Down
15 changes: 15 additions & 0 deletions packages/server/common/src/decorators/list-options-query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common';

import { parseFilter } from '../helpers';

/**
* ListOptionsQuery 데코레이터는 ListOptions를 가져오는 데코레이터입니다.
* exclude는 제외할 필드를, acceptRelations은 relations의 사용 여부를 설정합니다.
*/
export const ListOptionsQuery = <Entity>(options?: {
excludes?: string[];
acceptRelations?: boolean;
Expand Down Expand Up @@ -35,6 +39,11 @@ export const ListOptionsQuery = <Entity>(options?: {

export type Relation<Entity> = keyof Entity | `${string & keyof Entity}.${string}`;

/**
* ListOptions는 Entity 목록을 가져오는 데 사용되는 옵션입니다.
* where는 검색 조건을, page는 페이지네이션을, order는 정렬을, relations는 연결된 Entity를 가져올 때 사용할 Relation을 설정합니다.
* ListOptionsQuery 데코레이터를 사용하여 ListOptions를 가져올 수 있습니다.
*/
export class ListOptions<Entity> {
private _where?: Record<string, unknown>;
private _page?: {
Expand All @@ -44,10 +53,16 @@ export class ListOptions<Entity> {
private _order?: { field: keyof Entity; direction: 'ASC' | 'DESC' };
private _relations?: Relation<Entity>[];

/**
* 기존 ListOptions를 복사합니다.
*/
static from<Entity = unknown>(existing?: ListOptions<Entity>) {
return existing != null ? existing.copy() : new ListOptions<Entity>();
}

/**
* 검색 조건을 설정합니다.
*/
static for(input: Record<string, unknown>) {
return new ListOptions().where(input);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/server/common/src/entities/base.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import {
import { PartialDeep } from '@awesome-dev/typings';
import { ApiProperty } from '@nestjs/swagger';

/**
* BaseIdEntity를 상속받아 Entity를 생성할 수 있습니다.
* id, createdAt, updatedAt를 기본으로 가지고 있습니다.
*/
export class BaseIdEntity extends TypeORMBaseEntity {
constructor(attrs?: PartialDeep<BaseIdEntity>) {
super();
Expand Down
3 changes: 3 additions & 0 deletions packages/server/common/src/exceptions/base.exception.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { HttpException, HttpStatus } from '@nestjs/common';

/**
* BaseException을 상속받아 예외를 정의할 수 있습니다.
*/
export class BaseException extends HttpException {
constructor(params: { code: string; message: string; status: HttpStatus }) {
const { code, message, status } = params;
Expand Down
6 changes: 6 additions & 0 deletions packages/server/common/src/filters/all-exceptions.filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ import { HttpAdapterHost } from '@nestjs/core';

import { BaseException } from '../exceptions';

/**
* 모든 예외를 처리하는 필터.
* BaseException을 상속받은 예외는 BaseException의 handle 메서드를 호출합니다.
* NotFoundException는 ENDPOINT_NOT_FOUND 예외로 처리합니다.
* 그 외의 예외는 UNKNOWN 예외로 처리합니다.
*/
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
constructor(private readonly httpAdapterHost: HttpAdapterHost) {}
Expand Down
3 changes: 3 additions & 0 deletions packages/server/common/src/helpers/date.helpers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { format, utcToZonedTime } from 'date-fns-tz';

/**
* 서울 시간대로 날짜를 포맷합니다.
*/
export const formatSeoulDate = (pattern: string, date = new Date()): string => {
const timeZone = 'Asia/Seoul';
const zonedDate = utcToZonedTime(date, timeZone);
Expand Down
11 changes: 11 additions & 0 deletions packages/server/common/src/helpers/filter.helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ const parseValue = (value: unknown) => {
return value;
};

/**
* 필터링을 위한 파싱 함수
*/
export const parseFilter = (filter: unknown) => {
const relations: string[] = [];

Expand Down Expand Up @@ -130,6 +133,11 @@ export const parseFilter = (filter: unknown) => {
};
};

/**
* 페이지네이션을 위한 파싱 함수
* offset: 시작점
* limit: 가져올 개수
*/
export const parsePaginator = (offset: number, limit: number) => {
if (offset === undefined && limit === undefined) {
return null;
Expand All @@ -141,6 +149,9 @@ export const parsePaginator = (offset: number, limit: number) => {
return { skip: Number(offset), take: Number(limit) };
};

/**
* 정렬을 위한 파싱 함수
*/
export const parseSorter = (order: string) => {
if (order === undefined) {
return null;
Expand Down
17 changes: 17 additions & 0 deletions packages/server/common/src/helpers/random.helpers.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
/**
* min ~ max 사이의 랜덤 정수를 반환합니다.
* min: 최소값
* max: 최대값
*/
export const getRandomIntBetween = (min: number, max: number): number => {
const num = Math.random() * (max - min) + min;
return Math.round(Math.max(min, Math.min(num, max)));
};

/**
* 배열을 무작위로 섞어 반환합니다.
*/
export const shuffleArray = <T = unknown>(arr: T[]): T[] => arr.sort(() => Math.random() - 0.5);

/**
* 배열에서 무작위 요소를 반환합니다.
*/
export const getRandomEle = <T = unknown>(arr: T[]): T =>
arr[Math.floor(Math.random() * arr.length)];

type UnknownEnum = Record<string, unknown>;

/**
* enum의 값들을 배열로 반환합니다.
*/
export const getEnumValues = (input: UnknownEnum) =>
Object.values(input).filter(value => typeof value === 'string');

Expand All @@ -21,6 +35,9 @@ export const getRandomEnumValue = <T extends UnknownEnum>(input: T, excludes: (k
return getRandomEle(valuesExcept);
};

/**
* 랜덤한 대소문자와 숫자를 포함한 6자리문자열을 반환합니다.
*/
export const getRandomString = (length = 6): string => {
let result = '';
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
Expand Down