ExtendableDto
ExtendableDto는 모든 DTO의 기본 클래스입니다. Sequelize 쿼리 옵션 생성, 데이터 변환, OpenAPI 스키마 생성을 담당합니다.
이 페이지에서 찾을 수 있는 것
| 심볼 | 타입 | 설명 |
|---|---|---|
ExtendableDto | class | DTO 기본 클래스 |
init() | 메서드 | OpenAPI 스키마 등록 |
map(data) | 메서드 | 원시 데이터 → DTO 변환 |
pagingMap(data) | 메서드 | 페이지네이션 데이터 변환 |
middleware(as?, user?, extraAs?) | 메서드 | Sequelize 쿼리 옵션 생성 |
generateScheme() | 메서드 | OpenAPI 스키마 객체 반환 |
swagger() | 메서드 | $ref 객체 반환 |
임포트
import { ExtendableDto } from '@asapjs/sequelize';클래스 개요
class ExtendableDto {
static isInitlized: boolean;
public init(): void;
public map(data: any): any;
public pagingMap(data: any): any;
public middleware(as?: string, user?: any, extraAs?: string[]): DtoMiddlewareReturn;
public generateScheme(): object;
public swagger(): object;
}생성자 패턴
ASAPJS DTO는 두 가지 패턴 중 하나를 사용하여 DTO 메타데이터를 등록합니다.
패턴 A — @Dto 클래스 데코레이터 (권장):
@Dto({ defineTable: UsersTable, timestamps: false, name: 'user_dto' })
export default class UserDto extends ExtendableDto {
@TypeIs.INT({ comment: '아이디' })
id: number;
@TypeIs.STRING({ comment: '이메일' })
email: string;
}@Dto를 사용할 때 init()을 수동으로 호출할 필요가 없습니다 — DTO 파일명이 *Dto.ts이면, initSequelizeModule()이 해당 파일을 스캔하여 자동으로 init()을 호출합니다.
패턴 B — 생성자에서 Reflect.defineMetadata 직접 사용 (레거시):
인스턴스별 제어가 필요한 특수한 경우에 사용할 수 있습니다.
init()
public init(): voidthis.constructor.name에서 DTO의 클래스 이름을 읽고 addScheme으로 OpenAPI 스키마를 등록합니다. sequelize::dtoInfo 메타데이터를 설정한 뒤 생성자 내부에서 DTO 클래스당 한 번 호출하세요. 여러 번 호출해도 무해하지만 중복입니다.
map(data)
public map(data: any): any원시 객체나 Sequelize 모델 인스턴스를 DTO에 선언된 키만 포함하는 일반 객체로 변환합니다.
TypeIs 종류별 동작
| 필드 종류 | 변환 방식 |
|---|---|
| 스칼라 TypeIs (INT, STRING 등) | fixValue가 정의된 경우 fixValue(o[key]) 호출; 그렇지 않으면 o[key] 직접 반환 |
TypeIs.DTO 필드 | 중첩 DTO를 인스턴스화하고 중첩 데이터에 재귀적으로 map()을 호출; 중첩 값이 없으면 null 반환 |
TypeIs.QUERY 필드 | QUERY 타입 자체의 fixValue 위임을 통해 fixValue(o[key]) 호출 |
| 입력의 알 수 없는 키 | 조용히 제거 — DTO에 선언된 키만 출력 |
입력 처리
data에.dataValues프로퍼티가 있으면(Sequelize 모델 인스턴스).dataValues객체를 소스로 사용합니다.data가 배열이면map()이 각 요소에 적용되어 배열을 반환합니다.
const user = await UsersTable.findOne({ where: { id: 1 } });
const dto = new UserInfoDto().map(user);
// dto = { id: 1, email: 'a@example.com', name: 'Alice' }
// password 및 목록에 없는 필드는 제외일반 객체도 전달 가능합니다:
const raw = { id: '5', email: 'b@example.com', name: 'Bob', password: 'secret' };
const dto = new UserInfoDto().map(raw);
// dto = { id: 5, email: 'b@example.com', name: 'Bob' }
// id는 TypeIs.INT의 fixValue에 의해 문자열 '5'에서 정수 5로 변환
// password는 UserInfoDto에 선언되지 않아 제거pagingMap(data)
public pagingMap(data: { data: any[]; page: number; page_size: number; [key: string]: any }): any페이지네이션된 쿼리 결과를 위한 편의 래퍼입니다. data.data의 모든 항목에 map()을 호출하고 결과를 원본 엔벨로프 객체에 다시 합칩니다.
const result = await someService.getPosts(paging);
const dto = new PostInfoDto().pagingMap(result);
// dto = { data: [...매핑된 DTO...], page: 1, page_size: 10, max_page: 5, ... }참고: 현재 Swagger 스키마(
paging.ts)에서는has_Next(대문자 N)로 정의되어 있으나,Repository의 실제 반환값은has_next(소문자 n)입니다. 향후 코드 수정으로 통일될 예정입니다.
middleware(as?, user?, extraAs?)
public middleware(
as?: string,
user?: any,
extraAs?: string[]
): DtoMiddlewareReturnDTO의 필드 선언에서 파생된 Sequelize 쿼리 옵션을 생성합니다.
반환 타입
interface DtoMiddlewareReturn {
as?: string; // 제공된 경우 연관 별칭
model: typeof Model; // defineTable의 엔티티 클래스
attributes: any[]; // Sequelize attributes 배열 (컬럼 + literal 표현식)
include?: any[]; // TypeIs.DTO 필드의 중첩 include 옵션
}필드 처리
| 필드 종류 | 반환값에 미치는 영향 |
|---|---|
| 스칼라 필드 (DTO 또는 QUERY가 아닌) | attributes 배열에 키 추가 |
TypeIs.QUERY 필드 | [Sequelize.literal(sql), alias] 튜플을 attributes에 추가; query() 함수는 { association, user }를 받음 |
TypeIs.DTO 필드 | 중첩 DTO에 재귀적으로 middleware()를 호출하고 결과를 include에 추가 |
파라미터
| 파라미터 | 타입 | 설명 |
|---|---|---|
as | string | 연관 별칭. 이 DTO가 TypeIs.DTO를 통해 다른 DTO 내부에 중첩될 때 사용됩니다. |
user | any | 인증된 사용자 객체(JWT 페이로드에서). TypeIs.QUERY의 query() 함수에 전달됩니다. |
extraAs | string[] | 깊이 중첩된 관계를 위한 추가 연관 경로 세그먼트. |
const options = new PostInfoDto().middleware();
const posts = await PostsTable.findAll({
...options,
where: { user_id: userId },
});generateScheme()
public generateScheme(): { type: 'object'; properties: Record<string, any> }DTO의 모든 TypeIs 데코레이터가 적용된 필드를 순회하며 각각에 toSwagger()를 호출하여 OpenAPI 스키마 객체를 반환합니다.
{
type: 'object',
properties: {
id: { type: 'integer', format: 'int32', description: 'User ID' },
email: { type: 'string', description: 'Email' },
name: { type: 'string', description: 'Display name' },
}
}swagger()
public swagger(): { $ref: string }이 DTO의 클래스 이름으로 등록된 스키마 컴포넌트를 가리키는 OpenAPI $ref 객체를 반환합니다.
new UserInfoDto().swagger()
// { $ref: '#/components/schemas/UserInfoDto' }전체 예제
요청 DTO (생성 작업)
// src/user/dto/CreateUserDto.ts
import { ExtendableDto, TypeIs } from '@asapjs/sequelize';
export default class CreateUserDto extends ExtendableDto {
@TypeIs.STRING({ comment: 'Email address' })
email: string;
@TypeIs.PASSWORD({ comment: 'Password' })
password: string;
@TypeIs.STRING({ comment: 'Display name' })
name: string;
}@Post 라우트의 body DTO로 사용:
@Post('/register', {
title: 'Register',
auth: false,
body: CreateUserDto,
response: UserInfoDto,
})
async register({ body }: ExecuteArgs) {
return await this.userService.register(body as CreateUserDto);
}응답 DTO (조회 작업)
// src/user/dto/UserInfoDto.ts
import { ExtendableDto, TypeIs } from '@asapjs/sequelize';
import UsersTable from '../domain/entity/UsersTable';
export default class UserInfoDto extends ExtendableDto {
@TypeIs.INT({ comment: 'User ID' })
id: number;
@TypeIs.STRING({ comment: 'Email' })
email: string;
@TypeIs.STRING({ comment: 'Display name' })
name: string;
}서비스에서 Sequelize 모델을 변환하는 데 사용:
// src/user/application/UserApplication.ts
async getUser(userId: number): Promise<UserInfoDto> {
const dto = new UserInfoDto();
const user = await UsersTable.findOne({
...dto.middleware(),
where: { id: userId },
});
return dto.map(user);
}페이지네이션 목록 응답
// src/post/application/PostApplication.ts
async getPosts(paging: { page: number; limit: number }) {
const dto = new PostInfoDto();
const { count, rows } = await PostsTable.findAndCountAll({
...dto.middleware(),
limit: paging.limit,
offset: (paging.page - 1) * paging.limit,
order: [['created_at', 'DESC']],
});
const max_page = Math.ceil(count / paging.limit);
return dto.pagingMap({
data: rows,
page: paging.page,
page_size: paging.limit,
max_page,
has_prev: paging.page > 1,
has_next: paging.page < max_page,
total_elements: count,
});
}라우트는 TypeIs.PAGING으로 엔벨로프 응답을 문서화합니다:
@Get('/', {
title: 'List posts',
query: PaginationQueryDto,
response: TypeIs.PAGING(PostInfoDto),
})
async getPosts({ paging }: ExecuteArgs) {
return await this.postService.getPosts(paging);
}중첩 DTO (관계형 데이터)
import { ExtendableDto, TypeIs, Dto } from '@asapjs/sequelize';
import PostsTable from '../domain/entity/PostsTable';
import UserInfoDto from '../../user/dto/UserInfoDto';
@Dto({ defineTable: PostsTable })
export default class PostInfoDto extends ExtendableDto {
@TypeIs.INT({ comment: 'Post ID' })
id: number;
@TypeIs.STRING({ comment: 'Title' })
title: string;
@TypeIs.TEXT({ comment: 'Content' })
content: string;
@TypeIs.DTO({ dto: UserInfoDto, as: 'user', comment: 'Author' })
user: UserInfoDto;
@TypeIs.DATETIME({ comment: 'Created at' })
created_at: Date;
}PostInfoDto에서 middleware()를 호출하면:
{
model: PostsTable,
attributes: ['id', 'title', 'content', 'created_at'],
include: [
{
model: UsersTable,
as: 'user',
attributes: ['id', 'email', 'name'],
}
]
}관련 항목
- @Dto 데코레이터 — DTO 메타데이터 등록
- TypeIs 복합 타입 — DTO, QUERY, ARRAY
- Repository —
IArgs.exportTo로 DTO를 사용하는 방법