Merge pull request #140 from strongloop/feature/refactor-relation
Feature/refactor relation
This commit is contained in:
commit
3f7e85101d
13
lib/list.js
13
lib/list.js
|
@ -13,7 +13,7 @@ function List(items, itemType, parent) {
|
|||
try {
|
||||
items = JSON.parse(items);
|
||||
} catch (e) {
|
||||
throw new Error('could not create List from JSON string: ', items);
|
||||
throw new Error(util.format('could not create List from JSON string: %j', items));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,7 +22,7 @@ function List(items, itemType, parent) {
|
|||
|
||||
items = items || [];
|
||||
if (!Array.isArray(items)) {
|
||||
throw new Error('Items must be an array: ' + items);
|
||||
throw new Error(util.format('Items must be an array: %j', items));
|
||||
}
|
||||
|
||||
if(!itemType) {
|
||||
|
@ -92,12 +92,3 @@ List.prototype.toString = function () {
|
|||
return JSON.stringify(this.toJSON());
|
||||
};
|
||||
|
||||
/*
|
||||
var strArray = new List(['1', 2], String);
|
||||
strArray.push(3);
|
||||
console.log(strArray);
|
||||
console.log(strArray.length);
|
||||
|
||||
console.log(strArray.toJSON());
|
||||
console.log(strArray.toString());
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,838 @@
|
|||
/*!
|
||||
* 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(modelFrom, modelTo, keyTo) {
|
||||
var relations = modelFrom.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.modelTo === modelTo &&
|
||||
rel.keyTo === keyTo) {
|
||||
return rel.keyFrom;
|
||||
}
|
||||
}
|
||||
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 definition = this.definition;
|
||||
var modelTo = definition.modelTo;
|
||||
var modelThrough = definition.modelThrough;
|
||||
|
||||
if (typeof data === 'function' && !done) {
|
||||
done = data;
|
||||
data = {};
|
||||
}
|
||||
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
// First create the target model
|
||||
modelTo.create(data, function (err, ac) {
|
||||
if (err) {
|
||||
return done && done(err, ac);
|
||||
}
|
||||
// The primary key for the target model
|
||||
var pk2 = modelTo.dataSource.idName(modelTo.modelName) || 'id';
|
||||
var fk1 = findBelongsTo(modelThrough, definition.modelFrom,
|
||||
definition.keyFrom);
|
||||
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2);
|
||||
var d = {};
|
||||
d[fk1] = modelInstance[definition.keyFrom];
|
||||
d[fk2] = ac[pk2];
|
||||
// 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 definition = this.definition;
|
||||
var modelThrough = definition.modelThrough;
|
||||
var modelTo = definition.modelTo;
|
||||
var pk1 = definition.keyFrom;
|
||||
|
||||
var data = {};
|
||||
var query = {};
|
||||
|
||||
var fk1 = findBelongsTo(modelThrough, definition.modelFrom,
|
||||
definition.keyFrom);
|
||||
|
||||
// The primary key for the target model
|
||||
var pk2 = modelTo.dataSource.idName(modelTo.modelName) || 'id';
|
||||
|
||||
var fk2 = findBelongsTo(modelThrough, definition.modelTo, pk2);
|
||||
|
||||
query[fk1] = this.modelInstance[pk1];
|
||||
query[fk2] = acInst[pk2] || acInst;
|
||||
|
||||
data[fk1] = this.modelInstance[pk1];
|
||||
data[fk2] = acInst[pk2] || 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);
|
||||
relationMethod._targetClass = relationDef.modelTo.modelName;
|
||||
return relationMethod;
|
||||
}
|
||||
});
|
||||
|
||||
// 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[relationName];
|
||||
f.apply(this, arguments);
|
||||
};
|
||||
|
||||
fn.shared = true;
|
||||
fn.http = {verb: 'get', path: '/' + relationName};
|
||||
fn.accepts = {arg: 'refresh', type: 'boolean', http: {source: 'query'}};
|
||||
fn.description = 'Fetches belongsTo relation ' + relationName;
|
||||
fn.returns = {arg: relationName, type: 'object', root: true};
|
||||
|
||||
modelFrom.prototype['__get__' + relationName] = fn;
|
||||
|
||||
};
|
||||
|
||||
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.
|
||||
* @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});
|
||||
|
||||
};
|
||||
|
||||
/**
|
||||
* A HasOne relation creates a one-to-one connection from modelFrom to modelTo.
|
||||
* This relation indicates that each instance of a model contains or possesses
|
||||
* one instance of another model. For example, each supplier in your application
|
||||
* has only one account.
|
||||
*
|
||||
* @param {Function} modelFrom The declaring model class
|
||||
* @param {String|Function} 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.hasOne = 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 pk = modelFrom.dataSource.idName(modelTo.modelName) || 'id';
|
||||
var relationName = params.as || i8n.camelize(modelTo.modelName, true);
|
||||
|
||||
var fk = params.foreignKey || i8n.camelize(modelFrom.modelName + '_id', true);
|
||||
|
||||
var relationDef = modelFrom.relations[relationName] = new RelationDefinition({
|
||||
name: relationName,
|
||||
type: RelationTypes.hasOne,
|
||||
modelFrom: modelFrom,
|
||||
keyFrom: pk,
|
||||
keyTo: fk,
|
||||
modelTo: modelTo
|
||||
});
|
||||
|
||||
modelFrom.dataSource.defineForeignKey(modelTo.modelName, fk, modelFrom.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 HasOne(relationDef, this);
|
||||
var relationMethod = relation.related.bind(relation)
|
||||
relationMethod.create = relation.create.bind(relation);
|
||||
relationMethod.build = relation.build.bind(relation);
|
||||
return relationMethod;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a target model instance
|
||||
* @param {Object} targetModelData The target model data
|
||||
* @callback {Function} [cb] Callback function
|
||||
* @param {String|Object} err Error string or object
|
||||
* @param {Object} The newly created target model instance
|
||||
*/
|
||||
HasOne.prototype.create = function(targetModelData, cb) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var fk = this.definition.keyTo;
|
||||
var pk = this.definition.keyFrom;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
targetModelData = targetModelData || {};
|
||||
targetModelData[fk] = modelInstance[pk];
|
||||
modelTo.create(targetModelData, function(err, targetModel) {
|
||||
if(!err) {
|
||||
cb && cb(err, targetModel);
|
||||
} else {
|
||||
cb && cb(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Build a target model instance
|
||||
* @param {Object} targetModelData The target model data
|
||||
* @returns {Object} The newly built target model instance
|
||||
*/
|
||||
HasOne.prototype.build = function(targetModelData) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var pk = this.definition.keyFrom;
|
||||
var fk = this.definition.keyTo;
|
||||
targetModelData = targetModelData || {};
|
||||
targetModelData[fk] = this.modelInstance[pk];
|
||||
return new modelTo(targetModelData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Define the method for the hasOne 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 {Boolean} refresh Reload from the data source
|
||||
* @param {Object|Function} params Query parameters
|
||||
* @returns {Object}
|
||||
*/
|
||||
HasOne.prototype.related = function (refresh, params) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var fk = this.definition.keyTo;
|
||||
var pk = 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
|
||||
params[fk] = modelInstance[pk];
|
||||
modelInstance.__cachedRelations[relationName] = params;
|
||||
} else if (typeof params === 'function') { // acts as async getter
|
||||
var cb = params;
|
||||
if (cachedValue === undefined) {
|
||||
var query = {where: {}};
|
||||
query.where[fk] = modelInstance[pk];
|
||||
modelTo.findOne(query, 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[fk] === modelInstance[pk]) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
cb(new Error('Permission denied'));
|
||||
}
|
||||
});
|
||||
return modelInstance[pk];
|
||||
} else {
|
||||
cb(null, cachedValue);
|
||||
return cachedValue;
|
||||
}
|
||||
} else if (params === undefined) { // acts as sync getter
|
||||
return modelInstance[pk];
|
||||
} else { // setter
|
||||
params[fk] = modelInstance[pk];
|
||||
delete modelInstance.__cachedRelations[relationName];
|
||||
}
|
||||
};
|
388
lib/relations.js
388
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,17 @@ 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);
|
||||
};
|
||||
|
||||
RelationMixin.hasOne = function hasMany(modelTo, params) {
|
||||
RelationDefinition.hasOne(this, modelTo, params);
|
||||
};
|
||||
|
|
249
lib/scope.js
249
lib/scope.js
|
@ -6,6 +6,52 @@ var defineCachedRelations = utils.defineCachedRelations;
|
|||
*/
|
||||
exports.defineScope = defineScope;
|
||||
|
||||
function ScopeDefinition(definition) {
|
||||
this.sourceModel = definition.sourceModel;
|
||||
this.targetModel = definition.targetModel || definition.sourceModel;
|
||||
this.name = definition.name;
|
||||
this.params = definition.params;
|
||||
this.methods = definition.methods;
|
||||
}
|
||||
|
||||
ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
|
||||
var name = this.name;
|
||||
var self = receiver;
|
||||
|
||||
var actualCond = {};
|
||||
var actualRefresh = false;
|
||||
var saveOnCache = true;
|
||||
if (arguments.length === 3) {
|
||||
cb = condOrRefresh;
|
||||
} else if (arguments.length === 4) {
|
||||
if (typeof condOrRefresh === 'boolean') {
|
||||
actualRefresh = condOrRefresh;
|
||||
} else {
|
||||
actualCond = condOrRefresh;
|
||||
actualRefresh = true;
|
||||
saveOnCache = false;
|
||||
}
|
||||
} else {
|
||||
throw new Error('Method can be only called with one or two arguments');
|
||||
}
|
||||
|
||||
if (!self.__cachedRelations || self.__cachedRelations[name] === undefined
|
||||
|| actualRefresh) {
|
||||
// It either doesn't hit the cache or refresh is required
|
||||
var params = mergeQuery(actualCond, scopeParams);
|
||||
return this.targetModel.find(params, function (err, data) {
|
||||
if (!err && saveOnCache) {
|
||||
defineCachedRelations(self);
|
||||
self.__cachedRelations[name] = data;
|
||||
}
|
||||
cb(err, data);
|
||||
});
|
||||
} else {
|
||||
// Return from cache
|
||||
cb(null, self.__cachedRelations[name]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Define a scope to the class
|
||||
* @param {Model} cls The class where the scope method is added
|
||||
|
@ -22,7 +68,7 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
cls._scopeMeta = {};
|
||||
}
|
||||
|
||||
// only makes sence to add scope in meta if base and target classes
|
||||
// only makes sense to add scope in meta if base and target classes
|
||||
// are same
|
||||
if (cls === targetClass) {
|
||||
cls._scopeMeta[name] = params;
|
||||
|
@ -32,6 +78,14 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
}
|
||||
}
|
||||
|
||||
var definition = new ScopeDefinition({
|
||||
sourceModel: cls,
|
||||
targetModel: targetClass,
|
||||
name: name,
|
||||
params: params,
|
||||
methods: methods
|
||||
});
|
||||
|
||||
// Define a property for the scope
|
||||
Object.defineProperty(cls, name, {
|
||||
enumerable: false,
|
||||
|
@ -49,42 +103,18 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
*/
|
||||
get: function () {
|
||||
var self = this;
|
||||
var f = function caller(condOrRefresh, cb) {
|
||||
var actualCond = {};
|
||||
var actualRefresh = false;
|
||||
var saveOnCache = true;
|
||||
if (arguments.length === 1) {
|
||||
cb = condOrRefresh;
|
||||
} else if (arguments.length === 2) {
|
||||
if (typeof condOrRefresh === 'boolean') {
|
||||
actualRefresh = condOrRefresh;
|
||||
} else {
|
||||
actualCond = condOrRefresh;
|
||||
actualRefresh = true;
|
||||
saveOnCache = false;
|
||||
}
|
||||
var f = function(condOrRefresh, cb) {
|
||||
if(arguments.length === 1) {
|
||||
definition.related(self, f._scope, condOrRefresh);
|
||||
} else {
|
||||
throw new Error('Method can be only called with one or two arguments');
|
||||
}
|
||||
|
||||
if (!self.__cachedRelations || self.__cachedRelations[name] === undefined
|
||||
|| actualRefresh) {
|
||||
// It either doesn't hit the cache or reresh is required
|
||||
var params = mergeParams(actualCond, caller._scope);
|
||||
return targetClass.find(params, function (err, data) {
|
||||
if (!err && saveOnCache) {
|
||||
defineCachedRelations(self);
|
||||
self.__cachedRelations[name] = data;
|
||||
}
|
||||
cb(err, data);
|
||||
});
|
||||
} else {
|
||||
// Return from cache
|
||||
cb(null, self.__cachedRelations[name]);
|
||||
definition.related(self, f._scope, condOrRefresh, cb);
|
||||
}
|
||||
};
|
||||
f._scope = typeof params === 'function' ? params.call(this) : params;
|
||||
f._targetClass = targetClass.modelName;
|
||||
|
||||
f._scope = typeof definition.params === 'function' ?
|
||||
definition.params.call(self) : definition.params;
|
||||
|
||||
f._targetClass = definition.targetModel.modelName;
|
||||
if (f._scope.collect) {
|
||||
f._targetClass = i8n.capitalize(f._scope.collect);
|
||||
}
|
||||
|
@ -92,20 +122,23 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
f.build = build;
|
||||
f.create = create;
|
||||
f.destroyAll = destroyAll;
|
||||
for (var i in methods) {
|
||||
f[i] = methods[i].bind(this);
|
||||
for (var i in definition.methods) {
|
||||
f[i] = definition.methods[i].bind(self);
|
||||
}
|
||||
|
||||
// define sub-scopes
|
||||
// Define scope-chaining, such as
|
||||
// Station.scope('active', {where: {isActive: true}});
|
||||
// Station.scope('subway', {where: {isUndeground: true}});
|
||||
// Station.active.subway(cb);
|
||||
Object.keys(targetClass._scopeMeta).forEach(function (name) {
|
||||
Object.defineProperty(f, name, {
|
||||
enumerable: false,
|
||||
get: function () {
|
||||
mergeParams(f._scope, targetClass._scopeMeta[name]);
|
||||
mergeQuery(f._scope, targetClass._scopeMeta[name]);
|
||||
return f;
|
||||
}
|
||||
});
|
||||
}.bind(this));
|
||||
}.bind(self));
|
||||
return f;
|
||||
}
|
||||
});
|
||||
|
@ -150,9 +183,40 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
|
||||
cls['__delete__' + name] = fn_delete;
|
||||
|
||||
/*
|
||||
* Extracting fixed property values for the scope from the where clause into
|
||||
* the data object
|
||||
*
|
||||
* @param {Object} The data object
|
||||
* @param {Object} The where clause
|
||||
*/
|
||||
function setScopeValuesFromWhere(data, where) {
|
||||
for (var i in where) {
|
||||
if (i === 'and') {
|
||||
// Find fixed property values from each subclauses
|
||||
for (var w = 0, n = where[i].length; w < n; w++) {
|
||||
setScopeValuesFromWhere(data, where[i][w]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
var prop = targetClass.definition.properties[i];
|
||||
if (prop) {
|
||||
var val = where[i];
|
||||
if (typeof val !== 'object' || val instanceof prop.type) {
|
||||
// Only pick the {propertyName: propertyValue}
|
||||
data[i] = where[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// and it should have create/build methods with binded thisModelNameId param
|
||||
function build(data) {
|
||||
return new targetClass(mergeParams(this._scope, {where: data || {}}).where);
|
||||
data = data || {};
|
||||
// Find all fixed property values for the scope
|
||||
var where = (this._scope && this._scope.where) || {};
|
||||
setScopeValuesFromWhere(data, where);
|
||||
return new targetClass(data);
|
||||
}
|
||||
|
||||
function create(data, cb) {
|
||||
|
@ -170,70 +234,57 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
- If fetching the Elements on which destroyAll is called results in an error
|
||||
*/
|
||||
function destroyAll(cb) {
|
||||
targetClass.find(this._scope, function (err, data) {
|
||||
if (err) {
|
||||
cb(err);
|
||||
} else {
|
||||
(function loopOfDestruction(data) {
|
||||
if (data.length > 0) {
|
||||
data.shift().destroy(function (err) {
|
||||
if (err && cb) cb(err);
|
||||
loopOfDestruction(data);
|
||||
});
|
||||
} else {
|
||||
if (cb) cb();
|
||||
}
|
||||
}(data));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mergeParams(base, update) {
|
||||
base = base || {};
|
||||
if (update.where) {
|
||||
base.where = merge(base.where, update.where);
|
||||
}
|
||||
if (update.include) {
|
||||
base.include = update.include;
|
||||
}
|
||||
if (update.collect) {
|
||||
base.collect = update.collect;
|
||||
}
|
||||
|
||||
// overwrite order
|
||||
if (update.order) {
|
||||
base.order = update.order;
|
||||
}
|
||||
|
||||
if(update.limit !== undefined) {
|
||||
base.limit = update.limit;
|
||||
}
|
||||
if(update.skip !== undefined) {
|
||||
base.skip = update.skip;
|
||||
}
|
||||
if(update.offset !== undefined) {
|
||||
base.offset = update.offset;
|
||||
}
|
||||
if(update.fields !== undefined) {
|
||||
base.fields = update.fields;
|
||||
}
|
||||
return base;
|
||||
|
||||
targetClass.destroyAll(this._scope, cb);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge `base` and `update` params
|
||||
* @param {Object} base - base object (updating this object)
|
||||
* @param {Object} update - object with new data to update base
|
||||
* @returns {Object} `base`
|
||||
/*!
|
||||
* Merge query parameters
|
||||
* @param {Object} base The base object to contain the merged results
|
||||
* @param {Object} update The object containing updates to be merged
|
||||
* @returns {*|Object} The base object
|
||||
* @private
|
||||
*/
|
||||
function merge(base, update) {
|
||||
function mergeQuery(base, update) {
|
||||
if (!update) {
|
||||
return;
|
||||
}
|
||||
base = base || {};
|
||||
if (update) {
|
||||
Object.keys(update).forEach(function (key) {
|
||||
base[key] = update[key];
|
||||
});
|
||||
if (update.where && Object.keys(update.where).length > 0) {
|
||||
if (base.where && Object.keys(base.where).length > 0) {
|
||||
base.where = {and: [base.where, update.where]};
|
||||
} else {
|
||||
base.where = update.where;
|
||||
}
|
||||
}
|
||||
|
||||
// Overwrite inclusion
|
||||
if (update.include) {
|
||||
base.include = update.include;
|
||||
}
|
||||
if (update.collect) {
|
||||
base.collect = update.collect;
|
||||
}
|
||||
|
||||
// overwrite order
|
||||
if (update.order) {
|
||||
base.order = update.order;
|
||||
}
|
||||
|
||||
// overwrite pagination
|
||||
if (update.limit !== undefined) {
|
||||
base.limit = update.limit;
|
||||
}
|
||||
if (update.skip !== undefined) {
|
||||
base.skip = update.skip;
|
||||
}
|
||||
if (update.offset !== undefined) {
|
||||
base.offset = update.offset;
|
||||
}
|
||||
|
||||
// Overwrite fields
|
||||
if (update.fields !== undefined) {
|
||||
base.fields = update.fields;
|
||||
}
|
||||
return base;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// This test written in mocha+should.js
|
||||
var should = require('./init.js');
|
||||
|
||||
var db, Book, Chapter, Author, Reader;
|
||||
var db, Book, Chapter, Author, Reader, Publisher;
|
||||
|
||||
describe('relations', function () {
|
||||
before(function (done) {
|
||||
|
@ -180,6 +180,42 @@ describe('relations', function () {
|
|||
|
||||
});
|
||||
|
||||
describe('hasOne', function () {
|
||||
var Supplier, Account;
|
||||
|
||||
before(function () {
|
||||
db = getSchema();
|
||||
Supplier = db.define('Supplier', {name: String});
|
||||
Account = db.define('Account', {accountNo: String});
|
||||
});
|
||||
|
||||
it('can be declared using hasOne method', function () {
|
||||
Supplier.hasOne(Account);
|
||||
Object.keys((new Account()).toObject()).should.include('supplierId');
|
||||
(new Supplier()).account.should.be.an.instanceOf(Function);
|
||||
});
|
||||
|
||||
it('can be used to query data', function (done) {
|
||||
// Supplier.hasOne(Account);
|
||||
db.automigrate(function () {
|
||||
Supplier.create({name: 'Supplier 1'}, function (e, supplier) {
|
||||
should.not.exist(e);
|
||||
should.exist(supplier);
|
||||
supplier.account.create({accountNo: 'a01'}, function (err, account) {
|
||||
supplier.account(function (e, act) {
|
||||
should.not.exist(e);
|
||||
should.exist(act);
|
||||
act.should.be.an.instanceOf(Account);
|
||||
supplier.account().should.equal(act.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('hasAndBelongsToMany', function () {
|
||||
var Article, Tag, ArticleTag;
|
||||
it('can be declared', function (done) {
|
||||
|
|
|
@ -3,7 +3,7 @@ var should = require('./init.js');
|
|||
|
||||
var db, Railway, Station;
|
||||
|
||||
describe('sc0pe', function () {
|
||||
describe('scope', function () {
|
||||
|
||||
before(function () {
|
||||
db = getSchema();
|
||||
|
|
Loading…
Reference in New Issue