ERP-node/src/utils/queryBuilder.ts

291 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 };
}
}