1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 | 'use strict';
|
7 | const ValidationError = require('../').ValidationError;
|
8 |
|
9 | const async = require('async');
|
10 | const contextTestHelpers = require('./helpers/context-test-helpers');
|
11 | const ContextRecorder = contextTestHelpers.ContextRecorder;
|
12 | const deepCloneToObject = contextTestHelpers.deepCloneToObject;
|
13 | const aCtxForModel = contextTestHelpers.aCtxForModel;
|
14 | const GeoPoint = require('../lib/geo.js').GeoPoint;
|
15 |
|
16 | const uid = require('./helpers/uid-generator');
|
17 | const getLastGeneratedUid = uid.last;
|
18 |
|
19 | const HookMonitor = require('./helpers/hook-monitor');
|
20 | let isNewInstanceFlag;
|
21 |
|
22 | module.exports = function(dataSource, should, connectorCapabilities) {
|
23 | isNewInstanceFlag = connectorCapabilities.replaceOrCreateReportsNewInstance;
|
24 | if (!connectorCapabilities) connectorCapabilities = {};
|
25 | if (isNewInstanceFlag === undefined) {
|
26 | const warn = 'The connector does not support a recently added feature:' +
|
27 | ' replaceOrCreateReportsNewInstance';
|
28 | console.warn(warn);
|
29 | }
|
30 | describe('Persistence hooks', function() {
|
31 | let ctxRecorder, hookMonitor, expectedError;
|
32 | let TestModel, existingInstance, GeoModel;
|
33 | let migrated = false;
|
34 |
|
35 | let undefinedValue = undefined;
|
36 |
|
37 | beforeEach(function setupDatabase(done) {
|
38 | ctxRecorder = new ContextRecorder('hook not called');
|
39 | hookMonitor = new HookMonitor({includeModelName: false});
|
40 | expectedError = new Error('test error');
|
41 |
|
42 | TestModel = dataSource.createModel('TestModel', {
|
43 |
|
44 | id: {type: String, id: true, generated: false, default: uid.next},
|
45 | name: {type: String, required: true},
|
46 | extra: {type: String, required: false},
|
47 | });
|
48 |
|
49 | GeoModel = dataSource.createModel('GeoModel', {
|
50 | id: {type: String, id: true, default: uid.next},
|
51 | name: {type: String, required: false},
|
52 | location: {type: GeoPoint, required: false},
|
53 | });
|
54 |
|
55 | uid.reset();
|
56 |
|
57 | if (migrated) {
|
58 | async.series([
|
59 | function(cb) {
|
60 | TestModel.deleteAll(cb);
|
61 | },
|
62 | function(cb) {
|
63 | GeoModel.deleteAll(cb);
|
64 | },
|
65 | ], done);
|
66 | } else {
|
67 | dataSource.automigrate([TestModel.modelName, 'GeoModel'], function(err) {
|
68 | migrated = true;
|
69 | done(err);
|
70 | });
|
71 | }
|
72 | });
|
73 |
|
74 | beforeEach(function createTestData(done) {
|
75 | TestModel.create({name: 'first'}, function(err, instance) {
|
76 | if (err) return done(err);
|
77 |
|
78 |
|
79 | TestModel.findById(instance.id, function(err, instance) {
|
80 | existingInstance = instance;
|
81 | undefinedValue = existingInstance.extra;
|
82 |
|
83 | TestModel.create({name: 'second'}, function(err) {
|
84 | if (err) return done(err);
|
85 | const location1 = new GeoPoint({lat: 10.2, lng: 6.7});
|
86 | const location2 = new GeoPoint({lat: 10.3, lng: 6.8});
|
87 | GeoModel.create([
|
88 | {name: 'Rome', location: location1},
|
89 | {name: 'Tokyo', location: location2},
|
90 | ], function(err) {
|
91 | done(err);
|
92 | });
|
93 | });
|
94 | });
|
95 | });
|
96 | });
|
97 |
|
98 | describe('PersistedModel.find', function() {
|
99 | it('triggers hooks in the correct order', function(done) {
|
100 | monitorHookExecution();
|
101 |
|
102 | TestModel.find(
|
103 | {where: {id: '1'}},
|
104 | function(err, list) {
|
105 | if (err) return done(err);
|
106 |
|
107 | hookMonitor.names.should.eql([
|
108 | 'access',
|
109 | 'loaded',
|
110 | ]);
|
111 | done();
|
112 | },
|
113 | );
|
114 | });
|
115 |
|
116 | it('triggers the loaded hook multiple times when multiple instances exist', function(done) {
|
117 | monitorHookExecution();
|
118 |
|
119 | TestModel.find(function(err, list) {
|
120 | if (err) return done(err);
|
121 |
|
122 | hookMonitor.names.should.eql([
|
123 | 'access',
|
124 | 'loaded',
|
125 | 'loaded',
|
126 | ]);
|
127 | done();
|
128 | });
|
129 | });
|
130 |
|
131 | it('should not trigger hooks, if notify is false', function(done) {
|
132 | monitorHookExecution();
|
133 | TestModel.find(
|
134 | {where: {id: '1'}},
|
135 | {notify: false},
|
136 | function(err, list) {
|
137 | if (err) return done(err);
|
138 | hookMonitor.names.should.be.empty();
|
139 | done();
|
140 | },
|
141 | );
|
142 | });
|
143 |
|
144 | it('triggers the loaded hook multiple times when multiple instances exist when near filter is used',
|
145 | function(done) {
|
146 | const hookMonitorGeoModel = new HookMonitor({includeModelName: false});
|
147 |
|
148 | function monitorHookExecutionGeoModel(hookNames) {
|
149 | hookMonitorGeoModel.install(GeoModel, hookNames);
|
150 | }
|
151 |
|
152 | monitorHookExecutionGeoModel();
|
153 |
|
154 | const query = {
|
155 | where: {location: {near: '10,5'}},
|
156 | };
|
157 | GeoModel.find(query, function(err, list) {
|
158 | if (err) return done(err);
|
159 |
|
160 | hookMonitorGeoModel.names.should.eql(['access', 'loaded', 'loaded']);
|
161 | done();
|
162 | });
|
163 | });
|
164 |
|
165 | it('applies updates from `loaded` hook when near filter is used', function(done) {
|
166 | GeoModel.observe('loaded', function(ctx, next) {
|
167 |
|
168 | ctx.data = Object.assign({}, ctx.data, {name: 'Berlin'});
|
169 | next();
|
170 | });
|
171 |
|
172 | const query = {
|
173 | where: {location: {near: '10,5'}},
|
174 | };
|
175 |
|
176 | GeoModel.find(query, function(err, list) {
|
177 | if (err) return done(err);
|
178 | list.map(get('name')).should.eql(['Berlin', 'Berlin']);
|
179 | done();
|
180 | });
|
181 | });
|
182 |
|
183 | it('applies updates to one specific instance from `loaded` hook when near filter is used',
|
184 | function(done) {
|
185 | GeoModel.observe('loaded', function(ctx, next) {
|
186 | if (ctx.data.name === 'Rome') {
|
187 |
|
188 | ctx.data = Object.assign({}, ctx.data, {name: 'Berlin'});
|
189 | }
|
190 | next();
|
191 | });
|
192 |
|
193 | const query = {
|
194 | where: {location: {near: '10,5'}},
|
195 | };
|
196 |
|
197 | GeoModel.find(query, function(err, list) {
|
198 | if (err) return done(err);
|
199 | list.map(get('name')).should.containEql('Berlin', 'Tokyo');
|
200 | done();
|
201 | });
|
202 | });
|
203 |
|
204 | it('applies updates from `loaded` hook when near filter is not used', function(done) {
|
205 | TestModel.observe('loaded', function(ctx, next) {
|
206 |
|
207 | ctx.data = Object.assign({}, ctx.data, {name: 'Paris'});
|
208 | next();
|
209 | });
|
210 |
|
211 | TestModel.find(function(err, list) {
|
212 | if (err) return done(err);
|
213 | list.map(get('name')).should.eql(['Paris', 'Paris']);
|
214 | done();
|
215 | });
|
216 | });
|
217 |
|
218 | it('applies updates to one specific instance from `loaded` hook when near filter is not used',
|
219 | function(done) {
|
220 | TestModel.observe('loaded', function(ctx, next) {
|
221 | if (ctx.data.name === 'first') {
|
222 |
|
223 | ctx.data = Object.assign({}, ctx.data, {name: 'Paris'});
|
224 | }
|
225 | next();
|
226 | });
|
227 |
|
228 | TestModel.find(function(err, list) {
|
229 | if (err) return done(err);
|
230 | list.map(get('name')).should.eql(['Paris', 'second']);
|
231 | done();
|
232 | });
|
233 | });
|
234 |
|
235 | it('should not trigger hooks for geo queries, if notify is false',
|
236 | function(done) {
|
237 | monitorHookExecution();
|
238 |
|
239 | TestModel.find(
|
240 | {where: {geo: {near: '10,20'}}},
|
241 | {notify: false},
|
242 | function(err, list) {
|
243 | if (err) return done(err);
|
244 | hookMonitor.names.should.be.empty();
|
245 | done();
|
246 | },
|
247 | );
|
248 | });
|
249 |
|
250 | it('should apply updates from `access` hook', function(done) {
|
251 | TestModel.observe('access', function(ctx, next) {
|
252 | ctx.query = {where: {name: 'second'}};
|
253 | next();
|
254 | });
|
255 |
|
256 | TestModel.find({name: 'first'}, function(err, list) {
|
257 | if (err) return done(err);
|
258 | list.map(get('name')).should.eql(['second']);
|
259 | done();
|
260 | });
|
261 | });
|
262 |
|
263 | it('triggers `access` hook', function(done) {
|
264 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
265 |
|
266 | TestModel.find({where: {id: '1'}}, function(err, list) {
|
267 | if (err) return done(err);
|
268 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
269 | query: {where: {id: '1'}},
|
270 | }));
|
271 | done();
|
272 | });
|
273 | });
|
274 |
|
275 | it('aborts when `access` hook fails', function(done) {
|
276 | TestModel.observe('access', nextWithError(expectedError));
|
277 |
|
278 | TestModel.find(function(err, list) {
|
279 | [err].should.eql([expectedError]);
|
280 | done();
|
281 | });
|
282 | });
|
283 |
|
284 | it('applies updates from `access` hook', function(done) {
|
285 | TestModel.observe('access', function(ctx, next) {
|
286 | ctx.query = {where: {id: existingInstance.id}};
|
287 | next();
|
288 | });
|
289 |
|
290 | TestModel.find(function(err, list) {
|
291 | if (err) return done(err);
|
292 | list.map(get('name')).should.eql([existingInstance.name]);
|
293 | done();
|
294 | });
|
295 | });
|
296 |
|
297 | it('triggers `access` hook for geo queries', function(done) {
|
298 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
299 |
|
300 | TestModel.find({where: {geo: {near: '10,20'}}}, function(err, list) {
|
301 | if (err) return done(err);
|
302 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
303 | query: {where: {geo: {near: '10,20'}}},
|
304 | }));
|
305 | done();
|
306 | });
|
307 | });
|
308 |
|
309 | it('applies updates from `access` hook for geo queries', function(done) {
|
310 | TestModel.observe('access', function(ctx, next) {
|
311 | ctx.query = {where: {id: existingInstance.id}};
|
312 | next();
|
313 | });
|
314 |
|
315 | TestModel.find({where: {geo: {near: '10,20'}}}, function(err, list) {
|
316 | if (err) return done(err);
|
317 | list.map(get('name')).should.eql([existingInstance.name]);
|
318 | done();
|
319 | });
|
320 | });
|
321 |
|
322 | it('applies updates from `loaded` hook', function(done) {
|
323 | TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
324 |
|
325 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
326 | }));
|
327 |
|
328 | TestModel.find(
|
329 | {where: {id: 1}},
|
330 | function(err, list) {
|
331 | if (err) return done(err);
|
332 |
|
333 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
334 | data: {
|
335 | id: '1',
|
336 | name: 'first',
|
337 | extra: 'hook data',
|
338 | },
|
339 | isNewInstance: false,
|
340 | options: {},
|
341 | }));
|
342 |
|
343 | list[0].should.have.property('extra', 'hook data');
|
344 | done();
|
345 | },
|
346 | );
|
347 | });
|
348 |
|
349 | it('emits error when `loaded` hook fails', function(done) {
|
350 | TestModel.observe('loaded', nextWithError(expectedError));
|
351 | TestModel.find(
|
352 | {where: {id: 1}},
|
353 | function(err, list) {
|
354 | [err].should.eql([expectedError]);
|
355 | done();
|
356 | },
|
357 | );
|
358 | });
|
359 | });
|
360 |
|
361 | describe('PersistedModel.create', function() {
|
362 | it('triggers hooks in the correct order', function(done) {
|
363 | monitorHookExecution();
|
364 |
|
365 | TestModel.create(
|
366 | {name: 'created'},
|
367 | function(err, record, created) {
|
368 | if (err) return done(err);
|
369 |
|
370 | hookMonitor.names.should.eql([
|
371 | 'before save',
|
372 | 'persist',
|
373 | 'loaded',
|
374 | 'after save',
|
375 | ]);
|
376 | done();
|
377 | },
|
378 | );
|
379 | });
|
380 |
|
381 | it('aborts when `after save` fires when option to notify is false', function(done) {
|
382 | monitorHookExecution();
|
383 |
|
384 | TestModel.create({name: 'created'}, {notify: false}, function(err, record, created) {
|
385 | if (err) return done(err);
|
386 |
|
387 | hookMonitor.names.should.not.containEql('after save');
|
388 | done();
|
389 | });
|
390 | });
|
391 |
|
392 | it('triggers `before save` hook', function(done) {
|
393 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
394 |
|
395 | TestModel.create({name: 'created'}, function(err, instance) {
|
396 | if (err) return done(err);
|
397 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
398 | instance: {
|
399 | id: instance.id,
|
400 | name: 'created',
|
401 | extra: undefined,
|
402 | },
|
403 | isNewInstance: true,
|
404 | }));
|
405 | done();
|
406 | });
|
407 | });
|
408 |
|
409 | it('aborts when `before save` hook fails', function(done) {
|
410 | TestModel.observe('before save', nextWithError(expectedError));
|
411 |
|
412 | TestModel.create({name: 'created'}, function(err, instance) {
|
413 | [err].should.eql([expectedError]);
|
414 | done();
|
415 | });
|
416 | });
|
417 |
|
418 | it('applies updates from `before save` hook', function(done) {
|
419 | TestModel.observe('before save', function(ctx, next) {
|
420 | ctx.instance.should.be.instanceOf(TestModel);
|
421 | ctx.instance.extra = 'hook data';
|
422 | next();
|
423 | });
|
424 |
|
425 | TestModel.create({id: uid.next(), name: 'a-name'}, function(err, instance) {
|
426 | if (err) return done(err);
|
427 | instance.should.have.property('extra', 'hook data');
|
428 | done();
|
429 | });
|
430 | });
|
431 |
|
432 | it('sends `before save` for each model in an array', function(done) {
|
433 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
434 |
|
435 | TestModel.create(
|
436 | [{name: '1'}, {name: '2'}],
|
437 | function(err, list) {
|
438 | if (err) return done(err);
|
439 |
|
440 | ctxRecorder.records.sort(function(c1, c2) {
|
441 | return c1.instance.name - c2.instance.name;
|
442 | });
|
443 | ctxRecorder.records.should.eql([
|
444 | aCtxForModel(TestModel, {
|
445 | instance: {id: list[0].id, name: '1', extra: undefined},
|
446 | isNewInstance: true,
|
447 | }),
|
448 | aCtxForModel(TestModel, {
|
449 | instance: {id: list[1].id, name: '2', extra: undefined},
|
450 | isNewInstance: true,
|
451 | }),
|
452 | ]);
|
453 | done();
|
454 | },
|
455 | );
|
456 | });
|
457 |
|
458 | it('validates model after `before save` hook', function(done) {
|
459 | TestModel.observe('before save', invalidateTestModel());
|
460 |
|
461 | TestModel.create({name: 'created'}, function(err) {
|
462 | (err || {}).should.be.instanceOf(ValidationError);
|
463 | (err.details.codes || {}).should.eql({name: ['presence']});
|
464 | done();
|
465 | });
|
466 | });
|
467 |
|
468 | it('triggers `persist` hook', function(done) {
|
469 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
470 |
|
471 | TestModel.create(
|
472 | {id: 'new-id', name: 'a name'},
|
473 | function(err, instance) {
|
474 | if (err) return done(err);
|
475 |
|
476 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
477 | data: {id: 'new-id', name: 'a name'},
|
478 | isNewInstance: true,
|
479 | currentInstance: {extra: null, id: 'new-id', name: 'a name'},
|
480 | }));
|
481 |
|
482 | done();
|
483 | },
|
484 | );
|
485 | });
|
486 |
|
487 | it('applies updates from `persist` hook', function(done) {
|
488 | TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
489 |
|
490 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
491 | }));
|
492 |
|
493 |
|
494 |
|
495 |
|
496 |
|
497 | TestModel.settings.updateOnLoad = true;
|
498 | TestModel.create(
|
499 | {id: 'new-id', name: 'a name'},
|
500 | function(err, instance) {
|
501 | if (err) return done(err);
|
502 |
|
503 | instance.should.have.property('extra', 'hook data');
|
504 |
|
505 |
|
506 |
|
507 | TestModel.findById('new-id', function(err, dbInstance) {
|
508 | if (err) return done(err);
|
509 | should.exists(dbInstance);
|
510 | dbInstance.toObject(true).should.eql({
|
511 | id: 'new-id',
|
512 | name: 'a name',
|
513 | extra: 'hook data',
|
514 | });
|
515 | done();
|
516 | });
|
517 | },
|
518 | );
|
519 | });
|
520 |
|
521 | it('triggers `loaded` hook', function(done) {
|
522 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
523 |
|
524 |
|
525 |
|
526 |
|
527 |
|
528 | TestModel.settings.updateOnLoad = true;
|
529 | TestModel.create(
|
530 | {id: 'new-id', name: 'a name'},
|
531 | function(err, instance) {
|
532 | if (err) return done(err);
|
533 |
|
534 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
535 | data: {id: 'new-id', name: 'a name'},
|
536 | isNewInstance: true,
|
537 | }));
|
538 |
|
539 | done();
|
540 | },
|
541 | );
|
542 | });
|
543 |
|
544 | it('emits error when `loaded` hook fails', function(done) {
|
545 | TestModel.observe('loaded', nextWithError(expectedError));
|
546 | TestModel.create(
|
547 | {id: 'new-id', name: 'a name'},
|
548 | function(err, instance) {
|
549 | [err].should.eql([expectedError]);
|
550 | done();
|
551 | },
|
552 | );
|
553 | });
|
554 |
|
555 | it('applies updates from `loaded` hook', function(done) {
|
556 | TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
557 |
|
558 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
559 | }));
|
560 |
|
561 |
|
562 |
|
563 |
|
564 |
|
565 | TestModel.settings.updateOnLoad = true;
|
566 | TestModel.create(
|
567 | {id: 'new-id', name: 'a name'},
|
568 | function(err, instance) {
|
569 | if (err) return done(err);
|
570 |
|
571 | instance.should.have.property('extra', 'hook data');
|
572 | done();
|
573 | },
|
574 | );
|
575 | });
|
576 |
|
577 | it('triggers `after save` hook', function(done) {
|
578 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
579 |
|
580 | TestModel.create({name: 'created'}, function(err, instance) {
|
581 | if (err) return done(err);
|
582 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
583 | instance: {
|
584 | id: instance.id,
|
585 | name: 'created',
|
586 | extra: undefined,
|
587 | },
|
588 | isNewInstance: true,
|
589 | }));
|
590 | done();
|
591 | });
|
592 | });
|
593 |
|
594 | it('aborts when `after save` hook fails', function(done) {
|
595 | TestModel.observe('after save', nextWithError(expectedError));
|
596 |
|
597 | TestModel.create({name: 'created'}, function(err, instance) {
|
598 | [err].should.eql([expectedError]);
|
599 | done();
|
600 | });
|
601 | });
|
602 |
|
603 | it('applies updates from `after save` hook', function(done) {
|
604 | TestModel.observe('after save', function(ctx, next) {
|
605 | ctx.instance.should.be.instanceOf(TestModel);
|
606 | ctx.instance.extra = 'hook data';
|
607 | next();
|
608 | });
|
609 |
|
610 | TestModel.create({name: 'a-name'}, function(err, instance) {
|
611 | if (err) return done(err);
|
612 | instance.should.have.property('extra', 'hook data');
|
613 | done();
|
614 | });
|
615 | });
|
616 |
|
617 | it('sends `after save` for each model in an array', function(done) {
|
618 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
619 |
|
620 | TestModel.create(
|
621 | [{name: '1'}, {name: '2'}],
|
622 | function(err, list) {
|
623 | if (err) return done(err);
|
624 |
|
625 | ctxRecorder.records.sort(function(c1, c2) {
|
626 | return c1.instance.name - c2.instance.name;
|
627 | });
|
628 | ctxRecorder.records.should.eql([
|
629 | aCtxForModel(TestModel, {
|
630 | instance: {id: list[0].id, name: '1', extra: undefined},
|
631 | isNewInstance: true,
|
632 | }),
|
633 | aCtxForModel(TestModel, {
|
634 | instance: {id: list[1].id, name: '2', extra: undefined},
|
635 | isNewInstance: true,
|
636 | }),
|
637 | ]);
|
638 | done();
|
639 | },
|
640 | );
|
641 | });
|
642 |
|
643 | it('emits `after save` when some models were not saved', function(done) {
|
644 | TestModel.observe('before save', function(ctx, next) {
|
645 | if (ctx.instance.name === 'fail')
|
646 | next(expectedError);
|
647 | else
|
648 | next();
|
649 | });
|
650 |
|
651 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
652 |
|
653 | TestModel.create(
|
654 | [{name: 'ok'}, {name: 'fail'}],
|
655 | function(err, list) {
|
656 | (err || []).should.have.length(2);
|
657 | err[1].should.eql(expectedError);
|
658 |
|
659 |
|
660 |
|
661 |
|
662 | list.map(get('name')).should.eql(['ok', 'fail']);
|
663 |
|
664 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
665 | instance: {id: list[0].id, name: 'ok', extra: undefined},
|
666 | isNewInstance: true,
|
667 | }));
|
668 | done();
|
669 | },
|
670 | );
|
671 | });
|
672 | });
|
673 |
|
674 | describe('PersistedModel.findOrCreate', function() {
|
675 | it('triggers `access` hook', function(done) {
|
676 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
677 |
|
678 | TestModel.findOrCreate(
|
679 | {where: {name: 'new-record'}},
|
680 | {name: 'new-record'},
|
681 | function(err, record, created) {
|
682 | if (err) return done(err);
|
683 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
684 | where: {name: 'new-record'},
|
685 | limit: 1,
|
686 | offset: 0,
|
687 | skip: 0,
|
688 | }}));
|
689 | done();
|
690 | },
|
691 | );
|
692 | });
|
693 |
|
694 | if (dataSource.connector.findOrCreate) {
|
695 | it('triggers `before save` hook when found', function(done) {
|
696 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
697 |
|
698 | TestModel.findOrCreate(
|
699 | {where: {name: existingInstance.name}},
|
700 | {name: existingInstance.name},
|
701 | function(err, record, created) {
|
702 | if (err) return done(err);
|
703 | record.id.should.eql(existingInstance.id);
|
704 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
705 | instance: {
|
706 | id: getLastGeneratedUid(),
|
707 | name: existingInstance.name,
|
708 | extra: undefined,
|
709 | },
|
710 | isNewInstance: true,
|
711 | }));
|
712 | done();
|
713 | },
|
714 | );
|
715 | });
|
716 | }
|
717 |
|
718 | it('triggers `before save` hook when not found', function(done) {
|
719 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
720 |
|
721 | TestModel.findOrCreate(
|
722 | {where: {name: 'new-record'}},
|
723 | {name: 'new-record'},
|
724 | function(err, record, created) {
|
725 | if (err) return done(err);
|
726 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
727 | instance: {
|
728 | id: record.id,
|
729 | name: 'new-record',
|
730 | extra: undefined,
|
731 | },
|
732 | isNewInstance: true,
|
733 | }));
|
734 | done();
|
735 | },
|
736 | );
|
737 | });
|
738 |
|
739 | it('validates model after `before save` hook', function(done) {
|
740 | TestModel.observe('before save', invalidateTestModel());
|
741 |
|
742 | TestModel.findOrCreate(
|
743 | {where: {name: 'new-record'}},
|
744 | {name: 'new-record'},
|
745 | function(err) {
|
746 | (err || {}).should.be.instanceOf(ValidationError);
|
747 | (err.details.codes || {}).should.eql({name: ['presence']});
|
748 | done();
|
749 | },
|
750 | );
|
751 | });
|
752 |
|
753 | it('triggers hooks in the correct order when not found', function(done) {
|
754 | monitorHookExecution();
|
755 |
|
756 | TestModel.findOrCreate(
|
757 | {where: {name: 'new-record'}},
|
758 | {name: 'new-record'},
|
759 | function(err, record, created) {
|
760 | if (err) return done(err);
|
761 | hookMonitor.names.should.eql([
|
762 | 'access',
|
763 | 'before save',
|
764 | 'persist',
|
765 | 'loaded',
|
766 | 'after save',
|
767 | ]);
|
768 | done();
|
769 | },
|
770 | );
|
771 | });
|
772 |
|
773 | it('triggers hooks in the correct order when found', function(done) {
|
774 | monitorHookExecution();
|
775 |
|
776 | TestModel.findOrCreate(
|
777 | {where: {name: existingInstance.name}},
|
778 | {name: existingInstance.name},
|
779 | function(err, record, created) {
|
780 | if (err) return done(err);
|
781 |
|
782 | if (dataSource.connector.findOrCreate) {
|
783 | hookMonitor.names.should.eql([
|
784 | 'access',
|
785 | 'before save',
|
786 | 'persist',
|
787 | 'loaded',
|
788 | ]);
|
789 | } else {
|
790 | hookMonitor.names.should.eql([
|
791 | 'access',
|
792 | 'loaded',
|
793 | ]);
|
794 | }
|
795 | done();
|
796 | },
|
797 | );
|
798 | });
|
799 |
|
800 | it('aborts when `access` hook fails', function(done) {
|
801 | TestModel.observe('access', nextWithError(expectedError));
|
802 |
|
803 | TestModel.findOrCreate(
|
804 | {where: {id: 'does-not-exist'}},
|
805 | {name: 'does-not-exist'},
|
806 | function(err, instance) {
|
807 | [err].should.eql([expectedError]);
|
808 | done();
|
809 | },
|
810 | );
|
811 | });
|
812 |
|
813 | it('aborts when `before save` hook fails', function(done) {
|
814 | TestModel.observe('before save', nextWithError(expectedError));
|
815 |
|
816 | TestModel.findOrCreate(
|
817 | {where: {id: 'does-not-exist'}},
|
818 | {name: 'does-not-exist'},
|
819 | function(err, instance) {
|
820 | [err].should.eql([expectedError]);
|
821 | done();
|
822 | },
|
823 | );
|
824 | });
|
825 |
|
826 | if (dataSource.connector.findOrCreate) {
|
827 | it('triggers `persist` hook when found', function(done) {
|
828 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
829 |
|
830 | TestModel.findOrCreate(
|
831 | {where: {name: existingInstance.name}},
|
832 | {name: existingInstance.name},
|
833 | function(err, record, created) {
|
834 | if (err) return done(err);
|
835 |
|
836 | record.id.should.eql(existingInstance.id);
|
837 |
|
838 |
|
839 |
|
840 |
|
841 |
|
842 |
|
843 |
|
844 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
845 | data: {
|
846 | id: getLastGeneratedUid(),
|
847 | name: existingInstance.name,
|
848 | },
|
849 | isNewInstance: true,
|
850 | currentInstance: {
|
851 | id: getLastGeneratedUid(),
|
852 | name: record.name,
|
853 | extra: null,
|
854 | },
|
855 | where: {name: existingInstance.name},
|
856 | }));
|
857 |
|
858 | done();
|
859 | },
|
860 | );
|
861 | });
|
862 | }
|
863 |
|
864 | it('triggers `persist` hook when not found', function(done) {
|
865 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
866 |
|
867 | TestModel.findOrCreate(
|
868 | {where: {name: 'new-record'}},
|
869 | {name: 'new-record'},
|
870 | function(err, record, created) {
|
871 | if (err) return done(err);
|
872 |
|
873 |
|
874 |
|
875 | if (dataSource.connector.findOrCreate) {
|
876 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
877 | data: {
|
878 | id: record.id,
|
879 | name: 'new-record',
|
880 | },
|
881 | isNewInstance: true,
|
882 | currentInstance: {
|
883 | id: record.id,
|
884 | name: record.name,
|
885 | extra: null,
|
886 | },
|
887 | where: {name: 'new-record'},
|
888 | }));
|
889 | } else {
|
890 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
891 | data: {
|
892 | id: record.id,
|
893 | name: 'new-record',
|
894 | },
|
895 | isNewInstance: true,
|
896 | currentInstance: {id: record.id, name: record.name, extra: null},
|
897 | }));
|
898 | }
|
899 | done();
|
900 | },
|
901 | );
|
902 | });
|
903 |
|
904 | if (dataSource.connector.findOrCreate) {
|
905 | it('applies updates from `persist` hook when found', function(done) {
|
906 | TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
907 |
|
908 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
909 | }));
|
910 |
|
911 | TestModel.findOrCreate(
|
912 | {where: {name: existingInstance.name}},
|
913 | {name: existingInstance.name},
|
914 | function(err, instance) {
|
915 | if (err) return done(err);
|
916 |
|
917 |
|
918 |
|
919 | instance.should.not.have.property('extra', 'hook data');
|
920 |
|
921 |
|
922 |
|
923 |
|
924 | TestModel.findById(existingInstance.id, function(err, dbInstance) {
|
925 | if (err) return done(err);
|
926 | should.exists(dbInstance);
|
927 | dbInstance.toObject(true).should.eql({
|
928 | id: existingInstance.id,
|
929 | name: existingInstance.name,
|
930 | extra: undefined,
|
931 | });
|
932 | });
|
933 |
|
934 | done();
|
935 | },
|
936 | );
|
937 | });
|
938 | }
|
939 |
|
940 | it('applies updates from `persist` hook when not found', function(done) {
|
941 | TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
942 |
|
943 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
944 | }));
|
945 |
|
946 | TestModel.findOrCreate(
|
947 | {where: {name: 'new-record'}},
|
948 | {name: 'new-record'},
|
949 | function(err, instance) {
|
950 | if (err) return done(err);
|
951 |
|
952 | if (dataSource.connector.findOrCreate) {
|
953 | instance.should.have.property('extra', 'hook data');
|
954 | } else {
|
955 |
|
956 |
|
957 |
|
958 |
|
959 |
|
960 |
|
961 |
|
962 |
|
963 | instance.should.not.have.property('extra', 'hook data');
|
964 | TestModel.findById(instance.id, function(err, dbInstance) {
|
965 | if (err) return done(err);
|
966 | should.exists(dbInstance);
|
967 | dbInstance.toObject(true).should.eql({
|
968 | id: instance.id,
|
969 | name: instance.name,
|
970 | extra: 'hook data',
|
971 | });
|
972 | });
|
973 | }
|
974 | done();
|
975 | },
|
976 | );
|
977 | });
|
978 |
|
979 | if (dataSource.connector.findOrCreate) {
|
980 | it('triggers `loaded` hook when found', function(done) {
|
981 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
982 |
|
983 | TestModel.findOrCreate(
|
984 | {where: {name: existingInstance.name}},
|
985 | {name: existingInstance.name},
|
986 | function(err, record, created) {
|
987 | if (err) return done(err);
|
988 |
|
989 | record.id.should.eql(existingInstance.id);
|
990 |
|
991 |
|
992 |
|
993 |
|
994 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
995 | data: {
|
996 | id: existingInstance.id,
|
997 | name: existingInstance.name,
|
998 | },
|
999 | isNewInstance: false,
|
1000 | }));
|
1001 |
|
1002 | done();
|
1003 | },
|
1004 | );
|
1005 | });
|
1006 | }
|
1007 |
|
1008 | it('triggers `loaded` hook when not found', function(done) {
|
1009 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
1010 |
|
1011 | TestModel.findOrCreate(
|
1012 | {where: {name: 'new-record'}},
|
1013 | {name: 'new-record'},
|
1014 | function(err, record, created) {
|
1015 | if (err) return done(err);
|
1016 |
|
1017 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1018 | data: {
|
1019 | id: record.id,
|
1020 | name: 'new-record',
|
1021 | },
|
1022 | isNewInstance: true,
|
1023 | }));
|
1024 |
|
1025 | done();
|
1026 | },
|
1027 | );
|
1028 | });
|
1029 |
|
1030 | it('emits error when `loaded` hook fails', function(done) {
|
1031 | TestModel.observe('loaded', nextWithError(expectedError));
|
1032 | TestModel.findOrCreate(
|
1033 | {where: {name: 'new-record'}},
|
1034 | {name: 'new-record'},
|
1035 | function(err, instance) {
|
1036 | [err].should.eql([expectedError]);
|
1037 | done();
|
1038 | },
|
1039 | );
|
1040 | });
|
1041 |
|
1042 | if (dataSource.connector.findOrCreate) {
|
1043 | it('applies updates from `loaded` hook when found', function(done) {
|
1044 | TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
1045 |
|
1046 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
1047 | }));
|
1048 |
|
1049 | TestModel.findOrCreate(
|
1050 | {where: {name: existingInstance.name}},
|
1051 | {name: existingInstance.name},
|
1052 | function(err, instance) {
|
1053 | if (err) return done(err);
|
1054 |
|
1055 | instance.should.have.property('extra', 'hook data');
|
1056 |
|
1057 | done();
|
1058 | },
|
1059 | );
|
1060 | });
|
1061 | }
|
1062 |
|
1063 | it('applies updates from `loaded` hook when not found', function(done) {
|
1064 | TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
1065 |
|
1066 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
1067 | }));
|
1068 |
|
1069 |
|
1070 |
|
1071 |
|
1072 |
|
1073 |
|
1074 |
|
1075 |
|
1076 | TestModel.settings.updateOnLoad = true;
|
1077 | TestModel.findOrCreate(
|
1078 | {where: {name: 'new-record'}},
|
1079 | {name: 'new-record'},
|
1080 | function(err, instance) {
|
1081 | if (err) return done(err);
|
1082 |
|
1083 | instance.should.have.property('extra', 'hook data');
|
1084 | done();
|
1085 | },
|
1086 | );
|
1087 | });
|
1088 |
|
1089 | it('triggers `after save` hook when not found', function(done) {
|
1090 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
1091 |
|
1092 | TestModel.findOrCreate(
|
1093 | {where: {name: 'new name'}},
|
1094 | {name: 'new name'},
|
1095 | function(err, instance) {
|
1096 | if (err) return done(err);
|
1097 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1098 | instance: {
|
1099 | id: instance.id,
|
1100 | name: 'new name',
|
1101 | extra: undefined,
|
1102 | },
|
1103 | isNewInstance: true,
|
1104 | }));
|
1105 | done();
|
1106 | },
|
1107 | );
|
1108 | });
|
1109 |
|
1110 | it('does not trigger `after save` hook when found', function(done) {
|
1111 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
1112 |
|
1113 | TestModel.findOrCreate(
|
1114 | {where: {id: existingInstance.id}},
|
1115 | {name: existingInstance.name},
|
1116 | function(err, instance) {
|
1117 | if (err) return done(err);
|
1118 | ctxRecorder.records.should.eql('hook not called');
|
1119 | done();
|
1120 | },
|
1121 | );
|
1122 | });
|
1123 | });
|
1124 |
|
1125 | describe('PersistedModel.count', function(done) {
|
1126 | it('triggers `access` hook', function(done) {
|
1127 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
1128 |
|
1129 | TestModel.count({id: existingInstance.id}, function(err, count) {
|
1130 | if (err) return done(err);
|
1131 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
1132 | where: {id: existingInstance.id},
|
1133 | }}));
|
1134 | done();
|
1135 | });
|
1136 | });
|
1137 |
|
1138 | it('applies updates from `access` hook', function(done) {
|
1139 | TestModel.observe('access', function(ctx, next) {
|
1140 | ctx.query.where = {id: existingInstance.id};
|
1141 | next();
|
1142 | });
|
1143 |
|
1144 | TestModel.count(function(err, count) {
|
1145 | if (err) return done(err);
|
1146 | count.should.equal(1);
|
1147 | done();
|
1148 | });
|
1149 | });
|
1150 | });
|
1151 |
|
1152 | describe('PersistedModel.prototype.save', function() {
|
1153 | it('triggers hooks in the correct order', function(done) {
|
1154 | monitorHookExecution();
|
1155 |
|
1156 | existingInstance.save(
|
1157 | function(err, record, created) {
|
1158 | if (err) return done(err);
|
1159 | hookMonitor.names.should.eql([
|
1160 | 'before save',
|
1161 | 'persist',
|
1162 | 'loaded',
|
1163 | 'after save',
|
1164 | ]);
|
1165 | done();
|
1166 | },
|
1167 | );
|
1168 | });
|
1169 |
|
1170 | it('triggers `before save` hook', function(done) {
|
1171 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
1172 |
|
1173 | existingInstance.name = 'changed';
|
1174 | existingInstance.save(function(err, instance) {
|
1175 | if (err) return done(err);
|
1176 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {instance: {
|
1177 | id: existingInstance.id,
|
1178 | name: 'changed',
|
1179 | extra: undefined,
|
1180 | }, options: {throws: false, validate: true}}));
|
1181 | done();
|
1182 | });
|
1183 | });
|
1184 |
|
1185 | it('aborts when `before save` hook fails', function(done) {
|
1186 | TestModel.observe('before save', nextWithError(expectedError));
|
1187 |
|
1188 | existingInstance.save(function(err, instance) {
|
1189 | [err].should.eql([expectedError]);
|
1190 | done();
|
1191 | });
|
1192 | });
|
1193 |
|
1194 | it('applies updates from `before save` hook', function(done) {
|
1195 | TestModel.observe('before save', function(ctx, next) {
|
1196 | ctx.instance.should.be.instanceOf(TestModel);
|
1197 | ctx.instance.extra = 'hook data';
|
1198 | next();
|
1199 | });
|
1200 |
|
1201 | existingInstance.save(function(err, instance) {
|
1202 | if (err) return done(err);
|
1203 | instance.should.have.property('extra', 'hook data');
|
1204 | done();
|
1205 | });
|
1206 | });
|
1207 |
|
1208 | it('validates model after `before save` hook', function(done) {
|
1209 | TestModel.observe('before save', invalidateTestModel());
|
1210 |
|
1211 | existingInstance.save(function(err) {
|
1212 | (err || {}).should.be.instanceOf(ValidationError);
|
1213 | (err.details.codes || {}).should.eql({name: ['presence']});
|
1214 | done();
|
1215 | });
|
1216 | });
|
1217 |
|
1218 | it('triggers `persist` hook', function(done) {
|
1219 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
1220 |
|
1221 | existingInstance.name = 'changed';
|
1222 | existingInstance.save(function(err, instance) {
|
1223 | if (err) return done(err);
|
1224 |
|
1225 |
|
1226 | delete ctxRecorder.records.data.extra;
|
1227 | delete ctxRecorder.records.currentInstance.extra;
|
1228 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1229 | data: {
|
1230 | id: existingInstance.id,
|
1231 | name: 'changed',
|
1232 | },
|
1233 | currentInstance: {
|
1234 | id: existingInstance.id,
|
1235 | name: 'changed',
|
1236 | },
|
1237 | where: {id: existingInstance.id},
|
1238 | options: {throws: false, validate: true},
|
1239 | }));
|
1240 |
|
1241 | done();
|
1242 | });
|
1243 | });
|
1244 |
|
1245 | it('applies updates from `persist` hook', function(done) {
|
1246 | TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
1247 |
|
1248 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
1249 | }));
|
1250 |
|
1251 | existingInstance.save(function(err, instance) {
|
1252 | if (err) return done(err);
|
1253 | instance.should.have.property('extra', 'hook data');
|
1254 | done();
|
1255 | });
|
1256 | });
|
1257 |
|
1258 | it('triggers `loaded` hook', function(done) {
|
1259 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
1260 |
|
1261 | existingInstance.extra = 'changed';
|
1262 | existingInstance.save(function(err, instance) {
|
1263 | if (err) return done(err);
|
1264 |
|
1265 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1266 | data: {
|
1267 | id: existingInstance.id,
|
1268 | name: existingInstance.name,
|
1269 | extra: 'changed',
|
1270 | },
|
1271 | isNewInstance: isNewInstanceFlag ? false : undefined,
|
1272 | options: {throws: false, validate: true},
|
1273 | }));
|
1274 |
|
1275 | done();
|
1276 | });
|
1277 | });
|
1278 |
|
1279 | it('emits error when `loaded` hook fails', function(done) {
|
1280 | TestModel.observe('loaded', nextWithError(expectedError));
|
1281 | existingInstance.save(
|
1282 | function(err, instance) {
|
1283 | [err].should.eql([expectedError]);
|
1284 | done();
|
1285 | },
|
1286 | );
|
1287 | });
|
1288 |
|
1289 | it('applies updates from `loaded` hook', function(done) {
|
1290 | TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
1291 |
|
1292 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
1293 | }));
|
1294 |
|
1295 | existingInstance.save(function(err, instance) {
|
1296 | if (err) return done(err);
|
1297 | instance.should.have.property('extra', 'hook data');
|
1298 | done();
|
1299 | });
|
1300 | });
|
1301 |
|
1302 | it('triggers `after save` hook on update', function(done) {
|
1303 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
1304 |
|
1305 | existingInstance.name = 'changed';
|
1306 | existingInstance.save(function(err, instance) {
|
1307 | if (err) return done(err);
|
1308 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1309 | instance: {
|
1310 | id: existingInstance.id,
|
1311 | name: 'changed',
|
1312 | extra: undefined,
|
1313 | },
|
1314 | isNewInstance: isNewInstanceFlag ? false : undefined,
|
1315 | options: {throws: false, validate: true},
|
1316 | }));
|
1317 | done();
|
1318 | });
|
1319 | });
|
1320 |
|
1321 | it('triggers `after save` hook on create', function(done) {
|
1322 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
1323 |
|
1324 |
|
1325 |
|
1326 |
|
1327 | const instance = new TestModel(
|
1328 | {id: 'new-id', name: 'created'},
|
1329 | {persisted: true},
|
1330 | );
|
1331 |
|
1332 | instance.save(function(err, instance) {
|
1333 | if (err) return done(err);
|
1334 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1335 | instance: {
|
1336 | id: instance.id,
|
1337 | name: 'created',
|
1338 | extra: undefined,
|
1339 | },
|
1340 | isNewInstance: isNewInstanceFlag ? true : undefined,
|
1341 | options: {throws: false, validate: true},
|
1342 | }));
|
1343 | done();
|
1344 | });
|
1345 | });
|
1346 |
|
1347 | it('aborts when `after save` hook fails', function(done) {
|
1348 | TestModel.observe('after save', nextWithError(expectedError));
|
1349 |
|
1350 | existingInstance.save(function(err, instance) {
|
1351 | [err].should.eql([expectedError]);
|
1352 | done();
|
1353 | });
|
1354 | });
|
1355 |
|
1356 | it('applies updates from `after save` hook', function(done) {
|
1357 | TestModel.observe('after save', function(ctx, next) {
|
1358 | ctx.instance.should.be.instanceOf(TestModel);
|
1359 | ctx.instance.extra = 'hook data';
|
1360 | next();
|
1361 | });
|
1362 |
|
1363 | existingInstance.save(function(err, instance) {
|
1364 | if (err) return done(err);
|
1365 | instance.should.have.property('extra', 'hook data');
|
1366 | done();
|
1367 | });
|
1368 | });
|
1369 | });
|
1370 |
|
1371 | describe('PersistedModel.prototype.updateAttributes', function() {
|
1372 | it('triggers hooks in the correct order', function(done) {
|
1373 | monitorHookExecution();
|
1374 |
|
1375 | existingInstance.updateAttributes(
|
1376 | {name: 'changed'},
|
1377 | function(err, record, created) {
|
1378 | if (err) return done(err);
|
1379 | hookMonitor.names.should.eql([
|
1380 | 'before save',
|
1381 | 'persist',
|
1382 | 'loaded',
|
1383 | 'after save',
|
1384 | ]);
|
1385 | done();
|
1386 | },
|
1387 | );
|
1388 | });
|
1389 |
|
1390 | it('triggers `before save` hook', function(done) {
|
1391 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
1392 |
|
1393 | const currentInstance = deepCloneToObject(existingInstance);
|
1394 |
|
1395 | existingInstance.updateAttributes({name: 'changed'}, function(err) {
|
1396 | if (err) return done(err);
|
1397 | existingInstance.name.should.equal('changed');
|
1398 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1399 | where: {id: existingInstance.id},
|
1400 | data: {name: 'changed'},
|
1401 | currentInstance: currentInstance,
|
1402 | }));
|
1403 | done();
|
1404 | });
|
1405 | });
|
1406 |
|
1407 | it('aborts when `before save` hook fails', function(done) {
|
1408 | TestModel.observe('before save', nextWithError(expectedError));
|
1409 |
|
1410 | existingInstance.updateAttributes({name: 'updated'}, function(err) {
|
1411 | [err].should.eql([expectedError]);
|
1412 | done();
|
1413 | });
|
1414 | });
|
1415 |
|
1416 | it('applies updates from `before save` hook', function(done) {
|
1417 | TestModel.observe('before save', function(ctx, next) {
|
1418 |
|
1419 | ctx.data = Object.assign({}, ctx.data, {
|
1420 | extra: 'extra data',
|
1421 | name: 'hooked name',
|
1422 | });
|
1423 | next();
|
1424 | });
|
1425 |
|
1426 | existingInstance.updateAttributes({name: 'updated'}, function(err) {
|
1427 | if (err) return done(err);
|
1428 |
|
1429 |
|
1430 | TestModel.findById(existingInstance.id, function(err, instance) {
|
1431 | if (err) return done(err);
|
1432 | should.exists(instance);
|
1433 | instance.toObject(true).should.eql({
|
1434 | id: existingInstance.id,
|
1435 | name: 'hooked name',
|
1436 | extra: 'extra data',
|
1437 | });
|
1438 | done();
|
1439 | });
|
1440 | });
|
1441 | });
|
1442 |
|
1443 | it('validates model after `before save` hook', function(done) {
|
1444 | TestModel.observe('before save', invalidateTestModel());
|
1445 |
|
1446 | existingInstance.updateAttributes({name: 'updated'}, function(err) {
|
1447 | (err || {}).should.be.instanceOf(ValidationError);
|
1448 | (err.details.codes || {}).should.eql({name: ['presence']});
|
1449 | done();
|
1450 | });
|
1451 | });
|
1452 |
|
1453 | it('triggers `persist` hook', function(done) {
|
1454 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
1455 | existingInstance.updateAttributes({name: 'changed'}, function(err) {
|
1456 | if (err) return done(err);
|
1457 |
|
1458 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1459 | where: {id: existingInstance.id},
|
1460 | data: {name: 'changed'},
|
1461 | currentInstance: {
|
1462 | id: existingInstance.id,
|
1463 | name: 'changed',
|
1464 | extra: null,
|
1465 | },
|
1466 | isNewInstance: false,
|
1467 | }));
|
1468 |
|
1469 | done();
|
1470 | });
|
1471 | });
|
1472 |
|
1473 | it('applies updates from `persist` hook', function(done) {
|
1474 | TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
1475 |
|
1476 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
1477 | }));
|
1478 |
|
1479 |
|
1480 |
|
1481 |
|
1482 |
|
1483 | TestModel.settings.updateOnLoad = true;
|
1484 | existingInstance.updateAttributes({name: 'changed'}, function(err, instance) {
|
1485 | if (err) return done(err);
|
1486 | instance.should.have.property('extra', 'hook data');
|
1487 | TestModel.findById(existingInstance.id, (err, found) => {
|
1488 | if (err) return done(err);
|
1489 | found.should.have.property('extra', 'hook data');
|
1490 | done();
|
1491 | });
|
1492 | });
|
1493 | });
|
1494 |
|
1495 | it('applies updates from `persist` hook - for nested model instance', function(done) {
|
1496 | const Address = dataSource.createModel('NestedAddress', {
|
1497 | id: {type: String, id: true, default: 1},
|
1498 | city: {type: String, required: true},
|
1499 | country: {type: String, required: true},
|
1500 | });
|
1501 |
|
1502 | const User = dataSource.createModel('UserWithAddress', {
|
1503 | id: {type: String, id: true, default: uid.next},
|
1504 | name: {type: String, required: true},
|
1505 | address: {type: Address, required: false},
|
1506 | extra: {type: String},
|
1507 | });
|
1508 |
|
1509 | dataSource.automigrate(['UserWithAddress', 'NestedAddress'], function(err) {
|
1510 | if (err) return done(err);
|
1511 | User.create({name: 'Joe'}, function(err, instance) {
|
1512 | if (err) return done(err);
|
1513 |
|
1514 | const existingUser = instance;
|
1515 |
|
1516 | User.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
1517 | should.exist(ctx.data.address);
|
1518 | ctx.data.address.should.be.type('object');
|
1519 | ctx.data.address.should.not.be.instanceOf(Address);
|
1520 |
|
1521 |
|
1522 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
1523 | }));
|
1524 |
|
1525 |
|
1526 |
|
1527 |
|
1528 |
|
1529 | User.settings.updateOnLoad = true;
|
1530 | existingUser.updateAttributes(
|
1531 | {address: new Address({city: 'Springfield', country: 'USA'})},
|
1532 | function(err, inst) {
|
1533 | if (err) return done(err);
|
1534 |
|
1535 | inst.should.have.property('extra', 'hook data');
|
1536 |
|
1537 | User.findById(existingUser.id, function(err, dbInstance) {
|
1538 | if (err) return done(err);
|
1539 | dbInstance.toObject(true).should.eql({
|
1540 | id: existingUser.id,
|
1541 | name: existingUser.name,
|
1542 | address: {id: '1', city: 'Springfield', country: 'USA'},
|
1543 | extra: 'hook data',
|
1544 | });
|
1545 | done();
|
1546 | });
|
1547 | },
|
1548 | );
|
1549 | });
|
1550 | });
|
1551 | });
|
1552 |
|
1553 | it('emits error when `persist` hook fails', function(done) {
|
1554 | TestModel.observe('persist', nextWithError(expectedError));
|
1555 |
|
1556 | TestModel.settings.updateOnLoad = true;
|
1557 | existingInstance.updateAttributes({name: 'test'}, function(err, instance) {
|
1558 | [err].should.eql([expectedError]);
|
1559 | done();
|
1560 | });
|
1561 | });
|
1562 |
|
1563 | it('triggers `loaded` hook', function(done) {
|
1564 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
1565 | existingInstance.updateAttributes({name: 'changed'}, function(err) {
|
1566 | if (err) return done(err);
|
1567 |
|
1568 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1569 | data: {name: 'changed'},
|
1570 | isNewInstance: false,
|
1571 | }));
|
1572 |
|
1573 | done();
|
1574 | });
|
1575 | });
|
1576 |
|
1577 | it('emits error when `loaded` hook fails', function(done) {
|
1578 | TestModel.observe('loaded', nextWithError(expectedError));
|
1579 | existingInstance.updateAttributes(
|
1580 | {name: 'changed'},
|
1581 | function(err, instance) {
|
1582 | [err].should.eql([expectedError]);
|
1583 | done();
|
1584 | },
|
1585 | );
|
1586 | });
|
1587 |
|
1588 | it('applies updates from `loaded` hook updateAttributes', function(done) {
|
1589 | TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
1590 |
|
1591 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
1592 | }));
|
1593 |
|
1594 |
|
1595 |
|
1596 |
|
1597 |
|
1598 | TestModel.settings.updateOnLoad = true;
|
1599 | existingInstance.updateAttributes({name: 'changed'}, function(err, instance) {
|
1600 | if (err) return done(err);
|
1601 | instance.should.have.property('extra', 'hook data');
|
1602 | done();
|
1603 | });
|
1604 | });
|
1605 |
|
1606 | it('triggers `after save` hook', function(done) {
|
1607 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
1608 |
|
1609 | existingInstance.name = 'changed';
|
1610 | existingInstance.updateAttributes({name: 'changed'}, function(err) {
|
1611 | if (err) return done(err);
|
1612 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1613 | instance: {
|
1614 | id: existingInstance.id,
|
1615 | name: 'changed',
|
1616 | extra: undefined,
|
1617 | },
|
1618 | isNewInstance: false,
|
1619 | }));
|
1620 | done();
|
1621 | });
|
1622 | });
|
1623 |
|
1624 | it('aborts when `after save` hook fails', function(done) {
|
1625 | TestModel.observe('after save', nextWithError(expectedError));
|
1626 |
|
1627 | existingInstance.updateAttributes({name: 'updated'}, function(err) {
|
1628 | [err].should.eql([expectedError]);
|
1629 | done();
|
1630 | });
|
1631 | });
|
1632 |
|
1633 | it('applies updates from `after save` hook', function(done) {
|
1634 | TestModel.observe('after save', function(ctx, next) {
|
1635 | ctx.instance.should.be.instanceOf(TestModel);
|
1636 | ctx.instance.extra = 'hook data';
|
1637 | next();
|
1638 | });
|
1639 |
|
1640 | existingInstance.updateAttributes({name: 'updated'}, function(err, instance) {
|
1641 | if (err) return done(err);
|
1642 | instance.should.have.property('extra', 'hook data');
|
1643 | done();
|
1644 | });
|
1645 | });
|
1646 | });
|
1647 |
|
1648 | if (!dataSource.connector.replaceById) {
|
1649 | describe.skip('replaceAttributes - not implemented', function() {});
|
1650 | } else {
|
1651 | describe('PersistedModel.prototype.replaceAttributes', function() {
|
1652 | it('triggers hooks in the correct order', function(done) {
|
1653 | monitorHookExecution();
|
1654 |
|
1655 | existingInstance.replaceAttributes(
|
1656 | {name: 'replaced'},
|
1657 | function(err, record, created) {
|
1658 | if (err) return done(err);
|
1659 | hookMonitor.names.should.eql([
|
1660 | 'before save',
|
1661 | 'persist',
|
1662 | 'loaded',
|
1663 | 'after save',
|
1664 | ]);
|
1665 | done();
|
1666 | },
|
1667 | );
|
1668 | });
|
1669 |
|
1670 | it('triggers `before save` hook', function(done) {
|
1671 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
1672 |
|
1673 | existingInstance.replaceAttributes({name: 'changed'}, function(err) {
|
1674 | if (err) return done(err);
|
1675 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1676 | instance: {
|
1677 | id: existingInstance.id,
|
1678 | name: 'changed',
|
1679 | extra: undefined,
|
1680 | },
|
1681 | isNewInstance: false,
|
1682 | }));
|
1683 | done();
|
1684 | });
|
1685 | });
|
1686 |
|
1687 | it('aborts when `before save` hook fails', function(done) {
|
1688 | TestModel.observe('before save', nextWithError(expectedError));
|
1689 |
|
1690 | existingInstance.replaceAttributes({name: 'replaced'}, function(err) {
|
1691 | [err].should.eql([expectedError]);
|
1692 | done();
|
1693 | });
|
1694 | });
|
1695 |
|
1696 | it('applies updates from `before save` hook', function(done) {
|
1697 | TestModel.observe('before save', function(ctx, next) {
|
1698 | ctx.instance.extra = 'extra data';
|
1699 | ctx.instance.name = 'hooked name';
|
1700 | next();
|
1701 | });
|
1702 |
|
1703 | existingInstance.replaceAttributes({name: 'updated'}, function(err) {
|
1704 | if (err) return done(err);
|
1705 | TestModel.findById(existingInstance.id, function(err, instance) {
|
1706 | if (err) return done(err);
|
1707 | should.exists(instance);
|
1708 | instance.toObject(true).should.eql({
|
1709 | id: existingInstance.id,
|
1710 | name: 'hooked name',
|
1711 | extra: 'extra data',
|
1712 | });
|
1713 | done();
|
1714 | });
|
1715 | });
|
1716 | });
|
1717 |
|
1718 | it('validates model after `before save` hook', function(done) {
|
1719 | TestModel.observe('before save', invalidateTestModel());
|
1720 |
|
1721 | existingInstance.replaceAttributes({name: 'updated'}, function(err) {
|
1722 | (err || {}).should.be.instanceOf(ValidationError);
|
1723 | (err.details.codes || {}).should.eql({name: ['presence']});
|
1724 | done();
|
1725 | });
|
1726 | });
|
1727 |
|
1728 | it('triggers `persist` hook', function(done) {
|
1729 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
1730 | existingInstance.replaceAttributes({name: 'replacedName'}, function(err) {
|
1731 | if (err) return done(err);
|
1732 |
|
1733 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1734 | where: {id: existingInstance.id},
|
1735 | data: {
|
1736 | name: 'replacedName',
|
1737 | id: existingInstance.id,
|
1738 | },
|
1739 | currentInstance: {
|
1740 | id: existingInstance.id,
|
1741 | name: 'replacedName',
|
1742 | extra: null,
|
1743 | },
|
1744 | isNewInstance: false,
|
1745 | }));
|
1746 |
|
1747 | done();
|
1748 | });
|
1749 | });
|
1750 |
|
1751 | it('applies delete from `persist` hook', function(done) {
|
1752 | TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
1753 | delete ctx.data.extra;
|
1754 | }));
|
1755 |
|
1756 | existingInstance.replaceAttributes({name: 'changed'}, function(err, instance) {
|
1757 | if (err) return done(err);
|
1758 | instance.should.not.have.property('extra', 'hook data');
|
1759 | done();
|
1760 | });
|
1761 | });
|
1762 |
|
1763 | it('applies updates from `persist` hook - for nested model instance', function(done) {
|
1764 | const Address = dataSource.createModel('NestedAddress', {
|
1765 | id: {type: String, id: true, default: 1},
|
1766 | city: {type: String, required: true},
|
1767 | country: {type: String, required: true},
|
1768 | });
|
1769 |
|
1770 | const User = dataSource.createModel('UserWithAddress', {
|
1771 | id: {type: String, id: true, default: uid.next},
|
1772 | name: {type: String, required: true},
|
1773 | address: {type: Address, required: false},
|
1774 | extra: {type: String},
|
1775 | });
|
1776 |
|
1777 | dataSource.automigrate(['UserWithAddress', 'NestedAddress'], function(err) {
|
1778 | if (err) return done(err);
|
1779 | User.create({name: 'Joe'}, function(err, instance) {
|
1780 | if (err) return done(err);
|
1781 |
|
1782 | const existingUser = instance;
|
1783 |
|
1784 | User.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
1785 | should.exist(ctx.data.address);
|
1786 | ctx.data.address.should.be.type('object');
|
1787 | ctx.data.address.should.not.be.instanceOf(Address);
|
1788 |
|
1789 |
|
1790 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
1791 | }));
|
1792 |
|
1793 | existingUser.replaceAttributes(
|
1794 | {name: 'John', address: new Address({city: 'Springfield', country: 'USA'})},
|
1795 | function(err, inst) {
|
1796 | if (err) return done(err);
|
1797 |
|
1798 | inst.should.have.property('extra', 'hook data');
|
1799 |
|
1800 | User.findById(existingUser.id, function(err, dbInstance) {
|
1801 | if (err) return done(err);
|
1802 | dbInstance.toObject(true).should.eql({
|
1803 | id: existingUser.id,
|
1804 | name: 'John',
|
1805 | address: {id: '1', city: 'Springfield', country: 'USA'},
|
1806 | extra: 'hook data',
|
1807 | });
|
1808 | done();
|
1809 | });
|
1810 | },
|
1811 | );
|
1812 | });
|
1813 | });
|
1814 | });
|
1815 |
|
1816 | it('triggers `loaded` hook', function(done) {
|
1817 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
1818 | existingInstance.replaceAttributes({name: 'changed'}, function(err, data) {
|
1819 | if (err) return done(err);
|
1820 |
|
1821 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1822 | data: {
|
1823 | name: 'changed',
|
1824 | id: data.id,
|
1825 | },
|
1826 | isNewInstance: false,
|
1827 | }));
|
1828 | done();
|
1829 | });
|
1830 | });
|
1831 |
|
1832 | it('emits error when `loaded` hook fails', function(done) {
|
1833 | TestModel.observe('loaded', nextWithError(expectedError));
|
1834 | existingInstance.replaceAttributes(
|
1835 | {name: 'replaced'},
|
1836 | function(err, instance) {
|
1837 | [err].should.eql([expectedError]);
|
1838 | done();
|
1839 | },
|
1840 | );
|
1841 | });
|
1842 |
|
1843 | it('applies updates from `loaded` hook replaceAttributes', function(done) {
|
1844 | TestModel.observe('loaded', ctxRecorder.recordAndNext(function(ctx) {
|
1845 |
|
1846 | ctx.data = Object.assign({}, ctx.data, {name: 'changed in hook'});
|
1847 | }));
|
1848 |
|
1849 | existingInstance.replaceAttributes({name: 'changed'}, function(err, instance) {
|
1850 | if (err) return done(err);
|
1851 | instance.should.have.property('name', 'changed in hook');
|
1852 | done();
|
1853 | });
|
1854 | });
|
1855 |
|
1856 | it('triggers `after save` hook', function(done) {
|
1857 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
1858 |
|
1859 | existingInstance.name = 'replaced';
|
1860 | existingInstance.replaceAttributes({name: 'replaced'}, function(err) {
|
1861 | if (err) return done(err);
|
1862 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
1863 | instance: {
|
1864 | id: existingInstance.id,
|
1865 | name: 'replaced',
|
1866 | extra: undefined,
|
1867 | },
|
1868 | isNewInstance: false,
|
1869 | }));
|
1870 | done();
|
1871 | });
|
1872 | });
|
1873 |
|
1874 | it('aborts when `after save` hook fails', function(done) {
|
1875 | TestModel.observe('after save', nextWithError(expectedError));
|
1876 |
|
1877 | existingInstance.replaceAttributes({name: 'replaced'}, function(err) {
|
1878 | [err].should.eql([expectedError]);
|
1879 | done();
|
1880 | });
|
1881 | });
|
1882 |
|
1883 | it('applies updates from `after save` hook', function(done) {
|
1884 | TestModel.observe('after save', function(ctx, next) {
|
1885 | ctx.instance.should.be.instanceOf(TestModel);
|
1886 | ctx.instance.extra = 'hook data';
|
1887 | next();
|
1888 | });
|
1889 |
|
1890 | existingInstance.replaceAttributes({name: 'updated'}, function(err, instance) {
|
1891 | if (err) return done(err);
|
1892 | instance.should.have.property('extra', 'hook data');
|
1893 | done();
|
1894 | });
|
1895 | });
|
1896 | });
|
1897 | }
|
1898 |
|
1899 | describe('PersistedModel.updateOrCreate', function() {
|
1900 | it('triggers hooks in the correct order on create', function(done) {
|
1901 | monitorHookExecution();
|
1902 |
|
1903 | TestModel.updateOrCreate(
|
1904 | {id: 'not-found', name: 'not found'},
|
1905 | function(err, record, created) {
|
1906 | if (err) return done(err);
|
1907 | hookMonitor.names.should.eql([
|
1908 | 'access',
|
1909 | 'before save',
|
1910 | 'persist',
|
1911 | 'loaded',
|
1912 | 'after save',
|
1913 | ]);
|
1914 | done();
|
1915 | },
|
1916 | );
|
1917 | });
|
1918 |
|
1919 | it('triggers hooks in the correct order on update', function(done) {
|
1920 | monitorHookExecution();
|
1921 |
|
1922 | TestModel.updateOrCreate(
|
1923 | {id: existingInstance.id, name: 'new name'},
|
1924 | function(err, record, created) {
|
1925 | if (err) return done(err);
|
1926 | hookMonitor.names.should.eql([
|
1927 | 'access',
|
1928 | 'before save',
|
1929 | 'persist',
|
1930 | 'loaded',
|
1931 | 'after save',
|
1932 | ]);
|
1933 | done();
|
1934 | },
|
1935 | );
|
1936 | });
|
1937 |
|
1938 | it('triggers `access` hook on create', function(done) {
|
1939 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
1940 |
|
1941 | TestModel.updateOrCreate(
|
1942 | {id: 'not-found', name: 'not found'},
|
1943 | function(err, instance) {
|
1944 | if (err) return done(err);
|
1945 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
1946 | where: {id: 'not-found'},
|
1947 | }}));
|
1948 | done();
|
1949 | },
|
1950 | );
|
1951 | });
|
1952 |
|
1953 | it('triggers `access` hook on update', function(done) {
|
1954 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
1955 |
|
1956 | TestModel.updateOrCreate(
|
1957 | {id: existingInstance.id, name: 'new name'},
|
1958 | function(err, instance) {
|
1959 | if (err) return done(err);
|
1960 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
1961 | where: {id: existingInstance.id},
|
1962 | }}));
|
1963 | done();
|
1964 | },
|
1965 | );
|
1966 | });
|
1967 |
|
1968 | it('does not trigger `access` on missing id', function(done) {
|
1969 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
1970 |
|
1971 | TestModel.updateOrCreate(
|
1972 | {name: 'new name'},
|
1973 | function(err, instance) {
|
1974 | if (err) return done(err);
|
1975 | ctxRecorder.records.should.equal('hook not called');
|
1976 | done();
|
1977 | },
|
1978 | );
|
1979 | });
|
1980 |
|
1981 | it('applies updates from `access` hook when found', function(done) {
|
1982 | TestModel.observe('access', function(ctx, next) {
|
1983 | ctx.query = {where: {id: {neq: existingInstance.id}}};
|
1984 | next();
|
1985 | });
|
1986 |
|
1987 | TestModel.updateOrCreate(
|
1988 | {id: existingInstance.id, name: 'new name'},
|
1989 | function(err, instance) {
|
1990 | if (err) return done(err);
|
1991 | findTestModels({fields: ['id', 'name']}, function(err, list) {
|
1992 | if (err) return done(err);
|
1993 | (list || []).map(toObject).should.eql([
|
1994 | {id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
1995 | {id: instance.id, name: 'new name', extra: undefined},
|
1996 | ]);
|
1997 | done();
|
1998 | });
|
1999 | },
|
2000 | );
|
2001 | });
|
2002 |
|
2003 | it('applies updates from `access` hook when not found', function(done) {
|
2004 | TestModel.observe('access', function(ctx, next) {
|
2005 | ctx.query = {where: {id: 'not-found'}};
|
2006 | next();
|
2007 | });
|
2008 |
|
2009 | TestModel.updateOrCreate(
|
2010 | {id: existingInstance.id, name: 'new name'},
|
2011 | function(err, instance) {
|
2012 | if (err) return done(err);
|
2013 | findTestModels({fields: ['id', 'name']}, function(err, list) {
|
2014 | if (err) return done(err);
|
2015 | (list || []).map(toObject).should.eql([
|
2016 | {id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
2017 | {id: list[1].id, name: 'second', extra: undefined},
|
2018 | {id: instance.id, name: 'new name', extra: undefined},
|
2019 | ]);
|
2020 | done();
|
2021 | });
|
2022 | },
|
2023 | );
|
2024 | });
|
2025 |
|
2026 | it('triggers hooks only once', function(done) {
|
2027 | monitorHookExecution(['access', 'before save']);
|
2028 |
|
2029 | TestModel.observe('access', function(ctx, next) {
|
2030 | ctx.query = {where: {id: {neq: existingInstance.id}}};
|
2031 | next();
|
2032 | });
|
2033 |
|
2034 | TestModel.updateOrCreate(
|
2035 | {id: 'ignored', name: 'new name'},
|
2036 | function(err, instance) {
|
2037 | if (err) return done(err);
|
2038 | hookMonitor.names.should.eql(['access', 'before save']);
|
2039 | done();
|
2040 | },
|
2041 | );
|
2042 | });
|
2043 |
|
2044 | it('triggers `before save` hook on update', function(done) {
|
2045 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
2046 |
|
2047 | TestModel.updateOrCreate(
|
2048 | {id: existingInstance.id, name: 'updated name'},
|
2049 | function(err, instance) {
|
2050 | if (err) return done(err);
|
2051 | if (dataSource.connector.updateOrCreate) {
|
2052 |
|
2053 |
|
2054 |
|
2055 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2056 | where: {id: existingInstance.id},
|
2057 | data: {id: existingInstance.id, name: 'updated name'},
|
2058 | }));
|
2059 | } else {
|
2060 |
|
2061 |
|
2062 |
|
2063 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2064 | where: {id: existingInstance.id},
|
2065 | data: {id: existingInstance.id, name: 'updated name'},
|
2066 | currentInstance: existingInstance,
|
2067 | }));
|
2068 | }
|
2069 | done();
|
2070 | },
|
2071 | );
|
2072 | });
|
2073 |
|
2074 | it('triggers `before save` hook on create', function(done) {
|
2075 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
2076 |
|
2077 | TestModel.updateOrCreate(
|
2078 | {id: 'new-id', name: 'a name'},
|
2079 | function(err, instance) {
|
2080 | if (err) return done(err);
|
2081 |
|
2082 | if (dataSource.connector.updateOrCreate) {
|
2083 |
|
2084 |
|
2085 |
|
2086 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2087 | where: {id: 'new-id'},
|
2088 | data: {id: 'new-id', name: 'a name'},
|
2089 | }));
|
2090 | } else {
|
2091 |
|
2092 |
|
2093 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2094 | instance: {id: 'new-id', name: 'a name', extra: undefined},
|
2095 | isNewInstance: true,
|
2096 | }));
|
2097 | }
|
2098 |
|
2099 | done();
|
2100 | },
|
2101 | );
|
2102 | });
|
2103 |
|
2104 | it('applies updates from `before save` hook on update', function(done) {
|
2105 | TestModel.observe('before save', function(ctx, next) {
|
2106 |
|
2107 | ctx.data = Object.assign({}, ctx.data, {name: 'hooked'});
|
2108 | next();
|
2109 | });
|
2110 |
|
2111 | TestModel.updateOrCreate(
|
2112 | {id: existingInstance.id, name: 'updated name'},
|
2113 | function(err, instance) {
|
2114 | if (err) return done(err);
|
2115 | instance.name.should.equal('hooked');
|
2116 | done();
|
2117 | },
|
2118 | );
|
2119 | });
|
2120 |
|
2121 | it('applies updates from `before save` hook on create', function(done) {
|
2122 | TestModel.observe('before save', function(ctx, next) {
|
2123 | if (ctx.instance) {
|
2124 | ctx.instance.name = 'hooked';
|
2125 | } else {
|
2126 |
|
2127 | ctx.data = Object.assign({}, ctx.data, {name: 'hooked'});
|
2128 | }
|
2129 | next();
|
2130 | });
|
2131 |
|
2132 | TestModel.updateOrCreate(
|
2133 | {id: 'new-id', name: 'new name'},
|
2134 | function(err, instance) {
|
2135 | if (err) return done(err);
|
2136 | instance.name.should.equal('hooked');
|
2137 | done();
|
2138 | },
|
2139 | );
|
2140 | });
|
2141 |
|
2142 |
|
2143 |
|
2144 | it.skip('validates model after `before save` hook on update', function(done) {
|
2145 | TestModel.observe('before save', invalidateTestModel());
|
2146 |
|
2147 | TestModel.updateOrCreate(
|
2148 | {id: existingInstance.id, name: 'updated name'},
|
2149 | function(err, instance) {
|
2150 | (err || {}).should.be.instanceOf(ValidationError);
|
2151 | (err.details.codes || {}).should.eql({name: ['presence']});
|
2152 | done();
|
2153 | },
|
2154 | );
|
2155 | });
|
2156 |
|
2157 |
|
2158 |
|
2159 | it.skip('validates model after `before save` hook on create', function(done) {
|
2160 | TestModel.observe('before save', invalidateTestModel());
|
2161 |
|
2162 | TestModel.updateOrCreate(
|
2163 | {id: 'new-id', name: 'new name'},
|
2164 | function(err, instance) {
|
2165 | (err || {}).should.be.instanceOf(ValidationError);
|
2166 | (err.details.codes || {}).should.eql({name: ['presence']});
|
2167 | done();
|
2168 | },
|
2169 | );
|
2170 | });
|
2171 |
|
2172 | it('triggers `persist` hook on create', function(done) {
|
2173 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
2174 |
|
2175 | TestModel.updateOrCreate(
|
2176 | {id: 'new-id', name: 'a name'},
|
2177 | function(err, instance) {
|
2178 | if (err) return done(err);
|
2179 |
|
2180 | if (dataSource.connector.updateOrCreate) {
|
2181 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2182 | where: {id: 'new-id'},
|
2183 | data: {id: 'new-id', name: 'a name'},
|
2184 | currentInstance: {
|
2185 | id: 'new-id',
|
2186 | name: 'a name',
|
2187 | extra: undefined,
|
2188 | },
|
2189 | }));
|
2190 | } else {
|
2191 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2192 | data: {
|
2193 | id: 'new-id',
|
2194 | name: 'a name',
|
2195 | },
|
2196 | isNewInstance: true,
|
2197 | currentInstance: {
|
2198 | id: 'new-id',
|
2199 | name: 'a name',
|
2200 | extra: undefined,
|
2201 | },
|
2202 | }));
|
2203 | }
|
2204 | done();
|
2205 | },
|
2206 | );
|
2207 | });
|
2208 |
|
2209 | it('triggers `persist` hook on update', function(done) {
|
2210 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
2211 |
|
2212 | TestModel.updateOrCreate(
|
2213 | {id: existingInstance.id, name: 'updated name'},
|
2214 | function(err, instance) {
|
2215 | if (err) return done(err);
|
2216 |
|
2217 | const expectedContext = aCtxForModel(TestModel, {
|
2218 | where: {id: existingInstance.id},
|
2219 | data: {
|
2220 | id: existingInstance.id,
|
2221 | name: 'updated name',
|
2222 | },
|
2223 | currentInstance: {
|
2224 | id: existingInstance.id,
|
2225 | name: 'updated name',
|
2226 | extra: undefined,
|
2227 | },
|
2228 | });
|
2229 |
|
2230 | if (!dataSource.connector.updateOrCreate) {
|
2231 |
|
2232 |
|
2233 | expectedContext.isNewInstance = false;
|
2234 | }
|
2235 |
|
2236 | ctxRecorder.records.should.eql(expectedContext);
|
2237 | done();
|
2238 | },
|
2239 | );
|
2240 | });
|
2241 |
|
2242 | it('triggers `loaded` hook on create', function(done) {
|
2243 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
2244 |
|
2245 | TestModel.updateOrCreate(
|
2246 | {id: 'new-id', name: 'a name'},
|
2247 | function(err, instance) {
|
2248 | if (err) return done(err);
|
2249 |
|
2250 | if (dataSource.connector.updateOrCreate) {
|
2251 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2252 | data: {id: 'new-id', name: 'a name'},
|
2253 | isNewInstance: isNewInstanceFlag ? true : undefined,
|
2254 | }));
|
2255 | } else {
|
2256 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2257 | data: {
|
2258 | id: 'new-id',
|
2259 | name: 'a name',
|
2260 | },
|
2261 | isNewInstance: true,
|
2262 | }));
|
2263 | }
|
2264 | done();
|
2265 | },
|
2266 | );
|
2267 | });
|
2268 |
|
2269 | it('triggers `loaded` hook on update', function(done) {
|
2270 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
2271 |
|
2272 | TestModel.updateOrCreate(
|
2273 | {id: existingInstance.id, name: 'updated name'},
|
2274 | function(err, instance) {
|
2275 | if (err) return done(err);
|
2276 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2277 | data: {
|
2278 | id: existingInstance.id,
|
2279 | name: 'updated name',
|
2280 | },
|
2281 | isNewInstance: isNewInstanceFlag ? false : undefined,
|
2282 | }));
|
2283 | done();
|
2284 | },
|
2285 | );
|
2286 | });
|
2287 |
|
2288 | it('emits error when `loaded` hook fails', function(done) {
|
2289 | TestModel.observe('loaded', nextWithError(expectedError));
|
2290 | TestModel.updateOrCreate(
|
2291 | {id: 'new-id', name: 'a name'},
|
2292 | function(err, instance) {
|
2293 | [err].should.eql([expectedError]);
|
2294 | done();
|
2295 | },
|
2296 | );
|
2297 | });
|
2298 |
|
2299 | it('triggers `after save` hook on update', function(done) {
|
2300 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
2301 |
|
2302 | TestModel.updateOrCreate(
|
2303 | {id: existingInstance.id, name: 'updated name'},
|
2304 | function(err, instance) {
|
2305 | if (err) return done(err);
|
2306 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2307 | instance: {
|
2308 | id: existingInstance.id,
|
2309 | name: 'updated name',
|
2310 | extra: undefined,
|
2311 | },
|
2312 | isNewInstance: isNewInstanceFlag ? false : undefined,
|
2313 | }));
|
2314 | done();
|
2315 | },
|
2316 | );
|
2317 | });
|
2318 |
|
2319 | it('aborts when `after save` fires on update or create when option to notify is false', function(done) {
|
2320 | monitorHookExecution();
|
2321 |
|
2322 | TestModel.updateOrCreate({name: 'created'}, {notify: false}, function(err, record, created) {
|
2323 | if (err) return done(err);
|
2324 |
|
2325 | hookMonitor.names.should.not.containEql('after save');
|
2326 | done();
|
2327 | });
|
2328 | });
|
2329 |
|
2330 | it('triggers `after save` hook on create', function(done) {
|
2331 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
2332 |
|
2333 | TestModel.updateOrCreate(
|
2334 | {id: 'new-id', name: 'a name'},
|
2335 | function(err, instance) {
|
2336 | if (err) return done(err);
|
2337 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2338 | instance: {
|
2339 | id: instance.id,
|
2340 | name: 'a name',
|
2341 | extra: undefined,
|
2342 | },
|
2343 | isNewInstance: isNewInstanceFlag ? true : undefined,
|
2344 | }));
|
2345 | done();
|
2346 | },
|
2347 | );
|
2348 | });
|
2349 | });
|
2350 |
|
2351 | if (!dataSource.connector.replaceById) {
|
2352 | describe.skip('replaceOrCreate - not implemented', function() {});
|
2353 | } else {
|
2354 | describe('PersistedModel.replaceOrCreate', function() {
|
2355 | it('triggers hooks in the correct order on create', function(done) {
|
2356 | monitorHookExecution();
|
2357 |
|
2358 | TestModel.replaceOrCreate(
|
2359 | {id: 'not-found', name: 'not found'},
|
2360 | function(err, record, created) {
|
2361 | if (err) return done(err);
|
2362 | hookMonitor.names.should.eql([
|
2363 | 'access',
|
2364 | 'before save',
|
2365 | 'persist',
|
2366 | 'loaded',
|
2367 | 'after save',
|
2368 | ]);
|
2369 | done();
|
2370 | },
|
2371 | );
|
2372 | });
|
2373 |
|
2374 | it('triggers hooks in the correct order on replace', function(done) {
|
2375 | monitorHookExecution();
|
2376 |
|
2377 | TestModel.replaceOrCreate(
|
2378 | {id: existingInstance.id, name: 'new name'},
|
2379 | function(err, record, created) {
|
2380 | if (err) return done(err);
|
2381 | hookMonitor.names.should.eql([
|
2382 | 'access',
|
2383 | 'before save',
|
2384 | 'persist',
|
2385 | 'loaded',
|
2386 | 'after save',
|
2387 | ]);
|
2388 | done();
|
2389 | },
|
2390 | );
|
2391 | });
|
2392 |
|
2393 | it('triggers `access` hook on create', function(done) {
|
2394 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
2395 |
|
2396 | TestModel.replaceOrCreate(
|
2397 | {id: 'not-found', name: 'not found'},
|
2398 | function(err, instance) {
|
2399 | if (err) return done(err);
|
2400 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
2401 | where: {id: 'not-found'},
|
2402 | }}));
|
2403 | done();
|
2404 | },
|
2405 | );
|
2406 | });
|
2407 |
|
2408 | it('triggers `access` hook on replace', function(done) {
|
2409 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
2410 |
|
2411 | TestModel.replaceOrCreate(
|
2412 | {id: existingInstance.id, name: 'new name'},
|
2413 | function(err, instance) {
|
2414 | if (err) return done(err);
|
2415 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
2416 | where: {id: existingInstance.id},
|
2417 | }}));
|
2418 | done();
|
2419 | },
|
2420 | );
|
2421 | });
|
2422 |
|
2423 | it('does not trigger `access` on missing id', function(done) {
|
2424 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
2425 |
|
2426 | TestModel.replaceOrCreate(
|
2427 | {name: 'new name'},
|
2428 | function(err, instance) {
|
2429 | if (err) return done(err);
|
2430 | ctxRecorder.records.should.equal('hook not called');
|
2431 | done();
|
2432 | },
|
2433 | );
|
2434 | });
|
2435 |
|
2436 | it('applies updates from `access` hook when found', function(done) {
|
2437 | TestModel.observe('access', function(ctx, next) {
|
2438 | ctx.query = {where: {id: {neq: existingInstance.id}}};
|
2439 | next();
|
2440 | });
|
2441 |
|
2442 | TestModel.replaceOrCreate(
|
2443 | {id: existingInstance.id, name: 'new name'},
|
2444 | function(err, instance) {
|
2445 | if (err) return done(err);
|
2446 | findTestModels({fields: ['id', 'name']}, function(err, list) {
|
2447 | if (err) return done(err);
|
2448 | (list || []).map(toObject).should.eql([
|
2449 | {id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
2450 | {id: instance.id, name: 'new name', extra: undefined},
|
2451 | ]);
|
2452 | done();
|
2453 | });
|
2454 | },
|
2455 | );
|
2456 | });
|
2457 |
|
2458 | it('applies updates from `access` hook when not found', function(done) {
|
2459 | TestModel.observe('access', function(ctx, next) {
|
2460 | ctx.query = {where: {id: 'not-found'}};
|
2461 | next();
|
2462 | });
|
2463 |
|
2464 | TestModel.replaceOrCreate(
|
2465 | {id: existingInstance.id, name: 'new name'},
|
2466 | function(err, instance) {
|
2467 | if (err) return done(err);
|
2468 | findTestModels({fields: ['id', 'name']}, function(err, list) {
|
2469 | if (err) return done(err);
|
2470 | (list || []).map(toObject).should.eql([
|
2471 | {id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
2472 | {id: list[1].id, name: 'second', extra: undefined},
|
2473 | {id: instance.id, name: 'new name', extra: undefined},
|
2474 | ]);
|
2475 | done();
|
2476 | });
|
2477 | },
|
2478 | );
|
2479 | });
|
2480 |
|
2481 | it('triggers hooks only once', function(done) {
|
2482 | monitorHookExecution(['access', 'before save']);
|
2483 |
|
2484 | TestModel.observe('access', function(ctx, next) {
|
2485 | ctx.query = {where: {id: {neq: existingInstance.id}}};
|
2486 | next();
|
2487 | });
|
2488 |
|
2489 | TestModel.replaceOrCreate(
|
2490 | {id: 'ignored', name: 'new name'},
|
2491 | function(err, instance) {
|
2492 | if (err) return done(err);
|
2493 | hookMonitor.names.should.eql(['access', 'before save']);
|
2494 | done();
|
2495 | },
|
2496 | );
|
2497 | });
|
2498 |
|
2499 | it('triggers `before save` hookon create', function(done) {
|
2500 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
2501 | TestModel.replaceOrCreate({id: existingInstance.id, name: 'new name'},
|
2502 | function(err, instance) {
|
2503 | if (err)
|
2504 | return done(err);
|
2505 |
|
2506 | const expectedContext = aCtxForModel(TestModel, {
|
2507 | instance: instance,
|
2508 | });
|
2509 |
|
2510 | if (!dataSource.connector.replaceOrCreate) {
|
2511 | expectedContext.isNewInstance = false;
|
2512 | }
|
2513 | done();
|
2514 | });
|
2515 | });
|
2516 |
|
2517 | it('triggers `before save` hook on replace', function(done) {
|
2518 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
2519 | TestModel.replaceOrCreate(
|
2520 | {id: existingInstance.id, name: 'replaced name'},
|
2521 | function(err, instance) {
|
2522 | if (err) return done(err);
|
2523 |
|
2524 | const expectedContext = aCtxForModel(TestModel, {
|
2525 | instance: {
|
2526 | id: existingInstance.id,
|
2527 | name: 'replaced name',
|
2528 | extra: undefined,
|
2529 | },
|
2530 | });
|
2531 |
|
2532 | if (!dataSource.connector.replaceOrCreate) {
|
2533 | expectedContext.isNewInstance = false;
|
2534 | }
|
2535 | ctxRecorder.records.should.eql(expectedContext);
|
2536 |
|
2537 | done();
|
2538 | },
|
2539 | );
|
2540 | });
|
2541 |
|
2542 | it('triggers `before save` hook on create', function(done) {
|
2543 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
2544 |
|
2545 | TestModel.replaceOrCreate(
|
2546 | {id: 'new-id', name: 'a name'},
|
2547 | function(err, instance) {
|
2548 | if (err) return done(err);
|
2549 |
|
2550 | const expectedContext = aCtxForModel(TestModel, {
|
2551 | instance: {
|
2552 | id: 'new-id',
|
2553 | name: 'a name',
|
2554 | extra: undefined,
|
2555 | },
|
2556 | });
|
2557 |
|
2558 | if (!dataSource.connector.replaceOrCreate) {
|
2559 | expectedContext.isNewInstance = true;
|
2560 | }
|
2561 | ctxRecorder.records.should.eql(expectedContext);
|
2562 |
|
2563 | done();
|
2564 | },
|
2565 | );
|
2566 | });
|
2567 |
|
2568 | it('applies updates from `before save` hook on create', function(done) {
|
2569 | TestModel.observe('before save', function(ctx, next) {
|
2570 | ctx.instance.name = 'hooked';
|
2571 | next();
|
2572 | });
|
2573 |
|
2574 | TestModel.replaceOrCreate(
|
2575 | {id: 'new-id', name: 'new name'},
|
2576 | function(err, instance) {
|
2577 | if (err) return done(err);
|
2578 | instance.name.should.equal('hooked');
|
2579 | done();
|
2580 | },
|
2581 | );
|
2582 | });
|
2583 |
|
2584 | it('validates model after `before save` hook on create', function(done) {
|
2585 | TestModel.observe('before save', invalidateTestModel());
|
2586 |
|
2587 | TestModel.replaceOrCreate(
|
2588 | {id: 'new-id', name: 'new name'},
|
2589 | function(err, instance) {
|
2590 | (err || {}).should.be.instanceOf(ValidationError);
|
2591 | (err.details.codes || {}).should.eql({name: ['presence']});
|
2592 | done();
|
2593 | },
|
2594 | );
|
2595 | });
|
2596 |
|
2597 | it('triggers `persist` hook on create', function(done) {
|
2598 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
2599 |
|
2600 | TestModel.replaceOrCreate(
|
2601 | {id: 'new-id', name: 'a name'},
|
2602 | function(err, instance) {
|
2603 | if (err) return done(err);
|
2604 |
|
2605 | const expectedContext = aCtxForModel(TestModel, {
|
2606 | currentInstance: {
|
2607 | id: 'new-id',
|
2608 | name: 'a name',
|
2609 | extra: undefined,
|
2610 | },
|
2611 | data: {
|
2612 | id: 'new-id',
|
2613 | name: 'a name',
|
2614 | },
|
2615 | });
|
2616 |
|
2617 | if (dataSource.connector.replaceOrCreate) {
|
2618 | expectedContext.where = {id: 'new-id'};
|
2619 | } else {
|
2620 |
|
2621 |
|
2622 |
|
2623 | expectedContext.isNewInstance = true;
|
2624 | }
|
2625 | ctxRecorder.records.should.eql(expectedContext);
|
2626 | done();
|
2627 | },
|
2628 | );
|
2629 | });
|
2630 |
|
2631 | it('triggers `persist` hook on replace', function(done) {
|
2632 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
2633 |
|
2634 | TestModel.replaceOrCreate(
|
2635 | {id: existingInstance.id, name: 'replaced name'},
|
2636 | function(err, instance) {
|
2637 | if (err) return done(err);
|
2638 |
|
2639 | const expected = {
|
2640 | where: {id: existingInstance.id},
|
2641 | data: {
|
2642 | id: existingInstance.id,
|
2643 | name: 'replaced name',
|
2644 | },
|
2645 | currentInstance: {
|
2646 | id: existingInstance.id,
|
2647 | name: 'replaced name',
|
2648 | extra: undefined,
|
2649 | },
|
2650 | };
|
2651 |
|
2652 | const expectedContext = aCtxForModel(TestModel, expected);
|
2653 |
|
2654 | if (!dataSource.connector.replaceOrCreate) {
|
2655 | expectedContext.isNewInstance = false;
|
2656 | }
|
2657 |
|
2658 | ctxRecorder.records.should.eql(expectedContext);
|
2659 | done();
|
2660 | },
|
2661 | );
|
2662 | });
|
2663 |
|
2664 | it('applies updates from `persist` hook on create', function(done) {
|
2665 | TestModel.observe('persist', (ctx, next) => {
|
2666 |
|
2667 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
2668 | next();
|
2669 | });
|
2670 |
|
2671 |
|
2672 |
|
2673 |
|
2674 |
|
2675 | TestModel.settings.updateOnLoad = true;
|
2676 |
|
2677 | TestModel.replaceOrCreate(
|
2678 | {name: 'a name'},
|
2679 | function(err, instance) {
|
2680 | if (err) return done(err);
|
2681 | instance.should.have.property('extra', 'hook data');
|
2682 | TestModel.findById(instance.id, (err, found) => {
|
2683 | if (err) return done(err);
|
2684 | found.should.have.property('extra', 'hook data');
|
2685 | done();
|
2686 | });
|
2687 | },
|
2688 | );
|
2689 | });
|
2690 |
|
2691 | it('applies updates from `persist` hook on update', function(done) {
|
2692 | TestModel.observe('persist', (ctx, next) => {
|
2693 |
|
2694 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
2695 | next();
|
2696 | });
|
2697 |
|
2698 | existingInstance.name = 'changed';
|
2699 | const data = existingInstance.toObject();
|
2700 |
|
2701 | TestModel.replaceOrCreate(data, function(err, instance) {
|
2702 | if (err) return done(err);
|
2703 | instance.should.have.property('extra', 'hook data');
|
2704 | TestModel.findById(existingInstance.id, (err, found) => {
|
2705 | if (err) return done(err);
|
2706 | found.should.have.property('extra', 'hook data');
|
2707 | done();
|
2708 | });
|
2709 | });
|
2710 | });
|
2711 |
|
2712 | it('triggers `loaded` hook on create', function(done) {
|
2713 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
2714 |
|
2715 | TestModel.replaceOrCreate(
|
2716 | {id: 'new-id', name: 'a name'},
|
2717 | function(err, instance) {
|
2718 | if (err) return done(err);
|
2719 |
|
2720 | const expected = {
|
2721 | data: {
|
2722 | id: 'new-id',
|
2723 | name: 'a name',
|
2724 | },
|
2725 | };
|
2726 |
|
2727 | expected.isNewInstance =
|
2728 | isNewInstanceFlag ?
|
2729 | true : undefined;
|
2730 |
|
2731 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, expected));
|
2732 | done();
|
2733 | },
|
2734 | );
|
2735 | });
|
2736 |
|
2737 | it('triggers `loaded` hook on replace', function(done) {
|
2738 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
2739 |
|
2740 | TestModel.replaceOrCreate(
|
2741 | {id: existingInstance.id, name: 'replaced name'},
|
2742 | function(err, instance) {
|
2743 | if (err) return done(err);
|
2744 |
|
2745 | const expected = {
|
2746 | data: {
|
2747 | id: existingInstance.id,
|
2748 | name: 'replaced name',
|
2749 | },
|
2750 | };
|
2751 |
|
2752 | expected.isNewInstance =
|
2753 | isNewInstanceFlag ?
|
2754 | false : undefined;
|
2755 |
|
2756 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, expected));
|
2757 | done();
|
2758 | },
|
2759 | );
|
2760 | });
|
2761 |
|
2762 | it('emits error when `loaded` hook fails', function(done) {
|
2763 | TestModel.observe('loaded', nextWithError(expectedError));
|
2764 | TestModel.replaceOrCreate(
|
2765 | {id: 'new-id', name: 'a name'},
|
2766 | function(err, instance) {
|
2767 | [err].should.eql([expectedError]);
|
2768 | done();
|
2769 | },
|
2770 | );
|
2771 | });
|
2772 |
|
2773 | it('triggers `after save` hook on replace', function(done) {
|
2774 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
2775 |
|
2776 | TestModel.replaceOrCreate(
|
2777 | {id: existingInstance.id, name: 'replaced name'},
|
2778 | function(err, instance) {
|
2779 | if (err) return done(err);
|
2780 |
|
2781 | const expected = {
|
2782 | instance: {
|
2783 | id: existingInstance.id,
|
2784 | name: 'replaced name',
|
2785 | extra: undefined,
|
2786 | },
|
2787 | };
|
2788 |
|
2789 | expected.isNewInstance =
|
2790 | isNewInstanceFlag ?
|
2791 | false : undefined;
|
2792 |
|
2793 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, expected));
|
2794 | done();
|
2795 | },
|
2796 | );
|
2797 | });
|
2798 |
|
2799 | it('triggers `after save` hook on create', function(done) {
|
2800 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
2801 |
|
2802 | TestModel.replaceOrCreate(
|
2803 | {id: 'new-id', name: 'a name'},
|
2804 | function(err, instance) {
|
2805 | if (err) return done(err);
|
2806 |
|
2807 | const expected = {
|
2808 | instance: {
|
2809 | id: instance.id,
|
2810 | name: 'a name',
|
2811 | extra: undefined,
|
2812 | },
|
2813 | };
|
2814 | expected.isNewInstance =
|
2815 | isNewInstanceFlag ?
|
2816 | true : undefined;
|
2817 |
|
2818 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, expected));
|
2819 | done();
|
2820 | },
|
2821 | );
|
2822 | });
|
2823 | });
|
2824 | }
|
2825 |
|
2826 | if (!dataSource.connector.replaceById) {
|
2827 | describe.skip('replaceById - not implemented', function() {});
|
2828 | } else {
|
2829 | describe('PersistedModel.replaceById', function() {
|
2830 | it('triggers hooks in the correct order on create', function(done) {
|
2831 | monitorHookExecution();
|
2832 |
|
2833 | existingInstance.name = 'replaced name';
|
2834 | TestModel.replaceById(
|
2835 | existingInstance.id,
|
2836 | existingInstance.toObject(),
|
2837 | function(err, record, created) {
|
2838 | if (err) return done(err);
|
2839 | hookMonitor.names.should.eql([
|
2840 | 'before save',
|
2841 | 'persist',
|
2842 | 'loaded',
|
2843 | 'after save',
|
2844 | ]);
|
2845 | done();
|
2846 | },
|
2847 | );
|
2848 | });
|
2849 |
|
2850 | it('triggers `persist` hook', function(done) {
|
2851 |
|
2852 |
|
2853 |
|
2854 |
|
2855 |
|
2856 |
|
2857 | TestModel.settings.persistUndefinedAsNull = true;
|
2858 |
|
2859 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
2860 |
|
2861 | existingInstance.name = 'replaced name';
|
2862 | TestModel.replaceById(
|
2863 | existingInstance.id,
|
2864 | existingInstance.toObject(),
|
2865 | function(err, instance) {
|
2866 | if (err) return done(err);
|
2867 |
|
2868 | const expected = {
|
2869 | where: {id: existingInstance.id},
|
2870 | data: {
|
2871 | id: existingInstance.id,
|
2872 | name: 'replaced name',
|
2873 | extra: null,
|
2874 | },
|
2875 | currentInstance: {
|
2876 | id: existingInstance.id,
|
2877 | name: 'replaced name',
|
2878 | extra: null,
|
2879 | },
|
2880 | };
|
2881 |
|
2882 | const expectedContext = aCtxForModel(TestModel, expected);
|
2883 | expectedContext.isNewInstance = false;
|
2884 |
|
2885 | ctxRecorder.records.should.eql(expectedContext);
|
2886 | done();
|
2887 | },
|
2888 | );
|
2889 | });
|
2890 |
|
2891 | it('applies updates from `persist` hook', function(done) {
|
2892 | TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
2893 |
|
2894 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
2895 | }));
|
2896 |
|
2897 | existingInstance.name = 'changed';
|
2898 | TestModel.replaceById(
|
2899 | existingInstance.id,
|
2900 | existingInstance.toObject(),
|
2901 | function(err, instance) {
|
2902 | if (err) return done(err);
|
2903 | instance.should.have.property('extra', 'hook data');
|
2904 | TestModel.findById(existingInstance.id, (err, found) => {
|
2905 | if (err) return done(err);
|
2906 | found.should.have.property('extra', 'hook data');
|
2907 | done();
|
2908 | });
|
2909 | },
|
2910 | );
|
2911 | });
|
2912 | });
|
2913 | }
|
2914 |
|
2915 | describe('PersistedModel.deleteAll', function() {
|
2916 | it('triggers `access` hook with query', function(done) {
|
2917 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
2918 |
|
2919 | TestModel.deleteAll({name: existingInstance.name}, function(err) {
|
2920 | if (err) return done(err);
|
2921 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2922 | query: {where: {name: existingInstance.name}},
|
2923 | }));
|
2924 | done();
|
2925 | });
|
2926 | });
|
2927 |
|
2928 | it('triggers `access` hook without query', function(done) {
|
2929 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
2930 |
|
2931 | TestModel.deleteAll(function(err) {
|
2932 | if (err) return done(err);
|
2933 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {where: {}}}));
|
2934 | done();
|
2935 | });
|
2936 | });
|
2937 |
|
2938 | it('applies updates from `access` hook', function(done) {
|
2939 | TestModel.observe('access', function(ctx, next) {
|
2940 | ctx.query = {where: {id: {neq: existingInstance.id}}};
|
2941 | next();
|
2942 | });
|
2943 |
|
2944 | TestModel.deleteAll(function(err) {
|
2945 | if (err) return done(err);
|
2946 | findTestModels(function(err, list) {
|
2947 | if (err) return done(err);
|
2948 | (list || []).map(get('id')).should.eql([existingInstance.id]);
|
2949 | done();
|
2950 | });
|
2951 | });
|
2952 | });
|
2953 |
|
2954 | it('triggers `before delete` hook with query', function(done) {
|
2955 | TestModel.observe('before delete', ctxRecorder.recordAndNext());
|
2956 |
|
2957 | TestModel.deleteAll({name: existingInstance.name}, function(err) {
|
2958 | if (err) return done(err);
|
2959 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
2960 | where: {name: existingInstance.name},
|
2961 | }));
|
2962 | done();
|
2963 | });
|
2964 | });
|
2965 |
|
2966 | it('triggers `before delete` hook without query', function(done) {
|
2967 | TestModel.observe('before delete', ctxRecorder.recordAndNext());
|
2968 |
|
2969 | TestModel.deleteAll(function(err) {
|
2970 | if (err) return done(err);
|
2971 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {where: {}}));
|
2972 | done();
|
2973 | });
|
2974 | });
|
2975 |
|
2976 | it('applies updates from `before delete` hook', function(done) {
|
2977 | TestModel.observe('before delete', function(ctx, next) {
|
2978 | ctx.where = {id: {neq: existingInstance.id}};
|
2979 | next();
|
2980 | });
|
2981 |
|
2982 | TestModel.deleteAll(function(err) {
|
2983 | if (err) return done(err);
|
2984 | findTestModels(function(err, list) {
|
2985 | if (err) return done(err);
|
2986 | (list || []).map(get('id')).should.eql([existingInstance.id]);
|
2987 | done();
|
2988 | });
|
2989 | });
|
2990 | });
|
2991 |
|
2992 | it('aborts when `before delete` hook fails', function(done) {
|
2993 | TestModel.observe('before delete', nextWithError(expectedError));
|
2994 |
|
2995 | TestModel.deleteAll(function(err, list) {
|
2996 | [err].should.eql([expectedError]);
|
2997 | TestModel.findById(existingInstance.id, function(err, inst) {
|
2998 | if (err) return done(err);
|
2999 | (inst ? inst.toObject() : 'null').should.
|
3000 | eql(existingInstance.toObject());
|
3001 | done();
|
3002 | });
|
3003 | });
|
3004 | });
|
3005 |
|
3006 | it('triggers `after delete` hook without query', function(done) {
|
3007 | TestModel.observe('after delete', ctxRecorder.recordAndNext());
|
3008 |
|
3009 | TestModel.deleteAll(function(err) {
|
3010 | if (err) return done(err);
|
3011 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
3012 | where: {},
|
3013 | info: {count: 2},
|
3014 | }));
|
3015 | done();
|
3016 | });
|
3017 | });
|
3018 |
|
3019 | it('triggers `after delete` hook with query', function(done) {
|
3020 | TestModel.observe('after delete', ctxRecorder.recordAndNext());
|
3021 |
|
3022 | TestModel.deleteAll({name: existingInstance.name}, function(err) {
|
3023 | if (err) return done(err);
|
3024 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
3025 | where: {name: existingInstance.name},
|
3026 | info: {count: 1},
|
3027 | }));
|
3028 | done();
|
3029 | });
|
3030 | });
|
3031 |
|
3032 | it('aborts when `after delete` hook fails', function(done) {
|
3033 | TestModel.observe('after delete', nextWithError(expectedError));
|
3034 |
|
3035 | TestModel.deleteAll(function(err) {
|
3036 | [err].should.eql([expectedError]);
|
3037 | done();
|
3038 | });
|
3039 | });
|
3040 | });
|
3041 |
|
3042 | describe('PersistedModel.prototype.delete', function() {
|
3043 | it('triggers `access` hook', function(done) {
|
3044 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
3045 |
|
3046 | existingInstance.delete(function(err) {
|
3047 | if (err) return done(err);
|
3048 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
3049 | query: {where: {id: existingInstance.id}},
|
3050 | }));
|
3051 | done();
|
3052 | });
|
3053 | });
|
3054 |
|
3055 | it('applies updated from `access` hook', function(done) {
|
3056 | TestModel.observe('access', function(ctx, next) {
|
3057 | ctx.query = {where: {id: {neq: existingInstance.id}}};
|
3058 | next();
|
3059 | });
|
3060 |
|
3061 | existingInstance.delete(function(err) {
|
3062 | if (err) return done(err);
|
3063 | findTestModels(function(err, list) {
|
3064 | if (err) return done(err);
|
3065 | (list || []).map(get('id')).should.eql([existingInstance.id]);
|
3066 | done();
|
3067 | });
|
3068 | });
|
3069 | });
|
3070 |
|
3071 | it('triggers `before delete` hook', function(done) {
|
3072 | TestModel.observe('before delete', ctxRecorder.recordAndNext());
|
3073 |
|
3074 | existingInstance.delete(function(err) {
|
3075 | if (err) return done(err);
|
3076 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
3077 | where: {id: existingInstance.id},
|
3078 | instance: existingInstance,
|
3079 | }));
|
3080 | done();
|
3081 | });
|
3082 | });
|
3083 |
|
3084 | it('applies updated from `before delete` hook', function(done) {
|
3085 | TestModel.observe('before delete', function(ctx, next) {
|
3086 | ctx.where = {id: {neq: existingInstance.id}};
|
3087 | next();
|
3088 | });
|
3089 |
|
3090 | existingInstance.delete(function(err) {
|
3091 | if (err) return done(err);
|
3092 | findTestModels(function(err, list) {
|
3093 | if (err) return done(err);
|
3094 | (list || []).map(get('id')).should.eql([existingInstance.id]);
|
3095 | done();
|
3096 | });
|
3097 | });
|
3098 | });
|
3099 |
|
3100 | it('aborts when `before delete` hook fails', function(done) {
|
3101 | TestModel.observe('before delete', nextWithError(expectedError));
|
3102 |
|
3103 | existingInstance.delete(function(err, list) {
|
3104 | [err].should.eql([expectedError]);
|
3105 | TestModel.findById(existingInstance.id, function(err, inst) {
|
3106 | if (err) return done(err);
|
3107 | (inst ? inst.toObject() : 'null').should.eql(
|
3108 | existingInstance.toObject(),
|
3109 | );
|
3110 | done();
|
3111 | });
|
3112 | });
|
3113 | });
|
3114 |
|
3115 | it('triggers `after delete` hook', function(done) {
|
3116 | TestModel.observe('after delete', ctxRecorder.recordAndNext());
|
3117 |
|
3118 | existingInstance.delete(function(err) {
|
3119 | if (err) return done(err);
|
3120 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
3121 | where: {id: existingInstance.id},
|
3122 | instance: existingInstance,
|
3123 | info: {count: 1},
|
3124 | }));
|
3125 | done();
|
3126 | });
|
3127 | });
|
3128 |
|
3129 | it('triggers `after delete` hook without query', function(done) {
|
3130 | TestModel.observe('after delete', ctxRecorder.recordAndNext());
|
3131 |
|
3132 | TestModel.deleteAll({name: existingInstance.name}, function(err) {
|
3133 | if (err) return done(err);
|
3134 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
3135 | where: {name: existingInstance.name},
|
3136 | info: {count: 1},
|
3137 | }));
|
3138 | done();
|
3139 | });
|
3140 | });
|
3141 |
|
3142 | it('aborts when `after delete` hook fails', function(done) {
|
3143 | TestModel.observe('after delete', nextWithError(expectedError));
|
3144 |
|
3145 | TestModel.deleteAll(function(err) {
|
3146 | [err].should.eql([expectedError]);
|
3147 | done();
|
3148 | });
|
3149 | });
|
3150 |
|
3151 | it('propagates hookState from `before delete` to `after delete`', function(done) {
|
3152 | TestModel.observe('before delete', ctxRecorder.recordAndNext(function(ctx) {
|
3153 | ctx.hookState.foo = 'bar';
|
3154 | }));
|
3155 |
|
3156 | TestModel.observe('after delete', ctxRecorder.recordAndNext(function(ctx) {
|
3157 | ctx.hookState.foo = ctx.hookState.foo.toUpperCase();
|
3158 | }));
|
3159 |
|
3160 | existingInstance.delete(function(err) {
|
3161 | if (err) return done(err);
|
3162 | ctxRecorder.records.should.eql([
|
3163 | aCtxForModel(TestModel, {
|
3164 | hookState: {foo: 'bar'},
|
3165 | where: {id: '1'},
|
3166 | instance: existingInstance,
|
3167 | }),
|
3168 | aCtxForModel(TestModel, {
|
3169 | hookState: {foo: 'BAR'},
|
3170 | info: {count: 1},
|
3171 | where: {id: '1'},
|
3172 | instance: existingInstance,
|
3173 | }),
|
3174 | ]);
|
3175 | done();
|
3176 | });
|
3177 | });
|
3178 |
|
3179 | it('triggers hooks only once', function(done) {
|
3180 | monitorHookExecution();
|
3181 | TestModel.observe('access', function(ctx, next) {
|
3182 | ctx.query = {where: {id: {neq: existingInstance.id}}};
|
3183 | next();
|
3184 | });
|
3185 |
|
3186 | existingInstance.delete(function(err) {
|
3187 | if (err) return done(err);
|
3188 | hookMonitor.names.should.eql(['access', 'before delete', 'after delete']);
|
3189 | done();
|
3190 | });
|
3191 | });
|
3192 | });
|
3193 |
|
3194 | describe('PersistedModel.updateAll', function() {
|
3195 | it('triggers `access` hook', function(done) {
|
3196 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
3197 |
|
3198 | TestModel.updateAll(
|
3199 | {name: 'searched'},
|
3200 | {name: 'updated'},
|
3201 | function(err, instance) {
|
3202 | if (err) return done(err);
|
3203 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
3204 | where: {name: 'searched'},
|
3205 | }}));
|
3206 | done();
|
3207 | },
|
3208 | );
|
3209 | });
|
3210 |
|
3211 | it('applies updates from `access` hook', function(done) {
|
3212 | TestModel.observe('access', function(ctx, next) {
|
3213 | ctx.query = {where: {id: {neq: existingInstance.id}}};
|
3214 | next();
|
3215 | });
|
3216 |
|
3217 | TestModel.updateAll(
|
3218 | {id: existingInstance.id},
|
3219 | {name: 'new name'},
|
3220 | function(err) {
|
3221 | if (err) return done(err);
|
3222 | findTestModels({fields: ['id', 'name']}, function(err, list) {
|
3223 | if (err) return done(err);
|
3224 | (list || []).map(toObject).should.eql([
|
3225 | {id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
3226 | {id: '2', name: 'new name', extra: undefined},
|
3227 | ]);
|
3228 | done();
|
3229 | });
|
3230 | },
|
3231 | );
|
3232 | });
|
3233 |
|
3234 | it('triggers `before save` hook', function(done) {
|
3235 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
3236 |
|
3237 | TestModel.updateAll(
|
3238 | {name: 'searched'},
|
3239 | {name: 'updated'},
|
3240 | function(err, instance) {
|
3241 | if (err) return done(err);
|
3242 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
3243 | where: {name: 'searched'},
|
3244 | data: {name: 'updated'},
|
3245 | }));
|
3246 | done();
|
3247 | },
|
3248 | );
|
3249 | });
|
3250 |
|
3251 | it('applies updates from `before save` hook', function(done) {
|
3252 | TestModel.observe('before save', function(ctx, next) {
|
3253 | ctx.data = {name: 'hooked', extra: 'added'};
|
3254 | next();
|
3255 | });
|
3256 |
|
3257 | TestModel.updateAll(
|
3258 | {id: existingInstance.id},
|
3259 | {name: 'updated name'},
|
3260 | function(err) {
|
3261 | if (err) return done(err);
|
3262 | loadTestModel(existingInstance.id, function(err, instance) {
|
3263 | if (err) return done(err);
|
3264 | instance.should.have.property('name', 'hooked');
|
3265 | instance.should.have.property('extra', 'added');
|
3266 | done();
|
3267 | });
|
3268 | },
|
3269 | );
|
3270 | });
|
3271 |
|
3272 | it('triggers `persist` hook', function(done) {
|
3273 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
3274 |
|
3275 | TestModel.updateAll(
|
3276 | {name: existingInstance.name},
|
3277 | {name: 'changed'},
|
3278 | function(err, instance) {
|
3279 | if (err) return done(err);
|
3280 |
|
3281 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
3282 | data: {name: 'changed'},
|
3283 | where: {name: existingInstance.name},
|
3284 | }));
|
3285 |
|
3286 | done();
|
3287 | },
|
3288 | );
|
3289 | });
|
3290 |
|
3291 | it('applies updates from `persist` hook', function(done) {
|
3292 | TestModel.observe('persist', ctxRecorder.recordAndNext(function(ctx) {
|
3293 |
|
3294 | ctx.data = Object.assign({}, ctx.data, {extra: 'hook data'});
|
3295 | }));
|
3296 |
|
3297 | TestModel.updateAll(
|
3298 | {id: existingInstance.id},
|
3299 | {name: 'changed'},
|
3300 | function(err) {
|
3301 | if (err) return done(err);
|
3302 | loadTestModel(existingInstance.id, function(err, instance) {
|
3303 | instance.should.have.property('extra', 'hook data');
|
3304 | done();
|
3305 | });
|
3306 | },
|
3307 | );
|
3308 | });
|
3309 |
|
3310 | it('does not trigger `loaded`', function(done) {
|
3311 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
3312 |
|
3313 | TestModel.updateAll(
|
3314 | {id: existingInstance.id},
|
3315 | {name: 'changed'},
|
3316 | function(err, instance) {
|
3317 | if (err) return done(err);
|
3318 | ctxRecorder.records.should.eql('hook not called');
|
3319 | done();
|
3320 | },
|
3321 | );
|
3322 | });
|
3323 |
|
3324 | it('triggers `after save` hook', function(done) {
|
3325 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
3326 |
|
3327 | TestModel.updateAll(
|
3328 | {id: existingInstance.id},
|
3329 | {name: 'updated name'},
|
3330 | function(err) {
|
3331 | if (err) return done(err);
|
3332 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
3333 | where: {id: existingInstance.id},
|
3334 | data: {name: 'updated name'},
|
3335 | info: {count: 1},
|
3336 | }));
|
3337 | done();
|
3338 | },
|
3339 | );
|
3340 | });
|
3341 |
|
3342 | it('accepts hookState from options', function(done) {
|
3343 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
3344 |
|
3345 | TestModel.updateAll(
|
3346 | {id: existingInstance.id},
|
3347 | {name: 'updated name'},
|
3348 | {foo: 'bar'},
|
3349 | function(err) {
|
3350 | if (err) return done(err);
|
3351 | ctxRecorder.records.options.should.eql({
|
3352 | foo: 'bar',
|
3353 | });
|
3354 | done();
|
3355 | },
|
3356 | );
|
3357 | });
|
3358 | });
|
3359 |
|
3360 | describe('PersistedModel.upsertWithWhere', function() {
|
3361 | it('triggers hooks in the correct order on create', function(done) {
|
3362 | monitorHookExecution();
|
3363 | TestModel.upsertWithWhere({extra: 'not-found'},
|
3364 | {id: 'not-found', name: 'not found', extra: 'not-found'},
|
3365 | function(err, record, created) {
|
3366 | if (err) return done(err);
|
3367 | hookMonitor.names.should.eql([
|
3368 | 'access',
|
3369 | 'before save',
|
3370 | 'persist',
|
3371 | 'loaded',
|
3372 | 'after save',
|
3373 | ]);
|
3374 | TestModel.findById('not-found', function(err, data) {
|
3375 | if (err) return done(err);
|
3376 | data.name.should.equal('not found');
|
3377 | data.extra.should.equal('not-found');
|
3378 | done();
|
3379 | });
|
3380 | });
|
3381 | });
|
3382 |
|
3383 | it('triggers hooks in the correct order on update', function(done) {
|
3384 | monitorHookExecution();
|
3385 | TestModel.upsertWithWhere({id: existingInstance.id},
|
3386 | {name: 'new name', extra: 'new extra'},
|
3387 | function(err, record, created) {
|
3388 | if (err) return done(err);
|
3389 | hookMonitor.names.should.eql([
|
3390 | 'access',
|
3391 | 'before save',
|
3392 | 'persist',
|
3393 | 'loaded',
|
3394 | 'after save',
|
3395 | ]);
|
3396 | TestModel.findById(existingInstance.id, function(err, data) {
|
3397 | if (err) return done(err);
|
3398 | data.name.should.equal('new name');
|
3399 | data.extra.should.equal('new extra');
|
3400 | done();
|
3401 | });
|
3402 | });
|
3403 | });
|
3404 |
|
3405 | it('triggers `access` hook on create', function(done) {
|
3406 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
3407 |
|
3408 | TestModel.upsertWithWhere({extra: 'not-found'},
|
3409 | {id: 'not-found', name: 'not found'},
|
3410 | function(err, instance) {
|
3411 | if (err) return done(err);
|
3412 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
3413 | where: {extra: 'not-found'},
|
3414 | }}));
|
3415 | done();
|
3416 | });
|
3417 | });
|
3418 |
|
3419 | it('triggers `access` hook on update', function(done) {
|
3420 | TestModel.observe('access', ctxRecorder.recordAndNext());
|
3421 |
|
3422 | TestModel.upsertWithWhere({id: existingInstance.id},
|
3423 | {name: 'new name', extra: 'new extra'},
|
3424 | function(err, instance) {
|
3425 | if (err) return done(err);
|
3426 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {query: {
|
3427 | where: {id: existingInstance.id},
|
3428 | }}));
|
3429 | done();
|
3430 | });
|
3431 | });
|
3432 |
|
3433 | it('triggers hooks only once', function(done) {
|
3434 | monitorHookExecution(['access', 'before save']);
|
3435 |
|
3436 | TestModel.observe('access', function(ctx, next) {
|
3437 | ctx.query = {where: {id: {neq: existingInstance.id}}};
|
3438 | next();
|
3439 | });
|
3440 |
|
3441 | TestModel.upsertWithWhere({id: existingInstance.id},
|
3442 | {name: 'new name'},
|
3443 | function(err, instance) {
|
3444 | if (err) return done(err);
|
3445 | hookMonitor.names.should.eql(['access', 'before save']);
|
3446 | done();
|
3447 | });
|
3448 | });
|
3449 |
|
3450 | it('applies updates from `access` hook when found', function(done) {
|
3451 | TestModel.observe('access', function(ctx, next) {
|
3452 | ctx.query = {where: {id: {neq: existingInstance.id}}};
|
3453 | next();
|
3454 | });
|
3455 |
|
3456 | TestModel.upsertWithWhere({id: existingInstance.id},
|
3457 | {name: 'new name'},
|
3458 | function(err, instance) {
|
3459 | if (err) return done(err);
|
3460 | findTestModels({fields: ['id', 'name']}, function(err, list) {
|
3461 | if (err) return done(err);
|
3462 | (list || []).map(toObject).should.eql([
|
3463 | {id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
3464 | {id: instance.id, name: 'new name', extra: undefined},
|
3465 | ]);
|
3466 | done();
|
3467 | });
|
3468 | });
|
3469 | });
|
3470 |
|
3471 | it('applies updates from `access` hook when not found', function(done) {
|
3472 | TestModel.observe('access', function(ctx, next) {
|
3473 | ctx.query = {where: {id: 'not-found'}};
|
3474 | next();
|
3475 | });
|
3476 |
|
3477 | TestModel.upsertWithWhere({id: existingInstance.id},
|
3478 | {name: 'new name'},
|
3479 | function(err, instance) {
|
3480 | if (err) return done(err);
|
3481 | findTestModels({fields: ['id', 'name']}, function(err, list) {
|
3482 | if (err) return done(err);
|
3483 | (list || []).map(toObject).should.eql([
|
3484 | {id: existingInstance.id, name: existingInstance.name, extra: undefined},
|
3485 | {id: list[1].id, name: 'second', extra: undefined},
|
3486 | {id: instance.id, name: 'new name', extra: undefined},
|
3487 | ]);
|
3488 | done();
|
3489 | });
|
3490 | });
|
3491 | });
|
3492 |
|
3493 | it('triggers `before save` hook on update', function(done) {
|
3494 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
3495 |
|
3496 | TestModel.upsertWithWhere({id: existingInstance.id},
|
3497 | {id: existingInstance.id, name: 'updated name'},
|
3498 | function(err, instance) {
|
3499 | if (err) return done(err);
|
3500 | const expectedContext = aCtxForModel(TestModel, {
|
3501 | where: {id: existingInstance.id},
|
3502 | data: {
|
3503 | id: existingInstance.id,
|
3504 | name: 'updated name',
|
3505 | },
|
3506 | });
|
3507 | if (!dataSource.connector.upsertWithWhere) {
|
3508 |
|
3509 |
|
3510 |
|
3511 |
|
3512 |
|
3513 | expectedContext.currentInstance = {id: existingInstance.id, name: 'first', extra: null};
|
3514 | }
|
3515 | ctxRecorder.records.should.eql(expectedContext);
|
3516 | done();
|
3517 | });
|
3518 | });
|
3519 |
|
3520 | it('triggers `before save` hook on create', function(done) {
|
3521 | TestModel.observe('before save', ctxRecorder.recordAndNext());
|
3522 |
|
3523 | TestModel.upsertWithWhere({id: 'new-id'},
|
3524 | {id: 'new-id', name: 'a name'},
|
3525 | function(err, instance) {
|
3526 | if (err) return done(err);
|
3527 | const expectedContext = aCtxForModel(TestModel, {});
|
3528 |
|
3529 | if (dataSource.connector.upsertWithWhere) {
|
3530 | expectedContext.data = {id: 'new-id', name: 'a name'};
|
3531 | expectedContext.where = {id: 'new-id'};
|
3532 | } else {
|
3533 | expectedContext.instance = {id: 'new-id', name: 'a name', extra: null};
|
3534 | expectedContext.isNewInstance = true;
|
3535 | }
|
3536 | ctxRecorder.records.should.eql(expectedContext);
|
3537 | done();
|
3538 | });
|
3539 | });
|
3540 |
|
3541 | it('applies updates from `before save` hook on update', function(done) {
|
3542 | TestModel.observe('before save', function(ctx, next) {
|
3543 |
|
3544 | ctx.data = Object.assign({}, ctx.data, {name: 'hooked'});
|
3545 | next();
|
3546 | });
|
3547 |
|
3548 | TestModel.upsertWithWhere({id: existingInstance.id},
|
3549 | {name: 'updated name'},
|
3550 | function(err, instance) {
|
3551 | if (err) return done(err);
|
3552 | instance.name.should.equal('hooked');
|
3553 | done();
|
3554 | });
|
3555 | });
|
3556 |
|
3557 | it('applies updates from `before save` hook on create', function(done) {
|
3558 | TestModel.observe('before save', function(ctx, next) {
|
3559 | if (ctx.instance) {
|
3560 | ctx.instance.name = 'hooked';
|
3561 | } else {
|
3562 |
|
3563 | ctx.data = Object.assign({}, ctx.data, {name: 'hooked'});
|
3564 | }
|
3565 | next();
|
3566 | });
|
3567 |
|
3568 | TestModel.upsertWithWhere({id: 'new-id'},
|
3569 | {id: 'new-id', name: 'new name'},
|
3570 | function(err, instance) {
|
3571 | if (err) return done(err);
|
3572 | instance.name.should.equal('hooked');
|
3573 | done();
|
3574 | });
|
3575 | });
|
3576 |
|
3577 | it('validates model after `before save` hook on create', function(done) {
|
3578 | TestModel.observe('before save', invalidateTestModel());
|
3579 |
|
3580 | TestModel.upsertWithWhere({id: 'new-id'},
|
3581 | {id: 'new-id', name: 'new name'},
|
3582 | function(err, instance) {
|
3583 | (err || {}).should.be.instanceOf(ValidationError);
|
3584 | (err.details.codes || {}).should.eql({name: ['presence']});
|
3585 | done();
|
3586 | });
|
3587 | });
|
3588 |
|
3589 | it('validates model after `before save` hook on update', function(done) {
|
3590 | TestModel.observe('before save', invalidateTestModel());
|
3591 |
|
3592 | TestModel.upsertWithWhere({id: existingInstance.id},
|
3593 | {id: existingInstance.id, name: 'updated name'},
|
3594 | function(err, instance) {
|
3595 | (err || {}).should.be.instanceOf(ValidationError);
|
3596 | (err.details.codes || {}).should.eql({name: ['presence']});
|
3597 | done();
|
3598 | });
|
3599 | });
|
3600 |
|
3601 | it('triggers `persist` hook on create', function(done) {
|
3602 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
3603 |
|
3604 | TestModel.upsertWithWhere({id: 'new-id'},
|
3605 | {id: 'new-id', name: 'a name'},
|
3606 | function(err, instance) {
|
3607 | if (err) return done(err);
|
3608 |
|
3609 | const expectedContext = aCtxForModel(TestModel, {
|
3610 | data: {id: 'new-id', name: 'a name'},
|
3611 | currentInstance: {
|
3612 | id: 'new-id',
|
3613 | name: 'a name',
|
3614 | extra: undefined,
|
3615 | },
|
3616 | });
|
3617 | if (dataSource.connector.upsertWithWhere) {
|
3618 | expectedContext.where = {id: 'new-id'};
|
3619 | } else {
|
3620 | expectedContext.isNewInstance = true;
|
3621 | }
|
3622 |
|
3623 | ctxRecorder.records.should.eql(expectedContext);
|
3624 | done();
|
3625 | });
|
3626 | });
|
3627 |
|
3628 | it('triggers persist hook on update', function(done) {
|
3629 | TestModel.observe('persist', ctxRecorder.recordAndNext());
|
3630 |
|
3631 | TestModel.upsertWithWhere({id: existingInstance.id},
|
3632 | {id: existingInstance.id, name: 'updated name'},
|
3633 | function(err, instance) {
|
3634 | if (err) return done(err);
|
3635 | const expectedContext = aCtxForModel(TestModel, {
|
3636 | where: {id: existingInstance.id},
|
3637 | data: {
|
3638 | id: existingInstance.id,
|
3639 | name: 'updated name',
|
3640 | },
|
3641 | currentInstance: {
|
3642 | id: existingInstance.id,
|
3643 | name: 'updated name',
|
3644 | extra: undefined,
|
3645 | },
|
3646 | });
|
3647 | if (!dataSource.connector.upsertWithWhere) {
|
3648 | expectedContext.isNewInstance = false;
|
3649 | }
|
3650 | ctxRecorder.records.should.eql(expectedContext);
|
3651 | done();
|
3652 | });
|
3653 | });
|
3654 |
|
3655 | it('triggers `loaded` hook on create', function(done) {
|
3656 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
3657 |
|
3658 | TestModel.upsertWithWhere({id: 'new-id'},
|
3659 | {id: 'new-id', name: 'a name'},
|
3660 | function(err, instance) {
|
3661 | if (err) return done(err);
|
3662 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
3663 | data: {id: 'new-id', name: 'a name'},
|
3664 | isNewInstance: true,
|
3665 | }));
|
3666 | done();
|
3667 | });
|
3668 | });
|
3669 |
|
3670 | it('triggers `loaded` hook on update', function(done) {
|
3671 | TestModel.observe('loaded', ctxRecorder.recordAndNext());
|
3672 |
|
3673 | TestModel.upsertWithWhere({id: existingInstance.id},
|
3674 | {id: existingInstance.id, name: 'updated name'},
|
3675 | function(err, instance) {
|
3676 | if (err) return done(err);
|
3677 | const expectedContext = aCtxForModel(TestModel, {
|
3678 | data: {
|
3679 | id: existingInstance.id,
|
3680 | name: 'updated name',
|
3681 | },
|
3682 | isNewInstance: false,
|
3683 | });
|
3684 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, expectedContext));
|
3685 | done();
|
3686 | });
|
3687 | });
|
3688 |
|
3689 | it('emits error when `loaded` hook fails', function(done) {
|
3690 | TestModel.observe('loaded', nextWithError(expectedError));
|
3691 | TestModel.upsertWithWhere({id: 'new-id'},
|
3692 | {id: 'new-id', name: 'a name'},
|
3693 | function(err, instance) {
|
3694 | [err].should.eql([expectedError]);
|
3695 | done();
|
3696 | });
|
3697 | });
|
3698 |
|
3699 | it('triggers `after save` hook on update', function(done) {
|
3700 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
3701 |
|
3702 | TestModel.upsertWithWhere({id: existingInstance.id},
|
3703 | {id: existingInstance.id, name: 'updated name'},
|
3704 | function(err, instance) {
|
3705 | if (err) return done(err);
|
3706 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
3707 | instance: {
|
3708 | id: existingInstance.id,
|
3709 | name: 'updated name',
|
3710 | extra: undefined,
|
3711 | },
|
3712 | isNewInstance: false,
|
3713 | }));
|
3714 | done();
|
3715 | });
|
3716 | });
|
3717 |
|
3718 | it('triggers `after save` hook on create', function(done) {
|
3719 | TestModel.observe('after save', ctxRecorder.recordAndNext());
|
3720 |
|
3721 | TestModel.upsertWithWhere({id: 'new-id'},
|
3722 | {id: 'new-id', name: 'a name'}, function(err, instance) {
|
3723 | if (err) return done(err);
|
3724 | ctxRecorder.records.should.eql(aCtxForModel(TestModel, {
|
3725 | instance: {
|
3726 | id: instance.id,
|
3727 | name: 'a name',
|
3728 | extra: undefined,
|
3729 | },
|
3730 | isNewInstance: true,
|
3731 | }));
|
3732 | done();
|
3733 | });
|
3734 | });
|
3735 | });
|
3736 |
|
3737 | function nextWithError(err) {
|
3738 | return function(context, next) {
|
3739 | next(err);
|
3740 | };
|
3741 | }
|
3742 |
|
3743 | function invalidateTestModel() {
|
3744 | return function(context, next) {
|
3745 | if (context.instance) {
|
3746 | context.instance.name = '';
|
3747 | } else {
|
3748 | context.data.name = '';
|
3749 | }
|
3750 | next();
|
3751 | };
|
3752 | }
|
3753 |
|
3754 | function findTestModels(query, cb) {
|
3755 | if (cb === undefined && typeof query === 'function') {
|
3756 | cb = query;
|
3757 | query = null;
|
3758 | }
|
3759 |
|
3760 | TestModel.find(query, {notify: false}, cb);
|
3761 | }
|
3762 |
|
3763 | function loadTestModel(id, cb) {
|
3764 | TestModel.findOne({where: {id: id}}, {notify: false}, cb);
|
3765 | }
|
3766 |
|
3767 | function monitorHookExecution(hookNames) {
|
3768 | hookMonitor.install(TestModel, hookNames);
|
3769 | }
|
3770 |
|
3771 | require('./operation-hooks.suite')(dataSource, should, connectorCapabilities);
|
3772 | });
|
3773 |
|
3774 | function get(propertyName) {
|
3775 | return function(obj) {
|
3776 | return obj[propertyName];
|
3777 | };
|
3778 | }
|
3779 |
|
3780 | function toObject(obj) {
|
3781 | return obj.toObject ? obj.toObject() : obj;
|
3782 | }
|
3783 | };
|