zs-core-db
Version:
Servicios generales de acceso a bases de datos
453 lines (380 loc) • 12.7 kB
JavaScript
//@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,
};