UNPKG

zs-core-db

Version:

Servicios generales de acceso a bases de datos

453 lines (380 loc) 12.7 kB
//@ts-check const { raise } = require("../utils/Logging"); const _CACHE = { models: {}, entities: {} }; const DEFAULTS = { driver: "", database: "", }; const DRIVERS = { oracle: "oracle", postgres: "postgres", }; // const ModelTemplate = {}; // /** // * Field // * @typedef {Object} Field // * @property {string} [column] // * @property {string} type // * @property {boolean} [required] // * @property {any} [default] Valor por defecto (si estuviera vacio) // // * property {string} [comment] Comentarios acerca del campo // */ // /** // * Key // * @typedef {Object} Key // * @property {string} [type] // * @property {Array<string>} fields // */ // /** // * ModelSchema // * @typedef {Object} ModelSchema // * @property {string} __filename Ruta de archivo (__filename) // * @property {string} [driver] Proveedor de datos (oracle, postgres, etc) // * @property {string} [database] Base de datos // * @property {string} table Nombre de tabla // // * property {string} [comment] Comentarios acerca de la entidad // * @property {Object.<string, Field>} fields Campos de la tabla // * @property {Object.<string, Key>} keys Indices de la tabla // */ // /** // * TranspiledField // * @typedef {Field} TranspiledField // */ // /** // * TranspiledKey // * @typedef {Object} TranspiledKey // * @property {string} type // * @property {Array<string>} fields // * @property {Array<string>} columns // */ // /** // * TranspiledPrimaryKey // * @typedef {Object} TranspiledPrimaryKey // * @property {string} name // * @property {Array<string>} fields // * @property {Array<string>} columns // */ // /** // * TranspiledSQL // * @typedef {Object} TranspiledSQL // * @property {string} select // * @property {string} insert // * @property {string} update // * @property {string} delete // */ // /** // * TranspiledSchema // * @typedef {object} TranspiledSchema // * @property {string} __filename Ruta de archivo (__filename) // * @property {string} driver Proveedor de datos (oracle, postgres, etc) // * @property {string} table Nombre de tabla // * @property {string} entity Nombre de entidad // * @property {Object.<string, TranspiledField>} fields Campos de la tabla // * @property {Object.<string, TranspiledKey>} keys Indices de la tabla // * @property {TranspiledPrimaryKey} pk Clave primaria de la tabla // * @property {TranspiledSQL} sql Clave primaria de la tabla // */ const VALID_FIELD_TYPES = [ { name: "UUID", expr: /^UUID$/i, }, { name: "SEQUENCE", expr: /^SEQUENCE$/i, }, { name: "STRING(N)", expr: /STRING *\( *[1-9]\d* *\)/i, }, { name: "NUMBER(N,N)", expr: /NUMBER\( *[1-9]\d* *(, *[1-9]\d* *)?\)/i, }, { name: "DATE", expr: /^DATE$/i, }, { name: "DATETIME", expr: /^DATETIME$/i, }, { name: "AUDITDATE", expr: /^AUDITDATE$/i, }, { name: "CLOB", expr: /^CLOB$/i, }, { name: "BOOLEAN", expr: /^BOOLEAN$/i, }, ]; const VALID_KEY_TYPES = [ { name: "PK", expr: /^PK$/i }, { name: "UK", expr: /^UK$/i }, { name: "NONE", expr: /^NONE$/i }, ]; const validateEntity = (model = {}) => { const schemaKeys = [ "__filename", "driver", "database", "table", "comment", "fields", "keys", "relations", "details", "references", ]; const invalidKey = Object.keys(model).filter( (key) => !schemaKeys.find((validKey) => key === validKey) )[0]; // raise( // invalidKey, // `Propiedad '${invalidKey}' es inválida a nivel de entidad`, // ); const filename = model.__filename; !filename && raise("Propiedad '__filename' es obligatoria"); !filename.endsWith(".js") && raise(`Archivo '${filename}' debe terminar con extensión '.js'`); !model.table && raise("Propiedad 'table' es obligatoria"); !model.fields && raise("Propiedad 'fields' es obligatoria"); !model.keys && raise("Propiedad 'keys' es obligatoria"); }; const validateFields = (model = {}) => { Object.keys(model.fields).forEach((fieldName) => { const field = model.fields[fieldName]; const fldPos = `en campo '${fieldName}'`; !field.type && raise(`Propiedad 'type' es obligatoria ${fldPos}`); !field.column && raise(`Propiedad 'column' es obligatoria ${fldPos}`); !VALID_FIELD_TYPES.find((type) => type.expr.test(field.type)) && raise(`Propiedad 'type' tiene valor no permitido ${fldPos}`); }); const duplicatedColumnName = Object.values(model.fields) .map((field) => field.column) .filter((item, idx, arr) => arr.indexOf(item) !== idx)[0]; duplicatedColumnName && raise(`Nombre de columna '${duplicatedColumnName}' está duplicado`); }; const validateKeys = (model = {}) => { const modelKeys = Object.entries(model.keys).map((key) => ({ name: key[0], ...key[1], })); modelKeys.forEach((key) => { const keyPos = `en clave '${key.name}'`; key.type = key.type?.toUpperCase() ?? "NONE"; !key.name && raise(`Propiedad 'name' es obligatoria ${keyPos}`); !key.fields && raise(`Propiedad 'fields' es obligatoria ${keyPos}`); !VALID_KEY_TYPES.find((type) => type.expr.test(key.type ?? "")) && raise(`Propiedad 'type' tiene valor incorrecto ${keyPos}`); const undeclaredField = key.fields.find((field) => !model.fields[field]); undeclaredField && raise(`Campo '${undeclaredField}' no está declarado ${keyPos}`); const duplicatedKeyFieldName = key.fields.find( (item, index, array) => array.indexOf(item) !== index ); duplicatedKeyFieldName && raise(`Campo '${duplicatedKeyFieldName}' está duplicado ${keyPos}`); }); const missingPK = !modelKeys.some((key) => key.type === "PK"); missingPK && raise("Se requiere tipo PK en una de las claves"); const duplicatedPK = modelKeys.filter((key) => key.type === "PK")[1]; duplicatedPK && raise("Se permite una sola clave de tipo PK"); }; const validateRelations = (model = {}) => { Object.keys(model.relations ?? {}).forEach((relationName) => { const relation = model.relations[relationName]; const relationPos = `en la relación '${relationName}'`; // raise( // !relation.detail && !relation.related, // `Propiedades 'detail' o 'related' son obligatorias ${relationPos}`, // ); // raise( // relation.detail && !relation.detail.model, // `Propiedad 'detail.model' es obligatoria ${relationPos}`, // ); // raise( // relation.detail && !relation.detail.id, // `Propiedad 'detail.id' es obligatoria ${relationPos}`, // ); // raise( // relation.detail && !relation.detail.param, // `Propiedad 'detail.param' es obligatoria ${relationPos}`, // ); // raise( // relation.related && !relation.related.model, // `Propiedad 'related.model' es obligatoria ${relationPos}`, // ); // raise( // relation.related && !relation.related.id, // `Propiedad 'realted.id' es obligatoria ${relationPos}`, // ); // raise( // relation.related && !relation.related.param, // `Propiedad 'related.param' es obligatoria ${relationPos}`, // ); // raise( // !relation.related && relation.relatedBy, // `Si se indica propiedad 'relatedBy' entonces 'related' es obligatoria ${relationPos}`, // ); }); const duplicatedColumnName = Object.values(model.fields) .map((field) => field.column) .filter((item, idx, arr) => arr.indexOf(item) !== idx)[0]; duplicatedColumnName && raise(`Nombre de columna '${duplicatedColumnName}' está duplicado`); }; const prepare = (model = {}) => { // if (!_CACHE.entities[model.table]) { model.driver = model.driver || DEFAULTS.driver; model.database = model.database || DEFAULTS.database; validateEntity(model); validateFields(model); validateKeys(model); validateRelations(model); const transpiled = transpileModel(model); const prepared = prepareSQL(transpiled); _CACHE.entities[prepared.entity] = prepared; _CACHE.models[prepared.entity] = model; // } return _CACHE.entities[prepared.entity]; }; const transpileModel = (model = {}) => { const transpiled = JSON.parse(JSON.stringify(model)); transpiled.entity = transpiled.__filename .split("/") .reverse()[0] .split(".js")[0]; Object.keys(model.fields).forEach((fieldName) => { const field = transpiled.fields[fieldName]; field.type = field.type.toUpperCase(); if (field.type.startsWith("STR") || field.type.startsWith("NUM")) { field.maxLen = Number( field.type.split("(")[1]?.split(",")[0]?.split(")")[0] ); } if (field.type.startsWith("NUM")) { field.precision = Number( field.type.split("(")[1]?.split(",")[1]?.split(")")[0] ); } field.type = field.type.split("(")[0].trim(); }); Object.keys(model.keys).forEach((keyName) => { const fields = transpiled.fields; const key = transpiled.keys[keyName]; key.columns = key.fields.map((name = "") => fields[name].column); key.type = key.type?.toUpperCase() ?? "NONE"; }); const pkName = String( Object.keys(transpiled.keys).find( (keyName) => transpiled.keys[keyName].type === "PK" ) ); transpiled.pk = { name: pkName, fields: transpiled.keys[pkName].fields, columns: transpiled.keys[pkName].columns, }; // transpiled.hasMany = Object.fromEntries( // Object.entries(transpiled.hasMany ?? {}).map((entry) => { // return [ // entry[0], // { // ...entry[1], // detailModel: getDeclaredModels()[entry[1].detail], // relatedModel: getDeclaredModels()[entry[1].relatedTo], // }, // ]; // }), // ); return transpiled; }; const prepareSQL = (transpiled = {}) => { const prepared = { ...JSON.parse(JSON.stringify(transpiled)), sql: {} }; prepared.sql.insert = prepareInsert(transpiled); prepared.sql.update = prepareUpdate(transpiled); prepared.sql.delete = prepareDelete(transpiled); prepared.sql.select = prepareSelect(transpiled); prepared.sql.selectByPK = prepareSelectByPK(transpiled); return prepared; }; const fieldNames = (transpiled = {}) => Object.keys(transpiled.fields).filter( (fName) => transpiled.fields[fName].column ); const selectColumns = (transpiled = {}) => fieldNames(transpiled).map( (name) => `${transpiled.fields[name].column} as "${name}"` ); const prepareInsert = (transpiled = {}) => { const columns = Object.keys(transpiled.fields) .filter((fName) => transpiled.fields[fName].column) .map((fName) => `${transpiled.fields[fName].column}`); const params = Object.keys(transpiled.fields) .filter((fName) => transpiled.fields[fName].column) .map((fName) => `:${fName}`); return `insert into ${transpiled.table} (${columns}) values (${params})`; }; const prepareUpdate = (transpiled = {}) => { const columns = Object.keys(transpiled.fields) .filter((fName) => transpiled.fields[fName].column) .map((fName) => `${transpiled.fields[fName].column}=:${fName}`); return { oracle: `update ${transpiled.table} set ${columns} where rowid=:row_id`, postgres: `update ${transpiled.table} set ${columns} where ctid=:row_id`, }[transpiled.driver]; }; const prepareDelete = (transpiled = {}) => ({ oracle: `delete ${transpiled.table} where rowid=:row_id`, postgres: `delete ${transpiled.table} where ctid=:row_id`, })[transpiled.driver]; const prepareSelect = (transpiled = {}) => ({ oracle: `select rowid as "row_id", ${selectColumns(transpiled)} from ${ transpiled.table }`, postgres: `select ctid as "row_id", ${selectColumns(transpiled)} from ${ transpiled.table }`, })[transpiled.driver]; const prepareSelectByPK = (transpiled = {}) => `${prepareSelect(transpiled)} where ${keyCondition(transpiled.pk)}`; const prepareSelectsByKeys = (model = {}) => { for (let keyName in model.keys) { const key = model.keys[keyName]; const sql = `${model.sql.select} where ${keyCondition(key)}`; model.sql[`selectBy${keyName}`] = sql; } }; const keyCondition = (key = {}) => key.fields .map((fieldName, index) => `${key.columns[index]}=:${fieldName}`) .join(" and "); const getDeclaredEntities = () => Object.fromEntries( Object.entries(_CACHE.entities).map((tableName) => [ tableName[0], _CACHE.models[tableName[0]], ]) ); const getDeclaredModels = () => Object.fromEntries( Object.entries(_CACHE.models).map((tableName) => [ tableName[0], _CACHE.models[tableName[0]], ]) ); module.exports = { DEFAULTS, DRIVERS, prepare, getDeclaredEntities, getDeclaredModels, };