Implemented referencesMany

This commit is contained in:
Fabien Franzen 2014-07-29 21:46:12 +02:00
parent 60fd39d311
commit 1782b439f1
2 changed files with 542 additions and 9 deletions

View File

@ -322,15 +322,15 @@ util.inherits(EmbedsMany, Relation);
* ReferencesMany subclass
* @param {RelationDefinition|Object} definition
* @param {Object} modelInstance
* @returns {HasMany}
* @returns {ReferencesMany}
* @constructor
* @class HasMany
* @class ReferencesMany
*/
function ReferencesMany(definition, modelInstance) {
if (!(this instanceof HasMany)) {
return new HasMany(definition, modelInstance);
if (!(this instanceof ReferencesMany)) {
return new ReferencesMany(definition, modelInstance);
}
assert(definition.type === RelationTypes.hasMany);
assert(definition.type === RelationTypes.referencesMany);
Relation.apply(this, arguments);
}
@ -550,6 +550,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;
@ -579,7 +580,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;
@ -1781,7 +1782,12 @@ EmbedsMany.prototype.build = function(targetModelData) {
* Add the target model instance to the 'embedsMany' relation
* @param {Object|ID} acInst The actual instance or id value
*/
EmbedsMany.prototype.add = function (acInst, cb) {
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;
@ -1807,7 +1813,7 @@ EmbedsMany.prototype.add = function (acInst, cb) {
referenceDef.modelTo.findOne(filter, function(err, ref) {
if (ref instanceof referenceDef.modelTo) {
var inst = self.build();
var inst = self.build(data || {});
inst[options.reference](ref);
modelInstance.save(function(err) {
cb(err, err ? null : inst);
@ -1860,6 +1866,318 @@ EmbedsMany.prototype.remove = function (acInst, cb) {
};
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.getPropertyType(idName);
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;
scopeMethods.create = scopeMethod(definition, 'create');
scopeMethods.build = scopeMethod(definition, 'build');
// Mix the property and scoped methods into the prototype class
var scopeDefinition = defineScope(modelFrom.prototype, modelTo, relationName, function () {
return {};
}, scopeMethods);
scopeDefinition.related = scopeMethod(definition, '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 insertId = function(id, done) {
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, inst) {
done(err, inst[fk] || []);
});
};
if (acInst instanceof modelTo) {
insertId(acInst[pk], 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, modelInstance[fk]);
insertId(inst[pk], 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); });
}
};

View File

@ -1843,6 +1843,8 @@ describe('relations', function () {
describe('referencesMany', function () {
var product1, product2, product3;
before(function (done) {
db = getSchema();
Category = db.define('Category', {name: String});
@ -1860,6 +1862,219 @@ describe('relations', function () {
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 create 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, ids) {
should.not.exist(err);
cat.productIds.should.eql([product2.id, product1.id]);
ids.should.eql(cat.productIds);
done();
});
});
});
it('should add a record to scope - object', function (done) {
Category.findOne(function(err, cat) {
cat.products.add(product3.id, function(err, ids) {
should.not.exist(err);
var expected = [product2.id, product1.id, product3.id];
cat.productIds.should.eql(expected);
ids.should.eql(cat.productIds);
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 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(product2.id);
cat.products[1].id.should.eql(product3.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();
});
});
});
});
});