Basic plugin architecture

Similar to http://mongoosejs.com/docs/plugins.html

Next, loopback-boot should be updated to support loading plugins from
dirs.
This commit is contained in:
Fabien Franzen 2014-08-05 16:16:10 +02:00
parent 5a2bb6548a
commit 1a4e8863ef
6 changed files with 159 additions and 0 deletions

View File

@ -3,6 +3,7 @@ exports.DataSource = exports.Schema = require('./lib/datasource.js').DataSource;
exports.ModelBaseClass = require('./lib/model.js'); exports.ModelBaseClass = require('./lib/model.js');
exports.GeoPoint = require('./lib/geo.js').GeoPoint; exports.GeoPoint = require('./lib/geo.js').GeoPoint;
exports.ValidationError = require('./lib/validations.js').ValidationError; exports.ValidationError = require('./lib/validations.js').ValidationError;
exports.plugins = require('./lib/plugins');
exports.__defineGetter__('version', function () { exports.__defineGetter__('version', function () {
return require('./package.json').version; return require('./package.json').version;

View File

@ -10,6 +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 plugins = require('./plugins');
// Set up types // Set up types
require('./types')(ModelBuilder); require('./types')(ModelBuilder);
@ -428,6 +429,22 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
ModelClass.registerProperty(propertyName); ModelClass.registerProperty(propertyName);
} }
var pluginSettings = settings.plugins || {};
var keys = Object.keys(pluginSettings);
var size = keys.length;
for (i = 0; i < size; i++) {
var name = keys[i];
var plugin = pluginSettings[name];
if (plugin === true) plugin = {};
if (typeof plugin === 'object') {
pluginSettings[name] = true;
plugins.apply(name, ModelClass, plugin);
} else {
// for settings metadata
pluginSettings[name] = false;
}
}
ModelClass.emit('defined', ModelClass); ModelClass.emit('defined', ModelClass);
return ModelClass; return ModelClass;

View File

@ -12,6 +12,7 @@ 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 plugins = require('./plugins');
// Set up an object for quick lookup // Set up an object for quick lookup
var BASE_TYPES = { var BASE_TYPES = {
@ -402,5 +403,9 @@ ModelBaseClass.prototype.setStrict = function (strict) {
this.__strict = strict; this.__strict = strict;
}; };
ModelBaseClass.plugin = function (name, options) {
plugins.apply(name, this, options);
};
jutil.mixin(ModelBaseClass, Hookable); jutil.mixin(ModelBaseClass, Hookable);
jutil.mixin(ModelBaseClass, validations.Validatable); jutil.mixin(ModelBaseClass, validations.Validatable);

69
lib/plugins.js Normal file
View File

@ -0,0 +1,69 @@
var fs = require('fs');
var path = require('path');
var debug = require('debug')('loopback:plugin');
var registry = {};
exports.apply = function applyPlugin(name, modelClass, options) {
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 plugin: %s', name);
}
};
var definePlugin = exports.define = function definePlugin(name, fn) {
if (typeof fn === 'function') {
if (registry[name]) {
debug('Overwriting plugin: %s', name);
} else {
debug('Defined plugin: %s', name);
}
registry[name] = fn;
} else {
debug('Invalid plugin function: %s', name);
}
};
var loadPlugin = exports.load = function loadPlugin(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 plugin = tryRequire(filepath);
if (typeof plugin === 'function') {
definePlugin(name, plugin);
}
}
}
});
};
loadPlugin(path.join(__dirname, 'plugins'));
function tryReadDir() {
try {
return fs.readdirSync.apply(fs, arguments);
} catch(e) {
return [];
}
};
function tryRequire(file) {
try {
return require(file);
} catch(e) {
}
};

23
lib/plugins/timestamps.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;
};
};

44
test/plugins.test.js Normal file
View File

@ -0,0 +1,44 @@
// This test written in mocha+should.js
var should = require('./init.js');
var assert = require('assert');
var jdb = require('../');
var ModelBuilder = jdb.ModelBuilder;
var DataSource = jdb.DataSource;
var Memory = require('../lib/connectors/memory');
var plugins = jdb.plugins;
describe('Model class', function () {
it('should define a plugin', function() {
plugins.define('example', function(Model, options) {
Model.prototype.example = function() {
return options;
};
});
})
it('should apply plugin', function(done) {
var memory = new DataSource({connector: Memory});
var Item = memory.createModel('Item', { name: 'string' }, {
plugins: { timestamps: true }
});
Item.plugin('example', { foo: 'bar' });
var def = memory.getModelDefinition('Item');
var json = def.toJSON();
var properties = json.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();
});
});
});