Merge branch 'feature/plugins' of github.com:fabien/loopback-datasource-juggler into fabien-feature/plugins

This commit is contained in:
Raymond Feng 2014-08-07 22:51:25 -07:00
commit 55ff28206f
9 changed files with 236 additions and 1 deletions

View File

@ -1,3 +1,4 @@
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');

87
lib/mixins.js Normal file
View File

@ -0,0 +1,87 @@
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 registry = exports.registry = {};
var modelBuilder = new ModelBuilder();
exports.apply = function applyMixin(modelClass, name, options) {
name = inflection.classify(name.replace(/-/g, '_'));
var fn = registry[name];
if (typeof fn === 'function') {
if (modelClass.dataSource) {
fn(modelClass, options || {});
} else {
modelClass.once('dataSourceAttached', function() {
fn(modelClass, 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);
} 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;
}
} 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) {
}
};

23
lib/mixins/time-stamp.js Normal file
View File

@ -0,0 +1,23 @@
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;
};
};

View File

@ -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 mixins = require('./mixins');
// Set up types
require('./types')(ModelBuilder);
@ -428,6 +429,22 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
ModelClass.registerProperty(propertyName);
}
var mixinSettings = settings.mixins || {};
var keys = Object.keys(mixinSettings);
var 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') {
mixinSettings[name] = true;
mixins.apply(ModelClass, name, mixin);
} else {
// for settings metadata
mixinSettings[name] = false;
}
}
ModelClass.emit('defined', ModelClass);
return ModelClass;

View File

@ -12,6 +12,7 @@ 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 = {
@ -387,7 +388,11 @@ ModelBaseClass.prototype.inspect = function () {
};
ModelBaseClass.mixin = function (anotherClass, options) {
if (typeof anotherClass === 'string') {
mixins.apply(this, anotherClass, options);
} else {
return jutil.mixin(this, anotherClass, options);
}
};
ModelBaseClass.prototype.getDataSource = function () {

12
test/fixtures/mixins/address.json vendored Normal file
View File

@ -0,0 +1,12 @@
{
"properties": {
"street": {
"type": "string",
"required": true
},
"city": {
"type": "string",
"required": true
}
}
}

5
test/fixtures/mixins/demo.js vendored Normal file
View File

@ -0,0 +1,5 @@
module.exports = function timestamps(Model, options) {
Model.demoMixin = options.ok;
};

3
test/fixtures/mixins/other.js vendored Normal file
View File

@ -0,0 +1,3 @@
module.exports = {
otherMixin: true
}

82
test/mixins.test.js Normal file
View File

@ -0,0 +1,82 @@
// 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;
describe('Model class', function () {
it('should define a mixin', function() {
mixins.define('Example', function(Model, options) {
Model.prototype.example = function() {
return options;
};
});
});
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 }
});
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 });
});
it('should apply mixins', function(done) {
var memory = new DataSource({connector: Memory});
var Item = memory.createModel('Item', { name: 'string' }, {
mixins: { TimeStamp: true, demo: { ok: true }, Address: 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;
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();
});
});
});