3784 lines
126 KiB
JavaScript
3784 lines
126 KiB
JavaScript
// Copyright IBM Corp. 2015,2019. 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
|
|
|
|
'use strict';
|
|
const ValidationError = require('../').ValidationError;
|
|
|
|
const async = require('async');
|
|
const contextTestHelpers = require('./helpers/context-test-helpers');
|
|
const ContextRecorder = contextTestHelpers.ContextRecorder;
|
|
const deepCloneToObject = contextTestHelpers.deepCloneToObject;
|
|
const aCtxForModel = contextTestHelpers.aCtxForModel;
|
|
const GeoPoint = require('../lib/geo.js').GeoPoint;
|
|
|
|
const uid = require('./helpers/uid-generator');
|
|
const getLastGeneratedUid = uid.last;
|
|
|
|
const HookMonitor = require('./helpers/hook-monitor');
|
|
let isNewInstanceFlag;
|
|
|
|
module.exports = function(dataSource, should, connectorCapabilities) {
|
|
isNewInstanceFlag = connectorCapabilities.replaceOrCreateReportsNewInstance;
|
|
if (!connectorCapabilities) connectorCapabilities = {};
|
|
if (isNewInstanceFlag === undefined) {
|
|
const warn = 'The connector does not support a recently added feature:' +
|
|
' replaceOrCreateReportsNewInstance';
|
|
console.warn(warn);
|
|
}
|
|
describe('Persistence hooks', function() {
|
|
let ctxRecorder, hookMonitor, expectedError;
|
|
let TestModel, existingInstance, GeoModel;
|
|
let migrated = false;
|
|
|
|
let undefinedValue = undefined;
|
|
|
|
beforeEach(function setupDatabase(done) {
|
|
ctxRecorder = new ContextRecorder('hook not called');
|
|
hookMonitor = new HookMonitor({includeModelName: false});
|
|
expectedError = new Error('test error');
|
|
|
|
TestModel = dataSource.createModel('TestModel', {
|
|
// 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},
|
|
});
|
|
|
|
GeoModel = dataSource.createModel('GeoModel', {
|
|
id: {type: String, id: true, default: uid.next},
|
|
name: {type: String, required: false},
|
|
location: {type: GeoPoint, required: false},
|
|
});
|
|
|
|
uid.reset();
|
|
|
|
if (migrated) {
|
|
async.series([
|
|
function(cb) {
|
|
TestModel.deleteAll(cb);
|
|
},
|
|
function(cb) {
|
|
GeoModel.deleteAll(cb);
|
|
},
|
|
], done);
|
|
} else {
|
|
dataSource.automigrate([TestModel.modelName, 'GeoModel'], 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);
|
|
const location1 = new GeoPoint({lat: 10.2, lng: 6.7});
|
|
const location2 = new GeoPoint({lat: 10.3, lng: 6.8});
|
|
GeoModel.create([
|
|
{name: 'Rome', location: location1},
|
|
{name: 'Tokyo', location: location2},
|
|
], function(err) {
|
|
done(err);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('PersistedModel.find', function() {
|
|
it('triggers hooks in the correct order', function(done) {
|
|
monitorHookExecution();
|
|
|
|
TestModel.find(
|
|
{where: {id: '1'}},
|
|
function(err, list) {
|
|
if (err) return done(err);
|
|
|
|
hookMonitor.names.should.eql([
|
|
'access',
|
|
'loaded',
|
|
]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers the loaded hook multiple times when multiple instances exist', function(done) {
|
|
monitorHookExecution();
|
|
|
|
TestModel.find(function(err, list) {
|
|
if (err) return done(err);
|
|
|
|
hookMonitor.names.should.eql([
|
|
'access',
|
|
'loaded',
|
|
'loaded',
|
|
]);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should not trigger hooks, if notify is false', function(done) {
|
|
monitorHookExecution();
|
|
TestModel.find(
|
|
{where: {id: '1'}},
|
|
{notify: false},
|
|
function(err, list) {
|
|
if (err) return done(err);
|
|
hookMonitor.names.should.be.empty();
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers the loaded hook multiple times when multiple instances exist when near filter is used',
|
|
function(done) {
|
|
const hookMonitorGeoModel = new HookMonitor({includeModelName: false});
|
|
|
|
function monitorHookExecutionGeoModel(hookNames) {
|
|
hookMonitorGeoModel.install(GeoModel, hookNames);
|
|
}
|
|
|
|
monitorHookExecutionGeoModel();
|
|
|
|
const query = {
|
|
where: {location: {near: '10,5'}},
|
|
};
|
|
GeoModel.find(query, function(err, list) {
|
|
if (err) return done(err);
|
|
|
|
hookMonitorGeoModel.names.should.eql(['access', 'loaded', 'loaded']);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updates from `loaded` hook when near filter is used', function(done) {
|
|
GeoModel.observe('loaded', function(ctx, next) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {name: 'Berlin'});
|
|
next();
|
|
});
|
|
|
|
const query = {
|
|
where: {location: {near: '10,5'}},
|
|
};
|
|
|
|
GeoModel.find(query, function(err, list) {
|
|
if (err) return done(err);
|
|
list.map(get('name')).should.eql(['Berlin', 'Berlin']);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updates to one specific instance from `loaded` hook when near filter is used',
|
|
function(done) {
|
|
GeoModel.observe('loaded', function(ctx, next) {
|
|
if (ctx.data.name === 'Rome') {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {name: 'Berlin'});
|
|
}
|
|
next();
|
|
});
|
|
|
|
const query = {
|
|
where: {location: {near: '10,5'}},
|
|
};
|
|
|
|
GeoModel.find(query, function(err, list) {
|
|
if (err) return done(err);
|
|
list.map(get('name')).should.containEql('Berlin', 'Tokyo');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updates from `loaded` hook when near filter is not used', function(done) {
|
|
TestModel.observe('loaded', function(ctx, next) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {name: 'Paris'});
|
|
next();
|
|
});
|
|
|
|
TestModel.find(function(err, list) {
|
|
if (err) return done(err);
|
|
list.map(get('name')).should.eql(['Paris', 'Paris']);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updates to one specific instance from `loaded` hook when near filter is not used',
|
|
function(done) {
|
|
TestModel.observe('loaded', function(ctx, next) {
|
|
if (ctx.data.name === 'first') {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {name: 'Paris'});
|
|
}
|
|
next();
|
|
});
|
|
|
|
TestModel.find(function(err, list) {
|
|
if (err) return done(err);
|
|
list.map(get('name')).should.eql(['Paris', 'second']);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('should not trigger hooks for geo queries, if notify is false',
|
|
function(done) {
|
|
monitorHookExecution();
|
|
|
|
TestModel.find(
|
|
{where: {geo: {near: '10,20'}}},
|
|
{notify: false},
|
|
function(err, list) {
|
|
if (err) return done(err);
|
|
hookMonitor.names.should.be.empty();
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('should apply updates from `access` hook', function(done) {
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {name: 'second'}};
|
|
next();
|
|
});
|
|
|
|
TestModel.find({name: 'first'}, function(err, list) {
|
|
if (err) return done(err);
|
|
list.map(get('name')).should.eql(['second']);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `access` hook', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.find({where: {id: '1'}}, function(err, list) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
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) {
|
|
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', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.find({where: {geo: {near: '10,20'}}}, function(err, list) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
query: {where: {geo: {near: '10,20'}}},
|
|
}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updates from `access` hook for geo queries', function(done) {
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {id: existingInstance.id}};
|
|
next();
|
|
});
|
|
|
|
TestModel.find({where: {geo: {near: '10,20'}}}, function(err, list) {
|
|
if (err) return done(err);
|
|
list.map(get('name')).should.eql([existingInstance.name]);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updates from `loaded` hook', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
}));
|
|
|
|
TestModel.find(
|
|
{where: {id: 1}},
|
|
function(err, list) {
|
|
if (err) return done(err);
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {
|
|
id: '1',
|
|
name: 'first',
|
|
extra: 'hook data',
|
|
},
|
|
isNewInstance: false,
|
|
options: {},
|
|
}));
|
|
|
|
list[0].should.have.property('extra', 'hook data');
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits error when `loaded` hook fails', function(done) {
|
|
TestModel.observe('loaded', nextWithError(expectedError));
|
|
TestModel.find(
|
|
{where: {id: 1}},
|
|
function(err, list) {
|
|
[err].should.eql([expectedError]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('PersistedModel.create', function() {
|
|
it('triggers hooks in the correct order', function(done) {
|
|
monitorHookExecution();
|
|
|
|
TestModel.create(
|
|
{name: 'created'},
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
|
|
hookMonitor.names.should.eql([
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
'after save',
|
|
]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('aborts when `after save` fires when option to notify is false', function(done) {
|
|
monitorHookExecution();
|
|
|
|
TestModel.create({name: 'created'}, {notify: false}, function(err, record, created) {
|
|
if (err) return done(err);
|
|
|
|
hookMonitor.names.should.not.containEql('after save');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `before save` hook', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.create({name: 'created'}, function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: instance.id,
|
|
name: 'created',
|
|
extra: undefined,
|
|
},
|
|
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.next(), 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', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.create(
|
|
[{name: '1'}, {name: '2'}],
|
|
function(err, list) {
|
|
if (err) return done(err);
|
|
// Creation of multiple instances is executed in parallel
|
|
ctxRecorder.records.sort(function(c1, c2) {
|
|
return c1.instance.name - c2.instance.name;
|
|
});
|
|
ctxRecorder.records.should.eql([
|
|
aCtxForModel(TestModel, {
|
|
instance: {id: list[0].id, name: '1', extra: undefined},
|
|
isNewInstance: true,
|
|
}),
|
|
aCtxForModel(TestModel, {
|
|
instance: {id: list[1].id, name: '2', extra: undefined},
|
|
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();
|
|
});
|
|
});
|
|
|
|
it('triggers `persist` hook', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.create(
|
|
{id: 'new-id', name: 'a name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {id: 'new-id', name: 'a name'},
|
|
isNewInstance: true,
|
|
currentInstance: {extra: null, id: 'new-id', name: 'a name'},
|
|
}));
|
|
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `persist` hook', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
}));
|
|
|
|
// 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,
|
|
// 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');
|
|
|
|
// Also query the database here to verify that, on `create`
|
|
// updates from `persist` hook are reflected into database
|
|
TestModel.findById('new-id', function(err, dbInstance) {
|
|
if (err) return done(err);
|
|
should.exists(dbInstance);
|
|
dbInstance.toObject(true).should.eql({
|
|
id: 'new-id',
|
|
name: 'a name',
|
|
extra: 'hook data',
|
|
});
|
|
done();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `loaded` hook', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
|
|
// 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,
|
|
// 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);
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {id: 'new-id', name: 'a name'},
|
|
isNewInstance: true,
|
|
}));
|
|
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
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();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `loaded` hook', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
}));
|
|
|
|
// 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,
|
|
// 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', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.create({name: 'created'}, function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: instance.id,
|
|
name: 'created',
|
|
extra: undefined,
|
|
},
|
|
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', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.create(
|
|
[{name: '1'}, {name: '2'}],
|
|
function(err, list) {
|
|
if (err) return done(err);
|
|
// Creation of multiple instances is executed in parallel
|
|
ctxRecorder.records.sort(function(c1, c2) {
|
|
return c1.instance.name - c2.instance.name;
|
|
});
|
|
ctxRecorder.records.should.eql([
|
|
aCtxForModel(TestModel, {
|
|
instance: {id: list[0].id, name: '1', extra: undefined},
|
|
isNewInstance: true,
|
|
}),
|
|
aCtxForModel(TestModel, {
|
|
instance: {id: list[1].id, name: '2', extra: undefined},
|
|
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', ctxRecorder.recordAndNext());
|
|
|
|
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']);
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {id: list[0].id, name: 'ok', extra: undefined},
|
|
isNewInstance: true,
|
|
}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('PersistedModel.findOrCreate', function() {
|
|
it('triggers `access` hook', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: 'new-record'}},
|
|
{name: 'new-record'},
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
|
where: {name: 'new-record'},
|
|
limit: 1,
|
|
offset: 0,
|
|
skip: 0,
|
|
}}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
if (dataSource.connector.findOrCreate) {
|
|
it('triggers `before save` hook when found', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: existingInstance.name}},
|
|
{name: existingInstance.name},
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
record.id.should.eql(existingInstance.id);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: getLastGeneratedUid(),
|
|
name: existingInstance.name,
|
|
extra: undefined,
|
|
},
|
|
isNewInstance: true,
|
|
}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
it('triggers `before save` hook when not found', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: 'new-record'}},
|
|
{name: 'new-record'},
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: record.id,
|
|
name: 'new-record',
|
|
extra: undefined,
|
|
},
|
|
isNewInstance: true,
|
|
}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('validates model after `before save` hook', function(done) {
|
|
TestModel.observe('before save', invalidateTestModel());
|
|
|
|
TestModel.findOrCreate(
|
|
{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) {
|
|
monitorHookExecution();
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: 'new-record'}},
|
|
{name: 'new-record'},
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
hookMonitor.names.should.eql([
|
|
'access',
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
'after save',
|
|
]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers hooks in the correct order when found', function(done) {
|
|
monitorHookExecution();
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: existingInstance.name}},
|
|
{name: existingInstance.name},
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
|
|
if (dataSource.connector.findOrCreate) {
|
|
hookMonitor.names.should.eql([
|
|
'access',
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
]);
|
|
} else {
|
|
hookMonitor.names.should.eql([
|
|
'access',
|
|
'loaded',
|
|
]);
|
|
}
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('aborts when `access` hook fails', function(done) {
|
|
TestModel.observe('access', nextWithError(expectedError));
|
|
|
|
TestModel.findOrCreate(
|
|
{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(
|
|
{where: {id: 'does-not-exist'}},
|
|
{name: 'does-not-exist'},
|
|
function(err, instance) {
|
|
[err].should.eql([expectedError]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
if (dataSource.connector.findOrCreate) {
|
|
it('triggers `persist` hook when found', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: existingInstance.name}},
|
|
{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
|
|
// the newly generated UID.
|
|
// Hence, the test below asserts both `data.id` and
|
|
// `currentInstance.id` to match getLastGeneratedUid().
|
|
// On same lines, it also asserts `isNewInstance` to be true.
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {
|
|
id: getLastGeneratedUid(),
|
|
name: existingInstance.name,
|
|
},
|
|
isNewInstance: true,
|
|
currentInstance: {
|
|
id: getLastGeneratedUid(),
|
|
name: record.name,
|
|
extra: null,
|
|
},
|
|
where: {name: existingInstance.name},
|
|
}));
|
|
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
it('triggers `persist` hook when not found', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: 'new-record'}},
|
|
{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) {
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {
|
|
id: record.id,
|
|
name: 'new-record',
|
|
},
|
|
isNewInstance: true,
|
|
currentInstance: {
|
|
id: record.id,
|
|
name: record.name,
|
|
extra: null,
|
|
},
|
|
where: {name: 'new-record'},
|
|
}));
|
|
} else {
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {
|
|
id: record.id,
|
|
name: 'new-record',
|
|
},
|
|
isNewInstance: true,
|
|
currentInstance: {id: record.id, name: record.name, extra: null},
|
|
}));
|
|
}
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
if (dataSource.connector.findOrCreate) {
|
|
it('applies updates from `persist` hook when found', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
}));
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: existingInstance.name}},
|
|
{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');
|
|
|
|
// Query the database. Here, since record already exists
|
|
// `findOrCreate`, does not update database for
|
|
// updates from `persist` hook
|
|
TestModel.findById(existingInstance.id, function(err, dbInstance) {
|
|
if (err) return done(err);
|
|
should.exists(dbInstance);
|
|
dbInstance.toObject(true).should.eql({
|
|
id: existingInstance.id,
|
|
name: existingInstance.name,
|
|
extra: undefined,
|
|
});
|
|
});
|
|
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
it('applies updates from `persist` hook when not found', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
}));
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: 'new-record'}},
|
|
{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
|
|
// 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
|
|
// of create.
|
|
// So, this test asserts unoptimized connector to
|
|
// NOT have `extra` property. And then verifes that the
|
|
// 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);
|
|
should.exists(dbInstance);
|
|
dbInstance.toObject(true).should.eql({
|
|
id: instance.id,
|
|
name: instance.name,
|
|
extra: 'hook data',
|
|
});
|
|
});
|
|
}
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
if (dataSource.connector.findOrCreate) {
|
|
it('triggers `loaded` hook when found', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: existingInstance.name}},
|
|
{name: existingInstance.name},
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
|
|
record.id.should.eql(existingInstance.id);
|
|
|
|
// After the call to `connector.findOrCreate`, since the record
|
|
// already exists, `data.id` matches `existingInstance.id`
|
|
// as against the behaviour noted for `persist` hook
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {
|
|
id: existingInstance.id,
|
|
name: existingInstance.name,
|
|
},
|
|
isNewInstance: false,
|
|
}));
|
|
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
it('triggers `loaded` hook when not found', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: 'new-record'}},
|
|
{name: 'new-record'},
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {
|
|
id: record.id,
|
|
name: 'new-record',
|
|
},
|
|
isNewInstance: true,
|
|
}));
|
|
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('emits error when `loaded` hook fails', function(done) {
|
|
TestModel.observe('loaded', nextWithError(expectedError));
|
|
TestModel.findOrCreate(
|
|
{where: {name: 'new-record'}},
|
|
{name: 'new-record'},
|
|
function(err, instance) {
|
|
[err].should.eql([expectedError]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
if (dataSource.connector.findOrCreate) {
|
|
it('applies updates from `loaded` hook when found', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
}));
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: existingInstance.name}},
|
|
{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) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
}));
|
|
|
|
// Unoptimized connector gives a call to `create. But,
|
|
// 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,
|
|
// 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(
|
|
{where: {name: 'new-record'}},
|
|
{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', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {name: 'new name'}},
|
|
{name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: instance.id,
|
|
name: 'new name',
|
|
extra: undefined,
|
|
},
|
|
isNewInstance: true,
|
|
}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not trigger `after save` hook when found', function(done) {
|
|
TestModel.observe('after save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.findOrCreate(
|
|
{where: {id: existingInstance.id}},
|
|
{name: existingInstance.name},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql('hook not called');
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('PersistedModel.count', function(done) {
|
|
it('triggers `access` hook', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.count({id: existingInstance.id}, function(err, count) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
|
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() {
|
|
it('triggers hooks in the correct order', function(done) {
|
|
monitorHookExecution();
|
|
|
|
existingInstance.save(
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
hookMonitor.names.should.eql([
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
'after save',
|
|
]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `before save` hook', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
|
|
existingInstance.name = 'changed';
|
|
existingInstance.save(function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {instance: {
|
|
id: existingInstance.id,
|
|
name: 'changed',
|
|
extra: undefined,
|
|
}, 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();
|
|
});
|
|
});
|
|
|
|
it('triggers `persist` hook', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext());
|
|
|
|
existingInstance.name = 'changed';
|
|
existingInstance.save(function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
// HACK: extra is undefined for NoSQL and null for SQL
|
|
delete ctxRecorder.records.data.extra;
|
|
delete ctxRecorder.records.currentInstance.extra;
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {
|
|
id: existingInstance.id,
|
|
name: 'changed',
|
|
},
|
|
currentInstance: {
|
|
id: existingInstance.id,
|
|
name: 'changed',
|
|
},
|
|
where: {id: existingInstance.id},
|
|
options: {throws: false, validate: true},
|
|
}));
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updates from `persist` hook', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, 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 `loaded` hook', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
|
|
existingInstance.extra = 'changed';
|
|
existingInstance.save(function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {
|
|
id: existingInstance.id,
|
|
name: existingInstance.name,
|
|
extra: 'changed',
|
|
},
|
|
isNewInstance: isNewInstanceFlag ? false : undefined,
|
|
options: {throws: false, validate: true},
|
|
}));
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('emits error when `loaded` hook fails', function(done) {
|
|
TestModel.observe('loaded', nextWithError(expectedError));
|
|
existingInstance.save(
|
|
function(err, instance) {
|
|
[err].should.eql([expectedError]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `loaded` hook', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, 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', ctxRecorder.recordAndNext());
|
|
|
|
existingInstance.name = 'changed';
|
|
existingInstance.save(function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: existingInstance.id,
|
|
name: 'changed',
|
|
extra: undefined,
|
|
},
|
|
isNewInstance: isNewInstanceFlag ? false : undefined,
|
|
options: {throws: false, validate: true},
|
|
}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `after save` hook on create', function(done) {
|
|
TestModel.observe('after save', ctxRecorder.recordAndNext());
|
|
|
|
// The rationale behind passing { persisted: true } is to bypass the check
|
|
// made by DAO to determine whether the instance should be saved via
|
|
// PersistedModel.create and force it to call connector.save()
|
|
const instance = new TestModel(
|
|
{id: 'new-id', name: 'created'},
|
|
{persisted: true},
|
|
);
|
|
|
|
instance.save(function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: instance.id,
|
|
name: 'created',
|
|
extra: undefined,
|
|
},
|
|
isNewInstance: isNewInstanceFlag ? true : undefined,
|
|
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() {
|
|
it('triggers hooks in the correct order', function(done) {
|
|
monitorHookExecution();
|
|
|
|
existingInstance.updateAttributes(
|
|
{name: 'changed'},
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
hookMonitor.names.should.eql([
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
'after save',
|
|
]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `before save` hook', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
|
|
const currentInstance = deepCloneToObject(existingInstance);
|
|
|
|
existingInstance.updateAttributes({name: 'changed'}, function(err) {
|
|
if (err) return done(err);
|
|
existingInstance.name.should.equal('changed');
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {id: existingInstance.id},
|
|
data: {name: 'changed'},
|
|
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) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {
|
|
extra: 'extra 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);
|
|
should.exists(instance);
|
|
instance.toObject(true).should.eql({
|
|
id: existingInstance.id,
|
|
name: 'hooked name',
|
|
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();
|
|
});
|
|
});
|
|
|
|
it('triggers `persist` hook', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext());
|
|
existingInstance.updateAttributes({name: 'changed'}, function(err) {
|
|
if (err) return done(err);
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {id: existingInstance.id},
|
|
data: {name: 'changed'},
|
|
currentInstance: {
|
|
id: existingInstance.id,
|
|
name: 'changed',
|
|
extra: null,
|
|
},
|
|
isNewInstance: false,
|
|
}));
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updates from `persist` hook', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
}));
|
|
|
|
// 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.
|
|
TestModel.settings.updateOnLoad = true;
|
|
existingInstance.updateAttributes({name: 'changed'}, function(err, instance) {
|
|
if (err) return done(err);
|
|
instance.should.have.property('extra', 'hook data');
|
|
TestModel.findById(existingInstance.id, (err, found) => {
|
|
if (err) return done(err);
|
|
found.should.have.property('extra', 'hook data');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('applies updates from `persist` hook - for nested model instance', function(done) {
|
|
const Address = dataSource.createModel('NestedAddress', {
|
|
id: {type: String, id: true, default: 1},
|
|
city: {type: String, required: true},
|
|
country: {type: String, required: true},
|
|
});
|
|
|
|
const User = dataSource.createModel('UserWithAddress', {
|
|
id: {type: String, id: true, default: uid.next},
|
|
name: {type: String, required: true},
|
|
address: {type: Address, required: false},
|
|
extra: {type: String},
|
|
});
|
|
|
|
dataSource.automigrate(['UserWithAddress', 'NestedAddress'], function(err) {
|
|
if (err) return done(err);
|
|
User.create({name: 'Joe'}, function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const existingUser = instance;
|
|
|
|
User.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
|
should.exist(ctx.data.address);
|
|
ctx.data.address.should.be.type('object');
|
|
ctx.data.address.should.not.be.instanceOf(Address);
|
|
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
}));
|
|
|
|
// 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(
|
|
{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: existingUser.name,
|
|
address: {id: '1', city: 'Springfield', country: 'USA'},
|
|
extra: 'hook data',
|
|
});
|
|
done();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('emits error when `persist` hook fails', function(done) {
|
|
TestModel.observe('persist', nextWithError(expectedError));
|
|
|
|
TestModel.settings.updateOnLoad = true;
|
|
existingInstance.updateAttributes({name: 'test'}, function(err, instance) {
|
|
[err].should.eql([expectedError]);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `loaded` hook', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
existingInstance.updateAttributes({name: 'changed'}, function(err) {
|
|
if (err) return done(err);
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {name: 'changed'},
|
|
isNewInstance: false,
|
|
}));
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
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();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `loaded` hook updateAttributes', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
}));
|
|
|
|
// 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.
|
|
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('triggers `after save` hook', function(done) {
|
|
TestModel.observe('after save', ctxRecorder.recordAndNext());
|
|
|
|
existingInstance.name = 'changed';
|
|
existingInstance.updateAttributes({name: 'changed'}, function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: existingInstance.id,
|
|
name: 'changed',
|
|
extra: undefined,
|
|
},
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
|
|
if (!dataSource.connector.replaceById) {
|
|
describe.skip('replaceAttributes - 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);
|
|
hookMonitor.names.should.eql([
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
'after save',
|
|
]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `before save` hook', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
|
|
existingInstance.replaceAttributes({name: 'changed'}, function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: existingInstance.id,
|
|
name: 'changed',
|
|
extra: undefined,
|
|
},
|
|
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',
|
|
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', ctxRecorder.recordAndNext());
|
|
existingInstance.replaceAttributes({name: 'replacedName'}, function(err) {
|
|
if (err) return done(err);
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {id: existingInstance.id},
|
|
data: {
|
|
name: 'replacedName',
|
|
id: existingInstance.id,
|
|
},
|
|
currentInstance: {
|
|
id: existingInstance.id,
|
|
name: 'replacedName',
|
|
extra: null,
|
|
},
|
|
isNewInstance: false,
|
|
}));
|
|
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies delete from `persist` hook', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext(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();
|
|
});
|
|
});
|
|
|
|
it('applies updates from `persist` hook - for nested model instance', function(done) {
|
|
const Address = dataSource.createModel('NestedAddress', {
|
|
id: {type: String, id: true, default: 1},
|
|
city: {type: String, required: true},
|
|
country: {type: String, required: true},
|
|
});
|
|
|
|
const User = dataSource.createModel('UserWithAddress', {
|
|
id: {type: String, id: true, default: uid.next},
|
|
name: {type: String, required: true},
|
|
address: {type: Address, required: false},
|
|
extra: {type: String},
|
|
});
|
|
|
|
dataSource.automigrate(['UserWithAddress', 'NestedAddress'], function(err) {
|
|
if (err) return done(err);
|
|
User.create({name: 'Joe'}, function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const existingUser = instance;
|
|
|
|
User.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
|
should.exist(ctx.data.address);
|
|
ctx.data.address.should.be.type('object');
|
|
ctx.data.address.should.not.be.instanceOf(Address);
|
|
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
}));
|
|
|
|
existingUser.replaceAttributes(
|
|
{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',
|
|
address: {id: '1', city: 'Springfield', country: 'USA'},
|
|
extra: 'hook data',
|
|
});
|
|
done();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
});
|
|
|
|
it('triggers `loaded` hook', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
existingInstance.replaceAttributes({name: 'changed'}, function(err, data) {
|
|
if (err) return done(err);
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {
|
|
name: 'changed',
|
|
id: data.id,
|
|
},
|
|
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) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, 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();
|
|
});
|
|
});
|
|
|
|
it('triggers `after save` hook', function(done) {
|
|
TestModel.observe('after save', ctxRecorder.recordAndNext());
|
|
|
|
existingInstance.name = 'replaced';
|
|
existingInstance.replaceAttributes({name: 'replaced'}, function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: existingInstance.id,
|
|
name: 'replaced',
|
|
extra: undefined,
|
|
},
|
|
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();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('PersistedModel.updateOrCreate', function() {
|
|
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);
|
|
hookMonitor.names.should.eql([
|
|
'access',
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
'after save',
|
|
]);
|
|
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);
|
|
hookMonitor.names.should.eql([
|
|
'access',
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
'after save',
|
|
]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `access` hook on create', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateOrCreate(
|
|
{id: 'not-found', name: 'not found'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
|
where: {id: 'not-found'},
|
|
}}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `access` hook on update', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateOrCreate(
|
|
{id: existingInstance.id, name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
|
where: {id: existingInstance.id},
|
|
}}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not trigger `access` on missing id', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateOrCreate(
|
|
{name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.equal('hook not called');
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `access` hook when found', function(done) {
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {id: {neq: existingInstance.id}}};
|
|
next();
|
|
});
|
|
|
|
TestModel.updateOrCreate(
|
|
{id: existingInstance.id, name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
findTestModels({fields: ['id', 'name']}, function(err, list) {
|
|
if (err) return done(err);
|
|
(list || []).map(toObject).should.eql([
|
|
{id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
|
{id: instance.id, name: 'new name', extra: undefined},
|
|
]);
|
|
done();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `access` hook when not found', function(done) {
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {id: 'not-found'}};
|
|
next();
|
|
});
|
|
|
|
TestModel.updateOrCreate(
|
|
{id: existingInstance.id, name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
findTestModels({fields: ['id', 'name']}, function(err, list) {
|
|
if (err) return done(err);
|
|
(list || []).map(toObject).should.eql([
|
|
{id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
|
{id: list[1].id, name: 'second', extra: undefined},
|
|
{id: instance.id, name: 'new name', extra: undefined},
|
|
]);
|
|
done();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers hooks only once', function(done) {
|
|
monitorHookExecution(['access', 'before save']);
|
|
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {id: {neq: existingInstance.id}}};
|
|
next();
|
|
});
|
|
|
|
TestModel.updateOrCreate(
|
|
{id: 'ignored', name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
hookMonitor.names.should.eql(['access', 'before save']);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `before save` hook on update', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
|
|
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
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {id: existingInstance.id},
|
|
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
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {id: existingInstance.id},
|
|
data: {id: existingInstance.id, name: 'updated name'},
|
|
currentInstance: existingInstance,
|
|
}));
|
|
}
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `before save` hook on create', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
|
|
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
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {id: 'new-id'},
|
|
data: {id: 'new-id', name: 'a name'},
|
|
}));
|
|
} else {
|
|
// The default unoptimized implementation runs
|
|
// `instance.save` and thus a full instance is availalbe
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {id: 'new-id', name: 'a name', extra: undefined},
|
|
isNewInstance: true,
|
|
}));
|
|
}
|
|
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `before save` hook on update', function(done) {
|
|
TestModel.observe('before save', function(ctx, next) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, 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 {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, 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();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `persist` hook on create', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateOrCreate(
|
|
{id: 'new-id', name: 'a name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
if (dataSource.connector.updateOrCreate) {
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {id: 'new-id'},
|
|
data: {id: 'new-id', name: 'a name'},
|
|
currentInstance: {
|
|
id: 'new-id',
|
|
name: 'a name',
|
|
extra: undefined,
|
|
},
|
|
}));
|
|
} else {
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {
|
|
id: 'new-id',
|
|
name: 'a name',
|
|
},
|
|
isNewInstance: true,
|
|
currentInstance: {
|
|
id: 'new-id',
|
|
name: 'a name',
|
|
extra: undefined,
|
|
},
|
|
}));
|
|
}
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `persist` hook on update', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateOrCreate(
|
|
{id: existingInstance.id, name: 'updated name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const expectedContext = aCtxForModel(TestModel, {
|
|
where: {id: existingInstance.id},
|
|
data: {
|
|
id: existingInstance.id,
|
|
name: 'updated name',
|
|
},
|
|
currentInstance: {
|
|
id: existingInstance.id,
|
|
name: 'updated name',
|
|
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;
|
|
}
|
|
|
|
ctxRecorder.records.should.eql(expectedContext);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `loaded` hook on create', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateOrCreate(
|
|
{id: 'new-id', name: 'a name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
if (dataSource.connector.updateOrCreate) {
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {id: 'new-id', name: 'a name'},
|
|
isNewInstance: isNewInstanceFlag ? true : undefined,
|
|
}));
|
|
} else {
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {
|
|
id: 'new-id',
|
|
name: 'a name',
|
|
},
|
|
isNewInstance: true,
|
|
}));
|
|
}
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `loaded` hook on update', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateOrCreate(
|
|
{id: existingInstance.id, name: 'updated name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {
|
|
id: existingInstance.id,
|
|
name: 'updated name',
|
|
},
|
|
isNewInstance: isNewInstanceFlag ? false : undefined,
|
|
}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
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', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateOrCreate(
|
|
{id: existingInstance.id, name: 'updated name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: existingInstance.id,
|
|
name: 'updated name',
|
|
extra: undefined,
|
|
},
|
|
isNewInstance: isNewInstanceFlag ? false : undefined,
|
|
}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('aborts when `after save` fires on update or create when option to notify is false', function(done) {
|
|
monitorHookExecution();
|
|
|
|
TestModel.updateOrCreate({name: 'created'}, {notify: false}, function(err, record, created) {
|
|
if (err) return done(err);
|
|
|
|
hookMonitor.names.should.not.containEql('after save');
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `after save` hook on create', function(done) {
|
|
TestModel.observe('after save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateOrCreate(
|
|
{id: 'new-id', name: 'a name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: instance.id,
|
|
name: 'a name',
|
|
extra: undefined,
|
|
},
|
|
isNewInstance: isNewInstanceFlag ? true : undefined,
|
|
}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
if (!dataSource.connector.replaceById) {
|
|
describe.skip('replaceOrCreate - 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);
|
|
hookMonitor.names.should.eql([
|
|
'access',
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
'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);
|
|
hookMonitor.names.should.eql([
|
|
'access',
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
'after save',
|
|
]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `access` hook on create', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.replaceOrCreate(
|
|
{id: 'not-found', name: 'not found'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
|
where: {id: 'not-found'},
|
|
}}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `access` hook on replace', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.replaceOrCreate(
|
|
{id: existingInstance.id, name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
|
where: {id: existingInstance.id},
|
|
}}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not trigger `access` on missing id', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.replaceOrCreate(
|
|
{name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.equal('hook not called');
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `access` hook when found', function(done) {
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {id: {neq: existingInstance.id}}};
|
|
next();
|
|
});
|
|
|
|
TestModel.replaceOrCreate(
|
|
{id: existingInstance.id, name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
findTestModels({fields: ['id', 'name']}, function(err, list) {
|
|
if (err) return done(err);
|
|
(list || []).map(toObject).should.eql([
|
|
{id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
|
{id: instance.id, name: 'new name', extra: undefined},
|
|
]);
|
|
done();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `access` hook when not found', function(done) {
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {id: 'not-found'}};
|
|
next();
|
|
});
|
|
|
|
TestModel.replaceOrCreate(
|
|
{id: existingInstance.id, name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
findTestModels({fields: ['id', 'name']}, function(err, list) {
|
|
if (err) return done(err);
|
|
(list || []).map(toObject).should.eql([
|
|
{id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
|
{id: list[1].id, name: 'second', extra: undefined},
|
|
{id: instance.id, name: 'new name', extra: undefined},
|
|
]);
|
|
done();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers hooks only once', function(done) {
|
|
monitorHookExecution(['access', 'before save']);
|
|
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {id: {neq: existingInstance.id}}};
|
|
next();
|
|
});
|
|
|
|
TestModel.replaceOrCreate(
|
|
{id: 'ignored', name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
hookMonitor.names.should.eql(['access', 'before save']);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `before save` hookon create', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
TestModel.replaceOrCreate({id: existingInstance.id, name: 'new name'},
|
|
function(err, instance) {
|
|
if (err)
|
|
return done(err);
|
|
|
|
const expectedContext = aCtxForModel(TestModel, {
|
|
instance: instance,
|
|
});
|
|
|
|
if (!dataSource.connector.replaceOrCreate) {
|
|
expectedContext.isNewInstance = false;
|
|
}
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `before save` hook on replace', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
TestModel.replaceOrCreate(
|
|
{id: existingInstance.id, name: 'replaced name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const expectedContext = aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: existingInstance.id,
|
|
name: 'replaced name',
|
|
extra: undefined,
|
|
},
|
|
});
|
|
|
|
if (!dataSource.connector.replaceOrCreate) {
|
|
expectedContext.isNewInstance = false;
|
|
}
|
|
ctxRecorder.records.should.eql(expectedContext);
|
|
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `before save` hook on create', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.replaceOrCreate(
|
|
{id: 'new-id', name: 'a name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const expectedContext = aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: 'new-id',
|
|
name: 'a name',
|
|
extra: undefined,
|
|
},
|
|
});
|
|
|
|
if (!dataSource.connector.replaceOrCreate) {
|
|
expectedContext.isNewInstance = true;
|
|
}
|
|
ctxRecorder.records.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', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.replaceOrCreate(
|
|
{id: 'new-id', name: 'a name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const expectedContext = aCtxForModel(TestModel, {
|
|
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;
|
|
}
|
|
ctxRecorder.records.should.eql(expectedContext);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `persist` hook on replace', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.replaceOrCreate(
|
|
{id: existingInstance.id, name: 'replaced name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const expected = {
|
|
where: {id: existingInstance.id},
|
|
data: {
|
|
id: existingInstance.id,
|
|
name: 'replaced name',
|
|
},
|
|
currentInstance: {
|
|
id: existingInstance.id,
|
|
name: 'replaced name',
|
|
extra: undefined,
|
|
},
|
|
};
|
|
|
|
const expectedContext = aCtxForModel(TestModel, expected);
|
|
|
|
if (!dataSource.connector.replaceOrCreate) {
|
|
expectedContext.isNewInstance = false;
|
|
}
|
|
|
|
ctxRecorder.records.should.eql(expectedContext);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `persist` hook on create', function(done) {
|
|
TestModel.observe('persist', (ctx, next) => {
|
|
// it's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
next();
|
|
});
|
|
|
|
// 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,
|
|
// which if set, will apply these changes to the model instance too.
|
|
TestModel.settings.updateOnLoad = true;
|
|
|
|
TestModel.replaceOrCreate(
|
|
{name: 'a name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
instance.should.have.property('extra', 'hook data');
|
|
TestModel.findById(instance.id, (err, found) => {
|
|
if (err) return done(err);
|
|
found.should.have.property('extra', 'hook data');
|
|
done();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `persist` hook on update', function(done) {
|
|
TestModel.observe('persist', (ctx, next) => {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
next();
|
|
});
|
|
|
|
existingInstance.name = 'changed';
|
|
const data = existingInstance.toObject();
|
|
|
|
TestModel.replaceOrCreate(data, function(err, instance) {
|
|
if (err) return done(err);
|
|
instance.should.have.property('extra', 'hook data');
|
|
TestModel.findById(existingInstance.id, (err, found) => {
|
|
if (err) return done(err);
|
|
found.should.have.property('extra', 'hook data');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('triggers `loaded` hook on create', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.replaceOrCreate(
|
|
{id: 'new-id', name: 'a name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const expected = {
|
|
data: {
|
|
id: 'new-id',
|
|
name: 'a name',
|
|
},
|
|
};
|
|
|
|
expected.isNewInstance =
|
|
isNewInstanceFlag ?
|
|
true : undefined;
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, expected));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `loaded` hook on replace', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.replaceOrCreate(
|
|
{id: existingInstance.id, name: 'replaced name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const expected = {
|
|
data: {
|
|
id: existingInstance.id,
|
|
name: 'replaced name',
|
|
},
|
|
};
|
|
|
|
expected.isNewInstance =
|
|
isNewInstanceFlag ?
|
|
false : undefined;
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, expected));
|
|
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();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `after save` hook on replace', function(done) {
|
|
TestModel.observe('after save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.replaceOrCreate(
|
|
{id: existingInstance.id, name: 'replaced name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const expected = {
|
|
instance: {
|
|
id: existingInstance.id,
|
|
name: 'replaced name',
|
|
extra: undefined,
|
|
},
|
|
};
|
|
|
|
expected.isNewInstance =
|
|
isNewInstanceFlag ?
|
|
false : undefined;
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, expected));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `after save` hook on create', function(done) {
|
|
TestModel.observe('after save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.replaceOrCreate(
|
|
{id: 'new-id', name: 'a name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const expected = {
|
|
instance: {
|
|
id: instance.id,
|
|
name: 'a name',
|
|
extra: undefined,
|
|
},
|
|
};
|
|
expected.isNewInstance =
|
|
isNewInstanceFlag ?
|
|
true : undefined;
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, expected));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
if (!dataSource.connector.replaceById) {
|
|
describe.skip('replaceById - not implemented', function() {});
|
|
} else {
|
|
describe('PersistedModel.replaceById', function() {
|
|
it('triggers hooks in the correct order on create', function(done) {
|
|
monitorHookExecution();
|
|
|
|
existingInstance.name = 'replaced name';
|
|
TestModel.replaceById(
|
|
existingInstance.id,
|
|
existingInstance.toObject(),
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
hookMonitor.names.should.eql([
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
'after save',
|
|
]);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `persist` hook', function(done) {
|
|
// "extra" property is undefined by default. As a result,
|
|
// NoSQL connectors omit this property from the data. Because
|
|
// SQL connectors store it as null, we have different results
|
|
// depending on the database used.
|
|
// By enabling "persistUndefinedAsNull", we force NoSQL connectors
|
|
// to store unset properties using "null" value and thus match SQL.
|
|
TestModel.settings.persistUndefinedAsNull = true;
|
|
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext());
|
|
|
|
existingInstance.name = 'replaced name';
|
|
TestModel.replaceById(
|
|
existingInstance.id,
|
|
existingInstance.toObject(),
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const expected = {
|
|
where: {id: existingInstance.id},
|
|
data: {
|
|
id: existingInstance.id,
|
|
name: 'replaced name',
|
|
extra: null,
|
|
},
|
|
currentInstance: {
|
|
id: existingInstance.id,
|
|
name: 'replaced name',
|
|
extra: null,
|
|
},
|
|
};
|
|
|
|
const expectedContext = aCtxForModel(TestModel, expected);
|
|
expectedContext.isNewInstance = false;
|
|
|
|
ctxRecorder.records.should.eql(expectedContext);
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `persist` hook', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
|
}));
|
|
|
|
existingInstance.name = 'changed';
|
|
TestModel.replaceById(
|
|
existingInstance.id,
|
|
existingInstance.toObject(),
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
instance.should.have.property('extra', 'hook data');
|
|
TestModel.findById(existingInstance.id, (err, found) => {
|
|
if (err) return done(err);
|
|
found.should.have.property('extra', 'hook data');
|
|
done();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
describe('PersistedModel.deleteAll', function() {
|
|
it('triggers `access` hook with query', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.deleteAll({name: existingInstance.name}, function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
query: {where: {name: existingInstance.name}},
|
|
}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `access` hook without query', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.deleteAll(function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {where: {}}}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updates from `access` hook', function(done) {
|
|
TestModel.observe('access', function(ctx, next) {
|
|
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', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.deleteAll({name: existingInstance.name}, function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {name: existingInstance.name},
|
|
}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `before delete` hook without query', function(done) {
|
|
TestModel.observe('before delete', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.deleteAll(function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {where: {}}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updates from `before delete` hook', function(done) {
|
|
TestModel.observe('before delete', function(ctx, next) {
|
|
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', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.deleteAll(function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {},
|
|
info: {count: 2},
|
|
}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `after delete` hook with query', function(done) {
|
|
TestModel.observe('after delete', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.deleteAll({name: existingInstance.name}, function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {name: existingInstance.name},
|
|
info: {count: 1},
|
|
}));
|
|
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', ctxRecorder.recordAndNext());
|
|
|
|
existingInstance.delete(function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
query: {where: {id: existingInstance.id}},
|
|
}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updated from `access` hook', function(done) {
|
|
TestModel.observe('access', function(ctx, next) {
|
|
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', ctxRecorder.recordAndNext());
|
|
|
|
existingInstance.delete(function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {id: existingInstance.id},
|
|
instance: existingInstance,
|
|
}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updated from `before delete` hook', function(done) {
|
|
TestModel.observe('before delete', function(ctx, next) {
|
|
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', ctxRecorder.recordAndNext());
|
|
|
|
existingInstance.delete(function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {id: existingInstance.id},
|
|
instance: existingInstance,
|
|
info: {count: 1},
|
|
}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `after delete` hook without query', function(done) {
|
|
TestModel.observe('after delete', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.deleteAll({name: existingInstance.name}, function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {name: existingInstance.name},
|
|
info: {count: 1},
|
|
}));
|
|
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) {
|
|
TestModel.observe('before delete', ctxRecorder.recordAndNext(function(ctx) {
|
|
ctx.hookState.foo = 'bar';
|
|
}));
|
|
|
|
TestModel.observe('after delete', ctxRecorder.recordAndNext(function(ctx) {
|
|
ctx.hookState.foo = ctx.hookState.foo.toUpperCase();
|
|
}));
|
|
|
|
existingInstance.delete(function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql([
|
|
aCtxForModel(TestModel, {
|
|
hookState: {foo: 'bar'},
|
|
where: {id: '1'},
|
|
instance: existingInstance,
|
|
}),
|
|
aCtxForModel(TestModel, {
|
|
hookState: {foo: 'BAR'},
|
|
info: {count: 1},
|
|
where: {id: '1'},
|
|
instance: existingInstance,
|
|
}),
|
|
]);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers hooks only once', function(done) {
|
|
monitorHookExecution();
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {id: {neq: existingInstance.id}}};
|
|
next();
|
|
});
|
|
|
|
existingInstance.delete(function(err) {
|
|
if (err) return done(err);
|
|
hookMonitor.names.should.eql(['access', 'before delete', 'after delete']);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('PersistedModel.updateAll', function() {
|
|
it('triggers `access` hook', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateAll(
|
|
{name: 'searched'},
|
|
{name: 'updated'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
|
where: {name: 'searched'},
|
|
}}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `access` hook', function(done) {
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {id: {neq: existingInstance.id}}};
|
|
next();
|
|
});
|
|
|
|
TestModel.updateAll(
|
|
{id: existingInstance.id},
|
|
{name: 'new name'},
|
|
function(err) {
|
|
if (err) return done(err);
|
|
findTestModels({fields: ['id', 'name']}, function(err, list) {
|
|
if (err) return done(err);
|
|
(list || []).map(toObject).should.eql([
|
|
{id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
|
{id: '2', name: 'new name', extra: undefined},
|
|
]);
|
|
done();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `before save` hook', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateAll(
|
|
{name: 'searched'},
|
|
{name: 'updated'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
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();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `persist` hook', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateAll(
|
|
{name: existingInstance.name},
|
|
{name: 'changed'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {name: 'changed'},
|
|
where: {name: existingInstance.name},
|
|
}));
|
|
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('applies updates from `persist` hook', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, 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();
|
|
});
|
|
},
|
|
);
|
|
});
|
|
|
|
it('does not trigger `loaded`', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateAll(
|
|
{id: existingInstance.id},
|
|
{name: 'changed'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql('hook not called');
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('triggers `after save` hook', function(done) {
|
|
TestModel.observe('after save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateAll(
|
|
{id: existingInstance.id},
|
|
{name: 'updated name'},
|
|
function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
where: {id: existingInstance.id},
|
|
data: {name: 'updated name'},
|
|
info: {count: 1},
|
|
}));
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
|
|
it('accepts hookState from options', function(done) {
|
|
TestModel.observe('after save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.updateAll(
|
|
{id: existingInstance.id},
|
|
{name: 'updated name'},
|
|
{foo: 'bar'},
|
|
function(err) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.options.should.eql({
|
|
foo: 'bar',
|
|
});
|
|
done();
|
|
},
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('PersistedModel.upsertWithWhere', function() {
|
|
it('triggers hooks in the correct order on create', function(done) {
|
|
monitorHookExecution();
|
|
TestModel.upsertWithWhere({extra: 'not-found'},
|
|
{id: 'not-found', name: 'not found', extra: 'not-found'},
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
hookMonitor.names.should.eql([
|
|
'access',
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
'after save',
|
|
]);
|
|
TestModel.findById('not-found', function(err, data) {
|
|
if (err) return done(err);
|
|
data.name.should.equal('not found');
|
|
data.extra.should.equal('not-found');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('triggers hooks in the correct order on update', function(done) {
|
|
monitorHookExecution();
|
|
TestModel.upsertWithWhere({id: existingInstance.id},
|
|
{name: 'new name', extra: 'new extra'},
|
|
function(err, record, created) {
|
|
if (err) return done(err);
|
|
hookMonitor.names.should.eql([
|
|
'access',
|
|
'before save',
|
|
'persist',
|
|
'loaded',
|
|
'after save',
|
|
]);
|
|
TestModel.findById(existingInstance.id, function(err, data) {
|
|
if (err) return done(err);
|
|
data.name.should.equal('new name');
|
|
data.extra.should.equal('new extra');
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('triggers `access` hook on create', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.upsertWithWhere({extra: 'not-found'},
|
|
{id: 'not-found', name: 'not found'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
|
where: {extra: 'not-found'},
|
|
}}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `access` hook on update', function(done) {
|
|
TestModel.observe('access', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.upsertWithWhere({id: existingInstance.id},
|
|
{name: 'new name', extra: 'new extra'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
|
where: {id: existingInstance.id},
|
|
}}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers hooks only once', function(done) {
|
|
monitorHookExecution(['access', 'before save']);
|
|
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {id: {neq: existingInstance.id}}};
|
|
next();
|
|
});
|
|
|
|
TestModel.upsertWithWhere({id: existingInstance.id},
|
|
{name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
hookMonitor.names.should.eql(['access', 'before save']);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updates from `access` hook when found', function(done) {
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {id: {neq: existingInstance.id}}};
|
|
next();
|
|
});
|
|
|
|
TestModel.upsertWithWhere({id: existingInstance.id},
|
|
{name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
findTestModels({fields: ['id', 'name']}, function(err, list) {
|
|
if (err) return done(err);
|
|
(list || []).map(toObject).should.eql([
|
|
{id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
|
{id: instance.id, name: 'new name', extra: undefined},
|
|
]);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('applies updates from `access` hook when not found', function(done) {
|
|
TestModel.observe('access', function(ctx, next) {
|
|
ctx.query = {where: {id: 'not-found'}};
|
|
next();
|
|
});
|
|
|
|
TestModel.upsertWithWhere({id: existingInstance.id},
|
|
{name: 'new name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
findTestModels({fields: ['id', 'name']}, function(err, list) {
|
|
if (err) return done(err);
|
|
(list || []).map(toObject).should.eql([
|
|
{id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
|
{id: list[1].id, name: 'second', extra: undefined},
|
|
{id: instance.id, name: 'new name', extra: undefined},
|
|
]);
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
it('triggers `before save` hook on update', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.upsertWithWhere({id: existingInstance.id},
|
|
{id: existingInstance.id, name: 'updated name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
const expectedContext = aCtxForModel(TestModel, {
|
|
where: {id: existingInstance.id},
|
|
data: {
|
|
id: existingInstance.id,
|
|
name: 'updated name',
|
|
},
|
|
});
|
|
if (!dataSource.connector.upsertWithWhere) {
|
|
// the difference between `existingInstance` and the following
|
|
// plain-data object is `currentInstance` the missing fields are
|
|
// null in `currentInstance`, wehere as in `existingInstance` they
|
|
// are undefined; please see other tests for example see:
|
|
// test for "PersistedModel.create triggers `persist` hook"
|
|
expectedContext.currentInstance = {id: existingInstance.id, name: 'first', extra: null};
|
|
}
|
|
ctxRecorder.records.should.eql(expectedContext);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `before save` hook on create', function(done) {
|
|
TestModel.observe('before save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.upsertWithWhere({id: 'new-id'},
|
|
{id: 'new-id', name: 'a name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
const expectedContext = aCtxForModel(TestModel, {});
|
|
|
|
if (dataSource.connector.upsertWithWhere) {
|
|
expectedContext.data = {id: 'new-id', name: 'a name'};
|
|
expectedContext.where = {id: 'new-id'};
|
|
} else {
|
|
expectedContext.instance = {id: 'new-id', name: 'a name', extra: null};
|
|
expectedContext.isNewInstance = true;
|
|
}
|
|
ctxRecorder.records.should.eql(expectedContext);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('applies updates from `before save` hook on update', function(done) {
|
|
TestModel.observe('before save', function(ctx, next) {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {name: 'hooked'});
|
|
next();
|
|
});
|
|
|
|
TestModel.upsertWithWhere({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 {
|
|
// It's crucial to change `ctx.data` reference, not only data props
|
|
ctx.data = Object.assign({}, ctx.data, {name: 'hooked'});
|
|
}
|
|
next();
|
|
});
|
|
|
|
TestModel.upsertWithWhere({id: 'new-id'},
|
|
{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.upsertWithWhere({id: 'new-id'},
|
|
{id: 'new-id', name: 'new name'},
|
|
function(err, instance) {
|
|
(err || {}).should.be.instanceOf(ValidationError);
|
|
(err.details.codes || {}).should.eql({name: ['presence']});
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('validates model after `before save` hook on update', function(done) {
|
|
TestModel.observe('before save', invalidateTestModel());
|
|
|
|
TestModel.upsertWithWhere({id: existingInstance.id},
|
|
{id: existingInstance.id, name: 'updated 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', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.upsertWithWhere({id: 'new-id'},
|
|
{id: 'new-id', name: 'a name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
|
|
const expectedContext = aCtxForModel(TestModel, {
|
|
data: {id: 'new-id', name: 'a name'},
|
|
currentInstance: {
|
|
id: 'new-id',
|
|
name: 'a name',
|
|
extra: undefined,
|
|
},
|
|
});
|
|
if (dataSource.connector.upsertWithWhere) {
|
|
expectedContext.where = {id: 'new-id'};
|
|
} else {
|
|
expectedContext.isNewInstance = true;
|
|
}
|
|
|
|
ctxRecorder.records.should.eql(expectedContext);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers persist hook on update', function(done) {
|
|
TestModel.observe('persist', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.upsertWithWhere({id: existingInstance.id},
|
|
{id: existingInstance.id, name: 'updated name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
const expectedContext = aCtxForModel(TestModel, {
|
|
where: {id: existingInstance.id},
|
|
data: {
|
|
id: existingInstance.id,
|
|
name: 'updated name',
|
|
},
|
|
currentInstance: {
|
|
id: existingInstance.id,
|
|
name: 'updated name',
|
|
extra: undefined,
|
|
},
|
|
});
|
|
if (!dataSource.connector.upsertWithWhere) {
|
|
expectedContext.isNewInstance = false;
|
|
}
|
|
ctxRecorder.records.should.eql(expectedContext);
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `loaded` hook on create', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.upsertWithWhere({id: 'new-id'},
|
|
{id: 'new-id', name: 'a name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
data: {id: 'new-id', name: 'a name'},
|
|
isNewInstance: true,
|
|
}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `loaded` hook on update', function(done) {
|
|
TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.upsertWithWhere({id: existingInstance.id},
|
|
{id: existingInstance.id, name: 'updated name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
const expectedContext = aCtxForModel(TestModel, {
|
|
data: {
|
|
id: existingInstance.id,
|
|
name: 'updated name',
|
|
},
|
|
isNewInstance: false,
|
|
});
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, expectedContext));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('emits error when `loaded` hook fails', function(done) {
|
|
TestModel.observe('loaded', nextWithError(expectedError));
|
|
TestModel.upsertWithWhere({id: 'new-id'},
|
|
{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', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.upsertWithWhere({id: existingInstance.id},
|
|
{id: existingInstance.id, name: 'updated name'},
|
|
function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: existingInstance.id,
|
|
name: 'updated name',
|
|
extra: undefined,
|
|
},
|
|
isNewInstance: false,
|
|
}));
|
|
done();
|
|
});
|
|
});
|
|
|
|
it('triggers `after save` hook on create', function(done) {
|
|
TestModel.observe('after save', ctxRecorder.recordAndNext());
|
|
|
|
TestModel.upsertWithWhere({id: 'new-id'},
|
|
{id: 'new-id', name: 'a name'}, function(err, instance) {
|
|
if (err) return done(err);
|
|
ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
|
instance: {
|
|
id: instance.id,
|
|
name: 'a name',
|
|
extra: undefined,
|
|
},
|
|
isNewInstance: true,
|
|
}));
|
|
done();
|
|
});
|
|
});
|
|
});
|
|
|
|
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 findTestModels(query, cb) {
|
|
if (cb === undefined && typeof query === 'function') {
|
|
cb = query;
|
|
query = null;
|
|
}
|
|
|
|
TestModel.find(query, {notify: false}, cb);
|
|
}
|
|
|
|
function loadTestModel(id, cb) {
|
|
TestModel.findOne({where: {id: id}}, {notify: false}, cb);
|
|
}
|
|
|
|
function monitorHookExecution(hookNames) {
|
|
hookMonitor.install(TestModel, hookNames);
|
|
}
|
|
|
|
require('./operation-hooks.suite')(dataSource, should, connectorCapabilities);
|
|
});
|
|
|
|
function get(propertyName) {
|
|
return function(obj) {
|
|
return obj[propertyName];
|
|
};
|
|
}
|
|
|
|
function toObject(obj) {
|
|
return obj.toObject ? obj.toObject() : obj;
|
|
}
|
|
};
|