ERP-node/backend-node/src/utils/queryBuilder.ts

288 lines
7.0 KiB
TypeScript

/**
* SQL 쿼리 빌더 유틸리티
*
* Raw Query 방식에서 안전하고 효율적인 쿼리 생성을 위한 헬퍼
*/
export interface SelectOptions {
columns?: string[];
where?: Record<string, any>;
joins?: JoinClause[];
orderBy?: string;
limit?: number;
offset?: number;
groupBy?: string[];
having?: Record<string, any>;
}
export interface JoinClause {
type: 'INNER' | 'LEFT' | 'RIGHT' | 'FULL';
table: string;
on: string;
}
export interface InsertOptions {
returning?: string[];
onConflict?: {
columns: string[];
action: 'DO NOTHING' | 'DO UPDATE';
updateSet?: string[];
};
}
export interface UpdateOptions {
returning?: string[];
}
export interface QueryResult {
query: string;
params: any[];
}
export class QueryBuilder {
/**
* SELECT 쿼리 생성
*/
static select(table: string, options: SelectOptions = {}): QueryResult {
const {
columns = ['*'],
where = {},
joins = [],
orderBy,
limit,
offset,
groupBy = [],
having = {},
} = options;
let query = `SELECT ${columns.join(', ')} FROM ${table}`;
const params: any[] = [];
let paramIndex = 1;
// JOIN 절 추가
for (const join of joins) {
query += ` ${join.type} JOIN ${join.table} ON ${join.on}`;
}
// WHERE 절 추가
const whereConditions = Object.keys(where);
if (whereConditions.length > 0) {
const whereClause = whereConditions
.map((key) => {
params.push(where[key]);
return `${key} = $${paramIndex++}`;
})
.join(' AND ');
query += ` WHERE ${whereClause}`;
}
// GROUP BY 절 추가
if (groupBy.length > 0) {
query += ` GROUP BY ${groupBy.join(', ')}`;
}
// HAVING 절 추가
const havingConditions = Object.keys(having);
if (havingConditions.length > 0) {
const havingClause = havingConditions
.map((key) => {
params.push(having[key]);
return `${key} = $${paramIndex++}`;
})
.join(' AND ');
query += ` HAVING ${havingClause}`;
}
// ORDER BY 절 추가
if (orderBy) {
query += ` ORDER BY ${orderBy}`;
}
// LIMIT 절 추가
if (limit !== undefined) {
params.push(limit);
query += ` LIMIT $${paramIndex++}`;
}
// OFFSET 절 추가
if (offset !== undefined) {
params.push(offset);
query += ` OFFSET $${paramIndex++}`;
}
return { query, params };
}
/**
* INSERT 쿼리 생성
*/
static insert(
table: string,
data: Record<string, any>,
options: InsertOptions = {}
): QueryResult {
const { returning = [], onConflict } = options;
const columns = Object.keys(data);
const values = Object.values(data);
const placeholders = values.map((_, index) => `$${index + 1}`).join(', ');
let query = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`;
// ON CONFLICT 절 추가
if (onConflict) {
query += ` ON CONFLICT (${onConflict.columns.join(', ')})`;
if (onConflict.action === 'DO NOTHING') {
query += ' DO NOTHING';
} else if (onConflict.action === 'DO UPDATE' && onConflict.updateSet) {
const updateSet = onConflict.updateSet
.map(col => `${col} = EXCLUDED.${col}`)
.join(', ');
query += ` DO UPDATE SET ${updateSet}`;
}
}
// RETURNING 절 추가
if (returning.length > 0) {
query += ` RETURNING ${returning.join(', ')}`;
}
return { query, params: values };
}
/**
* UPDATE 쿼리 생성
*/
static update(
table: string,
data: Record<string, any>,
where: Record<string, any>,
options: UpdateOptions = {}
): QueryResult {
const { returning = [] } = options;
const dataKeys = Object.keys(data);
const dataValues = Object.values(data);
const whereKeys = Object.keys(where);
const whereValues = Object.values(where);
let paramIndex = 1;
// SET 절 생성
const setClause = dataKeys
.map((key) => `${key} = $${paramIndex++}`)
.join(', ');
// WHERE 절 생성
const whereClause = whereKeys
.map((key) => `${key} = $${paramIndex++}`)
.join(' AND ');
let query = `UPDATE ${table} SET ${setClause} WHERE ${whereClause}`;
// RETURNING 절 추가
if (returning.length > 0) {
query += ` RETURNING ${returning.join(', ')}`;
}
const params = [...dataValues, ...whereValues];
return { query, params };
}
/**
* DELETE 쿼리 생성
*/
static delete(table: string, where: Record<string, any>): QueryResult {
const whereKeys = Object.keys(where);
const whereValues = Object.values(where);
const whereClause = whereKeys
.map((key, index) => `${key} = $${index + 1}`)
.join(' AND ');
const query = `DELETE FROM ${table} WHERE ${whereClause}`;
return { query, params: whereValues };
}
/**
* COUNT 쿼리 생성
*/
static count(table: string, where: Record<string, any> = {}): QueryResult {
const whereKeys = Object.keys(where);
const whereValues = Object.values(where);
let query = `SELECT COUNT(*) as count FROM ${table}`;
if (whereKeys.length > 0) {
const whereClause = whereKeys
.map((key, index) => `${key} = $${index + 1}`)
.join(' AND ');
query += ` WHERE ${whereClause}`;
}
return { query, params: whereValues };
}
/**
* EXISTS 쿼리 생성
*/
static exists(table: string, where: Record<string, any>): QueryResult {
const whereKeys = Object.keys(where);
const whereValues = Object.values(where);
const whereClause = whereKeys
.map((key, index) => `${key} = $${index + 1}`)
.join(' AND ');
const query = `SELECT EXISTS(SELECT 1 FROM ${table} WHERE ${whereClause}) as exists`;
return { query, params: whereValues };
}
/**
* 동적 WHERE 절 생성 (복잡한 조건)
*/
static buildWhereClause(
conditions: Record<string, any>,
startParamIndex: number = 1
): { clause: string; params: any[]; nextParamIndex: number } {
const keys = Object.keys(conditions);
const params: any[] = [];
let paramIndex = startParamIndex;
if (keys.length === 0) {
return { clause: '', params: [], nextParamIndex: paramIndex };
}
const clause = keys
.map((key) => {
const value = conditions[key];
// 특수 연산자 처리
if (key.includes('>>') || key.includes('->')) {
// JSON 쿼리
params.push(value);
return `${key} = $${paramIndex++}`;
} else if (Array.isArray(value)) {
// IN 절
const placeholders = value.map(() => `$${paramIndex++}`).join(', ');
params.push(...value);
return `${key} IN (${placeholders})`;
} else if (value === null) {
// NULL 체크
return `${key} IS NULL`;
} else {
// 일반 조건
params.push(value);
return `${key} = $${paramIndex++}`;
}
})
.join(' AND ');
return { clause, params, nextParamIndex: paramIndex };
}
}