UNPKG

firebird-query

Version:

node-firebird plugin for easy and safe query building.

460 lines (459 loc) 16.4 kB
import Firebird from "node-firebird"; const isolationMap = { READ_COMMITTED: Firebird.ISOLATION_READ_COMMITTED, READ_COMMITTED_READ_ONLY: Firebird.ISOLATION_READ_COMMITTED_READ_ONLY, READ_UNCOMMITTED: Firebird.ISOLATION_READ_UNCOMMITTED, REPEATABLE_READ: Firebird.ISOLATION_REPEATABLE_READ, SERIALIZABLE: Firebird.ISOLATION_SERIALIZABLE, }; const escape = (...val) => Firebird.escape(val.join("")); const buildWhereClause = (obj, prefix = "", clause = "AND") => { const clauses = []; if (isSingleUndefinedValue(obj)) { return defaultCondition(clause); } for (const key in obj) { const val = obj[key]; if (isWhereObjectArr(val)) { clauses.push(handleOrCondition(val, prefix, key)); } else if (isWhereObject(val)) { clauses.push(...handleObjectCondition(val, prefix, key)); } else if (isPrimitiveValue(val)) { clauses.push(handlePrimitiveValue(val, prefix, key)); } else if (isManuallyEscapedStatement(val)) { return val(Firebird.escape); } else { clauses.push(defaultCondition(clause)); } } return clauses.join(" AND "); }; const isSingleUndefinedValue = (obj) => Object.values(obj).length === 1 && Object.values(obj)[0] === undefined; const defaultCondition = (clause) => clause === "AND" ? "1=1" : "1=0"; const isWhereObject = (val) => typeof val === "object" && !Array.isArray(val) && !(val instanceof Date); const isWhereObjectArr = (val) => Array.isArray(val) && isWhereObject(val[0]); const isManuallyEscapedStatement = (val) => typeof val === "function"; const isPrimitiveValue = (val) => typeof val === "string" || typeof val === "number" || typeof val === "boolean" || val instanceof Date || val === null || val === undefined; const handleOrCondition = (val, prefix, type = "OR") => { const orClauses = val.map((orObj) => buildWhereClause(orObj, prefix, type)); return `(${orClauses.join(` ${type} `)})`; }; const handleObjectCondition = (val, prefix, key) => { const clauses = []; for (const subKey in val) { const value = val[subKey]; let condition = ""; switch (subKey) { case "ne": condition = `${prefix}${key} != ${Firebird.escape(value)}`; break; case "gt": condition = `${prefix}${key} > ${Firebird.escape(value)}`; break; case "gte": condition = `${prefix}${key} >= ${Firebird.escape(value)}`; break; case "lt": condition = `${prefix}${key} < ${Firebird.escape(value)}`; break; case "lte": condition = `${prefix}${key} <= ${Firebird.escape(value)}`; break; case "between": condition = `${prefix}${key} BETWEEN ${Firebird.escape(value["from"])} AND ${Firebird.escape(value["to"])}`; break; case "IN": condition = `${prefix}${key} IN (${value .map((i) => Firebird.escape(i)) .join(", ")})`; break; case "notIN": condition = `${prefix}${key} NOT IN (${value .map((i) => Firebird.escape(i)) .join(", ")})`; break; case "startsWith": condition = `${prefix}${key} LIKE ${escape(value, "%")}`; break; case "endsWith": condition = `${prefix}${key} LIKE ${escape("%", value)}`; break; case "contains": condition = `${prefix}${key} CONTAINING ${Firebird.escape(value)}`; break; } if (value === undefined) { condition = "1=1"; } if (Array.isArray(value) && value.length === 0) { condition = "1=1"; } if (condition) { clauses.push(condition); } } return clauses; }; const handlePrimitiveValue = (val, prefix, key) => val === undefined ? "1=1" : `${prefix}${key} = ${Firebird.escape(val)}`; const sqlBuilder = (strings, params) => { return strings .map((cur, i) => { const param = params[i]; if (isWhereObject(param)) { const conditions = param; const conditionResult = buildWhereClause(conditions); return cur + conditionResult; } else if (isPrimitiveValue(param)) { const isLastStr = i === strings.length - 1; const valueResult = !isLastStr ? Firebird.escape(param) : ""; return cur + valueResult; } else if (isManuallyEscapedStatement(param)) { return cur + param((s) => Firebird.escape(s)); } else { return ""; } }) .join(""); }; const paginatedQuery = (query, take, page) => { const skip = take * (page - 1); return `SELECT FIRST ${take} SKIP ${skip} * FROM (${query.replace(/;*$/g, "")});`; }; const insertOneQuery = (params) => { const { tableName, rowValues, returning = [] } = params; const columns = Object.keys(rowValues); const columnsStr = columns.join(", "); const escapedValues = columns.map((key) => Firebird.escape(rowValues[key])); const valuesStr = escapedValues.join(", "); let query = `INSERT INTO ${tableName} (${columnsStr}) VALUES (${valuesStr})`; if (returning.length > 0) { query += ` RETURNING ${returning.join(", ")}`; } query += ";"; return query; }; const insertManyQuery = ({ tableName, columnNames, rowValues, }) => { const sortedColumnsStr = columnNames .slice() .sort((a, b) => String(a).localeCompare(String(b))) .join(", "); const selectStatements = rowValues.map((row) => { const sortedRow = Object.entries(row).sort(([a], [b]) => String(a).localeCompare(String(b))); const valuesList = sortedRow .map(([, value]) => Firebird.escape(value)) .join(", "); return `SELECT ${valuesList} FROM RDB$DATABASE`; }); const toInsertStatement = selectStatements.join(" UNION ALL "); const query = `INSERT INTO ${tableName} (${sortedColumnsStr}) ${toInsertStatement};`; return query; }; const updateOneQuery = ({ tableName, rowValues, returning = [], where, }) => { const toSet = Object.entries(rowValues).map(([columnName, value]) => value !== undefined ? `${columnName} = ${escape(value)}` : undefined); const filteredToSet = toSet.filter((item) => item !== undefined); const valuesStr = filteredToSet.join(", "); const whereStr = buildWhereClause(where); let query = `UPDATE ${tableName} SET ${valuesStr} WHERE ${whereStr}`; if (returning.length > 0) { query += ` RETURNING ${returning.join(", ")}`; } query += ";"; return query; }; const updateOrInsertQuery = ({ tableName, rowValues, returning = [], }) => { const columns = Object.keys(rowValues); const columnsStr = columns.join(", "); const escapedValues = columns.map((key) => escape(rowValues[key])); const valuesStr = escapedValues.join(", "); let query = `UPDATE OR INSERT INTO ${tableName} (${columnsStr}) VALUES (${valuesStr})`; if (returning.length > 0) { query += ` RETURNING ${returning.join(", ")}`; } query += ";"; return query; }; const deleteOneQuery = (params) => { const { where, returning = [], tableName } = params; const whereClauses = buildWhereClause(where); let query = `DELETE FROM ${tableName} WHERE ${whereClauses}`; if (returning.length > 0) { query += ` RETURNING ${returning.join(", ")}`; } query += ";"; return query; }; const defaultOptions = { host: process.env.DB_HOST, port: Number.parseInt(process.env.DB_PORT || "") || 3050, database: process.env.DB_DATABASE, user: process.env.DB_USER || "SYSDBA", password: process.env.DB_PASSWORD, }; export class FirebirdQuery { options; conn; queryLogger; defaultIsolationLevel; constructor(poolOptions = { ...defaultOptions }, fqOptions) { const { maxConnections = 15, ...options } = poolOptions; this.options = options; this.queryLogger = fqOptions?.queryLogger; this.conn = Firebird.pool(maxConnections, options); this.defaultIsolationLevel = fqOptions?.defaultIsolationLevel || "READ_COMMITTED"; } get db() { return new Promise((res, rej) => { this.conn.get((err, db) => { if (err) { return rej(err); } return res(db); }); }); } manageQuery(query) { return new Promise((res, rej) => { if (this.queryLogger) { this.queryLogger(query); } this.db .then((db) => { db.query(query, [], (err, data) => { if (err) { return rej(err); } db.detach((err) => { if (err) { return rej(err); } return res(data); }); }); }) .catch((err) => rej(err)); }); } get queryRaw() { return this.handleRawQuery(async (query) => { return this.manageQuery(query); }); } get insertOne() { return this.handleInsertOne(async (query) => { return this.manageQuery(query); }); } get insertMany() { return this.handleInsertMany(async (query, length) => { await this.manageQuery(query); return `${length} rows inserted`; }); } get updateOne() { return this.handleUpdateOne(async (query) => { return this.manageQuery(query); }); } get updateOrInsert() { return this.handleUpdateOrInsert(async (query) => { return this.manageQuery(query); }); } get deleteOne() { return this.handleDeleteOne(async (query) => { return this.manageQuery(query); }); } destroy() { return new Promise((res, rej) => { this.conn.destroy((err) => { if (err) { if (err instanceof Error) { return rej(err); } return rej(new Error("Error destroying connection")); } return res(); }); }); } initTransaction(cb, isolation) { const isolationLevel = isolation || this.defaultIsolationLevel; return new Promise((res, rej) => { Firebird.attach(this.options, (err, db) => { if (err) { return rej(new Error("Error Establishing a Database Connection")); } db.transaction(isolationMap[isolationLevel], (err, tx) => { if (err) { return rej(new Error("Error initializing transaction")); } return res(cb(this.txHandler(tx, db))); }); }); }); } handleRawQuery(cb) { return (strings, ...params) => { const sanitizedQuery = sqlBuilder(strings, params); return { getQuery: () => sanitizedQuery, execute: () => { return cb(sanitizedQuery); }, paginated: async (take, page = 1) => { const pagQuery = paginatedQuery(sanitizedQuery, take, page); return cb(pagQuery); }, }; }; } handleInsertOne(cb) { return (params) => { const query = insertOneQuery(params); return { getQuery: () => query, execute: () => { return cb(query); }, }; }; } handleInsertMany(cb) { return (params) => { const query = insertManyQuery(params); return { getQuery: () => query, execute: () => { return cb(query, params.rowValues.length); }, }; }; } handleUpdateOne(cb) { return (params) => { const query = updateOneQuery(params); return { getQuery: () => query, execute: () => { return cb(query); }, }; }; } handleUpdateOrInsert(cb) { return (params) => { const query = updateOrInsertQuery(params); return { getQuery: () => query, execute: () => { return cb(query); }, }; }; } handleDeleteOne(cb) { return (params) => { const query = deleteOneQuery(params); return { getQuery: () => query, execute: () => { return cb(query); }, }; }; } txHandler(tx, db) { const executeTransactionQuery = (query, processResult) => { return new Promise((res, rej) => { if (this.queryLogger) { this.queryLogger(query); } tx.query(query, [], (err, data) => { if (err) { tx.rollback((txErr) => { if (txErr) { return rej(txErr); } }); return rej(err); } return res(processResult(data)); }); }); }; return { queryRaw: this.handleRawQuery((query) => { return executeTransactionQuery(query, (data) => data); }), insertOne: this.handleInsertOne((query) => { return executeTransactionQuery(query, (data) => data); }), insertMany: this.handleInsertMany((query, length) => { return executeTransactionQuery(query, () => `${length} row${length > 1 ? "s" : ""} inserted`); }), updateOne: this.handleUpdateOne((query) => { return executeTransactionQuery(query, (data) => data); }), updateOrInsert: this.handleUpdateOrInsert((query) => { return executeTransactionQuery(query, (data) => data); }), deleteOne: this.handleDeleteOne((query) => { return executeTransactionQuery(query, (data) => data); }), commit: () => { return new Promise((res, rej) => { tx.commit((err) => { if (err) { return rej(err); } db.detach((err) => { if (err) { return rej(err); } return res(); }); }); }); }, rollback: () => new Promise((res, rej) => { tx.rollback((err) => { if (err) { return rej(err); } return res(); }); }), commitRetaining: () => { return new Promise((res, rej) => { tx.commitRetaining((err) => { if (err) { return rej(err); } return res(); }); }); }, rollbackRetaining: () => new Promise((res, rej) => { tx.rollbackRetaining((err) => { if (err) { return rej(err); } return res(); }); }), }; } }