UNPKG

@js-ak/db-manager

Version:
883 lines (882 loc) 38.3 kB
"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;