node-mysql-query-utils
Version:
A simple MySQL query utils for Node.js
785 lines (702 loc) • 30.7 kB
text/typescript
import {
BuildQueryOptions,
DeleteQueryBuilder,
FieldAlias,
FromQueryBuilder,
GroupByField,
GroupByQueryBuilder,
InsertOptions,
InsertQueryBuilder,
InsertValue,
JoinQueryBuilder,
JoinType,
Limit,
LimitQueryBuilder,
Offset,
OffsetQueryBuilder,
OrderByField,
OrderByQueryBuilder,
QueryFunction,
RawField,
SelectFields,
SelectQueryBuilder,
SetQueryBuilder,
SQL_CONSTRUCTORS,
UpdateOptions,
UpdateQueryBuilder,
UpdateQueryBuilderWithoutSet,
UpdateValue,
WhereCondition,
WhereQueryBuilder
} from '@dto/types';
import { format as _format } from 'mysql2';
// Type Guards
function isRawField<T extends string>(field: any): field is RawField {
return field && typeof field === 'object' && 'raw' in field && typeof field.raw === 'string';
}
function isFieldAlias<T extends string>(field: any): field is FieldAlias<T> {
return field && typeof field === 'object' && !('raw' in field);
}
export class SQLBuilder<ColumnKeys extends string, QueryReturnType = any> {
#queryParts: SQL_CONSTRUCTORS;
queryFn?: QueryFunction;
// message: string = 'Call .buildQuery() or .executeQuery() to get the result';
constructor(queryFn?: QueryFunction) {
this.#queryParts = {
count: { sql: "", params: [] },
select: { sql: "", params: [] },
from: { sql: "", params: [] },
join: { sql: "", params: [] },
where: { sql: "", params: [] },
groupBy: { sql: "", params: [] },
orderBy: { sql: "", params: [] },
limit: { sql: "", params: [] },
offset: { sql: "", params: [] },
insert: { sql: "", params: [] },
update: { sql: "", params: [] },
set: { sql: "", params: [] },
delete: { sql: "", params: [] },
};
this.queryFn = queryFn;
}
private extractTableAndAlias(table: string, alias?: string): [string, string | undefined] {
let tableName: string;
let extractedAlias: string | undefined;
if (alias) {
tableName = table;
} else {
[tableName, extractedAlias] = table.split(' ');
}
return [tableName, alias || extractedAlias];
}
private getCurrentUnixTimestamp(): number {
return Math.floor(Date.now() / 1000); // Unix timestamp
}
private processFields<T extends string>(
fields: SelectFields<T>
): { sql: string; params: any[] } {
const wildcardPattern = /^[a-zA-Z_][a-zA-Z0-9_]*\.\*$/; // Pattern to match table.* (e.g., c.*)
if (typeof fields === 'string') {
if (fields === '*') {
// Case 1: Select all fields
return { sql: 'SELECT *', params: [] };
} else if (wildcardPattern.test(fields)) {
// Case 2: Select all fields from a specific table (e.g., c.*)
return { sql: `SELECT ${fields}`, params: [] };
} else {
// Case 3: Select a single specific field with placeholder
return { sql: 'SELECT ??', params: [fields] };
}
} else if (Array.isArray(fields) && fields.length > 0) {
// Handling an array of fields
const fieldClauses: string[] = []; // Array to accumulate SQL fragments for fields
const fieldParams: any[] = []; // Array to accumulate parameters corresponding to placeholders
fields.forEach((field) => {
if (typeof field === 'string') {
if (wildcardPattern.test(field)) {
// Field is in the format table.*
fieldClauses.push(field);
} else {
// Regular field with placeholder
fieldClauses.push('??');
fieldParams.push(field);
}
} else if (isFieldAlias(field)) {
// Field with an alias (e.g., { email: 'email_address' })
Object.entries(field).forEach(([col, alias]) => {
fieldClauses.push('?? AS ??'); // SQL fragment with two placeholders
fieldParams.push(col, alias); // Corresponding parameters
});
} else if (isRawField(field)) {
// Raw SQL field (e.g., { raw: 'COUNT(*)', alias: 'total' })
let rawField = field.raw;
if (field.alias) {
// Append alias if provided
rawField += ` AS \`${field.alias}\``;
}
fieldClauses.push(rawField); // Insert raw SQL directly without placeholders
if (field.params) {
// Append raw field parameters to the main params array
fieldParams.push(...field.params);
}
}
});
// Combine all field clauses into a single SELECT statement
const sql = `SELECT ${fieldClauses.join(', ')}`;
return { sql, params: fieldParams };
} else if (typeof fields === 'object' && fields !== null) {
// Handling a single field object
if (isRawField(fields)) {
// Single RawField object
let rawField = fields.raw;
if (fields.alias) {
// Append alias if provided
rawField += ` AS \`${fields.alias}\``;
}
const params = fields.params ? [...fields.params] : []; // Clone params if they exist
return { sql: `SELECT ${rawField}`, params };
}
// Single FieldAlias object (e.g., { email: 'email_address' })
const aliases = Object.entries(fields)
.map(([col, alias]) => `?? AS ??`)
.join(', ');
const params = Object.entries(fields).flatMap(([col, alias]) => [col, alias]);
return { sql: `SELECT ${aliases}`, params };
} else {
// Default case: Select all fields
return { sql: 'SELECT *', params: [] };
}
}
private checkEmptyObject(obj: Object) {
return Object.keys(obj || {}).length === 0;
}
private checkTableName(tableName: string, caller?: string) {
if (!tableName) {
throw new Error(this.printPrefixMessage('Table name is required'));
}
}
private throwEmptyObjectError(obj: Object, message?: string) {
if (this.checkEmptyObject(obj)) {
throw new Error(message || 'Object cannot be empty');
}
}
private printPrefixMessage(message: string) {
return `[SQLBuilder] :: ${message}`;
}
/**
* Validates that increment and decrement are not both provided for the same field
*/
private validateIncrementDecrement(values: UpdateValue<ColumnKeys>, context: string = 'Values'): void {
Object.entries(values).forEach(([key, value]) => {
if (typeof value === 'object' && value !== null) {
if ('increment' in value && 'decrement' in value) {
throw new Error(`${context} :: Both increment and decrement provided for field ${key}`);
}
}
});
}
/**
* Builds SQL clauses and parameters for update operations, handling increment/decrement values
* @returns Object with clauses array and params array
*/
private buildUpdateClausesAndParams(
values: UpdateValue<ColumnKeys>
): { clauses: string[]; params: any[] } {
const clauses: string[] = [];
const params: any[] = [];
Object.entries(values).forEach(([key, value]) => {
if (typeof value === 'object' && value !== null) {
if ('increment' in value) {
clauses.push(`?? = ?? + ?`);
params.push(key, key, value.increment);
} else if ('decrement' in value) {
clauses.push(`?? = ?? - ?`);
params.push(key, key, value.decrement);
} else {
clauses.push(`?? = ?`);
params.push(key, value);
}
} else {
clauses.push(`?? = ?`);
params.push(key, value);
}
});
return { clauses, params };
}
// private uniqueFields<T extends string>(fields: (T | FieldAlias<T>)[]) {
// const seen = new Map<string, T | FieldAlias<T>>();
// fields.forEach(field => {
// if (typeof field === 'string') {
// seen.set(field, field);
// } else if (typeof field === 'object') {
// const key = Object.keys(field)[0];
// const value = field[key as T];
// const fieldString = `${key}:${value}`;
// if (!seen.has(fieldString)) {
// seen.set(fieldString, field);
// }
// }
// });
// return Array.from(seen.values());
// }
private uniqueFields<T extends string>(
fields: Array<T | FieldAlias<T> | RawField>
): Array<T | FieldAlias<T> | RawField> {
const seen = new Map<string, T | FieldAlias<T> | RawField>();
fields.forEach((field) => {
if (typeof field === 'string') {
// Handle string fields
if (!seen.has(field)) {
seen.set(field, field);
}
} else if (isRawField<T>(field)) {
// Handle RawField
const key = field.alias ? `${field.raw} AS ${field.alias}` : field.raw;
if (!seen.has(key)) {
seen.set(key, field);
}
} else if (isFieldAlias<T>(field)) {
// Handle FieldAlias
const key = Object.keys(field)[0];
const alias = field[key as T];
const fieldString = alias ? `${key}:${alias}` : key;
if (!seen.has(fieldString)) {
seen.set(fieldString, field);
}
}
});
return Array.from(seen.values());
}
private buildWhereClause(conditions: WhereCondition<ColumnKeys>, parentOperator: string = 'AND'): { clause: string, params: any[] } {
const processConditions = (conditions: WhereCondition<ColumnKeys>, parentOperator: string = 'AND'): { clause: string, params: any[] } => {
const clauses: string[] = [];
const localParams: any[] = [];
for (const key in conditions) {
const value = (conditions as any)[key];
switch (key) {
case 'AND':
case 'OR':
if (!Array.isArray(value) || value.length === 0) {
throw new Error(this.printPrefixMessage(`processConditions :: ${key} :: condition must be a non-empty array`));
}
const nestedConditions = value as WhereCondition<ColumnKeys>[];
const nestedClauses = nestedConditions.map(nestedCondition => processConditions(nestedCondition, key));
const nestedClauseStrings = nestedClauses.map(nestedResult => `(${nestedResult.clause})`);
clauses.push(nestedClauseStrings.join(` ${key} `));
nestedClauses.forEach(nestedResult => localParams.push(...nestedResult.params));
break;
case 'RAW':
// Handle RAW SQL condition
const isValidRaw = typeof value === 'object' && value !== null && 'sql' in value && typeof value.sql === 'string' && value.sql.length > 0;
if (!isValidRaw) {
throw new Error(this.printPrefixMessage('processConditions :: RAW :: raw (sql) must be a string'));
}
clauses.push(value.sql);
if (value.params && Array.isArray(value.params)) {
localParams.push(...value.params);
}
break;
default:
const sanitizedKey = key.replace(/[^a-zA-Z0-9_.]/g, '');
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
for (const operator in value) {
// Handle IN
if (operator === 'IN' && Array.isArray(value[operator])) {
if (value[operator].length === 0) {
throw new Error(this.printPrefixMessage(`processConditions :: IN :: condition must be a non-empty array`));
}
clauses.push(`${sanitizedKey} IN (${value[operator].map(() => '?').join(', ')})`);
localParams.push(...value[operator]);
// Handle NOT_IN
} else if (operator === 'NOT_IN' && Array.isArray(value[operator])) {
if (value[operator].length === 0) {
throw new Error(this.printPrefixMessage(`processConditions :: NOT_IN :: condition must be a non-empty array`));
}
clauses.push(`${sanitizedKey} NOT IN (${value[operator].map(() => '?').join(', ')})`);
localParams.push(...value[operator]);
// Handle BETWEEN
} else if (operator === 'BETWEEN' && Array.isArray(value[operator])) {
if (value[operator].length !== 2) {
throw new Error(this.printPrefixMessage(`processConditions :: BETWEEN :: condition must be an array with exactly 2 elements`));
}
clauses.push(`${sanitizedKey} BETWEEN ? AND ?`);
localParams.push(value[operator][0], value[operator][1]);
// Handle NOT_BETWEEN
} else if (operator === 'NOT_BETWEEN' && Array.isArray(value[operator])) {
if (value[operator].length !== 2) {
throw new Error(this.printPrefixMessage(`processConditions :: NOT_BETWEEN :: condition must be an array with exactly 2 elements`));
}
clauses.push(`${sanitizedKey} NOT BETWEEN ? AND ?`);
localParams.push(value[operator][0], value[operator][1]);
// Handle =, !=, <, <=, >, >=
} else if (['=', '!=', '<', '<=', '>', '>='].includes(operator)) {
clauses.push(`${sanitizedKey} ${operator} ?`);
localParams.push(value[operator]);
// Handle LIKE, NOT LIKE
} else if (operator === 'LIKE' || operator === 'NOT_LIKE') {
const patternType = value[operator];
const clauseString = `${sanitizedKey} ${operator === 'NOT_LIKE' ? 'NOT LIKE' : 'LIKE'} ?`;
if (typeof patternType === 'string') {
// Direct string pattern
clauses.push(clauseString);
localParams.push(patternType);
} else if (typeof patternType === 'object' && patternType !== null) {
// Validation step to ensure only one of contains, startsWith, or endsWith is present
const patternKeys = ['contains', 'startsWith', 'endsWith'];
const presentKeys = patternKeys.filter(key => key in patternType);
if (presentKeys.length > 1) {
throw new Error(this.printPrefixMessage(`processConditions :: ${operator} :: Only one of 'contains', 'startsWith', or 'endsWith' can be provided`));
}
switch (true) {
case !!patternType.contains:
clauses.push(clauseString);
localParams.push(`%${patternType.contains}%`);
break;
case !!patternType.startsWith:
clauses.push(clauseString);
localParams.push(`${patternType.startsWith}%`);
break;
case !!patternType.endsWith:
clauses.push(clauseString);
localParams.push(`%${patternType.endsWith}`);
break;
default:
throw new Error(this.printPrefixMessage(`processConditions :: ${operator} :: Invalid pattern type`));
}
}
// Handle REGEXP
} else if (operator === "REGEXP") {
if (typeof value[operator] !== 'string' || value[operator].length === 0) {
throw new Error(this.printPrefixMessage(`processConditions :: ${operator} :: condition must be a non empty string`));
}
clauses.push(`${sanitizedKey} REGEXP ?`);
localParams.push(value[operator]);
// Handle IS_NULL, IS_NOT_NULL
} else if (['IS_NULL', 'IS_NOT_NULL'].includes(operator)) {
if (value[operator] !== true) {
throw new Error(this.printPrefixMessage(`processConditions :: ${operator} :: condition must be true`));
}
clauses.push(`${sanitizedKey} ${operator.replace(/_/g, ' ')}`);
} else {
throw new Error(this.printPrefixMessage(`processConditions :: Unsupported operator: ${operator}`));
}
}
} else {
// Determine if the value is an array for IN condition
// e.g. { user_id: [1, 2, 3] } => user_id IN (1, 2, 3)
if (Array.isArray(value)) {
if (value.length === 0) {
throw new Error(this.printPrefixMessage(`processConditions :: ${key} :: condition must be a non-empty array`));
}
clauses.push(`${sanitizedKey} IN (${value.map(() => '?').join(', ')})`);
localParams.push(...value);
} else {
// Normal equal condition
// e.g. { user_id: 1 } => user_id = 1
clauses.push(`${sanitizedKey} = ?`);
localParams.push(value);
}
}
}
}
return { clause: clauses.join(` ${parentOperator} `), params: localParams };
};
return processConditions(conditions, parentOperator);
}
// For simple count query e.g. SELECT COUNT(*) FROM table
count(field: ColumnKeys | (string & {}) = '*', alias?: string): SelectQueryBuilder<ColumnKeys, QueryReturnType> {
const countClause = `COUNT(${field === '*' ? '*' : '??'})`;
this.#queryParts.select.sql = `SELECT ${alias ? `${countClause} AS ??` : countClause}`;
this.#queryParts.select.params = field === '*' ? [] : [field];
if (alias) {
this.#queryParts.select.params.push(alias);
}
return this;
}
max(field: ColumnKeys, alias?: string): SelectQueryBuilder<ColumnKeys, QueryReturnType> {
this.#queryParts.select.sql = `SELECT MAX(??)${alias ? ` AS ??` : ''}`;
this.#queryParts.select.params = alias ? [field, alias] : [field];
return this;
}
min(field: ColumnKeys, alias?: string): SelectQueryBuilder<ColumnKeys, QueryReturnType> {
this.#queryParts.select.sql = `SELECT MIN(??)${alias ? ` AS ??` : ''}`;
this.#queryParts.select.params = alias ? [field, alias] : [field];
return this;
}
avg(field: ColumnKeys, alias?: string): SelectQueryBuilder<ColumnKeys, QueryReturnType> {
this.#queryParts.select.sql = `SELECT AVG(??)${alias ? ` AS ??` : ''}`;
this.#queryParts.select.params = alias ? [field, alias] : [field];
return this;
}
sum(field: ColumnKeys, alias?: string): SelectQueryBuilder<ColumnKeys, QueryReturnType> {
this.#queryParts.select.sql = `SELECT SUM(??)${alias ? ` AS ??` : ''}`;
this.#queryParts.select.params = alias ? [field, alias] : [field];
return this;
}
select(fields: SelectFields<ColumnKeys> = "*"): SelectQueryBuilder<ColumnKeys, QueryReturnType> {
const _fields = Array.isArray(fields) ? this.uniqueFields<ColumnKeys>(fields) : fields;
const { sql, params } = this.processFields<ColumnKeys>(_fields as SelectFields<ColumnKeys>);
this.#queryParts.select.sql = sql;
this.#queryParts.select.params = params;
return this
}
from(table: string, alias?: string): FromQueryBuilder<ColumnKeys, QueryReturnType> {
this.checkTableName(table, 'from');
const [tableName, extractedAlias] = this.extractTableAndAlias(table, alias);
this.#queryParts.from.sql = `FROM ??`;
this.#queryParts.from.params = [tableName];
if (extractedAlias) {
this.#queryParts.from.sql += ` AS ??`;
this.#queryParts.from.params.push(alias || extractedAlias);
}
return this;
}
// Overload signatures for the join method
join(joinType: JoinType, table: string, onCondition: string): JoinQueryBuilder<ColumnKeys, QueryReturnType>;
join(joinType: JoinType, table: string, alias: string, onCondition: string): JoinQueryBuilder<ColumnKeys, QueryReturnType>;
// Implementation of the join method
join(joinType: JoinType, table: string, aliasOrOnCondition: string, onCondition?: string): JoinQueryBuilder<ColumnKeys, QueryReturnType> {
let alias: string | undefined;
let actualOnCondition: string;
if (onCondition) {
alias = aliasOrOnCondition;
actualOnCondition = onCondition;
} else {
actualOnCondition = aliasOrOnCondition;
}
const [tableName, extractedAlias] = this.extractTableAndAlias(table, alias);
const joinClause = `${joinType.toUpperCase()} JOIN ??${extractedAlias ? ' AS ??' : ''} ON ${actualOnCondition}`;
const joinParams = extractedAlias ? [tableName, extractedAlias] : [tableName];
// Append the new join clause and parameters
this.#queryParts.join.sql += (this.#queryParts.join.sql ? ' ' : '') + joinClause;
this.#queryParts.join.params.push(...joinParams);
return this;
}
where(conditions: WhereCondition<ColumnKeys>): WhereQueryBuilder<ColumnKeys, QueryReturnType> {
const { clause, params } = this.buildWhereClause(conditions);
this.#queryParts.where.sql = clause ? `WHERE ${clause}` : '';
this.#queryParts.where.params = params;
return this;
}
groupBy(fields: GroupByField<ColumnKeys>): GroupByQueryBuilder<QueryReturnType> {
if (typeof fields === 'string') {
this.#queryParts.groupBy.sql = 'GROUP BY ??';
this.#queryParts.groupBy.params = [fields];
} else if (Array.isArray(fields) && fields.length > 0) {
this.#queryParts.groupBy.sql = 'GROUP BY ' + fields.map(() => '??').join(', ');
this.#queryParts.groupBy.params = fields;
}
return this;
}
orderBy(fields: OrderByField<ColumnKeys>[]): OrderByQueryBuilder<QueryReturnType> {
if (!Array.isArray(fields) || fields.length === 0) return this;
// this.#queryParts.orderBy.sql = `ORDER BY ${fields.map(({ field, direction }) => `?? ${direction || 'ASC'}`).join(', ')}`;
// this.#queryParts.orderBy.params = fields.map(({ field }) => field);
const orderItems: string[] = [];
const params: any[] = [];
fields.forEach((field) => {
if (field.raw) {
// Raw SQL expression - use raw value
orderItems.push(`${field.raw} ${field.direction || 'ASC'}`);
} else {
// Regular column name - use placeholder
orderItems.push(`?? ${field.direction || 'ASC'}`);
params.push(field.field);
}
});
this.#queryParts.orderBy.sql = `ORDER BY ${orderItems.join(', ')}`;
this.#queryParts.orderBy.params = params;
return this;
}
limit(limit?: Limit): LimitQueryBuilder<QueryReturnType> {
if (limit === undefined || limit === null || Number.isNaN(limit) || limit < 0) return this;
this.#queryParts.limit.sql = `LIMIT ?`;
this.#queryParts.limit.params = [limit];
return this;
}
offset(offset?: Offset): OffsetQueryBuilder<QueryReturnType> {
if (offset === undefined
|| offset === null
|| Number.isNaN(offset)
|| offset < 0
|| this.#queryParts.limit.params.length === 0
) return this;
this.#queryParts.offset.sql = `OFFSET ?`;
this.#queryParts.offset.params = [offset];
return this;
}
update(table: string): UpdateQueryBuilder<ColumnKeys, QueryReturnType>;
update(table: string, values: UpdateValue<ColumnKeys>): UpdateQueryBuilderWithoutSet<ColumnKeys, QueryReturnType>;
update(table: string, values: UpdateValue<ColumnKeys>, options?: UpdateOptions): UpdateQueryBuilderWithoutSet<ColumnKeys, QueryReturnType>;
update(table: string, values?: UpdateValue<ColumnKeys>, options?: UpdateOptions): UpdateQueryBuilder<ColumnKeys, QueryReturnType> | UpdateQueryBuilderWithoutSet<ColumnKeys, QueryReturnType> {
this.checkTableName(table, 'update');
const {
enableTimestamps = false,
utimeField = 'utime',
utimeValue = this.getCurrentUnixTimestamp,
primaryKey
} = options || {};
if (enableTimestamps) {
const currentTime = typeof utimeValue === 'function' ? utimeValue() : utimeValue;
if (values) {
(values as Record<string, any>)[utimeField] = currentTime;
}
}
// Avoid updating the primary key
if ((primaryKey && values) && (values as Record<string, any>)?.[primaryKey]) {
delete (values as Record<string, any>)[primaryKey];
}
this.#queryParts.update.sql = `UPDATE ??`;
this.#queryParts.update.params = [table];
if (values) {
this.throwEmptyObjectError(values as Object, this.printPrefixMessage('Update :: Data cannot be empty'));
this.set(values);
return this as UpdateQueryBuilderWithoutSet<ColumnKeys, QueryReturnType>;
}
return this;
}
set(values: UpdateValue<ColumnKeys>): SetQueryBuilder<ColumnKeys, QueryReturnType> {
this.throwEmptyObjectError(values, this.printPrefixMessage('Set :: Values cannot be empty'));
this.validateIncrementDecrement(values, 'Set');
const { clauses, params } = this.buildUpdateClausesAndParams(values);
this.#queryParts.set.sql = `SET ${clauses.join(', ')}`;
this.#queryParts.set.params = params;
return this;
}
insert(table: string, values: InsertValue<ColumnKeys>, options?: InsertOptions<ColumnKeys>): InsertQueryBuilder<QueryReturnType> {
this.checkTableName(table, 'insert');
const {
enableTimestamps = false,
ctimeField = 'ctime',
utimeField = 'utime',
ctimeValue = this.getCurrentUnixTimestamp,
utimeValue = this.getCurrentUnixTimestamp,
} = options || {};
const isMultipleInsert = Array.isArray(values);
const rows = isMultipleInsert ? values : [values];
if (isMultipleInsert && rows.length === 0) {
throw new Error(this.printPrefixMessage('Insert :: Values cannot be empty'));
}
if (enableTimestamps) {
if (typeof ctimeValue !== 'function') {
throw new Error(this.printPrefixMessage('ctimeValue must be a function'));
}
if (typeof utimeValue !== 'function') {
throw new Error(this.printPrefixMessage('utimeValue must be a function'));
}
rows.forEach((row) => {
// if not provided, set the current time
if (!row.hasOwnProperty(ctimeField)) {
(row as Record<string, any>)[ctimeField] = ctimeValue();
}
// if not provided, set the current time
if (!row.hasOwnProperty(utimeField)) {
(row as Record<string, any>)[utimeField] = utimeValue();
}
});
}
const columns = Object.keys(rows[0]); // e.g. ['name', 'email', 'password']
const placeholders = columns.map(() => '?').join(', '); // e.g. '?, ?, ?'
const columnPlaceholders = columns.map(() => '??').join(', '); // e.g. 'name, email, password'
const valueParams = rows.flatMap(row => Object.values(row)); // e.g. ['doe', 'doe@gmail.com', 'password']
const columnParams = columns;
const valuesPlaceholders = rows.map(() => `(${placeholders})`).join(', '); // e.g. '(?, ?, ?), (?, ?, ?)'
let insertClause = `INSERT ${options?.insertIgnore ? 'IGNORE ' : ''}INTO ?? (${columnPlaceholders}) VALUES ${valuesPlaceholders}`;
if (options?.onDuplicateKeyUpdate) {
this.validateIncrementDecrement(options.onDuplicateKeyUpdate, 'onDuplicateKeyUpdate');
const { clauses, params } = this.buildUpdateClausesAndParams(options.onDuplicateKeyUpdate);
insertClause += ` ON DUPLICATE KEY UPDATE ${clauses.join(', ')}`;
valueParams.push(...params);
}
this.#queryParts.insert.sql = insertClause;
this.#queryParts.insert.params = [table, ...columnParams, ...valueParams];
return this;
}
deleteFrom(table: string): DeleteQueryBuilder<ColumnKeys, QueryReturnType> {
this.checkTableName(table, 'delete');
this.#queryParts.delete.sql = `DELETE FROM ??`;
this.#queryParts.delete.params = [table];
return this;
}
buildQuery(options?: BuildQueryOptions): {
sql: string;
params: any[];
[Symbol.iterator](): Iterator<any>
} {
let sql = '';
let params: any[] = [];
// Handle COUNT clause
if (this.#queryParts.count.sql) {
sql += this.#queryParts.count.sql;
params.push(...this.#queryParts.count.params);
}
// Handle SELECT clause
if (this.#queryParts.select.sql) {
sql += this.#queryParts.select.sql;
params.push(...this.#queryParts.select.params);
}
// Handle UPDATE clause
if (this.#queryParts.update.sql) {
sql += this.#queryParts.update.sql;
params.push(...this.#queryParts.update.params);
}
// Handle INSERT clause
if (this.#queryParts.insert.sql) {
sql += this.#queryParts.insert.sql;
params.push(...this.#queryParts.insert.params);
}
// Handle DELETE clause
if (this.#queryParts.delete.sql) {
sql += this.#queryParts.delete.sql;
params.push(...this.#queryParts.delete.params);
}
// Handle SET clause
if (this.#queryParts.set.sql) {
sql += ` ${this.#queryParts.set.sql}`;
params.push(...this.#queryParts.set.params);
}
// Handle FROM clause
if (this.#queryParts.from.sql) {
sql += ` ${this.#queryParts.from.sql}`;
params.push(...this.#queryParts.from.params);
}
// Handle JOIN clauses
if (this.#queryParts.join.sql) {
sql += ` ${this.#queryParts.join.sql}`;
params.push(...this.#queryParts.join.params);
}
// Handle WHERE clause
if (this.#queryParts.where.sql) {
sql += ` ${this.#queryParts.where.sql}`;
params.push(...this.#queryParts.where.params);
}
// Handle GROUP BY clause
if (this.#queryParts.groupBy.sql) {
sql += ` ${this.#queryParts.groupBy.sql}`;
params.push(...this.#queryParts.groupBy.params);
}
// Handle ORDER BY clause
if (this.#queryParts.orderBy.sql) {
sql += ` ${this.#queryParts.orderBy.sql}`;
params.push(...this.#queryParts.orderBy.params);
}
// Handle LIMIT clause
if (this.#queryParts.limit.sql) {
sql += ` ${this.#queryParts.limit.sql}`;
params.push(...this.#queryParts.limit.params);
}
// Handle OFFSET clause
if (this.#queryParts.offset.sql) {
sql += ` ${this.#queryParts.offset.sql}`;
params.push(...this.#queryParts.offset.params);
}
// Format SQL if required
if (options?.format) {
sql = _format(sql, params);
params = [];
}
return {
sql,
params: params,
// For allowing array destructuring
[Symbol.iterator](): Iterator<any> {
let index = 0;
const values = [sql, params];
return {
next(): IteratorResult<any> {
if (index < values.length) {
return { value: values[index++], done: false };
} else {
return { value: undefined, done: true };
}
}
};
}
};
}
executeQuery<T = QueryReturnType>(): Promise<T> {
if (!this.queryFn) throw new Error(this.printPrefixMessage('executeQuery :: Query function is not defined / provided in the constructor'));
const [sql, params] = this.buildQuery();
return this.queryFn<T>(sql, params);
}
}