tuain-dbpool-generic
Version: 
Database pool interface to perform standard database operations as part of the Tuain Application Development Framework
319 lines (300 loc) • 12.4 kB
JavaScript
const knex = require('knex');
const { attachPaginate } = require('knex-paginate');
const queryUtils = require('./query-utils');
attachPaginate();
const modErrs = {
  common: {
    queryType: ['01', 'Tipo de sentencia inválido'],
    queryProcessing: ['02', 'Error en procesamiento de consulta'],
    notInfoQuery: ['03', 'Consulta no generó resultados'],
    procedureError: ['04', 'Error en ejecución de procedimiento'],
    wrongQueryResult: ['05', 'El resultado de la consulta no tiene la estructura esperada'],
    generalError: ['99', 'Error general de base de datos'],
  },
};
const label = 'DatabasePool';
const SELECTONE = 'SELECTONE';
class DatabasePool {
  constructor(logger, errors) {
    this.logger = logger;
    this.errors = errors;
    this.errors.addModuleSet('dbpool-lib', modErrs);
    this.databasePool = {};
    this.engineName = '';
    this.knex = knex;
    this.mappingFunctions = {
      SELECTONE: 'executeQuery',
      SELECT: 'executeQuery',
      INSERT: 'executeInsert',
      UPDATE: 'executeUpdate',
      DELETE: 'executeDelete',
      PROCEDURE: 'executeProcedure',
    };
  }
  async launchQuery(queryObject, recordData, dataConstraints) {
    const { queryType } = queryObject;
    let errorObj = null;
    let message = '';
    const action = 'launchQuery';
    try {
      const methodName = this.mappingFunctions[queryType];
      if (!this[methodName]) {
        message = `Tipo de query ${queryType} no soportado en libreria`;
        this.logger.log({ level: 'error', label, action, message });
        errorObj = this.errors.get(modErrs.common.queryType, message);
        return [errorObj, null];
      }
      const queryResult = await this[methodName](queryObject, recordData, dataConstraints);
      if (!queryResult || !Array.isArray(queryResult) || queryResult.length !== 2) {
        errorObj = this.errors.get(modErrs.common.wrongQueryResult);
        return [errorObj, queryResult];
      }
      return queryResult;
    } catch (err) {
      message = `Error preparando sentencia ${queryType}: ${err.message}/${err.stack}`;
      this.logger.log({ level: 'error', label, action, message });
      errorObj = this.errors.get(modErrs.common.queryProcessing, message);
      return [errorObj, null];
    }
  }
  async executeRawQuery() {
    const message = 'Método virtual puro que requiere de implementación en la subclase';
    this.logger.log({ level: 'warn', label, action: 'executeRawQuery', message });
  }
  async executeProcedure() {
    const message = 'Método virtual puro que requiere de implementación en la subclase';
    this.logger.log({ level: 'warn', label, action: 'executeProcedure', message });
  }
  async executeQuery(queryObject, recordData, inputConstraints) {
    let joinArgs = '';
    let message;
    const action = 'executeQuery';
    const response = {};
    const dataConstraints = queryUtils.completeFilters(queryObject, recordData, inputConstraints);
    const [selectColumns, knexColArgs] = queryUtils.processColumns(this.knexPool, queryObject);
    const knexQuery = this.knexPool.select(knexColArgs);
    const tableObj = queryUtils.processTable(queryObject);
    knexQuery.from(tableObj);
    joinArgs += queryUtils.processFixedJoins(queryObject);
    let simpleFilterFields = selectColumns;
    if (queryObject.simpleFilterFields) {
      simpleFilterFields = {};
      Object.keys(selectColumns).forEach((alias) => {
        if (queryObject.simpleFilterFields.includes(alias)) {
          simpleFilterFields[alias] = selectColumns[alias];
        }
      });
    }
    queryUtils.processSimpleFilters(queryObject, dataConstraints, simpleFilterFields, knexQuery);
    if (dataConstraints && dataConstraints.filtersByValue) {
      joinArgs += queryUtils.processFiltersByValue(queryObject, dataConstraints, selectColumns, knexQuery);
    }
    knexQuery.joinRaw(joinArgs.trim());
    queryUtils.processOrder(dataConstraints, selectColumns, knexQuery);
    let queryData;
    let executedPaginated = false;
    if (dataConstraints) {
      const { recordsNumber: perPage, pageNumber: currentPage, includeTotalRows: isLengthAware } = dataConstraints;
      if (perPage && currentPage) {
        executedPaginated = true;
        const queryToBeExecuted = knexQuery.paginate({ perPage, currentPage, isLengthAware: isLengthAware ?? false });
        message = `Ejecución consulta paginada: ${knexQuery.toString()}`;
        this.logger.log({ level: 'silly', label, action, message });
        const { data, pagination } = await queryToBeExecuted;
        queryData = data;
        response._pagination = {
          pageNumber: pagination.currentPage,
          rows: pagination.total ?? null,
          lastPage: pagination?.lastPage ?? null,
          // pagination.from, pagination.to
        };
      }
    }
    if (!executedPaginated) {
      message = `Ejecución consulta sin paginación: ${knexQuery.toString()}`;
      this.logger.log({ level: 'silly', label, action, message });
      queryData = await this.executeRawQuery(knexQuery.toString());
    }
    return this.processResponse(queryObject, response, queryData);
  }
  async executeInsert(queryObject, recordData) {
    const action = 'executeInsert';
    const recordsToInsert = Array.isArray(recordData) ? recordData : [recordData];
    const table = queryObject.dataSource[0];
    const columns = queryObject.queryFields;
    const predefinedSets = queryObject.defaultAssign;
    const fieldValues = queryObject.fieldAliases;
    const insertRecords = [];
    const defaultArgs = {};
    if (predefinedSets) {
      const preFields = Object.keys(predefinedSets);
      if (preFields && Array.isArray(preFields) && preFields.length > 0) {
        for (let index = 0; index < preFields.length; index++) {
          const fieldName = preFields[index];
          const fieldValue = predefinedSets[fieldName];
          defaultArgs[fieldName] = fieldValue;
        }
      }
    }
    for (let recordIndex = 0; recordIndex < recordsToInsert.length; recordIndex++) {
      const recordFields = recordsToInsert[recordIndex];
      const insertArgs = { ...defaultArgs };
      for (let index = 0; index < columns.length; index++) {
        const column = columns[index];
        const requestKey = fieldValues[index];
        if (recordFields && requestKey in recordFields) {
          insertArgs[column] = recordFields[requestKey];
        }
      }
      insertRecords.push(insertArgs);
    }
    let keyColumn = null;
    let keyFieldName = null;
    if (queryObject && 'recordKey' in queryObject) {
      [keyFieldName] = Object.keys(queryObject.recordKey);
      keyColumn = queryObject.recordKey[keyFieldName];
    }
    const sql = this.knexPool(table).insert(insertRecords).toString();
    const message = `DBPOOL: Ejecución insert: ${sql}`;
    this.logger.log({ level: 'silly', label, action, message });
    let fieldToRecover = '';
    let insertedRecords;
    if (this.engineName === 'mssql') {
      fieldToRecover = queryObject.recoverAll || !keyColumn ? '*' : keyColumn;
      insertedRecords = await this.knexPool(table).insert(insertRecords).returning(fieldToRecover);
    } else {
      insertedRecords = await this.knexPool(table).insert(insertRecords);
    }
    if (fieldToRecover !== '*' || typeof insertedRecords?.[0] === 'number') {
      // No se tienen campos
      return [null, { ids: insertedRecords }];
    }
    let insertedKeys;
    if (insertedRecords && insertedRecords.length > 0) {
      const mappingColumns = columns;
      const mappingTable = {};
      for (let index = 0; index < columns.length; index++) {
        const columnName = columns[index];
        const fieldName = queryObject.fieldAliases[index];
        mappingTable[columnName] = fieldName;
      }
      if (keyColumn) {
        mappingColumns.push(keyColumn);
        mappingTable[keyColumn] = keyFieldName;
      }
      insertedKeys = insertedRecords.map((newRecord) => newRecord[keyColumn]);
      insertedRecords = insertedRecords.map((newRecord) => {
        const externalRecord = {};
        mappingColumns.forEach((columnName) => {
          const fieldName = mappingTable[columnName];
          if (fieldName) {
            externalRecord[fieldName] = newRecord[columnName];
          }
        });
        return externalRecord;
      });
    }
    if (insertedKeys && insertedKeys.length > 0) {
      return [null, { ids: insertedKeys, records: insertedRecords }];
    }
    return [null, { records: insertedRecords }];
  }
  async executeUpdate(queryObject, recordFields, dataConstraints) {
    const action = 'executeUpdate';
    let message;
    const table = queryObject.dataSource[0];
    const setColumns = queryObject.queryFields;
    const predefinedSets = queryObject.defaultAssign;
    const setFieldValues = queryObject.fieldAliases;
    const whereFields = queryObject.requiredFields;
    const filters = dataConstraints && dataConstraints.filtersByValue ? dataConstraints.filtersByValue : null;
    if (!whereFields && filters === null) {
      message = '¡Sentencia update sin where!';
      this.logger.log({ level: 'error', label, action, message });
    }
    const updateArgs = {};
    for (let index = 0; index < setColumns.length; index++) {
      const column = setColumns[index];
      const requestKey = setFieldValues[index];
      if (requestKey && recordFields && requestKey in recordFields) {
        updateArgs[column] = recordFields[requestKey];
      }
    }
    if (predefinedSets) {
      const preFields = Object.keys(predefinedSets);
      if (preFields && Array.isArray(preFields) && preFields.length > 0) {
        for (let index = 0; index < preFields.length; index++) {
          const fieldName = preFields[index];
          const fieldValue = predefinedSets[fieldName];
          updateArgs[fieldName] = fieldValue;
        }
      }
    }
    let sql;
    if (!whereFields) {
      sql = this.knexPool(table).update(updateArgs).toString();
    } else {
      sql = this.knexPool(table)
        .where((builder) => {
          queryUtils.processWhereSentence(whereFields, filters, builder);
        })
        .update(updateArgs)
        .toString();
    }
    message = `Ejecución update: ${sql}`;
    this.logger.log({ level: 'silly', label, action, message });
    const update = await this.knexPool.raw(sql);
    return [null, { changedRows: update[0].changedRows }];
  }
  async executeDelete(queryObject, recordData, dataConstraints) {
    const action = 'executeDelete';
    let message;
    const table = queryObject.dataSource;
    const whereFields = queryObject.requiredFields;
    const filters =
      dataConstraints && dataConstraints.filtersByValue
        ? dataConstraints.filtersByValue
        : queryUtils.completeDeleteFilters(recordData, whereFields);
    if (!whereFields && filters === null) {
      message = '¡Sentencia delete sin where!';
      this.logger.log({ level: 'warn', label, action, message });
    }
    let sql;
    if (!whereFields) {
      sql = this.knexPool(table).del().toString();
    } else {
      sql = this.knexPool(table)
        .where((builder) => {
          queryUtils.processWhereSentence(whereFields, filters, builder);
        })
        .del()
        .toString();
    }
    message = `Ejecución delete: ${sql}`;
    this.logger.log({ level: 'silly', label, action, message });
    const del = await this.knexPool.raw(sql);
    return [null, { affectedRows: del[0].affectedRows }];
  }
  processResponse(queryObject, response, data) {
    const action = 'processResponse';
    let message;
    const containerName = queryObject.container || 'recordSet';
    if (!data || data.length === 0) {
      message = 'La consulta No retorno información';
      this.logger.log({ level: 'silly', label, action, message });
      const errorObj = this.errors.get(modErrs.common.notInfoQuery, message);
      return [errorObj, null];
    }
    if (queryObject.queryType === SELECTONE) {
      response[containerName] = Array.isArray(data) ? data[0] : data;
    } else if (Array.isArray(data)) {
      response[containerName] = data;
    } else {
      response[containerName] = [data];
    }
    message = `Respuesta de la consulta: ${JSON.stringify(response)}`;
    this.logger.log({ level: 'silly', label, action, message });
    return [null, response];
  }
}
module.exports = DatabasePool;