ksmf
Version:
Modular Microframework for create minimalistic CLI/Web application or REST API
905 lines (851 loc) • 31.7 kB
JavaScript
/**
* @author Antonio Membrides Espinosa
* @email tonykssa@gmail.com
* @date 21/05/2022
* @copyright Copyright (c) 2020-2030
* @license GPL
* @version 1.0
**/
const ksdp = require("ksdp");
const kscrip = require("kscryp");
const Utl = require("../common/Utl");
class DataService extends ksdp.integration.Dip {
/**
* @type {Object|null}
*/
helper = null;
/**
* @type {Console|null}
*/
logger = null;
constructor(cfg) {
super();
this.utl = new Utl();
this.configure(cfg);
}
/**
* @description configure action
* @param {Object} cfg
* @param {String} [cfg.modelName]
* @param {String} [cfg.modelKey]
* @param {Array} [cfg.modelKeys]
* @param {String} [cfg.modelKeyStr]
* @param {Object} [cfg.modelInclude]
* @param {String} [cfg.modelStatus]
* @param {Array} [cfg.updateOnDuplicate]
* @param {Object} [cfg.constant]
* @param {Object} [cfg.mapAttributeKey]
* @param {Object} [cfg.mapSearchKey]
* @param {Object} [cfg.mapOrderKey]
* @param {Object} [cfg.utl]
* @param {{ models?: Object; driver?: Object; manager?: Object}} [cfg.dao]
* @param {Object} [cfg.logger]
* @returns {DataService} self
*/
configure(cfg) {
this.modelName = cfg?.modelName || this.modelName || "";
this.modelKey = cfg?.modelKey || this.modelKey || "";
this.modelKeys = cfg?.modelKeys || this.modelKeys || null;
this.modelKeyStr = cfg?.modelKeyStr || this.modelKeyStr || "";
this.modelInclude = cfg?.modelInclude || this.modelInclude || null;
this.modelStatus = cfg?.modelStatus || this.modelStatus || null;
this.updateOnDuplicate = cfg?.updateOnDuplicate || this.updateOnDuplicate || null;
this.dao = cfg?.dao || this.dao || {};
this.logger = cfg?.logger || this.logger || null;
this.utl = cfg?.utl || this.utl || null;
this.mapSearchKey = cfg?.mapSearchKey || {}
this.mapAttributeKey = cfg?.mapSearchKey || {}
this.mapOrderKey = cfg?.mapSearchKey || {}
this.constant = cfg?.constant || this.constant || {
action: {
none: 0,
read: 1,
update: 2,
create: 3,
write: 4,
all: 5
},
quantity: {
all: "all",
one: "one"
},
status: {
disabled: 0,
activated: 1,
blocked: 3
}
};
return this;
}
/**
* @description get paginator options
* @param {Object} payload
* @param {Object} [options]
* @returns {Object}
*/
getPaginator(payload, options) {
let { page, size, limit, jump } = payload;
page = parseInt(page) || 1;
jump = page > 0 ? page - 1 : 0;
size = parseInt(limit) || parseInt(size) || 10;
return {
page,
size,
offset: jump * size,
limit: size
}
}
/**
* @description format the where clause
* @param {Object} payload
* @param {Object} [options]
* @returns {Object}
*/
getWhere({ where, query }, options) {
let subFilter = {};
if (typeof query === "number" || !isNaN(query)) {
let pks = this.modelKey || this.getPKs()[0] || "id"; // TODO: check this PK selection
subFilter[pks] = parseInt(query);
} else if (typeof query === "string" && this.modelKeyStr && this.hasAttr(this.modelKeyStr)) {
subFilter[this.modelKeyStr] = query;
}
let subQuery = this.getAttrs(query);
return { ...where, ...subQuery, ...subFilter };
}
/**
* @description format the include clause
* @param {Object} payload
* @param {Object} options
* @returns {Object} include
*/
getInclude({ include }, options) {
return include || this.modelInclude;
}
/**
* @description format the filters on count
* @param {Object} payload
* @param {Object} options
* @returns {Object}
*/
getCountFilter(payload, options) {
const where = this.getWhere(payload, options);
const include = this.getInclude(payload, options);
return { include, where, distinct: true };
}
/**
* @description get if it is single or multiple selection
* @param {Object} payload
* @param {Object} [payload.where]
* @param {Boolean} [payload.auto]
* @param {Number} [payload.limit]
* @param {String} [payload.quantity]
* @param {Object} opt
* @returns {Boolean}
*/
iSingle(payload, opt) {
try {
if (payload?.limit === 1) {
return true;
}
const map = payload?.where || {};
if (payload.auto || payload.auto === undefined) {
const pks = this.getPKs();
const con = this.utl.contains(pks, Object.keys(map));
if (con.length === pks.length || map[this.modelKeyStr]) {
let driver = this.getManager();
let mainKey = map[pks[0]];
return typeof mainKey === 'object' ? !!mainKey[driver?.Op?.eq] : !!mainKey;
}
}
payload.quantity = payload?.quantity?.toLocaleLowerCase() || this.constant?.quantity?.all;
return Boolean(payload.quantity === this.constant?.quantity?.one);
}
catch (_) {
return false;
}
}
/**
* @description overload action for findAll/findOne
* @param {Object} payload
* @param {Object|String|Number} [payload.query]
* @param {Array} [payload.attributes]
* @param {Object} [payload.include]
* @param {Object} [payload.where]
* @param {String} [payload.quantity]
* @param {Number} [payload.limit]
* @param {Number} [payload.page]
* @param {Number} [payload.size]
* @param {String} [payload.order]
* @param {Number} [payload.jump]
* @param {Boolean} [payload.auto]
* @param {Boolean} [payload.valid]
* @param {Object} [payload.tmp]
* @returns {Promise<any>} row
*/
async select(payload, opt) {
opt = opt || {};
try {
payload = payload || {};
payload.tmp = {};
const model = this.getModel();
const paginator = this.getPaginator(payload, opt);
const include = this.getInclude(payload, opt);
const where = this.getWhere(payload, opt) || {};
const iSingle = this.iSingle({ ...payload, where }, opt);
const action = iSingle ? "findOne" : "findAll";
const whereCount = iSingle ? {} : this.getCountFilter(payload, opt);
if (!model) {
return null;
}
if (this.modelStatus && (payload.valid === undefined || payload.valid)) {
where[this.modelStatus] = 1;
whereCount.where = whereCount.where || {};
whereCount.where[this.modelStatus] = 1;
}
const cfg = {
attributes: payload?.attributes,
offset: paginator.offset,
limit: paginator.limit,
where,
include,
};
payload.order && (cfg.order = payload.order);
const [data, total] = await Promise.all([
model[action](cfg),
iSingle ? Promise.resolve(1) : model["count"](whereCount)
]);
return !iSingle ? {
page: paginator.page,
size: paginator.size,
total,
data
} : data;
}
catch (error) {
const logger = this.getLogger();
opt.error = error;
logger?.error({
flow: opt?.flow,
src: "KsMf:DAO:" + this.modelName + ":Select",
data: payload,
error: { message: error?.message || error, stack: error?.stack },
});
return null;
}
}
/**
* @description format request payload before perform the query
* @param {Object} data
* @param {String} [action]
* @param {Object} [options]
* @param {Object} [row]
* @returns {Object}
*/
getRequest(data, action, options, row) {
return this.getAttrs(data);
}
/**
* @description format the result of the query
* @param {Object} data
* @param {String} [action]
* @param {Object} [options]
* @returns {Object}
*/
getResponse(data, action, options) {
return data;
}
/**
* @description get the object model
* @returns {Object}
*/
getModel(name = null) {
return this.dao?.models[name || this.modelName];
}
getDriver() {
this.driver = this.dao?.driver;
return this.driver;
}
getManager() {
this.manager = this.dao?.manager;
return this.manager;
}
/**
* @description get attribute list configuration
* @param {Object} [option]
* @param {String} [option.key]
* @param {String} [option.defaults]
* @param {String} [option.model]
* @returns {String|Object|Array}
*/
getAttrList(option) {
let { key = null, defaults = 'basic', model } = option || {};
let list = this.getModel(model)?.attrs || {};
return (key ? (list[key] || list[defaults]) : list) || {};
}
/**
* @description get attributes map
* @param {Object|Array} lst
* @param {Number} [mode]
* @returns {Object} attributes
*/
getAttrs(lst, mode = 0) {
if (Array.isArray(lst)) {
let tmp = [];
for (let i in lst) {
tmp[i] = this.getAttrs(lst[i], mode);
}
return tmp;
}
const model = this.getModel();
if (!model || (!lst && !mode)) return {};
if (!lst) {
return mode === 2 ? model?.tableAttributes : Object.keys(model?.tableAttributes || {});
}
let tmp = {};
for (let i in lst) {
if (model.tableAttributes[i] && lst[i] !== undefined) {
tmp[i] = lst[i];
}
}
return tmp;
}
/**
* @description verify an attribute from the model
* @param {String} key
* @param {Object} map
* @returns {Object} Attribute or null
*/
hasAttr(key, map) {
map = map || this.getAttrs(null, 2);
return map && key && map[key];
}
/**
* @description get the primary key
* @returns {Array<string>}
*/
getPKs() {
const model = this.getModel();
return (Array.isArray(this.modelKeys) && this.modelKeys) ||
Object.keys(model?.primaryKeys || {}) ||
[this.modelKey];
}
/**
* @description get the table name
* @returns {String}
*/
getTableName(name = null) {
const model = this.getModel(name);
return model.tableName;
}
/**
* @description read/update/create
* @param {Object} payload
* @param {Object} [payload.data]
* @param {Object} [payload.where]
* @param {Object} [payload.row]
* @param {Number} [payload.mode]
* @param {Boolean} [payload.strict]
* @param {Boolean} [payload.error]
* @param {Object} [payload.tmp]
* @param {String} [payload.flow]
* @param {Array} [payload.updateOnDuplicate]
* @param {Object} [payload.transaction]
* @param {Object} [opt]
* @param {String} [opt.action]
* @param {String} [opt.flow]
* @param {Object} [opt.error]
* @param {Boolean} [opt.reload]
* @returns {Promise<any>} row
*/
async save(payload, opt) {
let { data, row, mode = this.constant?.action?.read, transaction = null, error = false, strict = false } = payload || {};
opt = opt || {};
try {
payload.flow = payload.flow || opt?.flow;
payload.tmp = {};
opt.action = opt.action || "select";
const model = this.getModel();
const where = this.getWhere(payload, opt);
if (!row && this.utl?.asBoolean(where) && !Array.isArray(data)) {
row = await this.select({ ...payload, limit: 1 }, opt);
}
if (mode < this.constant?.action?.read) {
return null;
}
if (mode === this.constant?.action?.read || row && mode === this.constant?.action?.create) {
return this.getResponse(row, "read", payload);
}
const options = {};
if (transaction) {
options.transaction = transaction;
}
if (!error && mode === this.constant?.action?.create) {
options.ignoreDuplicates = true;
}
if (!error && (mode >= this.constant?.action?.write || mode === this.constant?.action?.update)) {
options.updateOnDuplicate = (Array.isArray(payload.updateOnDuplicate) && payload.updateOnDuplicate) ||
(Array.isArray(this.updateOnDuplicate) && this.updateOnDuplicate)
|| this.getPKs();
}
if (!row && (mode >= this.constant?.action?.write || mode === this.constant?.action?.create)) {
opt.action = "create";
let res = model[Array.isArray(data) ? "bulkCreate" : opt.action](this.getRequest(data, opt.action, payload), options);
return this.getResponse(await res, opt.action, payload);
}
if (row && (mode >= this.constant?.action?.write || mode === this.constant?.action?.update) && this.utl?.isDifferent(row, data)) {
opt.action = "update";
if (strict && !Array.isArray(data) && options.updateOnDuplicate) {
let tmp = {};
for (let i of options.updateOnDuplicate) {
if (data[i] !== undefined && row[i] !== data[i]) {
tmp[i] = data[i];
}
}
if (Object.keys(tmp).length === 0) {
return this.getResponse(row, opt.action, payload);
}
payload.tmp.data = data;
data = tmp;
}
if (opt?.reload || opt?.reload === undefined) {
options.returning = true;
options.include = this.getInclude(payload, opt);
}
let res = Array.isArray(data) ?
await model.bulkCreate(this.getRequest(data, opt.action, payload, row), options) :
await row.update(this.getRequest(data, opt.action, payload, row), options);
options.returning && options.include && (res = await res.reload({ include: options.include }));
return this.getResponse(res, opt.action, payload);
}
return this.getResponse(row, opt.action, payload);
} catch (error) {
const logger = this.getLogger();
logger?.error({
flow: opt?.flow,
src: "KsMf:DAO:" + this.modelName + ":Save",
data,
error: { message: error?.message || error, stack: error?.stack },
});
if (transaction) {
await transaction.rollback();
}
opt.error = error;
return null;
}
}
/**
* @description perform a raw query
* @param {Object} payload
* @param {String} [payload.sql]
* @param {Object} [payload.params]
* @param {Object} [payload.options]
* @param {String} [payload.src]
* @param {String} [payload.flow]
* @param {Error} [payload.error]
* @returns {Promise<any>} result
*/
async query(payload = {}) {
try {
payload = payload || {};
const driver = this.getDriver();
let sql = payload.sql.replace(/:table/ig, this.getTableName());
let params = payload.params || {};
let options = {
replacements: params
};
if (/^[\n|\r|\s]*SELECT/ig.test(sql)) {
options.type = driver.QueryTypes.SELECT
}
sql = sql.replace(/\n/g, "").replace(/\s\s/g, " ");
return await driver.query(sql, payload.options || options);
}
catch (error) {
payload.error = error;
const logger = this.getLogger();
logger?.error({
flow: payload.flow,
data: payload.params,
src: "KsMf:DAO:" + this.modelName + ":" + (payload.src || "Query"),
error: { message: error?.message || error, stack: error?.stack, sql: payload.sql },
});
return null;
}
}
/**
* @description read/update/create
* @param {Object} payload
* @param {Object} payload.data
* @param {Object} payload.where
* @param {Object} payload.row
* @param {Number} payload.mode
* @param {Object} payload.transaction
* @returns {Promise<any>} row
*/
async delete(payload, opt) {
const model = this.getModel();
if (!model.destroy) {
return null;
}
try {
const row = await this.select(payload, opt);
if (row?.data) {
let where = this.getWhere(payload, opt) || {};
let res = await model.destroy({ where });
return res ? row : null;
} else {
return row && await row.destroy();
}
} catch (error) {
const logger = this.getLogger();
logger?.error({
flow: opt?.flow,
src: "KsMf:DAO:" + this.modelName + ":Delete",
data: payload,
error: { message: error?.message || error, stack: error?.stack },
});
return null;
}
}
/**
* @description insert an entity
* @param {Object} payload
* @param {Object} payload.data
* @param {Object} payload.where
* @param {Object} payload.row
* @param {Number} payload.mode
* @param {Object} payload.transaction
* @returns {Object} row
*/
create(payload, opt) {
return this.insert(payload, opt);
}
/**
* @description insert an entity
* @param {Object} payload
* @param {Object} [payload.data]
* @param {Object} [payload.where]
* @param {Object} [payload.row]
* @param {Number} [payload.mode]
* @param {Object} [payload.transaction]
* @returns {Object} row
*/
insert(payload, opt) {
payload = payload || {};
payload.mode = this.constant?.action?.create;
return this.save(payload, opt);
}
/**
* @description update an entity
* @param {Object} payload
* @param {Object} [payload.data]
* @param {Object} [payload.where]
* @param {Object} [payload.row]
* @param {Number} [payload.mode]
* @param {Object} [payload.transaction]
* @param {boolean} [payload.strict]
* @param {any[]} [payload.updateOnDuplicate]
* @param {Object} [opt]
* @returns {Promise<any>} row
*/
async update(payload, opt) {
let { data, transaction, ...options } = payload;
transaction = transaction || await this.createTransaction();
try {
let model = this.getModel();
await model.update(data, { ...options, transaction });
let result = await model.findAll({ ...options, transaction });
await transaction.commit();
return result;
} catch (error) {
opt = opt || {};
opt.error = error;
await transaction?.rollback();
const logger = this.getLogger();
logger?.error({
flow: opt.flow,
src: "KsMf:DAO:" + this.modelName + ":Update",
data: payload,
error: { message: error?.message || error, stack: error?.stack },
});
return null;
}
}
/**
* @description update an entity
* @param {Object} target
* @param {Object|String|Number} [target.query]
* @param {Array} [target.attributes]
* @param {Object} [target.include]
* @param {Array<String>} [target.exclude]
* @param {Object} [target.where]
* @param {Number} [target.limit]
* @param {Object} [payload]
* @param {Object} [payload.data]
* @param {Number} [payload.mode]
* @param {Object} [payload.transaction]
* @param {boolean} [payload.strict]
* @param {any[]} [payload.updateOnDuplicate]
* @param {Object} [option]
* @returns {Promise<Object>} row
*/
async clone(target, payload, option) {
payload = payload || {};
payload.mode = this.constant?.action?.create;
target && (target.limit = 1);
let targetRow = target ? await this.select(target, option) : null;
let contentRow = targetRow?.dataValues || targetRow;
if (!contentRow && option?.strict) {
const logger = this.getLogger();
logger?.error({
flow: option?.flow,
src: "KsMf:DAO:" + this.modelName + ":clone",
data: payload,
error: { message: "Target not found" },
});
return null;
} else {
contentRow = contentRow || {};
}
if (targetRow) {
let keys = this.getPKs()
for (let key of keys) {
delete contentRow[key];
}
if (Array.isArray(target?.exclude)) {
for (let i in target.exclude) {
delete contentRow[target.exclude[i]];
}
}
}
const content = payload?.data || payload;
payload.data = { ...contentRow, ...content };
return this.save(payload, option);
}
/**
* @description get count of data from model
* @param {Object} options
* @param {String} [options.col] specify the column on which you want to call the count() method with the col
* @param {Boolean} [options.distinct] tell Sequelize to generate and execute a COUNT( DISTINCT( lastName ) ) query
* @returns {Promise<number>}
*/
async count(options = {}) {
if (!this.dao) return null;
try {
const model = this.getModel();
return await model.count(options);
} catch (error) {
const logger = this.getLogger();
if (logger) {
logger.prefix('CRUD.Service').error(error);
}
return null;
}
}
/**
* @description get a search vector per item
* @param {Object|Array} item
* @returns {Array} vector
*/
asFilterItemVector(item) {
let [field, value, operator] = Array.isArray(item) ? item : [item?.field, item?.value, item?.operator];
operator && typeof operator === 'string' && (operator = operator.toLowerCase());
!operator && Array.isArray(value) && (operator = 'in');
value = this.asFilterItemValue(value, operator);
return [field, value, operator || 'eq'];
}
/**
* @description get the vector value
* @param {*} value
* @param {String} operator
* @returns {*} value
*/
asFilterItemValue(value, operator) {
if (value && typeof value === 'string') {
// CSV support
if (operator === 'in') {
value = value.split(',');
}
// auto like support
if (!(value[0] === '%' || value[value.length - 1] === '%') && ['like', 'ilike'].includes(operator)) {
value = '%' + value + '%';
}
}
if (operator === 'and' || operator === 'or') {
return this.asFilter(value);
}
return value;
}
/**
* @description get filters from query as JSON format
* see: https://sequelize.org/docs/v6/core-concepts/model-querying-basics/#operators
* @param {String} filter
* @returns {Object}
* @example
* filter=[["id", [78,79,80]]]
* filter=[["name", "Ant", "eq"],["age", 12]]
* filter=[{"field":"name", "value":"Ant", "operator":"eq"},["field":"age", "value":12]]
* filter={"field":"name", "value":"Ant", "operator":"eq"}
* filter={"field":"name", "value":"1,5,8", "operator":"in"}
* filter={"field":"name", "value":[1,5,8], "operator":"in"}
* filter={"value":[{"field":"name", "value":"demo1"},{"field":"group", "value":"demo1"}],"operator":"or"}
* filter={"value":[["name", "demo1"],["group", "value"]],"operator":"or"}
*/
asFilter(filter) {
try {
filter = typeof filter === "string" ? (new URLSearchParams("val=" + filter)).get('val') : filter;
let filters = kscrip.decode(filter, "json");
if (!filters) return {};
filters = Array.isArray(filters) ? filters : [filters];
const driver = this.getManager();
const model = this.getModel();
const where = {};
for (let item of filters) {
let [field, value, operator] = this.asFilterItemVector(item);
if (field) {
if (model?.tableAttributes?.hasOwnProperty(field)) {
if (driver.Op[operator]) {
where[field] = {
[driver.Op[operator]]: value
}
}
}
} else {
where[driver.Op[operator]] = value
}
}
return where;
}
catch (_) {
return {};
}
}
/**
* @description get sort obtion as order format
* see: https://sequelize.org/docs/v6/core-concepts/model-querying-basics/#ordering-and-grouping
* @param {Array} sort
* @returns {Array} order options
* @example
* ['title', 'DESC'],
* ['Task', 'createdAt', 'DESC'],
* [{model: Task, as: 'Task'}, 'createdAt', 'DESC'],
*/
asOrder(sort) {
try {
const list = kscrip.decode(sort, "json");
if (!list) return [];
const model = this.getModel();
return list?.filter && list
.filter(item => item && item[0] && model.tableAttributes.hasOwnProperty(item[0]))
.map(item => this.mapOrderKey[item[0]] ?? item);
}
catch (_) {
return null;
}
}
/**
* @description map attributes from service
* @param {String} attributes
* @returns {Object}
*/
asAttributes(attributes) {
let list = kscrip.decode(attributes, "json");
list = Array.isArray(list) ? list : [list];
return list?.map(attr => this.mapAttributeKey[attr] ?? attr);
}
/**
* @description transform to a query language
* @param {String} data
* @returns {Object}
*/
asQuery(data) {
data = (new URLSearchParams("val=" + data)).get('val');
const driver = this.getManager();
const query = kscrip.decode(data, "json");
return this.utl.transform(query, {
onKey: (key, value) => this.mapSearchKey[key] ?? driver.Op[key] ?? key,
onVal: (value, key) => (key === "in" && typeof value === "string") ? value.split(",") : value
});
}
/**
* @description Get Logger Object
* @returns {Object} Logger
*/
getLogger() {
if (this.logger) {
return this.logger;
}
if (this.helper) {
return this.helper.get('logger');
}
}
/**
* @description Extract hotkeys from request parameters
* @param {Object} payload
* @returns {import("../types").TSearchOption}
*/
extract(payload) {
const req = { ...payload };
const res = {};
if (req.page || req.offset) {
res.page = req.offset;
res.page = res.page ?? req.page;
delete req["page"];
delete req["offset"];
}
if (req.size) {
res.size = req.size;
delete req["size"];
}
if (req.limit) {
res.limit = parseInt(req.limit);
delete req["limit"];
}
if (req.filter) {
if (Array.isArray(req.filter)) {
for (let item of req.filter) {
let filterValue = this.asFilter(item);
res.where = res.where ? { ...res.where, ...filterValue } : filterValue;
}
} else {
let filterValue = this.asFilter(req.filter);
res.where = res.where ? { ...res.where, ...filterValue } : filterValue;
}
delete req["filter"];
}
if (req.ql) {
if (Array.isArray(req.ql)) {
for (let item of req.ql) {
let query = this.asQuery(item);
res.where = res.where ? { ...res.where, ...query } : query;
}
} else {
let query = this.asQuery(req.ql);
res.where = res.where ? { ...res.where, ...query } : query;
}
delete req["ql"];
}
if (req.attributes) {
res.attributes = this.asAttributes(req.attributes);
delete req["attributes"];
}
if (req.exclude) {
res.attributes = { exclude: this.asAttributes(req.exclude) };
delete req["exclude"];
}
if (req.order) {
res.order = this.asOrder(req.order);
delete req["order"];
}
res.query = { ...req };
return res;
}
/**
* @description Create a transaction
* @returns {Object}
*/
createTransaction(handler) {
handler = handler instanceof Function ? handler : undefined;
return this.dao.driver.transaction(handler);
}
}
module.exports = DataService;