@js-ak/db-manager
Version:
883 lines (882 loc) • 38.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryHandler = void 0;
const Helpers = __importStar(require("../helpers/index.js"));
const SharedHelpers = __importStar(require("../../../shared-helpers/index.js"));
const queries_js_1 = require("../model/queries.js");
/**
* Class to handle SQL query construction.
*/
class QueryHandler {
#groupBy = "";
#join = [];
#subqueryName = "";
#mainHaving = "";
#mainQuery = "";
#mainWhere = "";
#orderBy = "";
#pagination = "";
#returning = "";
#for = "";
#dataSourcePrepared;
#dataSourceRaw;
#valuesOrder = 0;
#values = [];
#with = "";
isSubquery = false;
/**
* Constructs a new QueryHandler instance.
*
* @param options - The configuration options.
* @param [options.groupBy] - Group by clause.
* @param [options.join] - Array of join clauses.
* @param [options.isSubquery] - Whether this is a subquery.
* @param [options.mainHaving] - Having clause.
* @param [options.mainQuery] - Main query string.
* @param [options.mainWhere] - Where clause.
* @param [options.orderBy] - Order by clause.
* @param [options.pagination] - Pagination clause.
* @param [options.returning] - Returning clause.
* @param options.dataSourcePrepared - Prepared data source name.
* @param options.dataSourceRaw - Raw data source name.
* @param [options.values] - Array of values to be used in the query.
* @param [options.valuesOrder] - Initial order for value placeholders.
* @param [options.with] - With clause for common table expressions.
*/
constructor(options) {
if (options.groupBy)
this.#groupBy = options.groupBy;
if (options.join)
this.#join = options.join;
if (options.isSubquery)
this.isSubquery = options.isSubquery;
if (options.mainHaving)
this.#mainHaving = options.mainHaving;
if (options.mainQuery)
this.#mainQuery = options.mainQuery;
if (options.mainWhere)
this.#mainWhere = options.mainWhere;
if (options.orderBy)
this.#orderBy = options.orderBy;
if (options.pagination)
this.#pagination = options.pagination;
if (options.returning)
this.#returning = options.returning;
if (options.values)
this.#values = options.values;
if (options.valuesOrder)
this.#valuesOrder = options.valuesOrder;
if (options.with)
this.#with = options.with;
this.#dataSourceRaw = options.dataSourceRaw;
this.#dataSourcePrepared = options.dataSourcePrepared;
}
/**
* Get the options to clone the current query.
*
* @returns Clonable options.
*/
get optionsToClone() {
return {
dataSourcePrepared: this.#dataSourcePrepared,
dataSourceRaw: this.#dataSourceRaw,
groupBy: this.#groupBy,
join: [...this.#join],
mainHaving: this.#mainHaving,
mainQuery: this.#mainQuery,
mainWhere: this.#mainWhere,
orderBy: this.#orderBy,
pagination: this.#pagination,
returning: this.#returning,
values: structuredClone(this.#values),
valuesOrder: this.#valuesOrder,
with: this.#with,
};
}
/**
* Constructs and returns a SQL query string based on the provided query components.
*
* The method assembles the SQL query by concatenating various parts such as `WITH` clause,
* main query, JOIN clauses, WHERE clause, GROUP BY clause, HAVING clause, ORDER BY clause,
* pagination, RETURNING clause, and FOR clause. It handles the case where the query is a
* subquery by wrapping the query in parentheses and optionally naming the subquery.
*
* @private
*
* @returns The assembled SQL query string. If the query is a subquery, it will be
* enclosed in parentheses and optionally followed by an alias name. Otherwise, the query
* will end with a semicolon.
*/
#compareSql() {
const query = ((this.#with ? this.#with + " " : "")
+ (this.#mainQuery ?? "")
+ (this.#join.length ? " " + this.#join.join(" ") : "")
+ (this.#mainWhere ? " " + this.#mainWhere : "")
+ (this.#groupBy ? " " + this.#groupBy : "")
+ (this.#mainHaving ? " " + this.#mainHaving : "")
+ (this.#orderBy ? " " + this.#orderBy : "")
+ (this.#pagination ? " " + this.#pagination : "")
+ (this.#returning ? " " + this.#returning : "")
+ (this.#for ? " " + this.#for : ""));
return this.isSubquery
? `(${query})${this.#subqueryName ? ` AS ${this.#subqueryName}` : ""}`
: query + ";";
}
/**
* Replaces dollar sign placeholders in the SQL query.
*
* @param text - The text containing placeholders.
*
* @returns The replaced text and growth in placeholders.
*
* @throws {Error} If values are not sequential starting from $1.
*/
#replaceDollarSign(text) {
const regex = /\$(\d+)/g;
const initialCounter = this.#valuesOrder;
const matches = text.match(regex);
if (matches) {
const uniqueNumbers = [...new Set(matches.map((match) => Number(match.slice(1))))].sort((a, b) => a - b);
const minNumber = Math.min(...uniqueNumbers);
const maxNumber = Math.max(...uniqueNumbers);
if (minNumber !== 1 || maxNumber !== uniqueNumbers.length) {
throw new Error("Values are not sequential starting from $1");
}
const replacedText = text.replace(regex, (_, p1) => {
const number = Number(p1);
return `$${number + this.#valuesOrder}`;
});
this.#valuesOrder += uniqueNumbers.length;
const growth = this.#valuesOrder - initialCounter;
return { growth, text: replacedText };
}
return { growth: 0, text };
}
/**
* Processes the provided data with the corresponding values.
*
* @param data - The SQL clause data.
* @param values - The values to be used in the SQL clause.
*
* @returns The processed SQL clause.
*
* @throws {Error} If the number of placeholders doesn't match the values length.
*/
#processDataWithValues(data, values) {
const { growth, text } = this.#replaceDollarSign(data);
if (growth !== values.length) {
throw new Error(`${text} - Invalid values: ${JSON.stringify(values)}`);
}
this.#values.push(...values);
return text;
}
/**
* Constructs and returns an object containing the SQL query string and its associated values.
*
* The method generates the SQL query string by invoking the `#compareSql` method, and returns
* an object that includes the query string and the associated parameter values.
*
* @returns An object containing the SQL query string
* and the array of values to be used as parameters in the query.
*/
compareQuery() {
return { query: this.#compareSql(), values: this.#values };
}
/**
* Adds a raw SQL `FOR` clause to the query.
*
* This method adds the given raw SQL data to the `FOR` clause of the query. If the provided
* string does not start with "FOR", it will prepend "FOR " to the data. The method also
* handles any placeholder values within the data string, processing them accordingly.
*
* @param data - The raw SQL data to be added to the `FOR` clause.
* @param [values] - An optional array of values to replace placeholders in the
* data string.
*
* @returns
*/
rawFor(data, values) {
if (!data)
return;
if (!this.#for) {
const dataLowerCased = data.toLowerCase();
if (dataLowerCased.slice(0, 3) !== "for") {
this.#for = "FOR ";
}
}
const dataPrepared = values?.length
? this.#processDataWithValues(data, values)
: data;
this.#for += ` ${dataPrepared}`;
}
/**
* Sets the main SQL query to a DELETE statement.
*
* This method sets the main part of the query to a `DELETE FROM` statement, targeting the
* specified data source.
*
* @returns
*/
delete() {
this.#mainQuery = `DELETE FROM ${this.#dataSourceRaw}`;
}
/**
* Constructs and sets an SQL INSERT query with optional conflict handling and timestamp updates.
*
* This method builds an SQL INSERT statement based on the provided parameters. It supports batch inserts,
* conflict handling using the `onConflict` option, and automatic updates of timestamp columns if specified.
*
* @param options - Options for constructing the INSERT query.
* @param [options.isUseDefaultValues] - Use default values for missing columns when options.params is an array.
* @param options.params - The parameters for the INSERT operation, which can be a single object or an array of objects.
* @param [options.onConflict] - Optional SQL clause to handle conflicts, typically used to specify `ON CONFLICT DO UPDATE`.
* @param [options.updateColumn] -
* An optional object specifying a column to update with the current timestamp. The `title` is the column name,
* and `type` specifies the format (either `timestamp` or `unix_timestamp`).
*
* @returns
*
* @throws {Error} Throws an error if parameters are invalid or if fields are undefined.
*/
insert(options) {
const v = [];
const headers = new Set();
let insertQuery = "";
if (Array.isArray(options.params)) {
const k = [];
const collectHeaders = (params) => {
for (const p of params) {
const keys = Object.keys(p);
for (const key of keys) {
headers.add(key);
}
}
return headers;
};
collectHeaders(options.params);
if (options.updateColumn) {
headers.add(options.updateColumn.title);
}
const headersArray = Array.from(headers);
for (const p of options.params) {
const keys = [];
const preparedParams = headersArray.reduce((acc, e) => {
const value = p[e];
if (options.updateColumn?.title === e) {
return acc;
}
if (value === undefined) {
if (options.isUseDefaultValues) {
keys.push([e, "DEFAULT"]);
}
else {
throw new Error(`Invalid parameters - ${e} is undefined at ${JSON.stringify(p)} for INSERT INTO ${this.#dataSourceRaw}(${Array.from(headers).join(",")})`);
}
return acc;
}
acc[e] = value;
keys.push([e, undefined]);
return acc;
}, {});
v.push(...Object.values(preparedParams));
if (options.updateColumn) {
keys.push([options.updateColumn.title, (0, queries_js_1.generateTimestampQuery)(options.updateColumn.type)]);
}
k.push(keys);
}
const valuesOrder = this.#valuesOrder;
let idx = valuesOrder;
insertQuery += k.map((e) => e.map((el) => {
if (el[1])
return el[1];
return "$" + (++idx);
})).join("),(");
}
else {
const k = [];
const params = SharedHelpers.clearUndefinedFields(options.params);
Object.keys(params).forEach((e) => { headers.add(e); k.push([e, undefined]); });
v.push(...Object.values(params));
if (!headers.size)
throw new Error(`Invalid params, all fields are undefined - ${Object.keys(options.params).join(", ")}`);
if (options.updateColumn) {
headers.add(options.updateColumn.title);
k.push([options.updateColumn.title, (0, queries_js_1.generateTimestampQuery)(options.updateColumn.type)]);
}
const valuesOrder = this.#valuesOrder;
let idx = valuesOrder;
insertQuery += k.map((e) => {
if (e[1])
return e[1];
idx += 1;
return "$" + (idx);
}).join(",");
}
this.#mainQuery = `INSERT INTO ${this.#dataSourceRaw}(${Array.from(headers).join(",")}) VALUES(${insertQuery})`;
if (options.onConflict)
this.#mainQuery += ` ${options.onConflict}`;
this.#values.push(...v);
this.#valuesOrder += v.length;
}
/**
* Constructs and sets a raw SQL INSERT query with optional value substitution.
*
* This method allows inserting raw SQL data into the specified data source. If values are provided,
* they are substituted into the SQL string using the internal value processing method.
*
* @param data - The raw SQL data string to be inserted.
* @param [values] - Optional array of values to be substituted into the SQL string.
*
* @returns
*/
rawInsert(data, values) {
if (!data)
return;
const dataPrepared = values?.length
? this.#processDataWithValues(data, values)
: data;
this.#mainQuery = `INSERT INTO ${this.#dataSourceRaw} ${dataPrepared}`;
}
/**
* Constructs and sets an SQL UPDATE query with optional conflict handling and timestamp updates.
*
* This method builds an SQL UPDATE statement based on the provided parameters. It supports
* automatic updates of timestamp columns if specified and optional conflict handling.
*
* @param options - Options for constructing the UPDATE query.
* @param options.params - The parameters for the UPDATE operation, which is a single object.
* @param [options.onConflict] - Optional SQL clause to handle conflicts, typically used to specify `ON CONFLICT DO UPDATE`.
* @param [options.updateColumn] -
* An optional object specifying a column to update with the current timestamp. The `title` is the column name,
* and `type` specifies the format (either `timestamp` or `unix_timestamp`).
*
* @returns
*
* @throws {Error} Throws an error if parameters are invalid or if fields are undefined.
*/
update(options) {
const params = SharedHelpers.clearUndefinedFields(options.params);
const k = Object.keys(params);
const v = Object.values(params);
if (!k.length)
throw new Error(`Invalid params, all fields are undefined - ${Object.keys(options.params).join(", ")}`);
const valuesOrder = this.#valuesOrder;
let updateQuery = k.map((e, idx) => `${e} = $${idx + 1 + valuesOrder}`).join(",");
if (options.updateColumn) {
updateQuery += `, ${options.updateColumn.title} = ${(0, queries_js_1.generateTimestampQuery)(options.updateColumn.type)}`;
}
this.#mainQuery = `UPDATE ${this.#dataSourceRaw} SET ${updateQuery}`;
if (options.onConflict)
this.#mainQuery += ` ${options.onConflict}`;
this.#values.push(...v);
this.#valuesOrder += v.length;
}
/**
* Constructs and sets a raw SQL UPDATE query with optional value substitution.
*
* This method allows updating raw SQL data in the specified data source. If values are provided,
* they are substituted into the SQL string using the internal value processing method. The method
* also ensures that the query starts with an `UPDATE` clause if it hasn't been set.
*
* @param data - The raw SQL data string to be updated.
* @param [values] - Optional array of values to be substituted into the SQL string.
*
* @returns
*/
rawUpdate(data, values) {
if (!data)
return;
const dataPrepared = values?.length
? this.#processDataWithValues(data, values)
: data;
if (!this.#mainQuery) {
const dataLowerCased = data.toLowerCase();
if (dataLowerCased.slice(0, 6) !== "UPDATE") {
this.#mainQuery = `UPDATE ${this.#dataSourceRaw} SET`;
}
this.#mainQuery += ` ${dataPrepared}`;
return;
}
this.#mainQuery += `, ${dataPrepared}`;
return;
}
/**
* Constructs and sets an SQL SELECT query.
*
* This method builds an SQL SELECT statement based on the provided column names.
* The `FROM` clause is automatically appended using the internal data source.
*
* @param data - An array of column names to select from the data source.
*
* @returns
*/
select(data) {
const fromClause = this.#dataSourceRaw
? ` FROM ${this.#dataSourceRaw}`
: "";
this.#mainQuery = `SELECT ${data.join(", ")}${fromClause}`;
}
/**
* Sets the source table for the query and optionally processes values for the SQL string.
*
* This method updates the internal data source (`#dataSourceRaw`) and adjusts the current SQL
* query to include the `FROM` clause. It also extracts and prepares the alias for the data source, if present.
*
* @param data - The table name or SQL string specifying the data source.
* @param [values] - Optional array of values to be substituted into the SQL string.
*
* @returns
*/
from(data, values) {
const dataPrepared = values?.length
? this.#processDataWithValues(data, values)
: data;
this.#dataSourceRaw = dataPrepared;
const clauses = this.#mainQuery.split(" FROM ");
const firstClause = clauses[0];
const fromClause = clauses[1];
if (fromClause) {
this.#mainQuery = `${firstClause} FROM ${this.#dataSourceRaw}`;
}
else {
this.#mainQuery = `${this.#mainQuery} FROM ${this.#dataSourceRaw}`;
}
const chunks = dataPrepared
.toLowerCase()
.split(" ")
.filter((e) => e && e !== "as");
const as = chunks[1]?.trim();
if (as) {
this.#dataSourcePrepared = as;
}
else {
this.#dataSourcePrepared = dataPrepared;
}
}
/**
* Appends a raw SQL JOIN clause to the current query with optional value substitution.
*
* This method allows adding any type of JOIN clause to the SQL query. If values are provided,
* they are substituted into the SQL string using the internal value processing method.
*
* @param data - The raw SQL JOIN clause to be appended.
* @param [values] - Optional array of values to be substituted into the SQL string.
*
* @returns
*/
rawJoin(data, values) {
const dataPrepared = values?.length
? this.#processDataWithValues(data, values)
: data;
this.#join.push(dataPrepared);
}
/**
* Appends a RIGHT JOIN clause to the current SQL query.
*
* This method constructs and appends a RIGHT JOIN clause, using the specified table and field names.
* It supports table aliasing and defaults to the main data source if the initial table is not provided.
*
* @param data - The details for constructing the RIGHT JOIN clause.
* @param data.targetTableName - The name of the target table to join with.
* @param [data.targetTableNameAs] - Optional alias for the target table.
* @param data.targetField - The field in the target table to join on.
* @param [data.initialTableName] - Optional name of the initial table, defaults to the main data source.
* @param data.initialField - The field in the initial table to join on.
*
* @returns
*/
rightJoin(data) {
const targetTableName = data.targetTableName + (data.targetTableNameAs ? ` AS ${data.targetTableNameAs}` : "");
this.#join.push(`RIGHT JOIN ${targetTableName} ON ${data.targetTableNameAs || data.targetTableName}.${data.targetField} = ${data.initialTableName || this.#dataSourcePrepared}.${data.initialField}`);
}
/**
* Appends a LEFT JOIN clause to the current SQL query.
*
* This method constructs and appends a LEFT JOIN clause, using the specified table and field names.
* It supports table aliasing and defaults to the main data source if the initial table is not provided.
*
* @param data - The details for constructing the LEFT JOIN clause.
* @param data.targetTableName - The name of the target table to join with.
* @param [data.targetTableNameAs] - Optional alias for the target table.
* @param data.targetField - The field in the target table to join on.
* @param [data.initialTableName] - Optional name of the initial table, defaults to the main data source.
* @param data.initialField - The field in the initial table to join on.
*
* @returns
*/
leftJoin(data) {
const targetTableName = data.targetTableName + (data.targetTableNameAs ? ` AS ${data.targetTableNameAs}` : "");
this.#join.push(`LEFT JOIN ${targetTableName} ON ${data.targetTableNameAs || data.targetTableName}.${data.targetField} = ${data.initialTableName || this.#dataSourcePrepared}.${data.initialField}`);
}
/**
* Appends an INNER JOIN clause to the current SQL query.
*
* This method constructs and appends an INNER JOIN clause, using the specified table and field names.
* It supports table aliasing and defaults to the main data source if the initial table is not provided.
*
* @param data - The details for constructing the INNER JOIN clause.
* @param data.targetTableName - The name of the target table to join with.
* @param [data.targetTableNameAs] - Optional alias for the target table.
* @param data.targetField - The field in the target table to join on.
* @param [data.initialTableName] - Optional name of the initial table, defaults to the main data source.
* @param data.initialField - The field in the initial table to join on.
*
* @returns
*/
innerJoin(data) {
const targetTableName = data.targetTableName + (data.targetTableNameAs ? ` AS ${data.targetTableNameAs}` : "");
this.#join.push(`INNER JOIN ${targetTableName} ON ${data.targetTableNameAs || data.targetTableName}.${data.targetField} = ${data.initialTableName || this.#dataSourcePrepared}.${data.initialField}`);
}
/**
* Appends a FULL OUTER JOIN clause to the current SQL query.
*
* This method constructs and appends a FULL OUTER JOIN clause, using the specified table and field names.
* It supports table aliasing and defaults to the main data source if the initial table is not provided.
*
* @param data - The details for constructing the FULL OUTER JOIN clause.
* @param data.targetTableName - The name of the target table to join with.
* @param [data.targetTableNameAs] - Optional alias for the target table.
* @param data.targetField - The field in the target table to join on.
* @param [data.initialTableName] - Optional name of the initial table, defaults to the main data source.
* @param data.initialField - The field in the initial table to join on.
*
* @returns
*/
fullOuterJoin(data) {
const targetTableName = data.targetTableName + (data.targetTableNameAs ? ` AS ${data.targetTableNameAs}` : "");
this.#join.push(`FULL OUTER JOIN ${targetTableName} ON ${data.targetTableNameAs || data.targetTableName}.${data.targetField} = ${data.initialTableName || this.#dataSourcePrepared}.${data.initialField}`);
}
/**
* Appends a CROSS JOIN clause to the current SQL query.
*
* This method constructs and appends a CROSS JOIN clause, using the specified table and field names.
* It supports table aliasing and defaults to the main data source if the initial table is not provided.
*
* @param data - The details for constructing the CROSS JOIN clause.
* @param data.targetTableName - The name of the target table to join with.
* @param [data.targetTableNameAs] - Optional alias for the target table.
* @param data.targetField - The field in the target table to join on.
* @param [data.initialTableName] - Optional name of the initial table, defaults to the main data source.
* @param data.initialField - The field in the initial table to join on.
*
* @returns
*/
crossJoin(data) {
const targetTableName = data.targetTableName + (data.targetTableNameAs ? ` AS ${data.targetTableNameAs}` : "");
this.#join.push(`CROSS JOIN ${targetTableName} ON ${data.targetTableNameAs || data.targetTableName}.${data.targetField} = ${data.initialTableName || this.#dataSourcePrepared}.${data.initialField}`);
}
/**
* Constructs and appends a WHERE clause to the current SQL query with AND/OR conditions.
*
* This method builds a WHERE clause based on the provided search parameters. It supports both AND and OR
* conditions and appends them to the existing WHERE clause in the query. The method also updates the list of values
* to be used in the SQL statement.
*
* @param data - The search parameters for constructing the WHERE clause.
* @param [data.params] - The primary search parameters, which are ANDed together.
* @param [data.paramsOr] - An array of search parameters, each set is ORed together.
*
* @returns
*/
where(data) {
const { queryArray, queryOrArray, values } = Helpers.compareFields(data.params, data.paramsOr);
if (queryArray.length) {
const comparedFields = queryArray.map((e) => {
const operatorFunction = Helpers.operatorMappings.get(e.operator);
if (operatorFunction) {
const operatorFunctionResult = operatorFunction(e, this.#valuesOrder);
const text = operatorFunctionResult[0];
const orderNumber = operatorFunctionResult[1];
this.#valuesOrder = orderNumber;
return text;
}
else {
this.#valuesOrder += 1;
const text = `${e.key} ${e.operator} $${this.#valuesOrder}`;
return text;
}
}).join(" AND ");
if (!this.#mainWhere) {
this.#mainWhere += `WHERE (${comparedFields})`;
}
else {
this.#mainWhere += ` AND (${comparedFields})`;
}
}
if (queryOrArray?.length) {
const comparedFieldsOr = [];
for (const row of queryOrArray) {
const { query } = row;
const comparedFields = query.map((e) => {
const operatorFunction = Helpers.operatorMappings.get(e.operator);
if (operatorFunction) {
const operatorFunctionResult = operatorFunction(e, this.#valuesOrder);
const text = operatorFunctionResult[0];
const orderNumber = operatorFunctionResult[1];
this.#valuesOrder = orderNumber;
return text;
}
else {
this.#valuesOrder += 1;
const text = `${e.key} ${e.operator} $${this.#valuesOrder}`;
return text;
}
}).join(" AND ");
comparedFieldsOr.push(`(${comparedFields})`);
}
if (!this.#mainWhere) {
this.#mainWhere += `WHERE (${comparedFieldsOr.join(" OR ")})`;
}
else {
this.#mainWhere += ` AND (${comparedFieldsOr.join(" OR ")})`;
}
}
this.#values.push(...values);
}
/**
* Adds a CTE (Common Table Expression) to the query with the specified name and query.
*
* This method constructs a `WITH` clause, allowing the inclusion of a CTE in the SQL query.
* The CTE is named and defined by the provided query. If there are existing CTEs, they are
* concatenated with commas.
*
* @param data - The CTE details.
* @param data.name - The name of the CTE.
* @param data.query - The SQL query that defines the CTE.
* @param [values] - Optional array of values to be substituted into the CTE query.
*
* @returns
*/
with(data, values) {
const queryPrepared = values?.length
? this.#processDataWithValues(data.query, values)
: data.query;
const text = `${data.name} AS (${queryPrepared})`;
if (!this.#with) {
this.#with = `WITH ${text}`;
}
else {
this.#with += `, ${text}`;
}
}
/**
* Appends a raw SQL WHERE clause to the current query with optional value substitution.
*
* This method adds a raw `WHERE` clause to the SQL query. If values are provided, they are
* substituted into the SQL string using the internal value processing method.
*
* @param data - The raw SQL WHERE clause to be appended.
* @param [values] - Optional array of values to be substituted into the SQL string.
*
* @returns
*/
rawWhere(data, values) {
if (!data)
return;
if (!this.#mainWhere) {
const dataLowerCased = data.toLowerCase();
if (dataLowerCased.slice(0, 5) !== "where") {
this.#mainWhere = "WHERE";
}
}
const dataPrepared = values?.length
? this.#processDataWithValues(data, values)
: data;
this.#mainWhere += ` ${dataPrepared}`;
}
/**
* Sets pagination for the query, specifying the limit and offset.
*
* This method adds a `LIMIT` and `OFFSET` clause to the query. It enforces that pagination
* can only be defined once per query.
*
* @param data - The pagination details.
* @param data.limit - The maximum number of records to return.
* @param data.offset - The number of records to skip before starting to return results.
*
* @returns
*
* @throws {Error} - Throws an error if pagination is already defined.
*/
pagination(data) {
if (this.#pagination)
throw new Error("pagination already defined");
this.#pagination = `LIMIT $${++this.#valuesOrder} OFFSET $${++this.#valuesOrder}`;
this.#values.push(data.limit);
this.#values.push(data.offset);
}
/**
* Adds an `ORDER BY` clause to the current query.
*
* This method specifies the columns and sorting direction for ordering the query results.
*
* @param data - Array of column and sorting direction objects.
* @param data.column - The column to sort by.
* @param data.sorting - The sorting direction, either "ASC" or "DESC".
*
* @returns
*/
orderBy(data) {
if (!this.#orderBy)
this.#orderBy = "ORDER BY";
this.#orderBy += ` ${data.map((o) => `${o.column} ${o.sorting}`).join(", ")}`;
}
/**
* Adds a `GROUP BY` clause to the current query.
*
* This method specifies the columns for grouping query results.
*
* @param data - Array of columns to group by.
*
* @returns
*/
groupBy(data) {
if (!this.#groupBy)
this.#groupBy = "GROUP BY";
this.#groupBy += ` ${data.join(", ")}`;
}
/**
* Constructs and appends a HAVING clause to the current SQL query with AND/OR conditions.
*
* This method builds a `HAVING` clause based on the provided search parameters. It supports
* both AND and OR conditions and appends them to the existing `HAVING` clause in the query.
* It also updates the list of values to be used in the SQL statement.
*
* @param data - The search parameters for constructing the HAVING clause.
* @param [data.params] - The primary search parameters, which are ANDed together.
* @param [data.paramsOr] - An array of search parameters, each set is ORed together.
*
* @returns
*/
having(data) {
const { queryArray, queryOrArray, values } = Helpers.compareFields(data.params, data.paramsOr);
if (queryArray.length) {
const comparedFields = queryArray.map((e) => {
const operatorFunction = Helpers.operatorMappings.get(e.operator);
if (operatorFunction) {
const operatorFunctionResult = operatorFunction(e, this.#valuesOrder);
const text = operatorFunctionResult[0];
const orderNumber = operatorFunctionResult[1];
this.#valuesOrder = orderNumber;
return text;
}
else {
this.#valuesOrder += 1;
const text = `${e.key} ${e.operator} $${this.#valuesOrder}`;
return text;
}
}).join(" AND ");
if (!this.#mainHaving) {
this.#mainHaving += `HAVING (${comparedFields})`;
}
else {
this.#mainHaving += ` AND (${comparedFields})`;
}
}
if (queryOrArray?.length) {
const comparedFieldsOr = [];
for (const row of queryOrArray) {
const { query } = row;
const comparedFields = query.map((e) => {
const operatorFunction = Helpers.operatorMappings.get(e.operator);
if (operatorFunction) {
const operatorFunctionResult = operatorFunction(e, this.#valuesOrder);
const text = operatorFunctionResult[0];
const orderNumber = operatorFunctionResult[1];
this.#valuesOrder = orderNumber;
return text;
}
else {
this.#valuesOrder += 1;
const text = `${e.key} ${e.operator} $${this.#valuesOrder}`;
return text;
}
}).join(" AND ");
comparedFieldsOr.push(`(${comparedFields})`);
}
if (!this.#mainHaving) {
this.#mainHaving += `HAVING (${comparedFieldsOr.join(" OR ")})`;
}
else {
this.#mainHaving += ` AND (${comparedFieldsOr.join(" OR ")})`;
}
}
this.#values.push(...values);
}
/**
* Appends a raw SQL HAVING clause to the current query with optional value substitution.
*
* This method allows adding a raw `HAVING` clause to the SQL query. If values are provided,
* they are substituted into the SQL string using the internal value processing method.
*
* @param data - The raw SQL HAVING clause to be appended.
* @param [values] - Optional array of values to be substituted into the SQL string.
*
* @returns
*/
rawHaving(data, values) {
if (!data)
return;
if (!this.#mainHaving) {
const dataLowerCased = data.toLowerCase();
if (dataLowerCased.slice(0, 6) !== "having") {
this.#mainHaving = "HAVING";
}
}
const dataPrepared = values?.length
? this.#processDataWithValues(data, values)
: data;
this.#mainHaving += ` ${dataPrepared}`;
}
/**
* Specifies the columns to be returned by the query using a RETURNING clause.
*
* This method constructs a `RETURNING` clause to specify which columns to return in the result set.
*
* @param data - Array of columns to return.
*
* @returns
*/
returning(data) {
this.#returning = `RETURNING ${data.join(", ")}`;
}
/**
* Marks the query as a subquery and optionally assigns a name to the subquery.
*
* This method configures the query to be treated as a subquery, and optionally assigns a name to it.
*
* @param [data] - Optional name for the subquery.
*
* @returns
*/
toSubquery(data) {
this.isSubquery = true;
if (data) {
this.#subqueryName = data;
}
}
}
exports.QueryHandler = QueryHandler;