syp-model
Version:
Simple yet powerful promise-based database model for Node.js
475 lines (474 loc) • 17.8 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const mysql = require("mysql");
const mylogger_1 = require("./myutil/mylogger");
let logger = new mylogger_1.default(true);
class Model {
constructor() {
this.fValues = {};
this.whereArr = {};
this.likeArr = {}; // values are wildcards indicating wether/not wildcards should be applied to the corresponding value in whereArr.
this.orderBy = [];
// Do nothing for factory generated models
// because initializations will be taken care of
// by the factory method
if (this.constructor.name == "Model") {
}
else {
this.className = this.constructor.name;
this.initialize();
}
}
static config(connConf, modelConf) {
Model.connConf = connConf;
Model.modelConf = modelConf;
}
initialize() {
/* Make sure that model configuration exists */
// if Model.config() was not called OR was not provided model conf:
if (!Model.modelConf) {
//logger.logln(`process.env['sypmodel-model-conf-file'] = ${process.env['sypmodel-model-conf-file']}`)
// if process environment contains a model file, use that
if (process.env.hasOwnProperty('sypmodel-model-conf-file')) {
Model.modelConf = require(process.env['sypmodel-model-conf-file']);
}
else {
let errMsg = `Model configurations are not provided.`;
logger.logln(errMsg);
throw new Error(errMsg);
}
}
if (!Model.modelConf.hasOwnProperty(this.className)) {
let errMsg = `Unknown model ${this.className}!`;
logger.logln(errMsg);
throw new Error(errMsg);
}
this.tablename = Model.modelConf[this.className]['tablename'];
this.fields = Object.keys(Model.modelConf[this.className]['columns']);
this.fValues = {};
this.whereArr = {};
}
static factory(className) {
let m = new Model();
m.className = className;
m.initialize();
return m;
}
static emptyModel() {
return new Model();
}
assign(property, value) {
if (this.fields.indexOf(property) > -1)
this.fValues[property] = value;
else
throw new Error(`Property ${property} does not exist!`);
// allow coalescing
return this;
}
assignAll(properties) {
for (let p in properties) {
if (!properties.hasOwnProperty(p))
continue;
if (this.fields.indexOf(p) > -1)
this.fValues[p] = properties[p];
else
throw new Error(`Property ${p} does not exist!`);
}
// allow coalescing
return this;
}
get(propertyName) {
return this.valueOf(propertyName);
}
valueOf(property) {
if (this.fields.indexOf(property) > -1)
return this.fValues[property];
else
throw new Error(`Property ${property} does not exist!`);
}
static execute(stmt, params) {
/* Make sure that database connection configuration exists */
// if Model.config() was not called OR was not provided database connection conf:
if (!Model.connConf) {
// if process environment contains a model file, use that
if (process.env.hasOwnProperty('sypmodel-conn-conf-file'))
Model.connConf = require(process.env['sypmodel-conn-conf-file']);
else
throw new Error(`Database connection configurations are not provided.`);
}
logger.log('Statement executed:');
logger.logln(stmt);
logger.log('Parameters:');
logger.logln(params);
return new Promise((resolve, reject) => {
let conn = mysql.createConnection(Model.connConf);
conn.connect();
// check: https://github.com/mysqljs/mysql/issues/528
let query = conn.query(stmt, params, function (error, results, fields) {
if (error) {
logger.logln(error);
conn.end();
return reject(error);
}
conn.end();
return resolve(results);
});
});
}
create() {
var cols = [];
var vals = {};
var placeholders = [];
this.fields.forEach(col => {
if (this.fValues.hasOwnProperty(col)) {
cols.push(col);
vals[col] = this.fValues[col];
placeholders.push(':' + col);
}
});
if (cols.length === 0)
throw new Error(`no value was assigned to any column`);
var stmt = ['insert into', this.tablename, 'set ?'].join(' ');
return Model.execute(stmt, vals);
}
update() {
var cols = [];
var vals = {};
var placeholders = [];
this.fields.forEach(col => {
if (col != 'id' && this.fValues.hasOwnProperty(col)) {
cols.push(col);
vals[col] = this.fValues[col];
placeholders.push(':' + col);
}
});
if (cols.length === 0)
throw new Error(`no value was assigned to any column`);
var stmt = ['update', this.tablename, 'set ? where id = ?'].join(' ');
return Model.execute(stmt, [vals, this.fValues['id']]);
}
deleteById() {
var stmt = ['delete from', this.tablename, 'where id = ?'].join(' ');
return Model.execute(stmt, this.fValues['id']);
}
deleteManyById(ids) {
if (ids.length > 0) {
let placeholders = [];
ids.forEach(element => {
placeholders.push('?');
});
let placeholderPart = "(" + placeholders.join(', ') + ")";
var stmt = ['delete from', this.tablename, 'where id in', placeholderPart].join(' ');
return Model.execute(stmt, ids);
}
else
return new Promise((resolve, reject) => {
resolve(null);
});
}
/**
* Deletes records based on the column name provided as argument.
*
* @param fieldName the name of the column which is used in the where clause of delete query.
* The table name and column value to use for deletion will be taken from the invoking object.
*
* @return promise that resolves to the result of the delete query.
*/
deleteByField(fieldName) {
var stmt = ['delete from', this.tablename, 'where', fieldName, '= ?'].join(' ');
return Model.execute(stmt, this.fValues[fieldName]);
}
/**
* Returns the object that matches the ID of the invoking model
*
* @return a promise that resolves to the the object which matches the id of the invoking model. If no record is found, the promise resolves to null.
*/
selectById() {
var stmt = ['select * from', this.tablename, 'where id = ?'].join(' ');
stmt = this.addOrderCmd(stmt);
return Model.execute(stmt, this.fValues['id']).then(result => {
if (result instanceof Array && result.length == 1) {
return Promise.resolve(result[0]);
}
else
return Promise.resolve(null);
})
.catch(err => Promise.reject(err));
}
where(wObj) {
Object.assign(this.whereArr, wObj);
return this;
}
setLikeArray(likeArr) {
this.likeArr = likeArr;
return this;
}
/**
* Sets ordering parameter
*
* @param ordering: can be a string or an array of strings or an object. \
* ordering can be a string such as 'order_no desc' OR
* it can be a string array such as ['date desc', 'order_no asc'] OR
* it can be an object such as {"date": "desc", "order_no": "asc"}
*
* @return the model itself
*/
order(ordering) {
if (this.isString(ordering)) {
this.orderBy.push(ordering);
}
else if (this.isArray(ordering)) {
ordering.forEach(o => {
this.orderBy.push(o);
});
}
else if (this.isObject(ordering)) {
let x = ordering;
for (let p in x) {
if (x.hasOwnProperty(p))
this.orderBy.push(p + ' ' + x[p]);
}
}
return this;
}
/**
* Finds records from the database
*
* @return a promise that resolves to the returned records
*/
select() {
// basic select all statement
let stmt = ['select * from', this.tablename].join(' ');
// add where clause
let wclause = [];
let wValues = [];
for (let f in this.whereArr) {
if (f in this.likeArr) {
wclause.push(f + " like ?");
// apply wildcards
if (this.likeArr[f])
wValues.push('%' + this.whereArr[f] + '%');
else // NO wildcards
wValues.push(this.whereArr[f]);
}
else if (this.whereArr[f] && this.whereArr[f].hasOwnProperty("in")) {
// in clause
// check if array was provided as the value:
if (this.whereArr[f]["in"].constructor != Array) {
let error = "wrong value for in clause";
return Promise.reject(error);
}
if (this.whereArr[f]["in"].length > 0) {
let placeholders = Array(this.whereArr[f]["in"].length).fill('?');
wclause.push(f + " in (" + placeholders + ")");
for (let key in this.whereArr[f]["in"])
wValues.push(this.whereArr[f]["in"][key]);
}
}
else {
wclause.push(f + " = ?");
wValues.push(this.whereArr[f]);
}
}
if (wclause.length > 0)
stmt = stmt + ' where ' + wclause.join(' and ');
stmt = this.addOrderCmd(stmt);
return Model.execute(stmt, wValues);
}
addOrderCmd(stmt) {
if (this.orderBy.length > 0)
stmt = stmt + ' order by ' + this.orderBy.join(',');
return stmt;
}
/**
* Insert more than one record
* valueArr: should be a 2D array of values for the columns
*/
static insertMany(modelName, columns, valueArr) {
console.log("valueArr in insertMany(): ");
console.log(valueArr);
let tablename = Model.modelConf[modelName]['tablename'];
let colStr = "(`" + columns.join("`, `") + "`)";
let phArr = [];
let flatVals = [];
let numRows = valueArr.length;
for (let i = 0; i < numRows; i++) {
let ph = Array(valueArr[i].length).fill('?');
phArr.push('(' + ph.join(',') + ')');
flatVals = flatVals.concat(valueArr[i]);
}
let stmt = "insert into " + tablename + colStr + " values " + phArr.join(",");
console.log("stmt:");
console.log(stmt);
console.log("flatvalues:");
console.log(flatVals);
return Model.execute(stmt, flatVals);
}
/**
* Promise to GET the first matching object from DB table
*/
static promiseGetOne(oInstance, keyName) {
return new Promise((resolve, reject) => {
oInstance.select()
.then(result => {
if (result.length < 1) // TODO: why should there be many?
reject({ message: "Wrong number of " + oInstance.constructor.name + "!" });
else {
let obj = {};
obj[keyName] = result[0];
return resolve(obj);
}
})
.catch(err => {
return reject(err);
});
});
}
/**
* Promise to GET all objects from DB table
* indexed by PK by defaule
*
* @param modelNameOrInstance can be either a model name or model instance. If it's a model name, an instance is created using Model.factory()
* @param indexBy how to index the objects in the returned list of objects
* @param isArrayMember whether or not an index can be associated with multiple objects
*/
static promiseGetAll(modelNameOrInstance, indexBy = 'id', isArrayMember = false) {
let oInstance;
if (modelNameOrInstance.constructor === String)
oInstance = Model.factory(modelNameOrInstance);
else
oInstance = modelNameOrInstance;
return new Promise((resolve, reject) => {
oInstance.select()
.then(result => {
let oRet = {};
let k = oInstance.tablename + '';
let resultByPK = {};
for (let i in result) {
let key = result[i][indexBy];
if (isArrayMember)
if (!resultByPK.hasOwnProperty(key))
resultByPK[key] = [];
if (isArrayMember)
resultByPK[key].push(result[i]);
else
resultByPK[key] = result[i];
}
oRet[k] = resultByPK;
resolve(oRet);
})
.catch(err => {
reject(err);
});
});
}
/**
* Promise to add all objects from DB table
*
*
* @param modelNameOrInstance can be either a model name or model instance. If it's a model name, an instance is created using Model.factory()
* @param indexBy how to index the objects in the returned list of objects
* @param isArrayMember is an index can be associated with multiple objects
*
* @returns a function, which takes an object argument and returns a promise. The promise resolves to the argument object with records added to it.
*/
static promiseAddAll(modelNameOrInstance, indexBy = 'id', isArrayMember = false) {
let oInstance;
if (modelNameOrInstance.constructor === String)
oInstance = Model.factory(modelNameOrInstance);
else
oInstance = modelNameOrInstance;
return function (pool) {
return new Promise((resolve, reject) => {
oInstance.select()
.then(result => {
let k = oInstance.tablename + '';
let resultByPK = {};
for (let i in result) {
let key = result[i][indexBy];
if (isArrayMember)
if (!resultByPK.hasOwnProperty(key))
resultByPK[key] = [];
if (isArrayMember)
resultByPK[key].push(result[i]);
else
resultByPK[key] = result[i];
}
pool[k] = resultByPK;
resolve(pool);
})
.catch(err => {
reject(err);
});
});
};
}
static promiseAddById(modelNameOrInstance, id, keyName) {
let oInstance;
if (modelNameOrInstance.constructor === String)
oInstance = Model.factory(modelNameOrInstance);
else
oInstance = modelNameOrInstance;
return function (pool) {
return new Promise((resolve, reject) => {
if (!id) {
logger.logln('Error: id is not set in promiseAddById(...)');
pool[keyName] = null;
return resolve(pool);
}
oInstance.fValues['id'] = id;
oInstance.selectById()
.then(result => {
// logger.log('(promiseAddById) result')
// logger.logln(result)
pool[keyName] = result;
return resolve(pool);
})
.catch(err => {
logger.log('Error occurred: ');
logger.logln(err);
return reject(err);
});
});
};
}
static promiseAddOne(oInstance, keyName) {
return function (pool) {
return new Promise((resolve, reject) => {
oInstance.select()
.then(result => {
if (result.length < 1) // TODO: why should there be many?
pool[keyName] = null;
else
pool[keyName] = result[0];
return resolve(pool);
})
.catch(err => {
logger.log('Error occurred: ');
logger.logln(err);
return reject(err);
});
});
};
}
// checking datatypes:
// isString (value)
// isArray (value)
// isObject (value)
// check more: https://www.webbjocke.com/javascript-check-data-types/
isString(value) {
return typeof value === 'string' || value instanceof String;
}
;
isArray(value) {
return value && typeof value === 'object' && value.constructor === Array;
}
;
isObject(value) {
return value && typeof value === 'object' && value.constructor === Object;
}
;
}
Model.connConf = null;
Model.modelConf = null;
exports.default = Model;