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 {
|
try {
|
||||||
items = JSON.parse(items);
|
items = JSON.parse(items);
|
||||||
} catch (e) {
|
} 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 || [];
|
items = items || [];
|
||||||
if (!Array.isArray(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) {
|
if(!itemType) {
|
||||||
|
@ -92,12 +92,3 @@ List.prototype.toString = function () {
|
||||||
return JSON.stringify(this.toJSON());
|
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
|
* Dependencies
|
||||||
*/
|
*/
|
||||||
var i8n = require('inflection');
|
var relation = require('./relation-definition');
|
||||||
var defineScope = require('./scope.js').defineScope;
|
var RelationDefinition = relation.RelationDefinition;
|
||||||
var ModelBaseClass = require('./model.js');
|
|
||||||
|
|
||||||
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() {
|
function RelationMixin() {
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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.
|
* @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} 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 {String} foreignKey Property name of foreign key field.
|
||||||
* @property {Object} model Model object
|
* @property {Object} model Model object
|
||||||
*/
|
*/
|
||||||
Relation.hasMany = function hasMany(anotherClass, params) {
|
RelationMixin.hasMany = function hasMany(modelTo, params) {
|
||||||
var thisClassName = this.modelName;
|
RelationDefinition.hasMany(this, modelTo, params);
|
||||||
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'));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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.
|
* 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.
|
* @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} 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.
|
* @property {String} foreignKey Name of foreign key property.
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
Relation.belongsTo = function (anotherClass, params) {
|
RelationMixin.belongsTo = function (modelTo, params) {
|
||||||
params = params || {};
|
RelationDefinition.belongsTo(this, modelTo, 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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -475,39 +147,17 @@ Relation.belongsTo = function (anotherClass, params) {
|
||||||
* user.groups.remove(group, callback);
|
* 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
|
* the relation
|
||||||
* @options {Object} params Configuration parameters; see below.
|
* @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} 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 {String} foreignKey Property name of foreign key field.
|
||||||
* @property {Object} model Model object
|
* @property {Object} model Model object
|
||||||
*/
|
*/
|
||||||
Relation.hasAndBelongsToMany = function hasAndBelongsToMany(anotherClass, params) {
|
RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params) {
|
||||||
params = params || {};
|
RelationDefinition.hasAndBelongsToMany(this, modelTo, params);
|
||||||
var models = this.dataSource.modelBuilder.models;
|
};
|
||||||
|
|
||||||
if ('string' === typeof anotherClass) {
|
RelationMixin.hasOne = function hasMany(modelTo, params) {
|
||||||
params.as = anotherClass;
|
RelationDefinition.hasOne(this, modelTo, params);
|
||||||
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});
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
249
lib/scope.js
249
lib/scope.js
|
@ -6,6 +6,52 @@ var defineCachedRelations = utils.defineCachedRelations;
|
||||||
*/
|
*/
|
||||||
exports.defineScope = defineScope;
|
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
|
* Define a scope to the class
|
||||||
* @param {Model} cls The class where the scope method is added
|
* @param {Model} cls The class where the scope method is added
|
||||||
|
@ -22,7 +68,7 @@ function defineScope(cls, targetClass, name, params, methods) {
|
||||||
cls._scopeMeta = {};
|
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
|
// are same
|
||||||
if (cls === targetClass) {
|
if (cls === targetClass) {
|
||||||
cls._scopeMeta[name] = params;
|
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
|
// Define a property for the scope
|
||||||
Object.defineProperty(cls, name, {
|
Object.defineProperty(cls, name, {
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
|
@ -49,42 +103,18 @@ function defineScope(cls, targetClass, name, params, methods) {
|
||||||
*/
|
*/
|
||||||
get: function () {
|
get: function () {
|
||||||
var self = this;
|
var self = this;
|
||||||
var f = function caller(condOrRefresh, cb) {
|
var f = function(condOrRefresh, cb) {
|
||||||
var actualCond = {};
|
if(arguments.length === 1) {
|
||||||
var actualRefresh = false;
|
definition.related(self, f._scope, condOrRefresh);
|
||||||
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;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Method can be only called with one or two arguments');
|
definition.related(self, f._scope, condOrRefresh, cb);
|
||||||
}
|
|
||||||
|
|
||||||
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]);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
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) {
|
if (f._scope.collect) {
|
||||||
f._targetClass = i8n.capitalize(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.build = build;
|
||||||
f.create = create;
|
f.create = create;
|
||||||
f.destroyAll = destroyAll;
|
f.destroyAll = destroyAll;
|
||||||
for (var i in methods) {
|
for (var i in definition.methods) {
|
||||||
f[i] = methods[i].bind(this);
|
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.keys(targetClass._scopeMeta).forEach(function (name) {
|
||||||
Object.defineProperty(f, name, {
|
Object.defineProperty(f, name, {
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
get: function () {
|
get: function () {
|
||||||
mergeParams(f._scope, targetClass._scopeMeta[name]);
|
mergeQuery(f._scope, targetClass._scopeMeta[name]);
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}.bind(this));
|
}.bind(self));
|
||||||
return f;
|
return f;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -150,9 +183,40 @@ function defineScope(cls, targetClass, name, params, methods) {
|
||||||
|
|
||||||
cls['__delete__' + name] = fn_delete;
|
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
|
// and it should have create/build methods with binded thisModelNameId param
|
||||||
function build(data) {
|
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) {
|
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
|
- If fetching the Elements on which destroyAll is called results in an error
|
||||||
*/
|
*/
|
||||||
function destroyAll(cb) {
|
function destroyAll(cb) {
|
||||||
targetClass.find(this._scope, function (err, data) {
|
targetClass.destroyAll(this._scope, cb);
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/*!
|
||||||
* Merge `base` and `update` params
|
* Merge query parameters
|
||||||
* @param {Object} base - base object (updating this object)
|
* @param {Object} base The base object to contain the merged results
|
||||||
* @param {Object} update - object with new data to update base
|
* @param {Object} update The object containing updates to be merged
|
||||||
* @returns {Object} `base`
|
* @returns {*|Object} The base object
|
||||||
|
* @private
|
||||||
*/
|
*/
|
||||||
function merge(base, update) {
|
function mergeQuery(base, update) {
|
||||||
|
if (!update) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
base = base || {};
|
base = base || {};
|
||||||
if (update) {
|
if (update.where && Object.keys(update.where).length > 0) {
|
||||||
Object.keys(update).forEach(function (key) {
|
if (base.where && Object.keys(base.where).length > 0) {
|
||||||
base[key] = update[key];
|
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;
|
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'}}});
|
var Account = ds.define('Account', {userId: Number, type: String}, {relations: {user: {type: 'belongsTo', model: 'User'}}});
|
||||||
|
|
||||||
assert(Post.relations['user']);
|
assert(Post.relations['user']);
|
||||||
assert.deepEqual(Post.relations['user'], {
|
assert.deepEqual(Post.relations['user'].toJSON(), {
|
||||||
|
name: 'user',
|
||||||
type: 'belongsTo',
|
type: 'belongsTo',
|
||||||
|
modelFrom: 'Post',
|
||||||
keyFrom: 'userId',
|
keyFrom: 'userId',
|
||||||
|
modelTo: 'User',
|
||||||
keyTo: 'id',
|
keyTo: 'id',
|
||||||
modelTo: User,
|
|
||||||
multiple: false
|
multiple: false
|
||||||
});
|
});
|
||||||
assert(User.relations['posts']);
|
assert(User.relations['posts']);
|
||||||
assert.deepEqual(User.relations['posts'], {
|
assert.deepEqual(User.relations['posts'].toJSON(), {
|
||||||
|
name: 'posts',
|
||||||
type: 'hasMany',
|
type: 'hasMany',
|
||||||
|
modelFrom: 'User',
|
||||||
keyFrom: 'id',
|
keyFrom: 'id',
|
||||||
|
modelTo: 'Post',
|
||||||
keyTo: 'userId',
|
keyTo: 'userId',
|
||||||
modelTo: Post,
|
|
||||||
multiple: true
|
multiple: true
|
||||||
});
|
});
|
||||||
assert(User.relations['accounts']);
|
assert(User.relations['accounts']);
|
||||||
assert.deepEqual(User.relations['accounts'], {
|
assert.deepEqual(User.relations['accounts'].toJSON(), {
|
||||||
|
name: 'accounts',
|
||||||
type: 'hasMany',
|
type: 'hasMany',
|
||||||
|
modelFrom: 'User',
|
||||||
keyFrom: 'id',
|
keyFrom: 'id',
|
||||||
|
modelTo: 'Account',
|
||||||
keyTo: 'userId',
|
keyTo: 'userId',
|
||||||
modelTo: Account,
|
|
||||||
multiple: true
|
multiple: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// This test written in mocha+should.js
|
// This test written in mocha+should.js
|
||||||
var should = require('./init.js');
|
var should = require('./init.js');
|
||||||
|
|
||||||
var db, Book, Chapter, Author, Reader;
|
var db, Book, Chapter, Author, Reader, Publisher;
|
||||||
|
|
||||||
describe('relations', function () {
|
describe('relations', function () {
|
||||||
before(function (done) {
|
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 () {
|
describe('hasAndBelongsToMany', function () {
|
||||||
var Article, Tag, ArticleTag;
|
var Article, Tag, ArticleTag;
|
||||||
it('can be declared', function (done) {
|
it('can be declared', function (done) {
|
||||||
|
|
|
@ -3,7 +3,7 @@ var should = require('./init.js');
|
||||||
|
|
||||||
var db, Railway, Station;
|
var db, Railway, Station;
|
||||||
|
|
||||||
describe('sc0pe', function () {
|
describe('scope', function () {
|
||||||
|
|
||||||
before(function () {
|
before(function () {
|
||||||
db = getSchema();
|
db = getSchema();
|
||||||
|
|
Loading…
Reference in New Issue