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/lib/relation-definition.js b/lib/relation-definition.js index bace4705..1171b998 100644 --- a/lib/relation-definition.js +++ b/lib/relation-definition.js @@ -553,7 +553,7 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) { } return filter; - }, scopeMethods); + }, scopeMethods, definition.options); }; @@ -1600,7 +1600,7 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params) // Mix the property and scoped methods into the prototype class var scopeDefinition = defineScope(modelFrom.prototype, modelTo, accessorName, function () { return {}; - }, scopeMethods); + }, scopeMethods, definition.options); scopeDefinition.related = scopeMethods.related; }; @@ -2014,7 +2014,7 @@ RelationDefinition.referencesMany = function referencesMany(modelFrom, modelTo, // Mix the property and scoped methods into the prototype class var scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function () { return {}; - }, scopeMethods); + }, scopeMethods, definition.options); scopeDefinition.related = scopeMethods.related; // bound to definition }; diff --git a/lib/scope.js b/lib/scope.js index 8f8dda1c..7ee30b3f 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -8,11 +8,12 @@ exports.defineScope = defineScope; exports.mergeQuery = mergeQuery; function ScopeDefinition(definition) { - this.sourceModel = definition.sourceModel; - this.targetModel = definition.targetModel || definition.sourceModel; + this.modelFrom = definition.modelFrom || definition.sourceModel; + this.modelTo = definition.modelTo || definition.targetModel; this.name = definition.name; this.params = definition.params; this.methods = definition.methods; + this.options = definition.options; } ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) { @@ -40,7 +41,7 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres || actualRefresh) { // It either doesn't hit the cache or refresh is required var params = mergeQuery(actualCond, scopeParams); - return this.targetModel.find(params, function (err, data) { + return this.modelTo.find(params, function (err, data) { if (!err && saveOnCache) { defineCachedRelations(self); self.__cachedRelations[name] = data; @@ -62,7 +63,7 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres * to return the query object * @param methods An object of methods keyed by the method name to be bound to the class */ -function defineScope(cls, targetClass, name, params, methods) { +function defineScope(cls, targetClass, name, params, methods, options) { // collect meta info about scope if (!cls._scopeMeta) { @@ -80,13 +81,17 @@ function defineScope(cls, targetClass, name, params, methods) { } var definition = new ScopeDefinition({ - sourceModel: cls, - targetModel: targetClass, + modelFrom: cls, + modelTo: targetClass, name: name, params: params, - methods: methods + methods: methods, + options: options || {} }); + cls.scopes = cls.scopes || {}; + cls.scopes[name] = definition; + // Define a property for the scope Object.defineProperty(cls, name, { enumerable: false, @@ -115,7 +120,7 @@ function defineScope(cls, targetClass, name, params, methods) { f._scope = typeof definition.params === 'function' ? definition.params.call(self) : definition.params; - f._targetClass = definition.targetModel.modelName; + f._targetClass = definition.modelTo.modelName; if (f._scope.collect) { f._targetClass = i8n.capitalize(f._scope.collect); } @@ -255,7 +260,7 @@ function mergeQuery(base, update) { } else { var saved = base.include; base.include = {}; - base.include[update.include] = [saved]; + base.include[update.include] = saved; } } if (update.collect) { diff --git a/package.json b/package.json index 9920da04..d1ba358b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "loopback-datasource-juggler", - "version": "2.2.2", + "version": "2.3.0", "description": "LoopBack DataSoure Juggler", "keywords": [ "StrongLoop", 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(); + }); + }); + +}); diff --git a/test/relations.test.js b/test/relations.test.js index 6f827760..d1a2003a 100644 --- a/test/relations.test.js +++ b/test/relations.test.js @@ -284,17 +284,17 @@ describe('relations', function () { Address.create({name: 'z'}, function (err, address) { patient.address(address); patient.save(function() { - verify(physician); + verify(physician, address.id); }); }); }); }); - function verify(physician) { + function verify(physician, addressId) { physician.patients({include: 'address'}, function (err, ch) { should.not.exist(err); should.exist(ch); ch.should.have.lengthOf(1); - ch[0].addressId.should.equal(1); + ch[0].addressId.should.eql(addressId); var address = ch[0].address(); should.exist(address); address.should.be.an.instanceof(Address); diff --git a/test/scope.test.js b/test/scope.test.js index ecaa8727..2060c80e 100644 --- a/test/scope.test.js +++ b/test/scope.test.js @@ -9,6 +9,14 @@ describe('scope', function () { db = getSchema(); Railway = db.define('Railway', { URID: {type: String, index: true} + }, { + scopes: { + highSpeed: { + where: { + highSpeed: true + } + } + } }); Station = db.define('Station', { USID: {type: String, index: true}, @@ -24,9 +32,15 @@ describe('scope', function () { Station.destroyAll(done); }); }); + + it('should define scope using options.scopes', function () { + Railway.scopes.should.have.property('highSpeed'); + Railway.highSpeed.should.be.function; + }); it('should define scope with query', function (done) { Station.scope('active', {where: {isActive: true}}); + Station.scopes.should.have.property('active'); Station.active.create(function (err, station) { should.not.exist(err); should.exist(station);