Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
87 changes: 87 additions & 0 deletions lib/mixins.js
Original file line number Diff line number Diff line change
@@ -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 changes: 23 additions & 0 deletions lib/mixins/time-stamp.js
Original file line number Diff line number Diff line change
@@ -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;
};

};
17 changes: 17 additions & 0 deletions lib/model-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
7 changes: 6 additions & 1 deletion lib/model.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -387,7 +388,11 @@ ModelBaseClass.prototype.inspect = function () {
};

ModelBaseClass.mixin = function (anotherClass, options) {
return jutil.mixin(this, anotherClass, options);
if (typeof anotherClass === 'string') {
mixins.apply(this, anotherClass, options);
} else {
return jutil.mixin(this, anotherClass, options);
}
};

ModelBaseClass.prototype.getDataSource = function () {
Expand Down
12 changes: 12 additions & 0 deletions test/fixtures/mixins/address.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"properties": {
"street": {
"type": "string",
"required": true
},
"city": {
"type": "string",
"required": true
}
}
}
5 changes: 5 additions & 0 deletions test/fixtures/mixins/demo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = function timestamps(Model, options) {

Model.demoMixin = options.ok;

};
3 changes: 3 additions & 0 deletions test/fixtures/mixins/other.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
otherMixin: true
}
82 changes: 82 additions & 0 deletions test/mixins.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});

});