UNPKG

92.8 kBJavaScriptView Raw
1// Copyright IBM Corp. 2013,2019. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6// This test written in mocha+should.js
7'use strict';
8
9/* global getSchema:false, connectorCapabilities:false */
10const async = require('async');
11const bdd = require('./helpers/bdd-if');
12const should = require('./init.js');
13const uid = require('./helpers/uid-generator');
14
15let db, Person;
16const ValidationError = require('..').ValidationError;
17
18const UUID_REGEXP = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
19
20const throwingSetter = (value) => {
21 if (!value) return; // no-op
22 throw new Error('Intentional error triggered from a property setter');
23};
24
25describe('manipulation', function() {
26 before(function(done) {
27 db = getSchema();
28
29 Person = db.define('Person', {
30 name: String,
31 gender: String,
32 married: Boolean,
33 age: {type: Number, index: true},
34 dob: Date,
35 createdAt: {type: Date, default: Date},
36 throwingSetter: {type: String, default: null},
37 }, {forceId: true, strict: true});
38
39 Person.setter.throwingSetter = throwingSetter;
40
41 db.automigrate(['Person'], done);
42 });
43
44 // A simplified implementation of LoopBack's User model
45 // to reproduce problems related to properties with dynamic setters
46 // For the purpose of the tests, we use a counter instead of a hash fn.
47 let StubUser;
48 let stubPasswordCounter;
49
50 before(function setupStubUserModel(done) {
51 StubUser = db.createModel('StubUser', {password: String}, {forceId: true});
52 StubUser.setter.password = function(plain) {
53 if (plain.length === 0) throw new Error('password cannot be empty');
54 let hashed = false;
55 if (!plain) return;
56 const pos = plain.indexOf('-');
57 if (pos !== -1) {
58 const head = plain.substr(0, pos);
59 const tail = plain.substr(pos + 1, plain.length);
60 hashed = head.toUpperCase() === tail;
61 }
62 if (hashed) return;
63 this.$password = plain + '-' + plain.toUpperCase();
64 };
65 db.automigrate('StubUser', done);
66 });
67
68 beforeEach(function resetStubPasswordCounter() {
69 stubPasswordCounter = 0;
70 });
71
72 describe('create', function() {
73 before(function(done) {
74 Person.destroyAll(done);
75 });
76
77 describe('forceId', function() {
78 let TestForceId;
79 before(function(done) {
80 TestForceId = db.define('TestForceId');
81 db.automigrate('TestForceId', done);
82 });
83
84 it('it defaults to forceId:true for generated id property', function(done) {
85 TestForceId.create({id: 1}, function(err, t) {
86 should.exist(err);
87 err.message.should.match(/can\'t be set/);
88 done();
89 });
90 });
91 });
92
93 it('should create instance', function(done) {
94 Person.create({name: 'Anatoliy'}, function(err, p) {
95 if (err) return done(err);
96 should.exist(p);
97 p.name.should.equal('Anatoliy');
98 Person.findById(p.id, function(err, person) {
99 if (err) return done(err);
100 person.id.should.eql(p.id);
101 person.name.should.equal('Anatoliy');
102 done();
103 });
104 });
105 });
106
107 it('should create instance (promise variant)', function(done) {
108 Person.create({name: 'Anatoliy'})
109 .then(function(p) {
110 p.name.should.equal('Anatoliy');
111 should.exist(p);
112 return Person.findById(p.id)
113 .then(function(person) {
114 person.id.should.eql(p.id);
115 person.name.should.equal('Anatoliy');
116 done();
117 });
118 })
119 .catch(done);
120 });
121
122 it('should return rejected promise when model initialization failed', async () => {
123 await Person.create({name: 'Sad Fail', age: 25, throwingSetter: 'something'}).should
124 .be.rejectedWith('Intentional error triggered from a property setter');
125 });
126
127 it('should instantiate an object', function(done) {
128 const p = new Person({name: 'Anatoliy'});
129 p.name.should.equal('Anatoliy');
130 p.isNewRecord().should.be.true;
131 p.save(function(err, inst) {
132 if (err) return done(err);
133 inst.isNewRecord().should.be.false;
134 inst.should.equal(p);
135 done();
136 });
137 });
138
139 it('should instantiate an object (promise variant)', function(done) {
140 const p = new Person({name: 'Anatoliy'});
141 p.name.should.equal('Anatoliy');
142 p.isNewRecord().should.be.true;
143 p.save()
144 .then(function(inst) {
145 inst.isNewRecord().should.be.false;
146 inst.should.equal(p);
147 done();
148 })
149 .catch(done);
150 });
151
152 it('should not return instance of object', function(done) {
153 const person = Person.create(function(err, p) {
154 if (err) return done(err);
155 should.exist(p.id);
156 if (person) person.should.not.be.an.instanceOf(Person);
157 done();
158 });
159 });
160
161 it('should not allow user-defined value for the id of object - create', function(done) {
162 Person.create({id: 123456}, function(err, p) {
163 err.should.be.instanceof(ValidationError);
164 err.statusCode.should.equal(422);
165 err.details.messages.id.should.eql(['can\'t be set']);
166 p.should.be.instanceof(Person);
167 p.isNewRecord().should.be.true;
168 done();
169 });
170 });
171
172 it('should not allow user-defined value for the id of object - create (promise variant)', function(done) {
173 Person.create({id: 123456})
174 .then(function(p) {
175 done(new Error('Person.create should have failed.'));
176 }, function(err) {
177 err.should.be.instanceof(ValidationError);
178 err.statusCode.should.equal(422);
179 err.details.messages.id.should.eql(['can\'t be set']);
180 done();
181 })
182 .catch(done);
183 });
184
185 it('should not allow user-defined value for the id of object - save', function(done) {
186 const p = new Person({id: 123456});
187 p.isNewRecord().should.be.true;
188 p.save(function(err, inst) {
189 err.should.be.instanceof(ValidationError);
190 err.statusCode.should.equal(422);
191 err.details.messages.id.should.eql(['can\'t be set']);
192 inst.isNewRecord().should.be.true;
193 done();
194 });
195 });
196
197 it('should not allow user-defined value for the id of object - save (promise variant)', function(done) {
198 const p = new Person({id: 123456});
199 p.isNewRecord().should.be.true;
200 p.save()
201 .then(function(inst) {
202 done(new Error('save should have failed.'));
203 }, function(err) {
204 err.should.be.instanceof(ValidationError);
205 err.statusCode.should.equal(422);
206 err.details.messages.id.should.eql(['can\'t be set']);
207 done();
208 })
209 .catch(done);
210 });
211
212 it('should work when called without callback', function(done) {
213 Person.afterCreate = function(next) {
214 this.should.be.an.instanceOf(Person);
215 this.name.should.equal('Nickolay');
216 should.exist(this.id);
217 Person.afterCreate = null;
218 next();
219 setTimeout(done, 10);
220 };
221 Person.create({name: 'Nickolay'});
222 });
223
224 it('should create instance with blank data', function(done) {
225 Person.create(function(err, p) {
226 if (err) return done(err);
227 should.exist(p);
228 should.not.exists(p.name);
229 Person.findById(p.id, function(err, person) {
230 if (err) return done(err);
231 person.id.should.eql(p.id);
232 should.not.exists(person.name);
233 done();
234 });
235 });
236 });
237
238 it('should create instance with blank data (promise variant)', function(done) {
239 Person.create()
240 .then(function(p) {
241 should.exist(p);
242 should.not.exists(p.name);
243 return Person.findById(p.id)
244 .then(function(person) {
245 person.id.should.eql(p.id);
246 should.not.exists(person.name);
247 done();
248 });
249 }).catch(done);
250 });
251
252 it('should work when called with no data and callback', function(done) {
253 Person.afterCreate = function(next) {
254 this.should.be.an.instanceOf(Person);
255 should.not.exist(this.name);
256 should.exist(this.id);
257 Person.afterCreate = null;
258 next();
259 setTimeout(done, 30);
260 };
261 Person.create();
262 });
263
264 it('should create batch of objects', function(done) {
265 const batch = [
266 {name: 'Shaltay'},
267 {name: 'Boltay'},
268 {},
269 ];
270 const res = Person.create(batch, function(e, ps) {
271 if (res) res.should.not.be.instanceOf(Array);
272 should.not.exist(e);
273 should.exist(ps);
274 ps.should.be.instanceOf(Array);
275 ps.should.have.lengthOf(batch.length);
276
277 Person.validatesPresenceOf('name');
278 Person.create(batch, function(errors, persons) {
279 delete Person.validations;
280 should.exist(errors);
281 errors.should.have.lengthOf(batch.length);
282 should.not.exist(errors[0]);
283 should.not.exist(errors[1]);
284 should.exist(errors[2]);
285
286 should.exist(persons);
287 persons.should.have.lengthOf(batch.length);
288 persons[0].errors.should.be.false;
289 done();
290 });
291 });
292 });
293
294 it('should create batch of objects (promise variant)', function(done) {
295 const batch = [
296 {name: 'ShaltayPromise'},
297 {name: 'BoltayPromise'},
298 {},
299 ];
300 Person.create(batch).then(function(ps) {
301 should.exist(ps);
302 ps.should.be.instanceOf(Array);
303 ps.should.have.lengthOf(batch.length);
304
305 Person.validatesPresenceOf('name');
306 Person.create(batch, function(errors, persons) {
307 delete Person.validations;
308 should.exist(errors);
309 errors.should.have.lengthOf(batch.length);
310 should.not.exist(errors[0]);
311 should.not.exist(errors[1]);
312 should.exist(errors[2]);
313
314 should.exist(persons);
315 persons.should.have.lengthOf(batch.length);
316 persons[0].errors.should.be.false;
317 done();
318 });
319 });
320 });
321
322 it('should create batch of objects with beforeCreate', function(done) {
323 Person.beforeCreate = function(next, data) {
324 if (data && data.name === 'A') {
325 return next(null, {id: 'a', name: 'A'});
326 } else {
327 return next();
328 }
329 };
330 const batch = [
331 {name: 'A'},
332 {name: 'B'},
333 undefined,
334 ];
335 Person.create(batch, function(e, ps) {
336 should.not.exist(e);
337 should.exist(ps);
338 ps.should.be.instanceOf(Array);
339 ps.should.have.lengthOf(batch.length);
340 ps[0].should.be.eql({id: 'a', name: 'A'});
341 done();
342 });
343 });
344
345 it('should preserve properties with "undefined" value', function(done) {
346 Person.create(
347 {name: 'a-name', gender: undefined},
348 function(err, created) {
349 if (err) return done(err);
350 created.toObject().should.have.properties({
351 id: created.id,
352 name: 'a-name',
353 gender: undefined,
354 });
355
356 Person.findById(created.id, function(err, found) {
357 if (err) return done(err);
358 const result = found.toObject();
359 result.should.containEql({
360 id: created.id,
361 name: 'a-name',
362 });
363 // The gender can be null from a RDB
364 should.equal(result.gender, null);
365 done();
366 });
367 },
368 );
369 });
370
371 bdd.itIf(connectorCapabilities.refuseDuplicateInsert !== false, 'should refuse to create ' +
372 'object with duplicate id', function(done) {
373 // NOTE(bajtos) We cannot reuse Person model here,
374 // `settings.forceId` aborts the CREATE request at the validation step.
375 const Product = db.define('ProductTest', {name: String}, {forceId: false});
376 db.automigrate('ProductTest', function(err) {
377 if (err) return done(err);
378
379 Product.create({name: 'a-name'}, function(err, p) {
380 if (err) return done(err);
381 Product.create({id: p.id, name: 'duplicate'}, function(err, result) {
382 if (!err) {
383 return done(new Error('Create should have rejected duplicate id.'));
384 }
385 err.message.should.match(/duplicate/i);
386 done();
387 });
388 });
389 });
390 });
391 });
392
393 describe('save', function() {
394 it('should save new object', function(done) {
395 const p = new Person;
396 should.not.exist(p.id);
397 p.save(function(err) {
398 if (err) return done(err);
399 should.exist(p.id);
400 done();
401 });
402 });
403
404 it('should save new object (promise variant)', function(done) {
405 const p = new Person;
406 should.not.exist(p.id);
407 p.save()
408 .then(function() {
409 should.exist(p.id);
410 done();
411 })
412 .catch(done);
413 });
414
415 bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
416 'should save existing object', function(done) {
417 // Cloudant could not guarantee findOne always return the same item
418 Person.findOne(function(err, p) {
419 if (err) return done(err);
420 p.name = 'Hans';
421 p.save(function(err) {
422 if (err) return done(err);
423 p.name.should.equal('Hans');
424 Person.findOne(function(err, p) {
425 if (err) return done(err);
426 p.name.should.equal('Hans');
427 done();
428 });
429 });
430 });
431 });
432
433 bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
434 'should save existing object (promise variant)', function(done) {
435 // Cloudant could not guarantee findOne always return the same item
436 Person.findOne()
437 .then(function(p) {
438 p.name = 'Fritz';
439 return p.save()
440 .then(function() {
441 return Person.findOne()
442 .then(function(p) {
443 p.name.should.equal('Fritz');
444 done();
445 });
446 });
447 })
448 .catch(done);
449 });
450
451 it('should save invalid object (skipping validation)', function(done) {
452 Person.findOne(function(err, p) {
453 if (err) return done(err);
454 p.isValid = function(done) {
455 process.nextTick(done);
456 return false;
457 };
458 p.name = 'Nana';
459 p.save(function(err) {
460 should.exist(err);
461 p.save({validate: false}, function(err) {
462 if (err) return done(err);
463 done();
464 });
465 });
466 });
467 });
468
469 it('should save invalid object (skipping validation - promise variant)', function(done) {
470 Person.findOne()
471 .then(function(p) {
472 p.isValid = function(done) {
473 process.nextTick(done);
474 return false;
475 };
476 p.name = 'Nana';
477 return p.save()
478 .then(function(d) {
479 done(new Error('save should have failed.'));
480 }, function(err) {
481 should.exist(err);
482 p.save({validate: false})
483 .then(function(d) {
484 should.exist(d);
485 done();
486 });
487 });
488 })
489 .catch(done);
490 });
491
492 it('should save throw error on validation', function(done) {
493 Person.findOne(function(err, p) {
494 if (err) return done(err);
495 p.isValid = function(cb) {
496 cb(false);
497 return false;
498 };
499 (function() {
500 p.save({
501 'throws': true,
502 });
503 }).should.throw(ValidationError);
504 done();
505 });
506 });
507
508 it('should preserve properties with dynamic setters', function(done) {
509 // This test reproduces a problem discovered by LoopBack unit-test
510 // "User.hasPassword() should match a password after it is changed"
511 StubUser.create({password: 'foo'}, function(err, created) {
512 if (err) return done(err);
513 created.password.should.equal('foo-FOO');
514 created.password = 'bar';
515 created.save(function(err, saved) {
516 if (err) return done(err);
517 created.id.should.eql(saved.id);
518 saved.password.should.equal('bar-BAR');
519 StubUser.findById(created.id, function(err, found) {
520 if (err) return done(err);
521 created.id.should.eql(found.id);
522 found.password.should.equal('bar-BAR');
523 done();
524 });
525 });
526 });
527 });
528 });
529
530 describe('updateAttributes', function() {
531 let person;
532
533 before(function(done) {
534 Person.destroyAll(function(err) {
535 if (err) return done(err);
536 Person.create({name: 'Mary', age: 15}, function(err, p) {
537 if (err) return done(err);
538 person = p;
539 done();
540 });
541 });
542 });
543
544 it('should have updated password hashed with updateAttribute',
545 function(done) {
546 StubUser.create({password: 'foo'}, function(err, created) {
547 if (err) return done(err);
548 created.updateAttribute('password', 'test', function(err, created) {
549 if (err) return done(err);
550 created.password.should.equal('test-TEST');
551 StubUser.findById(created.id, function(err, found) {
552 if (err) return done(err);
553 found.password.should.equal('test-TEST');
554 done();
555 });
556 });
557 });
558 });
559
560 it('should reject created StubUser with empty password', function(done) {
561 StubUser.create({email: 'b@example.com', password: ''}, function(err, createdUser) {
562 (err.message).should.match(/password cannot be empty/);
563 done();
564 });
565 });
566
567 it('should reject updated empty password with updateAttribute', function(done) {
568 StubUser.create({password: 'abc123'}, function(err, createdUser) {
569 if (err) return done(err);
570 createdUser.updateAttribute('password', '', function(err, updatedUser) {
571 (err.message).should.match(/password cannot be empty/);
572 done();
573 });
574 });
575 });
576
577 it('should update one attribute', function(done) {
578 person.updateAttribute('name', 'Paul Graham', function(err, p) {
579 if (err) return done(err);
580 Person.all(function(e, ps) {
581 if (e) return done(e);
582 ps.should.have.lengthOf(1);
583 ps.pop().name.should.equal('Paul Graham');
584 done();
585 });
586 });
587 });
588
589 it('should update one attribute (promise variant)', function(done) {
590 person.updateAttribute('name', 'Teddy Graham')
591 .then(function(p) {
592 return Person.all()
593 .then(function(ps) {
594 ps.should.have.lengthOf(1);
595 ps.pop().name.should.equal('Teddy Graham');
596 done();
597 });
598 }).catch(done);
599 });
600
601 it('should ignore undefined values on updateAttributes', function(done) {
602 person.updateAttributes({'name': 'John', age: undefined},
603 function(err, p) {
604 if (err) return done(err);
605 Person.findById(p.id, function(e, p) {
606 if (e) return done(e);
607 p.name.should.equal('John');
608 p.age.should.equal(15);
609 done();
610 });
611 });
612 });
613
614 bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
615 'should discard undefined values before strict validation',
616 function(done) {
617 Person.definition.settings.strict = true;
618 Person.findById(person.id, function(err, p) {
619 if (err) return done(err);
620 p.updateAttributes({name: 'John', unknownVar: undefined},
621 function(err, p) {
622 // if uknownVar was defined, it would return validationError
623 if (err) return done(err);
624 person.id.should.eql(p.id);
625 Person.findById(p.id, function(e, p) {
626 if (e) return done(e);
627 p.name.should.equal('John');
628 p.should.not.have.property('unknownVar');
629 done();
630 });
631 });
632 });
633 });
634
635 it('should allow unknown attributes when strict: false',
636 function(done) {
637 Person.definition.settings.strict = false;
638 Person.findById(person.id, function(err, p) {
639 if (err) return done(err);
640 p.updateAttributes({name: 'John', foo: 'bar'},
641 function(err, p) {
642 if (err) return done(err);
643 p.should.have.property('foo');
644 done();
645 });
646 });
647 });
648
649 it('should remove unknown attributes when strict: filter',
650 function(done) {
651 Person.definition.settings.strict = 'filter';
652 Person.findById(person.id, function(err, p) {
653 if (err) return done(err);
654 p.updateAttributes({name: 'John', foo: 'bar'},
655 function(err, p) {
656 if (err) return done(err);
657 p.should.not.have.property('foo');
658 done();
659 });
660 });
661 });
662
663 // Prior to version 3.0 `strict: true` used to silently remove unknown properties,
664 // now return validationError upon unknown properties
665 it('should return error on unknown attributes when strict: true',
666 function(done) {
667 // Using {foo: 'bar'} only causes dependent test failures due to the
668 // stripping of object properties when in strict mode (ie. {foo: 'bar'}
669 // changes to '{}' and breaks other tests
670 Person.definition.settings.strict = true;
671 Person.findById(person.id, function(err, p) {
672 if (err) return done(err);
673 p.updateAttributes({name: 'John', foo: 'bar'},
674 function(err, p) {
675 should.exist(err);
676 err.name.should.equal('ValidationError');
677 err.message.should.containEql('`foo` is not defined in the model');
678 p.should.not.have.property('foo');
679 Person.findById(p.id, function(e, p) {
680 if (e) return done(e);
681 p.should.not.have.property('foo');
682 done();
683 });
684 });
685 });
686 });
687
688 // strict: throw is deprecated, use strict: true instead
689 // which returns Validation Error for unknown properties
690 it('should fallback to strict:true when using strict: throw', function(done) {
691 Person.definition.settings.strict = 'throw';
692 Person.findById(person.id, function(err, p) {
693 if (err) return done(err);
694 p.updateAttributes({foo: 'bar'},
695 function(err, p) {
696 should.exist(err);
697 err.name.should.equal('ValidationError');
698 err.message.should.containEql('`foo` is not defined in the model');
699 Person.findById(person.id, function(e, p) {
700 if (e) return done(e);
701 p.should.not.have.property('foo');
702 done();
703 });
704 });
705 });
706 });
707
708 // strict: validate is deprecated, use strict: true instead
709 // behavior remains the same as before, because validate is now default behavior
710 it('should fallback to strict:true when using strict:validate', function(done) {
711 Person.definition.settings.strict = 'validate';
712 Person.findById(person.id, function(err, p) {
713 if (err) return done(err);
714 p.updateAttributes({foo: 'bar'},
715 function(err, p) {
716 should.exist(err);
717 err.name.should.equal('ValidationError');
718 err.message.should.containEql('`foo` is not defined in the model');
719 Person.findById(person.id, function(e, p) {
720 if (e) return done(e);
721 p.should.not.have.property('foo');
722 done();
723 });
724 });
725 });
726 });
727
728 it('should allow same id value on updateAttributes', function(done) {
729 person.updateAttributes({id: person.id, name: 'John'},
730 function(err, p) {
731 if (err) return done(err);
732 Person.findById(p.id, function(e, p) {
733 if (e) return done(e);
734 p.name.should.equal('John');
735 p.age.should.equal(15);
736 done();
737 });
738 });
739 });
740
741 it('should allow same stringified id value on updateAttributes',
742 function(done) {
743 let pid = person.id;
744 if (typeof person.id === 'object' || typeof person.id === 'number') {
745 // For example MongoDB ObjectId
746 pid = person.id.toString();
747 }
748 person.updateAttributes({id: pid, name: 'John'},
749 function(err, p) {
750 if (err) return done(err);
751 Person.findById(p.id, function(e, p) {
752 if (e) return done(e);
753 p.name.should.equal('John');
754 p.age.should.equal(15);
755 done();
756 });
757 });
758 });
759
760 it('should fail if an id value is to be changed on updateAttributes',
761 function(done) {
762 person.updateAttributes({id: person.id + 1, name: 'John'},
763 function(err, p) {
764 should.exist(err);
765 done();
766 });
767 });
768
769 it('has an alias "patchAttributes"', function(done) {
770 person.updateAttributes.should.equal(person.patchAttributes);
771 done();
772 });
773
774 it('should allow model instance on updateAttributes', function(done) {
775 person.updateAttributes(new Person({'name': 'John', age: undefined}),
776 function(err, p) {
777 if (err) return done(err);
778 Person.findById(p.id, function(e, p) {
779 if (e) return done(e);
780 p.name.should.equal('John');
781 p.age.should.equal(15);
782 done();
783 });
784 });
785 });
786
787 it('should allow model instance on updateAttributes (promise variant)', function(done) {
788 person.updateAttributes(new Person({'name': 'Jane', age: undefined}))
789 .then(function(p) {
790 return Person.findById(p.id)
791 .then(function(p) {
792 p.name.should.equal('Jane');
793 p.age.should.equal(15);
794 done();
795 });
796 })
797 .catch(done);
798 });
799
800 it('should raises on connector error', function(done) {
801 const fakeConnector = {
802 updateAttributes: function(model, id, data, options, cb) {
803 cb(new Error('Database Error'));
804 },
805 };
806 person.getConnector = function() { return fakeConnector; };
807 person.updateAttributes({name: 'John'}, function(err, p) {
808 should.exist(err);
809 done();
810 });
811 });
812 });
813
814 describe('updateOrCreate', function() {
815 let Post, Todo;
816
817 before('prepare "Post" and "Todo" models', function(done) {
818 Post = db.define('Post', {
819 title: {type: String, id: true},
820 content: {type: String},
821 });
822 Todo = db.define('Todo', {
823 content: String,
824 });
825 // Here `Person` model overrides the one outside 'updataOrCreate'
826 // with forceId: false. Related test cleanup see issue:
827 // https://github.com/strongloop/loopback-datasource-juggler/issues/1317
828 Person = db.define('Person', {
829 name: String,
830 gender: String,
831 married: Boolean,
832 age: {type: Number, index: true},
833 dob: Date,
834 createdAt: {type: Date, default: Date},
835 }, {forceId: false});
836 db.automigrate(['Post', 'Todo', 'Person'], done);
837 });
838
839 beforeEach(function deleteModelsInstances(done) {
840 Todo.deleteAll(done);
841 });
842
843 it('has an alias "patchOrCreate"', function() {
844 StubUser.updateOrCreate.should.equal(StubUser.patchOrCreate);
845 });
846
847 it('creates a model when one does not exist', function(done) {
848 Todo.updateOrCreate({content: 'a'}, function(err, data) {
849 if (err) return done(err);
850
851 Todo.findById(data.id, function(err, todo) {
852 should.exist(todo);
853 should.exist(todo.content);
854 todo.content.should.equal('a');
855
856 done();
857 });
858 });
859 });
860
861 it('updates a model if it exists', function(done) {
862 Todo.create({content: 'a'}, function(err, todo) {
863 Todo.updateOrCreate({id: todo.id, content: 'b'}, function(err, data) {
864 if (err) return done(err);
865
866 should.exist(data);
867 should.exist(data.id);
868 data.id.should.eql(todo.id);
869 should.exist(data.content);
870 data.content.should.equal('b');
871
872 done();
873 });
874 });
875 });
876
877 it('should reject updated empty password with updateOrCreate', function(done) {
878 StubUser.create({password: 'abc123'}, function(err, createdUser) {
879 if (err) return done(err);
880 StubUser.updateOrCreate({id: createdUser.id, 'password': ''}, function(err, updatedUser) {
881 (err.message).should.match(/password cannot be empty/);
882 done();
883 });
884 });
885 });
886
887 it('throws error for queries with array input', function(done) {
888 Todo.updateOrCreate([{content: 'a'}], function(err, data) {
889 should.exist(err);
890 err.message.should.containEql('bulk');
891 should.not.exist(data);
892
893 done();
894 });
895 });
896
897 it('should preserve properties with dynamic setters on create', function(done) {
898 StubUser.updateOrCreate({password: 'foo'}, function(err, created) {
899 if (err) return done(err);
900 created.password.should.equal('foo-FOO');
901 StubUser.findById(created.id, function(err, found) {
902 if (err) return done(err);
903 found.password.should.equal('foo-FOO');
904 done();
905 });
906 });
907 });
908
909 it('should preserve properties with dynamic setters on update', function(done) {
910 StubUser.create({password: 'foo'}, function(err, created) {
911 if (err) return done(err);
912 const data = {id: created.id, password: 'bar'};
913 StubUser.updateOrCreate(data, function(err, updated) {
914 if (err) return done(err);
915 updated.password.should.equal('bar-BAR');
916 StubUser.findById(created.id, function(err, found) {
917 if (err) return done(err);
918 found.password.should.equal('bar-BAR');
919 done();
920 });
921 });
922 });
923 });
924
925 it('should preserve properties with "undefined" value', function(done) {
926 Person.create(
927 {name: 'a-name', gender: undefined},
928 function(err, instance) {
929 if (err) return done(err);
930 const result = instance.toObject();
931 result.id.should.eql(instance.id);
932 should.equal(result.name, 'a-name');
933 should.equal(result.gender, undefined);
934
935 Person.updateOrCreate(
936 {id: instance.id, name: 'updated name'},
937 function(err, updated) {
938 if (err) return done(err);
939 const result = updated.toObject();
940 result.id.should.eql(instance.id);
941 should.equal(result.name, 'updated name');
942 should.equal(result.gender, null);
943
944 done();
945 },
946 );
947 },
948 );
949 });
950
951 it('updates specific instances when PK is not an auto-generated id', function(done) {
952 // skip the test if the connector is mssql
953 // https://github.com/strongloop/loopback-connector-mssql/pull/92#r72853474
954 const dsName = Post.dataSource.name;
955 if (dsName === 'mssql') return done();
956
957 Post.create([
958 {title: 'postA', content: 'contentA'},
959 {title: 'postB', content: 'contentB'},
960 ], function(err, instance) {
961 if (err) return done(err);
962
963 Post.updateOrCreate({
964 title: 'postA', content: 'newContent',
965 }, function(err, instance) {
966 if (err) return done(err);
967
968 const result = instance.toObject();
969 result.should.have.properties({
970 title: 'postA',
971 content: 'newContent',
972 });
973 Post.find(function(err, posts) {
974 if (err) return done(err);
975
976 posts.should.have.length(2);
977 posts[0].title.should.equal('postA');
978 posts[0].content.should.equal('newContent');
979 posts[1].title.should.equal('postB');
980 posts[1].content.should.equal('contentB');
981 done();
982 });
983 });
984 });
985 });
986
987 it('should allow save() of the created instance', function(done) {
988 const unknownId = uid.fromConnector(db) || 999;
989 Person.updateOrCreate(
990 {id: unknownId, name: 'a-name'},
991 function(err, inst) {
992 if (err) return done(err);
993 inst.save(done);
994 },
995 );
996 });
997
998 it('preserves empty values from the database', async () => {
999 // https://github.com/strongloop/loopback-datasource-juggler/issues/1692
1000
1001 // Initially, all Players were always active, no property was needed
1002 const Player = db.define('Player', {name: String});
1003
1004 await db.automigrate('Player');
1005 const created = await Player.create({name: 'Pen'});
1006
1007 // Later on, we decide to introduce `active` property
1008 Player.defineProperty('active', {
1009 type: Boolean,
1010 default: false,
1011 });
1012 await db.autoupdate('Player');
1013
1014 // And updateOrCreate an existing record
1015 const found = await Player.updateOrCreate({id: created.id, name: 'updated'});
1016 should(found.toObject().active).be.oneOf([
1017 undefined, // databases supporting `undefined` value
1018 null, // databases representing `undefined` as `null`
1019 ]);
1020 });
1021 });
1022
1023 bdd.describeIf(connectorCapabilities.supportForceId !== false,
1024 'updateOrCreate when forceId is true', function() {
1025 let Post;
1026 before(function definePostModel(done) {
1027 const ds = getSchema();
1028 Post = ds.define('Post', {
1029 title: {type: String, length: 255},
1030 content: {type: String},
1031 }, {forceId: true});
1032 ds.automigrate('Post', done);
1033 });
1034
1035 it('fails when id does not exist in db & validate is true', function(done) {
1036 const unknownId = uid.fromConnector(db) || 123;
1037 const post = {id: unknownId, title: 'a', content: 'AAA'};
1038 Post.updateOrCreate(post, {validate: true}, (err) => {
1039 should(err).have.property('statusCode', 404);
1040 done();
1041 });
1042 });
1043
1044 it('fails when id does not exist in db & validate is false', function(done) {
1045 const unknownId = uid.fromConnector(db) || 123;
1046 const post = {id: unknownId, title: 'a', content: 'AAA'};
1047 Post.updateOrCreate(post, {validate: false}, (err) => {
1048 should(err).have.property('statusCode', 404);
1049 done();
1050 });
1051 });
1052
1053 it('fails when id does not exist in db & validate is false when using updateAttributes',
1054 function(done) {
1055 const unknownId = uid.fromConnector(db) || 123;
1056 const post = new Post({id: unknownId});
1057 post.updateAttributes({title: 'updated title', content: 'AAA'}, {validate: false}, (err) => {
1058 should(err).have.property('statusCode', 404);
1059 done();
1060 });
1061 });
1062
1063 it('works on create if the request does not include an id', function(done) {
1064 const post = {title: 'a', content: 'AAA'};
1065 Post.updateOrCreate(post, (err, p) => {
1066 if (err) return done(err);
1067 p.title.should.equal(post.title);
1068 p.content.should.equal(post.content);
1069 done();
1070 });
1071 });
1072
1073 it('works on update if the request includes an existing id in db', function(done) {
1074 Post.create({title: 'a', content: 'AAA'}, (err, post) => {
1075 if (err) return done(err);
1076 post = post.toObject();
1077 delete post.content;
1078 post.title = 'b';
1079 Post.updateOrCreate(post, function(err, p) {
1080 if (err) return done(err);
1081 p.id.should.equal(post.id);
1082 p.title.should.equal('b');
1083 done();
1084 });
1085 });
1086 });
1087 });
1088
1089 const hasReplaceById = connectorCapabilities.cloudantCompatible !== false &&
1090 !!getSchema().connector.replaceById;
1091
1092 if (!hasReplaceById) {
1093 describe.skip('replaceById - not implemented', function() {});
1094 } else {
1095 describe('replaceOrCreate', function() {
1096 let Post, unknownId;
1097 before(function(done) {
1098 db = getSchema();
1099 unknownId = uid.fromConnector(db) || 123;
1100 Post = db.define('Post', {
1101 title: {type: String, length: 255, index: true},
1102 content: {type: String},
1103 comments: [String],
1104 }, {forceId: false});
1105 db.automigrate('Post', done);
1106 });
1107
1108 it('works without options on create (promise variant)', function(done) {
1109 const post = {id: unknownId, title: 'a', content: 'AAA'};
1110 Post.replaceOrCreate(post)
1111 .then(function(p) {
1112 should.exist(p);
1113 p.should.be.instanceOf(Post);
1114 p.id.should.eql(post.id);
1115 p.should.not.have.property('_id');
1116 p.title.should.equal(post.title);
1117 p.content.should.equal(post.content);
1118 return Post.findById(p.id)
1119 .then(function(p) {
1120 p.id.should.eql(post.id);
1121 p.id.should.not.have.property('_id');
1122 p.title.should.equal(p.title);
1123 p.content.should.equal(p.content);
1124 done();
1125 });
1126 })
1127 .catch(done);
1128 });
1129
1130 it('works with options on create (promise variant)', function(done) {
1131 const post = {id: unknownId, title: 'a', content: 'AAA'};
1132 Post.replaceOrCreate(post, {validate: false})
1133 .then(function(p) {
1134 should.exist(p);
1135 p.should.be.instanceOf(Post);
1136 p.id.should.eql(post.id);
1137 p.should.not.have.property('_id');
1138 p.title.should.equal(post.title);
1139 p.content.should.equal(post.content);
1140 return Post.findById(p.id)
1141 .then(function(p) {
1142 p.id.should.eql(post.id);
1143 p.id.should.not.have.property('_id');
1144 p.title.should.equal(p.title);
1145 p.content.should.equal(p.content);
1146 done();
1147 });
1148 })
1149 .catch(done);
1150 });
1151
1152 it('works without options on update (promise variant)', function(done) {
1153 const post = {title: 'a', content: 'AAA', comments: ['Comment1']};
1154 Post.create(post)
1155 .then(function(created) {
1156 created = created.toObject();
1157 delete created.comments;
1158 delete created.content;
1159 created.title = 'b';
1160 return Post.replaceOrCreate(created)
1161 .then(function(p) {
1162 should.exist(p);
1163 p.should.be.instanceOf(Post);
1164 p.id.should.eql(created.id);
1165 p.should.not.have.property('_id');
1166 p.title.should.equal('b');
1167 p.should.have.property('content').be.oneOf(null, undefined);
1168 p.should.have.property('comments').be.oneOf(null, undefined);
1169
1170 return Post.findById(created.id)
1171 .then(function(p) {
1172 p.should.not.have.property('_id');
1173 p.title.should.equal('b');
1174 should.not.exist(p.content);
1175 should.not.exist(p.comments);
1176 done();
1177 });
1178 });
1179 })
1180 .catch(done);
1181 });
1182
1183 it('works with options on update (promise variant)', function(done) {
1184 const post = {title: 'a', content: 'AAA', comments: ['Comment1']};
1185 Post.create(post)
1186 .then(function(created) {
1187 created = created.toObject();
1188 delete created.comments;
1189 delete created.content;
1190 created.title = 'b';
1191 return Post.replaceOrCreate(created, {validate: false})
1192 .then(function(p) {
1193 should.exist(p);
1194 p.should.be.instanceOf(Post);
1195 p.id.should.eql(created.id);
1196 p.should.not.have.property('_id');
1197 p.title.should.equal('b');
1198 p.should.have.property('content').be.oneOf(null, undefined);
1199 p.should.have.property('comments').be.oneOf(null, undefined);
1200
1201 return Post.findById(created.id)
1202 .then(function(p) {
1203 p.should.not.have.property('_id');
1204 p.title.should.equal('b');
1205 should.not.exist(p.content);
1206 should.not.exist(p.comments);
1207 done();
1208 });
1209 });
1210 })
1211 .catch(done);
1212 });
1213
1214 it('works without options on update (callback variant)', function(done) {
1215 Post.create({title: 'a', content: 'AAA', comments: ['Comment1']},
1216 function(err, post) {
1217 if (err) return done(err);
1218 post = post.toObject();
1219 delete post.comments;
1220 delete post.content;
1221 post.title = 'b';
1222 Post.replaceOrCreate(post, function(err, p) {
1223 if (err) return done(err);
1224 p.id.should.eql(post.id);
1225 p.should.not.have.property('_id');
1226 p.title.should.equal('b');
1227 p.should.have.property('content').be.oneOf(null, undefined);
1228 p.should.have.property('comments').be.oneOf(null, undefined);
1229
1230 Post.findById(post.id, function(err, p) {
1231 if (err) return done(err);
1232 p.id.should.eql(post.id);
1233 p.should.not.have.property('_id');
1234 p.title.should.equal('b');
1235 should.not.exist(p.content);
1236 should.not.exist(p.comments);
1237 done();
1238 });
1239 });
1240 });
1241 });
1242
1243 it('works with options on update (callback variant)', function(done) {
1244 Post.create({title: 'a', content: 'AAA', comments: ['Comment1']},
1245 {validate: false},
1246 function(err, post) {
1247 if (err) return done(err);
1248 post = post.toObject();
1249 delete post.comments;
1250 delete post.content;
1251 post.title = 'b';
1252 Post.replaceOrCreate(post, function(err, p) {
1253 if (err) return done(err);
1254 p.id.should.eql(post.id);
1255 p.should.not.have.property('_id');
1256 p.title.should.equal('b');
1257 p.should.have.property('content').be.oneOf(null, undefined);
1258 p.should.have.property('comments').be.oneOf(null, undefined);
1259
1260 Post.findById(post.id, function(err, p) {
1261 if (err) return done(err);
1262 p.id.should.eql(post.id);
1263 p.should.not.have.property('_id');
1264 p.title.should.equal('b');
1265 should.not.exist(p.content);
1266 should.not.exist(p.comments);
1267 done();
1268 });
1269 });
1270 });
1271 });
1272
1273 it('works without options on create (callback variant)', function(done) {
1274 const post = {id: unknownId, title: 'a', content: 'AAA'};
1275 Post.replaceOrCreate(post, function(err, p) {
1276 if (err) return done(err);
1277 p.id.should.eql(post.id);
1278 p.should.not.have.property('_id');
1279 p.title.should.equal(post.title);
1280 p.content.should.equal(post.content);
1281
1282 Post.findById(p.id, function(err, p) {
1283 if (err) return done(err);
1284 p.id.should.eql(post.id);
1285 p.should.not.have.property('_id');
1286 p.title.should.equal(post.title);
1287 p.content.should.equal(post.content);
1288 done();
1289 });
1290 });
1291 });
1292
1293 it('works with options on create (callback variant)', function(done) {
1294 const post = {id: unknownId, title: 'a', content: 'AAA'};
1295 Post.replaceOrCreate(post, {validate: false}, function(err, p) {
1296 if (err) return done(err);
1297 p.id.should.eql(post.id);
1298 p.should.not.have.property('_id');
1299 p.title.should.equal(post.title);
1300 p.content.should.equal(post.content);
1301
1302 Post.findById(p.id, function(err, p) {
1303 if (err) return done(err);
1304 p.id.should.eql(post.id);
1305 p.should.not.have.property('_id');
1306 p.title.should.equal(post.title);
1307 p.content.should.equal(post.content);
1308 done();
1309 });
1310 });
1311 });
1312 });
1313 }
1314
1315 bdd.describeIf(hasReplaceById && connectorCapabilities.supportForceId !== false, 'replaceOrCreate ' +
1316 'when forceId is true', function() {
1317 let Post, unknownId;
1318 before(function(done) {
1319 db = getSchema();
1320 unknownId = uid.fromConnector(db) || 123;
1321 Post = db.define('Post', {
1322 title: {type: String, length: 255},
1323 content: {type: String},
1324 }, {forceId: true});
1325 db.automigrate('Post', done);
1326 });
1327
1328 it('fails when id does not exist in db', function(done) {
1329 const post = {id: unknownId, title: 'a', content: 'AAA'};
1330
1331 Post.replaceOrCreate(post, function(err, p) {
1332 err.statusCode.should.equal(404);
1333 done();
1334 });
1335 });
1336
1337 // eslint-disable-next-line mocha/no-identical-title
1338 it('works on create if the request does not include an id', function(done) {
1339 const post = {title: 'a', content: 'AAA'};
1340 Post.replaceOrCreate(post, function(err, p) {
1341 if (err) return done(err);
1342 p.title.should.equal(post.title);
1343 p.content.should.equal(post.content);
1344 done();
1345 });
1346 });
1347
1348 // eslint-disable-next-line mocha/no-identical-title
1349 it('works on update if the request includes an existing id in db', function(done) {
1350 Post.create({title: 'a', content: 'AAA'},
1351 function(err, post) {
1352 if (err) return done(err);
1353 post = post.toObject();
1354 delete post.content;
1355 post.title = 'b';
1356 Post.replaceOrCreate(post, function(err, p) {
1357 if (err) return done(err);
1358 p.id.should.eql(post.id);
1359 done();
1360 });
1361 });
1362 });
1363 });
1364
1365 if (!hasReplaceById) {
1366 describe.skip('replaceAttributes/replaceById - not implemented', function() {});
1367 } else {
1368 describe('replaceAttributes', function() {
1369 let postInstance;
1370 let Post;
1371 const ds = getSchema();
1372 before(function(done) {
1373 Post = ds.define('Post', {
1374 title: {type: String, length: 255, index: true},
1375 content: {type: String},
1376 comments: [String],
1377 });
1378 ds.automigrate('Post', done);
1379 });
1380 beforeEach(function(done) {
1381 // TODO(bajtos) add API to lib/observer - remove observers for all hooks
1382 Post._observers = {};
1383 Post.destroyAll(function() {
1384 Post.create({title: 'a', content: 'AAA'}, function(err, p) {
1385 if (err) return done(err);
1386 postInstance = p;
1387 done();
1388 });
1389 });
1390 });
1391
1392 it('should have updated password hashed with replaceAttributes',
1393 function(done) {
1394 StubUser.create({password: 'foo'}, function(err, created) {
1395 if (err) return done(err);
1396 created.replaceAttributes({password: 'test'}, function(err, created) {
1397 if (err) return done(err);
1398 created.password.should.equal('test-TEST');
1399 StubUser.findById(created.id, function(err, found) {
1400 if (err) return done(err);
1401 found.password.should.equal('test-TEST');
1402 done();
1403 });
1404 });
1405 });
1406 });
1407
1408 it('should reject updated empty password with replaceAttributes', function(done) {
1409 StubUser.create({password: 'abc123'}, function(err, createdUser) {
1410 if (err) return done(err);
1411 createdUser.replaceAttributes({'password': ''}, function(err, updatedUser) {
1412 (err.message).should.match(/password cannot be empty/);
1413 done();
1414 });
1415 });
1416 });
1417
1418 it('should ignore PK if it is set for `instance`' +
1419 'in `before save` operation hook', function(done) {
1420 Post.findById(postInstance.id, function(err, p) {
1421 if (err) return done(err);
1422 changePostIdInHook('before save');
1423 p.replaceAttributes({title: 'b'}, function(err, data) {
1424 if (err) return done(err);
1425 data.id.should.eql(postInstance.id);
1426 Post.find(function(err, p) {
1427 if (err) return done(err);
1428 p[0].id.should.eql(postInstance.id);
1429 done();
1430 });
1431 });
1432 });
1433 });
1434
1435 it('should set cannotOverwritePKInBeforeSaveHook flag, if `instance` in' +
1436 '`before save` operation hook is set, so we report a warning just once',
1437 function(done) {
1438 Post.findById(postInstance.id, function(err, p) {
1439 if (err) return done(err);
1440 changePostIdInHook('before save');
1441 p.replaceAttributes({title: 'b'}, function(err, data) {
1442 if (err) return done(err);
1443 Post._warned.cannotOverwritePKInBeforeSaveHook.should.equal(true);
1444 data.id.should.eql(postInstance.id);
1445 done();
1446 });
1447 });
1448 });
1449
1450 it('should ignore PK if it is set for `data`' +
1451 'in `loaded` operation hook', function(done) {
1452 Post.findById(postInstance.id, function(err, p) {
1453 if (err) return done(err);
1454 changePostIdInHook('loaded');
1455 p.replaceAttributes({title: 'b'}, function(err, data) {
1456 data.id.should.eql(postInstance.id);
1457 if (err) return done(err);
1458 // clear observers to make sure `loaded`
1459 // hook does not affect `find()` method
1460 Post.clearObservers('loaded');
1461 Post.find(function(err, p) {
1462 if (err) return done(err);
1463 p[0].id.should.eql(postInstance.id);
1464 done();
1465 });
1466 });
1467 });
1468 });
1469
1470 it('should set cannotOverwritePKInLoadedHook flag, if `instance` in' +
1471 '`before save` operation hook is set, so we report a warning just once',
1472 function(done) {
1473 Post.findById(postInstance.id, function(err, p) {
1474 if (err) return done(err);
1475 changePostIdInHook('loaded');
1476 p.replaceAttributes({title: 'b'}, function(err, data) {
1477 if (err) return done(err);
1478 Post._warned.cannotOverwritePKInLoadedHook.should.equal(true);
1479 data.id.should.eql(postInstance.id);
1480 done();
1481 });
1482 });
1483 });
1484
1485 it('works without options(promise variant)', function(done) {
1486 Post.findById(postInstance.id)
1487 .then(function(p) {
1488 p.replaceAttributes({title: 'b'})
1489 .then(function(p) {
1490 should.exist(p);
1491 p.should.be.instanceOf(Post);
1492 p.title.should.equal('b');
1493 p.should.have.property('content').be.oneOf(null, undefined);
1494 return Post.findById(postInstance.id)
1495 .then(function(p) {
1496 p.title.should.equal('b');
1497 should.not.exist(p.content);
1498 done();
1499 });
1500 });
1501 })
1502 .catch(done);
1503 });
1504
1505 it('works with options(promise variant)', function(done) {
1506 Post.findById(postInstance.id)
1507 .then(function(p) {
1508 p.replaceAttributes({title: 'b'}, {validate: false})
1509 .then(function(p) {
1510 should.exist(p);
1511 p.should.be.instanceOf(Post);
1512 p.title.should.equal('b');
1513 p.should.have.property('content').be.oneOf(null, undefined);
1514 return Post.findById(postInstance.id)
1515 .then(function(p) {
1516 p.title.should.equal('b');
1517 should.not.exist(p.content);
1518 done();
1519 });
1520 });
1521 })
1522 .catch(done);
1523 });
1524
1525 it('should fail when changing id', function(done) {
1526 const unknownId = uid.fromConnector(db) || 999;
1527 Post.findById(postInstance.id, function(err, p) {
1528 if (err) return done(err);
1529 p.replaceAttributes({title: 'b', id: unknownId}, function(err, p) {
1530 should.exist(err);
1531 const expectedErrMsg = 'id property (id) cannot be updated from ' +
1532 postInstance.id + ' to ' + unknownId;
1533 err.message.should.equal(expectedErrMsg);
1534 done();
1535 });
1536 });
1537 });
1538
1539 it('works without options(callback variant)', function(done) {
1540 Post.findById(postInstance.id, function(err, p) {
1541 if (err) return done(err);
1542 p.replaceAttributes({title: 'b'}, function(err, p) {
1543 if (err) return done(err);
1544 p.should.have.property('content').be.oneOf(null, undefined);
1545 p.title.should.equal('b');
1546 done();
1547 });
1548 });
1549 });
1550
1551 it('works with options(callback variant)', function(done) {
1552 Post.findById(postInstance.id, function(err, p) {
1553 if (err) return done(err);
1554 p.replaceAttributes({title: 'b'}, {validate: false}, function(err, p) {
1555 if (err) return done(err);
1556 p.should.have.property('content').be.oneOf(null, undefined);
1557 p.title.should.equal('b');
1558 done();
1559 });
1560 });
1561 });
1562
1563 function changePostIdInHook(operationHook) {
1564 Post.observe(operationHook, function(ctx, next) {
1565 (ctx.data || ctx.instance).id = 99;
1566 next();
1567 });
1568 }
1569 });
1570 }
1571
1572 bdd.describeIf(hasReplaceById, 'replaceById', function() {
1573 let Post;
1574 before(function(done) {
1575 db = getSchema();
1576 Post = db.define('Post', {
1577 title: {type: String, length: 255},
1578 content: {type: String},
1579 throwingSetter: {type: String, default: null},
1580 }, {forceId: true});
1581 Post.setter.throwingSetter = throwingSetter;
1582 db.automigrate('Post', done);
1583 });
1584
1585 bdd.itIf(connectorCapabilities.supportForceId !== false, 'fails when id does not exist in db ' +
1586 'using replaceById', function(done) {
1587 const unknownId = uid.fromConnector(db) || 123;
1588 const post = {id: unknownId, title: 'a', content: 'AAA'};
1589 Post.replaceById(post.id, post, function(err, p) {
1590 err.statusCode.should.equal(404);
1591 done();
1592 });
1593 });
1594
1595 it('correctly coerces the PK value', async () => {
1596 const created = await Post.create({
1597 title: 'a title',
1598 content: 'a content',
1599 });
1600
1601 // Emulate what happens when model instance is received by REST API clients
1602 const data = JSON.parse(JSON.stringify(created));
1603
1604 // Modify some of the data
1605 data.title = 'Draft';
1606
1607 // Call replaceById to modify the database record
1608 await Post.replaceById(data.id, data);
1609
1610 // Verify what has been stored
1611 const found = await Post.findById(data.id);
1612 found.toObject().should.eql({
1613 id: created.id,
1614 title: 'Draft',
1615 content: 'a content',
1616 throwingSetter: null,
1617 });
1618
1619 // Verify that no warnings were triggered
1620 Object.keys(Post._warned).should.be.empty();
1621 });
1622
1623 it('should return rejected promise when model initialization failed', async () => {
1624 const firstNotFailedPost = await Post.create({title: 'Sad Post'}); // no property with failing setter
1625 await Post.replaceById(firstNotFailedPost.id, {
1626 title: 'Sad Post', throwingSetter: 'somethingElse',
1627 }).should.be.rejectedWith('Intentional error triggered from a property setter');
1628 });
1629 });
1630
1631 describe('findOrCreate', function() {
1632 it('should create a record with if new', function(done) {
1633 Person.findOrCreate({name: 'Zed', gender: 'male'},
1634 function(err, p, created) {
1635 if (err) return done(err);
1636 should.exist(p);
1637 p.should.be.instanceOf(Person);
1638 p.name.should.equal('Zed');
1639 p.gender.should.equal('male');
1640 created.should.equal(true);
1641 done();
1642 });
1643 });
1644
1645 it('should find a record if exists', function(done) {
1646 Person.findOrCreate(
1647 {where: {name: 'Zed'}},
1648 {name: 'Zed', gender: 'male'},
1649 function(err, p, created) {
1650 if (err) return done(err);
1651 should.exist(p);
1652 p.should.be.instanceOf(Person);
1653 p.name.should.equal('Zed');
1654 p.gender.should.equal('male');
1655 created.should.equal(false);
1656 done();
1657 },
1658 );
1659 });
1660
1661 it('should create a record with if new (promise variant)', function(done) {
1662 Person.findOrCreate({name: 'Jed', gender: 'male'})
1663 .then(function(res) {
1664 should.exist(res);
1665 res.should.be.instanceOf(Array);
1666 res.should.have.lengthOf(2);
1667 const p = res[0];
1668 const created = res[1];
1669 p.should.be.instanceOf(Person);
1670 p.name.should.equal('Jed');
1671 p.gender.should.equal('male');
1672 created.should.equal(true);
1673 done();
1674 })
1675 .catch(done);
1676 });
1677
1678 it('should find a record if exists (promise variant)', function(done) {
1679 Person.findOrCreate(
1680 {where: {name: 'Jed'}},
1681 {name: 'Jed', gender: 'male'},
1682 )
1683 .then(function(res) {
1684 res.should.be.instanceOf(Array);
1685 res.should.have.lengthOf(2);
1686 const p = res[0];
1687 const created = res[1];
1688 p.should.be.instanceOf(Person);
1689 p.name.should.equal('Jed');
1690 p.gender.should.equal('male');
1691 created.should.equal(false);
1692 done();
1693 })
1694 .catch(done);
1695 });
1696
1697 it('preserves empty values from the database', async () => {
1698 // https://github.com/strongloop/loopback-datasource-juggler/issues/1692
1699
1700 // Initially, all Players were always active, no property was needed
1701 const Player = db.define('Player', {name: String});
1702
1703 await db.automigrate('Player');
1704 const created = await Player.create({name: 'Pen'});
1705
1706 // Later on, we decide to introduce `active` property
1707 Player.defineProperty('active', {
1708 type: Boolean,
1709 default: false,
1710 });
1711 await db.autoupdate('Player');
1712
1713 // And findOrCreate an existing record
1714 const [found] = await Player.findOrCreate({id: created.id}, {name: 'updated'});
1715 should(found.toObject().active).be.oneOf([
1716 undefined, // databases supporting `undefined` value
1717 null, // databases representing `undefined` as `null`
1718 ]);
1719 });
1720 });
1721
1722 describe('destroy', function() {
1723 it('should destroy record', function(done) {
1724 Person.create(function(err, p) {
1725 if (err) return done(err);
1726 p.destroy(function(err) {
1727 if (err) return done(err);
1728 Person.exists(p.id, function(err, ex) {
1729 if (err) return done(err);
1730 ex.should.not.be.ok;
1731 done();
1732 });
1733 });
1734 });
1735 });
1736
1737 it('should destroy record (promise variant)', function(done) {
1738 Person.create()
1739 .then(function(p) {
1740 return p.destroy()
1741 .then(function() {
1742 return Person.exists(p.id)
1743 .then(function(ex) {
1744 ex.should.not.be.ok;
1745 done();
1746 });
1747 });
1748 })
1749 .catch(done);
1750 });
1751
1752 it('should destroy all records', function(done) {
1753 Person.destroyAll(function(err) {
1754 if (err) return done(err);
1755 Person.all(function(err, posts) {
1756 if (err) return done(err);
1757 posts.should.have.lengthOf(0);
1758 Person.count(function(err, count) {
1759 if (err) return done(err);
1760 count.should.eql(0);
1761 done();
1762 });
1763 });
1764 });
1765 });
1766
1767 it('should destroy all records (promise variant)', function(done) {
1768 Person.create()
1769 .then(function() {
1770 return Person.destroyAll()
1771 .then(function() {
1772 return Person.all()
1773 .then(function(ps) {
1774 ps.should.have.lengthOf(0);
1775 return Person.count()
1776 .then(function(count) {
1777 count.should.eql(0);
1778 done();
1779 });
1780 });
1781 });
1782 })
1783 .catch(done);
1784 });
1785
1786 // TODO: implement destroy with filtered set
1787 it('should destroy filtered set of records');
1788 });
1789
1790 bdd.describeIf(connectorCapabilities.reportDeletedCount !== false &&
1791 connectorCapabilities.deleteWithOtherThanId !== false, 'deleteAll/destroyAll', function() {
1792 beforeEach(function clearOldData(done) {
1793 Person.deleteAll(done);
1794 });
1795
1796 beforeEach(function createTestData(done) {
1797 Person.create([{
1798 name: 'John',
1799 }, {
1800 name: 'Jane',
1801 }], function(err, data) {
1802 should.not.exist(err);
1803 done();
1804 });
1805 });
1806
1807 it('should be defined as function', function() {
1808 Person.deleteAll.should.be.a.Function;
1809 Person.destroyAll.should.be.a.Function;
1810 });
1811
1812 it('should only delete instances that satisfy the where condition',
1813 function(done) {
1814 Person.deleteAll({name: 'John'}, function(err, info) {
1815 if (err) return done(err);
1816 info.should.have.property('count', 1);
1817 Person.find({where: {name: 'John'}}, function(err, data) {
1818 if (err) return done(err);
1819 data.should.have.length(0);
1820 Person.find({where: {name: 'Jane'}}, function(err, data) {
1821 if (err) return done(err);
1822 data.should.have.length(1);
1823 done();
1824 });
1825 });
1826 });
1827 });
1828
1829 it('should report zero deleted instances when no matches are found',
1830 function(done) {
1831 Person.deleteAll({name: 'does-not-match'}, function(err, info) {
1832 if (err) return done(err);
1833 info.should.have.property('count', 0);
1834 Person.count(function(err, count) {
1835 if (err) return done(err);
1836 count.should.equal(2);
1837 done();
1838 });
1839 });
1840 });
1841
1842 it('should delete all instances when the where condition is not provided',
1843 function(done) {
1844 Person.deleteAll(function(err, info) {
1845 if (err) return done(err);
1846 info.should.have.property('count', 2);
1847 Person.count(function(err, count) {
1848 if (err) return done(err);
1849 count.should.equal(0);
1850 done();
1851 });
1852 });
1853 });
1854 });
1855
1856 bdd.describeIf(connectorCapabilities.reportDeletedCount === false &&
1857 connectorCapabilities.deleteWithOtherThanId === false, 'deleteAll/destroyAll case 2', function() {
1858 let idJohn, idJane;
1859 beforeEach(function clearOldData(done) {
1860 Person.deleteAll(done);
1861 });
1862
1863 beforeEach(function createTestData(done) {
1864 Person.create([{
1865 name: 'John',
1866 }, {
1867 name: 'Jane',
1868 }], function(err, data) {
1869 should.not.exist(err);
1870 data.forEach(function(person) {
1871 if (person.name === 'John') idJohn = person.id;
1872 if (person.name === 'Jane') idJane = person.id;
1873 });
1874 should.exist(idJohn);
1875 should.exist(idJane);
1876 done();
1877 });
1878 });
1879
1880 // eslint-disable-next-line mocha/no-identical-title
1881 it('should be defined as function', function() {
1882 Person.deleteAll.should.be.a.Function;
1883 Person.destroyAll.should.be.a.Function;
1884 });
1885
1886 // eslint-disable-next-line mocha/no-identical-title
1887 it('should only delete instances that satisfy the where condition',
1888 function(done) {
1889 Person.deleteAll({id: idJohn}, function(err, info) {
1890 if (err) return done(err);
1891 should.not.exist(info.count);
1892 Person.find({where: {name: 'John'}}, function(err, data) {
1893 if (err) return done(err);
1894 should.not.exist(data.count);
1895 data.should.have.length(0);
1896 Person.find({where: {name: 'Jane'}}, function(err, data) {
1897 if (err) return done(err);
1898 data.should.have.length(1);
1899 done();
1900 });
1901 });
1902 });
1903 });
1904
1905 // eslint-disable-next-line mocha/no-identical-title
1906 it('should report zero deleted instances when no matches are found',
1907 function(done) {
1908 const unknownId = uid.fromConnector(db) || 1234567890;
1909 Person.deleteAll({id: unknownId}, function(err, info) {
1910 if (err) return done(err);
1911 should.not.exist(info.count);
1912 Person.count(function(err, count) {
1913 if (err) return done(err);
1914 count.should.equal(2);
1915 done();
1916 });
1917 });
1918 });
1919
1920 // eslint-disable-next-line mocha/no-identical-title
1921 it('should delete all instances when the where condition is not provided',
1922 function(done) {
1923 Person.deleteAll(function(err, info) {
1924 if (err) return done(err);
1925 should.not.exist(info.count);
1926 Person.count(function(err, count) {
1927 if (err) return done(err);
1928 count.should.equal(0);
1929 done();
1930 });
1931 });
1932 });
1933 });
1934
1935 describe('deleteById', function() {
1936 beforeEach(givenSomePeople);
1937 afterEach(function() {
1938 Person.settings.strictDelete = false;
1939 });
1940
1941 it('should allow deleteById(id) - success', function(done) {
1942 Person.findOne(function(e, p) {
1943 Person.deleteById(p.id, function(err, info) {
1944 if (err) return done(err);
1945 if (connectorCapabilities.reportDeletedCount !== false) {
1946 info.should.have.property('count', 1);
1947 } else {
1948 should.not.exist(info.count);
1949 }
1950 done();
1951 });
1952 });
1953 });
1954
1955 it('should allow deleteById(id) - fail', function(done) {
1956 const unknownId = uid.fromConnector(db) || 9999;
1957 Person.settings.strictDelete = false;
1958 Person.deleteById(unknownId, function(err, info) {
1959 if (err) return done(err);
1960 if (connectorCapabilities.reportDeletedCount !== false) {
1961 info.should.have.property('count', 0);
1962 } else {
1963 should.not.exist(info.count);
1964 }
1965 done();
1966 });
1967 });
1968
1969 it('should allow deleteById(id) - fail with error', function(done) {
1970 const unknownId = uid.fromConnector(db) || 9999;
1971 const errMsg = 'No instance with id ' + unknownId.toString() + ' found for Person';
1972 Person.settings.strictDelete = true;
1973 Person.deleteById(unknownId, function(err) {
1974 should.exist(err);
1975 err.message.should.equal(errMsg);
1976 err.should.have.property('code', 'NOT_FOUND');
1977 err.should.have.property('statusCode', 404);
1978 done();
1979 });
1980 });
1981 });
1982
1983 describe('prototype.delete', function() {
1984 beforeEach(givenSomePeople);
1985 afterEach(function() {
1986 Person.settings.strictDelete = false;
1987 });
1988
1989 it('should allow delete(id) - success', function(done) {
1990 Person.findOne(function(e, p) {
1991 if (e) return done(e);
1992 p.delete(function(err, info) {
1993 if (err) return done(err);
1994 if (connectorCapabilities.reportDeletedCount !== false) {
1995 info.should.have.property('count', 1);
1996 } else {
1997 should.not.exist(info.count);
1998 }
1999 done();
2000 });
2001 });
2002 });
2003
2004 it('should allow delete(id) - fail', function(done) {
2005 Person.settings.strictDelete = false;
2006 Person.findOne(function(e, p) {
2007 if (e) return done(e);
2008 p.delete(function(err, info) {
2009 if (err) return done(err);
2010 if (connectorCapabilities.reportDeletedCount !== false) {
2011 info.should.have.property('count', 1);
2012 } else {
2013 should.not.exist(info.count);
2014 }
2015 p.delete(function(err, info) {
2016 if (err) return done(err);
2017 if (connectorCapabilities.reportDeletedCount !== false) {
2018 info.should.have.property('count', 0);
2019 } else {
2020 should.not.exist(info.count);
2021 }
2022 done();
2023 });
2024 });
2025 });
2026 });
2027
2028 bdd.itIf(connectorCapabilities.supportStrictDelete !== false, 'should allow delete(id) - ' +
2029 'fail with error', function(done) {
2030 Person.settings.strictDelete = true;
2031 Person.findOne(function(err, u) {
2032 if (err) return done(err);
2033 u.delete(function(err, info) {
2034 if (err) return done(err);
2035 info.should.have.property('count', 1);
2036 u.delete(function(err) {
2037 should.exist(err);
2038 err.message.should.equal('No instance with id ' + u.id + ' found for Person');
2039 err.should.have.property('code', 'NOT_FOUND');
2040 err.should.have.property('statusCode', 404);
2041 done();
2042 });
2043 });
2044 });
2045 });
2046 });
2047
2048 describe('initialize', function() {
2049 it('should initialize object properly', function() {
2050 const hw = 'Hello word',
2051 now = Date.now(),
2052 person = new Person({name: hw});
2053
2054 person.name.should.equal(hw);
2055 person.name = 'Goodbye, Lenin';
2056 (person.createdAt >= now).should.be.true;
2057 person.isNewRecord().should.be.true;
2058 });
2059
2060 describe('Date $now function (type: Date)', function() {
2061 let CustomModel;
2062
2063 before(function(done) {
2064 CustomModel = db.define('CustomModel1', {
2065 createdAt: {type: Date, default: '$now'},
2066 });
2067 db.automigrate('CustomModel1', done);
2068 });
2069
2070 it('should report current date as default value for date property',
2071 function(done) {
2072 const now = Date.now();
2073
2074 CustomModel.create(function(err, model) {
2075 should.not.exists(err);
2076 model.createdAt.should.be.instanceOf(Date);
2077 (model.createdAt >= now).should.be.true;
2078 });
2079
2080 done();
2081 });
2082 });
2083
2084 describe('Date $now function (type: String)', function() {
2085 let CustomModel;
2086
2087 before(function(done) {
2088 CustomModel = db.define('CustomModel2', {
2089 now: {type: String, default: '$now'},
2090 });
2091 db.automigrate('CustomModel2', done);
2092 });
2093
2094 it('should report \'$now\' as default value for string property',
2095 function(done) {
2096 CustomModel.create(function(err, model) {
2097 if (err) return done(err);
2098 model.now.should.be.instanceOf(String);
2099 model.now.should.equal('$now');
2100 });
2101
2102 done();
2103 });
2104 });
2105
2106 describe('now defaultFn', function() {
2107 let CustomModel;
2108
2109 before(function(done) {
2110 CustomModel = db.define('CustomModel3', {
2111 now: {type: Date, defaultFn: 'now'},
2112 });
2113 db.automigrate('CustomModel3', done);
2114 });
2115
2116 it('should generate current time when "defaultFn" is "now"',
2117 function(done) {
2118 const now = Date.now();
2119 CustomModel.create(function(err, model) {
2120 if (err) return done(err);
2121 model.now.should.be.instanceOf(Date);
2122 model.now.should.be.within(now, now + 200);
2123 done();
2124 });
2125 });
2126 });
2127
2128 describe('guid defaultFn', function() {
2129 let CustomModel;
2130
2131 before(function(done) {
2132 CustomModel = db.define('CustomModel4', {
2133 guid: {type: String, defaultFn: 'guid'},
2134 });
2135 db.automigrate('CustomModel4', done);
2136 });
2137
2138 it('should generate a new id when "defaultFn" is "guid"', function(done) {
2139 CustomModel.create(function(err, model) {
2140 if (err) return done(err);
2141 model.guid.should.match(UUID_REGEXP);
2142 done();
2143 });
2144 });
2145 });
2146
2147 describe('uuid defaultFn', function() {
2148 let CustomModel;
2149
2150 before(function(done) {
2151 CustomModel = db.define('CustomModel5', {
2152 guid: {type: String, defaultFn: 'uuid'},
2153 });
2154 db.automigrate('CustomModel5', done);
2155 });
2156
2157 it('should generate a new id when "defaultfn" is "uuid"', function(done) {
2158 CustomModel.create(function(err, model) {
2159 if (err) return done(err);
2160 model.guid.should.match(UUID_REGEXP);
2161 done();
2162 });
2163 });
2164 });
2165
2166 describe('uuidv4 defaultFn', function() {
2167 let CustomModel;
2168
2169 before(function(done) {
2170 CustomModel = db.define('CustomModel5', {
2171 guid: {type: String, defaultFn: 'uuidv4'},
2172 });
2173 db.automigrate('CustomModel5', done);
2174 });
2175
2176 it('should generate a new id when "defaultfn" is "uuidv4"', function(done) {
2177 CustomModel.create(function(err, model) {
2178 should.not.exists(err);
2179 model.guid.should.match(UUID_REGEXP);
2180 done();
2181 });
2182 });
2183 });
2184
2185 describe('shortid defaultFn', function() {
2186 let ModelWithShortId;
2187 before(createModelWithShortId);
2188
2189 it('should generate a new id when "defaultFn" is "shortid"', function(done) {
2190 const SHORTID_REGEXP = /^[0-9a-z_\-]{7,14}$/i;
2191 ModelWithShortId.create(function(err, modelWithShortId) {
2192 if (err) return done(err);
2193 modelWithShortId.shortid.should.match(SHORTID_REGEXP);
2194 done();
2195 });
2196 });
2197
2198 function createModelWithShortId(cb) {
2199 ModelWithShortId = db.define('ModelWithShortId', {
2200 shortid: {type: String, defaultFn: 'shortid'},
2201 });
2202 db.automigrate('ModelWithShortId', cb);
2203 }
2204 });
2205
2206 // it('should work when constructor called as function', function() {
2207 // var p = Person({name: 'John Resig'});
2208 // p.should.be.an.instanceOf(Person);
2209 // p.name.should.equal('John Resig');
2210 // });
2211 });
2212
2213 describe('property value coercion', function() {
2214 it('should coerce boolean types properly', function() {
2215 let p1 = new Person({name: 'John', married: 'false'});
2216 p1.married.should.equal(false);
2217
2218 p1 = new Person({name: 'John', married: 'true'});
2219 p1.married.should.equal(true);
2220
2221 p1 = new Person({name: 'John', married: '1'});
2222 p1.married.should.equal(true);
2223
2224 p1 = new Person({name: 'John', married: '0'});
2225 p1.married.should.equal(false);
2226
2227 p1 = new Person({name: 'John', married: true});
2228 p1.married.should.equal(true);
2229
2230 p1 = new Person({name: 'John', married: false});
2231 p1.married.should.equal(false);
2232
2233 p1 = new Person({name: 'John', married: 'null'});
2234 p1.married.should.equal(true);
2235
2236 p1 = new Person({name: 'John', married: ''});
2237 p1.married.should.equal(false);
2238
2239 p1 = new Person({name: 'John', married: 'X'});
2240 p1.married.should.equal(true);
2241
2242 p1 = new Person({name: 'John', married: 0});
2243 p1.married.should.equal(false);
2244
2245 p1 = new Person({name: 'John', married: 1});
2246 p1.married.should.equal(true);
2247
2248 p1 = new Person({name: 'John', married: null});
2249 p1.should.have.property('married', null);
2250
2251 p1 = new Person({name: 'John', married: undefined});
2252 p1.should.have.property('married', undefined);
2253 });
2254
2255 it('should coerce date types properly', function() {
2256 let p1 = new Person({name: 'John', dob: '2/1/2015'});
2257 p1.dob.should.eql(new Date('2/1/2015'));
2258
2259 p1 = new Person({name: 'John', dob: '2/1/2015'});
2260 p1.dob.should.eql(new Date('2/1/2015'));
2261
2262 p1 = new Person({name: 'John', dob: '12'});
2263 p1.dob.should.eql(new Date('12'));
2264
2265 p1 = new Person({name: 'John', dob: 12});
2266 p1.dob.should.eql(new Date(12));
2267
2268 p1 = new Person({name: 'John', dob: null});
2269 p1.should.have.property('dob', null);
2270
2271 p1 = new Person({name: 'John', dob: undefined});
2272 p1.should.have.property('dob', undefined);
2273
2274 p1 = new Person({name: 'John', dob: 'X'});
2275 p1.should.have.property('dob');
2276 p1.dob.toString().should.be.eql('Invalid Date');
2277 });
2278 });
2279
2280 describe('update/updateAll', function() {
2281 let idBrett, idCarla, idDonna, idFrank, idGrace, idHarry;
2282 let filterBrett, filterHarry;
2283
2284 beforeEach(function clearOldData(done) {
2285 db = getSchema();
2286 Person.destroyAll(done);
2287 });
2288
2289 beforeEach(function createTestData(done) {
2290 Person.create([{
2291 name: 'Brett Boe',
2292 age: 19,
2293 }, {
2294 name: 'Carla Coe',
2295 age: 20,
2296 }, {
2297 name: 'Donna Doe',
2298 age: 21,
2299 }, {
2300 name: 'Frank Foe',
2301 age: 22,
2302 }, {
2303 name: 'Grace Goe',
2304 age: 23,
2305 }], function(err, data) {
2306 should.not.exist(err);
2307 data.forEach(function(person) {
2308 if (person.name === 'Brett Boe') idBrett = person.id;
2309 if (person.name === 'Carla Coe') idCarla = person.id;
2310 if (person.name === 'Donna Doe') idDonna = person.id;
2311 if (person.name === 'Frank Foe') idFrank = person.id;
2312 if (person.name === 'Grace Goe') idGrace = person.id;
2313 });
2314 should.exist(idBrett);
2315 should.exist(idCarla);
2316 should.exist(idDonna);
2317 should.exist(idFrank);
2318 should.exist(idGrace);
2319 done();
2320 });
2321 });
2322
2323 it('should be defined as a function', function() {
2324 Person.update.should.be.a.Function;
2325 Person.updateAll.should.be.a.Function;
2326 });
2327
2328 it('should not update instances that do not satisfy the where condition',
2329 function(done) {
2330 idHarry = uid.fromConnector(db) || undefined;
2331 const filter = connectorCapabilities.updateWithOtherThanId === false ?
2332 {id: idHarry} : {name: 'Harry Hoe'};
2333 Person.update(filter, {name: 'Marta Moe'}, function(err,
2334 info) {
2335 if (err) return done(err);
2336 if (connectorCapabilities.reportDeletedCount !== false) {
2337 info.should.have.property('count', 0);
2338 } else {
2339 should.not.exist(info.count);
2340 }
2341 Person.find({where: {name: 'Harry Hoe'}}, function(err, people) {
2342 if (err) return done(err);
2343 people.should.be.empty;
2344 done();
2345 });
2346 });
2347 });
2348
2349 it('should only update instances that satisfy the where condition',
2350 function(done) {
2351 const filter = connectorCapabilities.deleteWithOtherThanId === false ?
2352 {id: idBrett} : {name: 'Brett Boe'};
2353 Person.update(filter, {name: 'Harry Hoe'}, function(err,
2354 info) {
2355 if (err) return done(err);
2356 if (connectorCapabilities.reportDeletedCount !== false) {
2357 info.should.have.property('count', 1);
2358 } else {
2359 should.not.exist(info.count);
2360 }
2361 Person.find({where: {age: 19}}, function(err, people) {
2362 if (err) return done(err);
2363 people.should.have.length(1);
2364 people[0].name.should.equal('Harry Hoe');
2365 done();
2366 });
2367 });
2368 });
2369
2370 it('should reject updated empty password with updateAll', function(done) {
2371 StubUser.create({password: 'abc123'}, function(err, createdUser) {
2372 if (err) return done(err);
2373 StubUser.updateAll({where: {id: createdUser.id}}, {'password': ''}, function(err, updatedUser) {
2374 (err.message).should.match(/password cannot be empty/);
2375 done();
2376 });
2377 });
2378 });
2379
2380 bdd.itIf(connectorCapabilities.updateWithoutId !== false,
2381 'should update all instances when the where condition is not provided', function(done) {
2382 filterHarry = connectorCapabilities.deleteWithOtherThanId === false ?
2383 {id: idHarry} : {name: 'Harry Hoe'};
2384 filterBrett = connectorCapabilities.deleteWithOtherThanId === false ?
2385 {id: idBrett} : {name: 'Brett Boe'};
2386 Person.update(filterHarry, function(err, info) {
2387 if (err) return done(err);
2388 info.should.have.property('count', 5);
2389 Person.find({where: filterBrett}, function(err, people) {
2390 if (err) return done(err);
2391 people.should.be.empty();
2392 Person.find({where: filterHarry}, function(err, people) {
2393 if (err) return done(err);
2394 people.should.have.length(5);
2395 done();
2396 });
2397 });
2398 });
2399 });
2400
2401 bdd.itIf(connectorCapabilities.ignoreUndefinedConditionValue !== false, 'should ignore where ' +
2402 'conditions with undefined values', function(done) {
2403 Person.update(filterBrett, {name: undefined, gender: 'male'},
2404 function(err, info) {
2405 if (err) return done(err);
2406 info.should.have.property('count', 1);
2407 Person.find({where: filterBrett}, function(err, people) {
2408 if (err) return done(err);
2409 people.should.have.length(1);
2410 people[0].name.should.equal('Brett Boe');
2411 done();
2412 });
2413 });
2414 });
2415
2416 it('should not coerce invalid values provided in where conditions', function(done) {
2417 Person.update({name: 'Brett Boe'}, {dob: 'notadate'}, function(err) {
2418 should.exist(err);
2419 err.message.should.equal('Invalid date: notadate');
2420 done();
2421 });
2422 });
2423 });
2424
2425 describe('upsertWithWhere', function() {
2426 let ds, Person;
2427 before('prepare "Person" model', function(done) {
2428 ds = getSchema();
2429 Person = ds.define('Person', {
2430 id: {type: Number, id: true},
2431 name: {type: String},
2432 city: {type: String},
2433 });
2434 ds.automigrate('Person', done);
2435 });
2436
2437 it('has an alias "patchOrCreateWithWhere"', function() {
2438 StubUser.upsertWithWhere.should.equal(StubUser.patchOrCreateWithWhere);
2439 });
2440
2441 it('should preserve properties with dynamic setters on create', function(done) {
2442 StubUser.upsertWithWhere({password: 'foo'}, {password: 'foo'}, function(err, created) {
2443 if (err) return done(err);
2444 created.password.should.equal('foo-FOO');
2445 StubUser.findById(created.id, function(err, found) {
2446 if (err) return done(err);
2447 found.password.should.equal('foo-FOO');
2448 done();
2449 });
2450 });
2451 });
2452
2453 it('should preserve properties with dynamic setters on update', function(done) {
2454 StubUser.create({password: 'foo'}, function(err, created) {
2455 if (err) return done(err);
2456 const data = {password: 'bar'};
2457 StubUser.upsertWithWhere({id: created.id}, data, function(err, updated) {
2458 if (err) return done(err);
2459 updated.password.should.equal('bar-BAR');
2460 StubUser.findById(created.id, function(err, found) {
2461 if (err) return done(err);
2462 found.password.should.equal('bar-BAR');
2463 done();
2464 });
2465 });
2466 });
2467 });
2468
2469 it('should preserve properties with "undefined" value', function(done) {
2470 Person.create(
2471 {id: 10, name: 'Ritz', city: undefined},
2472 function(err, instance) {
2473 if (err) return done(err);
2474 instance.toObject().should.have.properties({
2475 id: 10,
2476 name: 'Ritz',
2477 city: undefined,
2478 });
2479
2480 Person.upsertWithWhere({id: 10},
2481 {name: 'updated name'},
2482 function(err, updated) {
2483 if (err) return done(err);
2484 const result = updated.toObject();
2485 result.should.have.properties({
2486 id: instance.id,
2487 name: 'updated name',
2488 });
2489 should.equal(result.city, null);
2490 done();
2491 });
2492 },
2493 );
2494 });
2495
2496 it('should allow save() of the created instance', function(done) {
2497 Person.upsertWithWhere({id: 999},
2498 // Todo @mountain: This seems a bug why in data object still I need to pass id?
2499 {id: 999, name: 'a-name'},
2500 function(err, inst) {
2501 if (err) return done(err);
2502 inst.save(done);
2503 });
2504 });
2505
2506 it('works without options on create (promise variant)', function(done) {
2507 const person = {id: 123, name: 'a', city: 'city a'};
2508 Person.upsertWithWhere({id: 123}, person)
2509 .then(function(p) {
2510 should.exist(p);
2511 p.should.be.instanceOf(Person);
2512 p.id.should.eql(person.id);
2513 p.should.not.have.property('_id');
2514 p.name.should.equal(person.name);
2515 p.city.should.equal(person.city);
2516 return Person.findById(p.id)
2517 .then(function(p) {
2518 p.id.should.eql(person.id);
2519 p.id.should.not.have.property('_id');
2520 p.name.should.equal(person.name);
2521 p.city.should.equal(person.city);
2522 done();
2523 });
2524 })
2525 .catch(done);
2526 });
2527
2528 it('works with options on create (promise variant)', function(done) {
2529 const person = {id: 234, name: 'b', city: 'city b'};
2530 Person.upsertWithWhere({id: 234}, person, {validate: false})
2531 .then(function(p) {
2532 should.exist(p);
2533 p.should.be.instanceOf(Person);
2534 p.id.should.eql(person.id);
2535 p.should.not.have.property('_id');
2536 p.name.should.equal(person.name);
2537 p.city.should.equal(person.city);
2538 return Person.findById(p.id)
2539 .then(function(p) {
2540 p.id.should.eql(person.id);
2541 p.id.should.not.have.property('_id');
2542 p.name.should.equal(person.name);
2543 p.city.should.equal(person.city);
2544 done();
2545 });
2546 })
2547 .catch(done);
2548 });
2549
2550 it('works without options on update (promise variant)', function(done) {
2551 const person = {id: 456, name: 'AAA', city: 'city AAA'};
2552 Person.create(person)
2553 .then(function(created) {
2554 created = created.toObject();
2555 delete created.city;
2556 created.name = 'BBB';
2557 return Person.upsertWithWhere({id: 456}, created)
2558 .then(function(p) {
2559 should.exist(p);
2560 p.should.be.instanceOf(Person);
2561 p.id.should.eql(created.id);
2562 p.should.not.have.property('_id');
2563 p.name.should.equal('BBB');
2564 p.should.have.property('city', 'city AAA');
2565 return Person.findById(created.id)
2566 .then(function(p) {
2567 p.should.not.have.property('_id');
2568 p.name.should.equal('BBB');
2569 p.city.should.equal('city AAA');
2570 done();
2571 });
2572 });
2573 })
2574 .catch(done);
2575 });
2576
2577 it('works with options on update (promise variant)', function(done) {
2578 const person = {id: 789, name: 'CCC', city: 'city CCC'};
2579 Person.create(person)
2580 .then(function(created) {
2581 created = created.toObject();
2582 delete created.city;
2583 created.name = 'Carlton';
2584 return Person.upsertWithWhere({id: 789}, created, {validate: false})
2585 .then(function(p) {
2586 should.exist(p);
2587 p.should.be.instanceOf(Person);
2588 p.id.should.eql(created.id);
2589 p.should.not.have.property('_id');
2590 p.name.should.equal('Carlton');
2591 p.should.have.property('city', 'city CCC');
2592 return Person.findById(created.id)
2593 .then(function(p) {
2594 p.should.not.have.property('_id');
2595 p.name.should.equal('Carlton');
2596 p.city.should.equal('city CCC');
2597 done();
2598 });
2599 });
2600 })
2601 .catch(done);
2602 });
2603
2604 it('fails the upsertWithWhere operation when data object is empty', function(done) {
2605 const options = {};
2606 Person.upsertWithWhere({name: 'John Lennon'}, {}, options,
2607 function(err) {
2608 err.message.should.equal('data object cannot be empty!');
2609 done();
2610 });
2611 });
2612
2613 it('creates a new record when no matching instance is found', function(done) {
2614 Person.upsertWithWhere({city: 'Florida'}, {name: 'Nick Carter', id: 1, city: 'Florida'},
2615 function(err, created) {
2616 if (err) return done(err);
2617 Person.findById(1, function(err, data) {
2618 if (err) return done(err);
2619 data.id.should.equal(1);
2620 data.name.should.equal('Nick Carter');
2621 data.city.should.equal('Florida');
2622 done();
2623 });
2624 });
2625 });
2626
2627 it('fails the upsertWithWhere operation when multiple instances are ' +
2628 'retrieved based on the filter criteria', function(done) {
2629 Person.create([
2630 {id: '2', name: 'Howie', city: 'Florida'},
2631 {id: '3', name: 'Kevin', city: 'Florida'},
2632 ], function(err, instance) {
2633 if (err) return done(err);
2634 Person.upsertWithWhere({city: 'Florida'}, {
2635 id: '4', name: 'Brian',
2636 }, function(err) {
2637 err.message.should.equal('There are multiple instances found.' +
2638 'Upsert Operation will not be performed!');
2639 done();
2640 });
2641 });
2642 });
2643
2644 it('updates the record when one matching instance is found ' +
2645 'based on the filter criteria', function(done) {
2646 Person.create([
2647 {id: '5', name: 'Howie', city: 'Kentucky'},
2648 ], function(err, instance) {
2649 if (err) return done(err);
2650 Person.upsertWithWhere({city: 'Kentucky'}, {
2651 name: 'Brian',
2652 }, {validate: false}, function(err, instance) {
2653 if (err) return done(err);
2654 Person.findById(5, function(err, data) {
2655 if (err) return done(err);
2656 should.equal(data.id, 5);
2657 data.name.should.equal('Brian');
2658 data.city.should.equal('Kentucky');
2659 done();
2660 });
2661 });
2662 });
2663 });
2664
2665 it('preserves empty values from the database', async () => {
2666 // https://github.com/strongloop/loopback-datasource-juggler/issues/1692
2667
2668 // Initially, all Players were always active, no property was needed
2669 const Player = db.define('Player', {name: String});
2670
2671 await db.automigrate('Player');
2672 const created = await Player.create({name: 'Pen'});
2673
2674 // Later on, we decide to introduce `active` property
2675 Player.defineProperty('active', {
2676 type: Boolean,
2677 default: false,
2678 });
2679 await db.autoupdate('Player');
2680
2681 // And upsertWithWhere an existing record
2682 const found = await Player.upsertWithWhere({id: created.id}, {name: 'updated'});
2683 should(found.toObject().active).be.oneOf([
2684 undefined, // databases supporting `undefined` value
2685 null, // databases representing `undefined` as `null` (e.g. SQL)
2686 ]);
2687 });
2688
2689 it('preserves custom type of auto-generated id property', async () => {
2690 // NOTE: This test is trying to reproduce the behavior observed
2691 // when using property defined as follows:
2692 // {type: 'string', generated: true, mongodb: {dataType: 'ObjectID'}}
2693 // We want to test that behavior for all connectors, which is tricky,
2694 // because not all connectors support autogenerated string PK values.
2695
2696 const User = db.define('UserWithStringId', {
2697 id: {
2698 type: String,
2699 id: true,
2700 useDefaultIdType: false,
2701 // `useDefaultIdType` is applied only when `generated: true`
2702 generated: true,
2703 },
2704 name: String,
2705 }, {forceId: false});
2706
2707 // disable `generated: true` because many SQL databases cannot
2708 // auto-generate string ids
2709 User.definition.properties.id.generated = false;
2710 User.definition.rawProperties.id.generated = false;
2711 await db.automigrate(User.modelName);
2712
2713 const userId = 'custom user id';
2714
2715 const createdUser = await User.create({id: userId, name: 'testUser'});
2716 // strict equality check
2717 createdUser.id.should.equal(userId);
2718
2719 const foundUser = await User.findById(userId);
2720 // strict equality check
2721 foundUser.id.should.equal(userId);
2722 });
2723 });
2724});
2725
2726function givenSomePeople(done) {
2727 const beatles = [
2728 {name: 'John Lennon', gender: 'male'},
2729 {name: 'Paul McCartney', gender: 'male'},
2730 {name: 'George Harrison', gender: 'male'},
2731 {name: 'Ringo Starr', gender: 'male'},
2732 {name: 'Pete Best', gender: 'male'},
2733 {name: 'Stuart Sutcliffe', gender: 'male'},
2734 ];
2735
2736 async.series([
2737 Person.destroyAll.bind(Person),
2738 function(cb) {
2739 async.each(beatles, Person.create.bind(Person), cb);
2740 },
2741 ], done);
2742}