UNPKG

@js-ak/db-manager

Version:
410 lines (409 loc) 17.3 kB
import * as Helpers from "../helpers/index.js"; import * as SharedHelpers from "../../../shared-helpers/index.js"; import * as connection from "../connection.js"; import { QueryBuilder } from "../query-builder/index.js"; import queries from "./queries.js"; import { setLoggerAndExecutor } from "../helpers/index.js"; export class BaseModel { #insertOptions; #sortingOrders = new Set(["ASC", "DESC"]); #tableFieldsSet; #isLoggerEnabled; #logger; #executeSql; /** * The PostgreSQL executor. * - pg.Pool * - pg.PoolClient * - pg.Client */ #executor; createField; primaryKey; tableName; tableFields; updateField; constructor(data, dbCreds, options) { this.createField = data.createField; this.#executor = connection.getStandardPool(dbCreds); this.primaryKey = data.primaryKey; this.tableName = data.tableName; this.tableFields = [...data.tableFields]; this.updateField = data.updateField; this.#tableFieldsSet = new Set([ ...this.tableFields, ...(data.additionalSortingFields || []), ]); const { insertOptions, isLoggerEnabled, logger } = options || {}; const preparedOptions = setLoggerAndExecutor(this.#executor, { isLoggerEnabled, logger }); this.#insertOptions = insertOptions; this.#executeSql = preparedOptions.executeSql; this.#isLoggerEnabled = preparedOptions.isLoggerEnabled; this.#logger = preparedOptions.logger; } /** * Gets the database client for the model. * * @returns The database client for the model. */ get pool() { return this.#executor; } /** * Gets the PostgreSQL executor for the model. * * @returns The PostgreSQL executor for the model. */ get executor() { return this.#executor; } /** * Sets the logger for the model. * * @param logger - The logger to use for the model. */ setLogger(logger) { const preparedOptions = setLoggerAndExecutor(this.#executor, { isLoggerEnabled: true, logger }); this.#executeSql = preparedOptions.executeSql; this.#isLoggerEnabled = preparedOptions.isLoggerEnabled; this.#logger = preparedOptions.logger; } /** * Sets the executor for the model. * * @param executor - The executor to use for the model. */ setExecutor(executor) { const preparedOptions = setLoggerAndExecutor(executor, { isLoggerEnabled: this.#isLoggerEnabled, logger: this.#logger }); this.#executeSql = preparedOptions.executeSql; this.#isLoggerEnabled = preparedOptions.isLoggerEnabled; this.#logger = preparedOptions.logger; this.#executor = executor; } get isLoggerEnabled() { return this.#isLoggerEnabled; } get executeSql() { return this.#executeSql; } compareFields = Helpers.compareFields; getFieldsToSearch = Helpers.getFieldsToSearch; compareQuery = { deleteAll: () => { return { query: queries.deleteAll(this.tableName) }; }, deleteByParams: ({ $and = {}, $or }) => { const { queryArray, queryOrArray, values } = this.compareFields($and, $or); const { searchFields } = this.getFieldsToSearch({ queryArray, queryOrArray }); return { query: queries.deleteByParams(this.tableName, searchFields), values, }; }, deleteOneByPk: (primaryKey) => { if (!this.primaryKey) { throw new Error("Primary key not specified"); } return { query: queries.deleteByPk(this.tableName, this.primaryKey), values: Array.isArray(primaryKey) ? primaryKey : [primaryKey], }; }, getArrByParams: ({ $and = {}, $or }, selected = ["*"], pagination, order) => { if (order?.length) { for (const o of order) { if (!this.#tableFieldsSet.has(o.orderBy)) { const allowedFields = Array.from(this.#tableFieldsSet).join(", "); throw new Error(`Invalid orderBy: ${o.orderBy}. Allowed fields are: ${allowedFields}`); } if (!this.#sortingOrders.has(o.ordering)) { throw new Error("Invalid ordering"); } } } if (!selected.length) selected.push("*"); const { queryArray, queryOrArray, values } = this.compareFields($and, $or); const { orderByFields, paginationFields, searchFields, selectedFields } = this.getFieldsToSearch({ queryArray, queryOrArray }, selected, pagination, order); return { query: queries.getByParams(this.tableName, selectedFields, searchFields, orderByFields, paginationFields), values, }; }, getCountByParams: ({ $and = {}, $or }) => { const { queryArray, queryOrArray, values } = this.compareFields($and, $or); const { searchFields } = this.getFieldsToSearch({ queryArray, queryOrArray }); return { query: queries.getCountByParams(this.tableName, searchFields), values, }; }, getCountByPks: (pks) => { if (!this.primaryKey) { throw new Error("Primary key not specified"); } if (Array.isArray(pks[0])) { if (!Array.isArray(this.primaryKey)) { throw new Error("invalid primary key type"); } return { query: queries.getCountByCompositePks(this.primaryKey, this.tableName, pks.length), values: [pks.flat()], }; } if (Array.isArray(this.primaryKey)) { throw new Error("invalid primary key type"); } return { query: queries.getCountByPks(this.primaryKey, this.tableName), values: [pks], }; }, getCountByPksAndParams: (pks, { $and = {}, $or }) => { if (!this.primaryKey) { throw new Error("Primary key not specified"); } const { queryArray, queryOrArray, values } = this.compareFields($and, $or); const { orderNumber, searchFields } = this.getFieldsToSearch({ queryArray, queryOrArray }); if (Array.isArray(pks[0])) { if (!Array.isArray(this.primaryKey)) { throw new Error("invalid primary key type"); } return { query: queries.getCountByCompositePksAndParams(this.primaryKey, this.tableName, searchFields, orderNumber, pks.length), values: pks.flat(), }; } if (Array.isArray(this.primaryKey)) { throw new Error("invalid primary key type"); } return { query: queries.getCountByPksAndParams(this.primaryKey, this.tableName, searchFields, orderNumber), values: [...values, pks], }; }, getOneByParams: ({ $and = {}, $or }, selected = ["*"]) => { if (!selected.length) selected.push("*"); const { queryArray, queryOrArray, values } = this.compareFields($and, $or); const { orderByFields, paginationFields, searchFields, selectedFields } = this.getFieldsToSearch({ queryArray, queryOrArray }, selected, { limit: 1, offset: 0 }); return { query: queries.getByParams(this.tableName, selectedFields, searchFields, orderByFields, paginationFields), values, }; }, getOneByPk: (pk) => { if (!this.primaryKey) { throw new Error("Primary key not specified"); } return { query: queries.getOneByPk(this.tableName, this.primaryKey), values: [pk], }; }, save: (recordParams = {}, saveOptions) => { const clearedParams = SharedHelpers.clearUndefinedFields(recordParams); const fields = Object.keys(clearedParams); const onConflict = this.#insertOptions?.onConflict || ""; if (!fields.length) { throw new Error("No one save field arrived"); } return { query: queries.createOne(this.tableName, fields, this.createField, onConflict, saveOptions?.returningFields), values: Object.values(clearedParams), }; }, updateByParams: (queryConditions, updateFields = {}) => { const { queryArray, queryOrArray, values } = this.compareFields(queryConditions.$and, queryConditions.$or); const { orderNumber, searchFields } = this.getFieldsToSearch({ queryArray, queryOrArray }); const clearedUpdate = SharedHelpers.clearUndefinedFields(updateFields); const fieldsToUpdate = Object.keys(clearedUpdate); if (!queryArray.length) throw new Error("No one update field arrived"); return { query: queries.updateByParams(this.tableName, fieldsToUpdate, searchFields, this.updateField, orderNumber + 1, queryConditions?.returningFields), values: [...values, ...Object.values(clearedUpdate)], }; }, updateOneByPk: (primaryKeyValue, updateFields = {}, updateOptions) => { if (!this.primaryKey) { throw new Error("Primary key not specified"); } const clearedParams = SharedHelpers.clearUndefinedFields(updateFields); const fields = Object.keys(clearedParams); if (!fields.length) throw new Error("No one update field arrived"); return { query: queries.updateByPk(this.tableName, fields, this.primaryKey, this.updateField, updateOptions?.returningFields), values: [...Object.values(clearedParams), primaryKeyValue], }; }, }; async deleteAll() { const sql = this.compareQuery.deleteAll(); await this.#executeSql(sql); return; } async deleteOneByPk(primaryKey) { const sql = this.compareQuery.deleteOneByPk(primaryKey); const { rows } = await this.#executeSql(sql); const entity = rows[0]; if (!entity) return null; if (Array.isArray(this.primaryKey)) { return this.primaryKey.map((e) => entity[e]); } else { return entity[this.primaryKey]; } } async deleteByParams(params) { const sql = this.compareQuery.deleteByParams(params); await this.#executeSql(sql); return null; } async getArrByParams(params, selected = ["*"], pagination, order) { const sql = this.compareQuery.getArrByParams(params, selected, pagination, order); const { rows } = await this.#executeSql(sql); return rows; } async getCountByPks(pks) { const sql = this.compareQuery.getCountByPks(pks); const { rows } = await this.#executeSql(sql); return Number(rows[0]?.count) || 0; } async getCountByPksAndParams(pks, params) { const sql = this.compareQuery.getCountByPksAndParams(pks, params); const { rows } = await this.#executeSql(sql); return Number(rows[0]?.count) || 0; } async getCountByParams(params) { const sql = this.compareQuery.getCountByParams(params); const { rows } = await this.#executeSql(sql); return Number(rows[0]?.count) || 0; } async getOneByParams(params, selected = ["*"]) { const sql = this.compareQuery.getOneByParams(params, selected); const { rows } = await this.#executeSql(sql); return rows[0]; } async getOneByPk(pk) { const sql = this.compareQuery.getOneByPk(pk); const { rows } = await this.#executeSql(sql); return rows[0]; } async save(recordParams = {}, saveOptions) { const sql = this.compareQuery.save(recordParams, saveOptions); const { rows } = await this.#executeSql(sql); return rows[0]; } async updateByParams(queryConditions, updateFields = {}) { const sql = this.compareQuery.updateByParams(queryConditions, updateFields); const { rows } = await this.#executeSql(sql); return rows; } async updateOneByPk(primaryKeyValue, updateFields = {}, updateOptions) { const sql = this.compareQuery.updateOneByPk(primaryKeyValue, updateFields, updateOptions); const { rows } = await this.#executeSql(sql); return rows[0]; } /** * @experimental */ queryBuilder(options) { const { client, isLoggerEnabled, logger, tableName } = options || {}; return new QueryBuilder(tableName ?? this.tableName, client ?? this.#executor, { isLoggerEnabled: isLoggerEnabled ?? this.#isLoggerEnabled, logger: logger ?? this.#logger, }); } // STATIC METHODS static getStandardPool(creds, poolName) { return connection.getStandardPool(creds, poolName); } static async removeStandardPool(creds, poolName) { return connection.removeStandardPool(creds, poolName); } static getTransactionPool(creds, poolName) { return connection.getTransactionPool(creds, poolName); } static async removeTransactionPool(creds, poolName) { return connection.removeTransactionPool(creds, poolName); } static getInsertFields(data) { const { params: paramsRaw, returning, tableName, } = data; if (Array.isArray(paramsRaw)) { const v = []; const k = []; const headers = new Set(); const example = paramsRaw[0]; if (!example) throw new Error("Invalid parameters"); const params = SharedHelpers.clearUndefinedFields(example); Object.keys(params).forEach((e) => headers.add(e)); for (const pR of paramsRaw) { const params = SharedHelpers.clearUndefinedFields(pR); const keys = Object.keys(params); k.push(keys); v.push(...Object.values(params)); if (!k.length) { throw new Error(`Invalid params, all fields are undefined - ${Object.keys(paramsRaw).join(", ")}`); } for (const key of keys) { if (!headers.has(key)) { throw new Error(`Invalid params, all fields are undefined - ${Object.keys(pR).join(", ")}`); } } } const returningSQL = returning?.length ? `RETURNING ${returning.join(",")}` : ""; let idx = 0; const query = ` INSERT INTO ${tableName}(${Array.from(headers).join(",")}) VALUES(${k.map((e) => e.map(() => "$" + ++idx)).join("),(")}) ${returningSQL} `; return { query, values: v }; } const params = SharedHelpers.clearUndefinedFields(paramsRaw); const k = Object.keys(params); const v = Object.values(params); if (!k.length) throw new Error(`Invalid params, all fields are undefined - ${Object.keys(paramsRaw).join(", ")}`); const returningSQL = returning?.length ? `RETURNING ${returning.join(",")}` : ""; const query = `INSERT INTO ${tableName}(${k.join(",")}) VALUES(${k.map((_, idx) => "$" + ++idx).join(",")}) ${returningSQL};`; return { query, values: v }; } static getUpdateFields(data) { const { params: paramsRaw, primaryKey, returning, tableName, updateField, } = data; const params = SharedHelpers.clearUndefinedFields(paramsRaw); const k = Object.keys(params); const v = Object.values(params); if (!k.length) throw new Error(`Invalid params, all fields are undefined - ${Object.keys(paramsRaw).join(", ")}`); let updateFields = k.map((e, idx) => `${e} = $${idx + 2}`).join(","); if (updateField) { switch (updateField.type) { case "timestamp": { updateFields += `, ${updateField.title} = NOW()`; break; } case "unix_timestamp": { updateFields += `, ${updateField.title} = ROUND((EXTRACT(EPOCH FROM NOW()) * (1000)::NUMERIC))`; break; } default: { throw new Error("Invalid type: " + updateField.type); } } } const returningSQL = returning?.length ? `RETURNING ${returning.join(",")}` : ""; const query = `UPDATE ${tableName} SET ${updateFields} WHERE ${primaryKey.field} = $1 ${returningSQL};`; return { query, values: [primaryKey.value, ...v] }; } }