데이터베이스
ASAPJS는 @asapjs/sequelize 패키지를 통해 Sequelize ORM과 통합됩니다. 이 패키지는 시작 시 모델과 DTO 파일을 자동으로 검색하고, 페이지네이션 쿼리를 위한 Repository 헬퍼를 제공하며, 스키마 동기화 및 헬스 체크 유틸리티를 노출합니다.
데이터베이스 레이어를 활성화하려면 config의 extensions 배열에 '@asapjs/sequelize'를 포함하고 db 블록을 제공하세요. 전체 config 레퍼런스는 부트스트랩을 참조하세요.
initSequelizeModule()
import { initSequelizeModule } from '@asapjs/sequelize';
const initSequelizeModule = async (dirname: string): Promise<Sequelize>전체 데이터베이스 레이어를 초기화합니다. extensions에 '@asapjs/sequelize'가 있으면 Application.run()이 자동으로 호출합니다. 일반적인 사용에서는 직접 호출할 필요가 없습니다.
| 파라미터 | 타입 | 설명 |
|---|---|---|
dirname | string | 컴파일된 출력 디렉토리의 절대 경로. 모든 하위 디렉토리를 재귀적으로 스캔합니다. |
반환값: Promise<Sequelize> — 초기화된 Sequelize 인스턴스.
초기화 단계:
getConfig().db를 읽어dbInit()으로 Sequelize 연결을 생성합니다.- 연결을 인증합니다. 대상 데이터베이스가 존재하지 않는 경우(MySQL
ER_BAD_DB_ERROR), 데이터베이스 이름 없이 임시 연결을 열고CREATE DATABASE IF NOT EXISTS를 실행한 후 재연결합니다. dirname에서*Table.js와*Dto.js에 해당하는 파일을 재귀적으로 스캔합니다.- 검색된 모든
*Table클래스를 Sequelize 모델로 등록합니다(addModels). - 검색된 모든
*Dto클래스를 인스턴스화하고dto.init()을 호출하여 해당 형태를 Swagger 컴포넌트 스키마로 등록합니다.
파일 네이밍 규칙:
| 파일명 패턴 | 등록 대상 |
|---|---|
*Table.js / *Table.ts | Sequelize 모델 — Sequelize 인스턴스에 추가 |
*Dto.js / *Dto.ts | DTO 클래스 — Swagger에 스키마 등록 |
.map으로 끝나는 파일은 무시됩니다.
getSequelize()
import { getSequelize } from '@asapjs/sequelize';
const sequelize = getSequelize(); // Sequelize 반환initSequelizeModule이 생성한 전역 Sequelize 인스턴스를 반환합니다. Application.run() 이후 어디서든 이 함수를 호출하여 Sequelize 인스턴스에 접근하고 로우 쿼리, 트랜잭션, 또는 모델에 직접 접근할 수 있습니다.
// 예시: 관리된 트랜잭션 시작
const sequelize = getSequelize();
await sequelize.transaction(async (t) => {
await UsersTable.create({ name: 'Alice' }, { transaction: t });
await PostsTable.create({ title: 'Hello', user_id: 1 }, { transaction: t });
});modelsSync()
import { modelsSync } from '@asapjs/sequelize';
const modelsSync = async (): Promise<true>등록된 모든 Sequelize 모델을 데이터베이스 스키마와 동기화합니다. Sequelize의 sync({ alter: { drop: false } }) 모드를 사용합니다 — 누락된 컬럼을 추가하고 컬럼 타입을 수정하지만 기존 컬럼이나 테이블을 절대 삭제하지 않습니다. 실행 중인 애플리케이션에서도 안전하게 실행할 수 있습니다.
// 일반적으로 initBeforeStartServer 콜백에서 호출:
app.run(async () => {
if (process.env.DB_SYNC === 'true') {
await modelsSync();
}
});alter: { drop: false } 플래그는 모델 정의에서 제거된 컬럼이 데이터베이스에 그대로 남아 있도록 보장하여 우발적인 데이터 손실을 방지합니다.
healthCheck()
import { healthCheck } from '@asapjs/sequelize';
const healthCheck = async (): Promise<true>1초 타임아웃으로 SELECT 1을 실행하여 데이터베이스 연결이 살아있는지 확인합니다. 연결이 초기화되지 않았거나 쿼리가 타임아웃 내에 완료되지 않으면 Error를 던집니다.
오케스트레이션 레이어(Kubernetes, 로드 밸런서 등)에 데이터베이스 상태를 노출하기 위한 헬스 체크 라우트에서 사용하세요.
import { healthCheck } from '@asapjs/sequelize';
import { Get } from '@asapjs/router';
import { RouterController, ExecuteArgs } from '@asapjs/router';
export default class HealthController extends RouterController {
public basePath = '/health';
public tag = 'Health';
constructor() {
super();
this.registerRoutes();
}
@Get('/', { title: 'Health check', auth: false })
async check({}: ExecuteArgs) {
await healthCheck(); // 비정상이면 예외 발생
return { status: 'ok' };
}
}Repository
Repository는 Sequelize의 findAll과 findOne을 자동 페이지네이션 및 DTO 인식 쿼리 빌딩으로 감싸는 기본 클래스입니다.
import { Repository } from '@asapjs/sequelize';클래스 정의
class Repository {
public repository: {
findAll<T extends Model>(model: ModelCtor<T>, args: IArgs<T>): Promise<FindAllResponse<T>>;
findOne<T extends Model>(model: ModelCtor<T>, args: IArgs<T>): Promise<T | null>;
};
}Application 레이어 클래스에서 Repository를 상속하면 this.repository.findAll과 this.repository.findOne에 접근할 수 있습니다.
IArgs
IArgs<T>는 Sequelize의 FindOptions<T>를 세 가지 추가 필드로 확장합니다:
interface IArgs<T extends Model> extends FindOptions<T> {
exportTo: typeof ExtendableDto; // 필수
user?: any; // 선택: DTO의 middleware()에 전달
paging?: { page: number; limit: number }; // 선택: 페이지네이션 활성화
}| 필드 | 타입 | 필수 여부 | 설명 |
|---|---|---|---|
exportTo | typeof ExtendableDto | 예 | dto.middleware(undefined, user)를 통해 필드 가시성 규칙을 적용하는 데 사용되는 DTO 클래스. 쿼리에 포함할 컬럼을 결정합니다. |
user | any | 아니오 | 인증된 사용자 객체. 사용자별 필드 필터링 규칙을 적용할 수 있도록 DTO의 middleware() 메서드에 전달됩니다. |
paging | { page: number; limit: number } | 아니오 | 제공되면 findAndCountAll을 활성화하고 페이지네이션된 응답 엔벨로프를 반환합니다. page는 0 기반입니다. |
표준 Sequelize FindOptions 필드(where, include, order, attributes 등)도 모두 허용되며 DTO에서 파생된 쿼리 옵션과 병합됩니다.
findAll()
repository.findAll<T>(model: ModelCtor<T>, args: IArgs<T>): Promise<T[] | PaginatedResponse<T>>paging없을 때:model.findAll(query)를 실행하고 일반 배열을 반환합니다.paging있을 때:paging.page와paging.limit에서 계산된limit과offset으로model.findAndCountAll(query)를 실행하고 페이지네이션된 엔벨로프를 반환합니다.
페이지네이션 응답 형태
paging이 제공되면 findAll은 다음 형태의 객체를 반환합니다:
{
data: T[]; // 현재 페이지의 모델 인스턴스 배열
page: number; // 현재 페이지 (0 기반)
page_size: number; // 페이지당 항목 수 (요청된 limit)
max_page: number; // 마지막 페이지 인덱스: Math.ceil(total / limit) - 1
has_prev: boolean; // page > 0이면 true
has_next: boolean; // max_page > page이면 true
total_elements: number; // 전체 페이지에 걸친 총 레코드 수
}findOne()
repository.findOne<T>(model: ModelCtor<T>, args: IArgs<T>): Promise<T | null>model.findOne(query)를 실행하고 첫 번째 매칭 레코드를 반환하거나, 없으면 null을 반환합니다.
예제
다음 Application 클래스는 Repository를 사용하여 페이지네이션을 지원하며 사용자를 쿼리합니다.
// src/user/application/UserApplication.ts
import { Repository } from '@asapjs/sequelize';
import UsersTable from '../domain/entity/UsersTable';
import UserInfoDto from '../dto/UserInfoDto';
export default class UserApplication extends Repository {
async getUsers(paging: { page: number; limit: number }, user: any) {
// paging이 제공되어 PaginatedResponse 엔벨로프를 반환
return await this.repository.findAll(UsersTable, {
exportTo: UserInfoDto,
user,
paging,
order: [['created_at', 'DESC']],
});
}
async getUserById(id: number, user: any) {
// 단일 UsersTable 인스턴스 또는 null 반환
return await this.repository.findOne(UsersTable, {
exportTo: UserInfoDto,
user,
where: { id },
});
}
}컨트롤러에서 ExecuteArgs의 paging을 직접 전달합니다:
// src/user/controller/UserController.ts
import { RouterController, Get, ExecuteArgs } from '@asapjs/router';
import { PaginationQueryDto } from '@asapjs/sequelize';
import UserApplication from '../application/UserApplication';
import UserInfoDto from '../dto/UserInfoDto';
import { TypeIs } from '@asapjs/sequelize';
export default class UserController extends RouterController {
public tag = 'User';
public basePath = '/users';
private userService: UserApplication;
constructor() {
super();
this.registerRoutes();
this.userService = new UserApplication();
}
@Get('/', {
title: 'List users',
query: PaginationQueryDto,
response: TypeIs.PAGING(UserInfoDto),
auth: true,
})
async getUsers({ paging, user }: ExecuteArgs) {
return await this.userService.getUsers(paging, user);
}
}