288 lines
7.0 KiB
TypeScript
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 };
|
||
|
|
}
|
||
|
|
}
|