Merge pull request #197 from fabien/feature/references
embedsMany (subdocs) and referencesMany (array of ids)
This commit is contained in:
commit
56a7df4c63
|
@ -17,6 +17,7 @@ exports.initialize = function initializeDataSource(dataSource, callback) {
|
|||
};
|
||||
|
||||
exports.Memory = Memory;
|
||||
exports.applyFilter = applyFilter;
|
||||
|
||||
function Memory(m, settings) {
|
||||
if (m instanceof Memory) {
|
||||
|
|
79
lib/dao.js
79
lib/dao.js
|
@ -14,6 +14,7 @@ var Relation = require('./relations.js');
|
|||
var Inclusion = require('./include.js');
|
||||
var List = require('./list.js');
|
||||
var geo = require('./geo');
|
||||
var mergeQuery = require('./scope.js').mergeQuery;
|
||||
var Memory = require('./connectors/memory').Memory;
|
||||
var utils = require('./utils');
|
||||
var fieldsToArray = utils.fieldsToArray;
|
||||
|
@ -324,6 +325,47 @@ DataAccessObject.findById = function find(id, cb) {
|
|||
}.bind(this));
|
||||
};
|
||||
|
||||
DataAccessObject.findByIds = function(ids, cond, cb) {
|
||||
if (typeof cond === 'function') {
|
||||
cb = cond;
|
||||
cond = {};
|
||||
}
|
||||
|
||||
var pk = this.dataSource.idName(this.modelName) || 'id';
|
||||
if (ids.length === 0) {
|
||||
process.nextTick(function() { cb(null, []); });
|
||||
return;
|
||||
}
|
||||
|
||||
var filter = { where: {} };
|
||||
filter.where[pk] = { inq: ids };
|
||||
mergeQuery(filter, cond || {});
|
||||
this.find(filter, function(err, results) {
|
||||
cb(err, err ? results : this.sortByIds(ids, results));
|
||||
}.bind(this));
|
||||
};
|
||||
|
||||
DataAccessObject.sortByIds = function(ids, results) {
|
||||
var pk = this.dataSource.idName(this.modelName) || 'id';
|
||||
ids = ids.map(function(id) {
|
||||
return (typeof id === 'object') ? id.toString() : id;
|
||||
});
|
||||
|
||||
results.sort(function(x, y) {
|
||||
var idA = (typeof x[pk] === 'object') ? x[pk].toString() : x[pk];
|
||||
var idB = (typeof y[pk] === 'object') ? y[pk].toString() : y[pk];
|
||||
var a = ids.indexOf(idA);
|
||||
var b = ids.indexOf(idB);
|
||||
if (a === -1 || b === -1) return 1; // last
|
||||
if (a !== b) {
|
||||
if (a > b) return 1;
|
||||
if (a < b) return -1;
|
||||
}
|
||||
});
|
||||
|
||||
return results;
|
||||
};
|
||||
|
||||
function convertNullToNotFoundError(ctx, cb) {
|
||||
if (ctx.result !== null) return cb();
|
||||
|
||||
|
@ -1046,6 +1088,17 @@ DataAccessObject.prototype.remove =
|
|||
}.bind(this));
|
||||
}, null, cb);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a single attribute.
|
||||
* Equivalent to `setAttributes({name: value})`
|
||||
*
|
||||
* @param {String} name Name of property
|
||||
* @param {Mixed} value Value of property
|
||||
*/
|
||||
DataAccessObject.prototype.setAttribute = function setAttribute(name, value) {
|
||||
this[name] = value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update a single attribute.
|
||||
|
@ -1062,7 +1115,27 @@ DataAccessObject.prototype.updateAttribute = function updateAttribute(name, valu
|
|||
};
|
||||
|
||||
/**
|
||||
* Update saet of attributes.
|
||||
* Update set of attributes.
|
||||
*
|
||||
* @trigger `change` hook
|
||||
* @param {Object} data Data to update
|
||||
*/
|
||||
DataAccessObject.prototype.setAttributes = function setAttributes(data) {
|
||||
if (typeof data !== 'object') return;
|
||||
|
||||
var Model = this.constructor;
|
||||
var inst = this;
|
||||
|
||||
// update instance's properties
|
||||
for (var key in data) {
|
||||
inst.setAttribute(key, data[key]);
|
||||
}
|
||||
|
||||
Model.emit('set', inst);
|
||||
};
|
||||
|
||||
/**
|
||||
* Update set of attributes.
|
||||
* Performs validation before updating.
|
||||
*
|
||||
* @trigger `validation`, `save` and `update` hooks
|
||||
|
@ -1086,9 +1159,7 @@ DataAccessObject.prototype.updateAttributes = function updateAttributes(data, cb
|
|||
}
|
||||
|
||||
// update instance's properties
|
||||
for (var key in data) {
|
||||
inst[key] = data[key];
|
||||
}
|
||||
inst.setAttributes(data);
|
||||
|
||||
inst.isValid(function (valid) {
|
||||
if (!valid) {
|
||||
|
|
|
@ -94,7 +94,7 @@ Inclusion.include = function (objects, include, cb) {
|
|||
subInclude = null;
|
||||
}
|
||||
var relation = relations[relationName];
|
||||
|
||||
|
||||
if (!relation) {
|
||||
cb(new Error('Relation "' + relationName + '" is not defined for '
|
||||
+ self.modelName + ' model'));
|
||||
|
@ -106,7 +106,7 @@ Inclusion.include = function (objects, include, cb) {
|
|||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Calling the relation method for each object
|
||||
async.each(objs, function (obj, callback) {
|
||||
if(relation.type === 'belongsTo') {
|
||||
|
@ -133,11 +133,11 @@ Inclusion.include = function (objects, include, cb) {
|
|||
obj.__cachedRelations[relationName] = result;
|
||||
if(obj === inst) {
|
||||
obj.__data[relationName] = result;
|
||||
obj.strict = false;
|
||||
obj.setStrict(false);
|
||||
} else {
|
||||
obj[relationName] = result;
|
||||
}
|
||||
|
||||
|
||||
if (subInclude && result) {
|
||||
var subItems = relation.multiple ? result : [result];
|
||||
// Recursively include the related models
|
||||
|
|
|
@ -7,6 +7,8 @@ var i8n = require('inflection');
|
|||
var defineScope = require('./scope.js').defineScope;
|
||||
var mergeQuery = require('./scope.js').mergeQuery;
|
||||
var ModelBaseClass = require('./model.js');
|
||||
var applyFilter = require('./connectors/memory').applyFilter;
|
||||
var ValidationError = require('./validations.js').ValidationError;
|
||||
|
||||
exports.Relation = Relation;
|
||||
exports.RelationDefinition = RelationDefinition;
|
||||
|
@ -15,7 +17,9 @@ var RelationTypes = {
|
|||
belongsTo: 'belongsTo',
|
||||
hasMany: 'hasMany',
|
||||
hasOne: 'hasOne',
|
||||
hasAndBelongsToMany: 'hasAndBelongsToMany'
|
||||
hasAndBelongsToMany: 'hasAndBelongsToMany',
|
||||
referencesMany: 'referencesMany',
|
||||
embedsMany: 'embedsMany'
|
||||
};
|
||||
|
||||
exports.RelationTypes = RelationTypes;
|
||||
|
@ -24,13 +28,17 @@ exports.HasManyThrough = HasManyThrough;
|
|||
exports.HasOne = HasOne;
|
||||
exports.HasAndBelongsToMany = HasAndBelongsToMany;
|
||||
exports.BelongsTo = BelongsTo;
|
||||
exports.ReferencesMany = ReferencesMany;
|
||||
exports.EmbedsMany = EmbedsMany;
|
||||
|
||||
var RelationClasses = {
|
||||
belongsTo: BelongsTo,
|
||||
hasMany: HasMany,
|
||||
hasManyThrough: HasManyThrough,
|
||||
hasOne: HasOne,
|
||||
hasAndBelongsToMany: HasAndBelongsToMany
|
||||
hasAndBelongsToMany: HasAndBelongsToMany,
|
||||
referencesMany: ReferencesMany,
|
||||
embedsMany: EmbedsMany
|
||||
};
|
||||
|
||||
function normalizeType(type) {
|
||||
|
@ -44,7 +52,35 @@ function normalizeType(type) {
|
|||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
function extendScopeMethods(definition, scopeMethods, ext) {
|
||||
var customMethods = [];
|
||||
var relationClass = RelationClasses[definition.type];
|
||||
if (definition.type === RelationTypes.hasMany && definition.modelThrough) {
|
||||
relationClass = RelationClasses.hasManyThrough;
|
||||
}
|
||||
if (typeof ext === 'function') {
|
||||
customMethods = ext.call(definition, scopeMethods, relationClass);
|
||||
} else if (typeof ext === 'object') {
|
||||
for (var key in ext) {
|
||||
var relationMethod = ext[key];
|
||||
var method = scopeMethods[key] = function () {
|
||||
var relation = new relationClass(definition, this);
|
||||
return relationMethod.apply(relation, arguments);
|
||||
};
|
||||
if (relationMethod.shared) {
|
||||
method.shared = true;
|
||||
method.accepts = relationMethod.accepts;
|
||||
method.returns = relationMethod.returns;
|
||||
method.http = relationMethod.http;
|
||||
method.description = relationMethod.description;
|
||||
}
|
||||
customMethods.push(key);
|
||||
}
|
||||
}
|
||||
return [].concat(customMethods || []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Relation definition class. Use to define relationships between models.
|
||||
|
@ -57,6 +93,7 @@ function RelationDefinition(definition) {
|
|||
}
|
||||
definition = definition || {};
|
||||
this.name = definition.name;
|
||||
this.accessor = definition.accessor || this.name;
|
||||
assert(this.name, 'Relation name is missing');
|
||||
this.type = normalizeType(definition.type);
|
||||
assert(this.type, 'Invalid relation type: ' + definition.type);
|
||||
|
@ -75,6 +112,7 @@ function RelationDefinition(definition) {
|
|||
this.properties = definition.properties || {};
|
||||
this.options = definition.options || {};
|
||||
this.scope = definition.scope;
|
||||
this.embed = definition.embed === true;
|
||||
}
|
||||
|
||||
RelationDefinition.prototype.toJSON = function () {
|
||||
|
@ -100,6 +138,7 @@ RelationDefinition.prototype.toJSON = function () {
|
|||
* @param {Object} filter (where, order, limit, fields, ...)
|
||||
*/
|
||||
RelationDefinition.prototype.applyScope = function(modelInstance, filter) {
|
||||
filter = filter || {};
|
||||
filter.where = filter.where || {};
|
||||
if ((this.type !== 'belongsTo' || this.type === 'hasOne')
|
||||
&& typeof this.discriminator === 'string') { // polymorphic
|
||||
|
@ -290,6 +329,41 @@ function HasOne(definition, modelInstance) {
|
|||
|
||||
util.inherits(HasOne, Relation);
|
||||
|
||||
/**
|
||||
* EmbedsMany subclass
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {EmbedsMany}
|
||||
* @constructor
|
||||
* @class EmbedsMany
|
||||
*/
|
||||
function EmbedsMany(definition, modelInstance) {
|
||||
if (!(this instanceof EmbedsMany)) {
|
||||
return new EmbedsMany(definition, modelInstance);
|
||||
}
|
||||
assert(definition.type === RelationTypes.embedsMany);
|
||||
Relation.apply(this, arguments);
|
||||
}
|
||||
|
||||
util.inherits(EmbedsMany, Relation);
|
||||
|
||||
/**
|
||||
* ReferencesMany subclass
|
||||
* @param {RelationDefinition|Object} definition
|
||||
* @param {Object} modelInstance
|
||||
* @returns {ReferencesMany}
|
||||
* @constructor
|
||||
* @class ReferencesMany
|
||||
*/
|
||||
function ReferencesMany(definition, modelInstance) {
|
||||
if (!(this instanceof ReferencesMany)) {
|
||||
return new ReferencesMany(definition, modelInstance);
|
||||
}
|
||||
assert(definition.type === RelationTypes.referencesMany);
|
||||
Relation.apply(this, arguments);
|
||||
}
|
||||
|
||||
util.inherits(ReferencesMany, Relation);
|
||||
|
||||
/*!
|
||||
* Find the relation by foreign key
|
||||
|
@ -455,6 +529,16 @@ RelationDefinition.hasMany = function hasMany(modelFrom, modelTo, params) {
|
|||
scopeMethods.build = scopeMethod(definition, 'build');
|
||||
}
|
||||
|
||||
var customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
|
||||
|
||||
for (var i = 0; i < customMethods.length; i++) {
|
||||
var methodName = customMethods[i];
|
||||
var method = scopeMethods[methodName];
|
||||
if (typeof method === 'function' && method.shared === true) {
|
||||
modelFrom.prototype['__' + methodName + '__' + relationName] = method;
|
||||
}
|
||||
};
|
||||
|
||||
// Mix the property and scoped methods into the prototype class
|
||||
defineScope(modelFrom.prototype, params.through || modelTo, relationName, function () {
|
||||
var filter = {};
|
||||
|
@ -505,6 +589,7 @@ function scopeMethod(definition, methodName) {
|
|||
*/
|
||||
HasMany.prototype.findById = function (fkId, cb) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var modelFrom = this.definition.modelFrom;
|
||||
var fk = this.definition.keyTo;
|
||||
var pk = this.definition.keyFrom;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
@ -534,7 +619,7 @@ HasMany.prototype.findById = function (fkId, cb) {
|
|||
if (inst[fk] && inst[fk].toString() === modelInstance[pk].toString()) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
err = new Error('Key mismatch: ' + this.definition.modelFrom.modelName + '.' + pk
|
||||
err = new Error('Key mismatch: ' + modelFrom.modelName + '.' + pk
|
||||
+ ': ' + modelInstance[pk]
|
||||
+ ', ' + modelTo.modelName + '.' + fk + ': ' + inst[fk]);
|
||||
err.statusCode = 400;
|
||||
|
@ -1013,6 +1098,11 @@ BelongsTo.prototype.related = function (refresh, params) {
|
|||
modelTo = params.constructor;
|
||||
modelInstance[fk] = params[pk];
|
||||
if (discriminator) modelInstance[discriminator] = params.constructor.modelName;
|
||||
|
||||
var data = {};
|
||||
this.definition.applyProperties(params, data);
|
||||
modelInstance.setAttributes(data);
|
||||
|
||||
self.resetCache(params);
|
||||
} else if (typeof params === 'function') { // acts as async getter
|
||||
|
||||
|
@ -1375,3 +1465,789 @@ HasOne.prototype.related = function (refresh, params) {
|
|||
self.resetCache();
|
||||
}
|
||||
};
|
||||
|
||||
RelationDefinition.embedsMany = function embedsMany(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 accessorName = params.as || (i8n.camelize(modelTo.modelName, true) + 'List');
|
||||
var relationName = params.property || i8n.camelize(modelTo.pluralModelName, true);
|
||||
var fk = modelTo.dataSource.idName(modelTo.modelName) || 'id';
|
||||
var idName = modelFrom.dataSource.idName(modelFrom.modelName) || 'id';
|
||||
|
||||
var definition = modelFrom.relations[accessorName] = new RelationDefinition({
|
||||
accessor: accessorName,
|
||||
name: relationName,
|
||||
type: RelationTypes.embedsMany,
|
||||
modelFrom: modelFrom,
|
||||
keyFrom: idName,
|
||||
keyTo: fk,
|
||||
modelTo: modelTo,
|
||||
multiple: true,
|
||||
properties: params.properties,
|
||||
scope: params.scope,
|
||||
options: params.options,
|
||||
embed: true
|
||||
});
|
||||
|
||||
modelFrom.dataSource.defineProperty(modelFrom.modelName, relationName, {
|
||||
type: [modelTo], default: function() { return []; }
|
||||
});
|
||||
|
||||
// unique id is required
|
||||
modelTo.validatesPresenceOf(idName);
|
||||
|
||||
if (!params.polymorphic) {
|
||||
modelFrom.validate(relationName, function(err) {
|
||||
var embeddedList = this[relationName] || [];
|
||||
var ids = embeddedList.map(function(m) { return m[idName]; });
|
||||
var uniqueIds = ids.filter(function(id, pos) {
|
||||
return ids.indexOf(id) === pos;
|
||||
});
|
||||
if (ids.length !== uniqueIds.length) {
|
||||
this.errors.add(relationName, 'Contains duplicate `' + idName + '`', 'uniqueness');
|
||||
err(false);
|
||||
}
|
||||
}, { code: 'uniqueness' })
|
||||
}
|
||||
|
||||
// validate all embedded items
|
||||
if (definition.options.validate) {
|
||||
modelFrom.validate(relationName, function(err) {
|
||||
var self = this;
|
||||
var embeddedList = this[relationName] || [];
|
||||
var hasErrors = false;
|
||||
embeddedList.forEach(function(item) {
|
||||
if (item instanceof modelTo) {
|
||||
if (!item.isValid()) {
|
||||
hasErrors = true;
|
||||
var id = item[idName] || '(blank)';
|
||||
var first = Object.keys(item.errors)[0];
|
||||
var msg = 'contains invalid item: `' + id + '`';
|
||||
msg += ' (' + first + ' ' + item.errors[first] + ')';
|
||||
self.errors.add(relationName, msg, 'invalid');
|
||||
}
|
||||
} else {
|
||||
hasErrors = true;
|
||||
self.errors.add(relationName, 'Contains invalid item', 'invalid');
|
||||
}
|
||||
});
|
||||
if (hasErrors) err(false);
|
||||
});
|
||||
}
|
||||
|
||||
var scopeMethods = {
|
||||
findById: scopeMethod(definition, 'findById'),
|
||||
destroy: scopeMethod(definition, 'destroyById'),
|
||||
updateById: scopeMethod(definition, 'updateById'),
|
||||
exists: scopeMethod(definition, 'exists'),
|
||||
add: scopeMethod(definition, 'add'),
|
||||
remove: scopeMethod(definition, 'remove'),
|
||||
get: scopeMethod(definition, 'get'),
|
||||
set: scopeMethod(definition, 'set'),
|
||||
unset: scopeMethod(definition, 'unset'),
|
||||
at: scopeMethod(definition, 'at')
|
||||
};
|
||||
|
||||
var findByIdFunc = scopeMethods.findById;
|
||||
modelFrom.prototype['__findById__' + accessorName] = findByIdFunc;
|
||||
|
||||
var destroyByIdFunc = scopeMethods.destroy;
|
||||
modelFrom.prototype['__destroyById__' + accessorName] = destroyByIdFunc;
|
||||
|
||||
var updateByIdFunc = scopeMethods.updateById;
|
||||
modelFrom.prototype['__updateById__' + accessorName] = updateByIdFunc;
|
||||
|
||||
var addFunc = scopeMethods.add;
|
||||
modelFrom.prototype['__link__' + accessorName] = addFunc;
|
||||
|
||||
var removeFunc = scopeMethods.remove;
|
||||
modelFrom.prototype['__unlink__' + accessorName] = removeFunc;
|
||||
|
||||
scopeMethods.create = scopeMethod(definition, 'create');
|
||||
scopeMethods.build = scopeMethod(definition, 'build');
|
||||
|
||||
scopeMethods.related = scopeMethod(definition, 'related'); // bound to definition
|
||||
|
||||
var customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
|
||||
|
||||
for (var i = 0; i < customMethods.length; i++) {
|
||||
var methodName = customMethods[i];
|
||||
var method = scopeMethods[methodName];
|
||||
if (typeof method === 'function' && method.shared === true) {
|
||||
modelFrom.prototype['__' + methodName + '__' + accessorName] = method;
|
||||
}
|
||||
};
|
||||
|
||||
// Mix the property and scoped methods into the prototype class
|
||||
var scopeDefinition = defineScope(modelFrom.prototype, modelTo, accessorName, function () {
|
||||
return {};
|
||||
}, scopeMethods);
|
||||
|
||||
scopeDefinition.related = scopeMethods.related;
|
||||
};
|
||||
|
||||
EmbedsMany.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var relationName = this.definition.name;
|
||||
var modelInstance = this.modelInstance;
|
||||
var self = receiver;
|
||||
|
||||
var actualCond = {};
|
||||
var actualRefresh = false;
|
||||
if (arguments.length === 3) {
|
||||
cb = condOrRefresh;
|
||||
} else if (arguments.length === 4) {
|
||||
if (typeof condOrRefresh === 'boolean') {
|
||||
actualRefresh = condOrRefresh;
|
||||
} else {
|
||||
actualCond = condOrRefresh;
|
||||
actualRefresh = true;
|
||||
}
|
||||
} else {
|
||||
throw new Error('Method can be only called with one or two arguments');
|
||||
}
|
||||
|
||||
var embeddedList = self[relationName] || [];
|
||||
|
||||
this.definition.applyScope(modelInstance, actualCond);
|
||||
|
||||
var params = mergeQuery(actualCond, scopeParams);
|
||||
|
||||
if (params.where) {
|
||||
embeddedList = embeddedList ? embeddedList.filter(applyFilter(params)) : embeddedList;
|
||||
}
|
||||
|
||||
var returnRelated = function(list) {
|
||||
if (params.include) {
|
||||
modelTo.include(list, params.include, cb);
|
||||
} else {
|
||||
process.nextTick(function() { cb(null, list); });
|
||||
}
|
||||
};
|
||||
|
||||
returnRelated(embeddedList);
|
||||
};
|
||||
|
||||
EmbedsMany.prototype.findById = function (fkId, cb) {
|
||||
var pk = this.definition.keyFrom;
|
||||
var modelTo = this.definition.modelTo;
|
||||
var relationName = this.definition.name;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
var embeddedList = modelInstance[relationName] || [];
|
||||
|
||||
var find = function(id) {
|
||||
for (var i = 0; i < embeddedList.length; i++) {
|
||||
var item = embeddedList[i];
|
||||
if (item[pk].toString() === id) return item;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
var item = find(fkId.toString()); // in case of explicit id
|
||||
item = (item instanceof modelTo) ? item : null;
|
||||
|
||||
if (typeof cb === 'function') {
|
||||
process.nextTick(function() {
|
||||
cb(null, item);
|
||||
});
|
||||
};
|
||||
|
||||
return item; // sync
|
||||
};
|
||||
|
||||
EmbedsMany.prototype.exists = function (fkId, cb) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var inst = this.findById(fkId, function (err, inst) {
|
||||
if (cb) cb(err, inst instanceof modelTo);
|
||||
});
|
||||
return inst instanceof modelTo; // sync
|
||||
};
|
||||
|
||||
EmbedsMany.prototype.updateById = function (fkId, data, cb) {
|
||||
if (typeof data === 'function') {
|
||||
cb = data;
|
||||
data = {};
|
||||
}
|
||||
|
||||
var modelTo = this.definition.modelTo;
|
||||
var relationName = this.definition.name;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
var embeddedList = modelInstance[relationName] || [];
|
||||
|
||||
var inst = this.findById(fkId);
|
||||
|
||||
if (inst instanceof modelTo) {
|
||||
if (typeof data === 'object') {
|
||||
for (var key in data) {
|
||||
inst[key] = data[key];
|
||||
}
|
||||
}
|
||||
var err = inst.isValid() ? null : new ValidationError(inst);
|
||||
if (err && typeof cb === 'function') {
|
||||
return process.nextTick(function() {
|
||||
cb(err, inst);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof cb === 'function') {
|
||||
modelInstance.updateAttribute(relationName,
|
||||
embeddedList, function(err) {
|
||||
cb(err, inst);
|
||||
});
|
||||
}
|
||||
} else if (typeof cb === 'function') {
|
||||
process.nextTick(function() {
|
||||
cb(null, null); // not found
|
||||
});
|
||||
}
|
||||
return inst; // sync
|
||||
};
|
||||
|
||||
EmbedsMany.prototype.destroyById = function (fkId, cb) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var relationName = this.definition.name;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
var embeddedList = modelInstance[relationName] || [];
|
||||
|
||||
var inst = (fkId instanceof modelTo) ? fkId : this.findById(fkId);
|
||||
|
||||
if (inst instanceof modelTo) {
|
||||
var index = embeddedList.indexOf(inst);
|
||||
if (index > -1) embeddedList.splice(index, 1);
|
||||
if (typeof cb === 'function') {
|
||||
modelInstance.updateAttribute(relationName,
|
||||
embeddedList, function(err) {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
} else if (typeof cb === 'function') {
|
||||
process.nextTick(cb); // not found
|
||||
}
|
||||
return inst; // sync
|
||||
};
|
||||
|
||||
EmbedsMany.prototype.get = EmbedsMany.prototype.findById;
|
||||
EmbedsMany.prototype.set = EmbedsMany.prototype.updateById;
|
||||
EmbedsMany.prototype.unset = EmbedsMany.prototype.destroyById;
|
||||
|
||||
EmbedsMany.prototype.at = function (index, cb) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var relationName = this.definition.name;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
var embeddedList = modelInstance[relationName] || [];
|
||||
|
||||
var item = embeddedList[parseInt(index)];
|
||||
item = (item instanceof modelTo) ? item : null;
|
||||
|
||||
if (typeof cb === 'function') {
|
||||
process.nextTick(function() {
|
||||
cb(null, item);
|
||||
});
|
||||
};
|
||||
|
||||
return item; // sync
|
||||
};
|
||||
|
||||
EmbedsMany.prototype.create = function (targetModelData, cb) {
|
||||
var pk = this.definition.keyFrom;
|
||||
var modelTo = this.definition.modelTo;
|
||||
var relationName = this.definition.name;
|
||||
var modelInstance = this.modelInstance;
|
||||
var autoId = this.definition.options.autoId !== false;
|
||||
|
||||
if (typeof targetModelData === 'function' && !cb) {
|
||||
cb = targetModelData;
|
||||
targetModelData = {};
|
||||
}
|
||||
targetModelData = targetModelData || {};
|
||||
|
||||
var embeddedList = modelInstance[relationName] || [];
|
||||
|
||||
var inst = this.build(targetModelData);
|
||||
|
||||
var err = inst.isValid() ? null : new ValidationError(inst);
|
||||
|
||||
if (err) {
|
||||
return process.nextTick(function() {
|
||||
cb(err);
|
||||
});
|
||||
}
|
||||
|
||||
modelInstance.updateAttribute(relationName,
|
||||
embeddedList, function(err, modelInst) {
|
||||
cb(err, err ? null : inst);
|
||||
});
|
||||
};
|
||||
|
||||
EmbedsMany.prototype.build = function(targetModelData) {
|
||||
var pk = this.definition.keyFrom;
|
||||
var modelTo = this.definition.modelTo;
|
||||
var relationName = this.definition.name;
|
||||
var modelInstance = this.modelInstance;
|
||||
var autoId = this.definition.options.autoId !== false;
|
||||
|
||||
var embeddedList = modelInstance[relationName] || [];
|
||||
|
||||
targetModelData = targetModelData || {};
|
||||
|
||||
if (typeof targetModelData[pk] !== 'number' && autoId) {
|
||||
var ids = embeddedList.map(function(m) {
|
||||
return (typeof m[pk] === 'number' ? m[pk] : 0);
|
||||
});
|
||||
if (ids.length > 0) {
|
||||
targetModelData[pk] = Math.max.apply(null, ids) + 1;
|
||||
} else {
|
||||
targetModelData[pk] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.definition.applyProperties(this.modelInstance, targetModelData);
|
||||
|
||||
var inst = new modelTo(targetModelData);
|
||||
|
||||
if (this.definition.options.prepend) {
|
||||
embeddedList.unshift(inst);
|
||||
} else {
|
||||
embeddedList.push(inst);
|
||||
}
|
||||
|
||||
return inst;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the target model instance to the 'embedsMany' relation
|
||||
* @param {Object|ID} acInst The actual instance or id value
|
||||
*/
|
||||
EmbedsMany.prototype.add = function (acInst, data, cb) {
|
||||
if (typeof data === 'function') {
|
||||
cb = data;
|
||||
data = {};
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var definition = this.definition;
|
||||
var modelTo = this.definition.modelTo;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
var options = definition.options;
|
||||
var belongsTo = options.belongsTo && modelTo.relations[options.belongsTo];
|
||||
|
||||
if (!belongsTo) {
|
||||
throw new Error('Invalid reference: ' + options.belongsTo || '(none)');
|
||||
}
|
||||
|
||||
var fk2 = belongsTo.keyTo;
|
||||
var pk2 = belongsTo.modelTo.definition.idName() || 'id';
|
||||
|
||||
var query = {};
|
||||
|
||||
query[fk2] = (acInst instanceof belongsTo.modelTo) ? acInst[pk2] : acInst;
|
||||
|
||||
var filter = { where: query };
|
||||
|
||||
belongsTo.applyScope(modelInstance, filter);
|
||||
|
||||
belongsTo.modelTo.findOne(filter, function(err, ref) {
|
||||
if (ref instanceof belongsTo.modelTo) {
|
||||
var inst = self.build(data || {});
|
||||
inst[options.belongsTo](ref);
|
||||
modelInstance.save(function(err) {
|
||||
cb(err, err ? null : inst);
|
||||
});
|
||||
} else {
|
||||
cb(null, null);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the target model instance from the 'embedsMany' relation
|
||||
* @param {Object|ID) acInst The actual instance or id value
|
||||
*/
|
||||
EmbedsMany.prototype.remove = function (acInst, cb) {
|
||||
var self = this;
|
||||
var definition = this.definition;
|
||||
var modelTo = this.definition.modelTo;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
var options = definition.options;
|
||||
var belongsTo = options.belongsTo && modelTo.relations[options.belongsTo];
|
||||
|
||||
if (!belongsTo) {
|
||||
throw new Error('Invalid reference: ' + options.belongsTo || '(none)');
|
||||
}
|
||||
|
||||
var fk2 = belongsTo.keyTo;
|
||||
var pk2 = belongsTo.modelTo.definition.idName() || 'id';
|
||||
|
||||
var query = {};
|
||||
|
||||
query[fk2] = (acInst instanceof belongsTo.modelTo) ? acInst[pk2] : acInst;
|
||||
|
||||
var filter = { where: query };
|
||||
|
||||
belongsTo.applyScope(modelInstance, filter);
|
||||
|
||||
modelInstance[definition.accessor](filter, function(err, items) {
|
||||
if (err) return cb(err);
|
||||
|
||||
items.forEach(function(item) {
|
||||
self.unset(item);
|
||||
});
|
||||
|
||||
modelInstance.save(function(err) {
|
||||
cb(err);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
RelationDefinition.referencesMany = function referencesMany(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(modelTo.modelName + '_ids', true);
|
||||
var idName = modelTo.dataSource.idName(modelTo.modelName) || 'id';
|
||||
var idType = modelTo.definition.properties[idName].type;
|
||||
|
||||
var definition = modelFrom.relations[relationName] = new RelationDefinition({
|
||||
name: relationName,
|
||||
type: RelationTypes.referencesMany,
|
||||
modelFrom: modelFrom,
|
||||
keyFrom: fk,
|
||||
keyTo: idName,
|
||||
modelTo: modelTo,
|
||||
multiple: true,
|
||||
properties: params.properties,
|
||||
scope: params.scope,
|
||||
options: params.options
|
||||
});
|
||||
|
||||
modelFrom.dataSource.defineProperty(modelFrom.modelName, fk, {
|
||||
type: [idType], default: function() { return []; }
|
||||
});
|
||||
|
||||
modelFrom.validate(relationName, function(err) {
|
||||
var ids = this[fk] || [];
|
||||
var uniqueIds = ids.filter(function(id, pos) {
|
||||
return ids.indexOf(id) === pos;
|
||||
});
|
||||
if (ids.length !== uniqueIds.length) {
|
||||
var msg = 'Contains duplicate `' + modelTo.modelName + '` instance';
|
||||
this.errors.add(relationName, msg, 'uniqueness');
|
||||
err(false);
|
||||
}
|
||||
}, { code: 'uniqueness' })
|
||||
|
||||
var scopeMethods = {
|
||||
findById: scopeMethod(definition, 'findById'),
|
||||
destroy: scopeMethod(definition, 'destroyById'),
|
||||
updateById: scopeMethod(definition, 'updateById'),
|
||||
exists: scopeMethod(definition, 'exists'),
|
||||
add: scopeMethod(definition, 'add'),
|
||||
remove: scopeMethod(definition, 'remove'),
|
||||
at: scopeMethod(definition, 'at')
|
||||
};
|
||||
|
||||
var findByIdFunc = scopeMethods.findById;
|
||||
modelFrom.prototype['__findById__' + relationName] = findByIdFunc;
|
||||
|
||||
var destroyByIdFunc = scopeMethods.destroy;
|
||||
modelFrom.prototype['__destroyById__' + relationName] = destroyByIdFunc;
|
||||
|
||||
var updateByIdFunc = scopeMethods.updateById;
|
||||
modelFrom.prototype['__updateById__' + relationName] = updateByIdFunc;
|
||||
|
||||
var addFunc = scopeMethods.add;
|
||||
modelFrom.prototype['__link__' + relationName] = addFunc;
|
||||
|
||||
var removeFunc = scopeMethods.remove;
|
||||
modelFrom.prototype['__unlink__' + relationName] = removeFunc;
|
||||
|
||||
scopeMethods.create = scopeMethod(definition, 'create');
|
||||
scopeMethods.build = scopeMethod(definition, 'build');
|
||||
|
||||
scopeMethods.related = scopeMethod(definition, 'related');
|
||||
|
||||
var customMethods = extendScopeMethods(definition, scopeMethods, params.scopeMethods);
|
||||
|
||||
for (var i = 0; i < customMethods.length; i++) {
|
||||
var methodName = customMethods[i];
|
||||
var method = scopeMethods[methodName];
|
||||
if (typeof method === 'function' && method.shared === true) {
|
||||
modelFrom.prototype['__' + methodName + '__' + relationName] = method;
|
||||
}
|
||||
};
|
||||
|
||||
// Mix the property and scoped methods into the prototype class
|
||||
var scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function () {
|
||||
return {};
|
||||
}, scopeMethods);
|
||||
|
||||
scopeDefinition.related = scopeMethods.related; // bound to definition
|
||||
};
|
||||
|
||||
ReferencesMany.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
|
||||
var fk = this.definition.keyFrom;
|
||||
var modelTo = this.definition.modelTo;
|
||||
var relationName = this.definition.name;
|
||||
var modelInstance = this.modelInstance;
|
||||
var self = receiver;
|
||||
|
||||
var actualCond = {};
|
||||
var actualRefresh = false;
|
||||
if (arguments.length === 3) {
|
||||
cb = condOrRefresh;
|
||||
} else if (arguments.length === 4) {
|
||||
if (typeof condOrRefresh === 'boolean') {
|
||||
actualRefresh = condOrRefresh;
|
||||
} else {
|
||||
actualCond = condOrRefresh;
|
||||
actualRefresh = true;
|
||||
}
|
||||
} else {
|
||||
throw new Error('Method can be only called with one or two arguments');
|
||||
}
|
||||
|
||||
var ids = self[fk] || [];
|
||||
|
||||
this.definition.applyScope(modelInstance, actualCond);
|
||||
|
||||
var params = mergeQuery(actualCond, scopeParams);
|
||||
|
||||
modelTo.findByIds(ids, params, cb);
|
||||
};
|
||||
|
||||
ReferencesMany.prototype.findById = function (fkId, cb) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
var modelFrom = this.definition.modelFrom;
|
||||
var relationName = this.definition.name;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
var modelTo = this.definition.modelTo;
|
||||
var pk = this.definition.keyTo;
|
||||
var fk = this.definition.keyFrom;
|
||||
|
||||
if (typeof fkId === 'object') {
|
||||
fkId = fkId.toString(); // mongodb
|
||||
}
|
||||
|
||||
var ids = [fkId];
|
||||
|
||||
var filter = {};
|
||||
|
||||
this.definition.applyScope(modelInstance, filter);
|
||||
|
||||
modelTo.findByIds(ids, filter, function (err, instances) {
|
||||
if (err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
var inst = instances[0];
|
||||
if (!inst) {
|
||||
err = new Error('No instance with id ' + fkId + ' found for ' + modelTo.modelName);
|
||||
err.statusCode = 404;
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
var currentIds = ids.map(function(id) { return id.toString(); });
|
||||
var id = (inst[pk] || '').toString(); // mongodb
|
||||
|
||||
// Check if the foreign key is amongst the ids
|
||||
if (currentIds.indexOf(id) > -1) {
|
||||
cb(null, inst);
|
||||
} else {
|
||||
err = new Error('Key mismatch: ' + modelFrom.modelName + '.' + fk
|
||||
+ ': ' + modelInstance[fk]
|
||||
+ ', ' + modelTo.modelName + '.' + pk + ': ' + inst[pk]);
|
||||
err.statusCode = 400;
|
||||
cb(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
ReferencesMany.prototype.exists = function (fkId, cb) {
|
||||
var fk = this.definition.keyFrom;
|
||||
var ids = this.modelInstance[fk] || [];
|
||||
var currentIds = ids.map(function(id) { return id.toString(); });
|
||||
var fkId = (fkId || '').toString(); // mongodb
|
||||
process.nextTick(function() { cb(null, currentIds.indexOf(fkId) > -1) });
|
||||
};
|
||||
|
||||
ReferencesMany.prototype.updateById = function (fkId, data, cb) {
|
||||
if (typeof data === 'function') {
|
||||
cb = data;
|
||||
data = {};
|
||||
}
|
||||
|
||||
this.findById(fkId, function(err, inst) {
|
||||
if (err) return cb(err);
|
||||
inst.updateAttributes(data, cb);
|
||||
});
|
||||
};
|
||||
|
||||
ReferencesMany.prototype.destroyById = function (fkId, cb) {
|
||||
var self = this;
|
||||
this.findById(fkId, function(err, inst) {
|
||||
if (err) return cb(err);
|
||||
self.remove(inst, function(err, ids) {
|
||||
inst.destroy(cb);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ReferencesMany.prototype.at = function (index, cb) {
|
||||
var fk = this.definition.keyFrom;
|
||||
var ids = this.modelInstance[fk] || [];
|
||||
this.findById(ids[index], cb);
|
||||
};
|
||||
|
||||
ReferencesMany.prototype.create = function (targetModelData, cb) {
|
||||
var definition = this.definition;
|
||||
var modelTo = this.definition.modelTo;
|
||||
var relationName = this.definition.name;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
var pk = this.definition.keyTo;
|
||||
var fk = this.definition.keyFrom;
|
||||
|
||||
if (typeof targetModelData === 'function' && !cb) {
|
||||
cb = targetModelData;
|
||||
targetModelData = {};
|
||||
}
|
||||
targetModelData = targetModelData || {};
|
||||
|
||||
var ids = modelInstance[fk] || [];
|
||||
|
||||
var inst = this.build(targetModelData);
|
||||
|
||||
inst.save(function(err, inst) {
|
||||
if (err) return cb(err, inst);
|
||||
|
||||
var id = inst[pk];
|
||||
|
||||
if (typeof id === 'object') {
|
||||
id = id.toString(); // mongodb
|
||||
}
|
||||
|
||||
if (definition.options.prepend) {
|
||||
ids.unshift(id);
|
||||
} else {
|
||||
ids.push(id);
|
||||
}
|
||||
|
||||
modelInstance.updateAttribute(fk,
|
||||
ids, function(err, modelInst) {
|
||||
cb(err, inst);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
ReferencesMany.prototype.build = function(targetModelData) {
|
||||
var modelTo = this.definition.modelTo;
|
||||
targetModelData = targetModelData || {};
|
||||
|
||||
this.definition.applyProperties(this.modelInstance, targetModelData);
|
||||
|
||||
return new modelTo(targetModelData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add the target model instance to the 'embedsMany' relation
|
||||
* @param {Object|ID} acInst The actual instance or id value
|
||||
*/
|
||||
ReferencesMany.prototype.add = function (acInst, cb) {
|
||||
var self = this;
|
||||
var definition = this.definition;
|
||||
var modelTo = this.definition.modelTo;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
var pk = this.definition.keyTo;
|
||||
var fk = this.definition.keyFrom;
|
||||
|
||||
var insert = function(inst, done) {
|
||||
var id = inst[pk];
|
||||
|
||||
if (typeof id === 'object') {
|
||||
id = id.toString(); // mongodb
|
||||
}
|
||||
|
||||
var ids = modelInstance[fk] || [];
|
||||
|
||||
if (definition.options.prepend) {
|
||||
ids.unshift(id);
|
||||
} else {
|
||||
ids.push(id);
|
||||
}
|
||||
|
||||
modelInstance.updateAttribute(fk, ids, function(err) {
|
||||
done(err, err ? null : inst);
|
||||
});
|
||||
};
|
||||
|
||||
if (acInst instanceof modelTo) {
|
||||
insert(acInst, cb);
|
||||
} else {
|
||||
var filter = { where: {} };
|
||||
filter.where[pk] = acInst;
|
||||
|
||||
definition.applyScope(modelInstance, filter);
|
||||
|
||||
modelTo.findOne(filter, function (err, inst) {
|
||||
if (err || !inst) return cb(err, null);
|
||||
insert(inst, cb);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove the target model instance from the 'embedsMany' relation
|
||||
* @param {Object|ID) acInst The actual instance or id value
|
||||
*/
|
||||
ReferencesMany.prototype.remove = function (acInst, cb) {
|
||||
var definition = this.definition;
|
||||
var modelInstance = this.modelInstance;
|
||||
|
||||
var pk = this.definition.keyTo;
|
||||
var fk = this.definition.keyFrom;
|
||||
|
||||
var ids = modelInstance[fk] || [];
|
||||
|
||||
var currentIds = ids.map(function(id) { return id.toString(); });
|
||||
|
||||
var id = (acInst instanceof definition.modelTo) ? acInst[pk] : acInst;
|
||||
id = id.toString();
|
||||
|
||||
var index = currentIds.indexOf(id);
|
||||
if (index > -1) {
|
||||
ids.splice(index, 1);
|
||||
modelInstance.updateAttribute(fk, ids, function(err, inst) {
|
||||
cb(err, inst[fk] || []);
|
||||
});
|
||||
} else {
|
||||
process.nextTick(function() { cb(null, ids); });
|
||||
}
|
||||
};
|
|
@ -161,3 +161,11 @@ RelationMixin.hasAndBelongsToMany = function hasAndBelongsToMany(modelTo, params
|
|||
RelationMixin.hasOne = function hasMany(modelTo, params) {
|
||||
RelationDefinition.hasOne(this, modelTo, params);
|
||||
};
|
||||
|
||||
RelationMixin.referencesMany = function hasMany(modelTo, params) {
|
||||
RelationDefinition.referencesMany(this, modelTo, params);
|
||||
};
|
||||
|
||||
RelationMixin.embedsMany = function hasMany(modelTo, params) {
|
||||
RelationDefinition.embedsMany(this, modelTo, params);
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ function ScopeDefinition(definition) {
|
|||
ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefresh, cb) {
|
||||
var name = this.name;
|
||||
var self = receiver;
|
||||
|
||||
|
||||
var actualCond = {};
|
||||
var actualRefresh = false;
|
||||
var saveOnCache = true;
|
||||
|
@ -224,6 +224,8 @@ function defineScope(cls, targetClass, name, params, methods) {
|
|||
var where = (this._scope && this._scope.where) || {};
|
||||
targetClass.destroyAll(where, cb);
|
||||
}
|
||||
|
||||
return definition;
|
||||
}
|
||||
|
||||
/*!
|
||||
|
|
|
@ -50,6 +50,45 @@ describe('basic-querying', function () {
|
|||
});
|
||||
|
||||
});
|
||||
|
||||
describe('findById', function () {
|
||||
|
||||
before(function(done) {
|
||||
var people = [
|
||||
{ id: 1, name: 'a', vip: true },
|
||||
{ id: 2, name: 'b' },
|
||||
{ id: 3, name: 'c' },
|
||||
{ id: 4, name: 'd', vip: true },
|
||||
{ id: 5, name: 'e' },
|
||||
{ id: 6, name: 'f' }
|
||||
];
|
||||
User.destroyAll(function() {
|
||||
User.create(people, done);
|
||||
});
|
||||
});
|
||||
|
||||
it('should query by ids', function (done) {
|
||||
User.findByIds([3, 2, 1], function (err, users) {
|
||||
should.exist(users);
|
||||
should.not.exist(err);
|
||||
var names = users.map(function(u) { return u.name; });
|
||||
names.should.eql(['c', 'b', 'a']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should query by ids and condition', function (done) {
|
||||
User.findByIds([4, 3, 2, 1],
|
||||
{ where: { vip: true } }, function (err, users) {
|
||||
should.exist(users);
|
||||
should.not.exist(err);
|
||||
var names = users.map(function(u) { return u.name; });
|
||||
names.should.eql(['d', 'a']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('find', function () {
|
||||
|
||||
|
|
|
@ -4,6 +4,8 @@ var should = require('./init.js');
|
|||
var db, Book, Chapter, Author, Reader;
|
||||
var Category, Product;
|
||||
var Picture, PictureLink;
|
||||
var Person, Address;
|
||||
var Link;
|
||||
|
||||
describe('relations', function () {
|
||||
|
||||
|
@ -580,7 +582,7 @@ describe('relations', function () {
|
|||
author.avatar(function (err, p) {
|
||||
should.not.exist(err);
|
||||
p.name.should.equal('Avatar');
|
||||
p.imageableId.should.equal(author.id);
|
||||
p.imageableId.should.eql(author.id);
|
||||
p.imageableType.should.equal('Author');
|
||||
done();
|
||||
});
|
||||
|
@ -592,7 +594,7 @@ describe('relations', function () {
|
|||
reader.mugshot(function (err, p) {
|
||||
should.not.exist(err);
|
||||
p.name.should.equal('Mugshot');
|
||||
p.imageableId.should.equal(reader.id);
|
||||
p.imageableId.should.eql(reader.id);
|
||||
p.imageableType.should.equal('Reader');
|
||||
done();
|
||||
});
|
||||
|
@ -654,7 +656,7 @@ describe('relations', function () {
|
|||
author.pictures.create({ name: 'Author Pic' }, function (err, p) {
|
||||
should.not.exist(err);
|
||||
should.exist(p);
|
||||
p.imageableId.should.equal(author.id);
|
||||
p.imageableId.should.eql(author.id);
|
||||
p.imageableType.should.equal('Author');
|
||||
done();
|
||||
});
|
||||
|
@ -666,7 +668,7 @@ describe('relations', function () {
|
|||
reader.pictures.create({ name: 'Reader Pic' }, function (err, p) {
|
||||
should.not.exist(err);
|
||||
should.exist(p);
|
||||
p.imageableId.should.equal(reader.id);
|
||||
p.imageableId.should.eql(reader.id);
|
||||
p.imageableType.should.equal('Reader');
|
||||
done();
|
||||
});
|
||||
|
@ -737,7 +739,7 @@ describe('relations', function () {
|
|||
Author.create({ name: 'Author 2' }, function(err, author) {
|
||||
var p = new Picture({ name: 'Sample' });
|
||||
p.imageable(author); // assign
|
||||
p.imageableId.should.equal(author.id);
|
||||
p.imageableId.should.eql(author.id);
|
||||
p.imageableType.should.equal('Author');
|
||||
p.save(done);
|
||||
});
|
||||
|
@ -1089,7 +1091,7 @@ describe('relations', function () {
|
|||
|
||||
it('should find record on scope', function (done) {
|
||||
Passport.findOne(function (err, p) {
|
||||
p.personId.should.equal(personCreated.id);
|
||||
p.personId.should.eql(personCreated.id);
|
||||
p.person(function(err, person) {
|
||||
person.name.should.equal('Fred');
|
||||
person.should.not.have.property('age');
|
||||
|
@ -1217,5 +1219,887 @@ describe('relations', function () {
|
|||
should.equal(Article.prototype.tags._targetClass, 'Tag');
|
||||
});
|
||||
});
|
||||
|
||||
describe('embedsMany', function () {
|
||||
|
||||
var address1, address2;
|
||||
|
||||
before(function (done) {
|
||||
db = getSchema();
|
||||
Person = db.define('Person', {name: String});
|
||||
Address = db.define('Address', {street: String});
|
||||
Address.validatesPresenceOf('street');
|
||||
|
||||
db.automigrate(function () {
|
||||
Person.destroyAll(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be declared', function (done) {
|
||||
Person.embedsMany(Address);
|
||||
db.automigrate(done);
|
||||
});
|
||||
|
||||
it('should have setup embedded accessor/scope', function() {
|
||||
var p = new Person({ name: 'Fred' });
|
||||
p.addresses.should.be.an.array;
|
||||
p.addresses.should.have.length(0);
|
||||
p.addressList.should.be.a.function;
|
||||
p.addressList.findById.should.be.a.function;
|
||||
p.addressList.updateById.should.be.a.function;
|
||||
p.addressList.destroy.should.be.a.function;
|
||||
p.addressList.exists.should.be.a.function;
|
||||
p.addressList.create.should.be.a.function;
|
||||
p.addressList.build.should.be.a.function;
|
||||
});
|
||||
|
||||
it('should create embedded items on scope', function(done) {
|
||||
Person.create({ name: 'Fred' }, function(err, p) {
|
||||
p.addressList.create({ street: 'Street 1' }, function(err, address) {
|
||||
should.not.exist(err);
|
||||
address1 = address;
|
||||
should.exist(address1.id);
|
||||
address1.street.should.equal('Street 1');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should create embedded items on scope', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList.create({ street: 'Street 2' }, function(err, address) {
|
||||
should.not.exist(err);
|
||||
address2 = address;
|
||||
should.exist(address2.id);
|
||||
address2.street.should.equal('Street 2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return embedded items from scope', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList(function(err, addresses) {
|
||||
should.not.exist(err);
|
||||
addresses.should.have.length(2);
|
||||
addresses[0].id.should.eql(address1.id);
|
||||
addresses[0].street.should.equal('Street 1');
|
||||
addresses[1].id.should.eql(address2.id);
|
||||
addresses[1].street.should.equal('Street 2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter embedded items on scope', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList({ where: { street: 'Street 2' } }, function(err, addresses) {
|
||||
should.not.exist(err);
|
||||
addresses.should.have.length(1);
|
||||
addresses[0].id.should.eql(address2.id);
|
||||
addresses[0].street.should.equal('Street 2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate embedded items', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList.create({}, function(err, address) {
|
||||
should.exist(err);
|
||||
should.not.exist(address);
|
||||
err.name.should.equal('ValidationError');
|
||||
err.details.codes.street.should.eql(['presence']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find embedded items by id', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList.findById(address2.id, function(err, address) {
|
||||
address.should.be.instanceof(Address);
|
||||
address.id.should.eql(address2.id);
|
||||
address.street.should.equal('Street 2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should check if item exists', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList.exists(address2.id, function(err, exists) {
|
||||
should.not.exist(err);
|
||||
exists.should.be.true;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should update embedded items by id', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList.updateById(address2.id, { street: 'New Street' }, function(err, address) {
|
||||
address.should.be.instanceof(Address);
|
||||
address.id.should.eql(address2.id);
|
||||
address.street.should.equal('New Street');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate the update of embedded items', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList.updateById(address2.id, { street: null }, function(err, address) {
|
||||
err.name.should.equal('ValidationError');
|
||||
err.details.codes.street.should.eql(['presence']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find embedded items by id - verify', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList.findById(address2.id, function(err, address) {
|
||||
address.should.be.instanceof(Address);
|
||||
address.id.should.eql(address2.id);
|
||||
address.street.should.equal('New Street');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should have accessors: at, get, set', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList.at(0).id.should.equal(address1.id);
|
||||
p.addressList.get(address1.id).id.should.equal(address1.id);
|
||||
p.addressList.set(address1.id, { street: 'Changed 1' });
|
||||
p.addresses[0].street.should.equal('Changed 1');
|
||||
p.addressList.at(1).id.should.equal(address2.id);
|
||||
p.addressList.get(address2.id).id.should.equal(address2.id);
|
||||
p.addressList.set(address2.id, { street: 'Changed 2' });
|
||||
p.addresses[1].street.should.equal('Changed 2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove embedded items by id', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addresses.should.have.length(2);
|
||||
p.addressList.destroy(address1.id, function(err) {
|
||||
should.not.exist(err);
|
||||
p.addresses.should.have.length(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should have embedded items - verify', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addresses.should.have.length(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('embedsMany - explicit ids', function () {
|
||||
before(function (done) {
|
||||
db = getSchema();
|
||||
Person = db.define('Person', {name: String});
|
||||
Address = db.define('Address', {id: { type: String, id: true }, street: String});
|
||||
Address.validatesPresenceOf('street');
|
||||
|
||||
db.automigrate(function () {
|
||||
Person.destroyAll(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be declared', function (done) {
|
||||
Person.embedsMany(Address, { options: { autoId: false, validate: true } });
|
||||
db.automigrate(done);
|
||||
});
|
||||
|
||||
it('should create embedded items on scope', function(done) {
|
||||
Person.create({ name: 'Fred' }, function(err, p) {
|
||||
p.addressList.create({ id: 'home', street: 'Street 1' }, function(err, addresses) {
|
||||
should.not.exist(err);
|
||||
p.addressList.create({ id: 'work', street: 'Work Street 2' }, function(err, address) {
|
||||
should.not.exist(err);
|
||||
address.id.should.equal('work');
|
||||
address.street.should.equal('Work Street 2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find embedded items by id', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList.findById('work', function(err, address) {
|
||||
address.should.be.instanceof(Address);
|
||||
address.id.should.equal('work');
|
||||
address.street.should.equal('Work Street 2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should check for duplicate ids', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList.create({ id: 'home', street: 'Invalid' }, function(err, addresses) {
|
||||
should.exist(err);
|
||||
err.name.should.equal('ValidationError');
|
||||
err.details.codes.addresses.should.eql(['uniqueness']);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should update embedded items by id', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addressList.updateById('home', { street: 'New Street' }, function(err, address) {
|
||||
address.should.be.instanceof(Address);
|
||||
address.id.should.equal('home');
|
||||
address.street.should.equal('New Street');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove embedded items by id', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addresses.should.have.length(2);
|
||||
p.addressList.destroy('home', function(err) {
|
||||
should.not.exist(err);
|
||||
p.addresses.should.have.length(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should have embedded items - verify', function(done) {
|
||||
Person.findOne(function(err, p) {
|
||||
p.addresses.should.have.length(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should validate all embedded items', function(done) {
|
||||
var addresses = [];
|
||||
addresses.push({ id: 'home', street: 'Home Street' });
|
||||
addresses.push({ id: 'work', street: '' });
|
||||
Person.create({ name: 'Wilma', addresses: addresses }, function(err, p) {
|
||||
err.name.should.equal('ValidationError');
|
||||
var expected = 'The `Person` instance is not valid. ';
|
||||
expected += 'Details: `addresses` contains invalid item: `work` (street can\'t be blank).';
|
||||
err.message.should.equal(expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should build embedded items', function(done) {
|
||||
Person.create({ name: 'Wilma' }, function(err, p) {
|
||||
p.addressList.build({ id: 'home', street: 'Home' });
|
||||
p.addressList.build({ id: 'work', street: 'Work' });
|
||||
p.addresses.should.have.length(2);
|
||||
p.save(function(err, p) {
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should have embedded items - verify', function(done) {
|
||||
Person.findOne({ where: { name: 'Wilma' } }, function(err, p) {
|
||||
p.name.should.equal('Wilma');
|
||||
p.addresses.should.have.length(2);
|
||||
p.addresses[0].id.should.equal('home');
|
||||
p.addresses[0].street.should.equal('Home');
|
||||
p.addresses[1].id.should.equal('work');
|
||||
p.addresses[1].street.should.equal('Work');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should have accessors: at, get, set', function(done) {
|
||||
Person.findOne({ where: { name: 'Wilma' } }, function(err, p) {
|
||||
p.name.should.equal('Wilma');
|
||||
p.addresses.should.have.length(2);
|
||||
p.addressList.at(0).id.should.equal('home');
|
||||
p.addressList.get('home').id.should.equal('home');
|
||||
p.addressList.set('home', { id: 'den' }).id.should.equal('den');
|
||||
p.addressList.at(1).id.should.equal('work');
|
||||
p.addressList.get('work').id.should.equal('work');
|
||||
p.addressList.set('work', { id: 'factory' }).id.should.equal('factory');
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('embedsMany - relations, scope and properties', function () {
|
||||
|
||||
var product1, product2, product3;
|
||||
|
||||
before(function (done) {
|
||||
db = getSchema();
|
||||
Category = db.define('Category', {name: String});
|
||||
Product = db.define('Product', {name: String});
|
||||
Link = db.define('Link', {name: String});
|
||||
|
||||
db.automigrate(function () {
|
||||
Person.destroyAll(done);
|
||||
});
|
||||
});
|
||||
|
||||
it('can be declared', function (done) {
|
||||
Category.embedsMany(Link, {
|
||||
as: 'items', // rename
|
||||
scope: { include: 'product' }, // always include
|
||||
options: { belongsTo: 'product' } // optional, for add()/remove()
|
||||
});
|
||||
Link.belongsTo(Product, {
|
||||
foreignKey: 'id', // re-use the actual product id
|
||||
properties: { id: 'id', name: 'name' }, // denormalize, transfer id
|
||||
});
|
||||
db.automigrate(function() {
|
||||
Product.create({ name: 'Product 0' }, done); // offset ids for tests
|
||||
});
|
||||
});
|
||||
|
||||
it('should setup related items', function(done) {
|
||||
Product.create({ name: 'Product 1' }, function(err, p) {
|
||||
product1 = p;
|
||||
Product.create({ name: 'Product 2' }, function(err, p) {
|
||||
product2 = p;
|
||||
Product.create({ name: 'Product 3' }, function(err, p) {
|
||||
product3 = p;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should create items on scope', function(done) {
|
||||
Category.create({ name: 'Category A' }, function(err, cat) {
|
||||
var link = cat.items.build();
|
||||
link.product(product1);
|
||||
var link = cat.items.build();
|
||||
link.product(product2);
|
||||
cat.save(function(err, cat) {
|
||||
var product = cat.items.at(0);
|
||||
product.should.be.instanceof(Link);
|
||||
product.should.not.have.property('productId');
|
||||
product.id.should.eql(product1.id);
|
||||
product.name.should.equal(product1.name);
|
||||
var product = cat.items.at(1);
|
||||
product.id.should.eql(product2.id);
|
||||
product.name.should.equal(product2.name);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should include related items on scope', function(done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.links.should.have.length(2);
|
||||
|
||||
// denormalized properties:
|
||||
cat.items.at(0).should.be.instanceof(Link);
|
||||
cat.items.at(0).id.should.eql(product1.id);
|
||||
cat.items.at(0).name.should.equal(product1.name);
|
||||
cat.items.at(1).id.should.eql(product2.id);
|
||||
cat.items.at(1).name.should.equal(product2.name);
|
||||
|
||||
// lazy-loaded relations
|
||||
should.not.exist(cat.items.at(0).product());
|
||||
should.not.exist(cat.items.at(1).product());
|
||||
|
||||
cat.items(function(err, items) {
|
||||
cat.items.at(0).product().should.be.instanceof(Product);
|
||||
cat.items.at(1).product().should.be.instanceof(Product);
|
||||
cat.items.at(1).product().name.should.equal('Product 2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove embedded items by id', function(done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.links.should.have.length(2);
|
||||
cat.items.destroy(product1.id, function(err) {
|
||||
should.not.exist(err);
|
||||
cat.links.should.have.length(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find items on scope', function(done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.links.should.have.length(1);
|
||||
cat.items.at(0).id.should.eql(product2.id);
|
||||
cat.items.at(0).name.should.equal(product2.name);
|
||||
|
||||
// lazy-loaded relations
|
||||
should.not.exist(cat.items.at(0).product());
|
||||
|
||||
cat.items(function(err, items) {
|
||||
cat.items.at(0).product().should.be.instanceof(Product);
|
||||
cat.items.at(0).product().name.should.equal('Product 2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should add related items to scope', function(done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.links.should.have.length(1);
|
||||
cat.items.add(product3, function(err, link) {
|
||||
link.should.be.instanceof(Link);
|
||||
link.id.should.eql(product3.id);
|
||||
link.name.should.equal('Product 3');
|
||||
|
||||
cat.links.should.have.length(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find items on scope', function(done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.links.should.have.length(2);
|
||||
|
||||
cat.items.at(0).should.be.instanceof(Link);
|
||||
cat.items.at(0).id.should.eql(product2.id);
|
||||
cat.items.at(0).name.should.equal(product2.name);
|
||||
cat.items.at(1).id.should.eql(product3.id);
|
||||
cat.items.at(1).name.should.equal(product3.name);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove embedded items by reference id', function(done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.links.should.have.length(2);
|
||||
cat.items.remove(product2.id, function(err) {
|
||||
should.not.exist(err);
|
||||
cat.links.should.have.length(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove embedded items by reference id', function(done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.links.should.have.length(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('embedsMany - polymorphic relations', function () {
|
||||
|
||||
var person1, person2;
|
||||
|
||||
before(function (done) {
|
||||
db = getSchema();
|
||||
Book = db.define('Book', {name: String});
|
||||
Author = db.define('Author', {name: String});
|
||||
Reader = db.define('Reader', {name: String});
|
||||
|
||||
Link = db.define('Link'); // generic model
|
||||
Link.validatesPresenceOf('linkedId');
|
||||
Link.validatesPresenceOf('linkedType');
|
||||
|
||||
db.automigrate(function () {
|
||||
Book.destroyAll(function() {
|
||||
Author.destroyAll(function() {
|
||||
Reader.destroyAll(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can be declared', function (done) {
|
||||
Book.embedsMany(Link, { as: 'people',
|
||||
polymorphic: 'linked',
|
||||
scope: { include: 'linked' }
|
||||
});
|
||||
Link.belongsTo('linked', {
|
||||
polymorphic: true, // needs unique auto-id
|
||||
properties: { name: 'name' } // denormalized
|
||||
});
|
||||
db.automigrate(done);
|
||||
});
|
||||
|
||||
it('should setup related items', function(done) {
|
||||
Author.create({ name: 'Author 1' }, function(err, p) {
|
||||
person1 = p;
|
||||
Reader.create({ name: 'Reader 1' }, function(err, p) {
|
||||
person2 = p;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should create items on scope', function(done) {
|
||||
Book.create({ name: 'Book' }, function(err, book) {
|
||||
var link = book.people.build({ notes: 'Something ...' });
|
||||
link.linked(person1);
|
||||
var link = book.people.build();
|
||||
link.linked(person2);
|
||||
book.save(function(err, book) {
|
||||
should.not.exist(err);
|
||||
|
||||
var link = book.people.at(0);
|
||||
link.should.be.instanceof(Link);
|
||||
link.id.should.equal(1);
|
||||
link.linkedId.should.eql(person1.id);
|
||||
link.linkedType.should.equal('Author');
|
||||
link.name.should.equal('Author 1');
|
||||
|
||||
var link = book.people.at(1);
|
||||
link.should.be.instanceof(Link);
|
||||
link.id.should.equal(2);
|
||||
link.linkedId.should.eql(person2.id);
|
||||
link.linkedType.should.equal('Reader');
|
||||
link.name.should.equal('Reader 1');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should include related items on scope', function(done) {
|
||||
Book.findOne(function(err, book) {
|
||||
book.links.should.have.length(2);
|
||||
|
||||
var link = book.people.at(0);
|
||||
link.should.be.instanceof(Link);
|
||||
link.id.should.equal(1);
|
||||
link.linkedId.should.eql(person1.id);
|
||||
link.linkedType.should.equal('Author');
|
||||
link.notes.should.equal('Something ...');
|
||||
|
||||
var link = book.people.at(1);
|
||||
link.should.be.instanceof(Link);
|
||||
link.id.should.equal(2);
|
||||
link.linkedId.should.eql(person2.id);
|
||||
link.linkedType.should.equal('Reader');
|
||||
|
||||
// lazy-loaded relations
|
||||
should.not.exist(book.people.at(0).linked());
|
||||
should.not.exist(book.people.at(1).linked());
|
||||
|
||||
book.people(function(err, people) {
|
||||
people[0].linked().should.be.instanceof(Author);
|
||||
people[0].linked().name.should.equal('Author 1');
|
||||
people[1].linked().should.be.instanceof(Reader);
|
||||
people[1].linked().name.should.equal('Reader 1');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should include nested related items on scope', function(done) {
|
||||
|
||||
// There's some date duplication going on, so it might
|
||||
// make sense to override toObject on a case-by-case basis
|
||||
// to sort this out (delete links, keep people).
|
||||
// In loopback, an afterRemote filter could do this as well.
|
||||
|
||||
Book.find({ include: 'people' }, function(err, books) {
|
||||
var obj = books[0].toObject();
|
||||
|
||||
obj.should.have.property('links');
|
||||
obj.should.have.property('people');
|
||||
|
||||
obj.links.should.have.length(2);
|
||||
obj.links[0].name.should.equal('Author 1');
|
||||
obj.links[1].name.should.equal('Reader 1');
|
||||
|
||||
obj.people.should.have.length(2);
|
||||
|
||||
obj.people[0].name.should.equal('Author 1');
|
||||
obj.people[0].notes.should.equal('Something ...');
|
||||
|
||||
obj.people[0].linked.name.should.equal('Author 1');
|
||||
obj.people[1].linked.name.should.equal('Reader 1');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('referencesMany', function () {
|
||||
|
||||
var product1, product2, product3;
|
||||
|
||||
before(function (done) {
|
||||
db = getSchema();
|
||||
Category = db.define('Category', {name: String});
|
||||
Product = db.define('Product', {name: String});
|
||||
|
||||
db.automigrate(function () {
|
||||
Category.destroyAll(function() {
|
||||
Product.destroyAll(done);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('can be declared', function (done) {
|
||||
var reverse = function(cb) {
|
||||
var modelInstance = this.modelInstance;
|
||||
var fk = this.definition.keyFrom;
|
||||
var ids = modelInstance[fk] || [];
|
||||
modelInstance.updateAttribute(fk, ids.reverse(), function(err, inst) {
|
||||
cb(err, inst[fk] || []);
|
||||
});
|
||||
};
|
||||
|
||||
reverse.shared = true; // remoting
|
||||
reverse.http = { verb: 'put', path: '/products/reverse' };
|
||||
|
||||
Category.referencesMany(Product, { scopeMethods: {
|
||||
reverse: reverse
|
||||
} });
|
||||
|
||||
Category.prototype['__reverse__products'].should.be.a.function;
|
||||
should.exist(Category.prototype['__reverse__products'].shared);
|
||||
Category.prototype['__reverse__products'].http.should.eql(reverse.http);
|
||||
|
||||
db.automigrate(done);
|
||||
});
|
||||
|
||||
it('should setup test records', function (done) {
|
||||
Product.create({ name: 'Product 1' }, function(err, p) {
|
||||
product1 = p;
|
||||
Product.create({ name: 'Product 3' }, function(err, p) {
|
||||
product3 = p;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should create record on scope', function (done) {
|
||||
Category.create({ name: 'Category A' }, function(err, cat) {
|
||||
cat.productIds.should.be.an.array;
|
||||
cat.productIds.should.have.length(0);
|
||||
cat.products.create({ name: 'Product 2' }, function(err, p) {
|
||||
should.not.exist(err);
|
||||
cat.productIds.should.have.length(1);
|
||||
cat.productIds.should.eql([p.id]);
|
||||
p.name.should.equal('Product 2');
|
||||
product2 = p;
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow duplicate record on scope', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.productIds = [product2.id, product2.id];
|
||||
cat.save(function(err, p) {
|
||||
should.exist(err);
|
||||
err.name.should.equal('ValidationError');
|
||||
err.details.codes.products.should.eql(['uniqueness']);
|
||||
var expected = 'The `Category` instance is not valid. ';
|
||||
expected += 'Details: `products` Contains duplicate `Product` instance.';
|
||||
err.message.should.equal(expected);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find items on scope', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.productIds.should.eql([product2.id]);
|
||||
cat.products(function(err, products) {
|
||||
should.not.exist(err);
|
||||
var p = products[0];
|
||||
p.id.should.eql(product2.id);
|
||||
p.name.should.equal('Product 2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find items on scope - findById', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.productIds.should.eql([product2.id]);
|
||||
cat.products.findById(product2.id, function(err, p) {
|
||||
should.not.exist(err);
|
||||
p.should.be.instanceof(Product);
|
||||
p.id.should.eql(product2.id);
|
||||
p.name.should.equal('Product 2');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should check if a record exists on scope', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.products.exists(product2.id, function(err, exists) {
|
||||
should.not.exist(err);
|
||||
should.exist(exists);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should update a record on scope', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
var attrs = { name: 'Product 2 - edit' };
|
||||
cat.products.updateById(product2.id, attrs, function(err, p) {
|
||||
should.not.exist(err);
|
||||
p.name.should.equal(attrs.name);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should get a record by index - at', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.products.at(0, function(err, p) {
|
||||
should.not.exist(err);
|
||||
p.should.be.instanceof(Product);
|
||||
p.id.should.eql(product2.id);
|
||||
p.name.should.equal('Product 2 - edit');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a record to scope - object', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.products.add(product1, function(err, prod) {
|
||||
should.not.exist(err);
|
||||
cat.productIds.should.eql([product2.id, product1.id]);
|
||||
prod.id.should.eql(product1.id);
|
||||
prod.should.have.property('name');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should add a record to scope - object', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.products.add(product3.id, function(err, prod) {
|
||||
should.not.exist(err);
|
||||
var expected = [product2.id, product1.id, product3.id];
|
||||
cat.productIds.should.eql(expected);
|
||||
prod.id.should.eql(product3.id);
|
||||
prod.should.have.property('name');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find items on scope - findById', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.products.findById(product3.id, function(err, p) {
|
||||
should.not.exist(err);
|
||||
p.id.should.eql(product3.id);
|
||||
p.name.should.equal('Product 3');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find items on scope - filter', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
var filter = { where: { name: 'Product 1' } };
|
||||
cat.products(filter, function(err, products) {
|
||||
should.not.exist(err);
|
||||
products.should.have.length(1);
|
||||
var p = products[0];
|
||||
p.id.should.eql(product1.id);
|
||||
p.name.should.equal('Product 1');
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove items from scope', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.products.remove(product1.id, function(err, ids) {
|
||||
should.not.exist(err);
|
||||
var expected = [product2.id, product3.id];
|
||||
cat.productIds.should.eql(expected);
|
||||
ids.should.eql(cat.productIds);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find items on scope - verify', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
var expected = [product2.id, product3.id];
|
||||
cat.productIds.should.eql(expected);
|
||||
cat.products(function(err, products) {
|
||||
should.not.exist(err);
|
||||
products.should.have.length(2);
|
||||
products[0].id.should.eql(product2.id);
|
||||
products[1].id.should.eql(product3.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow custom scope methods - reverse', function(done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.products.reverse(function(err, ids) {
|
||||
var expected = [product3.id, product2.id];
|
||||
ids.should.eql(expected);
|
||||
cat.productIds.should.eql(expected);
|
||||
done();
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
it('should include related items from scope', function(done) {
|
||||
Category.find({ include: 'products' }, function(err, categories) {
|
||||
categories.should.have.length(1);
|
||||
var cat = categories[0].toObject();
|
||||
cat.name.should.equal('Category A');
|
||||
cat.products.should.have.length(2);
|
||||
cat.products[0].id.should.eql(product3.id);
|
||||
cat.products[1].id.should.eql(product2.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should destroy items from scope - destroyById', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
cat.products.destroy(product2.id, function(err) {
|
||||
should.not.exist(err);
|
||||
var expected = [product3.id];
|
||||
cat.productIds.should.eql(expected);
|
||||
Product.exists(product2.id, function(err, exists) {
|
||||
should.not.exist(err);
|
||||
should.exist(exists);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should find items on scope - verify', function (done) {
|
||||
Category.findOne(function(err, cat) {
|
||||
var expected = [product3.id];
|
||||
cat.productIds.should.eql(expected);
|
||||
cat.products(function(err, products) {
|
||||
should.not.exist(err);
|
||||
products.should.have.length(1);
|
||||
products[0].id.should.eql(product3.id);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue