const async = require('async'); const path = require('path'); const Sequelize = require('sequelize'); const DB_NAME = process.env['CSOL_DB_NAME'] || 'csol'; const USERNAME = process.env['CSOL_DB_USER'] || 'root'; const PASSWORD = process.env['CSOL_DB_PASS']; const DB_HOST = process.env['CSOL_DB_HOST']; const DB_PORT = process.env['CSOL_DB_PORT']; const MODEL_PATH = process.env['CSOL_MODEL_PATH'] || path.join(__dirname, 'models'); const db = new Sequelize(DB_NAME, USERNAME, PASSWORD, { host: DB_HOST, port: DB_PORT, define: { charset: 'utf8' } }); /** * In order to get useful bi-directional relationships on models, the relationship * has to be defined on both sides: * * Learner.belongsTo(Guardian); * Guardian.hasMany(Learner); * * This automatically adds functionality to both parties, and without both * relationships being defined, half would be missing: * * .[get|set]Guardian(); * .[get|set]Learners(); * .[add|remove]Learner(); * * However, this doesn't work when models are split out into their own files, as * we end up with circular references. That is, in order for `Learner` to reference * `Guardian`, it has to `require(.../guardian)`, but `Guardian` has to do likewise * in order to reference `Learner`. * * Hence the following abstraction, which allows us to define models in their own * files, but also allows for fully-defined bi-directional relationships. So, * rather than pull in models directly by `require(.../model)`, we now go via the * database instead. * * model = require(.../db).model('Model'); */ var modelCache = {}; db.model = function(name) { // `normalizedName` is a conversion from 'some name' to 'someName' var normalized = name.replace(/(^| +)([a-z])/ig, function(match, space, character) { return character[space ? 'toUpperCase' : 'toLowerCase'](); }); // `name` is a conversion from 'some name' into 'SomeName' var name = normalized.replace(/(^| +)([a-z])/ig, function(match, space, character) { return character.toUpperCase(); }); // `key` is a conversion from 'some name' to 'somename' var key = name.toLowerCase(); if (!modelCache[key]) { console.log('Defining model:', name); var definition = require(path.join(MODEL_PATH, normalized)), properties = definition.properties, relationships = definition.relationships, setup = definition.setup; delete definition.properties; delete definition.relationships; delete definition.setup; var model = db.define(name, properties, definition); // We need to cache the model before resolving any relationships, so that it // is available to any related models that might reference it. modelCache[key] = model; if (relationships) { relationships.forEach(function(relationship) { var relatedModel = db.model(relationship.model), type = relationship.type; console.log('Establishing relationship:', name + '.' + type + '(' + relatedModel.name + ')'); delete relationship.model; delete relationship.type; model[type](relatedModel, relationship); }); } if (typeof setup === 'function') setup(model); } return modelCache[key]; } db.type = Sequelize; db.healthCheck = function(meta, cb) { var conn = require('mysql').createConnection({ host: DB_HOST, port: DB_PORT, database: DB_NAME, user: USERNAME, password: PASSWORD, }); meta.notes = 'mysql://' + USERNAME + "@" + (DB_HOST || "localhost") + ':' + (DB_PORT || '3306') + '/' + DB_NAME; conn.connect(); conn.query('SHOW TABLES', cb); conn.end(); }; const ALLOWED_ERROR_TYPES = [ 'ER_DUP_FIELDNAME', 'ER_DUP_KEYNAME' ]; db.runMigrations = function (target, migrations, callback) { async.mapSeries(migrations, function (migration, callback) { target[migration.type] .apply(target, migration.args) .complete(callback); }, function (err) { if (err && ALLOWED_ERROR_TYPES.indexOf(err.code) < 0) // There are some errors that happen because the database is // more up-to-date than migrations know, so we ignore them return callback(err); callback(); }); } module.exports = db;