brest-maria
Version:
MariaDB layer with some bREST API binding
443 lines (400 loc) • 15.4 kB
JavaScript
var _ = require("lodash"),
async = require("async"),
EventEmitter = require('events').EventEmitter,
util = require("util");
/**
* Table constructor
* @param {DB} db
* @param {Object} settings
* @constructor
*/
var Table = function(db, settings) {
var self = this;
self.db = db;
self.settings = settings;
self.name = self.settings.table;
self.defaultIds = self.settings.defaultIds;
if (_.isUndefined(self.settings.queries)) self.settings.queries = {};
self.columns = {};
self.primary = [];
self.dynamic = self.settings.dynamic || [];
self.p = {};
self.db.query(util.format('SHOW COLUMNS FROM `%s`', self.name), function(err, columns){
if (err) {
console.log(err);
throw err;
}
else {
var defaultIds = [];
_.each(columns, function(column){
self.columns[column.Field] = {
isNull: (column.Null == 'YES'),
isPrimary: (column.Key == 'PRI'),
isDynamic: (self.dynamic.indexOf(column.Field) > -1)
};
if (column.Key == 'PRI') {
self.primary.push(column.Field);
defaultIds.push(util.format('`%s`.`%s` = :%s',self.name, column.Field, column.Field));
}
});
if (defaultIds.length){
self.defaultIds = defaultIds.join(' AND ');
} else {
console.log('Failed to set default id for table '+self.name);
}
//Set default filters for all table columns
var defaultFilters = {};
_.each(self.columns, function(props, column){
defaultFilters[column] = {where: util.format(" AND `%s`.`%s` = ?", self.name, column)};
defaultFilters[column+'s'] = {where: util.format(" AND `%s`.`%s` IN (?)", self.name, column)};
});
//Check for default values set in custom modules and for settings
self.filters = _.defaults(self.filters || {}, self.settings.filters);
//Mix with default filters, if latter are not overridden
self.filters = _.defaults(self.filters || {}, defaultFilters);
//If the queries are already defined in model object, we use them instead of default queries
self.queries = _.defaults(self.queries || {}, {
row: self.settings.queries.row ||
util.format("SELECT {{columns}}{{select}} " +
"FROM `%s`{{join}} " +
"WHERE {{whereClause}}{{where}} LIMIT 1", self.name),
list: self.settings.queries.list ||
util.format("SELECT {{columns}}{{select}} " +
"FROM `%s`{{join}} " +
"WHERE 1{{where}}{{order}}{{limit}}", self.name),
insert: self.settings.queries.insert ||
util.format("INSERT INTO `%s` ({{columns}}) VALUES ({{values}}){{duplicate}}", self.name),
update: self.settings.queries.update ||
util.format("UPDATE `%s` SET {{columns}} WHERE %s", self.name, self.defaultIds),
del: self.settings.queries.del ||
util.format("DELETE FROM `%s` WHERE %s", self.name, self.defaultIds),
count: self.settings.queries.count ||
util.format("SELECT COUNT(*) as cnt FROM `%s` WHERE 1 {{where}}", self.name)
});
self.updatePersistent(function(err){
if (err) console.log('ERROR: persistent fields creation failed for ', self.name, 'with error:\n',err);
});
}
});
/**
* Prevent persistent fields updates from being fired
*/
this.suspendPersistentUpdates = function(){
self.persistentUpdatesSuspended = true;
};
/**
* Make persistent updates possible again
*/
this.resumePersistentUpdates = function(callback){
self.persistentUpdatesSuspended = false;
self.updatePersistent(function(err){
if (err) console.log('ERROR: persistent fields update failed on resume for ', self.name, 'with error:\n', err);
callback(err);
});
};
/**
* Call persistent data update functions
* @param callback
*/
this.updatePersistent = function(callback){
if (self.persistent && !self.persistentUpdatesSuspended) {
console.log('UPDATING PERSISTENT ', self.name);
async.each(_.keys(self.persistent), function(key, callback){
self.persistent[key](function(err, data){
if (err) {
self.p[key] = false;
callback(err);
}
else {
self.p[key] = data;
callback();
}
});
}, callback)
}
};
this.flattenJSON = function(json, glue){
var res = glue+"(";
var parts = [];
_.each(json, function(value, key){
if (!_.isObject(value)) {
parts.push("'"+key+"','"+ self.db.esc(value)+"'");
} else {
parts.push("'"+key+"',"+self.flattenJSON(value, glue));
}
});
res += parts.join(", ") + ")";
return res;
};
/**
* Select one row as an object
* @param {int|String|Object} ids
* @param {Object} filters
* @param {Function} callback
*/
this.row = function(ids, filters, callback){
if (_.isFunction(filters)) {
callback = filters;
filters = {};
}
if (!_.isFunction(callback)) throw new Error("Callback must be a function");
var whereClause = "";
if (!_.isObject(ids)) {
var params = {};
params[self.primary[0]] = ids;
ids = params;
whereClause = self.defaultIds;
} else {
var fields = [];
_.each(_.keys(ids), function(field){
fields.push(util.format("%s = :%s", field, field))
});
whereClause = fields.join(" AND ");
}
var columns = [];
_.each(self.columns, function(column, name){
if (column.isDynamic) {
columns.push(util.format('COLUMN_JSON(`%s`.`%s`) as `%s`', self.name, name, name));
} else {
columns.push(util.format('`%s`.`%s`', self.name, name))
}
});
var sql = self.queries.row
.replace('{{columns}}', columns.join(', '))
.replace('{{whereClause}}', whereClause);
sql = self.db.injectFilters(sql, filters, self.filters);
self.db.query(sql, ids, function(err, rows){
if (err) callback(err);
else callback(err, rows[0]);
});
};
/**
* Inject sorting into the request
* @param sql
* @param filters
* @returns {*}
*/
this.injectSort = function(sql, filters){
if (filters['order']) {
var sort = filters['order'].split(',');
if (_.isArray(sort)){
var direction = 'ASC';
if (sort.indexOf('desc')>-1){
direction = 'DESC';
delete sort[sort.indexOf('desc')];
}
if (sort.length) {
sort.forEach(function(sortField, i, arr){
arr[i] = sortField.replace('.','`.`');
});
var fields = sort.join("`, `");
sql = sql.replace('{{order}}', util.format(" ORDER BY `%s` %s", fields, direction));
}
} else throw "Failed to parse order filter"
}
return sql;
};
/**
* Inject limit into the request
* @param sql
* @param filters
*/
this.injectLimit = function(sql, filters){
if (filters['limit']) {
var self = this;
var limit = filters['limit'].split(',');
if (_.isArray(limit)){
for (var i=0; i<limit.length; i++){
limit[i] = parseInt(limit[i]);
}
sql = sql.replace('{{limit}}', util.format(" LIMIT %s", limit.join(', ')))
} else throw "Failed to parse limit filter"
}
return sql;
};
/**
* Select data by query as an array of objects
* @param {Object} filters
* @param {Function} callback
*/
this.list = function(filters, callback) {
if (_.isFunction(filters)) {
callback = filters;
filters = {};
} else {
if (!_.isObject(filters)) {
console.log("WARNING. Invalid filters provided for " + self.name + ".list(). Object required");
console.log(filters);
filters = {};
}
}
if (!_.isFunction(callback)) throw new Error("Callback must be a function");
var columns = [];
_.each(self.columns, function (column, name) {
if (column.isDynamic) {
columns.push(util.format('COLUMN_JSON(`%s`.`%s`) as `%s`', self.name, name, name));
} else {
columns.push(util.format('`%s`.`%s`', self.name, name))
}
});
var sql = self.queries.list.replace('{{columns}}', columns.join(', '));
sql = self.injectLimit(sql, filters);
sql = self.injectSort(sql, filters);
sql = self.db.injectFilters(sql, filters, self.filters);
return self.db.query(sql, callback);
};
/**
* Insert
* @param {Object} data
* @param {Object} options
* @param {Function} callback
*/
this.insert = function(data, options, callback) {
if (_.isFunction(options)) {
callback = options;
options = {};
}
if (!options) options = {};
var duplicate = options.duplicate ? " ON DUPLICATE KEY UPDATE "+options.duplicate : "";
_.each(data, function(value, column){
if (!self.columns[column]) delete data[column];
if (_.isObject(value)){
if (self.columns[column].isDynamic) data[column] = self.flattenJSON(value, 'COLUMN_CREATE');
else data[column] = JSON.stringify(value);
}
});
var columns = _.keys(data).join(', ');
var names = [];
_.each(_.keys(data), function(val){
if (self.columns[val].isDynamic) {
names.push(data[val]);
delete data[val];
} else {
names.push(':' + val)
}
});
var sql = self.queries.insert
.replace('{{columns}}', columns)
.replace('{{values}}', names)
.replace('{{duplicate}}', duplicate);
var q = self.db.query(sql, data, function(err){
if (err) callback(err);
else {
callback(null, {insert: 'success'});
self.updatePersistent(function(err){
if (err) console.log('\nERROR: persistent fields update failed for ', self.name, 'with error:\n'+err);
});
}
});
};
/**
* Update
* @param data
* @param options
* @param callback
*/
this.update = function(data, options, callback) {
var values = {};
var valuesStr = [];
if (_.isFunction(options)) {
callback = options;
options = {};
}
_.each(data, function(value, column){
if (!self.columns[column]) delete data[column];
else if (!self.primary[column]) {
if (self.columns[column].isDynamic && _.isObject(value))
values[column] = self.flattenJSON(value, 'COLUMN_CREATE');
else
values[column] = data[column];
}
});
_.each(values, function(value, column){
valuesStr.push(util.format('`%s` = :%s', column, column));
});
valuesStr = valuesStr.join(', ');
var sql = self.queries.update.replace('{{columns}}', valuesStr);
return self.db.query(sql, data, function(err){
if (err) callback(err);
else {
callback(null, {update: 'success'});
self.updatePersistent(function(err){
if (err) console.log('\nERROR: persistent fields update failed for ', self.name, 'with error:\n'+err);
});
}
});
};
/**
* Simple deletion by primary ids
* @param ids
* @param callback
*/
this.del = function(ids, callback) {
if (!_.isObject(ids)) {
if (self.primary.length == 1) {
var id = {};
id[self.primary[0]] = ids;
ids = id;
} else callback({Error: "Incorrect delete id"});
}
return self.db.query(self.queries.del, ids, function(err){
if (err) callback(err);
else {
callback(null, {delete: 'success'});
self.updatePersistent(function(err){
if (err) console.log('\nERROR: persistent fields update failed for ', self.name, 'with error:\n'+err);
});
}
});
};
/**
* Return the number of records matching the request. Count all records by default
* @param filters
* @param callback
*/
this.count = function(filters, callback) {
if (_.isFunction(filters)) {
callback = filters;
filters = {};
}
var sql = self.db.injectFilters(self.queries.count, filters, self.filters);
self.db.query(sql, function(err, count){
if (err) callback(err);
else callback(null, count[0]['cnt']);
});
};
/**
* Returns true if quer
*
* @param filters
* @param callback
*/
this.exists = function(filters, callback) {
self.count(filters, function(err, count){
callback(err, count>0);
})
};
/**
* Shortcut for sql query to use in extended models
*
* @param {String} sql
* @param {Object} params
* @param {Function} callback
* @returns {Object} MariaDB query object*
*/
this.query = function(sql, params, callback) {
return self.db.query(sql, params, callback);
};
/**
* Shortcut for sql row query to use in extended models
*
* @param {String} sql
* @param {Object} params
* @param {Function} callback
* @returns {Object} MariaDB query object
*/
this.queryRow = function(sql, params, callback) {
return self.db.queryRow(sql, params, callback)
};
};
module.exports = Table;