Clean up the mixin processing
This commit is contained in:
parent
55ff28206f
commit
f671c9c726
1
index.js
1
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.ModelBuilder = exports.LDL = require('./lib/model-builder.js').ModelBuilder;
|
||||||
exports.DataSource = exports.Schema = require('./lib/datasource.js').DataSource;
|
exports.DataSource = exports.Schema = require('./lib/datasource.js').DataSource;
|
||||||
exports.ModelBaseClass = require('./lib/model.js');
|
exports.ModelBaseClass = require('./lib/model.js');
|
||||||
|
|
118
lib/mixins.js
118
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 debug = require('debug')('loopback:mixin');
|
||||||
var ModelBuilder = require('./model-builder').ModelBuilder;
|
var assert = require('assert');
|
||||||
|
var DefaultModelBaseClass = require('./model.js');
|
||||||
|
|
||||||
var registry = exports.registry = {};
|
function isModelClass(cls) {
|
||||||
var modelBuilder = new ModelBuilder();
|
if (!cls) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return cls.prototype instanceof DefaultModelBaseClass;
|
||||||
|
}
|
||||||
|
|
||||||
exports.apply = function applyMixin(modelClass, name, options) {
|
module.exports = MixinProvider;
|
||||||
name = inflection.classify(name.replace(/-/g, '_'));
|
|
||||||
var fn = registry[name];
|
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 (typeof fn === 'function') {
|
||||||
if (modelClass.dataSource) {
|
if (modelClass.dataSource) {
|
||||||
fn(modelClass, options || {});
|
fn(modelClass, options || {});
|
||||||
|
@ -20,68 +33,35 @@ exports.apply = function applyMixin(modelClass, name, options) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debug('Invalid mixin: %s', name);
|
// Try model name
|
||||||
}
|
var model = this.modelBuilder.getModel(name);
|
||||||
};
|
if(model) {
|
||||||
|
debug('Mixin is resolved to a model: %s', name);
|
||||||
var defineMixin = exports.define = function defineMixin(name, mixin, ldl) {
|
modelClass.mixin(model, options);
|
||||||
if (typeof mixin === 'function' || typeof mixin === 'object') {
|
|
||||||
name = inflection.classify(name.replace(/-/g, '_'));
|
|
||||||
if (registry[name]) {
|
|
||||||
debug('Duplicate mixin: %s', name);
|
|
||||||
} else {
|
} else {
|
||||||
debug('Defining mixin: %s', name);
|
debug('Invalid 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;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
} else {
|
||||||
debug('Invalid mixin function: %s', name);
|
debug('Defining mixin: %s', name);
|
||||||
}
|
}
|
||||||
};
|
if (isModelClass(mixin)) {
|
||||||
|
this.mixins[name] = function (Model, options) {
|
||||||
var loadMixins = exports.load = function loadMixins(dir) {
|
Model.mixin(mixin, options);
|
||||||
var files = tryReadDir(path.resolve(dir));
|
};
|
||||||
files.forEach(function(filename) {
|
} else if (typeof mixin === 'function') {
|
||||||
var filepath = path.resolve(path.join(dir, filename));
|
this.mixins[name] = mixin;
|
||||||
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) {
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -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;
|
|
||||||
};
|
|
||||||
|
|
||||||
};
|
|
|
@ -10,7 +10,7 @@ var DefaultModelBaseClass = require('./model.js');
|
||||||
var List = require('./list.js');
|
var List = require('./list.js');
|
||||||
var ModelDefinition = require('./model-definition.js');
|
var ModelDefinition = require('./model-definition.js');
|
||||||
var mergeSettings = require('./utils').mergeSettings;
|
var mergeSettings = require('./utils').mergeSettings;
|
||||||
var mixins = require('./mixins');
|
var MixinProvider = require('./mixins');
|
||||||
|
|
||||||
// Set up types
|
// Set up types
|
||||||
require('./types')(ModelBuilder);
|
require('./types')(ModelBuilder);
|
||||||
|
@ -38,6 +38,7 @@ function ModelBuilder() {
|
||||||
// create blank models pool
|
// create blank models pool
|
||||||
this.models = {};
|
this.models = {};
|
||||||
this.definitions = {};
|
this.definitions = {};
|
||||||
|
this.mixins = new MixinProvider(this);
|
||||||
this.defaultModelBaseClass = DefaultModelBaseClass;
|
this.defaultModelBaseClass = DefaultModelBaseClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,7 +190,7 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
||||||
|
|
||||||
// Add metadata to the ModelClass
|
// Add metadata to the ModelClass
|
||||||
hiddenProperty(ModelClass, 'modelBuilder', modelBuilder);
|
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, 'pluralModelName', pluralName);
|
||||||
hiddenProperty(ModelClass, 'relations', {});
|
hiddenProperty(ModelClass, 'relations', {});
|
||||||
hiddenProperty(ModelClass, 'http', { path: '/' + pathName });
|
hiddenProperty(ModelClass, 'http', { path: '/' + pathName });
|
||||||
|
@ -430,18 +431,16 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
||||||
}
|
}
|
||||||
|
|
||||||
var mixinSettings = settings.mixins || {};
|
var mixinSettings = settings.mixins || {};
|
||||||
var keys = Object.keys(mixinSettings);
|
keys = Object.keys(mixinSettings);
|
||||||
var size = keys.length;
|
size = keys.length;
|
||||||
for (i = 0; i < size; i++) {
|
for (i = 0; i < size; i++) {
|
||||||
var name = keys[i];
|
var name = keys[i];
|
||||||
var mixin = mixinSettings[name];
|
var mixin = mixinSettings[name];
|
||||||
if (mixin === true) mixin = {};
|
if (mixin === true) {
|
||||||
|
mixin = {};
|
||||||
|
}
|
||||||
if (typeof mixin === 'object') {
|
if (typeof mixin === 'object') {
|
||||||
mixinSettings[name] = true;
|
modelBuilder.mixins.applyMixin(ModelClass, name, mixin);
|
||||||
mixins.apply(ModelClass, name, mixin);
|
|
||||||
} else {
|
|
||||||
// for settings metadata
|
|
||||||
mixinSettings[name] = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
18
lib/model.js
18
lib/model.js
|
@ -12,7 +12,6 @@ var jutil = require('./jutil');
|
||||||
var List = require('./list');
|
var List = require('./list');
|
||||||
var Hookable = require('./hooks');
|
var Hookable = require('./hooks');
|
||||||
var validations = require('./validations.js');
|
var validations = require('./validations.js');
|
||||||
var mixins = require('./mixins');
|
|
||||||
|
|
||||||
// Set up an object for quick lookup
|
// Set up an object for quick lookup
|
||||||
var BASE_TYPES = {
|
var BASE_TYPES = {
|
||||||
|
@ -202,7 +201,11 @@ ModelBaseClass.prototype._initProperties = function (data, options) {
|
||||||
* @param {Object} params Various property configuration
|
* @param {Object} params Various property configuration
|
||||||
*/
|
*/
|
||||||
ModelBaseClass.defineProperty = function (prop, params) {
|
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) {
|
ModelBaseClass.getPropertyType = function (propName) {
|
||||||
|
@ -389,8 +392,17 @@ ModelBaseClass.prototype.inspect = function () {
|
||||||
|
|
||||||
ModelBaseClass.mixin = function (anotherClass, options) {
|
ModelBaseClass.mixin = function (anotherClass, options) {
|
||||||
if (typeof anotherClass === 'string') {
|
if (typeof anotherClass === 'string') {
|
||||||
mixins.apply(this, anotherClass, options);
|
this.modelBuilder.mixins.applyMixin(this, anotherClass, options);
|
||||||
} else {
|
} 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);
|
return jutil.mixin(this, anotherClass, options);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"properties": {
|
|
||||||
"street": {
|
|
||||||
"type": "string",
|
|
||||||
"required": true
|
|
||||||
},
|
|
||||||
"city": {
|
|
||||||
"type": "string",
|
|
||||||
"required": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
module.exports = function timestamps(Model, options) {
|
|
||||||
|
|
||||||
Model.demoMixin = options.ok;
|
|
||||||
|
|
||||||
};
|
|
|
@ -1,3 +0,0 @@
|
||||||
module.exports = {
|
|
||||||
otherMixin: true
|
|
||||||
}
|
|
|
@ -1,14 +1,44 @@
|
||||||
// This test written in mocha+should.js
|
// This test written in mocha+should.js
|
||||||
var should = require('./init.js');
|
var should = require('./init.js');
|
||||||
var assert = require('assert');
|
|
||||||
var path = require('path');
|
|
||||||
|
|
||||||
var jdb = require('../');
|
var jdb = require('../');
|
||||||
var ModelBuilder = jdb.ModelBuilder;
|
var ModelBuilder = jdb.ModelBuilder;
|
||||||
var DataSource = jdb.DataSource;
|
var DataSource = jdb.DataSource;
|
||||||
var Memory = require('../lib/connectors/memory');
|
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 () {
|
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() {
|
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', {
|
var Address = modelBuilder.define('Address', {
|
||||||
street: { type: 'string', required: true },
|
street: { type: 'string', required: true },
|
||||||
city: { type: 'string', required: true }
|
city: { type: 'string', required: true }
|
||||||
});
|
});
|
||||||
|
|
||||||
Item.mixin(Address);
|
var memory = new DataSource('mem', {connector: Memory}, modelBuilder);
|
||||||
|
var Item = memory.createModel('Item', { name: 'string' }, {
|
||||||
|
mixins: { TimeStamp: true, demo: true, Address: true }
|
||||||
|
});
|
||||||
|
|
||||||
var def = memory.getModelDefinition('Item');
|
var properties = Item.definition.properties;
|
||||||
var properties = def.toJSON().properties;
|
|
||||||
|
|
||||||
// properties.street.should.eql({ type: 'String', required: true });
|
properties.street.should.eql({ type: String, required: true });
|
||||||
// properties.city.should.eql({ type: 'String', required: true });
|
properties.city.should.eql({ type: String, required: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should apply mixins', function(done) {
|
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' }, {
|
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('Example', { foo: 'bar' });
|
||||||
Item.mixin('other');
|
Item.mixin('other');
|
||||||
|
|
||||||
var def = memory.getModelDefinition('Item');
|
var properties = Item.definition.properties;
|
||||||
var properties = def.toJSON().properties;
|
properties.createdAt.should.eql({ type: Date });
|
||||||
properties.createdAt.should.eql({ type: 'Date' });
|
properties.updatedAt.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;
|
|
||||||
|
|
||||||
Item.create({ name: 'Item 1' }, function(err, inst) {
|
Item.create({ name: 'Item 1' }, function(err, inst) {
|
||||||
inst.createdAt.should.be.a.date;
|
inst.createdAt.should.be.a.date;
|
||||||
|
|
Loading…
Reference in New Issue