we-core
Version:
We.js is a node.js framework for build real time applications, sites or blogs!
945 lines (803 loc) • 24.4 kB
JavaScript
/**
* We.js database module
*
* This file load default database logic with sequelize
*/
const Sequelize = require('sequelize'),
lodash = require('lodash'),
defaultsDeep = lodash.defaultsDeep,
isArray = lodash.isArray,
clone = lodash.clone,
i18n = require('../localization/i18n.js');
function Database (we) {
this.we = we;
this.env = we.env;
let db = this;
this.defaultConnection = null;
this.models = {};
this.modelsConfigs = {};
this.modelHooks = {};
this.modelClassMethods = {};
this.modelInstanceMethods = {};
this.modelHaveAliasCache = {};
this.modelsConfigs = {
t: getTranslationsModelConfigs(we)
};
this.Sequelize = Sequelize;
we.Op = Sequelize.Op;
this.projectFolder = process.cwd();
this.defaultModelDefinitionConfigs = {
dialect: 'mysql', // default mysql dialect
define: {
// table configs
timestamps: true,
createdAt: 'createdAt',
updatedAt: 'updatedAt',
deletedAt: 'deletedAt',
paranoid: false,
// enable we.js url alias for all models by default
// change this config in your model to false to disable
enableAlias: true
}
};
this.defaultClassMethods = {
/**
* Context loader, preload current request record and related data
*
* @param {Object} req express.js request
* @param {Object} res express.js response
* @param {Function} done callback
*/
contextLoader(req, res, done) {
if (!res.locals.id || !res.locals.loadCurrentRecord) return done();
return this.findOne({
where: { id: res.locals.id },
include: [{ all: true }]
})
.then(function afterLoadContextRecord (record) {
res.locals.data = record;
if (record && record.dataValues.creatorId && req.isAuthenticated()) {
// Set role owner
if (record.isOwner(req.user.id)) {
if(req.userRoleNames.indexOf('owner') == -1 ) req.userRoleNames.push('owner');
}
}
done();
})
.catch(done);
},
findById(id) {
return this.findByPk(id);
},
find() {
return this.findOne(...arguments);
}
};
this.defaultInstanceMethods = {
/**
* Default method to check if user is owner
*/
isOwner(uid) {
if (uid == this.creatorId) return true;
return false;
},
/**
* Function to run after send records in response
* Overryde this function to remove private data
*
* @return {Object}
*/
toJSON() {
return this.get();
},
getJSONAPIAttributes() {
const modelName = this.getModelName(),
attributeList = we.db.modelsConfigs[modelName].attributeList,
attributes = {};
for (let i = 0; i < attributeList.length; i++) {
attributes[ attributeList[i] ] = this.get(attributeList[i]);
}
return attributes;
},
getJSONAPIRelationships() {
const modelName = this.getModelName(),
model = we.db.models[modelName],
associationList = we.db.modelsConfigs[modelName].associationNames,
relationships = {};
for (let j = 0; j < associationList.length; j++) {
let values = this.get(associationList[j]);
if (values) {
if (isArray(values)) {
// NxN association
relationships[ associationList[j] ] = this.getJSONAPINxNRelationship(
associationList[j]
);
} else {
// 1xN association
relationships[ associationList[j] ] = {
data: {
id: this.getDataValue([associationList[j]]).id,
type: model.associations[ associationList[j] ].target.name
}
};
}
}
}
return relationships;
},
getJSONAPINxNRelationship(assocName) {
const assocs = [],
modelName = this.getModelName(),
model = we.db.models[modelName],
type = model.associations[ assocName ].target.name,
items = this.get(assocName);
for (let i = 0; i < items.length; i++) {
assocs.push({
id: items[i].id,
type: type
});
}
return { data: assocs };
},
toJSONAPI() {
const modelName = this.getModelName();
let formated = {
id: this.id,
type: modelName,
attributes: this.getJSONAPIAttributes(),
relationships: this.getJSONAPIRelationships()
};
// delete the relationships key if is empty
if (!Object.keys(formated.relationships).length) {
delete formated.relationships;
}
return formated;
},
/**
* Default function to set all associated model ids changing associations from objects array to ids array
*
* @param {Function} cb callback
*/
fetchAssociatedIds(cb) {
const modelName = this.getModelName(),
associations = db.models[modelName].associations;
for (let associationName in associations ) {
// get bellongs to from values id
if ( associations[associationName].associationType == 'BelongsTo' ) {
this.dataValues[associationName] = this.dataValues[ associations[associationName].identifier ];
} else {
we.log.verbose('db.connect:fetchAssociatedIds unknow join: ', associations);
}
}
cb();
},
/**
* Default get url path instance method
*
* @return {String} url path
*/
getUrlPath() {
if (db.modelHaveAlias( this.getModelName() )) {
return we.router.urlTo(
this.getModelName() + '.findOne', [this.id]
);
} else {
return this.getModelName() + '/' + this.id;
}
},
/**
* Get url path with suport to url alias
*
* @return {String} url path
*/
getUrlPathAlias() {
if (we.router.alias) {
// with url alias
let p = this.getUrlPath();
return ( we.router.alias.forPath(p) || p );
} else {
// without url alias
return this.getUrlPath();
}
},
/**
* return model path with request
* Try to use the getUrlPath if possible
*
* @param {Object} req express request
* @return {String} url path
*/
getPath(req) {
if (!req) throw new Error('Request is required in record.getPath()');
return req.we.router.urlTo(
this.getModelName() + '.findOne', req.paramsArray.concat([this.id])
);
},
getLink(req) {
if (!req) throw new Error('Request is required in record.getLink()');
return req.we.config.hostname + this.getPath(req);
},
/**
* Get record model name
*
* @return {String}
*/
getModelName() {
return this.constructor.name;
},
updateAttributes() {
return this.update(...arguments);
}
};
}
Database.prototype = {
/**
* Connect in database
*
* @return {object} sequelize database connection
*/
connect() {
const configs = this.getDBConnectionConfigs();
this.activeConnectionConfig = configs;
// connect with uri or with username and pass
// See: http://sequelize.readthedocs.org/en/latest/api/sequelize/
if (configs.uri) {
return new Sequelize( configs.uri, configs );
} else {
return new Sequelize( configs.database, configs.username, configs.password, configs );
}
},
/**
* Get database connection configuration
* @return {Object} database configs
*/
getDBConnectionConfigs() {
const dbC = this.we.config.database;
let configs = dbC[this.env];
if (!configs) {
this.we.log.error(`Database configuration not found for enviroment: ${this.env}`);
return this.we.exit( ()=> {
process.exit();
});
}
// set we.js core model definition configs
defaultsDeep(configs, this.defaultModelDefinitionConfigs);
// disable database logging by deffault
if (!configs || !configs.logging) {
configs.logging = false;
}
return configs;
},
/**
* we.js db define | is a alias to current sequelize connection define
*
* @param {String} name model name
* @param {object} configs model configs
* @return {Object} sequelize model
*/
define(name, definition, options) {
// suport for uuids:
if (
this.activeConnectionConfig.UUIDInAllModels &&
!definition.id
) {
definition.id = {
type: Sequelize.UUID,
primaryKey: true,
defaultValue: Sequelize.UUIDV4
};
}
let Model;
try {
Model = this.defaultConnection.define(name, definition, options);
} catch(e) {
console.log(e);
console.log('name>', name);
process.exit();
}
this.setDefinedModelClassMethods(Model, options);
this.setDefinedModelInstanceMethods(Model, options);
return Model;
},
setDefinedModelClassMethods(Model, options) {
// first set default class methods:
for (let name in this.defaultClassMethods) {
Model[name] = this.defaultClassMethods[name];
}
if (!options || !options.classMethods) return;
// this model only class methods:
for (let name in options.classMethods) {
Model[name] = options.classMethods[name];
}
},
setDefinedModelInstanceMethods(Model, options) {
if (!Model.prototype) Model.prototype = {};
// first set default instance methods:
for (let name in this.defaultInstanceMethods) {
Model.prototype[name] = this.defaultInstanceMethods[name];
}
if (!options || !options.instanceMethods) return;
// this model instance methods:
for (let name in options.instanceMethods) {
Model.prototype[name] = options.instanceMethods[name];
}
},
/**
* Load we.js core models: system
*
* @return {Object} models db.models var
*/
loadCoreModels(done) {
const db = this;
const we = this.we;
we.utils.async.parallel([
(done)=> {
// system / plugins table
db.models.plugin = db.define('plugin', {
filename: {
comment: 'plugin.js file',
type: Sequelize.STRING(1000),
allowNull: false
},
name: {
comment: 'plugin name',
type: Sequelize.STRING,
allowNull: false
},
type: {
type: Sequelize.STRING(12),
defaultValue: 'plugin',
allowNull: false
},
status: {
comment: 'status, 1 for enabled',
type: Sequelize.INTEGER,
defaultValue: 0,
allowNull: false
},
version: {
comment: 'last version of plugin models in database',
type: Sequelize.STRING(10),
defaultValue: '0.0.0',
allowNull: false
},
weight: {
comment: 'plugin weight how controll plugin load order',
type: Sequelize.INTEGER,
defaultValue: 0,
allowNull: false
},
info: {
type: Sequelize.TEXT
}
});
return db.models.plugin.sync()
.then( ()=> {
done();
return null;
})
.catch(done);
}
], done);
},
/**
* Sync all db models | create table if now exists
*
* @param {Object} cd configuration optional
* @param {Function} cb callback optional
*/
syncAllModels(cd, cb) {
if (cd && !cb) {
cb = cd;
cd = null;
}
// cd and cb is optional
if (!cb) cb = function(){ };
if (this.env == 'test' || (this.env != 'prod' && cd && cd.resetAllData)) {
this.defaultConnection.sync({force: true}).nodeify(cb);
} else {
this.defaultConnection.sync().nodeify(cb);
}
},
/**
* Set all models associations
*/
setModelAllJoins() {
let attrConfig;
for ( let modelName in this.modelsConfigs) {
for (let attributeName in this.modelsConfigs[modelName].associations) {
attrConfig = this.modelsConfigs[modelName].associations[attributeName];
// skip if are emberOnly
if (attrConfig.emberOnly) continue;
let config = {
scope: attrConfig.scope
};
config.as = attributeName;
if (attrConfig.through) {
if (typeof attrConfig.through == 'object') {
config.through = attrConfig.through;
config.through.model = this.models[attrConfig.through.model];
} else {
config.through = attrConfig.through;
}
}
if (attrConfig.onDelete) config.onDelete = attrConfig.onDelete;
if (attrConfig.onUpdate) config.onUpdate = attrConfig.onUpdate;
if (attrConfig.constraints === false) config.constraints = false;
if (attrConfig.otherKey) config.otherKey = attrConfig.otherKey;
if (attrConfig.targetKey) config.targetKey = attrConfig.targetKey;
if (attrConfig.foreignKey) config.foreignKey = attrConfig.foreignKey;
if (attrConfig.sourceKey) config.sourceKey = attrConfig.sourceKey;
try {
this.models[modelName][attrConfig.type]( this.models[attrConfig.model], config);
} catch(e) {
console.log('Error on setModelAllJoins 2: ', attrConfig.model, this.models[attrConfig.model]);
throw e;
}
}
}
},
/**
* Set model hooks from hook configuration in json model
*/
setModelHooks() {
const db = this;
let hookFNName;
let modelNames = Object.keys(db.modelsConfigs);
modelNames
.filter(mn => { return db.modelsConfigs[mn].hooks; })
.forEach(function setHook (mn) {
let hooks = db.modelsConfigs[mn].hooks, hname, i, fns;
for (hname in hooks) {
// hooks may be defined with arrays or objects
if (isArray(hooks[hname])) {
fns = hooks[hname];
} else {
fns = Object.keys(hooks[hname]);
}
// is array
for (i = 0; i < fns.length; i++) {
hookFNName = fns[i];
if (!db.modelHooks[hookFNName]) {
db.we.log.warn('db.setModelHooks: model hook function not found', hookFNName);
} else {
db.models[mn]
.addHook(hname, hookFNName+'_'+i, db.modelHooks[hookFNName] );
}
}
}
});
},
/**
* Set model class methods from classMethods configuration in json model
*/
setModelClassMethods() {
const db = this;
let fnName;
let modelNames = Object.keys(db.modelsConfigs);
modelNames
.filter((mn) => { return db.modelsConfigs[mn].classMethods; })
.forEach(function setHook (mn) {
let clName;
let cms = db.modelsConfigs[mn].classMethods;
if (!db.modelsConfigs[mn].options)
db.modelsConfigs[mn].options = {};
if (!db.modelsConfigs[mn].options.classMethods)
db.modelsConfigs[mn].options.classMethods = {};
for (clName in cms) {
fnName = cms[clName];
if (!db.modelClassMethods[fnName]) {
db.we.log.warn('db.setModelClassMethods: model classMethod function not found', fnName);
} else {
db.modelsConfigs[mn].options.classMethods[clName] = db.modelClassMethods[fnName];
}
}
});
},
/**
* Set model instance methods from instanceMethods configuration in json model
*/
setModelInstanceMethods() {
const db = this;
let fnName;
let modelNames = Object.keys(db.modelsConfigs);
modelNames
.filter(mn => { return db.modelsConfigs[mn].instanceMethods; })
.forEach(function setHook (mn) {
let clName;
let ims = db.modelsConfigs[mn].instanceMethods;
if (!db.modelsConfigs[mn].options.instanceMethods)
db.modelsConfigs[mn].options.instanceMethods = {};
for (clName in ims) {
fnName = ims[clName];
if (!db.modelInstanceMethods[fnName]) {
db.we.log.warn('db.setModelInstanceMethods: model instanceMethod function not found', fnName);
} else {
db.modelsConfigs[mn].options.instanceMethods[clName] = db.modelInstanceMethods[fnName];
}
}
});
},
/**
* Check records privacity
*
* @param {Object|Array} data records
*/
checkRecordsPrivacity(data) {
if (isArray(data)) {
for (let i = data.length - 1; i >= 0; i--) {
if (data[i].privacity) {
this.checkPrivacity(data[i]);
}
}
} else if(data && data.privacity) {
this.checkPrivacity(data);
}
},
/**
* Check records privacity fields
*
* @param {Object} data record
*/
checkPrivacity(obj) {
for (let i = obj.privacity.length - 1; i >= 0; i--) {
if (obj.privacity[i].privacity == 'private') {
delete obj.dataValues[obj.privacity[i].field];
}
if (isArray(obj[i])) {
for (let j = obj[i].length - 1; j >= 0; j--) {
if (obj[i][j].privacity && obj[i][j].dataValues) {
this.checkPrivacity(obj[i][j]);
}
}
}
}
},
/**
* Build model config for definition from JSON model config
*
* @param {Object} model
* @param {Object} we
* @return {Object}
*/
defineModelFromJson (model, we) {
return {
definition: parseModelAttributes(model.attributes, we),
associations: model.associations,
options: ( model.options || {} ),
hooks: model.hooks,
classMethods: model.classMethods,
instanceMethods: model.instanceMethods
};
},
/**
* Check database configuration and connection
*
* @param {Object} we
* @param {Function} cb callback
*/
checkDBConnection(we, cb) {
const log = this.we.log,
db = this;
// skip if is exiting ...
if (this.we.isExiting) return;
// try to connect in database for check if database configuration is right
we.db.defaultConnection
.authenticate()
.nodeify(function afterCheckConnection (err) {
if (!err) return cb(null, true); // all fine
// handle database connection error:
if (err.name == 'SequelizeAccessDeniedError') {
log.warn('Cannot connect to the database');
log.warn(`This behavior occurs if one of the following conditions is true:
1. The SQL database is not running or you need to create the database.
2. The account that is used by the project in config/local.js file does not have the required permissions to the database server.
Check the database documentation in https://wejs.org site`);
log.verbose('Error: ', err);
process.exit();
}
// connected but database dont exists
if (err.name == 'SequelizeConnectionError') {
return db.tryToCreateDB(function (e, success) {
if (e) return cb(e); // unknow error on try to create
if (!success) return cb(err); // cant create
cb(null, true); // success
});
}
// unknow error
cb(err);
});
},
/**
* Method for try to create one database on db with active connection and run db related queryes
*
* @param {Function} cb callback
*/
tryToCreateDB(cb) {
const db = this,
cfg = db.activeConnectionConfig;
switch (cfg.dialect) {
case 'mysql':
return db.createMysqlDatabase(cb);
case 'postgres':
return db.createPostgreDatabase(cb);
default:
return cb();
}
},
/**
* Get mysqli or mysql lib
*
* @return {Object} npm mysqli or mysql lib
*/
getMysqlLib() {
try {
// new improved mysql module:
return require('mysqli');
} catch(e) {
// fallback to default mysql module:
return require('mysql2');
}
},
/**
* Create one database on mysql dbs
*
* @param {Function} cb callback
*/
createMysqlDatabase(cb) {
const db = this,
cfg = db.activeConnectionConfig;
const mysql = this.getMysqlLib();
let connection, dbName;
if (cfg.uri) {
let uriParts = cfg.uri.split('/');
dbName = uriParts.pop();
let uriWithoutDB = uriParts.join('/')+ '/';
connection = mysql.createConnection( uriWithoutDB );
} else {
dbName = cfg.database;
connection = mysql.createConnection({
host : cfg.host || 'localhost',
user : cfg.username,
password : cfg.password,
port : cfg.port || 3306
});
}
connection.connect();
connection.query(`CREATE DATABASE ${dbName};`, function(err) {
if (err) {
db.we.log.warn('Unknow error on try to create mysql DB: ', err);
return cb(err);
}
connection.end();
db.we.log.info(`Database "${dbName}" created`);
cb(null, true);
});
},
/**
* Create the database in postgre database
*/
createPostgreDatabase(callback) {
const db = this,
cfg = db.activeConnectionConfig,
pg = require('pg');
let dbName = cfg.database,
username = cfg.username,
password = cfg.password,
host = cfg.host || 'localhost';
let conStringPri = 'postgres://' + username + ':' + password + '@' + host + '/postgres';
// connect to postgres db
pg.connect(conStringPri, function afterConnectWithPG(err, client) {
if (err) return callback(err);
// create the db and ignore any errors, for example if it already exists.
client.query('CREATE DATABASE ' + dbName, function afterCreateTheDB(err) {
if (err) {
db.we.log.warn('unknow error on try to create postgres DB:', err);
return callback(err);
}
db.we.log.info(`Database "${dbName}" created`);
callback(null, true);
client.end(); // close the connection
});
});
},
/**
* modelHanveAlias
* Check if model have alias
*
* @param {String} modelName
* @return {Boolean} true if have alias
*/
modelHaveAlias(modelName) {
if (typeof this.modelHaveAliasCache[modelName] != 'undefined') {
return this.modelHaveAliasCache[modelName];
}
if (
this.models[modelName] &&
this.models[modelName].options &&
this.models[modelName].options.enableAlias
) {
this.modelHaveAliasCache[modelName] = true;
return true;
}
this.modelHaveAliasCache[modelName] = false;
return false;
}
};
// -- private methods:
function parseModelAttributes (attrs, we) {
if (!attrs) return {};
let attr = {};
for (let name in attrs) {
attr[name] = clone(attrs[name]);
attr[name].type = getModelTypeFromDefinition(attrs[name], we);
}
return attr;
}
function getModelTypeFromDefinition (attr, we) {
if (attr.size) {
if (isArray(attr.size)) {
let fn = we.db.Sequelize[attr.type.toUpperCase()];
fn.apply(null, attr.size);
return fn(attr.size);
} else {
return we.db.Sequelize[attr.type.toUpperCase()](attr.size);
}
} else {
return we.db.Sequelize[attr.type.toUpperCase()];
}
}
function getTranslationsModelConfigs(we) {
const moment = we.utils.moment;
return {
definition: {
s: {
comment: 'String to translate',
type: Sequelize.STRING(2200),
},
t: {
comment: 'Translated string',
type: Sequelize.TEXT,
skipSanitizer: true
},
l: {
comment: 'Related language',
type: Sequelize.STRING(10),
},
isChanged: {
comment: 'Is localy changed?',
type: Sequelize.BOOLEAN,
defaultValue: true,
}
},
associations: {},
options: {
tableName: 't',
comments: 'Translations table',
classMethods: {
publishLocationChanges(r) {
if (r && r.l) {
if (we.systemSettings) {
let d = moment(r.updatedAt).unix();
we.plugins['we-plugin-db-system-settings']
.setConfigs({
LLUT: d
}, function(){} );
} else {
i18n.parseAndImportTraslation(r, r.l);
}
}
}
},
hooks: {
afterCreate(r) {
we.db.models.t.publishLocationChanges(r);
},
afterUpdate(r) {
we.db.models.t.publishLocationChanges(r);
}
}
}
};
}
module.exports = Database;