diff --git a/index.js b/index.js index a1e5420e..eca8ba9b 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ exports.DataSource = exports.Schema = require('./lib/datasource.js').DataSource; exports.ModelBaseClass = require('./lib/model.js'); exports.GeoPoint = require('./lib/geo.js').GeoPoint; exports.ValidationError = require('./lib/validations.js').ValidationError; +exports.plugins = require('./lib/plugins'); exports.__defineGetter__('version', function () { return require('./package.json').version; diff --git a/lib/model-builder.js b/lib/model-builder.js index aa052dea..9fef88b6 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 plugins = require('./plugins'); // Set up types require('./types')(ModelBuilder); @@ -428,6 +429,22 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett 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); return ModelClass; diff --git a/lib/model.js b/lib/model.js index 51a59397..1709a226 100644 --- a/lib/model.js +++ b/lib/model.js @@ -12,6 +12,7 @@ var jutil = require('./jutil'); var List = require('./list'); var Hookable = require('./hooks'); var validations = require('./validations.js'); +var plugins = require('./plugins'); // Set up an object for quick lookup var BASE_TYPES = { @@ -402,5 +403,9 @@ ModelBaseClass.prototype.setStrict = function (strict) { this.__strict = strict; }; +ModelBaseClass.plugin = function (name, options) { + plugins.apply(name, this, options); +}; + jutil.mixin(ModelBaseClass, Hookable); jutil.mixin(ModelBaseClass, validations.Validatable); diff --git a/lib/plugins.js b/lib/plugins.js new file mode 100644 index 00000000..9788aed6 --- /dev/null +++ b/lib/plugins.js @@ -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) { + } +}; + diff --git a/lib/plugins/timestamps.js b/lib/plugins/timestamps.js new file mode 100644 index 00000000..91a4af0f --- /dev/null +++ b/lib/plugins/timestamps.js @@ -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; + }; + +}; \ No newline at end of file diff --git a/test/plugins.test.js b/test/plugins.test.js new file mode 100644 index 00000000..353d0046 --- /dev/null +++ b/test/plugins.test.js @@ -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(); + }); + }); + +}); \ No newline at end of file