Merge pull request #34 from strongloop/type-or-relation-resolution
Enable deferred type/relation resolutions
This commit is contained in:
commit
5d92bcb098
|
@ -12,6 +12,7 @@ var EventEmitter = require('events').EventEmitter;
|
|||
var util = require('util');
|
||||
var path = require('path');
|
||||
var fs = require('fs');
|
||||
var assert = require('assert');
|
||||
var async = require('async');
|
||||
|
||||
var existsSync = fs.existsSync || path.existsSync;
|
||||
|
@ -302,6 +303,13 @@ DataSource.prototype.setup = function(name, settings) {
|
|||
};
|
||||
};
|
||||
|
||||
function isModelClass(cls) {
|
||||
if(!cls) {
|
||||
return false;
|
||||
}
|
||||
return cls.prototype instanceof ModelBaseClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a model class
|
||||
*
|
||||
|
@ -364,6 +372,10 @@ DataSource.prototype.createModel = DataSource.prototype.define = function define
|
|||
|
||||
var NewClass = ModelBuilder.prototype.define.call(this, className, properties, settings);
|
||||
|
||||
if(settings.unresolved) {
|
||||
return NewClass;
|
||||
}
|
||||
|
||||
// add data access objects
|
||||
this.mixin(NewClass);
|
||||
|
||||
|
@ -376,6 +388,80 @@ DataSource.prototype.createModel = DataSource.prototype.define = function define
|
|||
});
|
||||
}
|
||||
|
||||
var relations = settings.relationships || settings.relations;
|
||||
|
||||
// Create a function for the closure in the loop
|
||||
var createListener = function (name, relation, targetModel, throughModel) {
|
||||
if (targetModel && targetModel.settings.unresolved) {
|
||||
targetModel.once('defined', function (model) {
|
||||
// Check if the through model doesn't exist or resolved
|
||||
if (!throughModel || !throughModel.settings.unresolved) {
|
||||
// The target model is resolved
|
||||
var params = {
|
||||
foreignKey: relation.foreignKey,
|
||||
as: name,
|
||||
model: model
|
||||
};
|
||||
if (throughModel) {
|
||||
params.through = throughModel;
|
||||
}
|
||||
NewClass[relation.type].call(NewClass, name, params);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (throughModel && throughModel.settings.unresolved) {
|
||||
// Set up a listener to the through model
|
||||
throughModel.once('defined', function (model) {
|
||||
if (!targetModel.settings.unresolved) {
|
||||
// The target model is resolved
|
||||
var params = {
|
||||
foreignKey: relation.foreignKey,
|
||||
as: name,
|
||||
model: targetModel,
|
||||
through: model
|
||||
};
|
||||
NewClass[relation.type].call(NewClass, name, params);
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Set up the relations
|
||||
if (relations) {
|
||||
for (var rn in relations) {
|
||||
var r = relations[rn];
|
||||
assert(['belongsTo', 'hasMany', 'hasAndBelongsToMany'].indexOf(r.type) !== -1, "Invalid relation type: " + r.type);
|
||||
var targetModel = isModelClass(r.model) ? r.model : this.models[r.model];
|
||||
if(!targetModel) {
|
||||
// The target model doesn't exist, let create a place holder for it
|
||||
targetModel = this.define(r.model, {}, {unresolved: true});
|
||||
}
|
||||
var throughModel = null;
|
||||
if(r.through) {
|
||||
throughModel = isModelClass(r.through) ? r.through : this.models[r.through];
|
||||
if(!throughModel) {
|
||||
// The through model doesn't exist, let create a place holder for it
|
||||
throughModel = this.define(r.through, {}, {unresolved: true});
|
||||
}
|
||||
}
|
||||
if(targetModel.settings.unresolved || (throughModel && throughModel.settings.unresolved)) {
|
||||
// Create a listener to defer the relation set up
|
||||
createListener(rn, r, targetModel, throughModel);
|
||||
} else {
|
||||
// The target model is resolved
|
||||
var params = {
|
||||
foreignKey: r.foreignKey,
|
||||
as: rn,
|
||||
model: targetModel
|
||||
};
|
||||
if(throughModel) {
|
||||
params.through = throughModel;
|
||||
}
|
||||
NewClass[r.type].call(NewClass, rn, params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return NewClass;
|
||||
};
|
||||
|
||||
|
|
|
@ -104,34 +104,49 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
settings.strict = false;
|
||||
}
|
||||
|
||||
// every class can receive hash of data as optional param
|
||||
var ModelClass = function ModelConstructor(data, dataSource) {
|
||||
if(!(this instanceof ModelConstructor)) {
|
||||
return new ModelConstructor(data, dataSource);
|
||||
}
|
||||
ModelBaseClass.apply(this, arguments);
|
||||
if(dataSource) {
|
||||
hiddenProperty(this, '__dataSource', dataSource);
|
||||
}
|
||||
};
|
||||
// Check if there is a unresolved model with the same name
|
||||
var ModelClass = this.models[className];
|
||||
|
||||
// mix in EventEmitter (don't inherit from)
|
||||
var events = new EventEmitter();
|
||||
for (var f in EventEmitter.prototype) {
|
||||
if (typeof EventEmitter.prototype[f] === 'function') {
|
||||
ModelClass[f] = events[f].bind(events);
|
||||
if(!ModelClass) {
|
||||
// every class can receive hash of data as optional param
|
||||
ModelClass = function ModelConstructor(data, dataSource) {
|
||||
if(!(this instanceof ModelConstructor)) {
|
||||
return new ModelConstructor(data, dataSource);
|
||||
}
|
||||
if(ModelClass.settings.unresolved) {
|
||||
throw new Error('Model ' + ModelClass.modelName + ' is not defined.');
|
||||
}
|
||||
ModelBaseClass.apply(this, arguments);
|
||||
if(dataSource) {
|
||||
hiddenProperty(this, '__dataSource', dataSource);
|
||||
}
|
||||
};
|
||||
// mix in EventEmitter (don't inherit from)
|
||||
var events = new EventEmitter();
|
||||
for (var f in EventEmitter.prototype) {
|
||||
if (typeof EventEmitter.prototype[f] === 'function') {
|
||||
ModelClass[f] = events[f].bind(events);
|
||||
}
|
||||
}
|
||||
util.inherits(ModelClass, ModelBaseClass);
|
||||
hiddenProperty(ModelClass, 'modelName', className);
|
||||
}
|
||||
|
||||
// store class in model pool
|
||||
this.models[className] = ModelClass;
|
||||
|
||||
// Return the unresolved model
|
||||
if(settings.unresolved) {
|
||||
ModelClass.settings = {unresolved: true};
|
||||
return ModelClass;
|
||||
}
|
||||
|
||||
// Add metadata to the ModelClass
|
||||
hiddenProperty(ModelClass, 'dataSource', dataSource);
|
||||
hiddenProperty(ModelClass, 'schema', dataSource); // For backward compatibility
|
||||
hiddenProperty(ModelClass, 'modelName', className);
|
||||
hiddenProperty(ModelClass, 'pluralModelName', pluralName || inflection.pluralize(className));
|
||||
hiddenProperty(ModelClass, 'relations', {});
|
||||
|
||||
util.inherits(ModelClass, ModelBaseClass);
|
||||
|
||||
// inherit ModelBaseClass static methods
|
||||
for (var i in ModelBaseClass) {
|
||||
if(i !== '_mixins') {
|
||||
|
@ -143,8 +158,7 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
ModelClass.setter = {};
|
||||
|
||||
var modelDefinition = new ModelDefinition(this, className, properties, settings);
|
||||
// store class in model pool
|
||||
this.models[className] = ModelClass;
|
||||
|
||||
this.definitions[className] = modelDefinition;
|
||||
|
||||
// expose properties on the ModelClass
|
||||
|
@ -343,6 +357,8 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
|
||||
ModelClass.forEachProperty(ModelClass.registerProperty);
|
||||
|
||||
ModelClass.emit('defined', ModelClass);
|
||||
|
||||
return ModelClass;
|
||||
|
||||
};
|
||||
|
@ -472,6 +488,8 @@ ModelBuilder.prototype.resolveType = function(type) {
|
|||
if (schemaType) {
|
||||
return schemaType;
|
||||
} else {
|
||||
// The type cannot be resolved, let's create a place holder
|
||||
type = this.define(type, {}, {unresolved: true});
|
||||
return type;
|
||||
}
|
||||
} else if (type.constructor.name === 'Object') {
|
||||
|
|
|
@ -44,16 +44,18 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
|||
var methodName = params.as || i8n.camelize(anotherClass.pluralModelName, true);
|
||||
var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true);
|
||||
|
||||
var idName = this.dataSource.idName(this.modelName) || 'id';
|
||||
|
||||
this.relations[methodName] = {
|
||||
type: 'hasMany',
|
||||
keyFrom: 'id',
|
||||
keyFrom: idName,
|
||||
keyTo: fk,
|
||||
modelTo: anotherClass,
|
||||
multiple: true
|
||||
};
|
||||
// each instance of this class should have method named
|
||||
// pluralize(anotherClass.modelName)
|
||||
// which is actually just anotherClass.all({where: {thisModelNameId: this.id}}, cb);
|
||||
// which is actually just anotherClass.all({where: {thisModelNameId: this[idName]}}, cb);
|
||||
var scopeMethods = {
|
||||
findById: find,
|
||||
destroy: destroy
|
||||
|
@ -69,7 +71,7 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
|||
done = function() {};
|
||||
}
|
||||
var self = this;
|
||||
var id = this.id;
|
||||
var id = this[idName];
|
||||
anotherClass.create(data, function(err, ac) {
|
||||
if (err) return done(err, ac);
|
||||
var d = {};
|
||||
|
@ -89,16 +91,16 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
|||
scopeMethods.add = function(acInst, done) {
|
||||
var data = {};
|
||||
var query = {};
|
||||
query[fk] = this.id;
|
||||
query[fk] = this[idName];
|
||||
data[params.through.relationNameFor(fk)] = this;
|
||||
query[fk2] = acInst.id || acInst;
|
||||
query[fk2] = acInst[idName] || acInst;
|
||||
data[params.through.relationNameFor(fk2)] = acInst;
|
||||
params.through.findOrCreate({where: query}, data, done);
|
||||
};
|
||||
scopeMethods.remove = function(acInst, done) {
|
||||
var self = this;
|
||||
var q = {};
|
||||
q[fk2] = acInst.id || acInst;
|
||||
q[fk2] = acInst[idName] || acInst;
|
||||
params.through.findOne({where: q}, function(err, d) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
|
@ -114,7 +116,7 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
|||
defineScope(this.prototype, params.through || anotherClass, methodName, function () {
|
||||
var filter = {};
|
||||
filter.where = {};
|
||||
filter.where[fk] = this.id;
|
||||
filter.where[fk] = this[idName];
|
||||
if (params.through) {
|
||||
filter.collect = i8n.camelize(anotherClass.modelName, true);
|
||||
filter.include = filter.collect;
|
||||
|
@ -131,7 +133,7 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
|||
anotherClass.findById(id, function (err, inst) {
|
||||
if (err) return cb(err);
|
||||
if (!inst) return cb(new Error('Not found'));
|
||||
if (inst[fk] && inst[fk].toString() == this.id.toString()) {
|
||||
if (inst[fk] && inst[fk].toString() == this[idName].toString()) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
|
@ -144,7 +146,7 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
|||
anotherClass.findById(id, function (err, inst) {
|
||||
if (err) return cb(err);
|
||||
if (!inst) return cb(new Error('Not found'));
|
||||
if (inst[fk] && inst[fk].toString() == self.id.toString()) {
|
||||
if (inst[fk] && inst[fk].toString() == self[idName].toString()) {
|
||||
inst.destroy(cb);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
|
@ -192,13 +194,15 @@ Relation.belongsTo = function (anotherClass, params) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
var idName = this.dataSource.idName(this.modelName) || 'id';
|
||||
var methodName = params.as || i8n.camelize(anotherClass.modelName, true);
|
||||
var fk = params.foreignKey || methodName + 'Id';
|
||||
|
||||
this.relations[methodName] = {
|
||||
type: 'belongsTo',
|
||||
keyFrom: fk,
|
||||
keyTo: 'id',
|
||||
keyTo: idName,
|
||||
modelTo: anotherClass,
|
||||
multiple: false
|
||||
};
|
||||
|
@ -214,7 +218,7 @@ Relation.belongsTo = function (anotherClass, params) {
|
|||
anotherClass.findById(id, function (err,inst) {
|
||||
if (err) return cb(err);
|
||||
if (!inst) return cb(null, null);
|
||||
if (inst.id === this[fk]) {
|
||||
if (inst[idName] === this[fk]) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
|
@ -235,7 +239,7 @@ Relation.belongsTo = function (anotherClass, params) {
|
|||
cachedValue = this.__cachedRelations[methodName];
|
||||
}
|
||||
if (p instanceof ModelBaseClass) { // acts as setter
|
||||
this[fk] = p.id;
|
||||
this[fk] = p[idName];
|
||||
this.__cachedRelations[methodName] = p;
|
||||
} else if (typeof p === 'function') { // acts as async getter
|
||||
if (typeof cachedValue === 'undefined') {
|
||||
|
|
|
@ -157,6 +157,38 @@ describe('ModelBuilder define model', function () {
|
|||
done(null, User);
|
||||
});
|
||||
|
||||
it('should be able to reference models by name before they are defined', function (done) {
|
||||
var modelBuilder = new ModelBuilder();
|
||||
|
||||
var User = modelBuilder.define('User', {name: String, address: 'Address'});
|
||||
|
||||
var user;
|
||||
try {
|
||||
user = new User({name: 'Joe', address: {street: '123 Main St', 'city': 'San Jose', state: 'CA'}});
|
||||
assert(false, 'An exception should have been thrown');
|
||||
} catch (e) {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
var Address = modelBuilder.define('Address', {
|
||||
street: String,
|
||||
city: String,
|
||||
state: String,
|
||||
zipCode: String,
|
||||
country: String
|
||||
});
|
||||
|
||||
user = new User({name: 'Joe', address: {street: '123 Main St', 'city': 'San Jose', state: 'CA'}});
|
||||
|
||||
User.modelName.should.equal('User');
|
||||
User.definition.properties.address.should.have.property('type', Address);
|
||||
user.should.be.a('object');
|
||||
assert(user.name === 'Joe');
|
||||
user.address.should.have.property('city', 'San Jose');
|
||||
user.address.should.have.property('state', 'CA');
|
||||
done(null, User);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
|
@ -387,6 +419,120 @@ describe('DataSource define model', function () {
|
|||
});
|
||||
|
||||
|
||||
describe('Load models with relations', function () {
|
||||
it('should set up relations', function (done) {
|
||||
var ds = new DataSource('memory');
|
||||
|
||||
var Post = ds.define('Post', {userId: Number, content: String});
|
||||
var User = ds.define('User', {name: String}, {relations: {posts: {type: 'hasMany', model: 'Post'}}});
|
||||
|
||||
assert(User.relations['posts']);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should set up belongsTo relations', function (done) {
|
||||
var ds = new DataSource('memory');
|
||||
|
||||
var User = ds.define('User', {name: String});
|
||||
var Post = ds.define('Post', {userId: Number, content: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}});
|
||||
|
||||
assert(Post.relations['user']);
|
||||
done();
|
||||
});
|
||||
|
||||
it('should set up hasMany and belongsTo relations', function (done) {
|
||||
var ds = new DataSource('memory');
|
||||
|
||||
var User = ds.define('User', {name: String}, {relations: {posts: {type: 'hasMany', model: 'Post'}, accounts: {type: 'hasMany', model: 'Account'}}});
|
||||
|
||||
assert(!User.relations['posts']);
|
||||
assert(!User.relations['accounts']);
|
||||
|
||||
var Post = ds.define('Post', {userId: Number, content: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}});
|
||||
|
||||
var Account = ds.define('Account', {userId: Number, type: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}});
|
||||
|
||||
assert(Post.relations['user']);
|
||||
assert.deepEqual(Post.relations['user'], {
|
||||
type: 'belongsTo',
|
||||
keyFrom: 'userId',
|
||||
keyTo: 'id',
|
||||
modelTo: User,
|
||||
multiple: false
|
||||
});
|
||||
assert(User.relations['posts']);
|
||||
assert.deepEqual(User.relations['posts'], {
|
||||
type: 'hasMany',
|
||||
keyFrom: 'id',
|
||||
keyTo: 'userId',
|
||||
modelTo: Post,
|
||||
multiple: true
|
||||
});
|
||||
assert(User.relations['accounts']);
|
||||
assert.deepEqual(User.relations['accounts'], {
|
||||
type: 'hasMany',
|
||||
keyFrom: 'id',
|
||||
keyTo: 'userId',
|
||||
modelTo: Account,
|
||||
multiple: true
|
||||
});
|
||||
|
||||
done();
|
||||
});
|
||||
|
||||
it('should throw if a relation is missing type', function (done) {
|
||||
var ds = new DataSource('memory');
|
||||
|
||||
var Post = ds.define('Post', {userId: Number, content: String});
|
||||
|
||||
try {
|
||||
var User = ds.define('User', {name: String}, {relations: {posts: {model: 'Post'}}});
|
||||
} catch (e) {
|
||||
done();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
it('should throw if the relation type is invalid', function (done) {
|
||||
var ds = new DataSource('memory');
|
||||
|
||||
var Post = ds.define('Post', {userId: Number, content: String});
|
||||
|
||||
try {
|
||||
var User = ds.define('User', {name: String}, {relations: {posts: {type: 'hasXYZ', model: 'Post'}}});
|
||||
} catch (e) {
|
||||
done();
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
it('should handle hasMany through', function (done) {
|
||||
var ds = new DataSource('memory');
|
||||
var Physician = ds.createModel('Physician', {
|
||||
name: String
|
||||
}, {relations: {patients: {model: 'Patient', type: 'hasMany', through: 'Appointment'}}});
|
||||
|
||||
var Patient = ds.createModel('Patient', {
|
||||
name: String
|
||||
}, {relations: {physicians: {model: 'Physician', type: 'hasMany', through: 'Appointment'}}});
|
||||
|
||||
assert(!Physician.relations['patients']); // Appointment hasn't been resolved yet
|
||||
assert(!Patient.relations['physicians']); // Appointment hasn't been resolved yet
|
||||
|
||||
var Appointment = ds.createModel('Appointment', {
|
||||
physicianId: Number,
|
||||
patientId: Number,
|
||||
appointmentDate: Date
|
||||
}, {relations: {patient: {type: 'belongsTo', model: 'Patient'}, physician: {type: 'belongsTo', model: 'Physician'}}});
|
||||
|
||||
assert(Physician.relations['patients']);
|
||||
assert(Patient.relations['physicians']);
|
||||
done();
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
describe('Load models from json', function () {
|
||||
it('should be able to define models from json', function () {
|
||||
var path = require('path'),
|
||||
|
|
Loading…
Reference in New Issue