Implemented referencesMany
This commit is contained in:
parent
60fd39d311
commit
1782b439f1
|
@ -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); });
|
||||
}
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue