Refactor relation into classes
This commit is contained in:
parent
0c13f8c23a
commit
1406c22a64
|
@ -0,0 +1,641 @@
|
|||
/*!
|
||||
* Dependencies
|
||||
*/
|
||||
var assert = require('assert');
|
||||
var util = require('util');
|
||||
var i8n = require('inflection');
|
||||
var defineScope = require('./scope.js').defineScope;
|
||||
var ModelBaseClass = require('./model.js');
|
||||
|
||||
exports.Relation = Relation;
|
||||
exports.RelationDefinition = RelationDefinition;
|
||||
|
||||
var RelationTypes = {
|
||||
belongsTo: 'belongsTo',
|
||||
hasMany: 'hasMany',
|
||||
hasOne: 'hasOne',
|
||||
hasAndBelongsToMany: 'hasAndBelongsToMany'
|
||||
};
|
||||
|
||||
exports.RelationTypes = RelationTypes;
|
||||
exports.HasMany = HasMany;
|
||||
exports.HasManyThrough = HasManyThrough;
|
||||
exports.HasOne = HasOne;
|
||||
exports.HasAndBelongsToMany = HasAndBelongsToMany;
|
||||
exports.BelongsTo = BelongsTo;
|
||||
|
||||
var RelationClasses = {
|
||||
belongsTo: BelongsTo,
|
||||
hasMany: HasMany,
|
||||
hasManyThrough: HasManyThrough,
|
||||
hasOne: HasOne,
|
||||
hasAndBelongsToMany: HasAndBelongsToMany
|
||||
};
|
||||
|
||||
function normalizeType(type) {
|
||||
if (!type) {
|
||||
return type;
|
||||
}
|
||||
var t1 = type.toLowerCase();
|
||||
for (var t2 in RelationTypes) {
|
||||
if (t2.toLowerCase() === t1) {
|
||||
return t2;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Relation definition class. Use to define relationships between models.
|
||||
* @param {Object} definition
|
||||
* @class RelationDefinition
|
||||
*/
|
||||
function RelationDefinition(definition) {
|
||||
if (!(this instanceof RelationDefinition)) {
|
||||
return new RelationDefinition(definition);
|
||||
}
|
||||
definition = definition || {};
|
||||
this.name = definition.name;
|
||||
assert(this.name, 'Relation name is missing');
|
||||
this.type = normalizeType(definition.type);
|
||||
assert(this.type, 'Invalid relation type: ' + definition.type);
|
||||
this.modelFrom = definition.modelFrom;
|
||||
assert(this.modelFrom);
|
||||
this.keyFrom = definition.keyFrom;
|
||||
this.modelTo = definition.modelTo;
|
||||
assert(this.modelTo);
|
||||
this.keyTo = definition.keyTo;
|
||||
this.modelThrough = definition.modelThrough;
|
||||
this.keyThrough = definition.keyThrough;
|
||||
this.multiple = (this.type !== 'belongsTo' && this.type !== 'hasOne');
|
||||
}
|
||||
|
||||
RelationDefinition.prototype.toJSON = function () {
|
||||
var json = {
|
||||
name: this.name,
|
||||
type: this.type,
|
||||
modelFrom: this.modelFrom.modelName,
|
||||
keyFrom: this.keyFrom,
|
||||
modelTo: this.modelTo.modelName,
|
||||
keyTo: this.keyTo,
|
||||
multiple: this.multiple
|
||||
};
|
||||
if (this.modelThrough) {
|
||||
json.modelThrough = this.modelThrough.modelName;
|
||||
json.keyThrough = this.keyThrough;
|
||||
}
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* A relation attaching to a given model instance
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {Relation}
|
||||
* @constructor
|
||||
* @class Relation
|
||||
*/
|
||||
function Relation(definition, modelInstance) {
|
||||
if (!(this instanceof Relation)) {
|
||||
return new Relation(definition, modelInstance);
|
||||
}
|
||||
if (!(definition instanceof RelationDefinition)) {
|
||||
definition = new RelationDefinition(definition);
|
||||
}
|
||||
this.definition = definition;
|
||||
this.modelInstance = modelInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* HasMany subclass
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {HasMany}
|
||||
* @constructor
|
||||
* @class HasMany
|
||||
*/
|
||||
function HasMany(definition, modelInstance) {
|
||||
if (!(this instanceof HasMany)) {
|
||||
return new HasMany(definition, modelInstance);
|
||||
}
|
||||
assert(definition.type === RelationTypes.hasMany);
|
||||
Relation.apply(this, arguments);
|
||||
}
|
||||
|
||||
util.inherits(HasMany, Relation);
|
||||
|
||||
/**
|
||||
* HasManyThrough subclass
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {HasManyThrough}
|
||||
* @constructor
|
||||
* @class HasManyThrough
|
||||
*/
|
||||
function HasManyThrough(definition, modelInstance) {
|
||||
if (!(this instanceof HasManyThrough)) {
|
||||
return new HasManyThrough(definition, modelInstance);
|
||||
}
|
||||
assert(definition.type === RelationTypes.hasMany);
|
||||
assert(definition.modelThrough);
|
||||
HasMany.apply(this, arguments);
|
||||
}
|
||||
|
||||
util.inherits(HasManyThrough, HasMany);
|
||||
|
||||
/**
|
||||
* BelongsTo subclass
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {BelongsTo}
|
||||
* @constructor
|
||||
* @class BelongsTo
|
||||
*/
|
||||
function BelongsTo(definition, modelInstance) {
|
||||
if (!(this instanceof BelongsTo)) {
|
||||
return new BelongsTo(definition, modelInstance);
|
||||
}
|
||||
assert(definition.type === RelationTypes.belongsTo);
|
||||
Relation.apply(this, arguments);
|
||||
}
|
||||
|
||||
util.inherits(BelongsTo, Relation);
|
||||
|
||||
/**
|
||||
* HasAndBelongsToMany subclass
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {HasAndBelongsToMany}
|
||||
* @constructor
|
||||
* @class HasAndBelongsToMany
|
||||
*/
|
||||
function HasAndBelongsToMany(definition, modelInstance) {
|
||||
if (!(this instanceof HasAndBelongsToMany)) {
|
||||
return new HasAndBelongsToMany(definition, modelInstance);
|
||||
}
|
||||
assert(definition.type === RelationTypes.hasAndBelongsToMany);
|
||||
Relation.apply(this, arguments);
|
||||
}
|
||||
|
||||
util.inherits(HasAndBelongsToMany, Relation);
|
||||
|
||||
/**
|
||||
* HasOne subclass
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {HasOne}
|
||||
* @constructor
|
||||
* @class HasOne
|
||||
*/
|
||||
function HasOne(definition, modelInstance) {
|
||||
if (!(this instanceof HasOne)) {
|
||||
return new HasOne(definition, modelInstance);
|
||||
}
|
||||
assert(definition.type === RelationTypes.hasOne);
|
||||
Relation.apply(this, arguments);
|
||||
}
|
||||
|
||||
util.inherits(HasOne, Relation);
|
||||
|
||||
|
||||
/*!
|
||||
* Find the relation by foreign key
|
||||
* @param {*} foreignKey The foreign key
|
||||
* @returns {Object} The relation object
|
||||
*/
|
||||
function findBelongsTo(model, foreignKey) {
|
||||
var relations = model.relations;
|
||||
var keys = Object.keys(relations);
|
||||
for (var k = 0; k < keys.length; k++) {
|
||||
var rel = relations[keys[k]];
|
||||
if (rel.type === RelationTypes.belongsTo &&
|
||||
rel.keyFrom === foreignKey) {
|
||||
return keys[k];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/*!
|
||||
* Look up a model by name from the list of given models
|
||||
* @param {Object} models Models keyed by name
|
||||
* @param {String} modelName The model name
|
||||
* @returns {*} The matching model class
|
||||
*/
|
||||
function lookupModel(models, modelName) {
|
||||
if(models[modelName]) {
|
||||
return models[modelName];
|
||||
}
|
||||
var lookupClassName = modelName.toLowerCase();
|
||||
for (var name in models) {
|
||||
if (name.toLowerCase() === lookupClassName) {
|
||||
return models[name];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a "one to many" relationship by specifying the model name
|
||||
*
|
||||
* Examples:
|
||||
* ```
|
||||
* User.hasMany(Post, {as: 'posts', foreignKey: 'authorId'});
|
||||
* ```
|
||||
*
|
||||
* ```
|
||||
* Book.hasMany(Chapter);
|
||||
* ```
|
||||
* Or, equivalently:
|
||||
* ```
|
||||
* Book.hasMany('chapters', {model: Chapter});
|
||||
* ```
|
||||
* @param {Model} modelFrom Source model class
|
||||
* @param {Object|String} modelTo Model object (or String name of model) to which you are creating the relationship.
|
||||
* @options {Object} params Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Property name of foreign key field.
|
||||
* @property {Object} model Model object
|
||||
*/
|
||||
RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
|
||||
var thisClassName = modelFrom.modelName;
|
||||
params = params || {};
|
||||
if (typeof modelTo === 'string') {
|
||||
params.as = modelTo;
|
||||
if (params.model) {
|
||||
modelTo = params.model;
|
||||
} else {
|
||||
var modelToName = i8n.singularize(modelTo).toLowerCase();
|
||||
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
|
||||
}
|
||||
}
|
||||
var relationName = params.as || i8n.camelize(modelTo.pluralModelName, true);
|
||||
var fk = params.foreignKey || i8n.camelize(thisClassName + '_id', true);
|
||||
|
||||
var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
|
||||
|
||||
var definition = new RelationDefinition({
|
||||
name: relationName,
|
||||
type: RelationTypes.hasMany,
|
||||
modelFrom: modelFrom,
|
||||
keyFrom: idName,
|
||||
keyTo: fk,
|
||||
modelTo: modelTo,
|
||||
multiple: true
|
||||
});
|
||||
|
||||
if (params.through) {
|
||||
definition.modelThrough = params.through;
|
||||
var keyThrough = definition.throughKey || i8n.camelize(modelTo.modelName + '_id', true);
|
||||
definition.keyThrough = keyThrough;
|
||||
}
|
||||
|
||||
modelFrom.relations[relationName] = definition;
|
||||
|
||||
if (!params.through) {
|
||||
// obviously, modelTo should have attribute called `fk`
|
||||
modelTo.dataSource.defineForeignKey(modelTo.modelName, fk, this.modelName);
|
||||
}
|
||||
|
||||
var scopeMethods = {
|
||||
findById: scopeMethod(definition, 'findById'),
|
||||
destroy: scopeMethod(definition, 'destroyById')
|
||||
}
|
||||
|
||||
if(definition.modelThrough) {
|
||||
scopeMethods.create = scopeMethod(definition, 'create');
|
||||
scopeMethods.add = scopeMethod(definition, 'add');
|
||||
scopeMethods.remove = scopeMethod(definition, 'remove');
|
||||
}
|
||||
|
||||
// Mix the property and scoped methods into the prototype class
|
||||
defineScope(modelFrom.prototype, params.through || modelTo, relationName, function () {
|
||||
var filter = {};
|
||||
filter.where = {};
|
||||
filter.where[fk] = this[idName];
|
||||
if (params.through) {
|
||||
filter.collect = i8n.camelize(modelTo.modelName, true);
|
||||
filter.include = filter.collect;
|
||||
}
|
||||
return filter;
|
||||
}, scopeMethods);
|
||||
|
||||
};
|
||||
|
||||
function scopeMethod(definition, methodName) {
|
||||
var method = function () {
|
||||
var relationClass = RelationClasses[definition.type];
|
||||
if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
|
||||
relationClass = RelationClasses.hasManyThrough;
|
||||
}
|
||||
var relation = new relationClass(definition, this);
|
||||
return relation[methodName].apply(relation, arguments);
|
||||
};
|
||||
return method;
|
||||
}
|
||||
|
||||
HasMany.prototype.findById = function (id, cb) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var fk = this.definition.keyTo;
|
||||
var pk = this.definition.keyFrom;
|
||||
var modelInstance = this.modelInstance;
|
||||
modelTo.findById(id, function (err, inst) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(new Error('Not found'));
|
||||
}
|
||||
// Check if the foreign key matches the primary key
|
||||
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
cb(new Error('Permission denied: foreign key does not match the primary key'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
HasMany.prototype.destroyById = function (id, cb) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var fk = this.definition.keyTo;
|
||||
var pk = this.definition.keyFrom;
|
||||
var modelInstance = this.modelInstance;
|
||||
modelTo.findById(id, function (err, inst) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(new Error('Not found'));
|
||||
}
|
||||
// Check if the foreign key matches the primary key
|
||||
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
|
||||
inst.destroy(cb);
|
||||
} else {
|
||||
cb(new Error('Permission denied: foreign key does not match the primary key'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Create an instance of the target model and connect it to the instance of
|
||||
// the source model by creating an instance of the through model
|
||||
HasManyThrough.prototype.create = function create(data, done) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var modelThrough = this.definition.modelThrough;
|
||||
var fk = this.definition.keyTo;
|
||||
var fk2 = this.definition.keyThrough;
|
||||
|
||||
if (typeof data === 'function' && !done) {
|
||||
done = data;
|
||||
data = {};
|
||||
}
|
||||
|
||||
var self = this.modelInstance;
|
||||
// First create the target model
|
||||
modelTo.create(data, function (err, ac) {
|
||||
if (err) {
|
||||
return done && done(err, ac);
|
||||
}
|
||||
var d = {};
|
||||
d[findBelongsTo(modelThrough, fk)] = self;
|
||||
d[findBelongsTo(modelThrough, fk2)] = ac;
|
||||
// Then create the through model
|
||||
modelThrough.create(d, function (e) {
|
||||
if (e) {
|
||||
// Undo creation of the target model
|
||||
ac.destroy(function () {
|
||||
done && done(e);
|
||||
});
|
||||
} else {
|
||||
done && done(err, ac);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the target model instance to the 'hasMany' relation
|
||||
* @param {Object|ID} acInst The actual instance or id value
|
||||
*/
|
||||
HasManyThrough.prototype.add = function (acInst, done) {
|
||||
var modelThrough = this.definition.modelThrough;
|
||||
var fk = this.definition.keyTo;
|
||||
var fk2 = this.definition.keyThrough;
|
||||
var pk = this.definition.keyFrom;
|
||||
|
||||
var data = {};
|
||||
var query = {};
|
||||
query[fk] = this[pk];
|
||||
data[findBelongsTo(modelThrough, fk)] = this.modelInstance;
|
||||
query[fk2] = acInst[pk] || acInst;
|
||||
data[findBelongsTo(modelThrough, fk2)] = acInst;
|
||||
// Create an instance of the through model
|
||||
modelThrough.findOrCreate({where: query}, data, done);
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the target model instance from the 'hasMany' relation
|
||||
* @param {Object|ID) acInst The actual instance or id value
|
||||
*/
|
||||
HasManyThrough.prototype.remove = function (acInst, done) {
|
||||
var modelThrough = this.definition.modelThrough;
|
||||
var fk2 = this.definition.keyThrough;
|
||||
var pk = this.definition.keyFrom;
|
||||
|
||||
var q = {};
|
||||
q[fk2] = acInst[pk] || acInst;
|
||||
modelThrough.deleteAll(q, done );
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Declare "belongsTo" relation that sets up a one-to-one connection with
|
||||
* another model, such that each instance of the declaring model "belongs to"
|
||||
* one instance of the other model.
|
||||
*
|
||||
* For example, if an application includes users and posts, and each post can
|
||||
* be written by exactly one user. The following code specifies that `Post` has
|
||||
* a reference called `author` to the `User` model via the `userId` property of
|
||||
* `Post` as the foreign key.
|
||||
* ```
|
||||
* Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
|
||||
* ```
|
||||
*
|
||||
* This optional parameter default value is false, so the related object will
|
||||
* be loaded from cache if available.
|
||||
*
|
||||
* @param {Class|String} modelTo Model object (or String name of model) to
|
||||
* which you are creating the relationship.
|
||||
* @options {Object} params Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that
|
||||
* corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Name of foreign key property.
|
||||
*
|
||||
*/
|
||||
RelationDefinition.belongsTo = function (modelFrom, modelTo, params) {
|
||||
params = params || {};
|
||||
if ('string' === typeof modelTo) {
|
||||
params.as = modelTo;
|
||||
if (params.model) {
|
||||
modelTo = params.model;
|
||||
} else {
|
||||
var modelToName = modelTo.toLowerCase();
|
||||
modelTo = lookupModel(modelFrom.dataSource.modelBuilder.models, modelToName);
|
||||
}
|
||||
}
|
||||
|
||||
var idName = modelFrom.dataSource.idName(modelTo.modelName) || 'id';
|
||||
var relationName = params.as || i8n.camelize(modelTo.modelName, true);
|
||||
var fk = params.foreignKey || relationName + 'Id';
|
||||
|
||||
var relationDef = modelFrom.relations[relationName] = new RelationDefinition({
|
||||
name: relationName,
|
||||
type: RelationTypes.belongsTo,
|
||||
modelFrom: modelFrom,
|
||||
keyFrom: fk,
|
||||
keyTo: idName,
|
||||
modelTo: modelTo
|
||||
});
|
||||
|
||||
modelFrom.dataSource.defineForeignKey(modelFrom.modelName, fk, modelTo.modelName);
|
||||
|
||||
// Define a property for the scope so that we have 'this' for the scoped methods
|
||||
Object.defineProperty(modelFrom.prototype, relationName, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function() {
|
||||
var relation = new BelongsTo(relationDef, this);
|
||||
var relationMethod = relation.related.bind(relation)
|
||||
relationMethod.create = relation.create.bind(relation);
|
||||
relationMethod.build = relation.build.bind(relation);
|
||||
return relationMethod;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
BelongsTo.prototype.create = function(targetModelData, cb) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var fk = this.definition.keyTo;
|
||||
var pk = this.definition.keyFrom;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
modelTo.create(targetModelData, function(err, targetModel) {
|
||||
if(!err) {
|
||||
modelInstance[fk] = targetModel[pk];
|
||||
cb && cb(err, targetModel);
|
||||
} else {
|
||||
cb && cb(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
BelongsTo.prototype.build = function(targetModelData) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
return new modelTo(targetModelData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Define the method for the belongsTo relation itself
|
||||
* It will support one of the following styles:
|
||||
* - order.customer(refresh, callback): Load the target model instance asynchronously
|
||||
* - order.customer(customer): Synchronous setter of the target model instance
|
||||
* - order.customer(): Synchronous getter of the target model instance
|
||||
*
|
||||
* @param refresh
|
||||
* @param params
|
||||
* @returns {*}
|
||||
*/
|
||||
BelongsTo.prototype.related = function (refresh, params) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var pk = this.definition.keyTo;
|
||||
var fk = this.definition.keyFrom;
|
||||
var modelInstance = this.modelInstance;
|
||||
var relationName = this.definition.name;
|
||||
|
||||
if (arguments.length === 1) {
|
||||
params = refresh;
|
||||
refresh = false;
|
||||
} else if (arguments.length > 2) {
|
||||
throw new Error('Method can\'t be called with more than two arguments');
|
||||
}
|
||||
|
||||
var cachedValue;
|
||||
if (!refresh && modelInstance.__cachedRelations
|
||||
&& (modelInstance.__cachedRelations[relationName] !== undefined)) {
|
||||
cachedValue = modelInstance.__cachedRelations[relationName];
|
||||
}
|
||||
if (params instanceof ModelBaseClass) { // acts as setter
|
||||
modelInstance[fk] = params[pk];
|
||||
modelInstance.__cachedRelations[relationName] = params;
|
||||
} else if (typeof params === 'function') { // acts as async getter
|
||||
var cb = params;
|
||||
if (cachedValue === undefined) {
|
||||
modelTo.findById(modelInstance[fk], function (err, inst) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(null, null);
|
||||
}
|
||||
// Check if the foreign key matches the primary key
|
||||
if (inst[pk] === modelInstance[fk]) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
}
|
||||
});
|
||||
return modelInstance[fk];
|
||||
} else {
|
||||
cb(null, cachedValue);
|
||||
return cachedValue;
|
||||
}
|
||||
} else if (params === undefined) { // acts as sync getter
|
||||
return modelInstance[fk];
|
||||
} else { // setter
|
||||
modelInstance[fk] = params;
|
||||
delete modelInstance.__cachedRelations[relationName];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* A hasAndBelongsToMany relation creates a direct many-to-many connection with another model, with no intervening model.
|
||||
* For example, if your application includes users and groups, with each group having many users and each user appearing
|
||||
* in many groups, you could declare the models this way:
|
||||
* ```
|
||||
* User.hasAndBelongsToMany('groups', {model: Group, foreignKey: 'groupId'});
|
||||
* ```
|
||||
*
|
||||
* @param {String|Object} modelTo Model object (or String name of model) to which you are creating the relationship.
|
||||
* the relation
|
||||
* @options {Object} params Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Property name of foreign key field.
|
||||
* @property {Object} model Model object
|
||||
*/
|
||||
RelationDefinition.hasAndBelongsToMany = function hasAndBelongsToMany(modelFrom, modelTo, params) {
|
||||
params = params || {};
|
||||
var models = modelFrom.dataSource.modelBuilder.models;
|
||||
|
||||
if ('string' === typeof modelTo) {
|
||||
params.as = modelTo;
|
||||
if (params.model) {
|
||||
modelTo = params.model;
|
||||
} else {
|
||||
modelTo = lookupModel(models, i8n.singularize(modelTo)) ||
|
||||
modelTo;
|
||||
}
|
||||
if (typeof modelTo === 'string') {
|
||||
throw new Error('Could not find "' + modelTo + '" relation for ' + modelFrom.modelName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.through) {
|
||||
var name1 = modelFrom.modelName + modelTo.modelName;
|
||||
var name2 = modelTo.modelName + modelFrom.modelName;
|
||||
params.through = lookupModel(models, name1) || lookupModel(models, name2) ||
|
||||
modelFrom.dataSource.define(name1);
|
||||
}
|
||||
params.through.belongsTo(modelFrom);
|
||||
params.through.belongsTo(modelTo);
|
||||
|
||||
this.hasMany(modelFrom, modelTo, {as: params.as, through: params.through});
|
||||
|
||||
};
|
384
lib/relations.js
384
lib/relations.js
|
@ -1,49 +1,17 @@
|
|||
/*!
|
||||
* Dependencies
|
||||
*/
|
||||
var i8n = require('inflection');
|
||||
var defineScope = require('./scope.js').defineScope;
|
||||
var ModelBaseClass = require('./model.js');
|
||||
var relation = require('./relation-definition');
|
||||
var RelationDefinition = relation.RelationDefinition;
|
||||
|
||||
module.exports = Relation;
|
||||
module.exports = RelationMixin;
|
||||
|
||||
/**
|
||||
* Relations class. Use to define relationships between models.
|
||||
* RelationMixin class. Use to define relationships between models.
|
||||
*
|
||||
* @class Relation
|
||||
* @class RelationMixin
|
||||
*/
|
||||
function Relation() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the relation by foreign key
|
||||
* @param {*} foreignKey The foreign key
|
||||
* @returns {Object} The relation object
|
||||
*/
|
||||
Relation.relationNameFor = function relationNameFor(foreignKey) {
|
||||
for (var rel in this.relations) {
|
||||
if (this.relations[rel].type === 'belongsTo' && this.relations[rel].keyFrom === foreignKey) {
|
||||
return rel;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/*!
|
||||
* Look up a model by name from the list of given models
|
||||
* @param {Object} models Models keyed by name
|
||||
* @param {String} modelName The model name
|
||||
* @returns {*} The matching model class
|
||||
*/
|
||||
function lookupModel(models, modelName) {
|
||||
if(models[modelName]) {
|
||||
return models[modelName];
|
||||
}
|
||||
var lookupClassName = modelName.toLowerCase();
|
||||
for (var name in models) {
|
||||
if (name.toLowerCase() === lookupClassName) {
|
||||
return models[name];
|
||||
}
|
||||
}
|
||||
function RelationMixin() {
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -90,173 +58,14 @@ function lookupModel(models, modelName) {
|
|||
* });
|
||||
* });
|
||||
*```
|
||||
* @param {Object|String} anotherClass Model object (or String name of model) to which you are creating the relationship.
|
||||
* @param {Object|String} modelTo Model object (or String name of model) to which you are creating the relationship.
|
||||
* @options {Object} parameters Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Property name of foreign key field.
|
||||
* @property {Object} model Model object
|
||||
*/
|
||||
Relation.hasMany = function hasMany(anotherClass, params) {
|
||||
var thisClassName = this.modelName;
|
||||
params = params || {};
|
||||
if (typeof anotherClass === 'string') {
|
||||
params.as = anotherClass;
|
||||
if (params.model) {
|
||||
anotherClass = params.model;
|
||||
} else {
|
||||
var anotherClassName = i8n.singularize(anotherClass).toLowerCase();
|
||||
anotherClass = lookupModel(this.dataSource.modelBuilder.models, anotherClassName);
|
||||
}
|
||||
}
|
||||
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: idName,
|
||||
keyTo: fk,
|
||||
modelTo: anotherClass,
|
||||
multiple: true
|
||||
};
|
||||
|
||||
if (params.through) {
|
||||
this.relations[methodName].modelThrough = params.through;
|
||||
}
|
||||
// each instance of this class should have method named
|
||||
// pluralize(anotherClass.modelName)
|
||||
// which is actually just anotherClass.find({where: {thisModelNameId: this[idName]}}, cb);
|
||||
var scopeMethods = {
|
||||
findById: findById,
|
||||
destroy: destroyById
|
||||
};
|
||||
if (params.through) {
|
||||
var fk2 = i8n.camelize(anotherClass.modelName + '_id', true);
|
||||
|
||||
// Create an instance of the target model and connect it to the instance of
|
||||
// the source model by creating an instance of the through model
|
||||
scopeMethods.create = function create(data, done) {
|
||||
if (typeof data !== 'object') {
|
||||
done = data;
|
||||
data = {};
|
||||
}
|
||||
if ('function' !== typeof done) {
|
||||
done = function () {
|
||||
};
|
||||
}
|
||||
var self = this;
|
||||
// First create the target model
|
||||
anotherClass.create(data, function (err, ac) {
|
||||
if (err) return done(err, ac);
|
||||
var d = {};
|
||||
d[params.through.relationNameFor(fk)] = self;
|
||||
d[params.through.relationNameFor(fk2)] = ac;
|
||||
// Then create the through model
|
||||
params.through.create(d, function (e) {
|
||||
if (e) {
|
||||
// Undo creation of the target model
|
||||
ac.destroy(function () {
|
||||
done(e);
|
||||
});
|
||||
} else {
|
||||
done(err, ac);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
/*!
|
||||
* Add the target model instance to the 'hasMany' relation
|
||||
* @param {Object|ID} acInst The actual instance or id value
|
||||
*/
|
||||
scopeMethods.add = function (acInst, done) {
|
||||
var data = {};
|
||||
var query = {};
|
||||
query[fk] = this[idName];
|
||||
data[params.through.relationNameFor(fk)] = this;
|
||||
query[fk2] = acInst[idName] || acInst;
|
||||
data[params.through.relationNameFor(fk2)] = acInst;
|
||||
// Create an instance of the through model
|
||||
params.through.findOrCreate({where: query}, data, done);
|
||||
};
|
||||
|
||||
/*!
|
||||
* Remove the target model instance from the 'hasMany' relation
|
||||
* @param {Object|ID) acInst The actual instance or id value
|
||||
*/
|
||||
scopeMethods.remove = function (acInst, done) {
|
||||
var q = {};
|
||||
q[fk2] = acInst[idName] || acInst;
|
||||
params.through.findOne({where: q}, function (err, d) {
|
||||
if (err) {
|
||||
return done(err);
|
||||
}
|
||||
if (!d) {
|
||||
return done();
|
||||
}
|
||||
d.destroy(done);
|
||||
});
|
||||
};
|
||||
|
||||
// No destroy method will be injected
|
||||
delete scopeMethods.destroy;
|
||||
}
|
||||
|
||||
// Mix the property and scoped methods into the prototype class
|
||||
defineScope(this.prototype, params.through || anotherClass, methodName, function () {
|
||||
var filter = {};
|
||||
filter.where = {};
|
||||
filter.where[fk] = this[idName];
|
||||
if (params.through) {
|
||||
filter.collect = i8n.camelize(anotherClass.modelName, true);
|
||||
filter.include = filter.collect;
|
||||
}
|
||||
return filter;
|
||||
}, scopeMethods);
|
||||
|
||||
if (!params.through) {
|
||||
// obviously, anotherClass should have attribute called `fk`
|
||||
anotherClass.dataSource.defineForeignKey(anotherClass.modelName, fk, this.modelName);
|
||||
}
|
||||
|
||||
// Find the target model instance by id
|
||||
function findById(id, cb) {
|
||||
anotherClass.findById(id, function (err, inst) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(new Error('Not found'));
|
||||
}
|
||||
// Check if the foreign key matches the primary key
|
||||
if (inst[fk] && inst[fk].toString() === this[idName].toString()) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
}
|
||||
}.bind(this));
|
||||
}
|
||||
|
||||
// Destroy the target model instance by id
|
||||
function destroyById(id, cb) {
|
||||
var self = this;
|
||||
anotherClass.findById(id, function (err, inst) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(new Error('Not found'));
|
||||
}
|
||||
// Check if the foreign key matches the primary key
|
||||
if (inst[fk] && inst[fk].toString() === self[idName].toString()) {
|
||||
inst.destroy(cb);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RelationMixin.hasMany = function hasMany(modelTo, params) {
|
||||
RelationDefinition.hasMany(this, modelTo, params);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -304,151 +113,14 @@ Relation.hasMany = function hasMany(anotherClass, params) {
|
|||
* ```
|
||||
* This optional parameter default value is false, so the related object will be loaded from cache if available.
|
||||
*
|
||||
* @param {Class|String} anotherClass Model object (or String name of model) to which you are creating the relationship.
|
||||
* @param {Class|String} modelTo Model object (or String name of model) to which you are creating the relationship.
|
||||
* @options {Object} params Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Name of foreign key property.
|
||||
*
|
||||
*/
|
||||
Relation.belongsTo = function (anotherClass, params) {
|
||||
params = params || {};
|
||||
if ('string' === typeof anotherClass) {
|
||||
params.as = anotherClass;
|
||||
if (params.model) {
|
||||
anotherClass = params.model;
|
||||
} else {
|
||||
var anotherClassName = anotherClass.toLowerCase();
|
||||
anotherClass = lookupModel(this.dataSource.modelBuilder.models, anotherClassName);
|
||||
}
|
||||
}
|
||||
|
||||
var idName = this.dataSource.idName(anotherClass.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: idName,
|
||||
modelTo: anotherClass,
|
||||
multiple: false
|
||||
};
|
||||
|
||||
this.dataSource.defineForeignKey(this.modelName, fk, anotherClass.modelName);
|
||||
this.prototype.__finders__ = this.prototype.__finders__ || {};
|
||||
|
||||
// Set up a finder to find by id and make sure the foreign key of the declaring
|
||||
// model matches the primary key of the target model
|
||||
this.prototype.__finders__[methodName] = function (id, cb) {
|
||||
if (id === null) {
|
||||
cb(null, null);
|
||||
return;
|
||||
}
|
||||
anotherClass.findById(id, function (err, inst) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
if (!inst) {
|
||||
return cb(null, null);
|
||||
}
|
||||
// Check if the foreign key matches the primary key
|
||||
if (inst[idName] === this[fk]) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
}
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
// Define the method for the belongsTo relation itself
|
||||
// It will support one of the following styles:
|
||||
// - order.customer(refresh, callback): Load the target model instance asynchronously
|
||||
// - order.customer(customer): Synchronous setter of the target model instance
|
||||
// - order.customer(): Synchronous getter of the target model instance
|
||||
var relationMethod = function (refresh, p) {
|
||||
if (arguments.length === 1) {
|
||||
p = refresh;
|
||||
refresh = false;
|
||||
} else if (arguments.length > 2) {
|
||||
throw new Error('Method can\'t be called with more than two arguments');
|
||||
}
|
||||
var self = this;
|
||||
var cachedValue;
|
||||
if (!refresh && this.__cachedRelations && (this.__cachedRelations[methodName] !== undefined)) {
|
||||
cachedValue = this.__cachedRelations[methodName];
|
||||
}
|
||||
if (p instanceof ModelBaseClass) { // acts as setter
|
||||
this[fk] = p[idName];
|
||||
this.__cachedRelations[methodName] = p;
|
||||
} else if (typeof p === 'function') { // acts as async getter
|
||||
if (typeof cachedValue === 'undefined') {
|
||||
this.__finders__[methodName].apply(self, [this[fk], function (err, inst) {
|
||||
if (!err) {
|
||||
self.__cachedRelations[methodName] = inst;
|
||||
}
|
||||
p(err, inst);
|
||||
}]);
|
||||
return this[fk];
|
||||
} else {
|
||||
p(null, cachedValue);
|
||||
return cachedValue;
|
||||
}
|
||||
} else if (typeof p === 'undefined') { // acts as sync getter
|
||||
return this[fk];
|
||||
} else { // setter
|
||||
this[fk] = p;
|
||||
delete this.__cachedRelations[methodName];
|
||||
}
|
||||
};
|
||||
|
||||
// Define a property for the scope so that we have 'this' for the scoped methods
|
||||
Object.defineProperty(this.prototype, methodName, {
|
||||
enumerable: true,
|
||||
configurable: true,
|
||||
get: function () {
|
||||
var fn = function() {
|
||||
// Call the relation method on the declaring model instance
|
||||
return relationMethod.apply(this, arguments);
|
||||
}
|
||||
// Create an instance of the target model and set the foreign key of the
|
||||
// declaring model instance to the id of the target instance
|
||||
fn.create = function(targetModelData, cb) {
|
||||
var self = this;
|
||||
anotherClass.create(targetModelData, function(err, targetModel) {
|
||||
if(!err) {
|
||||
self[fk] = targetModel[idName];
|
||||
cb && cb(err, targetModel);
|
||||
} else {
|
||||
cb && cb(err);
|
||||
}
|
||||
});
|
||||
}.bind(this);
|
||||
|
||||
// Build an instance of the target model
|
||||
fn.build = function(targetModelData) {
|
||||
return new anotherClass(targetModelData);
|
||||
}.bind(this);
|
||||
|
||||
fn._targetClass = anotherClass.modelName;
|
||||
|
||||
return fn;
|
||||
}});
|
||||
|
||||
// Wrap the property into a function for remoting
|
||||
// so that it can be accessed as /api/<model>/<id>/<belongsToRelationName>
|
||||
// For example, /api/orders/1/customer
|
||||
var fn = function() {
|
||||
var f = this[methodName];
|
||||
f.apply(this, arguments);
|
||||
};
|
||||
|
||||
fn.shared = true;
|
||||
fn.http = {verb: 'get', path: '/' + methodName};
|
||||
fn.accepts = {arg: 'refresh', type: 'boolean', http: {source: 'query'}};
|
||||
fn.description = 'Fetches belongsTo relation ' + methodName;
|
||||
fn.returns = {arg: methodName, type: 'object', root: true};
|
||||
|
||||
this.prototype['__get__' + methodName] = fn;
|
||||
RelationMixin.belongsTo = function (modelTo, params) {
|
||||
RelationDefinition.belongsTo(this, modelTo, params);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -475,39 +147,13 @@ Relation.belongsTo = function (anotherClass, params) {
|
|||
* user.groups.remove(group, callback);
|
||||
* ```
|
||||
*
|
||||
* @param {String|Object} anotherClass Model object (or String name of model) to which you are creating the relationship.
|
||||
* @param {String|Object} modelTo Model object (or String name of model) to which you are creating the relationship.
|
||||
* the relation
|
||||
* @options {Object} params Configuration parameters; see below.
|
||||
* @property {String} as Name of the property in the referring model that corresponds to the foreign key field in the related model.
|
||||
* @property {String} foreignKey Property name of foreign key field.
|
||||
* @property {Object} model Model object
|
||||
*/
|
||||
Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) {
|
||||
params = params || {};
|
||||
var models = this.dataSource.modelBuilder.models;
|
||||
|
||||
if ('string' === typeof anotherClass) {
|
||||
params.as = anotherClass;
|
||||
if (params.model) {
|
||||
anotherClass = params.model;
|
||||
} else {
|
||||
anotherClass = lookupModel(models, i8n.singularize(anotherClass).toLowerCase()) ||
|
||||
anotherClass;
|
||||
}
|
||||
if (typeof anotherClass === 'string') {
|
||||
throw new Error('Could not find "' + anotherClass + '" relation for ' + this.modelName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!params.through) {
|
||||
var name1 = this.modelName + anotherClass.modelName;
|
||||
var name2 = anotherClass.modelName + this.modelName;
|
||||
params.through = lookupModel(models, name1) || lookupModel(models, name2) ||
|
||||
this.dataSource.define(name1);
|
||||
}
|
||||
params.through.belongsTo(this);
|
||||
params.through.belongsTo(anotherClass);
|
||||
|
||||
this.hasMany(anotherClass, {as: params.as, through: params.through});
|
||||
|
||||
RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params) {
|
||||
RelationDefinition.hasAndBelongsToMany(this, modelTo, params);
|
||||
};
|
||||
|
|
|
@ -872,27 +872,33 @@ describe('Load models with relations', function () {
|
|||
var Account = ds.define('Account', {userId: Number, type: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}});
|
||||
|
||||
assert(Post.relations['user']);
|
||||
assert.deepEqual(Post.relations['user'], {
|
||||
assert.deepEqual(Post.relations['user'].toJSON(), {
|
||||
name: 'user',
|
||||
type: 'belongsTo',
|
||||
modelFrom: 'Post',
|
||||
keyFrom: 'userId',
|
||||
modelTo: 'User',
|
||||
keyTo: 'id',
|
||||
modelTo: User,
|
||||
multiple: false
|
||||
});
|
||||
assert(User.relations['posts']);
|
||||
assert.deepEqual(User.relations['posts'], {
|
||||
assert.deepEqual(User.relations['posts'].toJSON(), {
|
||||
name: 'posts',
|
||||
type: 'hasMany',
|
||||
modelFrom: 'User',
|
||||
keyFrom: 'id',
|
||||
modelTo: 'Post',
|
||||
keyTo: 'userId',
|
||||
modelTo: Post,
|
||||
multiple: true
|
||||
});
|
||||
assert(User.relations['accounts']);
|
||||
assert.deepEqual(User.relations['accounts'], {
|
||||
assert.deepEqual(User.relations['accounts'].toJSON(), {
|
||||
name: 'accounts',
|
||||
type: 'hasMany',
|
||||
modelFrom: 'User',
|
||||
keyFrom: 'id',
|
||||
modelTo: 'Account',
|
||||
keyTo: 'userId',
|
||||
modelTo: Account,
|
||||
multiple: true
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue