loopback-datasource-juggler/test/persistence-hooks.suite.js

3089 lines
101 KiB
JavaScript
Raw Normal View History

2016-04-01 22:25:16 +00:00
// 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 traverse = require('traverse');
module.exports = function(dataSource, should, connectorCapabilities) {
if (!connectorCapabilities) connectorCapabilities = {};
if (connectorCapabilities.replaceOrCreateReportsNewInstance === undefined) {
2016-04-01 13:23:42 +00:00
console.warn('The connector does not support a recently added feature:' +
' replaceOrCreateReportsNewInstance');
}
describe('Persistence hooks', function() {
var observedContexts, expectedError, observersCalled;
var TestModel, existingInstance;
var migrated = false, lastId;
2015-05-21 11:51:30 +00:00
var triggered;
var undefinedValue = undefined;
beforeEach(function setupDatabase(done) {
2016-04-01 11:48:17 +00:00
observedContexts = 'hook not called';
expectedError = new Error('test error');
observersCalled = [];
TestModel = dataSource.createModel('TestModel', {
2015-06-16 20:35:35 +00:00
// Set id.generated to false to honor client side values
id: { type: String, id: true, generated: false, default: uid },
name: { type: String, required: true },
2016-04-01 11:48:17 +00:00
extra: { type: String, required: false },
});
lastId = 0;
if (migrated) {
TestModel.deleteAll(done);
} else {
dataSource.automigrate(TestModel.modelName, function(err) {
migrated = true;
done(err);
});
}
});
beforeEach(function createTestData(done) {
TestModel.create({ name: 'first' }, function(err, instance) {
if (err) return done(err);
// Look it up from DB so that default values are retrieved
TestModel.findById(instance.id, function(err, instance) {
existingInstance = instance;
undefinedValue = existingInstance.extra;
TestModel.create({ name: 'second' }, function(err) {
if (err) return done(err);
done();
});
});
});
});
describe('PersistedModel.find', function() {
2015-07-21 09:05:55 +00:00
it('triggers hooks in the correct order', function(done) {
monitorHookExecution();
TestModel.find(
2016-04-01 11:48:17 +00:00
{ where: { id: '1' }},
2015-07-21 09:05:55 +00:00
function(err, list) {
if (err) return done(err);
triggered.should.eql([
'access',
2016-04-01 11:48:17 +00:00
'loaded',
2015-07-21 09:05:55 +00:00
]);
done();
});
});
it('triggers `access` hook', function(done) {
TestModel.observe('access', pushContextAndNext());
2016-04-01 11:48:17 +00:00
TestModel.find({ where: { id: '1' }}, function(err, list) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
2016-04-01 11:48:17 +00:00
query: { where: { id: '1' }},
}));
done();
});
});
it('aborts when `access` hook fails', function(done) {
TestModel.observe('access', nextWithError(expectedError));
TestModel.find(function(err, list) {
[err].should.eql([expectedError]);
done();
});
});
it('applies updates from `access` hook', function(done) {
TestModel.observe('access', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.query = { where: { id: existingInstance.id }};
next();
});
TestModel.find(function(err, list) {
if (err) return done(err);
list.map(get('name')).should.eql([existingInstance.name]);
done();
});
});
it('triggers `access` hook for geo queries', function(done) {
TestModel.observe('access', pushContextAndNext());
TestModel.find({ where: { geo: { near: '10,20' }}}, function(err, list) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
2016-04-01 11:48:17 +00:00
query: { where: { geo: { near: '10,20' }}},
}));
done();
});
});
it('applies updates from `access` hook for geo queries', function(done) {
TestModel.observe('access', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.query = { where: { id: existingInstance.id }};
next();
});
2016-04-01 11:48:17 +00:00
TestModel.find({ where: { geo: { near: '10,20' }}}, function(err, list) {
if (err) return done(err);
list.map(get('name')).should.eql([existingInstance.name]);
done();
});
});
2015-07-21 09:05:55 +00:00
it('applies updates from `loaded` hook', function(done) {
TestModel.observe('loaded', pushContextAndNext(function(ctx) {
ctx.data.extra = 'hook data';
2015-07-21 09:05:55 +00:00
}));
TestModel.find(
2016-04-01 11:48:17 +00:00
{ where: { id: 1 }},
function(err, list) {
2015-07-21 09:05:55 +00:00
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
data: {
2016-04-01 11:48:17 +00:00
id: '1',
name: 'first',
2016-04-01 13:23:42 +00:00
extra: 'hook data',
2015-07-21 09:05:55 +00:00
},
isNewInstance: false,
hookState: { test: true },
2016-04-01 11:48:17 +00:00
options: {},
2015-07-21 09:05:55 +00:00
}));
list[0].should.have.property('extra', 'hook data');
2015-07-21 09:05:55 +00:00
done();
});
2016-04-01 11:48:17 +00:00
});
2015-07-21 09:05:55 +00:00
it('emits error when `loaded` hook fails', function(done) {
TestModel.observe('loaded', nextWithError(expectedError));
TestModel.find(
2016-04-01 11:48:17 +00:00
{ where: { id: 1 }},
2015-07-21 09:05:55 +00:00
function(err, list) {
[err].should.eql([expectedError]);
done();
});
});
});
describe('PersistedModel.create', function() {
2015-05-21 11:51:30 +00:00
it('triggers hooks in the correct order', function(done) {
monitorHookExecution();
TestModel.create(
{ name: 'created' },
function(err, record, created) {
if (err) return done(err);
triggered.should.eql([
'before save',
'persist',
'loaded',
2016-04-01 11:48:17 +00:00
'after save',
2015-05-21 11:51:30 +00:00
]);
done();
});
});
it('triggers `before save` hook', function(done) {
TestModel.observe('before save', pushContextAndNext());
TestModel.create({ name: 'created' }, function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: instance.id,
name: 'created',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
2016-04-01 11:48:17 +00:00
isNewInstance: true,
}));
done();
});
});
it('aborts when `before save` hook fails', function(done) {
TestModel.observe('before save', nextWithError(expectedError));
TestModel.create({ name: 'created' }, function(err, instance) {
[err].should.eql([expectedError]);
done();
});
});
it('applies updates from `before save` hook', function(done) {
TestModel.observe('before save', function(ctx, next) {
ctx.instance.should.be.instanceOf(TestModel);
ctx.instance.extra = 'hook data';
next();
});
TestModel.create({ id: uid(), name: 'a-name' }, function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
it('sends `before save` for each model in an array', function(done) {
TestModel.observe('before save', pushContextAndNext());
TestModel.create(
[{ name: '1' }, { name: '2' }],
function(err, list) {
if (err) return done(err);
// Creation of multiple instances is executed in parallel
observedContexts.sort(function(c1, c2) {
return c1.instance.name - c2.instance.name;
});
observedContexts.should.eql([
aTestModelCtx({
instance: { id: list[0].id, name: '1', extra: undefined },
2016-04-01 11:48:17 +00:00
isNewInstance: true,
}),
aTestModelCtx({
instance: { id: list[1].id, name: '2', extra: undefined },
2016-04-01 11:48:17 +00:00
isNewInstance: true,
}),
]);
done();
});
});
it('validates model after `before save` hook', function(done) {
TestModel.observe('before save', invalidateTestModel());
TestModel.create({ name: 'created' }, function(err) {
(err || {}).should.be.instanceOf(ValidationError);
(err.details.codes || {}).should.eql({ name: ['presence'] });
done();
});
});
2015-05-13 23:14:40 +00:00
it('triggers `persist` hook', function(done) {
TestModel.observe('persist', pushContextAndNext());
TestModel.create(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
data: { id: 'new-id', name: 'a name' },
isNewInstance: true,
2016-04-01 11:48:17 +00:00
currentInstance: { extra: null, id: 'new-id', name: 'a name' },
2015-05-13 23:14:40 +00:00
}));
done();
});
});
it('applies updates from `persist` hook', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('persist', pushContextAndNext(function(ctx) {
2015-05-13 23:14:40 +00:00
ctx.data.extra = 'hook data';
}));
2015-07-02 21:56:46 +00:00
// By default, the instance passed to create callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
2015-05-21 11:51:30 +00:00
// which if set, will apply these changes to the model instance too.
TestModel.settings.updateOnLoad = true;
2015-05-13 23:14:40 +00:00
TestModel.create(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
2015-05-21 11:51:30 +00:00
instance.should.have.property('extra', 'hook data');
2015-05-13 23:14:40 +00:00
2015-07-02 21:56:46 +00:00
// Also query the database here to verify that, on `create`
2015-05-13 23:14:40 +00:00
// updates from `persist` hook are reflected into database
TestModel.findById('new-id', function(err, dbInstance) {
if (err) return done(err);
2015-07-02 21:56:46 +00:00
should.exists(dbInstance);
2015-05-13 23:14:40 +00:00
dbInstance.toObject(true).should.eql({
id: 'new-id',
name: 'a name',
2016-04-01 11:48:17 +00:00
extra: 'hook data',
2015-05-13 23:14:40 +00:00
});
done();
2015-05-13 23:14:40 +00:00
});
});
});
2015-05-21 11:51:30 +00:00
it('triggers `loaded` hook', function(done) {
TestModel.observe('loaded', pushContextAndNext());
2015-07-02 21:56:46 +00:00
// By default, the instance passed to create callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
2015-05-21 11:51:30 +00:00
// which if set, will apply these changes to the model instance too.
TestModel.settings.updateOnLoad = true;
TestModel.create(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
data: { id: 'new-id', name: 'a name' },
2016-04-01 11:48:17 +00:00
isNewInstance: true,
2015-05-21 11:51:30 +00:00
}));
done();
});
});
2015-07-21 09:05:55 +00:00
it('emits error when `loaded` hook fails', function(done) {
TestModel.observe('loaded', nextWithError(expectedError));
TestModel.create(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
[err].should.eql([expectedError]);
done();
});
});
2015-05-21 11:51:30 +00:00
it('applies updates from `loaded` hook', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('loaded', pushContextAndNext(function(ctx) {
2015-05-21 11:51:30 +00:00
ctx.data.extra = 'hook data';
}));
2015-07-02 21:56:46 +00:00
// By default, the instance passed to create callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
2015-05-21 11:51:30 +00:00
// which if set, will apply these changes to the model instance too.
TestModel.settings.updateOnLoad = true;
TestModel.create(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
it('triggers `after save` hook', function(done) {
TestModel.observe('after save', pushContextAndNext());
TestModel.create({ name: 'created' }, function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: instance.id,
name: 'created',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
2016-04-01 11:48:17 +00:00
isNewInstance: true,
}));
done();
});
});
it('aborts when `after save` hook fails', function(done) {
TestModel.observe('after save', nextWithError(expectedError));
TestModel.create({ name: 'created' }, function(err, instance) {
[err].should.eql([expectedError]);
done();
});
});
it('applies updates from `after save` hook', function(done) {
TestModel.observe('after save', function(ctx, next) {
ctx.instance.should.be.instanceOf(TestModel);
ctx.instance.extra = 'hook data';
next();
});
TestModel.create({ name: 'a-name' }, function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
it('sends `after save` for each model in an array', function(done) {
TestModel.observe('after save', pushContextAndNext());
TestModel.create(
[{ name: '1' }, { name: '2' }],
function(err, list) {
if (err) return done(err);
// Creation of multiple instances is executed in parallel
observedContexts.sort(function(c1, c2) {
return c1.instance.name - c2.instance.name;
});
observedContexts.should.eql([
aTestModelCtx({
instance: { id: list[0].id, name: '1', extra: undefined },
2016-04-01 11:48:17 +00:00
isNewInstance: true,
}),
aTestModelCtx({
instance: { id: list[1].id, name: '2', extra: undefined },
2016-04-01 11:48:17 +00:00
isNewInstance: true,
}),
]);
done();
});
});
it('emits `after save` when some models were not saved', function(done) {
TestModel.observe('before save', function(ctx, next) {
if (ctx.instance.name === 'fail')
next(expectedError);
else
next();
});
TestModel.observe('after save', pushContextAndNext());
TestModel.create(
[{ name: 'ok' }, { name: 'fail' }],
function(err, list) {
(err || []).should.have.length(2);
err[1].should.eql(expectedError);
// NOTE(bajtos) The current implementation of `Model.create(array)`
// passes all models in the second callback argument, including
// the models that were not created due to an error.
list.map(get('name')).should.eql(['ok', 'fail']);
observedContexts.should.eql(aTestModelCtx({
instance: { id: list[0].id, name: 'ok', extra: undefined },
2016-04-01 11:48:17 +00:00
isNewInstance: true,
}));
done();
});
});
});
describe('PersistedModel.findOrCreate', function() {
it('triggers `access` hook', function(done) {
TestModel.observe('access', pushContextAndNext());
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: 'new-record' }},
{ name: 'new-record' },
function(err, record, created) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ query: {
where: { name: 'new-record' },
limit: 1,
offset: 0,
2016-04-01 11:48:17 +00:00
skip: 0,
}}));
done();
});
});
if (dataSource.connector.findOrCreate) {
it('triggers `before save` hook when found', function(done) {
TestModel.observe('before save', pushContextAndNext());
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: existingInstance.name }},
{ name: existingInstance.name },
function(err, record, created) {
if (err) return done(err);
record.id.should.eql(existingInstance.id);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: getLastGeneratedUid(),
name: existingInstance.name,
2016-04-01 11:48:17 +00:00
extra: undefined,
},
2016-04-01 11:48:17 +00:00
isNewInstance: true,
}));
done();
});
});
}
it('triggers `before save` hook when not found', function(done) {
TestModel.observe('before save', pushContextAndNext());
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: 'new-record' }},
{ name: 'new-record' },
function(err, record, created) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: record.id,
name: 'new-record',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
2016-04-01 11:48:17 +00:00
isNewInstance: true,
}));
done();
});
});
it('validates model after `before save` hook', function(done) {
TestModel.observe('before save', invalidateTestModel());
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: 'new-record' }},
{ name: 'new-record' },
function(err) {
(err || {}).should.be.instanceOf(ValidationError);
(err.details.codes || {}).should.eql({ name: ['presence'] });
done();
});
});
it('triggers hooks in the correct order when not found', function(done) {
2015-05-21 11:51:30 +00:00
monitorHookExecution();
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: 'new-record' }},
{ name: 'new-record' },
function(err, record, created) {
if (err) return done(err);
triggered.should.eql([
'access',
'before save',
2015-05-13 23:14:40 +00:00
'persist',
2015-05-21 11:51:30 +00:00
'loaded',
2016-04-01 11:48:17 +00:00
'after save',
]);
done();
});
});
2015-05-21 11:51:30 +00:00
it('triggers hooks in the correct order when found', function(done) {
monitorHookExecution();
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: existingInstance.name }},
2015-05-21 11:51:30 +00:00
{ name: existingInstance.name },
function(err, record, created) {
if (err) return done(err);
if (dataSource.connector.findOrCreate) {
triggered.should.eql([
'access',
'before save',
'persist',
2016-04-01 11:48:17 +00:00
'loaded',
2015-05-21 11:51:30 +00:00
]);
} else {
triggered.should.eql([
'access',
2016-04-01 11:48:17 +00:00
'loaded',
2015-05-21 11:51:30 +00:00
]);
}
done();
});
});
it('aborts when `access` hook fails', function(done) {
TestModel.observe('access', nextWithError(expectedError));
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { id: 'does-not-exist' }},
{ name: 'does-not-exist' },
function(err, instance) {
[err].should.eql([expectedError]);
done();
});
});
it('aborts when `before save` hook fails', function(done) {
TestModel.observe('before save', nextWithError(expectedError));
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { id: 'does-not-exist' }},
{ name: 'does-not-exist' },
function(err, instance) {
[err].should.eql([expectedError]);
done();
});
});
2015-05-13 23:14:40 +00:00
if (dataSource.connector.findOrCreate) {
it('triggers `persist` hook when found', function(done) {
TestModel.observe('persist', pushContextAndNext());
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: existingInstance.name }},
2015-05-13 23:14:40 +00:00
{ name: existingInstance.name },
function(err, record, created) {
if (err) return done(err);
record.id.should.eql(existingInstance.id);
// `findOrCreate` creates a new instance of the object everytime.
// So, `data.id` as well as `currentInstance.id` always matches
2015-06-16 20:35:35 +00:00
// the newly generated UID.
// Hence, the test below asserts both `data.id` and
2015-05-13 23:14:40 +00:00
// `currentInstance.id` to match getLastGeneratedUid().
// On same lines, it also asserts `isNewInstance` to be true.
observedContexts.should.eql(aTestModelCtx({
data: {
id: getLastGeneratedUid(),
2016-04-01 11:48:17 +00:00
name: existingInstance.name,
2015-05-13 23:14:40 +00:00
},
isNewInstance: true,
currentInstance: {
id: getLastGeneratedUid(),
name: record.name,
2016-04-01 11:48:17 +00:00
extra: null,
2015-05-13 23:14:40 +00:00
},
2016-04-01 11:48:17 +00:00
where: { name: existingInstance.name },
2015-05-13 23:14:40 +00:00
}));
done();
});
});
}
it('triggers `persist` hook when not found', function(done) {
TestModel.observe('persist', pushContextAndNext());
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: 'new-record' }},
2015-05-13 23:14:40 +00:00
{ name: 'new-record' },
function(err, record, created) {
if (err) return done(err);
// `context.where` is present in Optimized connector context,
// but, unoptimized connector does NOT have it.
if (dataSource.connector.findOrCreate) {
observedContexts.should.eql(aTestModelCtx({
data: {
id: record.id,
2016-04-01 11:48:17 +00:00
name: 'new-record',
2015-05-13 23:14:40 +00:00
},
isNewInstance: true,
currentInstance: {
id: record.id,
name: record.name,
2016-04-01 11:48:17 +00:00
extra: null,
2015-05-13 23:14:40 +00:00
},
2016-04-01 11:48:17 +00:00
where: { name: 'new-record' },
2015-05-13 23:14:40 +00:00
}));
} else {
observedContexts.should.eql(aTestModelCtx({
data: {
id: record.id,
2016-04-01 11:48:17 +00:00
name: 'new-record',
2015-05-13 23:14:40 +00:00
},
isNewInstance: true,
2016-04-01 11:48:17 +00:00
currentInstance: { id: record.id, name: record.name, extra: null },
2015-05-13 23:14:40 +00:00
}));
}
done();
});
});
if (dataSource.connector.findOrCreate) {
it('applies updates from `persist` hook when found', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('persist', pushContextAndNext(function(ctx) {
2015-05-13 23:14:40 +00:00
ctx.data.extra = 'hook data';
}));
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: existingInstance.name }},
2015-05-13 23:14:40 +00:00
{ name: existingInstance.name },
function(err, instance) {
if (err) return done(err);
// instance returned by `findOrCreate` context does not
// have the values updated from `persist` hook
instance.should.not.have.property('extra', 'hook data');
2015-06-16 20:35:35 +00:00
// Query the database. Here, since record already exists
2015-05-13 23:14:40 +00:00
// `findOrCreate`, does not update database for
// updates from `persist` hook
TestModel.findById(existingInstance.id, function(err, dbInstance) {
if (err) return done(err);
2015-07-02 21:56:46 +00:00
should.exists(dbInstance);
2015-05-13 23:14:40 +00:00
dbInstance.toObject(true).should.eql({
id: existingInstance.id,
name: existingInstance.name,
2016-04-01 11:48:17 +00:00
extra: undefined,
2015-05-13 23:14:40 +00:00
});
});
done();
});
});
}
it('applies updates from `persist` hook when not found', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('persist', pushContextAndNext(function(ctx) {
2015-05-13 23:14:40 +00:00
ctx.data.extra = 'hook data';
}));
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: 'new-record' }},
2015-05-13 23:14:40 +00:00
{ name: 'new-record' },
function(err, instance) {
if (err) return done(err);
if (dataSource.connector.findOrCreate) {
instance.should.have.property('extra', 'hook data');
} else {
// Unoptimized connector gives a call to `create. And during
2015-06-16 20:35:35 +00:00
// create the updates applied through persist hook are
// reflected into the database, but the same updates are
// NOT reflected in the instance object obtained in callback
2015-05-13 23:14:40 +00:00
// of create.
2015-06-16 20:35:35 +00:00
// So, this test asserts unoptimized connector to
// NOT have `extra` property. And then verifes that the
2015-05-13 23:14:40 +00:00
// property `extra` is actually updated in DB
instance.should.not.have.property('extra', 'hook data');
TestModel.findById(instance.id, function(err, dbInstance) {
if (err) return done(err);
2015-07-02 21:56:46 +00:00
should.exists(dbInstance);
2015-05-13 23:14:40 +00:00
dbInstance.toObject(true).should.eql({
id: instance.id,
name: instance.name,
2016-04-01 11:48:17 +00:00
extra: 'hook data',
2015-05-13 23:14:40 +00:00
});
});
}
done();
});
});
2015-05-21 11:51:30 +00:00
if (dataSource.connector.findOrCreate) {
it('triggers `loaded` hook when found', function(done) {
TestModel.observe('loaded', pushContextAndNext());
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: existingInstance.name }},
2015-05-21 11:51:30 +00:00
{ name: existingInstance.name },
function(err, record, created) {
if (err) return done(err);
record.id.should.eql(existingInstance.id);
2015-07-02 21:56:46 +00:00
// After the call to `connector.findOrCreate`, since the record
// already exists, `data.id` matches `existingInstance.id`
// as against the behaviour noted for `persist` hook
2015-05-21 11:51:30 +00:00
observedContexts.should.eql(aTestModelCtx({
data: {
id: existingInstance.id,
2016-04-01 11:48:17 +00:00
name: existingInstance.name,
2015-05-21 11:51:30 +00:00
},
2016-04-01 11:48:17 +00:00
isNewInstance: false,
2015-05-21 11:51:30 +00:00
}));
done();
});
});
}
it('triggers `loaded` hook when not found', function(done) {
TestModel.observe('loaded', pushContextAndNext());
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: 'new-record' }},
2015-05-21 11:51:30 +00:00
{ name: 'new-record' },
function(err, record, created) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
data: {
id: record.id,
2016-04-01 11:48:17 +00:00
name: 'new-record',
2015-05-21 11:51:30 +00:00
},
2016-04-01 11:48:17 +00:00
isNewInstance: true,
2015-05-21 11:51:30 +00:00
}));
done();
});
});
2015-07-21 09:05:55 +00:00
it('emits error when `loaded` hook fails', function(done) {
TestModel.observe('loaded', nextWithError(expectedError));
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: 'new-record' }},
2015-07-21 09:05:55 +00:00
{ name: 'new-record' },
function(err, instance) {
[err].should.eql([expectedError]);
done();
});
});
2015-05-21 11:51:30 +00:00
if (dataSource.connector.findOrCreate) {
it('applies updates from `loaded` hook when found', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('loaded', pushContextAndNext(function(ctx) {
2015-05-21 11:51:30 +00:00
ctx.data.extra = 'hook data';
}));
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: existingInstance.name }},
2015-05-21 11:51:30 +00:00
{ name: existingInstance.name },
function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
}
it('applies updates from `loaded` hook when not found', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('loaded', pushContextAndNext(function(ctx) {
2015-05-21 11:51:30 +00:00
ctx.data.extra = 'hook data';
}));
// Unoptimized connector gives a call to `create. But,
2015-07-02 21:56:46 +00:00
// by default, the instance passed to create callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
2015-05-21 11:51:30 +00:00
// which if set, will apply these changes to the model instance too.
// Note - in case of findOrCreate, this setting is needed ONLY for
// unoptimized connector.
TestModel.settings.updateOnLoad = true;
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: 'new-record' }},
2015-05-21 11:51:30 +00:00
{ name: 'new-record' },
function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
it('triggers `after save` hook when not found', function(done) {
TestModel.observe('after save', pushContextAndNext());
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { name: 'new name' }},
{ name: 'new name' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: instance.id,
name: 'new name',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
2016-04-01 11:48:17 +00:00
isNewInstance: true,
}));
done();
});
});
it('does not trigger `after save` hook when found', function(done) {
TestModel.observe('after save', pushContextAndNext());
TestModel.findOrCreate(
2016-04-01 11:48:17 +00:00
{ where: { id: existingInstance.id }},
{ name: existingInstance.name },
function(err, instance) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
observedContexts.should.eql('hook not called');
done();
});
});
});
describe('PersistedModel.count', function(done) {
it('triggers `access` hook', function(done) {
TestModel.observe('access', pushContextAndNext());
TestModel.count({ id: existingInstance.id }, function(err, count) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ query: {
2016-04-01 11:48:17 +00:00
where: { id: existingInstance.id },
}}));
done();
});
});
it('applies updates from `access` hook', function(done) {
TestModel.observe('access', function(ctx, next) {
ctx.query.where = { id: existingInstance.id };
next();
});
TestModel.count(function(err, count) {
if (err) return done(err);
count.should.equal(1);
done();
});
});
});
describe('PersistedModel.prototype.save', function() {
2015-05-21 11:51:30 +00:00
it('triggers hooks in the correct order', function(done) {
monitorHookExecution();
existingInstance.save(
function(err, record, created) {
if (err) return done(err);
triggered.should.eql([
'before save',
'persist',
'loaded',
2016-04-01 11:48:17 +00:00
'after save',
2015-05-21 11:51:30 +00:00
]);
done();
});
});
it('triggers `before save` hook', function(done) {
TestModel.observe('before save', pushContextAndNext());
existingInstance.name = 'changed';
existingInstance.save(function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ instance: {
id: existingInstance.id,
name: 'changed',
extra: undefined,
2016-04-01 11:48:17 +00:00
}, options: { throws: false, validate: true }}));
done();
});
});
it('aborts when `before save` hook fails', function(done) {
TestModel.observe('before save', nextWithError(expectedError));
existingInstance.save(function(err, instance) {
[err].should.eql([expectedError]);
done();
});
});
it('applies updates from `before save` hook', function(done) {
TestModel.observe('before save', function(ctx, next) {
ctx.instance.should.be.instanceOf(TestModel);
ctx.instance.extra = 'hook data';
next();
});
existingInstance.save(function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
it('validates model after `before save` hook', function(done) {
TestModel.observe('before save', invalidateTestModel());
existingInstance.save(function(err) {
(err || {}).should.be.instanceOf(ValidationError);
(err.details.codes || {}).should.eql({ name: ['presence'] });
done();
});
});
2015-05-13 23:14:40 +00:00
it('triggers `persist` hook', function(done) {
TestModel.observe('persist', pushContextAndNext());
existingInstance.name = 'changed';
existingInstance.save(function(err, instance) {
if (err) return done(err);
2015-06-16 20:35:35 +00:00
// HACK: extra is undefined for NoSQL and null for SQL
delete observedContexts.data.extra;
delete observedContexts.currentInstance.extra;
2015-05-13 23:14:40 +00:00
observedContexts.should.eql(aTestModelCtx({
data: {
id: existingInstance.id,
2016-04-01 11:48:17 +00:00
name: 'changed',
2015-05-13 23:14:40 +00:00
},
currentInstance: {
id: existingInstance.id,
2016-04-01 11:48:17 +00:00
name: 'changed',
2015-05-13 23:14:40 +00:00
},
where: { id: existingInstance.id },
2016-04-01 11:48:17 +00:00
options: { throws: false, validate: true },
2015-05-13 23:14:40 +00:00
}));
done();
});
});
it('applies updates from `persist` hook', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('persist', pushContextAndNext(function(ctx) {
2015-05-13 23:14:40 +00:00
ctx.data.extra = 'hook data';
}));
existingInstance.save(function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
2015-05-21 11:51:30 +00:00
it('triggers `loaded` hook', function(done) {
TestModel.observe('loaded', pushContextAndNext());
existingInstance.extra = 'changed';
2015-05-21 11:51:30 +00:00
existingInstance.save(function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
data: {
id: existingInstance.id,
name: existingInstance.name,
extra: 'changed',
2015-05-21 11:51:30 +00:00
},
isNewInstance: false,
2016-04-01 11:48:17 +00:00
options: { throws: false, validate: true },
2015-05-21 11:51:30 +00:00
}));
done();
});
});
2015-07-21 09:05:55 +00:00
it('emits error when `loaded` hook fails', function(done) {
TestModel.observe('loaded', nextWithError(expectedError));
existingInstance.save(
function(err, instance) {
[err].should.eql([expectedError]);
done();
});
});
2015-05-21 11:51:30 +00:00
it('applies updates from `loaded` hook', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('loaded', pushContextAndNext(function(ctx) {
2015-05-21 11:51:30 +00:00
ctx.data.extra = 'hook data';
}));
existingInstance.save(function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
it('triggers `after save` hook on update', function(done) {
TestModel.observe('after save', pushContextAndNext());
existingInstance.name = 'changed';
existingInstance.save(function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: existingInstance.id,
name: 'changed',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
isNewInstance: false,
2016-04-01 11:48:17 +00:00
options: { throws: false, validate: true },
}));
done();
});
});
it('triggers `after save` hook on create', function(done) {
TestModel.observe('after save', pushContextAndNext());
var instance = new TestModel(
{ id: 'new-id', name: 'created' },
{ persisted: true });
instance.save(function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: instance.id,
name: 'created',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
isNewInstance: true,
2016-04-01 11:48:17 +00:00
options: { throws: false, validate: true },
}));
done();
});
});
it('aborts when `after save` hook fails', function(done) {
TestModel.observe('after save', nextWithError(expectedError));
existingInstance.save(function(err, instance) {
[err].should.eql([expectedError]);
done();
});
});
it('applies updates from `after save` hook', function(done) {
TestModel.observe('after save', function(ctx, next) {
ctx.instance.should.be.instanceOf(TestModel);
ctx.instance.extra = 'hook data';
next();
});
existingInstance.save(function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
});
describe('PersistedModel.prototype.updateAttributes', function() {
2015-05-21 11:51:30 +00:00
it('triggers hooks in the correct order', function(done) {
monitorHookExecution();
existingInstance.updateAttributes(
{ name: 'changed' },
function(err, record, created) {
if (err) return done(err);
triggered.should.eql([
'before save',
'persist',
'loaded',
2016-04-01 11:48:17 +00:00
'after save',
2015-05-21 11:51:30 +00:00
]);
done();
});
});
it('triggers `before save` hook', function(done) {
TestModel.observe('before save', pushContextAndNext());
var currentInstance = deepCloneToObject(existingInstance);
existingInstance.updateAttributes({ name: 'changed' }, function(err) {
if (err) return done(err);
existingInstance.name.should.equal('changed');
observedContexts.should.eql(aTestModelCtx({
where: { id: existingInstance.id },
data: { name: 'changed' },
2016-04-01 11:48:17 +00:00
currentInstance: currentInstance,
}));
done();
});
});
it('aborts when `before save` hook fails', function(done) {
TestModel.observe('before save', nextWithError(expectedError));
existingInstance.updateAttributes({ name: 'updated' }, function(err) {
[err].should.eql([expectedError]);
done();
});
});
it('applies updates from `before save` hook', function(done) {
TestModel.observe('before save', function(ctx, next) {
ctx.data.extra = 'extra data';
ctx.data.name = 'hooked name';
next();
});
existingInstance.updateAttributes({ name: 'updated' }, function(err) {
if (err) return done(err);
// We must query the database here because `updateAttributes`
// returns effectively `this`, not the data from the datasource
TestModel.findById(existingInstance.id, function(err, instance) {
if (err) return done(err);
2015-07-02 21:56:46 +00:00
should.exists(instance);
instance.toObject(true).should.eql({
id: existingInstance.id,
name: 'hooked name',
2016-04-01 11:48:17 +00:00
extra: 'extra data',
});
done();
});
});
});
it('validates model after `before save` hook', function(done) {
TestModel.observe('before save', invalidateTestModel());
existingInstance.updateAttributes({ name: 'updated' }, function(err) {
(err || {}).should.be.instanceOf(ValidationError);
(err.details.codes || {}).should.eql({ name: ['presence'] });
done();
});
});
2015-05-13 23:14:40 +00:00
it('triggers `persist` hook', function(done) {
TestModel.observe('persist', pushContextAndNext());
existingInstance.updateAttributes({ name: 'changed' }, function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
where: { id: existingInstance.id },
data: { name: 'changed' },
currentInstance: {
id: existingInstance.id,
name: 'changed',
2016-04-01 11:48:17 +00:00
extra: null,
},
2016-04-01 11:48:17 +00:00
isNewInstance: false,
2015-05-13 23:14:40 +00:00
}));
done();
});
});
it('applies updates from `persist` hook', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('persist', pushContextAndNext(function(ctx) {
2015-05-13 23:14:40 +00:00
ctx.data.extra = 'hook data';
}));
2015-07-02 21:56:46 +00:00
// By default, the instance passed to updateAttributes callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
2015-05-21 11:51:30 +00:00
// which if set, will apply these changes to the model instance too.
TestModel.settings.updateOnLoad = true;
existingInstance.updateAttributes({ name: 'changed' }, function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
it('applies updates from `persist` hook - for nested model instance', function(done) {
2015-07-30 18:24:47 +00:00
var Address = dataSource.createModel('NestedAddress', {
id: { type: String, id: true, default: 1 },
city: { type: String, required: true },
2016-04-01 11:48:17 +00:00
country: { type: String, required: true },
});
2015-07-30 18:24:47 +00:00
var User = dataSource.createModel('UserWithAddress', {
id: { type: String, id: true, default: uid() },
name: { type: String, required: true },
2016-04-01 11:48:17 +00:00
address: { type: Address, required: false },
extra: { type: String },
});
2015-07-30 18:24:47 +00:00
dataSource.automigrate(['UserWithAddress', 'NestedAddress'], function(err) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
User.create({ name: 'Joe' }, function(err, instance) {
2015-07-30 18:24:47 +00:00
if (err) return done(err);
2015-07-30 18:24:47 +00:00
var existingUser = instance;
2015-07-30 18:24:47 +00:00
User.observe('persist', pushContextAndNext(function(ctx) {
2016-04-01 11:48:17 +00:00
should.exist(ctx.data.address);
2015-07-30 18:24:47 +00:00
ctx.data.address.should.be.type('object');
ctx.data.address.should.not.be.instanceOf(Address);
2015-07-30 18:24:47 +00:00
ctx.data.extra = 'hook data';
}));
2015-07-30 18:24:47 +00:00
// By default, the instance passed to updateAttributes callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
// which if set, will apply these changes to the model instance too.
User.settings.updateOnLoad = true;
existingUser.updateAttributes(
2016-04-01 11:48:17 +00:00
{ address: new Address({ city: 'Springfield', country: 'USA' }) },
2015-07-30 18:24:47 +00:00
function(err, inst) {
if (err) return done(err);
2015-07-30 18:24:47 +00:00
inst.should.have.property('extra', 'hook data');
User.findById(existingUser.id, function(err, dbInstance) {
if (err) return done(err);
dbInstance.toObject(true).should.eql({
id: existingUser.id,
name: existingUser.name,
2016-04-01 11:48:17 +00:00
address: { id: '1', city: 'Springfield', country: 'USA' },
extra: 'hook data',
2015-07-30 18:24:47 +00:00
});
done();
});
});
2015-07-30 18:24:47 +00:00
});
});
});
2015-05-21 11:51:30 +00:00
it('triggers `loaded` hook', function(done) {
TestModel.observe('loaded', pushContextAndNext());
existingInstance.updateAttributes({ name: 'changed' }, function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
2016-04-01 11:48:17 +00:00
data: { name: 'changed' },
2015-05-21 11:51:30 +00:00
}));
done();
});
});
2015-07-21 09:05:55 +00:00
it('emits error when `loaded` hook fails', function(done) {
TestModel.observe('loaded', nextWithError(expectedError));
existingInstance.updateAttributes(
{ name: 'changed' },
function(err, instance) {
[err].should.eql([expectedError]);
done();
});
});
2015-05-21 11:51:30 +00:00
it('applies updates from `loaded` hook updateAttributes', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('loaded', pushContextAndNext(function(ctx) {
2015-05-21 11:51:30 +00:00
ctx.data.extra = 'hook data';
}));
2015-07-02 21:56:46 +00:00
// By default, the instance passed to updateAttributes callback is NOT updated
// with the changes made through persist/loaded hooks. To preserve
// backwards compatibility, we introduced a new setting updateOnLoad,
2015-05-21 11:51:30 +00:00
// which if set, will apply these changes to the model instance too.
TestModel.settings.updateOnLoad = true;
existingInstance.updateAttributes({ name: 'changed' }, function(err, instance) {
2015-05-13 23:14:40 +00:00
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
it('triggers `after save` hook', function(done) {
TestModel.observe('after save', pushContextAndNext());
existingInstance.name = 'changed';
existingInstance.updateAttributes({ name: 'changed' }, function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: existingInstance.id,
name: 'changed',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
2016-04-01 11:48:17 +00:00
isNewInstance: false,
}));
done();
});
});
it('aborts when `after save` hook fails', function(done) {
TestModel.observe('after save', nextWithError(expectedError));
existingInstance.updateAttributes({ name: 'updated' }, function(err) {
[err].should.eql([expectedError]);
done();
});
});
it('applies updates from `after save` hook', function(done) {
TestModel.observe('after save', function(ctx, next) {
ctx.instance.should.be.instanceOf(TestModel);
ctx.instance.extra = 'hook data';
next();
});
existingInstance.updateAttributes({ name: 'updated' }, function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
});
});
2016-04-01 11:48:17 +00:00
if (!getSchema().connector.replaceById) {
2016-04-01 11:48:17 +00:00
describe.skip('replaceById - not implemented', function() {});
} else {
describe('PersistedModel.prototype.replaceAttributes', function() {
it('triggers hooks in the correct order', function(done) {
monitorHookExecution();
existingInstance.replaceAttributes(
{ name: 'replaced' },
function(err, record, created) {
if (err) return done(err);
triggered.should.eql([
'before save',
'persist',
'loaded',
2016-04-01 11:48:17 +00:00
'after save',
]);
done();
});
});
it('triggers `before save` hook', function(done) {
TestModel.observe('before save', pushContextAndNext());
existingInstance.replaceAttributes({ name: 'changed' }, function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: existingInstance.id,
name: 'changed',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
2016-04-01 11:48:17 +00:00
isNewInstance: false,
}));
done();
});
});
it('aborts when `before save` hook fails', function(done) {
TestModel.observe('before save', nextWithError(expectedError));
existingInstance.replaceAttributes({ name: 'replaced' }, function(err) {
[err].should.eql([expectedError]);
done();
});
});
it('applies updates from `before save` hook', function(done) {
TestModel.observe('before save', function(ctx, next) {
ctx.instance.extra = 'extra data';
ctx.instance.name = 'hooked name';
next();
});
existingInstance.replaceAttributes({ name: 'updated' }, function(err) {
if (err) return done(err);
TestModel.findById(existingInstance.id, function(err, instance) {
if (err) return done(err);
should.exists(instance);
instance.toObject(true).should.eql({
id: existingInstance.id,
name: 'hooked name',
2016-04-01 11:48:17 +00:00
extra: 'extra data',
});
done();
});
});
});
it('validates model after `before save` hook', function(done) {
TestModel.observe('before save', invalidateTestModel());
existingInstance.replaceAttributes({ name: 'updated' }, function(err) {
(err || {}).should.be.instanceOf(ValidationError);
(err.details.codes || {}).should.eql({ name: ['presence'] });
done();
});
});
it('triggers `persist` hook', function(done) {
TestModel.observe('persist', pushContextAndNext());
existingInstance.replaceAttributes({ name: 'replacedName' }, function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
where: { id: existingInstance.id },
data: {
name: 'replacedName',
2016-04-01 11:48:17 +00:00
id: existingInstance.id,
},
currentInstance: {
id: existingInstance.id,
name: 'replacedName',
2016-04-01 11:48:17 +00:00
extra: null,
},
2016-04-01 11:48:17 +00:00
isNewInstance: false,
}));
done();
});
});
it('applies delete from `persist` hook', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('persist', pushContextAndNext(function(ctx) {
delete ctx.data.extra;
}));
existingInstance.replaceAttributes({ name: 'changed' }, function(err, instance) {
if (err) return done(err);
instance.should.not.have.property('extra', 'hook data');
done();
});
2016-04-01 11:48:17 +00:00
});
it('applies updates from `persist` hook - for nested model instance', function(done) {
var Address = dataSource.createModel('NestedAddress', {
id: { type: String, id: true, default: 1 },
city: { type: String, required: true },
2016-04-01 11:48:17 +00:00
country: { type: String, required: true },
});
var User = dataSource.createModel('UserWithAddress', {
id: { type: String, id: true, default: uid() },
name: { type: String, required: true },
2016-04-01 11:48:17 +00:00
address: { type: Address, required: false },
extra: { type: String },
});
dataSource.automigrate(['UserWithAddress', 'NestedAddress'], function(err) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
User.create({ name: 'Joe' }, function(err, instance) {
if (err) return done(err);
var existingUser = instance;
User.observe('persist', pushContextAndNext(function(ctx) {
2016-04-01 11:48:17 +00:00
should.exist(ctx.data.address);
ctx.data.address.should.be.type('object');
ctx.data.address.should.not.be.instanceOf(Address);
ctx.data.extra = 'hook data';
}));
existingUser.replaceAttributes(
2016-04-01 11:48:17 +00:00
{ name: 'John', address: new Address({ city: 'Springfield', country: 'USA' }) },
function(err, inst) {
if (err) return done(err);
inst.should.have.property('extra', 'hook data');
User.findById(existingUser.id, function(err, dbInstance) {
if (err) return done(err);
dbInstance.toObject(true).should.eql({
id: existingUser.id,
name: 'John',
2016-04-01 11:48:17 +00:00
address: { id: '1', city: 'Springfield', country: 'USA' },
extra: 'hook data',
});
done();
});
});
});
});
});
it('triggers `loaded` hook', function(done) {
TestModel.observe('loaded', pushContextAndNext());
existingInstance.replaceAttributes({ name: 'changed' }, function(err, data) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
2016-04-01 11:48:17 +00:00
data: {
name: 'changed',
2016-04-01 11:48:17 +00:00
id: data.id,
},
2016-04-01 13:23:42 +00:00
isNewInstance: false,
}));
done();
});
});
it('emits error when `loaded` hook fails', function(done) {
TestModel.observe('loaded', nextWithError(expectedError));
existingInstance.replaceAttributes(
{ name: 'replaced' },
function(err, instance) {
[err].should.eql([expectedError]);
done();
});
});
it('applies updates from `loaded` hook replaceAttributes', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('loaded', pushContextAndNext(function(ctx) {
ctx.data.name = 'changed in hook';
}));
existingInstance.replaceAttributes({ name: 'changed' }, function(err, instance) {
if (err) return done(err);
instance.should.have.property('name', 'changed in hook');
done();
});
2016-04-01 11:48:17 +00:00
});
it('triggers `after save` hook', function(done) {
TestModel.observe('after save', pushContextAndNext());
existingInstance.name = 'replaced';
existingInstance.replaceAttributes({ name: 'replaced' }, function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: existingInstance.id,
name: 'replaced',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
2016-04-01 11:48:17 +00:00
isNewInstance: false,
}));
done();
});
});
it('aborts when `after save` hook fails', function(done) {
TestModel.observe('after save', nextWithError(expectedError));
existingInstance.replaceAttributes({ name: 'replaced' }, function(err) {
[err].should.eql([expectedError]);
done();
});
});
it('applies updates from `after save` hook', function(done) {
TestModel.observe('after save', function(ctx, next) {
ctx.instance.should.be.instanceOf(TestModel);
ctx.instance.extra = 'hook data';
next();
});
existingInstance.replaceAttributes({ name: 'updated' }, function(err, instance) {
if (err) return done(err);
instance.should.have.property('extra', 'hook data');
done();
});
2016-04-01 11:48:17 +00:00
});
});
}
describe('PersistedModel.updateOrCreate', function() {
2015-05-21 11:51:30 +00:00
it('triggers hooks in the correct order on create', function(done) {
monitorHookExecution();
TestModel.updateOrCreate(
{ id: 'not-found', name: 'not found' },
function(err, record, created) {
if (err) return done(err);
triggered.should.eql([
'access',
'before save',
'persist',
'loaded',
2016-04-01 11:48:17 +00:00
'after save',
2015-05-21 11:51:30 +00:00
]);
done();
});
});
it('triggers hooks in the correct order on update', function(done) {
monitorHookExecution();
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'new name' },
function(err, record, created) {
if (err) return done(err);
if (dataSource.connector.updateOrCreate) {
triggered.should.eql([
'access',
'before save',
'persist',
'loaded',
2016-04-01 11:48:17 +00:00
'after save',
2015-05-21 11:51:30 +00:00
]);
} else {
triggered.should.eql([
'access',
'loaded',
'before save',
'persist',
'loaded',
2016-04-01 11:48:17 +00:00
'after save',
2015-05-21 11:51:30 +00:00
]);
}
done();
});
});
it('triggers `access` hook on create', function(done) {
TestModel.observe('access', pushContextAndNext());
TestModel.updateOrCreate(
{ id: 'not-found', name: 'not found' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ query: {
2016-04-01 11:48:17 +00:00
where: { id: 'not-found' },
}}));
done();
});
});
it('triggers `access` hook on update', function(done) {
TestModel.observe('access', pushContextAndNext());
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'new name' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ query: {
2016-04-01 11:48:17 +00:00
where: { id: existingInstance.id },
}}));
done();
});
});
it('does not trigger `access` on missing id', function(done) {
TestModel.observe('access', pushContextAndNext());
TestModel.updateOrCreate(
{ name: 'new name' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.equal('hook not called');
done();
});
});
it('applies updates from `access` hook when found', function(done) {
TestModel.observe('access', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.query = { where: { id: { neq: existingInstance.id }}};
next();
});
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'new name' },
function(err, instance) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
findTestModels({ fields: ['id', 'name'] }, function(err, list) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
(list || []).map(toObject).should.eql([
{ id: existingInstance.id, name: existingInstance.name, extra: undefined },
2016-04-01 11:48:17 +00:00
{ id: instance.id, name: 'new name', extra: undefined },
]);
done();
});
2016-04-01 11:48:17 +00:00
});
});
it('applies updates from `access` hook when not found', function(done) {
TestModel.observe('access', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.query = { where: { id: 'not-found' }};
next();
});
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'new name' },
function(err, instance) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
findTestModels({ fields: ['id', 'name'] }, function(err, list) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
(list || []).map(toObject).should.eql([
{ id: existingInstance.id, name: existingInstance.name, extra: undefined },
{ id: list[1].id, name: 'second', extra: undefined },
2016-04-01 11:48:17 +00:00
{ id: instance.id, name: 'new name', extra: undefined },
]);
done();
});
2016-04-01 11:48:17 +00:00
});
});
it('triggers hooks only once', function(done) {
TestModel.observe('access', pushNameAndNext('access'));
TestModel.observe('before save', pushNameAndNext('before save'));
TestModel.observe('access', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.query = { where: { id: { neq: existingInstance.id }}};
next();
});
TestModel.updateOrCreate(
{ id: 'ignored', name: 'new name' },
function(err, instance) {
if (err) return done(err);
observersCalled.should.eql(['access', 'before save']);
done();
});
});
it('triggers `before save` hook on update', function(done) {
TestModel.observe('before save', pushContextAndNext());
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'updated name' },
function(err, instance) {
if (err) return done(err);
if (dataSource.connector.updateOrCreate) {
// Atomic implementations of `updateOrCreate` cannot
// provide full instance as that depends on whether
// UPDATE or CREATE will be triggered
observedContexts.should.eql(aTestModelCtx({
where: { id: existingInstance.id },
2016-04-01 11:48:17 +00:00
data: { id: existingInstance.id, name: 'updated name' },
}));
} else {
// currentInstance is set, because a non-atomic `updateOrCreate`
// will use `prototype.updateAttributes` internally, which
// exposes this to the context
observedContexts.should.eql(aTestModelCtx({
where: { id: existingInstance.id },
data: { id: existingInstance.id, name: 'updated name' },
2016-04-01 11:48:17 +00:00
currentInstance: existingInstance,
}));
}
done();
});
});
it('triggers `before save` hook on create', function(done) {
TestModel.observe('before save', pushContextAndNext());
TestModel.updateOrCreate(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
if (dataSource.connector.updateOrCreate) {
// Atomic implementations of `updateOrCreate` cannot
// provide full instance as that depends on whether
// UPDATE or CREATE will be triggered
observedContexts.should.eql(aTestModelCtx({
where: { id: 'new-id' },
2016-04-01 11:48:17 +00:00
data: { id: 'new-id', name: 'a name' },
}));
} else {
// The default unoptimized implementation runs
// `instance.save` and thus a full instance is availalbe
observedContexts.should.eql(aTestModelCtx({
instance: { id: 'new-id', name: 'a name', extra: undefined },
2016-04-01 11:48:17 +00:00
isNewInstance: true,
}));
}
done();
});
});
it('applies updates from `before save` hook on update', function(done) {
TestModel.observe('before save', function(ctx, next) {
ctx.data.name = 'hooked';
next();
});
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'updated name' },
function(err, instance) {
if (err) return done(err);
instance.name.should.equal('hooked');
done();
});
});
it('applies updates from `before save` hook on create', function(done) {
TestModel.observe('before save', function(ctx, next) {
if (ctx.instance) {
ctx.instance.name = 'hooked';
} else {
ctx.data.name = 'hooked';
}
next();
});
TestModel.updateOrCreate(
{ id: 'new-id', name: 'new name' },
function(err, instance) {
if (err) return done(err);
instance.name.should.equal('hooked');
done();
});
});
// FIXME(bajtos) this fails with connector-specific updateOrCreate
// implementations, see the comment inside lib/dao.js (updateOrCreate)
it.skip('validates model after `before save` hook on update', function(done) {
TestModel.observe('before save', invalidateTestModel());
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'updated name' },
function(err, instance) {
(err || {}).should.be.instanceOf(ValidationError);
(err.details.codes || {}).should.eql({ name: ['presence'] });
done();
});
});
// FIXME(bajtos) this fails with connector-specific updateOrCreate
// implementations, see the comment inside lib/dao.js (updateOrCreate)
it.skip('validates model after `before save` hook on create', function(done) {
TestModel.observe('before save', invalidateTestModel());
TestModel.updateOrCreate(
{ id: 'new-id', name: 'new name' },
function(err, instance) {
(err || {}).should.be.instanceOf(ValidationError);
(err.details.codes || {}).should.eql({ name: ['presence'] });
done();
});
});
2015-05-13 23:14:40 +00:00
it('triggers `persist` hook on create', function(done) {
TestModel.observe('persist', pushContextAndNext());
TestModel.updateOrCreate(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
if (dataSource.connector.updateOrCreate) {
observedContexts.should.eql(aTestModelCtx({
where: { id: 'new-id' },
data: { id: 'new-id', name: 'a name' },
currentInstance: {
id: 'new-id',
name: 'a name',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
2015-05-13 23:14:40 +00:00
}));
} else {
observedContexts.should.eql(aTestModelCtx({
data: {
id: 'new-id',
2016-04-01 11:48:17 +00:00
name: 'a name',
2015-05-13 23:14:40 +00:00
},
isNewInstance: true,
currentInstance: {
id: 'new-id',
name: 'a name',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
2015-05-13 23:14:40 +00:00
}));
}
done();
});
});
it('triggers `persist` hook on update', function(done) {
TestModel.observe('persist', pushContextAndNext());
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'updated name' },
function(err, instance) {
if (err) return done(err);
var expectedContext = aTestModelCtx({
2015-05-13 23:14:40 +00:00
where: { id: existingInstance.id },
data: {
id: existingInstance.id,
2016-04-01 11:48:17 +00:00
name: 'updated name',
2015-05-13 23:14:40 +00:00
},
currentInstance: {
id: existingInstance.id,
name: 'updated name',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
});
if (!dataSource.connector.updateOrCreate) {
// When the connector does not provide updateOrCreate,
// DAO falls back to updateAttributes which sets this flag
expectedContext.isNewInstance = false;
}
observedContexts.should.eql(expectedContext);
2015-05-13 23:14:40 +00:00
done();
});
});
2015-05-21 11:51:30 +00:00
it('triggers `loaded` hook on create', function(done) {
TestModel.observe('loaded', pushContextAndNext());
TestModel.updateOrCreate(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
if (dataSource.connector.updateOrCreate) {
observedContexts.should.eql(aTestModelCtx({
data: { id: 'new-id', name: 'a name' },
isNewInstance: true,
2015-05-21 11:51:30 +00:00
}));
} else {
observedContexts.should.eql(aTestModelCtx({
data: {
id: 'new-id',
2016-04-01 11:48:17 +00:00
name: 'a name',
2015-05-21 11:51:30 +00:00
},
2016-04-01 11:48:17 +00:00
isNewInstance: true,
2015-05-21 11:51:30 +00:00
}));
}
done();
});
});
it('triggers `loaded` hook on update', function(done) {
TestModel.observe('loaded', pushContextAndNext());
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'updated name' },
function(err, instance) {
if (err) return done(err);
if (dataSource.connector.updateOrCreate) {
observedContexts.should.eql(aTestModelCtx({
data: {
id: existingInstance.id,
2016-04-01 11:48:17 +00:00
name: 'updated name',
},
2016-04-01 11:48:17 +00:00
isNewInstance: false,
2015-05-21 11:51:30 +00:00
}));
} else {
2015-07-02 21:56:46 +00:00
// For Unoptimized connector, the callback function `pushContextAndNext`
2015-05-21 11:51:30 +00:00
// is called twice. As a result, observedContexts
// returns an array and NOT a single instance.
observedContexts.should.eql([
aTestModelCtx({
data: {
2015-05-21 11:51:30 +00:00
id: existingInstance.id,
name: 'first',
},
isNewInstance: false,
2016-04-01 11:48:17 +00:00
options: { notify: false },
2015-05-21 11:51:30 +00:00
}),
aTestModelCtx({
data: {
id: existingInstance.id,
2016-04-01 11:48:17 +00:00
name: 'updated name',
},
}),
2015-05-21 11:51:30 +00:00
]);
}
done();
});
});
2015-07-21 09:05:55 +00:00
it('emits error when `loaded` hook fails', function(done) {
TestModel.observe('loaded', nextWithError(expectedError));
TestModel.updateOrCreate(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
[err].should.eql([expectedError]);
done();
});
});
it('triggers `after save` hook on update', function(done) {
TestModel.observe('after save', pushContextAndNext());
TestModel.updateOrCreate(
{ id: existingInstance.id, name: 'updated name' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: existingInstance.id,
name: 'updated name',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
2016-04-01 11:48:17 +00:00
isNewInstance: false,
}));
done();
});
});
it('triggers `after save` hook on create', function(done) {
TestModel.observe('after save', pushContextAndNext());
TestModel.updateOrCreate(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
instance: {
id: instance.id,
name: 'a name',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
2016-04-01 11:48:17 +00:00
isNewInstance: true,
}));
done();
});
});
});
if (!getSchema().connector.replaceById) {
2016-04-01 11:48:17 +00:00
describe.skip('replaceById - not implemented', function() {});
} else {
describe('PersistedModel.replaceOrCreate', function() {
it('triggers hooks in the correct order on create', function(done) {
monitorHookExecution();
TestModel.replaceOrCreate(
{ id: 'not-found', name: 'not found' },
function(err, record, created) {
if (err) return done(err);
triggered.should.eql([
'access',
'before save',
'persist',
'loaded',
2016-04-01 11:48:17 +00:00
'after save',
]);
done();
});
});
it('triggers hooks in the correct order on replace', function(done) {
monitorHookExecution();
TestModel.replaceOrCreate(
{ id: existingInstance.id, name: 'new name' },
function(err, record, created) {
if (err) return done(err);
if (dataSource.connector.replaceOrCreate) {
triggered.should.eql([
'access',
'before save',
'persist',
'loaded',
2016-04-01 11:48:17 +00:00
'after save',
]);
} else {
// TODO: Please see loopback-datasource-juggler/issues#836
2016-04-01 11:48:17 +00:00
//
// loaded hook is triggered twice in non-atomic version:
// 1) It gets triggered once by "find()" in this chain:
2016-04-01 11:48:17 +00:00
// "replaceORCreate()->findOne()->find()",
// which is a bug; Please see this ticket:
// loopback-datasource-juggler/issues#836.
2016-04-01 11:48:17 +00:00
// 2) It, also, gets triggered in "replaceAttributes()"
// in this chain replaceORCreate()->replaceAttributes()
triggered.should.eql([
'access',
'loaded',
'before save',
'persist',
'loaded',
2016-04-01 11:48:17 +00:00
'after save',
]);
};
done();
});
});
it('triggers `access` hook on create', function(done) {
TestModel.observe('access', pushContextAndNext());
TestModel.replaceOrCreate(
{ id: 'not-found', name: 'not found' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ query: {
2016-04-01 11:48:17 +00:00
where: { id: 'not-found' },
}}));
done();
});
});
it('triggers `access` hook on replace', function(done) {
TestModel.observe('access', pushContextAndNext());
TestModel.replaceOrCreate(
{ id: existingInstance.id, name: 'new name' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ query: {
2016-04-01 11:48:17 +00:00
where: { id: existingInstance.id },
}}));
done();
});
});
it('does not trigger `access` on missing id', function(done) {
TestModel.observe('access', pushContextAndNext());
TestModel.replaceOrCreate(
{ name: 'new name' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.equal('hook not called');
done();
});
});
it('applies updates from `access` hook when found', function(done) {
TestModel.observe('access', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.query = { where: { id: { neq: existingInstance.id }}};
next();
});
TestModel.replaceOrCreate(
{ id: existingInstance.id, name: 'new name' },
function(err, instance) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
findTestModels({ fields: ['id', 'name'] }, function(err, list) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
(list || []).map(toObject).should.eql([
{ id: existingInstance.id, name: existingInstance.name, extra: undefined },
2016-04-01 11:48:17 +00:00
{ id: instance.id, name: 'new name', extra: undefined },
]);
done();
});
2016-04-01 11:48:17 +00:00
});
});
it('applies updates from `access` hook when not found', function(done) {
TestModel.observe('access', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.query = { where: { id: 'not-found' }};
next();
});
TestModel.replaceOrCreate(
{ id: existingInstance.id, name: 'new name' },
function(err, instance) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
findTestModels({ fields: ['id', 'name'] }, function(err, list) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
(list || []).map(toObject).should.eql([
{ id: existingInstance.id, name: existingInstance.name, extra: undefined },
{ id: list[1].id, name: 'second', extra: undefined },
2016-04-01 11:48:17 +00:00
{ id: instance.id, name: 'new name', extra: undefined },
]);
done();
});
2016-04-01 11:48:17 +00:00
});
});
it('triggers hooks only once', function(done) {
TestModel.observe('access', pushNameAndNext('access'));
TestModel.observe('before save', pushNameAndNext('before save'));
TestModel.observe('access', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.query = { where: { id: { neq: existingInstance.id }}};
next();
});
TestModel.replaceOrCreate(
{ id: 'ignored', name: 'new name' },
function(err, instance) {
if (err) return done(err);
observersCalled.should.eql(['access', 'before save']);
done();
});
});
it('triggers `before save` hookon create', function(done) {
TestModel.observe('before save', pushContextAndNext());
2016-04-01 11:48:17 +00:00
TestModel.replaceOrCreate({ id: existingInstance.id, name: 'new name' },
function(err, instance) {
if (err)
return done(err);
var expectedContext = aTestModelCtx({
2016-04-01 11:48:17 +00:00
instance: instance,
});
if (!dataSource.connector.replaceOrCreate) {
expectedContext.isNewInstance = false;
}
done();
});
2016-04-01 11:48:17 +00:00
});
it('triggers `before save` hook on replace', function(done) {
TestModel.observe('before save', pushContextAndNext());
TestModel.replaceOrCreate(
{ id: existingInstance.id, name: 'replaced name' },
function(err, instance) {
if (err) return done(err);
var expectedContext = aTestModelCtx({
instance: {
id: existingInstance.id,
name: 'replaced name',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
});
if (!dataSource.connector.replaceOrCreate) {
expectedContext.isNewInstance = false;
}
observedContexts.should.eql(expectedContext);
done();
});
});
it('triggers `before save` hook on create', function(done) {
TestModel.observe('before save', pushContextAndNext());
TestModel.replaceOrCreate(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
var expectedContext = aTestModelCtx({
instance: {
id: 'new-id',
name: 'a name',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
});
if (!dataSource.connector.replaceOrCreate) {
expectedContext.isNewInstance = true;
}
observedContexts.should.eql(expectedContext);
done();
});
});
it('applies updates from `before save` hook on create', function(done) {
TestModel.observe('before save', function(ctx, next) {
ctx.instance.name = 'hooked';
next();
});
TestModel.replaceOrCreate(
{ id: 'new-id', name: 'new name' },
function(err, instance) {
if (err) return done(err);
instance.name.should.equal('hooked');
done();
});
});
it('validates model after `before save` hook on create', function(done) {
TestModel.observe('before save', invalidateTestModel());
TestModel.replaceOrCreate(
{ id: 'new-id', name: 'new name' },
function(err, instance) {
(err || {}).should.be.instanceOf(ValidationError);
(err.details.codes || {}).should.eql({ name: ['presence'] });
done();
});
});
it('triggers `persist` hook on create', function(done) {
TestModel.observe('persist', pushContextAndNext());
TestModel.replaceOrCreate(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
var expectedContext = aTestModelCtx({
2016-04-01 13:23:42 +00:00
currentInstance: {
id: 'new-id',
name: 'a name',
extra: undefined,
},
data: {
id: 'new-id',
name: 'a name',
},
});
if (dataSource.connector.replaceOrCreate) {
expectedContext.where = { id: 'new-id' };
} else {
// non-atomic implementation does not provide ctx.where
// because a new instance is being created, so there
// are not records to match where filter.
expectedContext.isNewInstance = true;
}
observedContexts.should.eql(expectedContext);
done();
});
});
it('triggers `persist` hook on replace', function(done) {
TestModel.observe('persist', pushContextAndNext());
TestModel.replaceOrCreate(
{ id: existingInstance.id, name: 'replaced name' },
function(err, instance) {
if (err) return done(err);
var expected = {
where: { id: existingInstance.id },
data: {
id: existingInstance.id,
2016-04-01 11:48:17 +00:00
name: 'replaced name',
},
currentInstance: {
id: existingInstance.id,
name: 'replaced name',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
};
var expectedContext = aTestModelCtx(expected);
var expectedContext;
if (!dataSource.connector.replaceOrCreate) {
expectedContext.isNewInstance = false;
}
observedContexts.should.eql(expectedContext);
done();
});
});
it('triggers `loaded` hook on create', function(done) {
TestModel.observe('loaded', pushContextAndNext());
TestModel.replaceOrCreate(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
var expected = {
data: {
id: 'new-id',
2016-04-01 11:48:17 +00:00
name: 'a name',
},
};
2016-04-01 11:48:17 +00:00
expected.isNewInstance =
connectorCapabilities.replaceOrCreateReportsNewInstance ?
2016-04-01 11:48:17 +00:00
true : undefined;
observedContexts.should.eql(aTestModelCtx(expected));
done();
});
});
it('triggers `loaded` hook on replace', function(done) {
TestModel.observe('loaded', pushContextAndNext());
TestModel.replaceOrCreate(
{ id: existingInstance.id, name: 'replaced name' },
function(err, instance) {
if (err) return done(err);
var expected = {
data: {
id: existingInstance.id,
2016-04-01 11:48:17 +00:00
name: 'replaced name',
},
};
2016-04-01 11:48:17 +00:00
expected.isNewInstance =
connectorCapabilities.replaceOrCreateReportsNewInstance ?
false : undefined;
if (dataSource.connector.replaceOrCreate) {
observedContexts.should.eql(aTestModelCtx(expected));
} else {
// TODO: Please see loopback-datasource-juggler/issues#836
2016-04-01 11:48:17 +00:00
//
// loaded hook is triggered twice in non-atomic version:
// 1) It gets triggered once by "find()" in this chain:
2016-04-01 11:48:17 +00:00
// "replaceORCreate()->findOne()->find()",
// which is a bug; Please see this ticket:
// loopback-datasource-juggler/issues#836.
2016-04-01 11:48:17 +00:00
// 2) It, also, gets triggered in "replaceAttributes()"
// in this chain replaceORCreate()->replaceAttributes()
observedContexts.should.eql([
aTestModelCtx({
data: {
id: existingInstance.id,
2016-04-01 11:48:17 +00:00
name: 'first',
},
isNewInstance: false,
2016-04-01 11:48:17 +00:00
options: { notify: false },
}),
aTestModelCtx({
data: {
id: existingInstance.id,
2016-04-01 11:48:17 +00:00
name: 'replaced name',
},
2016-04-01 11:48:17 +00:00
isNewInstance: false,
}),
]);
}
done();
});
});
it('emits error when `loaded` hook fails', function(done) {
TestModel.observe('loaded', nextWithError(expectedError));
TestModel.replaceOrCreate(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
[err].should.eql([expectedError]);
done();
});
2016-04-01 11:48:17 +00:00
});
it('triggers `after save` hook on replace', function(done) {
TestModel.observe('after save', pushContextAndNext());
TestModel.replaceOrCreate(
{ id: existingInstance.id, name: 'replaced name' },
function(err, instance) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
var expected = {
instance: {
id: existingInstance.id,
name: 'replaced name',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
};
2016-04-01 11:48:17 +00:00
expected.isNewInstance =
connectorCapabilities.replaceOrCreateReportsNewInstance ?
false : undefined;
observedContexts.should.eql(aTestModelCtx(expected));
done();
});
});
it('triggers `after save` hook on create', function(done) {
TestModel.observe('after save', pushContextAndNext());
TestModel.replaceOrCreate(
{ id: 'new-id', name: 'a name' },
function(err, instance) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
var expected = {
instance: {
id: instance.id,
name: 'a name',
2016-04-01 11:48:17 +00:00
extra: undefined,
},
};
2016-04-01 11:48:17 +00:00
expected.isNewInstance =
connectorCapabilities.replaceOrCreateReportsNewInstance ?
true : undefined;
2016-04-01 11:48:17 +00:00
observedContexts.should.eql(aTestModelCtx(expected));
done();
});
});
});
}
describe('PersistedModel.deleteAll', function() {
it('triggers `access` hook with query', function(done) {
TestModel.observe('access', pushContextAndNext());
TestModel.deleteAll({ name: existingInstance.name }, function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
2016-04-01 11:48:17 +00:00
query: { where: { name: existingInstance.name }},
}));
done();
});
});
it('triggers `access` hook without query', function(done) {
TestModel.observe('access', pushContextAndNext());
TestModel.deleteAll(function(err) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
observedContexts.should.eql(aTestModelCtx({ query: { where: {}}}));
done();
});
});
it('applies updates from `access` hook', function(done) {
TestModel.observe('access', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.query = { where: { id: { neq: existingInstance.id }}};
next();
});
TestModel.deleteAll(function(err) {
if (err) return done(err);
findTestModels(function(err, list) {
if (err) return done(err);
(list || []).map(get('id')).should.eql([existingInstance.id]);
done();
});
});
});
it('triggers `before delete` hook with query', function(done) {
TestModel.observe('before delete', pushContextAndNext());
TestModel.deleteAll({ name: existingInstance.name }, function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
2016-04-01 11:48:17 +00:00
where: { name: existingInstance.name },
}));
done();
});
});
it('triggers `before delete` hook without query', function(done) {
TestModel.observe('before delete', pushContextAndNext());
TestModel.deleteAll(function(err) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
observedContexts.should.eql(aTestModelCtx({ where: {}}));
done();
});
});
it('applies updates from `before delete` hook', function(done) {
TestModel.observe('before delete', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.where = { id: { neq: existingInstance.id }};
next();
});
TestModel.deleteAll(function(err) {
if (err) return done(err);
findTestModels(function(err, list) {
if (err) return done(err);
(list || []).map(get('id')).should.eql([existingInstance.id]);
done();
});
});
});
it('aborts when `before delete` hook fails', function(done) {
TestModel.observe('before delete', nextWithError(expectedError));
TestModel.deleteAll(function(err, list) {
[err].should.eql([expectedError]);
TestModel.findById(existingInstance.id, function(err, inst) {
if (err) return done(err);
(inst ? inst.toObject() : 'null').should.
eql(existingInstance.toObject());
done();
});
});
});
it('triggers `after delete` hook without query', function(done) {
TestModel.observe('after delete', pushContextAndNext());
TestModel.deleteAll(function(err) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
observedContexts.should.eql(aTestModelCtx({ where: {}}));
done();
});
});
it('triggers `after delete` hook without query', function(done) {
TestModel.observe('after delete', pushContextAndNext());
TestModel.deleteAll({ name: existingInstance.name }, function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
2016-04-01 11:48:17 +00:00
where: { name: existingInstance.name },
}));
done();
});
});
it('aborts when `after delete` hook fails', function(done) {
TestModel.observe('after delete', nextWithError(expectedError));
TestModel.deleteAll(function(err) {
[err].should.eql([expectedError]);
done();
});
});
});
describe('PersistedModel.prototype.delete', function() {
it('triggers `access` hook', function(done) {
TestModel.observe('access', pushContextAndNext());
existingInstance.delete(function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
2016-04-01 11:48:17 +00:00
query: { where: { id: existingInstance.id }},
}));
done();
});
});
it('applies updated from `access` hook', function(done) {
TestModel.observe('access', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.query = { where: { id: { neq: existingInstance.id }}};
next();
});
existingInstance.delete(function(err) {
if (err) return done(err);
findTestModels(function(err, list) {
if (err) return done(err);
(list || []).map(get('id')).should.eql([existingInstance.id]);
done();
});
});
});
it('triggers `before delete` hook', function(done) {
TestModel.observe('before delete', pushContextAndNext());
existingInstance.delete(function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
2016-04-01 11:48:17 +00:00
where: { id: existingInstance.id },
instance: existingInstance,
}));
done();
});
});
it('applies updated from `before delete` hook', function(done) {
TestModel.observe('before delete', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.where = { id: { neq: existingInstance.id }};
next();
});
existingInstance.delete(function(err) {
if (err) return done(err);
findTestModels(function(err, list) {
if (err) return done(err);
(list || []).map(get('id')).should.eql([existingInstance.id]);
done();
});
});
});
it('aborts when `before delete` hook fails', function(done) {
TestModel.observe('before delete', nextWithError(expectedError));
existingInstance.delete(function(err, list) {
[err].should.eql([expectedError]);
TestModel.findById(existingInstance.id, function(err, inst) {
if (err) return done(err);
(inst ? inst.toObject() : 'null').should.eql(
existingInstance.toObject());
done();
});
});
});
it('triggers `after delete` hook', function(done) {
TestModel.observe('after delete', pushContextAndNext());
existingInstance.delete(function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
where: { id: existingInstance.id },
2016-04-01 11:48:17 +00:00
instance: existingInstance,
}));
done();
});
});
it('triggers `after delete` hook without query', function(done) {
TestModel.observe('after delete', pushContextAndNext());
TestModel.deleteAll({ name: existingInstance.name }, function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
2016-04-01 11:48:17 +00:00
where: { name: existingInstance.name },
}));
done();
});
});
it('aborts when `after delete` hook fails', function(done) {
TestModel.observe('after delete', nextWithError(expectedError));
TestModel.deleteAll(function(err) {
[err].should.eql([expectedError]);
done();
});
});
it('propagates hookState from `before delete` to `after delete`', function(done) {
2015-03-05 10:55:04 +00:00
TestModel.observe('before delete', pushContextAndNext(function(ctx) {
ctx.hookState.foo = 'bar';
}));
TestModel.observe('after delete', pushContextAndNext(function(ctx) {
ctx.hookState.foo = ctx.hookState.foo.toUpperCase();
}));
existingInstance.delete(function(err) {
if (err) return done(err);
observedContexts.should.eql([
aTestModelCtx({
2015-03-05 10:55:04 +00:00
hookState: { foo: 'bar', test: true },
where: { id: '1' },
2016-04-01 11:48:17 +00:00
instance: existingInstance,
2015-03-05 10:55:04 +00:00
}),
aTestModelCtx({
2015-03-05 10:55:04 +00:00
hookState: { foo: 'BAR', test: true },
where: { id: '1' },
2016-04-01 11:48:17 +00:00
instance: existingInstance,
}),
2015-03-05 10:55:04 +00:00
]);
done();
});
});
it('triggers hooks only once', function(done) {
TestModel.observe('access', pushNameAndNext('access'));
TestModel.observe('after delete', pushNameAndNext('after delete'));
TestModel.observe('access', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.query = { where: { id: { neq: existingInstance.id }}};
next();
});
existingInstance.delete(function(err) {
if (err) return done(err);
observersCalled.should.eql(['access', 'after delete']);
done();
});
});
});
describe('PersistedModel.updateAll', function() {
it('triggers `access` hook', function(done) {
TestModel.observe('access', pushContextAndNext());
TestModel.updateAll(
{ name: 'searched' },
{ name: 'updated' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({ query: {
2016-04-01 11:48:17 +00:00
where: { name: 'searched' },
}}));
done();
});
});
it('applies updates from `access` hook', function(done) {
TestModel.observe('access', function(ctx, next) {
2016-04-01 11:48:17 +00:00
ctx.query = { where: { id: { neq: existingInstance.id }}};
next();
});
TestModel.updateAll(
{ id: existingInstance.id },
{ name: 'new name' },
function(err) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
findTestModels({ fields: ['id', 'name'] }, function(err, list) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
(list || []).map(toObject).should.eql([
{ id: existingInstance.id, name: existingInstance.name, extra: undefined },
2016-04-01 11:48:17 +00:00
{ id: '2', name: 'new name', extra: undefined },
]);
done();
});
2016-04-01 11:48:17 +00:00
});
});
it('triggers `before save` hook', function(done) {
TestModel.observe('before save', pushContextAndNext());
TestModel.updateAll(
{ name: 'searched' },
{ name: 'updated' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
where: { name: 'searched' },
data: { name: 'updated' },
}));
done();
});
});
it('applies updates from `before save` hook', function(done) {
TestModel.observe('before save', function(ctx, next) {
ctx.data = { name: 'hooked', extra: 'added' };
next();
});
TestModel.updateAll(
{ id: existingInstance.id },
{ name: 'updated name' },
function(err) {
if (err) return done(err);
loadTestModel(existingInstance.id, function(err, instance) {
if (err) return done(err);
instance.should.have.property('name', 'hooked');
instance.should.have.property('extra', 'added');
done();
});
});
});
2015-05-13 23:14:40 +00:00
it('triggers `persist` hook', function(done) {
TestModel.observe('persist', pushContextAndNext());
TestModel.updateAll(
{ name: existingInstance.name },
2015-05-13 23:14:40 +00:00
{ name: 'changed' },
function(err, instance) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
data: { name: 'changed' },
2016-04-01 11:48:17 +00:00
where: { name: existingInstance.name },
2015-05-13 23:14:40 +00:00
}));
done();
});
});
it('applies updates from `persist` hook', function(done) {
2016-04-01 11:48:17 +00:00
TestModel.observe('persist', pushContextAndNext(function(ctx) {
2015-05-13 23:14:40 +00:00
ctx.data.extra = 'hook data';
}));
TestModel.updateAll(
{ id: existingInstance.id },
{ name: 'changed' },
function(err) {
if (err) return done(err);
loadTestModel(existingInstance.id, function(err, instance) {
instance.should.have.property('extra', 'hook data');
done();
});
});
});
2015-05-21 11:51:30 +00:00
it('does not trigger `loaded`', function(done) {
TestModel.observe('loaded', pushContextAndNext());
TestModel.updateAll(
{ id: existingInstance.id },
2015-05-21 11:51:30 +00:00
{ name: 'changed' },
function(err, instance) {
if (err) return done(err);
2016-04-01 11:48:17 +00:00
observedContexts.should.eql('hook not called');
2015-05-21 11:51:30 +00:00
done();
});
});
it('triggers `after save` hook', function(done) {
TestModel.observe('after save', pushContextAndNext());
TestModel.updateAll(
{ id: existingInstance.id },
{ name: 'updated name' },
function(err) {
if (err) return done(err);
observedContexts.should.eql(aTestModelCtx({
where: { id: existingInstance.id },
2016-04-01 11:48:17 +00:00
data: { name: 'updated name' },
}));
done();
});
});
it('accepts hookState from options', function(done) {
TestModel.observe('after save', pushContextAndNext());
TestModel.updateAll(
{ id: existingInstance.id },
{ name: 'updated name' },
{ foo: 'bar' },
function(err) {
if (err) return done(err);
observedContexts.options.should.eql({
2016-04-01 11:48:17 +00:00
foo: 'bar',
});
done();
});
});
});
2015-03-05 10:55:04 +00:00
function pushContextAndNext(fn) {
return function(context, next) {
2015-03-05 10:55:04 +00:00
if (typeof fn === 'function') {
fn(context);
}
context = deepCloneToObject(context);
2015-03-05 10:55:04 +00:00
context.hookState.test = true;
if (typeof observedContexts === 'string') {
observedContexts = context;
return next();
}
if (!Array.isArray(observedContexts)) {
observedContexts = [observedContexts];
}
observedContexts.push(context);
next();
};
}
function pushNameAndNext(name) {
return function(context, next) {
observersCalled.push(name);
next();
};
}
function nextWithError(err) {
return function(context, next) {
next(err);
};
}
function invalidateTestModel() {
return function(context, next) {
if (context.instance) {
context.instance.name = '';
} else {
context.data.name = '';
}
next();
};
}
function aTestModelCtx(ctx) {
ctx.Model = TestModel;
2015-03-05 10:55:04 +00:00
if (!ctx.hookState) {
ctx.hookState = { test: true };
}
if (!ctx.options) {
ctx.options = {};
}
return deepCloneToObject(ctx);
}
function findTestModels(query, cb) {
if (cb === undefined && typeof query === 'function') {
cb = query;
query = null;
}
TestModel.find(query, { notify: false }, cb);
}
function loadTestModel(id, cb) {
2016-04-01 11:48:17 +00:00
TestModel.findOne({ where: { id: id }}, { notify: false }, cb);
}
function uid() {
lastId += 1;
return '' + lastId;
}
function getLastGeneratedUid() {
return '' + lastId;
}
2015-05-21 11:51:30 +00:00
function monitorHookExecution() {
triggered = [];
TestModel._notify = TestModel.notifyObserversOf;
TestModel.notifyObserversOf = function(operation, context, callback) {
triggered.push(operation);
this._notify.apply(this, arguments);
};
}
});
function deepCloneToObject(obj) {
return traverse(obj).map(function(x) {
if (x === undefined) {
// RDBMSs return null
return null;
}
if (x && x.toObject)
return x.toObject(true);
if (x && typeof x === 'function' && x.modelName)
return '[ModelCtor ' + x.modelName + ']';
});
}
function get(propertyName) {
return function(obj) {
return obj[propertyName];
};
}
function toObject(obj) {
return obj.toObject ? obj.toObject() : obj;
}
};