diff --git a/lib/mixins.js b/lib/mixins.js new file mode 100644 index 00000000..d16d6561 --- /dev/null +++ b/lib/mixins.js @@ -0,0 +1,67 @@ +var debug = require('debug')('loopback:mixin'); +var assert = require('assert'); +var DefaultModelBaseClass = require('./model.js'); + +function isModelClass(cls) { + if (!cls) { + return false; + } + return cls.prototype instanceof DefaultModelBaseClass; +} + +module.exports = MixinProvider; + +function MixinProvider(modelBuilder) { + this.modelBuilder = modelBuilder; + this.mixins = {}; +} + +/** + * Apply named mixin to the model class + * @param {Model} modelClass + * @param {String} name + * @param {Object} options + */ +MixinProvider.prototype.applyMixin = function applyMixin(modelClass, name, options) { + var fn = this.mixins[name]; + if (typeof fn === 'function') { + if (modelClass.dataSource) { + fn(modelClass, options || {}); + } else { + modelClass.once('dataSourceAttached', function() { + fn(modelClass, options || {}); + }); + } + } else { + // Try model name + var model = this.modelBuilder.getModel(name); + if(model) { + debug('Mixin is resolved to a model: %s', name); + modelClass.mixin(model, options); + } else { + debug('Invalid mixin: %s', name); + } + } +}; + +/** + * Define a mixin with name + * @param {String} name Name of the mixin + * @param {*) mixin The mixin function or a model + */ +MixinProvider.prototype.define = function defineMixin(name, mixin) { + assert(typeof mixin === 'function', 'The mixin must be a function or model class'); + if (this.mixins[name]) { + debug('Duplicate mixin: %s', name); + } else { + debug('Defining mixin: %s', name); + } + if (isModelClass(mixin)) { + this.mixins[name] = function (Model, options) { + Model.mixin(mixin, options); + }; + } else if (typeof mixin === 'function') { + this.mixins[name] = mixin; + } +}; + diff --git a/lib/model-builder.js b/lib/model-builder.js index aa052dea..73936be5 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -10,6 +10,7 @@ var DefaultModelBaseClass = require('./model.js'); var List = require('./list.js'); var ModelDefinition = require('./model-definition.js'); var mergeSettings = require('./utils').mergeSettings; +var MixinProvider = require('./mixins'); // Set up types require('./types')(ModelBuilder); @@ -37,6 +38,7 @@ function ModelBuilder() { // create blank models pool this.models = {}; this.definitions = {}; + this.mixins = new MixinProvider(this); this.defaultModelBaseClass = DefaultModelBaseClass; } @@ -188,7 +190,7 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett // Add metadata to the ModelClass hiddenProperty(ModelClass, 'modelBuilder', modelBuilder); - hiddenProperty(ModelClass, 'dataSource', modelBuilder); // Keep for back-compatibility + hiddenProperty(ModelClass, 'dataSource', null); // Keep for back-compatibility hiddenProperty(ModelClass, 'pluralModelName', pluralName); hiddenProperty(ModelClass, 'relations', {}); hiddenProperty(ModelClass, 'http', { path: '/' + pathName }); @@ -428,6 +430,20 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett ModelClass.registerProperty(propertyName); } + var mixinSettings = settings.mixins || {}; + keys = Object.keys(mixinSettings); + size = keys.length; + for (i = 0; i < size; i++) { + var name = keys[i]; + var mixin = mixinSettings[name]; + if (mixin === true) { + mixin = {}; + } + if (typeof mixin === 'object') { + modelBuilder.mixins.applyMixin(ModelClass, name, mixin); + } + } + ModelClass.emit('defined', ModelClass); return ModelClass; diff --git a/lib/model.js b/lib/model.js index 51a59397..b1c28a99 100644 --- a/lib/model.js +++ b/lib/model.js @@ -201,7 +201,11 @@ ModelBaseClass.prototype._initProperties = function (data, options) { * @param {Object} params Various property configuration */ ModelBaseClass.defineProperty = function (prop, params) { - this.dataSource.defineProperty(this.modelName, prop, params); + if(this.dataSource) { + this.dataSource.defineProperty(this.modelName, prop, params); + } else { + this.modelBuilder.defineProperty(this.modelName, prop, params); + } }; ModelBaseClass.getPropertyType = function (propName) { @@ -387,7 +391,20 @@ ModelBaseClass.prototype.inspect = function () { }; ModelBaseClass.mixin = function (anotherClass, options) { - return jutil.mixin(this, anotherClass, options); + if (typeof anotherClass === 'string') { + this.modelBuilder.mixins.applyMixin(this, anotherClass, options); + } else { + if (anotherClass.prototype instanceof ModelBaseClass) { + var props = anotherClass.definition.properties; + for (var i in props) { + if (this.definition.properties[i]) { + continue; + } + this.defineProperty(i, props[i]); + } + } + return jutil.mixin(this, anotherClass, options); + } }; ModelBaseClass.prototype.getDataSource = function () { diff --git a/test/mixins.test.js b/test/mixins.test.js new file mode 100644 index 00000000..f1dacf9d --- /dev/null +++ b/test/mixins.test.js @@ -0,0 +1,91 @@ +// This test written in mocha+should.js +var should = require('./init.js'); + +var jdb = require('../'); +var ModelBuilder = jdb.ModelBuilder; +var DataSource = jdb.DataSource; +var Memory = require('../lib/connectors/memory'); + +var modelBuilder = new ModelBuilder(); +var mixins = modelBuilder.mixins; + +function timestamps(Model, options) { + + Model.defineProperty('createdAt', { type: Date }); + Model.defineProperty('updatedAt', { type: Date }); + + var originalBeforeSave = Model.beforeSave; + Model.beforeSave = function(next, data) { + Model.applyTimestamps(data, this.isNewRecord()); + if (data.createdAt) { + this.createdAt = data.createdAt; + } + if (data.updatedAt) { + this.updatedAt = data.updatedAt; + } + if (originalBeforeSave) { + originalBeforeSave.apply(this, arguments); + } else { + next(); + } + }; + + Model.applyTimestamps = function(data, creation) { + data.updatedAt = new Date(); + if (creation) { + data.createdAt = data.updatedAt; + } + }; +} + +mixins.define('TimeStamp', timestamps); + +describe('Model class', function () { + + it('should define a mixin', function() { + mixins.define('Example', function(Model, options) { + Model.prototype.example = function() { + return options; + }; + }); + }); + + it('should apply a mixin class', function() { + var Address = modelBuilder.define('Address', { + street: { type: 'string', required: true }, + city: { type: 'string', required: true } + }); + + var memory = new DataSource('mem', {connector: Memory}, modelBuilder); + var Item = memory.createModel('Item', { name: 'string' }, { + mixins: { TimeStamp: true, demo: true, Address: true } + }); + + var properties = Item.definition.properties; + + properties.street.should.eql({ type: String, required: true }); + properties.city.should.eql({ type: String, required: true }); + }); + + it('should apply mixins', function(done) { + var memory = new DataSource('mem', {connector: Memory}, modelBuilder); + var Item = memory.createModel('Item', { name: 'string' }, { + mixins: { TimeStamp: true, demo: { ok: true } } + }); + + Item.mixin('Example', { foo: 'bar' }); + Item.mixin('other'); + + var properties = Item.definition.properties; + properties.createdAt.should.eql({ type: Date }); + properties.updatedAt.should.eql({ type: Date }); + + Item.create({ name: 'Item 1' }, function(err, inst) { + inst.createdAt.should.be.a.date; + inst.updatedAt.should.be.a.date; + inst.example().should.eql({ foo: 'bar' }); + done(); + }); + }); + +});