Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 78 additions & 25 deletions lib/relation-definition.js
Original file line number Diff line number Diff line change
Expand Up @@ -2581,6 +2581,7 @@ EmbedsMany.prototype.updateById = function(fkId, data, options, cb) {
cb = data;
data = {};
}
options = options || {};

var modelTo = this.definition.modelTo;
var propertyName = this.definition.keyFrom;
Expand All @@ -2591,22 +2592,48 @@ EmbedsMany.prototype.updateById = function(fkId, data, options, cb) {
var inst = this.findById(fkId);

if (inst instanceof modelTo) {
if (typeof data === 'object') {
var hookState = {};
var context = {
Model: modelTo,
currentInstance: inst,
data: data,
options: options,
hookState: hookState,
};
modelTo.notifyObserversOf('before save', context, function(err) {
if (err) return cb && cb(err);

inst.setAttributes(data);
}
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(propertyName, embeddedList, options,
function(err) {
cb(err, inst);
});
}
var err = inst.isValid() ? null : new ValidationError(inst);
if (err && typeof cb === 'function') {
return process.nextTick(function() {
cb(err, inst);
});
}

context = {
Model: modelTo,
instance: inst,
options: options,
hookState: hookState,
};

if (typeof cb === 'function') {
modelInstance.updateAttribute(propertyName, embeddedList, options,
function(err) {
if (err) return cb(err, inst);
modelTo.notifyObserversOf('after save', context, function(err) {
cb(err, inst);
});
});
} else {
modelTo.notifyObserversOf('after save', context, function(err) {
if (!err) return;
debug('Unhandled error in "after save" hooks: %s', err.stack || err);
});
}
});
} else if (typeof cb === 'function') {
process.nextTick(function() {
cb(null, null); // not found
Expand All @@ -2630,15 +2657,27 @@ EmbedsMany.prototype.destroyById = function(fkId, options, cb) {
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') {
var context = {
Model: modelTo,
instance: inst,
options: options || {},
hookState: {},
};
modelTo.notifyObserversOf('before delete', context, function(err) {
if (err) return cb(err);

var index = embeddedList.indexOf(inst);
if (index > -1) embeddedList.splice(index, 1);
if (typeof cb !== 'function') return;
modelInstance.updateAttribute(propertyName,
embeddedList, function(err) {
cb(err);
modelTo.emit('deleted', inst.id, inst.toJSON());
if (err) return cb(err);
modelTo.notifyObserversOf('after delete', context, function(err) {
cb(err);
modelTo.emit('deleted', inst.id, inst.toJSON());
});
});
}
});
} else if (typeof cb === 'function') {
process.nextTick(cb); // not found
}
Expand Down Expand Up @@ -2724,24 +2763,24 @@ EmbedsMany.prototype.create = function(targetModelData, options, cb) {

var inst = this.callScopeMethod('build', targetModelData);

var updateEmbedded = function() {
var updateEmbedded = function(callback) {
if (modelInstance.isNewRecord()) {
modelInstance.setAttribute(propertyName, embeddedList);
modelInstance.save(options, function(err) {
cb(err, err ? null : inst);
callback(err, err ? null : inst);
});
} else {
modelInstance.updateAttribute(propertyName,
embeddedList, options, function(err) {
cb(err, err ? null : inst);
callback(err, err ? null : inst);
});
}
};

if (this.definition.options.persistent) {
inst.save(function(err) { // will validate
if (err) return cb(err, inst);
updateEmbedded();
updateEmbedded(cb);
});
} else {
var err = inst.isValid() ? null : new ValidationError(inst);
Expand All @@ -2750,7 +2789,21 @@ EmbedsMany.prototype.create = function(targetModelData, options, cb) {
cb(err);
});
} else {
updateEmbedded();
var context = {
Model: modelTo,
instance: inst,
options: options || {},
hookState: {},
};
modelTo.notifyObserversOf('before save', context, function(err) {
if (err) return cb(err);
updateEmbedded(function(err, inst) {
if (err) return cb(err, null);
modelTo.notifyObserversOf('after save', context, function(err) {
cb(err, err ? null : inst);
});
});
});
}
}
return cb.promise;
Expand Down
191 changes: 191 additions & 0 deletions test/operation-hooks.suite/embeds-many-create.suite.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright IBM Corp. 2015,2016. All Rights Reserved.
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

var ValidationError = require('../..').ValidationError;

var contextTestHelpers = require('../helpers/context-test-helpers');
var ContextRecorder = contextTestHelpers.ContextRecorder;
var aCtxForModel = contextTestHelpers.aCtxForModel;

var uid = require('../helpers/uid-generator');
var HookMonitor = require('../helpers/hook-monitor');

module.exports = function(dataSource, should, connectorCapabilities) {
describe('EmbedsMany - create', function() {
var ctxRecorder, hookMonitor, expectedError;

beforeEach(function setupHelpers() {
ctxRecorder = new ContextRecorder('hook not called');
hookMonitor = new HookMonitor({ includeModelName: true });
expectedError = new Error('test error');
});

var Owner, Embedded, ownerInstance;
var migrated = false;

beforeEach(function setupDatabase() {
Embedded = dataSource.createModel('Embedded', {
// Set id.generated to false to honor client side values
id: { type: String, id: true, generated: false, default: uid.next },
name: { type: String, required: true },
extra: { type: String, required: false },
});

Owner = dataSource.createModel('Owner', {});
Owner.embedsMany(Embedded);

hookMonitor.install(Embedded);
hookMonitor.install(Owner);

if (migrated) {
return Owner.deleteAll();
} else {
return dataSource.automigrate(Owner.modelName)
.then(function() { migrated = true; });
}
});

beforeEach(function setupData() {
return Owner.create({}).then(function(inst) {
ownerInstance = inst;
hookMonitor.resetNames();
});
});

function callCreate() {
var item = new Embedded({ name: 'created' });
return ownerInstance.embeddedList.create(item);
}

it('triggers hooks in the correct order', function() {
return callCreate().then(function(result) {
hookMonitor.names.should.eql([
'Embedded:before save',
//TODO 'Embedded:persist',
'Owner:before save',
'Owner:persist',
'Owner:loaded',
'Owner:after save',
//TODO 'Embedded:loaded',
'Embedded:after save',
]);
});
});

it('trigers `before save` hook on embedded model', function() {
Embedded.observe('before save', ctxRecorder.recordAndNext());
return callCreate().then(function(instance) {
ctxRecorder.records.should.eql(aCtxForModel(Embedded, {
instance: {
id: instance.id,
name: 'created',
extra: undefined,
},
// TODO isNewInstance: true,
}));
});
});

// TODO
it('trigers `before save` hook on owner model');

it('applies updates from `before save` hook', function() {
Embedded.observe('before save', function(ctx, next) {
ctx.instance.should.be.instanceOf(Embedded);
ctx.instance.extra = 'hook data';
next();
});
return callCreate().then(function(instance) {
instance.should.have.property('extra', 'hook data');
});
});

it('validates model after `before save` hook', function() {
Embedded.observe('before save', invalidateEmbeddedModel);
return callCreate().then(throwShouldHaveFailed, function(err) {
err.should.be.instanceOf(ValidationError);
// NOTE Apparently `create` is deferring validation to the owner
// model, which in turn validates all embedded instances
// and produces a single "invalid" error only
// Compare this to `embedsOne.create`, which correctly reports
// codes: { name: ['presence'] }
(err.details.codes || {}).should.eql({ embeddeds: ['invalid'] });
});
});

it('aborts when `before save` hook fails', function() {
Embedded.observe('before save', nextWithError(expectedError));
return callCreate().then(throwShouldHaveFailed, function(err) {
err.should.eql(expectedError);
});
});

// TODO
it('triggers `persist` hook on embedded model');
it('triggers `persist` hook on owner model');
it('applies updates from `persist` hook');
it('aborts when `persist` hook fails');

// TODO
it('triggers `loaded` hook on embedded model');
it('triggers `loaded` hook on owner model');
it('applies updates from `loaded` hook');
it('aborts when `loaded` hook fails');

it('triggers `after save` hook on embedded model', function() {
Embedded.observe('after save', ctxRecorder.recordAndNext());
return callCreate().then(function(instance) {
ctxRecorder.records.should.eql(aCtxForModel(Embedded, {
instance: {
id: instance.id,
name: 'created',
extra: undefined,
},
// TODO isNewInstance: true,
}));
});
});

// TODO
it('triggers `after save` hook on owner model');

it('applies updates from `after save` hook', function() {
Embedded.observe('after save', function(ctx, next) {
ctx.instance.should.be.instanceOf(Embedded);
ctx.instance.extra = 'hook data';
next();
});
return callCreate().then(function(instance) {
instance.should.have.property('extra', 'hook data');
});
});

it('aborts when `after save` hook fails', function() {
Embedded.observe('after save', nextWithError(expectedError));
return callCreate().then(throwShouldHaveFailed, function(err) {
err.should.eql(expectedError);
});
});

function invalidateEmbeddedModel(context, next) {
if (context.instance) {
context.instance.name = '';
} else {
context.data.name = '';
}
next();
}

function nextWithError(err) {
return function(context, next) {
next(err);
};
}

function throwShouldHaveFailed() {
throw new Error('operation should have failed');
}
});
};
Loading