diff --git a/lib/classes/Model.js b/lib/classes/Model.js index 7e07327..61de861 100644 --- a/lib/classes/Model.js +++ b/lib/classes/Model.js @@ -1,20 +1,10 @@ -'use strict'; - -var injector = require('injector') - , Promise = require('bluebird') - , async = require('async') - , util = require('util') - , utils = require('utils') - , path = require('path') - , underscore = require('underscore') - , inflect = require('i')() - , debuggr = require('debug')('cleverstack:models') - , moduleLdr = injector.getInstance('moduleLoader') - , Class = injector.getInstance('Class') - , Validator = require(path.resolve(path.join(__dirname, 'Validator'))) - , modelUtils = utils.modelUtils - , defineProp = utils.helpers.defineProperty - , models = {} +var injector = require('injector') + , utils = require('utils') + , Class = injector.getInstance('Class') + , modelUtils = utils.model + , helpers = modelUtils.helpers + , instance = modelUtils.instance + , models = {} , Model; /** @@ -34,545 +24,33 @@ Model = Class.extend( * @lends Model */ { - /** - * Default Options, defined as properties of the Classes default options Object Literal. - * @see http://cleverstack.io/documentation/backend/models/#options - * - * @property {String} type=ORM the default model type, either 'ORM' or 'ODM' - * @property {String} dbName=false the name of the database table - * @property {String} engine=false the database engine to use for this model (ORM ONLY) - * @property {String} charset=false the database charset to use for this model (ORM ONLY) - * @property {String} comment=false the database comment to use for this model (ORM ONLY) - * @property {String} collate=false the database collate to use for this model (ORM ONLY) - * @property {Object} indexes=false custom definition of indexes for this model - * @property {String} createdAt=createdAt for use with the timeStampable behaviour - * @property {String} updatedAt=updatedAt for use with the timeStampable behaviour - * @property {String} deletedAt=deletedAt for use with the softDeleteable behaviour - * @property {Boolean} underscored=false the database underscored to use for this model - * @property {Boolean} versionable=false the versionable behaviour - * @property {Boolean} freezeDbName=false if set to true your models tableName(dbName) won't be plural or camelized - * @property {Boolean} timeStampable=true the timeStampable behaviour - * @property {Boolean} softDeleteable=true the softDeleteable behaviour - */ - defaults: { - type : 'ORM', - dbName : false, - engine : false, - charset : false, - comment : false, - collate : false, - indexes : false, - createdAt : 'createdAt', - updatedAt : 'updatedAt', - deletedAt : 'deletedAt', - underscored : false, - versionable : false, - freezeDbName : false, - timeStampable : true, - softDeleteable : true - }, - - /** - * Field Types you can use when Native JavaScript Types simply don't cut it... - * @see http://cleverstack.io/documentation/backend/models/#field-types - * - * @property {ENUM} ENUM custom type - * @property {TINYINT} TINYINT custom type - * @property {BIGINT} BIGINT custom type - * @property {FLOAT} FLOAT custom type - * @property {DECIMAL} DECIMAL custom type - * @property {TEXT} TEXT custom type - */ - Types : utils.modelTypes, - - /** - * Helper function to setup an association where SourceModel.hasOne(TargetModel), with optional options for the association. - * @see http://cleverstack.io/documentation/backend/models/#assocations - * - * @function Model.hasOne - * @param {Model} targetModel the TargetModel that this SourceModel belongsTo. - * @param {Array} options the optional options for this association - * @returns {Object} the association object - */ - hasOne : modelUtils.hasOne, - - /** - * Helper function to setup an association where SourceModel.hasMany(TargetModel), with optional options for the association. - * @see http://cleverstack.io/documentation/backend/models/#assocations - * - * @function Model.hasMany - * @param {Model} targetModel the TargetModel that this SourceModel belongsTo. - * @param {Array} options the optional options for this association - * @returns {Object} the association object - */ - hasMany : modelUtils.hasMany, - - /** - * Helper function to setup an association where SourceModel.belongsTo(TargetModel), with optional options for the association. - * @see http://cleverstack.io/documentation/backend/models/#assocations - * - * @function Model.belongsTo - * @param {Model} targetModel the TargetModel that this SourceModel belongsTo. - * @param {Array} options the optional options for this association - * @returns {Object} the association object - */ - belongsTo : modelUtils.belongsTo, - - /** - * The Validator Class, used to validate Instance Fields. - * @see http://cleverstack.io/documentation/backend/models/#validation - */ - validator : Validator, - - /** - * Life Cycle Event Types, An Array listing of all available Events. - * @see http://cleverstack.io/documentation/backend/models/#events - */ - eventNames : modelUtils.eventNames, - - /** - * Creates a new Model that extends from this one - * @see http://cleverstack.io/documentation/backend/models/#definition - * - * @todo refactor this out, and allow extend to be called on existing models, to put partial schema's in different databases - * - * @override - * @param {String} tableName optionally define the name of the table/collection - * @param {Object} Static={} Class Static - * @param {Object} Proto Class Prototype - * @return {Model} - */ - extend: function() { - var extendingArgs = [].slice.call(arguments) - , modelName = (typeof extendingArgs[0] === 'string') ? extendingArgs.shift() : false - , Static = (extendingArgs.length === 2) ? extendingArgs.shift() : {} - , Proto = extendingArgs.shift() - , modelType = ( Static.type !== undefined ? Static.type : this.defaults.type ).toUpperCase() - , moduleName = 'clever-' + modelType.toLowerCase() - , model = null - , debug = null - , allowedOptions = underscore.without(Object.keys(this.defaults), 'type'); - - extendingArgs = [Static, Proto]; - - if (!modelName) { - if ((modelName = utils.helpers.getClassName(4)) !== false) { - modelName = modelName.replace('Model', ''); - } else { - throw new Error('Unable to determine model name.'); - } - } - - if (models[modelName] !== undefined) { - debuggr(modelName + 'Model: Returning model class from the cache...'); - return models[modelName]; - } - - debuggr(modelName + 'Model: Defining model, checking to see if the driver (' + modelType + ') is installed and enabled...'); - if (moduleLdr.moduleIsEnabled(moduleName) !== true) { - throw new Error(['To use type', modelType, 'on your', modelName, 'model you need to enable the', moduleName, 'module!'].join(' ')); - } else { - defineProp(Static, 'driver', { value: injector.getInstance(inflect.camelize(moduleName.replace(/-/g, '_'), false)) }); - } - - debug = require('debug')('cleverstack:models:'+modelName); - - debug('Setting up options, behaviours and properties...'); - - Static.type = modelType; - Static.modelName = modelName; - Static.fields = {}; - Static.aliases = []; - Static.primaryKey = false; - Static.primaryKeys = []; - Static.hasPrimaryKey = false; - Static.singlePrimaryKey = false; - - allowedOptions.forEach(Model.callback(function(optionName) { - Static[optionName] = Static[optionName] !== undefined ? Static[optionName] : this.defaults[optionName]; - })); - - debug('Checking for defined getters and setters...'); - - Static.getters = Proto.getters !== undefined ? Proto.getters : {}; - Static.setters = Proto.setters !== undefined ? Proto.setters : {}; - - delete Proto.getters; - delete Proto.setters; - - debug('Defining Fields...'); - Object.keys(Proto).forEach(this.callback(utils.modelUtils.getSchemaFromProto, Proto, Static)); - - debug('Defining models this.debug() helper...'); - Proto.debug = Static.debug = debug; - Proto.debug.enabled = Static.debug.enabled = Static.driver.debug.enabled; - - debug('Defining timeStampable behaviour schema fields...'); - utils.modelUtils.setupTimeStampable.apply(this, [Static]); - - debug('Defining softDeleteable behaviour schema fields...'); - utils.modelUtils.setupSoftDeleteable.apply(this, [Static]); - - debug('Generating native model using driver.parseModelSchema()...'); - Static.entity = Static.driver.parseModelSchema(Static, Proto); - - Proto.setup = utils[modelType.toLowerCase() + 'Utils'].setup; - - debug('Creating model class...'); - model = this._super.apply(this, extendingArgs); - - // Lock things down! - ['entity', 'defaults', 'connection', 'type', 'modelName', 'fields', 'aliases','primaryKey', 'primaryKeys','hasPrimaryKey', 'hasSinglePrimaryKey'] - .forEach(function(key) { - defineProp(model, key, { value: model[key] }); - }) - defineProp(model, 'associations', { value: model.entity.associations }); - - utils.modelDynamicFinders(model) - - models[modelName] = model; - - moduleLdr.on('routesInitialized', function() { - debug( 'Parsing templated event handlers...' ); - Object.keys( Static ).forEach( function( propName ) { - if ( propName.indexOf( ' ' ) !== -1 || modelUtils.eventNames.indexOf( propName ) !== -1 ) { - var parts = propName.split( ' ' ) - , resource = parts.length === 2 ? parts.shift() : modelName + 'Model' - , eventName = parts.shift(); - - injector.getInstance( resource ).on( eventName, model.callback( propName ) ); - } - }); - }); - - return model; - }, - - /** - * Create a new model using the values provided and persist/save it to the database. - * @see http://cleverstack.io/documentation/backend/models/#creating-instances - * - * @function Model.create - * @param {Object} values The values that will be used to create a model instance - * @param {Object} queryOptions={} - * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query - * @return {Promise} - */ - create: function(values, queryOptions) { - var utilName = this.type.toLowerCase() + 'Utils'; - - queryOptions = modelUtils.normalizeQueryOptions(queryOptions); - - if (this.debug.enabled) { - this.debug(util.format('create(%s)', modelUtils.debugInspect(values))); - } - - return new Promise(function create(resolve, reject) { - async.waterfall([ - this.callback(modelUtils.isValidSchema), - this.callback(modelUtils.isNewModel, values), - this.callback(modelUtils.setDefaultValues, values), - this.callback(modelUtils.aliasFieldsForOutput, values), - this.callback(modelUtils.validateValues, this.validator, values), - this.callback(modelUtils.timeStampable, values), - /** - * beforeCreate event. - * - * @event Model.beforeCreate - * @type {Object} - */ - this.callback(modelUtils.beforeEvent, 'beforeCreate', values, queryOptions), - this.callback(modelUtils.aliasAssociationsForQuery, values, false), - this.callback(modelUtils.aliasFieldsForQuery, values), - this.callback(utils[utilName].create, values, queryOptions), - /** - * afterCreate event. - * - * @event Model.afterCreate - * @type {Object} - */ - this.callback(modelUtils.afterEvent, 'afterCreate', values, queryOptions) - ], - this.callback(modelUtils.returnModels, resolve, reject)); - } - .bind(this)); - }, - - // @todo refactor - findOrCreate: function(findOptions, data, queryOptions) { - var that = this; - - return new Promise(function findOrCreate(resolve, reject) { - this - .find(findOptions, queryOptions) - .then(function(model) { - if (model === null) { - that.create(data, queryOptions) - .then(resolve) - .catch(reject); - } else { - resolve(model); - } - }.bind(this)) - .catch(function(err) { - reject(err instanceof Error ? err : new Error(err)); - }); - } - .bind(this)); - }, + Types : helpers.types, - /** - * Find and return the first model that matches the provided findOptions. - * @see http://cleverstack.io/documentation/backend/models/#finders-find - * - * @function Model.find - * @param {Object} findOptions={} - * @param {Object} findOptions.where Criteria for the query - * @param {Number} findOptions.limit Result Limiting (Paging) - * @param {Number} findOptions.offset Result Offset (Paging) - * @param {Array} findOptions.include Lazy/Eager Loading including Nested - * @param {Object} queryOptions={} - * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query - * @return {Promise} - */ - find: function(findOptions, queryOptions) { - var utilName = this.type.toLowerCase() + 'Utils'; + defaults : helpers.options.defaults, + validator : helpers.validator, + eventNames : helpers.eventNames, - findOptions = modelUtils.normalizeFindOptions(findOptions); - queryOptions = modelUtils.normalizeQueryOptions(queryOptions); + hasOne : helpers.associateModel('hasOne'), + hasMany : helpers.associateModel('hasMany'), + belongsTo : helpers.associateModel('belongsTo'), - if (this.debug.enabled) { - this.debug(util.format('find(%s)', modelUtils.debugInspect(findOptions))); - } + extend : modelUtils.extend, - return new Promise(function find(resolve, reject) { - async.waterfall([ - this.callback(modelUtils.isValidSchema), - this.callback(modelUtils.ensurePrimaryKeyInWhere, findOptions), - /** - * beforeAllFindersOptions event. - * - * @event Model.beforeAllFindersOptions - * @type {Object} - */ - this.callback(modelUtils.beforeEvent, 'beforeAllFindersOptions', findOptions, queryOptions), - /** - * beforeFindOptions event. - * - * @event Model.beforeFindOptions - * @type {Object} - */ - this.callback(modelUtils.beforeEvent, 'beforeFindOptions', findOptions, queryOptions), - this.callback(modelUtils.ensureFindOptionsValid, findOptions), // needed anymore? - this.callback(modelUtils.aliasFieldsForQuery, findOptions.where), - this.callback(modelUtils.aliasAssociationsForQuery, findOptions.where, true), - this.callback(utils[utilName].softDeleteable, findOptions, queryOptions), - /** - * afterFind event. - * - * @event Model.beforeFind - * @type {Object} - */ - this.callback(modelUtils.beforeEvent, 'beforeFind', findOptions, queryOptions), - this.callback(utils[utilName].find, findOptions, queryOptions), - /** - * afterFind event. - * - * @event Model.afterFind - * @type {Object} - */ - this.callback(modelUtils.afterEvent, 'afterFind', findOptions, queryOptions) - ], - this.callback(modelUtils.returnModels, resolve, reject)); - } - .bind(this)); - }, + create : modelUtils.create, + find : modelUtils.find, + findAll : modelUtils.findAll, + findOrCreate : modelUtils.findOrCreate, + findAndUpdate : modelUtils.findAndUpdate, + update : modelUtils.update, + destroy : modelUtils.destroy, - /** - * Find and return all models that match the provided findOptions. - * @see http://cleverstack.io/documentation/backend/models/#finders-findAll - * - * @function Model.findAll - * @param {Object} findOptions={} - * @param {Object} findOptions.where Criteria for the query - * @param {Number} findOptions.limit Result Limiting (Paging) - * @param {Number} findOptions.offset Result Offset (Paging) - * @param {Array} findOptions.include Lazy/Eager Loading including Nested - * @param {Object} queryOptions={} - * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query - * @return {Promise} - */ - findAll: function(findOptions, queryOptions) { - var utilName = this.type.toLowerCase() + 'Utils'; + // @todo make it a non-enumerable property + models : models, - findOptions = modelUtils.normalizeFindOptions(findOptions); - queryOptions = modelUtils.normalizeQueryOptions(queryOptions); - - if (this.debug.enabled) { - this.debug(util.format('findAll(%s)', modelUtils.debugInspect(findOptions))); - } - - return new Promise(function findAll(resolve, reject) { - async.waterfall([ - this.callback(modelUtils.isValidSchema), - this.callback(modelUtils.beforeEvent, 'beforeAllFindersOptions', findOptions, queryOptions), - /** - * beforeFindAllOptions event. - * - * @event Model.beforeFindAllOptions - * @type {Object} - */ - this.callback(modelUtils.beforeEvent, 'beforeFindAllOptions', findOptions, queryOptions), - this.callback(modelUtils.aliasFieldsForQuery, findOptions.where), - this.callback(modelUtils.aliasAssociationsForQuery, findOptions.where, true), - this.callback(utils[utilName].softDeleteable, findOptions, queryOptions), - /** - * beforeFindAll event. - * - * @event Model.beforeFindAll - * @type {Object} - */ - this.callback(modelUtils.beforeEvent, 'beforeFindAll', findOptions, queryOptions), - this.callback(utils[utilName].findAll, findOptions, queryOptions), - /** - * afterFindAll event. - * - * @event Model.afterFindAll - * @type {Object} - */ - this.callback(modelUtils.afterEvent, 'afterFindAll', findOptions, queryOptions) - ], - this.callback(modelUtils.returnModels, resolve, reject)); - } - .bind(this)); - }, - - /** - * Update a model using the provided values, and with where criteria supplied in queryOptions. - * @see http://cleverstack.io/documentation/backend/models/#updating-or-saving-instances - * - * @function Model.update - * @param {Object} values The values that will be used to create a model instance - * @param {Object} queryOptions={} - * @param {Object} queryOptions.where Criteria to use for the UPDATE statement - * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query - * @return {Promise} - */ - update: function(values, queryOptions) { - var utilName = this.type.toLowerCase() + 'Utils'; - - values = values || {}; - queryOptions = modelUtils.normalizeFindOptions(queryOptions); - - if (this.debug.enabled) { - this.debug(util.format('update(%s) %s', modelUtils.debugInspect(values), modelUtils.debugInspect(queryOptions))); - } - - return new Promise(function update(resolve, reject) { - async.waterfall([ - this.callback(modelUtils.isValidSchema), - this.callback(modelUtils.hasValidWhere, 'update', values, queryOptions), - this.callback(modelUtils.aliasFieldsForOutput, values), - this.callback(modelUtils.aliasFieldsForOutput, queryOptions.where), - // @todo implement the validator for updating instances - // this.callback(modelUtils.validateValues, this.validator, values), - /** - * beforeUpdate event. - * - * @event Model.beforeUpdate - * @type {Object} - */ - this.callback(modelUtils.beforeEvent, 'beforeUpdate', values, queryOptions), - this.callback(modelUtils.aliasAssociationsForQuery, queryOptions.where, false), - this.callback(modelUtils.aliasFieldsForQuery, queryOptions.where), - this.callback(modelUtils.aliasAssociationsForQuery, values, false), - this.callback(modelUtils.aliasFieldsForQuery, values), - this.callback(utils[utilName].update, values, queryOptions), - - /** - * afterUpdate event. - * - * @event Model.afterUpdate - * @type {Object} - */ - this.callback(modelUtils.afterEvent, 'afterUpdate', values, queryOptions) - ], - this.callback(modelUtils.returnModels, resolve, reject)); - } - .bind(this)); - }, - - /** - * Destroy a model using the provided queryOptions's where criteria. - * @see http://cleverstack.io/documentation/backend/models/#destroying-instances - * - * @param {Object} queryOptions={} - * @param {Object} queryOptions.where Criteria to use for the UPDATE statement - * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query - * @return {Promise} - */ - destroy: function(queryOptions) { - var utilName = this.type.toLowerCase() + 'Utils'; - - queryOptions = modelUtils.normalizeFindOptions(queryOptions); - - if (this.debug.enabled) { - this.debug(util.format('destroy where %s', modelUtils.debugInspect(queryOptions ? queryOptions.where : queryOptions))); - } - - return new Promise(function destroy(resolve, reject) { - async.waterfall([ - this.callback(modelUtils.isValidSchema), - this.callback(modelUtils.hasValidWhere, 'destroy', queryOptions.where, queryOptions), - /** - * beforeDestroy event. - * - * @event Model.beforeDestroy - * @type {Object} - */ - this.callback(modelUtils.beforeEvent, 'beforeDestroy', queryOptions.where, queryOptions), - this.callback(modelUtils.aliasAssociationsForQuery, queryOptions.where, true), - this.callback(modelUtils.aliasFieldsForQuery, queryOptions.where), - this.callback(utils[utilName].destroy, queryOptions), - /** - * afterDestroy event. - * - * @event Model.afterDestroy - * @type {Object} - */ - this.callback(modelUtils.afterEvent, 'afterDestroy', queryOptions) - ], - this.callback(modelUtils.removeReferencedModel, resolve, reject)); - } - .bind(this)); - }, - - // @todo all - // @todo describe - // @todo findAndCountAll // @todo findAllJoin // @todo findOrInitialize // @todo findOrBuild - // @todo bulkCreate - // @todo aggregate // @todo build - // @todo count - // @todo min - // @todo max - - // @todo refactor - findAndUpdate: function(findOptions, data, queryOptions) { - var that = this; - - return new Promise(function(resolve, reject) { - that - .find(findOptions, queryOptions) - .then(function(model) { - return model.update(data, queryOptions).then(resolve); - }) - .catch(reject); - }); - }, // @todo refactor getDefinedModels: function() { @@ -583,179 +61,15 @@ Model = Class.extend( * @lends Model# */ { - setup: function() { - Object.keys(this.Class.getters).forEach(this.proxy('_setupProperty')); - - if (this.Class.timeStampable) { - Object.defineProperty(this, this.Class.createdAt, { - get: function() { return this.entity.createdAt }, - set: function(val) { this.entity.createdAt = val; }, - enumerable: true, - configurable: false - }); - Object.defineProperty(this, this.Class.updatedAt, { - get: function() { return this.entity.updatedAt }, - set: function(val) { this.entity.updatedAt = val; }, - enumerable: true, - configurable: false - }); - } - - if (this.Class.softDeleteable) { - this._setupProperty(this.Class.softDeleteable); - Object.defineProperty(this, this.Class.deletedAt, { - get: function() { return this.entity.deletedAt }, - set: function(val) { this.entity.deletedAt = val; }, - enumerable: true, - configurable: false - }); - } - }, - - // @todo refactor - _setupProperty: function(propName) { - Object.defineProperty(this, propName, { - get: this.proxy(this.Class.getters[propName]), - set: this.proxy(this.Class.setters[propName]), - enumerable: true, - configurable: false - }); - }, - - // @todo refactor - map: function() { - return this.entity.map.apply(this, arguments); - }, - - /** - * Update the current model using the provided values (optional, defaults to this.changed()) - * @see http://cleverstack.io/documentation/backend/models/#updating-or-saving-instances - * - * @param {Object} values=this.changed() The values that will be used to create a model instance - * @param {Object} queryOptions={} - * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query - * @return {Promise} - */ - save: function(values, queryOptions) { - var utilName = this.Class.type.toLowerCase() + 'Utils' - , omitFields; - - queryOptions = queryOptions || {}; - - return new Promise(function(resolve, reject) { - if (this.debug.enabled) { - this.debug(util.format('save(%s)', modelUtils.debugInspect(values))); - } - - if (typeof values === 'object') { - omitFields = []//.concat(this.primaryKey); - if (!!this.Class.timeStampable) { - omitFields.push('createdAt'); - omitFields.push('updatedAt'); - } - if (!!this.Class.softDeleteable) { - omitFields.push('deletedAt'); - } - - Object.keys( values ).forEach(this.callback(function( i ) { - if (omitFields.indexOf(i) === -1 && typeof this.Class.setters[i] === 'function') { - this.Class.setters[i].apply(this, [values[i]]); - } - })); - } - - values = underscore.pick(this.values, this.changed); - if (Object.keys(values).length === 0 && !queryOptions.force) { - return resolve(this); - } - - async.waterfall([ - // @todo fix validator - // this.Class.callback(modelUtils.validateValues, this.Class.validator, this), - - /** - * beforeSave event. - * - * @todo fix update/create hook, make it work with the current afterSave event - * @event Model#beforeSave - * @type {Object} - */ - this.Class.callback(modelUtils.beforeEvent, 'beforeSave', values, queryOptions), - this.callback(utils[utilName].save, values, queryOptions), - - /** - * afterSave event. - * - * @todo fix destroy hook, make it work with the current afterSave event - * @event Model#afterSave - * @type {Object} - */ - this.Class.callback(modelUtils.afterEvent, 'afterSave', values, queryOptions) - ], - this.callback(modelUtils.updateReferencedModel, resolve, reject)); - } - .bind(this)); - }, - - /** - * Destroy the current instance - * @see http://cleverstack.io/documentation/backend/models/#destroying-instances - * - * @param {Object} queryOptions={} - * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query - * @return {Promise} - */ - destroy: function(queryOptions) { - var utilName = this.Class.type.toLowerCase() + 'Utils'; - - queryOptions = queryOptions || {}; - - if (this.debug.enabled) { - this.debug('destroy(queryOptions)'); - } - - return new Promise(function(resolve, reject) { - async.waterfall([ - /** - * beforeDestroy event. - * - * @event Model#beforeDestroy - * @type {Object} - */ - this.Class.callback(modelUtils.beforeEvent, 'beforeDestroy', this, queryOptions), - this.callback(utils[utilName].destroyInstance, queryOptions), - /** - * afterDestroy event. - * - * @event Model#afterDestroy - * @type {Object} - */ - this.Class.callback(modelUtils.afterEvent, 'afterDestroy', this, queryOptions) - ], - this.callback(modelUtils.removeReferencedModel, resolve, reject)); - } - .bind(this)); - }, - - /** - * Convert the model into a JSON ready format (Object literal) - * - * @see http://cleverstack.io/documentation/backend/models/#instance-methods-toJSON - * @return {Object} - */ - toJSON: function() { - return modelUtils.toJSON.apply(this, arguments); - }, - - /** - * Special handler for util.inspect to use Model#toJSON - * - * @see http://cleverstack.io/documentation/backend/models/#instance-methods-inspect - * @return {Object} - */ - inspect: function() { - return JSON.stringify(this.toJSON(), null, ' '); - } + setup : instance.setup, + map : instance.map, + save : instance.save, + destroy : instance.destroy, + toJSON : instance.toJSON, + inspect : instance.inspect + + // @todo restore/revert + // @todo refresh/reload }); -module.exports = Model; +module.exports = Model; \ No newline at end of file diff --git a/lib/utils/bootstrapEnv.js b/lib/utils/bootstrapEnv.js index 8629700..d0c2b5e 100644 --- a/lib/utils/bootstrapEnv.js +++ b/lib/utils/bootstrapEnv.js @@ -1,14 +1,14 @@ -'use strict'; - -var injector = require('injector') - , env = null - , path = require('path') - , chalk = require('chalk') - , debug = require('debug')('cleverstack:app') - , config = require('config') - , moment = require('moment') - , Promise = require('bluebird') - , appRoot = path.resolve(path.join(__dirname, '..', '..')); +var injector = require('injector') + , env = null + , path = require('path') + , chalk = require('chalk') + , debug = require('debug')('cleverstack:app') + , config = require('config') + , express = require('express') + , moment = require('moment') + , Promise = require('bluebird') + , appRoot = path.resolve(path.join(__dirname, '..', '..')) + , packageJson = require(appRoot + '/package.json'); function logger(msg) { if (debug.enabled) { @@ -16,10 +16,22 @@ function logger(msg) { } } -function injectCommonResources() { +if (process.env.NODE_ENV !== 'PROD') { + logger('Turning longStackTraces on'); + Promise.longStackTraces(); +} + +function injectBaseCoreResources() { + injector.instance('injector', injector); + injector.instance('packageJson',packageJson); + injector.instance('express', express); injector.instance('appRoot', appRoot); injector.instance('config', config); - injector.instance('injector', injector); +} + +function injectCommonResources() { + logger('Setting up injector...'); + injector.instance('logger', logger); injector.instance('Exceptions', require('exceptions')); injector.instance('Promise', Promise); @@ -42,23 +54,14 @@ function injectClasses() { } function bootApplication() { - var packageJson = require(appRoot + '/package.json') - , express = require('express') - , app = express() + var app = express() , moduleLdr; - logger('Booting in %s mode...', chalk.yellow(process.env.NODE_ENV ? process.env.NODE_ENV : "LOCAL")); - - if (process.env.NODE_ENV !== 'PROD') { - logger('Turning longStackTraces on'); - Promise.longStackTraces(); - } - - logger('Setting up injector...') + logger('Booting in %s mode...', chalk.yellow(process.env.NODE_ENV ? process.env.NODE_ENV : 'LOCAL')); + + injectBaseCoreResources(); injectCommonResources(); - injector.instance('express', express); - injector.instance('app', app); - injector.instance('packageJson', packageJson); + injector.instance('app', app); env = { app : app, @@ -69,7 +72,7 @@ function bootApplication() { packageJson : packageJson }; - logger('Setting up module loader....') + logger('Setting up module loader....'); env.moduleLoader = moduleLdr = require(path.resolve(path.join(__dirname, '..', 'classes', 'ModuleLoader.js'))).getInstance(env); injector.instance('moduleLoader', moduleLdr); @@ -82,4 +85,4 @@ module.exports = function bootstrapEnv() { bootApplication(); } return env; -} +}; \ No newline at end of file diff --git a/lib/utils/getModulePaths.js b/lib/utils/getModulePaths.js index 20038b2..fc4255a 100644 --- a/lib/utils/getModulePaths.js +++ b/lib/utils/getModulePaths.js @@ -8,7 +8,7 @@ module.exports = function() { , args = [].slice.call(arguments); packageJson.bundledDependencies.forEach(function(name) { - var modulePath = [ 'modules', name ].concat(args).join(path.sep); + var modulePath = ['modules', name].concat(args).join(path.sep); if ((modulePath.indexOf('*') !== -1 && /^([^\*]+)(.*)/.test(modulePath) && fs.existsSync(RegExp.$1)) || fs.existsSync(modulePath)) { paths.push(modulePath); } diff --git a/lib/utils/model.js b/lib/utils/model.js new file mode 100644 index 0000000..b5c34d6 --- /dev/null +++ b/lib/utils/model.js @@ -0,0 +1,4 @@ +var path = require('path') + , model = path.join(__dirname, 'model') + path.sep; + +module.exports = require('require-folder-tree')(model); \ No newline at end of file diff --git a/lib/utils/model/aggregate.js b/lib/utils/model/aggregate.js new file mode 100644 index 0000000..70ce890 --- /dev/null +++ b/lib/utils/model/aggregate.js @@ -0,0 +1 @@ +// @todo aggregate \ No newline at end of file diff --git a/lib/utils/model/behaviours/softDeleteable.js b/lib/utils/model/behaviours/softDeleteable.js new file mode 100644 index 0000000..7a6a215 --- /dev/null +++ b/lib/utils/model/behaviours/softDeleteable.js @@ -0,0 +1,21 @@ +module.exports.setup = function softDeleteableBehaviour(Klass) { + if (Klass.softDeleteable === true) { + Klass.fields.deletedAt = { + type : Date, + columnName : Klass.deletedAt + }; + if (Klass.deletedAt !== 'deletedAt') { + Klass.aliases.push({ + key : 'deletedAt', + columnName : Klass.deletedAt + }); + } + } +}; + +module.exports.beforeFindersOptions = function beforeFindSoftDeleteableModel(findOptions, callback) { + if (findOptions[this.deletedAt] === undefined) { + findOptions[this.deletedAt] = {is: null}; + } + callback(null); +}; \ No newline at end of file diff --git a/lib/utils/model/behaviours/timeStampable.js b/lib/utils/model/behaviours/timeStampable.js new file mode 100644 index 0000000..7cf616a --- /dev/null +++ b/lib/utils/model/behaviours/timeStampable.js @@ -0,0 +1,33 @@ +module.exports.setup = function timeStampableBehaviour(Static) { + if (Static.timeStampable === true) { + Static.fields.createdAt = { + type : Date, + columnName : Static.createdAt + }; + if (Static.createdAt !== 'createdAt') { + Static.aliases.push({ + key : 'createdAt', + columnName : Static.createdAt + }); + } + + Static.fields.updatedAt = { + type : Date, + columnName : Static.updatedAt + }; + if (Static.updatedAt !== 'updatedAt') { + Static.aliases.push({ + key : 'updatedAt', + columnName : Static.updatedAt + }); + } + } +}; + +module.exports.beforeCreate = function beforeCreateTimeStampableModel(modelData, callback) { + if (this.modelType === 'ODM' && !!this.timeStampable) { + modelData[this.createdAt] = Date.now(); + modelData[this.updatedAt] = Date.now(); + } + callback(null); +}; \ No newline at end of file diff --git a/lib/utils/model/bulkCreate.js b/lib/utils/model/bulkCreate.js new file mode 100644 index 0000000..419ce1e --- /dev/null +++ b/lib/utils/model/bulkCreate.js @@ -0,0 +1 @@ +// @todo bulkCreate \ No newline at end of file diff --git a/lib/utils/model/count.js b/lib/utils/model/count.js new file mode 100644 index 0000000..516a46f --- /dev/null +++ b/lib/utils/model/count.js @@ -0,0 +1 @@ +// @todo count \ No newline at end of file diff --git a/lib/utils/model/create.js b/lib/utils/model/create.js new file mode 100644 index 0000000..5f1decd --- /dev/null +++ b/lib/utils/model/create.js @@ -0,0 +1,44 @@ +var utils = require('utils') + , async = require('async') + , Promise = require('bluebird'); + +/** + * Create a new model using the values provided and persist/save it to the database. + * @see http://cleverstack.io/documentation/backend/models/#creating-instances + * + * @function Model.create + * @param {Object} values The values that will be used to create a model instance + * @param {Object} queryOptions={} + * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query + * @return {Promise} + */ +module.exports = function createModel(values, queryOptions) { + var utilName = this.type.toLowerCase() + 'Utils' + , helpers = utils.model.helpers + , driverUtil = utils[utilName] + , timeStampable = utils.model.behaviours.timeStampable.beforeCreate; + + queryOptions = utils.model.helpers.queryOptions.normalize(queryOptions); + + if (this.debug.enabled) { + this.debug('create(%s)', utils.model.helpers.debugInspect(values)); + } + + return new Promise(function create(resolve, reject) { + async.waterfall([ + this.callback(helpers.isExtendedModel), + this.callback(helpers.isNewModel, values), + this.callback(helpers.defaultValues, values), + this.callback(helpers.alias.fields.forOutput, values), + this.callback(helpers.validator, values), + this.callback(timeStampable, values), + this.callback(helpers.events.beforeEvent, 'beforeCreate', values, queryOptions), + this.callback(helpers.alias.associations.forQuery, values, false), + this.callback(helpers.alias.fields.forQuery, values), + this.callback(driverUtil.create, values, queryOptions), + this.callback(helpers.events.afterEvent, 'afterCreate', values, queryOptions) + ], + this.callback(helpers.handleResult.returnModels, resolve, reject)); + } + .bind(this)); +}; \ No newline at end of file diff --git a/lib/utils/model/describe.js b/lib/utils/model/describe.js new file mode 100644 index 0000000..bc374c6 --- /dev/null +++ b/lib/utils/model/describe.js @@ -0,0 +1 @@ +// @todo describe \ No newline at end of file diff --git a/lib/utils/model/destroy.js b/lib/utils/model/destroy.js new file mode 100644 index 0000000..647114c --- /dev/null +++ b/lib/utils/model/destroy.js @@ -0,0 +1,38 @@ +var utils = require('utils') + , async = require('async') + , Promise = require('bluebird'); + +/** + * Destroy a model using the provided queryOptions's where criteria. + * @see http://cleverstack.io/documentation/backend/models/#destroying-instances + * + * @param {Object} queryOptions={} + * @param {Object} queryOptions.where Criteria to use for the UPDATE statement + * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query + * @return {Promise} + */ +module.exports = function destroyModels(queryOptions) { + var utilName = this.type.toLowerCase() + 'Utils' + , helpers = utils.model.helpers + , driverUtil = utils[utilName]; + + queryOptions = utils.model.helpers.queryOptions.normalize(queryOptions); + + if (this.debug.enabled) { + this.debug('destroy where %s', utils.model.helpers.debugInspect(queryOptions ? queryOptions.where : queryOptions)); + } + + return new Promise(function destroy(resolve, reject) { + async.waterfall([ + this.callback(helpers.isExtendedModel), + this.callback(helpers.where.exists, 'destroy', queryOptions.where, queryOptions), + this.callback(helpers.events.beforeEvent, 'beforeDestroy', queryOptions.where, queryOptions), + this.callback(helpers.alias.associations.forQuery, queryOptions.where, true), + this.callback(helpers.alias.fields.forQuery, queryOptions.where), + this.callback(driverUtil.destroy, queryOptions), + this.callback(helpers.events.afterEvent, 'afterDestroy', queryOptions) + ], + this.callback(helpers.handleResult.removeReferencedModel, resolve, reject)); + } + .bind(this)); +}; \ No newline at end of file diff --git a/lib/utils/model/extend.js b/lib/utils/model/extend.js new file mode 100644 index 0000000..8323a57 --- /dev/null +++ b/lib/utils/model/extend.js @@ -0,0 +1,53 @@ +var utils = require('utils') + , injector = require('injector') + , moduleLdr = injector.getInstance('moduleLoader') + , defineProp = utils.helpers.defineProperty + , debuggr = require('debug')('cleverstack:models'); + +/** + * Creates a new Model that extends from this one + * @see http://cleverstack.io/documentation/backend/models/#definition + * + * @todo refactor this out, and allow extend to be called on existing models, to put partial schema's in different databases + * + * @override + * @param {String} tableName optionally define the name of the table/collection + * @param {Object} Static={} Class Static + * @param {Object} Proto Class Prototype + * @return {Model} + */ +module.exports = function extendModel() { + var extendingArgs = [].slice.call(arguments) + , modelName = utils.model.helpers.resolveName((typeof extendingArgs[0] === 'string') ? extendingArgs.shift() : false) + , Klass = (extendingArgs.length === 2) ? extendingArgs.shift() : {} + , Proto = extendingArgs.shift() + , modelType = (Klass.type !== undefined ? Klass.type : this.defaults.type).toUpperCase() + , moduleName = 'clever-' + modelType.toLowerCase() + , model = null + , debug = null; + + // Return the cached (already built) model if we have it + if (this.models[modelName] !== undefined) { + debuggr(modelName + 'Model: Returning model class from the cache...'); + return this.models[modelName]; + } + + // Ensure that there is a driver loaded and available (clever-orm or clever-odm) + utils.model.helpers.driver(moduleLdr, Klass, modelType, moduleName, modelName, debuggr); + + // Create a unique debugger for the new model + debug = require('debug')('cleverstack:models:' + modelName); + utils.model.helpers.defineFields.apply(this, [Klass, Proto, modelName, modelType, debug]); + + Proto.setup = utils[modelType.toLowerCase() + 'Utils'].setup; + + debug('Creating model class...'); + model = this._super.apply(this, [Klass, Proto]); + + utils.model.helpers.afterExtend.apply(this, [utils, defineProp, model, Klass, modelName, debug]); + + // Cache the model + this.models[modelName] = model; + + return model; +}; \ No newline at end of file diff --git a/lib/utils/model/find.js b/lib/utils/model/find.js new file mode 100644 index 0000000..2b33057 --- /dev/null +++ b/lib/utils/model/find.js @@ -0,0 +1,49 @@ +var utils = require('utils') + , async = require('async') + , Promise = require('bluebird'); + +/** + * Find and return the first model that matches the provided findOptions. + * @see http://cleverstack.io/documentation/backend/models/#finders-find + * + * @function Model.find + * @param {Object} findOptions={} + * @param {Object} findOptions.where Criteria for the query + * @param {Number} findOptions.limit Result Limiting (Paging) + * @param {Number} findOptions.offset Result Offset (Paging) + * @param {Array} findOptions.include Lazy/Eager Loading including Nested + * @param {Object} queryOptions={} + * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query + * @return {Promise} + */ +module.exports = function findModel(findOptions, queryOptions) { + var utilName = this.type.toLowerCase() + 'Utils' + , helpers = utils.model.helpers + , driverUtil = utils[utilName] + , softDeleteable = driverUtil.softDeleteable; + + findOptions = helpers.findOptions.normalize(findOptions); + queryOptions = helpers.queryOptions.normalize(queryOptions); + + if (this.debug.enabled) { + this.debug('find(%s)', helpers.debugInspect(findOptions)); + } + + return new Promise(function find(resolve, reject) { + async.waterfall([ + this.callback(helpers.isExtendedModel), + this.callback(helpers.criteria.requirePrimaryKeys, findOptions), + this.callback(helpers.events.beforeEvent, 'beforeAllFindersOptions', findOptions, queryOptions), + this.callback(helpers.events.beforeEvent, 'beforeFindOptions', findOptions, queryOptions), + this.callback(helpers.findOptions.valid, findOptions), // @todo needed anymore? + this.callback(helpers.alias.fields.forQuery, findOptions.where), + this.callback(helpers.alias.associations.forQuery, findOptions.where, true), + this.callback(softDeleteable, findOptions, queryOptions), + this.callback(helpers.events.beforeEvent, 'beforeFind', findOptions, queryOptions), + this.callback(driverUtil.find, findOptions, queryOptions), + this.callback(helpers.events.afterEvent, 'afterFind', findOptions, queryOptions) + ], + this.callback(helpers.handleResult.returnModels, resolve, reject)); + } + .bind(this)); +}; \ No newline at end of file diff --git a/lib/utils/model/findAll.js b/lib/utils/model/findAll.js new file mode 100644 index 0000000..3ce5e58 --- /dev/null +++ b/lib/utils/model/findAll.js @@ -0,0 +1,47 @@ +var utils = require('utils') + , async = require('async') + , Promise = require('bluebird'); + +/** + * Find and return all models that match the provided findOptions. + * @see http://cleverstack.io/documentation/backend/models/#finders-findAll + * + * @function Model.findAll + * @param {Object} findOptions={} + * @param {Object} findOptions.where Criteria for the query + * @param {Number} findOptions.limit Result Limiting (Paging) + * @param {Number} findOptions.offset Result Offset (Paging) + * @param {Array} findOptions.include Lazy/Eager Loading including Nested + * @param {Object} queryOptions={} + * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query + * @return {Promise} + */ +module.exports = function findModels(findOptions, queryOptions) { + var utilName = this.type.toLowerCase() + 'Utils' + , helpers = utils.model.helpers + , driverUtil = utils[utilName] + , softDeleteable = driverUtil.softDeleteable; + + findOptions = helpers.findOptions.normalize(findOptions); + queryOptions = helpers.queryOptions.normalize(queryOptions); + + if (this.debug.enabled) { + this.debug('findAll(%s)', helpers.debugInspect(findOptions)); + } + + return new Promise(function findAll(resolve, reject) { + async.waterfall([ + this.callback(helpers.isExtendedModel), + this.callback(helpers.events.beforeEvent, 'beforeAllFindersOptions', findOptions, queryOptions), + this.callback(helpers.events.beforeEvent, 'beforeFindAllOptions', findOptions, queryOptions), + this.callback(helpers.alias.fields.forQuery, findOptions.where), + this.callback(helpers.alias.associations.forQuery, findOptions.where, true), + this.callback(softDeleteable, findOptions, queryOptions), + this.callback(helpers.events.beforeEvent, 'beforeFindAll', findOptions, queryOptions), + this.callback(driverUtil.findAll, findOptions, queryOptions), + this.callback(helpers.events.afterEvent, 'afterFindAll', findOptions, queryOptions) + ], + this.callback(helpers.handleResult.returnModels, resolve, reject)); + } + .bind(this)); +}; \ No newline at end of file diff --git a/lib/utils/model/findAndCountAll.js b/lib/utils/model/findAndCountAll.js new file mode 100644 index 0000000..0efb85d --- /dev/null +++ b/lib/utils/model/findAndCountAll.js @@ -0,0 +1 @@ +// @todo findAndCountAll \ No newline at end of file diff --git a/lib/utils/model/findAndUpdate.js b/lib/utils/model/findAndUpdate.js new file mode 100644 index 0000000..1eedf30 --- /dev/null +++ b/lib/utils/model/findAndUpdate.js @@ -0,0 +1,14 @@ +var Promise = require('bluebird'); + +// @todo refactor +module.exports = function findAndUpdateModels(findOptions, data, queryOptions) { + return new Promise(function(resolve, reject) { + this + .find(findOptions, queryOptions) + .then(function(model) { + return model.update(data, queryOptions).then(resolve); + }) + .catch(reject); + } + .bind(this)); +}; \ No newline at end of file diff --git a/lib/utils/model/findOrCreate.js b/lib/utils/model/findOrCreate.js new file mode 100644 index 0000000..03a98e5 --- /dev/null +++ b/lib/utils/model/findOrCreate.js @@ -0,0 +1,23 @@ +var Promise = require('bluebird'); + +// @todo refactor +module.exports = function findOrCreateModel(findOptions, data, queryOptions) { + return new Promise(function findOrCreate(resolve, reject) { + this + .find(findOptions, queryOptions) + .then(this.callback(function(model) { + if (model === null) { + this + .create(data, queryOptions) + .then(resolve) + .catch(reject); + } else { + resolve(model); + } + })) + .catch(function(err) { + reject(err instanceof Error ? err : new Error(err)); + }); + } + .bind(this)); +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/afterExtend.js b/lib/utils/model/helpers/afterExtend.js new file mode 100644 index 0000000..fc8d1bb --- /dev/null +++ b/lib/utils/model/helpers/afterExtend.js @@ -0,0 +1,13 @@ +module.exports = function afterExtendModel(utils, defineProp, model, Klass, modelName, debug) { + // Lock things down! + ['entity', 'defaults', 'connection', 'type', 'modelName', 'fields', 'aliases','primaryKey', 'primaryKeys','hasPrimaryKey', 'hasSinglePrimaryKey'].forEach(function(key) { + defineProp(model, key, { value: model[key] }); + }); + defineProp(model, 'associations', { value: model.entity.associations }); + + // Setup the dynamic finders + utils.model.helpers.dynamicFinders(model); + + // Bind for nestedOperations + utils.model.helpers.nestedOperations.apply(this, [Klass, modelName, model, debug]); +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/alias/associations/forOutput.js b/lib/utils/model/helpers/alias/associations/forOutput.js new file mode 100644 index 0000000..948f3fb --- /dev/null +++ b/lib/utils/model/helpers/alias/associations/forOutput.js @@ -0,0 +1,23 @@ +var util = require('util') + , utils = require('utils') + , debug = require('debug')('cleverstack:utils:model:alias:associations:forOutput'); + +module.exports = function aliasAssociationsForOutput(fields, callback) { + if (this.entity.associations || this.Class.entity.associations) { + if (debug.enabled) { + debug(util.format('aliasAssociationsForOutput(%s)', utils.model.helpers.debugInspect(Object.keys(fields)))); + } + + Object.keys(this.entity.associations || this.Class.entity.associations).forEach(function(includeName) { + var options = (this.entity.associations || this.Class.entity.associations)[includeName].options; + if (!!options.as && fields[options.foreignKey] !== undefined) { + if (!fields[options.as]) { + fields[options.as] = fields[options.foreignKey]; + } + delete fields[options.foreignKey]; + } + }.bind(this)); + } + + return callback ? callback(null) : fields; +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/alias/associations/forQuery.js b/lib/utils/model/helpers/alias/associations/forQuery.js new file mode 100644 index 0000000..6ceb286 --- /dev/null +++ b/lib/utils/model/helpers/alias/associations/forQuery.js @@ -0,0 +1,44 @@ +var util = require('util') + , utils = require('utils') + , async = require('async') + , debug = require('debug')('cleverstack:utils:model:alias:associations:forQuery'); + +function aliasAssociationForQuery(data, remove, fieldName, callback) { + var associations = this.Class ? this.Class.entity.associations : this.entity.associations + , hasAssociation = associations ? associations[fieldName] : false + , isArray = data[fieldName] instanceof Array; + + if (!!hasAssociation && data[fieldName] !== undefined && data[fieldName] !== null) { + if (debug.enabled) { + debug(util.format('aliasAssociationForQuery(%s)', utils.model.helpers.debugInspect(fieldName))); + } + + if (hasAssociation.associationType === 'BelongsTo') { + if (!isArray) { + if (data[fieldName].entity !== undefined) { + data[hasAssociation.options.foreignKey] = data[fieldName].entity[hasAssociation.foreignKeyAttribute.referencesKey]; + } else { + data[hasAssociation.options.foreignKey] = data[fieldName]; + } + } else if (!!isArray) { + data[fieldName] = data[fieldName].map(function(model) { + return model.entity !== undefined ? model.entity : model; + }); + } + + if (remove === true) { + delete data[fieldName]; + } + } + } + + return callback ? callback(null) : true; +} + +module.exports = function aliasAssociationsForQuery(data, remove, callback) { + async.each( + Object.keys(data), + this.callback(aliasAssociationForQuery, data, remove), + callback + ); +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/alias/fields/forOutput.js b/lib/utils/model/helpers/alias/fields/forOutput.js new file mode 100644 index 0000000..82dcaf7 --- /dev/null +++ b/lib/utils/model/helpers/alias/fields/forOutput.js @@ -0,0 +1,21 @@ +var util = require('util') + , utils = require('utils') + , debug = require('debug')('cleverstack:utils:model:alias:fields:forOutput'); + +module.exports = function aliasFieldsForOutput(fields, callback) { + if (debug.enabled && !fields.where) { + debug(util.format('aliasFieldsForOutput(%s)', utils.model.helpers.debugInspect(Object.keys(fields)))); + } + + (!this.Class ? this.aliases : this.Class.aliases).forEach(function(column) { + var hasAssociation = (!this.Class ? this.entity.associations : this.Class.entity.associations)[column.fieldName]; + + if (!hasAssociation && fields[column.columnName] !== undefined) { + fields[column.fieldName] = fields[column.columnName]; + delete fields[column.columnName]; + } + } + .bind(this)); + + return callback ? callback(null) : fields; +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/alias/fields/forQuery.js b/lib/utils/model/helpers/alias/fields/forQuery.js new file mode 100644 index 0000000..d34ab18 --- /dev/null +++ b/lib/utils/model/helpers/alias/fields/forQuery.js @@ -0,0 +1,27 @@ +var util = require('util') + , utils = require('utils') + , underscore = require('underscore') + , debug = require('debug')('cleverstack:utils:model:alias:fields:forQuery'); + +module.exports = function aliasFieldsForQuery(fields, callback) { + if (Object.keys(fields).length > 0) { + if (debug.enabled) { + debug(util.format('aliasFieldsForQuery(%s)', utils.model.helpers.debugInspect(fields))); + } + + Object.keys(fields).forEach(function(key) { + var val = fields[key] + , associations = !this.Class ? this.entity.associations : this.Class.entity.associations + , hasAssociation = associations ? associations[key] : false + , newKey; + + if ( !hasAssociation && !!( newKey = underscore.findWhere(!this.Class ? this.aliases : this.Class.aliases, { fieldName: key }) )) { + fields[newKey.columnName] = val; + delete fields[key]; + } + } + .bind(this)); + } + + return callback ? callback(null) : true; +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/associateModel.js b/lib/utils/model/helpers/associateModel.js new file mode 100644 index 0000000..44b8e74 --- /dev/null +++ b/lib/utils/model/helpers/associateModel.js @@ -0,0 +1,21 @@ +var utils = require('utils') + , odmUtils = utils.odmUtils; + +/** + * Helper function to setup an association where SourceModel.belongsTo(TargetModel), with optional options for the association. + * @see http://cleverstack.io/documentation/backend/models/#assocations + * + * @param {String} assocType the type of association + * @param {Model} targetModel the TargetModel that this SourceModel belongsTo. + * @param {Array} options the optional options for this association + * @returns {Object} the association object + */ +module.exports = function addAssociateModelHelper(assocType) { + return function associateModelHelper(targetModel, options) { + if (this.type === 'ODM') { + return odmUtils[assocType].apply(this, [targetModel, options]); + } else { + return this.entity[assocType].apply(this.entity, [targetModel, options]); + } + }; +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/criteria/exists.js b/lib/utils/model/helpers/criteria/exists.js new file mode 100644 index 0000000..19f50fb --- /dev/null +++ b/lib/utils/model/helpers/criteria/exists.js @@ -0,0 +1,13 @@ +var Exceptions = require('exceptions'); + +module.exports = function exists(type, values, queryOptions, callback) { + var valid = false; + + if (type === 'update') { + valid = typeof queryOptions === 'object' && queryOptions.where && Object.keys(queryOptions.where).length >= 1; + } else if (type === 'destroy') { + valid = typeof queryOptions === 'object' && queryOptions.where && queryOptions.where.id; + } + + callback(!!valid ? null : new Exceptions.InvalidData('Unable to ' + type + ' without find criteria.')); +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/criteria/requirePrimaryKeys.js b/lib/utils/model/helpers/criteria/requirePrimaryKeys.js new file mode 100644 index 0000000..6ab7954 --- /dev/null +++ b/lib/utils/model/helpers/criteria/requirePrimaryKeys.js @@ -0,0 +1,16 @@ +var Exceptions = require('exceptions'); + +module.exports = function requirePrimaryKeys(findOptions, callback) { + if (/^[0-9a-fA-F]{24}$/.test(findOptions) || !isNaN(findOptions)) { + if (this.primaryKeys.length === 1) { + var findOptionsOverride = { where: {} }; + findOptionsOverride.where[this.primaryKey] = findOptions; + findOptions = findOptionsOverride; + callback(null); + } else { + callback(new Exceptions.InvalidData('You must provide an object when using Model.find() when there are multiple primaryKeys')); + } + } else { + callback(null); + } +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/debugInspect.js b/lib/utils/model/helpers/debugInspect.js new file mode 100644 index 0000000..963e363 --- /dev/null +++ b/lib/utils/model/helpers/debugInspect.js @@ -0,0 +1,9 @@ +var util = require('util') + , inspectUtil = util.inspect; + +function debugInspect(obj) { + return inspectUtil(obj, {showHidden: false, colors: true, customInspect: true, depth: 2}).replace(/\n[\ ]+/igm, ' '); + // return inspectUtil(obj, {showHidden: false, colors: true, customInspect: true, depth: 0}).replace(/(\n[\ ]+|\{\ )/igm, '\n\ \ \ \ '); +} + +module.exports = debugInspect; \ No newline at end of file diff --git a/lib/utils/model/helpers/defaultValues.js b/lib/utils/model/helpers/defaultValues.js new file mode 100644 index 0000000..25d2421 --- /dev/null +++ b/lib/utils/model/helpers/defaultValues.js @@ -0,0 +1,26 @@ +var util = require('util') + , utils = require('utils') + , async = require('async'); + +function setDefaultValue(data, fieldName, callback) { + var field = this.fields[fieldName] + , value = data[fieldName] + , defaultValue = field['default']; + + if (field.type && value === undefined && defaultValue !== undefined) { + if (this.debug.enabled) { + this.debug(util.format('setDefaultValue(%s=%s)', fieldName, utils.model.helpers.debugInspect(defaultValue))); + } + data[fieldName] = defaultValue; + } + + callback(null); +} + +module.exports = function setDefaultValues(data, callback) { + async.each( + Object.keys(this.fields || this.Class.fields), + this.callback(setDefaultValue, data), + callback + ); +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/defineField.js b/lib/utils/model/helpers/defineField.js new file mode 100644 index 0000000..437f066 --- /dev/null +++ b/lib/utils/model/helpers/defineField.js @@ -0,0 +1,57 @@ +var inflect = require('i')(); + +module.exports = function defineField(Proto, Klass, fieldName) { + var prop = Proto[fieldName] + , columnName = !!Klass.underscored ? inflect.underscore(fieldName) : fieldName; + + if (!!prop.columnName && fieldName !== prop.columnName) { + Klass.aliases.push({ fieldName: fieldName, columnName: prop.columnName }); + columnName = prop.columnName; + } else if (!!Klass.underscored && fieldName !== columnName) { + Klass.aliases.push({ fieldName: fieldName, columnName: columnName }); + } + + if (typeof prop === 'function' && [String, Number, Boolean, Date, Buffer, this.Types.ENUM, this.Types.TINYINT, this.Types.BIGINT, this.Types.FLOAT, this.Types.DECIMAL, this.Types.TEXT].indexOf(Proto[fieldName]) === -1 && fieldName !== 'defaults') { + + // Allow definition of custom getters and setters for fields, but make sure not to include association accessor functions. + if (/^(set|get)(.*)$/.test(fieldName)) { + var getOrSet = RegExp.$1; + fieldName = RegExp.$2; + + if (fieldName !== false && Klass.fields[fieldName] !== undefined && typeof Klass.getters[fieldName] === 'function') { + Klass[(getOrSet === 'get' ? 'getters' : 'setters')][fieldName] = function() { + return prop.apply(this, arguments); + }; + } + } + } else if (fieldName !== 'defaults') { + + if (typeof Klass.fields !== 'object') { + Klass.fields = {}; + } + + if (typeof Klass.getters !== 'object') { + Klass.getters = {}; + } + + if (typeof Klass.setters !== 'object') { + Klass.setters = {}; + } + + Klass.fields[fieldName] = prop; + Klass.getters[fieldName] = function() { + if (fieldName === 'id' && Klass.type.toLowerCase() === 'odm') { + return this.entity._id; + } else { + return this.entity.get(columnName); + } + }; + Klass.setters[fieldName] = function(val) { + this.entity.set(columnName, val); + + return this; + }; + + delete Proto[fieldName]; + } +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/defineFields.js b/lib/utils/model/helpers/defineFields.js new file mode 100644 index 0000000..5e382d2 --- /dev/null +++ b/lib/utils/model/helpers/defineFields.js @@ -0,0 +1,25 @@ +var utils = require('utils'); + +module.exports = function defineModelFields(Klass, Proto, modelName, modelType, debug) { + debug('Setting up models this.debug() helper...'); + Proto.debug = Klass.debug = debug; + Proto.debug.enabled = Klass.debug.enabled = Klass.driver.debug.enabled; + + debug('Setting up options, behaviours and properties...'); + utils.model.helpers.options.setup.apply(this, [Klass, modelName, modelType, this.defaults]); + + debug('Checking for defined getters and setters...'); + utils.model.helpers.gettersAndSetters.apply(this, [Klass, Proto]); + + debug('Defining Fields...'); + Object.keys(Proto).forEach(this.callback(utils.model.helpers.defineField, Proto, Klass)); + + debug('Defaultining timeStampable behaviour schema fields...'); + utils.model.behaviours.timeStampable.setup.apply(this, [Klass]); + + debug('Defining softDeleteable behaviour schema fields...'); + utils.model.behaviours.softDeleteable.setup.apply(this, [Klass]); + + debug('Generating native model using driver.parseModelSchema()...'); + Klass.entity = Klass.driver.parseModelSchema(Klass, Proto); +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/driver.js b/lib/utils/model/helpers/driver.js new file mode 100644 index 0000000..97d00af --- /dev/null +++ b/lib/utils/model/helpers/driver.js @@ -0,0 +1,12 @@ +var utils = require('utils') + , inflect = require('i')() + , injector = require('injector'); + +module.exports = function linkDriver(moduleLdr, Klass, modelType, moduleName, modelName, debuggr) { + debuggr(modelName + 'Model: Defining model, checking to see if the driver (' + modelType + ') is installed and enabled...'); + if (moduleLdr.moduleIsEnabled(moduleName) !== true) { + throw new Error(['To use type', modelType, 'on your', modelName, 'model you need to enable the', moduleName, 'module!'].join(' ')); + } else { + utils.helpers.defineProperty(Klass, 'driver', { value: injector.getInstance(inflect.camelize(moduleName.replace(/-/g, '_'), false)) }); + } +}; \ No newline at end of file diff --git a/lib/utils/modelDynamicFinders.js b/lib/utils/model/helpers/dynamicFinders.js similarity index 93% rename from lib/utils/modelDynamicFinders.js rename to lib/utils/model/helpers/dynamicFinders.js index 0fafc06..72ebbc0 100644 --- a/lib/utils/modelDynamicFinders.js +++ b/lib/utils/model/helpers/dynamicFinders.js @@ -9,8 +9,8 @@ module.exports = function modelDynamicFindersGenerator(model) { 'not', 'notEqual', 'between', 'notBetween', 'greaterThan', 'lessThan', 'greaterThanOrEqualTo', 'lessThanOrEqualTo', 'contains', 'doesNotContain', 'contained', 'overlap' ] - , helpers = criteria.map(function(criterion) { return '$' + criterion }) - , methods = criteria.map(function(criterion) { return inflect.camelize(criterion, true) }) + , helpers = criteria.map(function(criterion) { return '$' + criterion; }) + , methods = criteria.map(function(criterion) { return inflect.camelize(criterion, true); }) , fieldNames = Object.keys(model.fields); fieldNames.forEach(function(field) { @@ -30,7 +30,7 @@ module.exports = function modelDynamicFindersGenerator(model) { if (method === 'Is') { findOptions.where[field] = val; } else { - findOptions.where[field] = {} + findOptions.where[field] = {}; findOptions.where[field][helper] = val; } @@ -40,4 +40,4 @@ module.exports = function modelDynamicFindersGenerator(model) { }); }); }); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/eventNames.js b/lib/utils/model/helpers/eventNames.js new file mode 100644 index 0000000..7d874af --- /dev/null +++ b/lib/utils/model/helpers/eventNames.js @@ -0,0 +1,23 @@ +/** + * Life Cycle Event Types, An Array listing of all available Events. + * @see http://cleverstack.io/documentation/backend/models/#events + */ +var eventNames = [ + 'beforeValidate', + 'afterValidate', + 'beforeCreate', + 'beforeUpdate', + 'beforeDestroy', + 'afterCreate', + 'afterUpdate', + 'afterDestroy', + 'beforeAllFindersOptions', + 'beforeFindOptions', + 'beforeFind', + 'afterFind', + 'beforeFindAllOptions', + 'beforeFindAll', + 'afterFindAll' +]; + +module.exports = eventNames; \ No newline at end of file diff --git a/lib/utils/model/helpers/events/afterEvent.js b/lib/utils/model/helpers/events/afterEvent.js new file mode 100644 index 0000000..0e90967 --- /dev/null +++ b/lib/utils/model/helpers/events/afterEvent.js @@ -0,0 +1,36 @@ +var util = require('util') + , utils = require('utils') + , underscore = require('underscore') + , debug = require('debug')('cleverstack:utils:model'); + +module.exports = function emitAfterEvent(eventName, modelDataOrFindOptions, queryOptions, model, callback) { + var listeners = this.listeners(eventName).length + , callbacks = 0 + , errors = []; + + if (!callback) { + callback = model; + model = null; + } + + utils.model.helpers.alias.fields.forOutput.apply(this, [modelDataOrFindOptions]); + if (listeners < 1) { + return callback(null, model); + } + + if (debug.enabled) { + debug(util.format('Running hook, emitting %s(%s) on %s listeners...', eventName, utils.model.helpers.debugInspect(modelDataOrFindOptions), listeners)); + } + + this.emit(eventName, model, modelDataOrFindOptions, queryOptions, function(err, updatedModelDataOrFindOptions) { + if (err) { + errors.push(err); + } else if (updatedModelDataOrFindOptions !== undefined) { + underscore.extend(modelDataOrFindOptions, updatedModelDataOrFindOptions); + } + + if (++callbacks === listeners) { + callback(errors.length ? errors.shift() : null, model); + } + }); +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/events/beforeEvent.js b/lib/utils/model/helpers/events/beforeEvent.js new file mode 100644 index 0000000..919382c --- /dev/null +++ b/lib/utils/model/helpers/events/beforeEvent.js @@ -0,0 +1,30 @@ +var util = require('util') + , utils = require('utils') + , underscore = require('underscore') + , debug = require('debug')('cleverstack:utils:model'); + +module.exports = function emitBeforeEvent(eventName, modelDataOrFindOptions, queryOptions, callback) { + var listeners = this.listeners(eventName).length + , callbacks = 0 + , errors = []; + + if (listeners < 1) { + return callback(null); + } + + if (debug.enabled) { + debug(util.format('Running hook, emitting %s(%s) on %s listeners...', eventName, utils.model.helpers.debugInspect(modelDataOrFindOptions), listeners)); + } + + this.emit(eventName, modelDataOrFindOptions, queryOptions, function(err, updatedModelDataOrFindOptions) { + if (err) { + errors.push(err); + } else if (updatedModelDataOrFindOptions !== undefined) { + underscore.extend(modelDataOrFindOptions, updatedModelDataOrFindOptions); + } + + if (++callbacks === listeners) { + callback(errors.length ? errors.shift() : null); + } + }); +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/findOptions/normalize.js b/lib/utils/model/helpers/findOptions/normalize.js new file mode 100644 index 0000000..b212af9 --- /dev/null +++ b/lib/utils/model/helpers/findOptions/normalize.js @@ -0,0 +1,7 @@ +module.exports = function normalizeFindOptions(findOptions) { + findOptions = findOptions || { where: {} }; + if (!findOptions.where) { + findOptions = {where: findOptions}; + } + return findOptions; +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/findOptions/valid.js b/lib/utils/model/helpers/findOptions/valid.js new file mode 100644 index 0000000..96ba189 --- /dev/null +++ b/lib/utils/model/helpers/findOptions/valid.js @@ -0,0 +1,9 @@ +var Exceptions = require('exceptions'); + +module.exports = function findOptionsAreValid(findOptions, callback) { + if (!findOptions) { + callback(new Exceptions.InvalidData('You must specify either an id or an object containing fields to find a %s')); + } else { + callback(null); + } +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/gettersAndSetters.js b/lib/utils/model/helpers/gettersAndSetters.js new file mode 100644 index 0000000..11b6340 --- /dev/null +++ b/lib/utils/model/helpers/gettersAndSetters.js @@ -0,0 +1,7 @@ +module.exports = function setupGettersAndSetters(Klass, Proto) { + Klass.getters = Proto.getters !== undefined ? Proto.getters : {}; + Klass.setters = Proto.setters !== undefined ? Proto.setters : {}; + + delete Proto.getters; + delete Proto.setters; +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/handleResult.js b/lib/utils/model/helpers/handleResult.js new file mode 100644 index 0000000..4ed1bc8 --- /dev/null +++ b/lib/utils/model/helpers/handleResult.js @@ -0,0 +1,40 @@ +var util = require('util') + , chalk = require('chalk') + , debug = require('debug')('cleverstack:utils:model:handleResult'); + +function handleErr(reject, err) { + if (debug.enabled) { + debug(util.format('%s (%s) %s %s', chalk.red('Error'), err.parent ? err.parent.message : err.message, util.inspect(err.stack ? err.stack.split('\n') : err))); + } + reject(err); +} + +function updateReferencedModel(resolve, reject, err, model) { + if ( !err ) { + this.entity = model; + resolve( this ); + } else { + handleErr(reject, err); + } +} + +function removeReferencedModel(resolve, reject, err) { + if (!err) { + delete this.entity; + resolve({}); + } else { + handleErr(reject, err); + } +} + +function returnModels(resolve, reject, err, models) { + if (err === undefined || err === null) { + resolve(models); + } else { + handleErr(reject, err); + } +} + +module.exports.returnModels = returnModels; +module.exports.updateReferencedModel = updateReferencedModel; +module.exports.removeReferencedModel = removeReferencedModel; \ No newline at end of file diff --git a/lib/utils/model/helpers/isExtendedModel.js b/lib/utils/model/helpers/isExtendedModel.js new file mode 100644 index 0000000..da54927 --- /dev/null +++ b/lib/utils/model/helpers/isExtendedModel.js @@ -0,0 +1,3 @@ +module.exports = function isExtendedModel(callback) { + callback(this.entity.name !== null ? null : 'You cannot call Model.create() directly on the model class.'); +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/isNewModel.js b/lib/utils/model/helpers/isNewModel.js new file mode 100644 index 0000000..f2914b1 --- /dev/null +++ b/lib/utils/model/helpers/isNewModel.js @@ -0,0 +1,31 @@ +var util = require('util') + , utils = require('utils') + , async = require('async') + , Exceptions = require('exceptions'); + +/** + * Ensures that for the given data, it will result in a new model instance upon Model.create() . (aka no primaryKeys set!) + * + * @param {Object} data the data that will be used to create a model + * @param {Function} callback + * @return {undefined} + */ +module.exports = function isNewModel(data, callback) { + if (this.debug.enabled) { + this.debug(util.format('isNewModel(%s)', utils.model.helpers.debugInspect(data))); + } + + async.each( + this.primaryKeys || this.Class.primaryKeys || [], + this.callback(function checkPrimaryKeys(key, checkDone) { + if (this.fields[key] && !!this.fields[key].autoIncrement) { + checkDone(!data[key] ? null : key); + } else { + checkDone(null); + } + }), + this.callback(function(key) { + callback(!key ? null : new Exceptions.InvalidData(util.format('Invalid data provided to %s.create(%s)', (this.modelName || this.Class.modelName), key))); + }) + ); +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/nestedOperations.js b/lib/utils/model/helpers/nestedOperations.js new file mode 100644 index 0000000..42a63c5 --- /dev/null +++ b/lib/utils/model/helpers/nestedOperations.js @@ -0,0 +1,17 @@ +var injector = require('injector') + , utils = require('utils'); + +module.exports = function setupNestedOperations(Klass, modelName, model, debug) { + injector.getInstance('moduleLoader').on('routesInitialized', function() { + debug( 'Parsing templated event handlers...' ); + Object.keys( Klass ).forEach( function( propName ) { + if ( propName.indexOf( ' ' ) !== -1 || utils.model.helpers.eventNames.indexOf( propName ) !== -1 ) { + var parts = propName.split( ' ' ) + , resource = parts.length === 2 ? parts.shift() : modelName + 'Model' + , eventName = parts.shift(); + + injector.getInstance( resource ).on( eventName, model.callback( propName ) ); + } + }); + }); +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/options/defaults.js b/lib/utils/model/helpers/options/defaults.js new file mode 100644 index 0000000..8badf79 --- /dev/null +++ b/lib/utils/model/helpers/options/defaults.js @@ -0,0 +1,37 @@ +/** + * Default Options, defined as properties of the Classes default options Object Literal. + * @see http://cleverstack.io/documentation/backend/models/#options + * + * @property {String} type=ORM the default model type, either 'ORM' or 'ODM' + * @property {String} dbName=false the name of the database table + * @property {String} engine=false the database engine to use for this model (ORM ONLY) + * @property {String} charset=false the database charset to use for this model (ORM ONLY) + * @property {String} comment=false the database comment to use for this model (ORM ONLY) + * @property {String} collate=false the database collate to use for this model (ORM ONLY) + * @property {Object} indexes=false custom definition of indexes for this model + * @property {String} createdAt=createdAt for use with the timeStampable behaviour + * @property {String} updatedAt=updatedAt for use with the timeStampable behaviour + * @property {String} deletedAt=deletedAt for use with the softDeleteable behaviour + * @property {Boolean} underscored=false the database underscored to use for this model + * @property {Boolean} versionable=false the versionable behaviour + * @property {Boolean} freezeDbName=false if set to true your models tableName(dbName) won't be plural or camelized + * @property {Boolean} timeStampable=true the timeStampable behaviour + * @property {Boolean} softDeleteable=true the softDeleteable behaviour + */ +module.exports = { + type : 'ORM', + dbName : false, + engine : false, + charset : 'utf8mb4', + comment : false, + collate : false, + indexes : false, + createdAt : 'created_at', + updatedAt : 'modified_at', + deletedAt : 'deleted_at', + underscored : true, + versionable : false, + freezeDbName : true, + timeStampable : false, + softDeleteable : false +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/options/setup.js b/lib/utils/model/helpers/options/setup.js new file mode 100644 index 0000000..a1dc084 --- /dev/null +++ b/lib/utils/model/helpers/options/setup.js @@ -0,0 +1,31 @@ +var underscore = require('underscore') + , inflect = require('i')(); + +module.exports = function setupOptions(Klass, modelName, modelType, defaults) { + Klass.type = modelType; + Klass.modelName = modelName; + Klass.fields = {}; + Klass.aliases = []; + Klass.primaryKey = false; + Klass.primaryKeys = []; + Klass.hasPrimaryKey = false; + Klass.singlePrimaryKey = false; + + // Setup the defaults + underscore.without(Object.keys(defaults), 'type').forEach(this.callback(function(optionName) { + Klass[optionName] = Klass[optionName] !== undefined ? Klass[optionName] : this.defaults[optionName]; + })); + + // Provide the database name + Klass.dbName = Klass.dbName !== false ? Klass.dbName : modelName; + + // Pluralize the dbName if not frozen + if (false === Klass.freezeDbName) { + Klass.dbName = inflect.pluralize(Klass.dbName); + } + + // Underscore the dbName if we have been told to + if (Klass.underscored === true) { + Klass.dbName = inflect.underscore(Klass.dbName); + } +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/queryOptions/normalize.js b/lib/utils/model/helpers/queryOptions/normalize.js new file mode 100644 index 0000000..85ba7c4 --- /dev/null +++ b/lib/utils/model/helpers/queryOptions/normalize.js @@ -0,0 +1,4 @@ +module.exports = function normalizeQueryOptions(queryOptions) { + queryOptions = typeof queryOptions === 'object' ? queryOptions : {}; + return queryOptions; +}; \ No newline at end of file diff --git a/lib/utils/model/helpers/resolveName.js b/lib/utils/model/helpers/resolveName.js new file mode 100644 index 0000000..1ed01fa --- /dev/null +++ b/lib/utils/model/helpers/resolveName.js @@ -0,0 +1,20 @@ +'use strict'; + +var utils = require('utils'); + +/** + * If no modelName is provided, or its not truthy, work out the modelName + * based on the filename where extend was called. + * Additionally, get rid of any superfluous 'Model' at the end of the modelName. + * + * @param {String|Boolean|Null} modelName optional strict name for the model + * @return {String} the final modelName + */ +module.exports = function resolveModelName(modelName) { + if (modelName === false && (modelName = utils.helpers.getClassName(5)) === false) { + throw new Error('Unable to determine the models name based on filename.'); + } else { + modelName = modelName.replace('Model', ''); + } + return modelName; +}; \ No newline at end of file diff --git a/lib/utils/modelTypes.js b/lib/utils/model/helpers/types.js similarity index 73% rename from lib/utils/modelTypes.js rename to lib/utils/model/helpers/types.js index 815af84..7a33bf9 100644 --- a/lib/utils/modelTypes.js +++ b/lib/utils/model/helpers/types.js @@ -1,3 +1,14 @@ +/** + * Field Types you can use when Native JavaScript Types simply don't cut it... + * @see http://cleverstack.io/documentation/backend/models/#field-types + * + * @property {ENUM} ENUM custom type + * @property {TINYINT} TINYINT custom type + * @property {BIGINT} BIGINT custom type + * @property {FLOAT} FLOAT custom type + * @property {DECIMAL} DECIMAL custom type + * @property {TEXT} TEXT custom type + */ var types = { ENUM: function() { return { @@ -6,7 +17,7 @@ var types = { toString: function() { return this.type; } - } + }; }, TINYINT: function(length) { return { @@ -15,7 +26,7 @@ var types = { toString: function() { return this.type; } - } + }; }, BIGINT: function(length) { return { @@ -24,7 +35,7 @@ var types = { toString: function() { return this.type; } - } + }; }, FLOAT: function(length, decimals) { var field = { @@ -33,7 +44,7 @@ var types = { toString: function() { return this.type; } - } + }; if (decimals !== undefined) { field.decimals = decimals; @@ -48,7 +59,7 @@ var types = { toString: function() { return this.type; } - } + }; if (scale !== undefined) { field.scale = scale; @@ -62,14 +73,14 @@ var types = { toString: function() { return this.type; } - } + }; } }; Object.keys(types).forEach(function(type) { types[ type ].toString = function() { return '' + type; - } + }; }); module.exports = types; \ No newline at end of file diff --git a/lib/utils/model/helpers/validator.js b/lib/utils/model/helpers/validator.js new file mode 100644 index 0000000..63022d7 --- /dev/null +++ b/lib/utils/model/helpers/validator.js @@ -0,0 +1,24 @@ +var util = require('util') + , utils = require('utils') + , injector = require('injector') + , Exceptions = require('exceptions'); + +/** + * The Validator Class, used to validate Instance Fields. + * @see http://cleverstack.io/documentation/backend/models/#validation + */ +function validator(modelData, callback) { + if (this.debug.enabled) { + this.debug(util.format('validateValues(%s)', utils.model.helpers.debugInspect(modelData))); + } + + injector + .getInstance('Validator') + .validate(this, modelData) + .then(callback) + .catch(function(err) { + callback(new Exceptions.ModelValidation(err)); + }); +} + +module.exports = validator; \ No newline at end of file diff --git a/lib/utils/model/instance/destroy.js b/lib/utils/model/instance/destroy.js new file mode 100644 index 0000000..4c55548 --- /dev/null +++ b/lib/utils/model/instance/destroy.js @@ -0,0 +1,33 @@ +var utils = require('utils') + , async = require('async') + , Promise = require('bluebird'); + +/** + * Destroy the current instance + * @see http://cleverstack.io/documentation/backend/models/#destroying-instances + * + * @param {Object} queryOptions={} + * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query + * @return {Promise} + */ +module.exports = function destroyModelInstance(queryOptions) { + var utilName = this.Class.type.toLowerCase() + 'Utils' + , helpers = utils.model.helpers + , driverUtil = utils[utilName]; + + queryOptions = utils.model.helpers.findOptions.normalize(queryOptions); + + if (this.debug.enabled) { + this.debug('destroy(queryOptions)'); + } + + return new Promise(function(resolve, reject) { + async.waterfall([ + this.Class.callback(helpers.events.beforeEvent, 'beforeDestroy', this, queryOptions), + this.callback(driverUtil.destroyInstance, queryOptions), + this.Class.callback(helpers.events.afterEvent, 'afterDestroy', this, queryOptions) + ], + this.callback(helpers.handleResult.removeReferencedModel, resolve, reject)); + } + .bind(this)); +}; \ No newline at end of file diff --git a/lib/utils/model/instance/inspect.js b/lib/utils/model/instance/inspect.js new file mode 100644 index 0000000..b55ee80 --- /dev/null +++ b/lib/utils/model/instance/inspect.js @@ -0,0 +1,9 @@ +/** + * Special handler for util.inspect to use Model#toJSON + * + * @see http://cleverstack.io/documentation/backend/models/#instance-methods-inspect + * @return {Object} + */ +module.exports = function inspectModel() { + return JSON.stringify(this.toJSON(), null, ' '); +} \ No newline at end of file diff --git a/lib/utils/model/instance/map.js b/lib/utils/model/instance/map.js new file mode 100644 index 0000000..539615a --- /dev/null +++ b/lib/utils/model/instance/map.js @@ -0,0 +1,9 @@ +/** + * Allows you to use modelInstance.map() + * @return {Mixed} + */ +function mapModelInstance() { + return this.entity.map.apply(this, arguments); +} + +module.exports = mapModelInstance; \ No newline at end of file diff --git a/lib/utils/model/instance/save.js b/lib/utils/model/instance/save.js new file mode 100644 index 0000000..b5e4fcd --- /dev/null +++ b/lib/utils/model/instance/save.js @@ -0,0 +1,59 @@ +var utils = require('utils') + , async = require('async') + , underscore = require('underscore') + , Promise = require('bluebird'); + +/** + * Update the current model using the provided values (optional, defaults to this.changed()) + * @see http://cleverstack.io/documentation/backend/models/#updating-or-saving-instances + * + * @param {Object} values=this.changed() The values that will be used to create a model instance + * @param {Object} queryOptions={} + * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query + * @return {Promise} + */ +module.exports = function saveModelInstance(values, queryOptions) { + var utilName = this.Class.type.toLowerCase() + 'Utils' + , helpers = utils.model.helpers + , driverUtil = utils[utilName] + , omitFields; + + queryOptions = queryOptions || {}; + + return new Promise(function(resolve, reject) { + if (typeof values === 'object') { + omitFields = [];//.concat(this.primaryKey); + if (!!this.Class.timeStampable) { + omitFields.push('createdAt'); + omitFields.push('updatedAt'); + } + if (!!this.Class.softDeleteable) { + omitFields.push('deletedAt'); + } + + Object.keys( values ).forEach(this.callback(function( i ) { + if (omitFields.indexOf(i) === -1 && typeof this.Class.setters[i] === 'function') { + this.Class.setters[i].apply(this, [values[i]]); + } + })); + } + + values = underscore.pick(this.values, this.changed); + if (this.debug.enabled) { + this.debug('save(%s)', helpers.debugInspect(values)); + } + if (Object.keys(values).length === 0 && !queryOptions.force) { + return resolve(this); + } + + async.waterfall([ + // @todo fix validator + // this.Class.callback(modelUtils.validateValues, this.Class.validator, this), + this.Class.callback(helpers.events.beforeEvent, 'beforeSave', values, queryOptions), + this.callback(driverUtil.save, values, queryOptions), + this.Class.callback(helpers.events.afterEvent, 'afterSave', values, queryOptions) + ], + this.callback(helpers.handleResult.updateReferencedModel, resolve, reject)); + } + .bind(this)); +}; \ No newline at end of file diff --git a/lib/utils/model/instance/setup.js b/lib/utils/model/instance/setup.js new file mode 100644 index 0000000..6d1e6e5 --- /dev/null +++ b/lib/utils/model/instance/setup.js @@ -0,0 +1,23 @@ +function setupProperty(propName) { + Object.defineProperty(this, propName, { + get : this.proxy(this.Class.getters[propName]), + set : this.proxy(this.Class.setters[propName]), + enumerable : true, + configurable : false + }); +} + +function setupModelInstance() { + Object.keys(this.Class.getters).forEach(this.proxy(setupProperty)); + + if (this.Class.timeStampable) { + setupProperty.apply(this, [this.Class.createdAt]); + setupProperty.apply(this, [this.Class.updatedAt]); + } + + if (this.Class.softDeleteable) { + setupProperty.apply(this, [this.Class.deletedAt]); + } +} + +module.exports = setupModelInstance; \ No newline at end of file diff --git a/lib/utils/model/instance/toJSON.js b/lib/utils/model/instance/toJSON.js new file mode 100644 index 0000000..b01f474 --- /dev/null +++ b/lib/utils/model/instance/toJSON.js @@ -0,0 +1,47 @@ +var utils = require('utils') + , underscore = require('underscore'); + +/** + * Convert the model into a JSON ready format (Object literal) + * + * @see http://cleverstack.io/documentation/backend/models/#instance-methods-toJSON + * @return {Object} + */ +module.exports = function toJSON() { + var json = {} + , helpers = utils.model.helpers; + + try { + + // Add in getters + Object.keys(this.Class.getters).forEach(this.proxy(function(getterName) { + if (this[getterName] !== undefined) { + json[getterName] = this[getterName]; + } + })); + + if (this.Class.type === 'ORM') { + if (this.entity.options.includeNames) { + json = helpers.alias.fields.forOutput.apply(this, [underscore.omit(underscore.clone(this.entity.values), this.entity.options.includeNames)]); + + this.entity.options.includeNames.forEach(this.proxy(function(includeName) { + var toJSON = this.entity[includeName] ? this.entity[includeName].toJSON : false + , subJSON = toJSON ? toJSON.apply(this.entity[includeName]) : this.entity[includeName]; + + json[includeName] = underscore.clone(subJSON); + })); + + helpers.alias.associations.forOutput.apply(this, [json]); + } else { + json = helpers.alias.fields.forOutput.apply(this, [underscore.clone(this.entity.values)]); + } + } else { + json = this.entity.toObject(); + } + } catch(e) { + console.error(e); + console.error(e.stack); + } + + return json; +}; \ No newline at end of file diff --git a/lib/utils/model/max.js b/lib/utils/model/max.js new file mode 100644 index 0000000..f26235b --- /dev/null +++ b/lib/utils/model/max.js @@ -0,0 +1 @@ +// @todo max \ No newline at end of file diff --git a/lib/utils/model/min.js b/lib/utils/model/min.js new file mode 100644 index 0000000..d5ea7d0 --- /dev/null +++ b/lib/utils/model/min.js @@ -0,0 +1 @@ +// @todo min \ No newline at end of file diff --git a/lib/utils/model/update.js b/lib/utils/model/update.js new file mode 100644 index 0000000..bbc6ecc --- /dev/null +++ b/lib/utils/model/update.js @@ -0,0 +1,45 @@ +var utils = require('utils') + , async = require('async') + , Promise = require('bluebird'); + +/** + * Update a model using the provided values, and with where criteria supplied in queryOptions. + * @see http://cleverstack.io/documentation/backend/models/#updating-or-saving-instances + * + * @function Model.update + * @param {Object} values The values that will be used to create a model instance + * @param {Object} queryOptions={} + * @param {Object} queryOptions.where Criteria to use for the UPDATE statement + * @param {Transaction} queryOptions.transaction The transaction (if any) to use in the query + * @return {Promise} + */ +module.exports = function updateModels(values, queryOptions) { + var utilName = this.type.toLowerCase() + 'Utils' + , helpers = utils.model.helpers + , driverUtil = utils[utilName]; + + values = values || {}; + queryOptions = utils.model.helpers.findOptions.normalize(queryOptions); + + if (this.debug.enabled) { + this.debug('update(%s) %s', utils.model.helpers.debugInspect(values), utils.model.helpers.debugInspect(queryOptions)); + } + + return new Promise(function update(resolve, reject) { + async.waterfall([ + this.callback(helpers.isExtendedModel), + this.callback(helpers.where.exists, 'update', values, queryOptions), + this.callback(helpers.alias.associations.forOutput,values), + this.callback(helpers.alias.fields.forOutput, queryOptions.where), + this.callback(helpers.events.beforeEvent, 'beforeUpdate', values, queryOptions), + this.callback(helpers.alias.associations.forQuery, queryOptions.where, false), + this.callback(helpers.alias.fields.forQuery, queryOptions.where), + this.callback(helpers.alias.associations.forQuery, values, false), + this.callback(helpers.alias.fields.forQuery, values), + this.callback(driverUtil.update, values, queryOptions), + this.callback(helpers.events.afterEvent, 'afterUpdate', values, queryOptions) + ], + this.callback(helpers.handleResult.returnModels, resolve, reject)); + } + .bind(this)); +}; \ No newline at end of file diff --git a/lib/utils/modelUtils.js b/lib/utils/modelUtils.js deleted file mode 100644 index 893e5ef..0000000 --- a/lib/utils/modelUtils.js +++ /dev/null @@ -1,494 +0,0 @@ -var util = require('util') - , async = require('async') - , inflect = require('i')() - , underscore = require('underscore') - , inspectUtil = require('util').inspect - , Exceptions = require('exceptions') - , chalk = require('chalk') - , debug = require('debug')('cleverstack:utils:modelUtils'); - -var modelUtils = { - - eventNames: [ - 'beforeValidate', - 'afterValidate', - 'beforeCreate', - 'beforeUpdate', - 'beforeDestroy', - 'afterCreate', - 'afterUpdate', - 'afterDestroy', - 'beforeAllFindersOptions', - 'beforeFindOptions', - 'beforeFind', - 'afterFind', - 'beforeFindAllOptions', - 'beforeFindAll', - 'afterFindAll' - ], - - debugInspect: function(obj) { - return inspectUtil(obj, {showHidden: false, colors: true, customInspect: true, depth: 1}).replace(/\n[\ ]+/igm, ' '); - // return inspectUtil(obj, {showHidden: false, colors: true, customInspect: true, depth: 0}).replace(/(\n[\ ]+|\{\ )/igm, '\n\ \ \ \ '); - }, - - normalizeFindOptions: function(findOptions) { - findOptions = findOptions || { where: {} }; - if (!findOptions.where) { - findOptions = {where: findOptions}; - } - return findOptions; - }, - - normalizeQueryOptions: function(queryOptions) { - queryOptions = typeof queryOptions === 'object' ? queryOptions : {}; - return queryOptions; - }, - - ensurePrimaryKeyInWhere: function(findOptions, callback) { - if (/^[0-9a-fA-F]{24}$/.test(findOptions) || !isNaN(findOptions)) { - if (this.primaryKeys.length === 1) { - var findOptionsOverride = { where: {} }; - findOptionsOverride.where[this.primaryKey] = findOptions; - findOptions = findOptionsOverride; - callback(null); - } else { - callback(new Exceptions.InvalidData('You must provide an object when using Model.find() when there are multiple primaryKeys')); - } - } else { - callback(null); - } - }, - - hasMany: function() { - if (this.type === 'ODM') { - return utils.odmUtils.hasMany.apply(this, arguments); - } else { - return this.entity.hasMany.apply(this.entity, arguments); - } - }, - - hasOne: function() { - if (this.type === 'ODM') { - return utils.odmUtils.hasOne.apply(this, arguments); - } else { - return this.entity.hasOne.apply(this.entity, arguments); - } - }, - - belongsTo: function() { - if (this.type === 'ODM') { - return utils.odmUtils.belongsTo.apply(this, arguments); - } else { - return this.entity.belongsTo.apply(this.entity, arguments); - } - }, - - isValidSchema: function(callback) { - callback(this.entity.name !== null ? null : 'You cannot call Model.create() directly on the model class.'); - }, - - isNewModel: function(modelData, callback) { - if (debug.enabled) { - debug(util.format('isNewModel(%s)', modelUtils.debugInspect(modelData))); - } - - async.each( - this.primaryKeys || this.Class.primaryKeys || [], - this.callback(function checkPrimaryKeys(key, checkDone) { - if (this.fields[key] && !!this.fields[key].autoIncrement) { - checkDone(!modelData[key] ? null : key); - } else { - checkDone(null); - } - }), - this.callback(function(key) { - callback(!key ? null : new Exceptions.InvalidData(util.format('Invalid modelData provided to %s.create(%s)', (this._name || this.Class._name), key))); - }) - ); - }, - - hasValidWhere: function(type, values, queryOptions, callback) { - var valid = false; - - if (type === 'update') { - valid = typeof queryOptions === 'object' && queryOptions.where && Object.keys(queryOptions.where).length >= 1; - } else if (type === 'destroy') { - valid = typeof queryOptions === 'object' && queryOptions.where && queryOptions.where.id; - } - - callback(!!valid ? null : new Exceptions.InvalidData('Unable to update without find criteria.')); - }, - - setDefaultValues: function(modelData, callback) { - async.each( - Object.keys(this.fields || this.Class.fields), - this.callback(modelUtils.setDefaultValue, modelData), - callback - ); - }, - - setDefaultValue: function(modelData, fieldName, callback) { - var field = this.fields[fieldName] - , value = modelData[fieldName] - , defaultValue = field['default']; - - if (field.type && value === undefined && defaultValue !== undefined) { - if (debug.enabled) { - debug(util.format('setDefaultValue(%s=%s)', fieldName, modelUtils.debugInspect(defaultValue))); - } - modelData[fieldName] = defaultValue; - } - - callback(null); - }, - - validateValues: function(validator, modelData, callback) { - if (debug.enabled) { - debug(util.format('validateValues(%s)', modelUtils.debugInspect(modelData))); - } - - validator - .validate(this, modelData) - .then(callback) - .catch(function(err) { - callback(new Exceptions.ModelValidation(err)); - }); - }, - - // @todo move this into the clever-odm module - timeStampable: function(modelData, callback) { - if (this.modelType === 'ODM' && !!this.timeStampable) { - modelData[this.createdAt] = Date.now(); - modelData[this.updatedAt] = Date.now(); - } - callback(null); - }, - - beforeEvent: function(eventName, modelDataOrFindOptions, queryOptions, callback) { - var listeners = this.listeners(eventName).length - , callbacks = 0 - , errors = []; - - if (listeners < 1) { - return callback(null); - } - - if (debug.enabled) { - debug(util.format('Running hook, emitting %s(%s) on %s listeners...', eventName, modelUtils.debugInspect(modelDataOrFindOptions), listeners)); - } - - this.emit(eventName, modelDataOrFindOptions, queryOptions, function(err, updatedModelDataOrFindOptions) { - if (err) { - errors.push(err); - } else if (updatedModelDataOrFindOptions !== undefined) { - underscore.extend(modelDataOrFindOptions, updatedModelDataOrFindOptions); - } - - if (++callbacks === listeners) { - callback(errors.length ? errors.shift() : null); - } - }); - }, - - afterEvent: function(eventName, modelDataOrFindOptions, queryOptions, model, callback) { - var listeners = this.listeners(eventName).length - , callbacks = 0 - , errors = []; - - if (!callback) { - callback = model; - model = null; - } - - modelUtils.aliasFieldsForOutput.apply(this, [modelDataOrFindOptions]); - if (listeners < 1) { - return callback(null, model); - } - - this.emit(eventName, model, modelDataOrFindOptions, queryOptions, function(err, updatedModelDataOrFindOptions) { - if (err) { - errors.push(err); - } else if (updatedModelDataOrFindOptions !== undefined) { - underscore.extend(modelDataOrFindOptions, updatedModelDataOrFindOptions); - } - - if (++callbacks === listeners) { - callback(errors.length ? errors.shift() : null, model); - } - }); - }, - - setupTimeStampable: function(Static) { - if (Static.timeStampable === true) { - Static.fields.createdAt = { - type : Date, - columnName : Static.createdAt - }; - if (Static.createdAt !== 'createdAt') { - Static.aliases.push({ - key : 'createdAt', - columnName : Static.createdAt - }) - } - - Static.fields.updatedAt = { - type : Date, - columnName : Static.updatedAt - }; - if (Static.updatedAt !== 'updatedAt') { - Static.aliases.push({ - key : 'updatedAt', - columnName : Static.updatedAt - }) - } - } - }, - - setupSoftDeleteable: function(Static) { - if (Static.softDeleteable === true) { - Static.fields.deletedAt = { - type : Date, - columnName : Static.deletedAt - }; - if (Static.deletedAt !== 'deletedAt') { - Static.aliases.push({ - key : 'deletedAt', - columnName : Static.deletedAt - }) - } - } - }, - - getSchemaFromProto: function(Proto, Static, key) { - var prop = Proto[key] - , columnName = !!Static.underscored ? inflect.underscore(key) : key; - - if (!!prop.columnName && key !== prop.columnName) { - Static.aliases.push({ key: key, columnName: prop.columnName }); - columnName = prop.columnName; - } else if (!!Static.underscored && key !== columnName) { - Static.aliases.push({ key: key, columnName: columnName }); - } - - if (typeof prop === 'function' && [String, Number, Boolean, Date, Buffer, this.Types.ENUM, this.Types.TINYINT, this.Types.BIGINT, this.Types.FLOAT, this.Types.DECIMAL, this.Types.TEXT].indexOf(Proto[key]) === -1 && key !== 'defaults') { - - // Allow definition of custom getters and setters for fields, but make sure not to include association accessor functions. - if (/^(set|get)(.*)$/.test(key)) { - var getOrSet = RegExp.$1 - , fieldName = RegExp.$2; - - if (fieldName !== false && Static.fields[key] !== undefined && typeof Static.getters[key] === 'function') { - Static[(getOrSet === 'get' ? 'getters' : 'setters')][key] = function() { - return prop.apply(this, arguments); - }; - } - } - } else if (key !== 'defaults') { - - if (typeof Static.fields !== 'object') { - Static.fields = {}; - } - - if (typeof Static.getters !== 'object') { - Static.getters = {}; - } - - if (typeof Static.setters !== 'object') { - Static.setters = {}; - } - - Static.fields[key] = prop; - Static.getters[key] = function() { - if (key === 'id' && Static.type.toLowerCase() === 'odm') { - return this.entity._id; - } else { - return this.entity.get(columnName); - } - }; - Static.setters[key] = function(val) { - this.entity.set(columnName, val); - - return this; - }; - - delete Proto[key]; - } - }, - - aliasAssociationsForQuery: function(data, remove, callback) { - async.each( - Object.keys(data), - this.callback(modelUtils.aliasAssociationForQuery, data, remove), - callback - ); - }, - - aliasAssociationForQuery: function(data, remove, fieldName, callback) { - var associations = this.Class ? this.Class.entity.associations : this.entity.associations - , hasAssociation = associations ? associations[fieldName] : false - , isArray = data[fieldName] instanceof Array; - - if (!!hasAssociation && data[fieldName] !== undefined && data[fieldName] !== null) { - if (debug.enabled) { - debug(util.format('aliasAssociationForQuery(%s)', modelUtils.debugInspect(fieldName))); - } - - if (hasAssociation.associationType === 'BelongsTo') { - if (!isArray) { - if (data[fieldName].entity !== undefined) { - data[hasAssociation.options.foreignKey] = data[fieldName].entity[hasAssociation.foreignKeyAttribute.referencesKey]; - } else { - data[hasAssociation.options.foreignKey] = data[fieldName]; - } - } else if (!!isArray) { - data[fieldName] = data[fieldName].map(function(model) { - return model.entity !== undefined ? model.entity : model; - }); - } - - if (remove === true) { - delete data[fieldName]; - } - } - } - - return callback ? callback(null) : true; - }, - - aliasFieldsForQuery: function(fields, callback) { - if (Object.keys(fields).length > 0) { - if (debug.enabled) { - debug(util.format('aliasFieldsForQuery(%s)', modelUtils.debugInspect(fields))); - } - - Object.keys(fields).forEach(function(key) { - var val = fields[key] - , associations = this.Class ? this.Class.entity.associations : this.entity.associations - , hasAssociation = associations ? associations[key] : false - , newKey; - - if ( !hasAssociation && !!( newKey = underscore.findWhere(this.aliases || this.Class.aliases, { key: key }) )) { - fields[newKey.columnName] = val; - delete fields[key]; - } - } - .bind(this)); - } - - return callback ? callback(null) : true; - }, - - aliasFieldsForOutput: function(fields, callback) { - if (debug.enabled && !fields.where) { - debug(util.format('aliasFieldsForOutput(%s)', modelUtils.debugInspect(Object.keys(fields)))); - } - - (this.aliases || this.Class.aliases).forEach(function(column) { - var hasAssociation = (this.entity.associations || this.Class.entity.associations)[column.key]; - - if (!hasAssociation && fields[column.columnName] !== undefined) { - fields[column.key] = fields[column.columnName]; - delete fields[column.columnName]; - } - } - .bind(this)); - - return callback ? callback(null) : fields; - }, - - aliasAssociationsForOutput: function(fields, callback) { - if (this.entity.associations || this.Class.entity.associations) { - if (debug.enabled) { - debug(util.format('aliasAssociationsForOutput(%s)', modelUtils.debugInspect(Object.keys(fields)))); - } - - Object.keys(this.entity.associations || this.Class.entity.associations).forEach(function(includeName) { - var options = (this.entity.associations || this.Class.entity.associations)[includeName].options; - if (!!options.as && fields[options.foreignKey] !== undefined) { - if (!fields[options.as]) { - fields[options.as] = fields[options.foreignKey]; - } - delete fields[options.foreignKey]; - } - }.bind(this)); - } - - return callback ? callback(null) : fields; - }, - - updateReferencedModel: function(resolve, reject, err, model) { - if ( !err ) { - this.entity = model; - resolve( this ); - } else { - reject( err ); - } - }, - - removeReferencedModel: function(resolve, reject, err) { - if (!err) { - delete this.entity; - resolve({}); - } else { - if (debug.enabled) { - debug(util.format('%s (%s) %s %s', chalk.red('Error'), err.parent ? err.parent.message : err.message, util.inspect(err.stack ? err.stack.split('\n') : err))); - } - reject(err); - } - }, - - returnModels: function(resolve, reject, err, models) { - if (err === undefined || err === null) { - resolve(models); - } else { - if (debug.enabled) { - debug(util.format('%s (%s) %s %s', chalk.red('Error'), err.parent ? err.parent.message : err.message, util.inspect(err.stack ? err.stack.split('\n') : err))); - } - reject(err); - } - }, - - toJSON: function() { - var json = {}; - try { - - // Add in getters - Object.keys(this.Class.getters).forEach(this.proxy(function(getterName) { - if (this[getterName] !== undefined) { - json[getterName] = this[getterName]; - } - })); - - if (this.Class.type === 'ORM') { - if (this.entity.options.includeNames) { - json = modelUtils.aliasFieldsForOutput.apply(this, [underscore.omit(underscore.clone(this.entity.values), this.entity.options.includeNames)]); - - this.entity.options.includeNames.forEach(this.proxy(function(includeName) { - var toJSON = this.entity[includeName] ? this.entity[includeName].toJSON : false - , subJSON = toJSON ? toJSON.apply(this.entity[includeName]) : this.entity[includeName]; - - json[includeName] = underscore.clone(subJSON); - })); - - modelUtils.aliasAssociationsForOutput.apply(this, [json]); - } else { - json = modelUtils.aliasFieldsForOutput.apply(this, [underscore.clone(this.entity.values)]); - } - } else { - json = this.entity.toObject(); - } - } catch(e) { - console.error(e); - console.error(e.stack); - } - - return json; - }, - - ensureFindOptionsValid: function(findOptions, callback) { - callback(!findOptions ? new Exceptions.InvalidData(util.format('You must specify either an id or an object containing fields to find a %s', that._name)) : null); - } -}; - -module.exports = modelUtils; \ No newline at end of file diff --git a/lib/utils/stacktrace.js b/lib/utils/stacktrace.js index 32373e9..e6d4fc0 100644 --- a/lib/utils/stacktrace.js +++ b/lib/utils/stacktrace.js @@ -1,5 +1,3 @@ -'use strict'; - module.exports = function () { return new Error().stack.split('\n').splice(2); }; diff --git a/package.json b/package.json index 47bb9d0..d3e1f42 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "moment": "~2.9.0", "morgan": "~1.5.1", "nconf": "~0.7.1", + "require-folder-tree": "^1.0.6", "response-time": "~2.3.0", "sendgrid": "~1.6.0", "uberclass": "~1.0.1",