From f671c9c726761a246fcafc058e940db26b617c13 Mon Sep 17 00:00:00 2001 From: Raymond Feng Date: Fri, 8 Aug 2014 01:20:57 -0700 Subject: [PATCH] Clean up the mixin processing --- index.js | 1 - lib/mixins.js | 118 +++++++++++++----------------- lib/mixins/time-stamp.js | 23 ------ lib/model-builder.js | 19 +++-- lib/model.js | 18 ++++- test/fixtures/mixins/address.json | 12 --- test/fixtures/mixins/demo.js | 5 -- test/fixtures/mixins/other.js | 3 - test/mixins.test.js | 85 +++++++++++---------- 9 files changed, 120 insertions(+), 164 deletions(-) delete mode 100644 lib/mixins/time-stamp.js delete mode 100644 test/fixtures/mixins/address.json delete mode 100644 test/fixtures/mixins/demo.js delete mode 100644 test/fixtures/mixins/other.js diff --git a/index.js b/index.js index de88d0ab..a1e5420e 100644 --- a/index.js +++ b/index.js @@ -1,4 +1,3 @@ -exports.mixins = require('./lib/mixins'); // require before ModelBuilder below - why? exports.ModelBuilder = exports.LDL = require('./lib/model-builder.js').ModelBuilder; exports.DataSource = exports.Schema = require('./lib/datasource.js').DataSource; exports.ModelBaseClass = require('./lib/model.js'); diff --git a/lib/mixins.js b/lib/mixins.js index 2314a0ae..d16d6561 100644 --- a/lib/mixins.js +++ b/lib/mixins.js @@ -1,16 +1,29 @@ -var fs = require('fs'); -var path = require('path'); -var extend = require('util')._extend; -var inflection = require('inflection'); var debug = require('debug')('loopback:mixin'); -var ModelBuilder = require('./model-builder').ModelBuilder; +var assert = require('assert'); +var DefaultModelBaseClass = require('./model.js'); -var registry = exports.registry = {}; -var modelBuilder = new ModelBuilder(); +function isModelClass(cls) { + if (!cls) { + return false; + } + return cls.prototype instanceof DefaultModelBaseClass; +} -exports.apply = function applyMixin(modelClass, name, options) { - name = inflection.classify(name.replace(/-/g, '_')); - var fn = registry[name]; +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 || {}); @@ -20,68 +33,35 @@ exports.apply = function applyMixin(modelClass, name, options) { }); } } else { - debug('Invalid mixin: %s', name); - } -}; - -var defineMixin = exports.define = function defineMixin(name, mixin, ldl) { - if (typeof mixin === 'function' || typeof mixin === 'object') { - name = inflection.classify(name.replace(/-/g, '_')); - if (registry[name]) { - debug('Duplicate mixin: %s', name); + // 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('Defining mixin: %s', name); - } - if (typeof mixin === 'object' && ldl) { - var model = modelBuilder.define(name, mixin); - registry[name] = function(Model, options) { - Model.mixin(model, options); - }; - } else if (typeof mixin === 'object') { - registry[name] = function(Model, options) { - extend(Model.prototype, mixin); - }; - } else if (typeof mixin === 'function') { - registry[name] = mixin; + 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('Invalid mixin function: %s', name); - } -}; - -var loadMixins = exports.load = function loadMixins(dir) { - var files = tryReadDir(path.resolve(dir)); - files.forEach(function(filename) { - var filepath = path.resolve(path.join(dir, filename)); - var ext = path.extname(filename); - var name = path.basename(filename, ext); - var stats = fs.statSync(filepath); - if (stats.isFile()) { - if (ext in require.extensions) { - var mixin = tryRequire(filepath); - if (typeof mixin === 'function' - || typeof mixin === 'object') { - defineMixin(name, mixin, ext === '.json'); - } - } - } - }); -}; - -loadMixins(path.join(__dirname, 'mixins')); - -function tryReadDir() { - try { - return fs.readdirSync.apply(fs, arguments); - } catch(e) { - return []; - } -}; - -function tryRequire(file) { - try { - return require(file); - } catch(e) { + 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/mixins/time-stamp.js b/lib/mixins/time-stamp.js deleted file mode 100644 index b6ee77d9..00000000 --- a/lib/mixins/time-stamp.js +++ /dev/null @@ -1,23 +0,0 @@ -module.exports = 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; - }; - -}; diff --git a/lib/model-builder.js b/lib/model-builder.js index e46ce3a6..73936be5 100644 --- a/lib/model-builder.js +++ b/lib/model-builder.js @@ -10,7 +10,7 @@ var DefaultModelBaseClass = require('./model.js'); var List = require('./list.js'); var ModelDefinition = require('./model-definition.js'); var mergeSettings = require('./utils').mergeSettings; -var mixins = require('./mixins'); +var MixinProvider = require('./mixins'); // Set up types require('./types')(ModelBuilder); @@ -38,6 +38,7 @@ function ModelBuilder() { // create blank models pool this.models = {}; this.definitions = {}; + this.mixins = new MixinProvider(this); this.defaultModelBaseClass = DefaultModelBaseClass; } @@ -189,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 }); @@ -430,18 +431,16 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett } var mixinSettings = settings.mixins || {}; - var keys = Object.keys(mixinSettings); - var size = keys.length; + 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 (mixin === true) { + mixin = {}; + } if (typeof mixin === 'object') { - mixinSettings[name] = true; - mixins.apply(ModelClass, name, mixin); - } else { - // for settings metadata - mixinSettings[name] = false; + modelBuilder.mixins.applyMixin(ModelClass, name, mixin); } } diff --git a/lib/model.js b/lib/model.js index 678226d3..b1c28a99 100644 --- a/lib/model.js +++ b/lib/model.js @@ -12,7 +12,6 @@ var jutil = require('./jutil'); var List = require('./list'); var Hookable = require('./hooks'); var validations = require('./validations.js'); -var mixins = require('./mixins'); // Set up an object for quick lookup var BASE_TYPES = { @@ -202,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) { @@ -389,8 +392,17 @@ ModelBaseClass.prototype.inspect = function () { ModelBaseClass.mixin = function (anotherClass, options) { if (typeof anotherClass === 'string') { - mixins.apply(this, anotherClass, options); + 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); } }; diff --git a/test/fixtures/mixins/address.json b/test/fixtures/mixins/address.json deleted file mode 100644 index b91ff47b..00000000 --- a/test/fixtures/mixins/address.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "properties": { - "street": { - "type": "string", - "required": true - }, - "city": { - "type": "string", - "required": true - } - } -} \ No newline at end of file diff --git a/test/fixtures/mixins/demo.js b/test/fixtures/mixins/demo.js deleted file mode 100644 index 8971ad1a..00000000 --- a/test/fixtures/mixins/demo.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = function timestamps(Model, options) { - - Model.demoMixin = options.ok; - -}; diff --git a/test/fixtures/mixins/other.js b/test/fixtures/mixins/other.js deleted file mode 100644 index b0656196..00000000 --- a/test/fixtures/mixins/other.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - otherMixin: true -} \ No newline at end of file diff --git a/test/mixins.test.js b/test/mixins.test.js index afa88887..f1dacf9d 100644 --- a/test/mixins.test.js +++ b/test/mixins.test.js @@ -1,14 +1,44 @@ // This test written in mocha+should.js var should = require('./init.js'); -var assert = require('assert'); -var path = require('path'); var jdb = require('../'); var ModelBuilder = jdb.ModelBuilder; var DataSource = jdb.DataSource; var Memory = require('../lib/connectors/memory'); -var mixins = jdb.mixins; +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 () { @@ -20,56 +50,35 @@ describe('Model class', function () { }); }); - it('should load mixins from directory', function() { - var expected = [ 'TimeStamp', 'Example', 'Address', 'Demo', 'Other' ]; - mixins.load(path.join(__dirname, 'fixtures', 'mixins')); - mixins.registry.should.have.property('TimeStamp'); - mixins.registry.should.have.property('Example'); - mixins.registry.should.have.property('Address'); - mixins.registry.should.have.property('Demo'); - mixins.registry.should.have.property('Other'); - }); - it('should apply a mixin class', function() { - var memory = new DataSource({connector: Memory}); - var Item = memory.createModel('Item', { name: 'string' }, { - mixins: { TimeStamp: true, demo: true, Address: true } - }); - - var modelBuilder = new ModelBuilder(); 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; - Item.mixin(Address); - - var def = memory.getModelDefinition('Item'); - var properties = def.toJSON().properties; - - // properties.street.should.eql({ type: 'String', required: true }); - // properties.city.should.eql({ type: 'String', required: true }); + 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({connector: Memory}); + var memory = new DataSource('mem', {connector: Memory}, modelBuilder); var Item = memory.createModel('Item', { name: 'string' }, { - mixins: { TimeStamp: true, demo: { ok: true }, Address: true } + mixins: { TimeStamp: true, demo: { ok: true } } }); Item.mixin('Example', { foo: 'bar' }); Item.mixin('other'); - var def = memory.getModelDefinition('Item'); - var properties = def.toJSON().properties; - properties.createdAt.should.eql({ type: 'Date' }); - properties.updatedAt.should.eql({ type: 'Date' }); - - // properties.street.should.eql({ type: 'String', required: true }); - // properties.city.should.eql({ type: 'String', required: true }); - - Item.demoMixin.should.be.true; - Item.prototype.otherMixin.should.be.true; + 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;