Merge pull request #295 from fabien/fix/embeds-many
Various improvements to embedded relations
This commit is contained in:
commit
3655107334
|
@ -1783,29 +1783,53 @@ EmbedsOne.prototype.create = function (targetModelData, cb) {
|
||||||
|
|
||||||
var inst = this.build(targetModelData);
|
var inst = this.build(targetModelData);
|
||||||
|
|
||||||
var err = inst.isValid() ? null : new ValidationError(inst);
|
var updateEmbedded = function() {
|
||||||
|
modelInstance.updateAttribute(propertyName,
|
||||||
if (err) {
|
inst, function(err) {
|
||||||
return process.nextTick(function() {
|
cb(err, err ? null : inst);
|
||||||
cb(err);
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
modelInstance.updateAttribute(propertyName,
|
if (this.definition.options.persistent) {
|
||||||
inst, function(err) {
|
inst.save(function(err) { // will validate
|
||||||
cb(err, err ? null : inst);
|
if (err) return cb(err, inst);
|
||||||
});
|
updateEmbedded();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var err = inst.isValid() ? null : new ValidationError(inst);
|
||||||
|
if (err) {
|
||||||
|
process.nextTick(function() {
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateEmbedded();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
EmbedsOne.prototype.build = function (targetModelData) {
|
EmbedsOne.prototype.build = function (targetModelData) {
|
||||||
var modelTo = this.definition.modelTo;
|
var modelTo = this.definition.modelTo;
|
||||||
var modelInstance = this.modelInstance;
|
var modelInstance = this.modelInstance;
|
||||||
var propertyName = this.definition.keyFrom;
|
var propertyName = this.definition.keyFrom;
|
||||||
|
var forceId = this.definition.options.forceId;
|
||||||
|
var persistent = this.definition.options.persistent;
|
||||||
|
var connector = modelTo.dataSource.connector;
|
||||||
|
|
||||||
targetModelData = targetModelData || {};
|
targetModelData = targetModelData || {};
|
||||||
|
|
||||||
this.definition.applyProperties(modelInstance, targetModelData);
|
this.definition.applyProperties(modelInstance, targetModelData);
|
||||||
|
|
||||||
|
var pk = this.definition.keyTo;
|
||||||
|
var pkProp = modelTo.definition.properties[pk];
|
||||||
|
|
||||||
|
var assignId = (forceId || targetModelData[pk] === undefined);
|
||||||
|
assignId = assignId && !persistent && (pkProp && pkProp.generated);
|
||||||
|
|
||||||
|
if (assignId && typeof connector.generateId === 'function') {
|
||||||
|
var id = connector.generateId(modelTo.modelName, targetModelData, pk);
|
||||||
|
targetModelData[pk] = id;
|
||||||
|
}
|
||||||
|
|
||||||
var embeddedInstance = new modelTo(targetModelData);
|
var embeddedInstance = new modelTo(targetModelData);
|
||||||
modelInstance[propertyName] = embeddedInstance;
|
modelInstance[propertyName] = embeddedInstance;
|
||||||
|
|
||||||
|
@ -1878,7 +1902,20 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (typeof modelTo.dataSource.connector.generateId !== 'function') {
|
if (typeof modelTo.dataSource.connector.generateId !== 'function') {
|
||||||
modelTo.validatesPresenceOf(idName); // unique id is required
|
modelFrom.validate(propertyName, function(err) {
|
||||||
|
var self = this;
|
||||||
|
var embeddedList = this[propertyName] || [];
|
||||||
|
var hasErrors = false;
|
||||||
|
embeddedList.forEach(function(item, idx) {
|
||||||
|
if (item instanceof modelTo && item[idName] == undefined) {
|
||||||
|
hasErrors = true;
|
||||||
|
var msg = 'contains invalid item at index `' + idx + '`:';
|
||||||
|
msg += ' `' + idName + '` is blank';
|
||||||
|
self.errors.add(propertyName, msg, 'invalid');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (hasErrors) err(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params.polymorphic) {
|
if (!params.polymorphic) {
|
||||||
|
@ -1901,13 +1938,17 @@ RelationDefinition.embedsMany = function embedsMany(modelFrom, modelTo, params)
|
||||||
var self = this;
|
var self = this;
|
||||||
var embeddedList = this[propertyName] || [];
|
var embeddedList = this[propertyName] || [];
|
||||||
var hasErrors = false;
|
var hasErrors = false;
|
||||||
embeddedList.forEach(function(item) {
|
embeddedList.forEach(function(item, idx) {
|
||||||
if (item instanceof modelTo) {
|
if (item instanceof modelTo) {
|
||||||
if (!item.isValid()) {
|
if (!item.isValid()) {
|
||||||
hasErrors = true;
|
hasErrors = true;
|
||||||
var id = item[idName] || '(blank)';
|
var id = item[idName];
|
||||||
var first = Object.keys(item.errors)[0];
|
var first = Object.keys(item.errors)[0];
|
||||||
var msg = 'contains invalid item: `' + id + '`';
|
if (id) {
|
||||||
|
var msg = 'contains invalid item: `' + id + '`';
|
||||||
|
} else {
|
||||||
|
var msg = 'contains invalid item at index `' + idx + '`';
|
||||||
|
}
|
||||||
msg += ' (`' + first + '` ' + item.errors[first] + ')';
|
msg += ' (`' + first + '` ' + item.errors[first] + ')';
|
||||||
self.errors.add(propertyName, msg, 'invalid');
|
self.errors.add(propertyName, msg, 'invalid');
|
||||||
}
|
}
|
||||||
|
@ -2193,28 +2234,39 @@ EmbedsMany.prototype.create = function (targetModelData, cb) {
|
||||||
|
|
||||||
var inst = this.build(targetModelData);
|
var inst = this.build(targetModelData);
|
||||||
|
|
||||||
var err = inst.isValid() ? null : new ValidationError(inst);
|
var updateEmbedded = function() {
|
||||||
|
modelInstance.updateAttribute(propertyName,
|
||||||
if (err) {
|
embeddedList, function(err, modelInst) {
|
||||||
return process.nextTick(function() {
|
cb(err, err ? null : inst);
|
||||||
cb(err);
|
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
modelInstance.updateAttribute(propertyName,
|
if (this.definition.options.persistent) {
|
||||||
embeddedList, function(err, modelInst) {
|
inst.save(function(err) { // will validate
|
||||||
cb(err, err ? null : inst);
|
if (err) return cb(err, inst);
|
||||||
});
|
updateEmbedded();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var err = inst.isValid() ? null : new ValidationError(inst);
|
||||||
|
if (err) {
|
||||||
|
process.nextTick(function() {
|
||||||
|
cb(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
updateEmbedded();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
EmbedsMany.prototype.build = function(targetModelData) {
|
EmbedsMany.prototype.build = function(targetModelData) {
|
||||||
var modelTo = this.definition.modelTo;
|
var modelTo = this.definition.modelTo;
|
||||||
var modelInstance = this.modelInstance;
|
var modelInstance = this.modelInstance;
|
||||||
var forceId = this.definition.options.forceId;
|
var forceId = this.definition.options.forceId;
|
||||||
|
var persistent = this.definition.options.persistent;
|
||||||
var connector = modelTo.dataSource.connector;
|
var connector = modelTo.dataSource.connector;
|
||||||
|
|
||||||
var pk = this.definition.keyTo;
|
var pk = this.definition.keyTo;
|
||||||
var pkProp = modelTo.definition.properties[pk]
|
var pkProp = modelTo.definition.properties[pk];
|
||||||
var pkType = pkProp && pkProp.type;
|
var pkType = pkProp && pkProp.type;
|
||||||
|
|
||||||
var embeddedList = this.embeddedList();
|
var embeddedList = this.embeddedList();
|
||||||
|
@ -2222,6 +2274,7 @@ EmbedsMany.prototype.build = function(targetModelData) {
|
||||||
targetModelData = targetModelData || {};
|
targetModelData = targetModelData || {};
|
||||||
|
|
||||||
var assignId = (forceId || targetModelData[pk] === undefined);
|
var assignId = (forceId || targetModelData[pk] === undefined);
|
||||||
|
assignId = assignId && !persistent;
|
||||||
|
|
||||||
if (assignId && pkType === Number) {
|
if (assignId && pkType === Number) {
|
||||||
var ids = embeddedList.map(function(m) {
|
var ids = embeddedList.map(function(m) {
|
||||||
|
|
|
@ -13,6 +13,10 @@ var getTransientDataSource = function(settings) {
|
||||||
return new DataSource('transient', settings, db.modelBuilder);
|
return new DataSource('transient', settings, db.modelBuilder);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var getMemoryDataSource = function(settings) {
|
||||||
|
return new DataSource('memory', settings, db.modelBuilder);
|
||||||
|
};
|
||||||
|
|
||||||
describe('relations', function () {
|
describe('relations', function () {
|
||||||
|
|
||||||
describe('hasMany', function () {
|
describe('hasMany', function () {
|
||||||
|
@ -1849,6 +1853,81 @@ describe('relations', function () {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('embedsOne - persisted model', function () {
|
||||||
|
|
||||||
|
// This test spefically uses the Memory connector
|
||||||
|
// in order to test the use of the auto-generated
|
||||||
|
// id, in the sequence of the related model.
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
db = getMemoryDataSource();
|
||||||
|
Person = db.define('Person', {name: String});
|
||||||
|
Passport = db.define('Passport',
|
||||||
|
{name:{type:'string', required: true}}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be declared using embedsOne method', function (done) {
|
||||||
|
Person.embedsOne(Passport, {
|
||||||
|
options: {persistent: true}
|
||||||
|
});
|
||||||
|
db.automigrate(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an item - to offset id', function(done) {
|
||||||
|
Passport.create({name:'Wilma'}, function(err, p) {
|
||||||
|
should.not.exist(err);
|
||||||
|
p.id.should.equal(1);
|
||||||
|
p.name.should.equal('Wilma');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an embedded item on scope', function(done) {
|
||||||
|
Person.create({name: 'Fred'}, function(err, p) {
|
||||||
|
should.not.exist(err);
|
||||||
|
p.passportItem.create({name: 'Fredric'}, function(err, passport) {
|
||||||
|
should.not.exist(err);
|
||||||
|
p.passport.id.should.eql(2);
|
||||||
|
p.passport.name.should.equal('Fredric');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('embedsOne - generated id', function () {
|
||||||
|
|
||||||
|
before(function () {
|
||||||
|
tmp = getTransientDataSource();
|
||||||
|
db = getSchema();
|
||||||
|
Person = db.define('Person', {name: String});
|
||||||
|
Passport = tmp.define('Passport',
|
||||||
|
{id: {type:'string', id: true, generated:true}},
|
||||||
|
{name: {type:'string', required: true}}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be declared using embedsOne method', function (done) {
|
||||||
|
Person.embedsOne(Passport);
|
||||||
|
db.automigrate(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an embedded item on scope', function(done) {
|
||||||
|
Person.create({name: 'Fred'}, function(err, p) {
|
||||||
|
should.not.exist(err);
|
||||||
|
p.passportItem.create({name: 'Fredric'}, function(err, passport) {
|
||||||
|
should.not.exist(err);
|
||||||
|
passport.id.should.match(/^[0-9a-fA-F]{24}$/);
|
||||||
|
p.passport.name.should.equal('Fredric');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe('embedsMany', function () {
|
describe('embedsMany', function () {
|
||||||
|
|
||||||
var address1, address2;
|
var address1, address2;
|
||||||
|
@ -2039,6 +2118,45 @@ describe('relations', function () {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('embedsMany - numeric ids + forceId', function () {
|
||||||
|
|
||||||
|
before(function (done) {
|
||||||
|
tmp = getTransientDataSource();
|
||||||
|
db = getSchema();
|
||||||
|
Person = db.define('Person', {name: String});
|
||||||
|
Address = tmp.define('Address', {
|
||||||
|
id: {type: Number, id:true},
|
||||||
|
street: String
|
||||||
|
});
|
||||||
|
|
||||||
|
db.automigrate(function () {
|
||||||
|
Person.destroyAll(done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be declared', function (done) {
|
||||||
|
Person.embedsMany(Address, {options: {forceId: true}});
|
||||||
|
db.automigrate(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
address.id.should.equal(1);
|
||||||
|
p.addressList.create({ street: 'Street 2' }, function(err, address) {
|
||||||
|
address.id.should.equal(2);
|
||||||
|
p.addressList.create({ id: 12345, street: 'Street 3' }, function(err, address) {
|
||||||
|
address.id.should.equal(3);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe('embedsMany - explicit ids', function () {
|
describe('embedsMany - explicit ids', function () {
|
||||||
before(function (done) {
|
before(function (done) {
|
||||||
tmp = getTransientDataSource();
|
tmp = getTransientDataSource();
|
||||||
|
@ -2210,6 +2328,134 @@ describe('relations', function () {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('embedsMany - persisted model', function () {
|
||||||
|
|
||||||
|
var address0, address1, address2;
|
||||||
|
var person;
|
||||||
|
|
||||||
|
// This test spefically uses the Memory connector
|
||||||
|
// in order to test the use of the auto-generated
|
||||||
|
// id, in the sequence of the related model.
|
||||||
|
|
||||||
|
before(function (done) {
|
||||||
|
db = getMemoryDataSource();
|
||||||
|
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) {
|
||||||
|
// to save related model itself, set
|
||||||
|
// persistent: true
|
||||||
|
Person.embedsMany(Address, {
|
||||||
|
scope: {order: 'street'},
|
||||||
|
options: {persistent: true}
|
||||||
|
});
|
||||||
|
db.automigrate(done);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create individual items (0)', function(done) {
|
||||||
|
Address.create({ street: 'Street 0' }, function(err, inst) {
|
||||||
|
inst.id.should.equal(1); // offset sequence
|
||||||
|
address0 = inst;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create individual items (1)', function(done) {
|
||||||
|
Address.create({ street: 'Street 1' }, function(err, inst) {
|
||||||
|
inst.id.should.equal(2);
|
||||||
|
address1 = inst;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create individual items (2)', function(done) {
|
||||||
|
Address.create({ street: 'Street 2' }, function(err, inst) {
|
||||||
|
inst.id.should.equal(3);
|
||||||
|
address2 = inst;
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create individual items (3)', function(done) {
|
||||||
|
Address.create({ street: 'Street 3' }, function(err, inst) {
|
||||||
|
inst.id.should.equal(4); // offset sequence
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add embedded items on scope', function(done) {
|
||||||
|
Person.create({ name: 'Fred' }, function(err, p) {
|
||||||
|
person = p;
|
||||||
|
p.addressList.create(address1.toObject(), function(err, address) {
|
||||||
|
should.not.exist(err);
|
||||||
|
address.id.should.eql(2);
|
||||||
|
address.street.should.equal('Street 1');
|
||||||
|
p.addressList.create(address2.toObject(), function(err, address) {
|
||||||
|
should.not.exist(err);
|
||||||
|
address.id.should.eql(3);
|
||||||
|
address.street.should.equal('Street 2');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create embedded items on scope', function(done) {
|
||||||
|
Person.findById(person.id, function(err, p) {
|
||||||
|
p.addressList.create({ street: 'Street 4' }, function(err, address) {
|
||||||
|
should.not.exist(err);
|
||||||
|
address.id.should.equal(5); // in Address sequence, correct offset
|
||||||
|
address.street.should.equal('Street 4');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have embedded items on scope', function(done) {
|
||||||
|
Person.findById(person.id, function(err, p) {
|
||||||
|
p.addressList(function(err, addresses) {
|
||||||
|
should.not.exist(err);
|
||||||
|
addresses.should.have.length(3);
|
||||||
|
addresses[0].street.should.equal('Street 1');
|
||||||
|
addresses[1].street.should.equal('Street 2');
|
||||||
|
addresses[2].street.should.equal('Street 4');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate embedded items on scope - id', function(done) {
|
||||||
|
Person.create({ name: 'Wilma' }, function(err, p) {
|
||||||
|
p.addressList.create({ id: null, street: 'Street 1' }, function(err, address) {
|
||||||
|
should.not.exist(err);
|
||||||
|
address.street.should.equal('Street 1');
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate embedded items on scope - street', function(done) {
|
||||||
|
Person.create({ name: 'Wilma' }, function(err, p) {
|
||||||
|
p.addressList.create({ id: 1234 }, function(err, address) {
|
||||||
|
should.exist(err);
|
||||||
|
err.name.should.equal('ValidationError');
|
||||||
|
err.details.codes.street.should.eql(['presence']);
|
||||||
|
var expected = 'The `Address` instance is not valid. ';
|
||||||
|
expected += 'Details: `street` can\'t be blank.';
|
||||||
|
err.message.should.equal(expected);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
describe('embedsMany - relations, scope and properties', function () {
|
describe('embedsMany - relations, scope and properties', function () {
|
||||||
|
|
||||||
var category, job1, job2, job3;
|
var category, job1, job2, job3;
|
||||||
|
|
Loading…
Reference in New Issue