UNPKG

226 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 assert = require('assert');
11const bdd = require('./helpers/bdd-if');
12const should = require('./init.js');
13const uid = require('./helpers/uid-generator');
14const jdb = require('../');
15const DataSource = jdb.DataSource;
16const createPromiseCallback = require('../lib/utils.js').createPromiseCallback;
17
18let db, tmp, Book, Chapter, Author, Reader, Article, Employee;
19let Category, Job;
20let Picture, PictureLink;
21let Person, Address;
22let Link;
23
24const getTransientDataSource = function(settings) {
25 return new DataSource('transient', settings, db.modelBuilder);
26};
27
28const getMemoryDataSource = function(settings) {
29 return new DataSource('memory', settings, db.modelBuilder);
30};
31
32describe('relations', function() {
33 before(function() {
34 db = getSchema();
35 });
36
37 describe('hasMany', function() {
38 before(function(done) {
39 Book = db.define('Book', {name: String, type: String});
40 Chapter = db.define('Chapter', {name: {type: String, index: true},
41 bookType: String});
42 Author = db.define('Author', {name: String});
43 Reader = db.define('Reader', {name: String});
44
45 db.automigrate(['Book', 'Chapter', 'Author', 'Reader'], done);
46 });
47
48 it('can be declared in different ways', function(done) {
49 Book.hasMany(Chapter);
50 Book.hasMany(Reader, {as: 'users'});
51 Book.hasMany(Author, {foreignKey: 'projectId'});
52 const b = new Book;
53 b.chapters.should.be.an.instanceOf(Function);
54 b.users.should.be.an.instanceOf(Function);
55 b.authors.should.be.an.instanceOf(Function);
56 Object.keys((new Chapter).toObject()).should.containEql('bookId');
57 Object.keys((new Author).toObject()).should.containEql('projectId');
58
59 db.automigrate(['Book', 'Chapter', 'Author', 'Reader'], done);
60 });
61
62 it('can be declared in short form', function(done) {
63 Author = db.define('Author', {name: String});
64 Reader = db.define('Reader', {name: String});
65 Author.hasMany('readers');
66 (new Author).readers.should.be.an.instanceOf(Function);
67 Object.keys((new Reader).toObject()).should.containEql('authorId');
68
69 db.autoupdate(['Author', 'Reader'], done);
70 });
71
72 describe('with scope', function() {
73 before(function(done) {
74 Book.hasMany(Chapter);
75 done();
76 });
77
78 it('should build record on scope', function(done) {
79 Book.create(function(err, book) {
80 const chaps = book.chapters;
81 const c = chaps.build();
82 c.bookId.should.eql(book.id);
83 c.save(done);
84 });
85 });
86
87 it('should create record on scope', function(done) {
88 Book.create(function(err, book) {
89 book.chapters.create(function(err, c) {
90 if (err) return done(err);
91 should.exist(c);
92 c.bookId.should.eql(book.id);
93 done(err);
94 });
95 });
96 });
97
98 it('should not update FK', function(done) {
99 Book.create(function(err, book) {
100 book.chapters.create({name: 'chapter 1'}, function(err, c) {
101 if (err) return done(err);
102 should.exist(c);
103 c.bookId.should.eql(book.id);
104 c.name.should.eql('chapter 1');
105 book.chapters.updateById(c.id, {name: 'chapter 0', bookId: 10}, function(err, cc) {
106 should.exist(err);
107 err.message.should.startWith('Cannot override foreign key');
108 done();
109 });
110 });
111 });
112 });
113
114 it('should create record on scope with promises', function(done) {
115 Book.create()
116 .then(function(book) {
117 return book.chapters.create()
118 .then(function(c) {
119 should.exist(c);
120 c.bookId.should.eql(book.id);
121 done();
122 });
123 }).catch(done);
124 });
125
126 it('should create a batch of records on scope', function(done) {
127 const chapters = [
128 {name: 'a'},
129 {name: 'z'},
130 {name: 'c'},
131 ];
132 Book.create(function(err, book) {
133 book.chapters.create(chapters, function(err, chs) {
134 if (err) return done(err);
135 should.exist(chs);
136 chs.should.have.lengthOf(chapters.length);
137 chs.forEach(function(c) {
138 c.bookId.should.eql(book.id);
139 });
140 done();
141 });
142 });
143 });
144
145 it('should create a batch of records on scope with promises', function(done) {
146 const chapters = [
147 {name: 'a'},
148 {name: 'z'},
149 {name: 'c'},
150 ];
151 Book.create(function(err, book) {
152 book.chapters.create(chapters)
153 .then(function(chs) {
154 should.exist(chs);
155 chs.should.have.lengthOf(chapters.length);
156 chs.forEach(function(c) {
157 c.bookId.should.eql(book.id);
158 });
159 done();
160 }).catch(done);
161 });
162 });
163
164 it('should fetch all scoped instances', function(done) {
165 Book.create(function(err, book) {
166 book.chapters.create({name: 'a'}, function() {
167 book.chapters.create({name: 'z'}, function() {
168 book.chapters.create({name: 'c'}, function() {
169 verify(book);
170 });
171 });
172 });
173 });
174 function verify(book) {
175 book.chapters(function(err, ch) {
176 if (err) return done(err);
177 should.exist(ch);
178 ch.should.have.lengthOf(3);
179
180 const chapters = book.chapters();
181 chapters.should.eql(ch);
182
183 book.chapters(function(e, c) {
184 should.not.exist(e);
185 should.exist(c);
186 ch.should.have.lengthOf(3);
187 const acz = ['a', 'c', 'z'];
188 acz.should.containEql(c[0].name);
189 acz.should.containEql(c[1].name);
190 acz.should.containEql(c[2].name);
191 done();
192 });
193 });
194 }
195 });
196
197 it('should fetch all scoped instances with promises', function(done) {
198 Book.create()
199 .then(function(book) {
200 return book.chapters.create({name: 'a'})
201 .then(function() {
202 return book.chapters.create({name: 'z'});
203 })
204 .then(function() {
205 return book.chapters.create({name: 'c'});
206 })
207 .then(function() {
208 return verify(book);
209 });
210 }).catch(done);
211
212 function verify(book) {
213 return book.chapters.find()
214 .then(function(ch) {
215 should.exist(ch);
216 ch.should.have.lengthOf(3);
217 const chapters = book.chapters();
218 chapters.should.eql(ch);
219 return book.chapters.find()
220 .then(function(c) {
221 should.exist(c);
222 ch.should.have.lengthOf(3);
223 const acz = ['a', 'c', 'z'];
224 acz.should.containEql(c[0].name);
225 acz.should.containEql(c[1].name);
226 acz.should.containEql(c[2].name);
227 done();
228 });
229 });
230 }
231 });
232
233 it('should fetch all scoped instances with find() with callback and condition', function(done) {
234 Book.create(function(err, book) {
235 book.chapters.create({name: 'a'}, function() {
236 book.chapters.create({name: 'z'}, function() {
237 book.chapters.create({name: 'c'}, function() {
238 verify(book);
239 });
240 });
241 });
242 });
243 function verify(book) {
244 book.chapters(function(err, ch) {
245 if (err) return done(err);
246 should.exist(ch);
247 ch.should.have.lengthOf(3);
248
249 const chapters = book.chapters();
250 chapters.should.eql(ch);
251 book.chapters.find(function(e, c) {
252 should.not.exist(e);
253 should.exist(c);
254 ch.should.have.lengthOf(3);
255 const acz = ['a', 'c', 'z'];
256 acz.should.containEql(c[0].name);
257 acz.should.containEql(c[1].name);
258 acz.should.containEql(c[2].name);
259 done();
260 });
261 });
262 }
263 });
264
265 it('should fetch all scoped instances with find() with callback and no condition', function(done) {
266 Book.create(function(err, book) {
267 book.chapters.create({name: 'a'}, function() {
268 book.chapters.create({name: 'z'}, function() {
269 book.chapters.create({name: 'c'}, function() {
270 verify(book);
271 });
272 });
273 });
274 });
275 function verify(book) {
276 book.chapters(function(err, ch) {
277 if (err) return done(err);
278 should.exist(ch);
279 ch.should.have.lengthOf(3);
280
281 const chapters = book.chapters();
282 chapters.should.eql(ch);
283
284 book.chapters.find(function(e, c) {
285 should.not.exist(e);
286 should.exist(c);
287 should.exist(c.length);
288 c.should.have.lengthOf(3);
289 const acz = ['a', 'c', 'z'];
290 acz.should.containEql(c[0].name);
291 acz.should.containEql(c[1].name);
292 acz.should.containEql(c[2].name);
293 done();
294 });
295 });
296 }
297 });
298
299 it('should find scoped record', function(done) {
300 let id;
301 Book.create(function(err, book) {
302 book.chapters.create({name: 'a'}, function(err, ch) {
303 id = ch.id;
304 book.chapters.create({name: 'z'}, function() {
305 book.chapters.create({name: 'c'}, function() {
306 verify(book);
307 });
308 });
309 });
310 });
311
312 function verify(book) {
313 book.chapters.findById(id, function(err, ch) {
314 if (err) return done(err);
315 should.exist(ch);
316 ch.id.should.eql(id);
317 done();
318 });
319 }
320 });
321
322 it('should find scoped record with promises', function(done) {
323 let id;
324 Book.create()
325 .then(function(book) {
326 return book.chapters.create({name: 'a'})
327 .then(function(ch) {
328 id = ch.id;
329 return book.chapters.create({name: 'z'});
330 })
331 .then(function() {
332 return book.chapters.create({name: 'c'});
333 })
334 .then(function() {
335 return verify(book);
336 });
337 }).catch(done);
338
339 function verify(book) {
340 return book.chapters.findById(id)
341 .then(function(ch) {
342 should.exist(ch);
343 ch.id.should.eql(id);
344 done();
345 });
346 }
347 });
348
349 it('should count scoped records - all and filtered', function(done) {
350 Book.create(function(err, book) {
351 book.chapters.create({name: 'a'}, function(err, ch) {
352 book.chapters.create({name: 'b'}, function() {
353 book.chapters.create({name: 'c'}, function() {
354 verify(book);
355 });
356 });
357 });
358 });
359
360 function verify(book) {
361 book.chapters.count(function(err, count) {
362 if (err) return done(err);
363 count.should.equal(3);
364 book.chapters.count({name: 'b'}, function(err, count) {
365 if (err) return done(err);
366 count.should.equal(1);
367 done();
368 });
369 });
370 }
371 });
372
373 it('should count scoped records - all and filtered with promises', function(done) {
374 Book.create()
375 .then(function(book) {
376 book.chapters.create({name: 'a'})
377 .then(function() {
378 return book.chapters.create({name: 'b'});
379 })
380 .then(function() {
381 return book.chapters.create({name: 'c'});
382 })
383 .then(function() {
384 return verify(book);
385 });
386 }).catch(done);
387
388 function verify(book) {
389 return book.chapters.count()
390 .then(function(count) {
391 count.should.equal(3);
392 return book.chapters.count({name: 'b'});
393 })
394 .then(function(count) {
395 count.should.equal(1);
396 done();
397 });
398 }
399 });
400
401 it('should set targetClass on scope property', function() {
402 should.equal(Book.prototype.chapters._targetClass, 'Chapter');
403 });
404
405 it('should update scoped record', function(done) {
406 let id;
407 Book.create(function(err, book) {
408 book.chapters.create({name: 'a'}, function(err, ch) {
409 id = ch.id;
410 book.chapters.updateById(id, {name: 'aa'}, function(err, ch) {
411 verify(book);
412 });
413 });
414 });
415
416 function verify(book) {
417 book.chapters.findById(id, function(err, ch) {
418 if (err) return done(err);
419 should.exist(ch);
420 ch.id.should.eql(id);
421 ch.name.should.equal('aa');
422 done();
423 });
424 }
425 });
426
427 it('should update scoped record with promises', function(done) {
428 let id;
429 Book.create()
430 .then(function(book) {
431 return book.chapters.create({name: 'a'})
432 .then(function(ch) {
433 id = ch.id;
434 return book.chapters.updateById(id, {name: 'aa'});
435 })
436 .then(function(ch) {
437 return verify(book);
438 });
439 })
440 .catch(done);
441
442 function verify(book) {
443 return book.chapters.findById(id)
444 .then(function(ch) {
445 should.exist(ch);
446 ch.id.should.eql(id);
447 ch.name.should.equal('aa');
448 done();
449 });
450 }
451 });
452
453 it('should destroy scoped record', function(done) {
454 let id;
455 Book.create(function(err, book) {
456 book.chapters.create({name: 'a'}, function(err, ch) {
457 id = ch.id;
458 book.chapters.destroy(id, function(err, ch) {
459 verify(book);
460 });
461 });
462 });
463
464 function verify(book) {
465 book.chapters.findById(id, function(err, ch) {
466 should.exist(err);
467 done();
468 });
469 }
470 });
471
472 it('should destroy scoped record with promises', function(done) {
473 let id;
474 Book.create()
475 .then(function(book) {
476 return book.chapters.create({name: 'a'})
477 .then(function(ch) {
478 id = ch.id;
479 return book.chapters.destroy(id);
480 })
481 .then(function(ch) {
482 return verify(book);
483 });
484 })
485 .catch(done);
486
487 function verify(book) {
488 return book.chapters.findById(id)
489 .catch(function(err) {
490 should.exist(err);
491 done();
492 });
493 }
494 });
495
496 it('should check existence of a scoped record', function(done) {
497 let id;
498 Book.create(function(err, book) {
499 book.chapters.create({name: 'a'}, function(err, ch) {
500 id = ch.id;
501 book.chapters.create({name: 'z'}, function() {
502 book.chapters.create({name: 'c'}, function() {
503 verify(book);
504 });
505 });
506 });
507 });
508
509 function verify(book) {
510 book.chapters.exists(id, function(err, flag) {
511 if (err) return done(err);
512 flag.should.be.eql(true);
513 done();
514 });
515 }
516 });
517
518 it('should check existence of a scoped record with promises', function(done) {
519 let id;
520 Book.create()
521 .then(function(book) {
522 return book.chapters.create({name: 'a'})
523 .then(function(ch) {
524 id = ch.id;
525 return book.chapters.create({name: 'z'});
526 })
527 .then(function() {
528 return book.chapters.create({name: 'c'});
529 })
530 .then(function() {
531 return verify(book);
532 });
533 }).catch(done);
534
535 function verify(book) {
536 return book.chapters.exists(id)
537 .then(function(flag) {
538 flag.should.be.eql(true);
539 done();
540 });
541 }
542 });
543
544 it('should check ignore related data on creation - array', function(done) {
545 Book.create({chapters: []}, function(err, book) {
546 if (err) return done(err);
547 book.chapters.should.be.a.function;
548 const obj = book.toObject();
549 should.not.exist(obj.chapters);
550 done();
551 });
552 });
553
554 it('should check ignore related data on creation with promises - array', function(done) {
555 Book.create({chapters: []})
556 .then(function(book) {
557 book.chapters.should.be.a.function;
558 const obj = book.toObject();
559 should.not.exist(obj.chapters);
560 done();
561 }).catch(done);
562 });
563
564 it('should check ignore related data on creation - object', function(done) {
565 Book.create({chapters: {}}, function(err, book) {
566 if (err) return done(err);
567 book.chapters.should.be.a.function;
568 const obj = book.toObject();
569 should.not.exist(obj.chapters);
570 done();
571 });
572 });
573
574 it('should check ignore related data on creation with promises - object', function(done) {
575 Book.create({chapters: {}})
576 .then(function(book) {
577 book.chapters.should.be.a.function;
578 const obj = book.toObject();
579 should.not.exist(obj.chapters);
580 done();
581 }).catch(done);
582 });
583 });
584 });
585
586 describe('hasMany through', function() {
587 let Physician, Patient, Appointment, Address;
588
589 before(function(done) {
590 Physician = db.define('Physician', {name: String});
591 Patient = db.define('Patient', {name: String, age: Number, realm: String,
592 sequence: {type: Number, index: true}});
593 Appointment = db.define('Appointment', {date: {type: Date,
594 default: function() {
595 return new Date();
596 }}});
597 Address = db.define('Address', {name: String});
598
599 Physician.hasMany(Patient, {through: Appointment});
600 Patient.hasMany(Physician, {through: Appointment});
601 Patient.belongsTo(Address);
602 Appointment.belongsTo(Patient);
603 Appointment.belongsTo(Physician);
604
605 db.automigrate(['Physician', 'Patient', 'Appointment', 'Address'], done);
606 });
607
608 it('should build record on scope', function(done) {
609 Physician.create(function(err, physician) {
610 const patient = physician.patients.build();
611 patient.physicianId.should.eql(physician.id);
612 patient.save(done);
613 });
614 });
615
616 it('should create record on scope', function(done) {
617 Physician.create(function(err, physician) {
618 physician.patients.create(function(err, patient) {
619 if (err) return done(err);
620 should.exist(patient);
621 Appointment.find({where: {physicianId: physician.id, patientId: patient.id}},
622 function(err, apps) {
623 if (err) return done(err);
624 apps.should.have.lengthOf(1);
625 done();
626 });
627 });
628 });
629 });
630
631 it('should create record on scope with promises', function(done) {
632 Physician.create()
633 .then(function(physician) {
634 return physician.patients.create()
635 .then(function(patient) {
636 should.exist(patient);
637 return Appointment.find({where: {physicianId: physician.id, patientId: patient.id}})
638 .then(function(apps) {
639 apps.should.have.lengthOf(1);
640 done();
641 });
642 });
643 }).catch(done);
644 });
645
646 it('should create multiple records on scope', function(done) {
647 const async = require('async');
648 Physician.create(function(err, physician) {
649 physician.patients.create([{}, {}], function(err, patients) {
650 if (err) return done(err);
651 should.exist(patients);
652 patients.should.have.lengthOf(2);
653 function verifyPatient(patient, next) {
654 Appointment.find({where: {
655 physicianId: physician.id,
656 patientId: patient.id,
657 }},
658 function(err, apps) {
659 if (err) return done(err);
660 apps.should.have.lengthOf(1);
661 next();
662 });
663 }
664 async.forEach(patients, verifyPatient, done);
665 });
666 });
667 });
668
669 it('should create multiple records on scope with promises', function(done) {
670 const async = require('async');
671 Physician.create()
672 .then(function(physician) {
673 return physician.patients.create([{}, {}])
674 .then(function(patients) {
675 should.exist(patients);
676 patients.should.have.lengthOf(2);
677 function verifyPatient(patient, next) {
678 Appointment.find({where: {
679 physicianId: physician.id,
680 patientId: patient.id,
681 }})
682 .then(function(apps) {
683 apps.should.have.lengthOf(1);
684 next();
685 });
686 }
687 async.forEach(patients, verifyPatient, done);
688 });
689 }).catch(done);
690 });
691
692 it('should fetch all scoped instances', function(done) {
693 Physician.create(function(err, physician) {
694 physician.patients.create({name: 'a'}, function() {
695 physician.patients.create({name: 'z'}, function() {
696 physician.patients.create({name: 'c'}, function() {
697 verify(physician);
698 });
699 });
700 });
701 });
702 function verify(physician) {
703 physician.patients(function(err, ch) {
704 const patients = physician.patients();
705 patients.should.eql(ch);
706
707 if (err) return done(err);
708 should.exist(ch);
709 ch.should.have.lengthOf(3);
710 done();
711 });
712 }
713 });
714
715 it('should fetch all scoped instances with promises', function(done) {
716 Physician.create()
717 .then(function(physician) {
718 return physician.patients.create({name: 'a'})
719 .then(function() {
720 return physician.patients.create({name: 'z'});
721 })
722 .then(function() {
723 return physician.patients.create({name: 'c'});
724 })
725 .then(function() {
726 return verify(physician);
727 });
728 }).catch(done);
729 function verify(physician) {
730 return physician.patients.find()
731 .then(function(ch) {
732 const patients = physician.patients();
733 should.equal(patients, ch);
734
735 should.exist(ch);
736 ch.should.have.lengthOf(3);
737 done();
738 });
739 }
740 });
741
742 describe('fetch scoped instances with paging filters', function() {
743 let samplePatientId;
744 let physician;
745
746 beforeEach(createSampleData);
747
748 context('with filter skip', function() {
749 bdd.itIf(connectorCapabilities.supportPagination !== false,
750 'skips the first patient', function(done) {
751 physician.patients({skip: 1, order: 'sequence'}, function(err, ch) {
752 if (err) return done(err);
753 should.exist(ch);
754 ch.should.have.lengthOf(2);
755 ch[0].name.should.eql('z');
756 ch[1].name.should.eql('c');
757 done();
758 });
759 });
760 });
761 context('with filter order', function() {
762 it('orders the result by patient name', function(done) {
763 const filter = connectorCapabilities.adhocSort !== false ? {order: 'name DESC'} : {};
764 physician.patients(filter, function(err, ch) {
765 if (err) return done(err);
766 should.exist(ch);
767 ch.should.have.lengthOf(3);
768 if (connectorCapabilities.adhocSort !== false) {
769 ch[0].name.should.eql('z');
770 ch[1].name.should.eql('c');
771 ch[2].name.should.eql('a');
772 } else {
773 const acz = ['a', 'c', 'z'];
774 ch[0].name.should.be.oneOf(acz);
775 ch[1].name.should.be.oneOf(acz);
776 ch[2].name.should.be.oneOf(acz);
777 }
778 done();
779 });
780 });
781 });
782 context('with filter limit', function() {
783 it('limits to 1 result', function(done) {
784 physician.patients({limit: 1, order: 'sequence'}, function(err, ch) {
785 if (err) return done(err);
786 should.exist(ch);
787 ch.should.have.lengthOf(1);
788 if (connectorCapabilities.adhocSort !== false) {
789 ch[0].name.should.eql('a');
790 } else {
791 ch[0].name.should.be.oneOf(['a', 'c', 'z']);
792 }
793 done();
794 });
795 });
796 });
797 context('with filter fields', function() {
798 it('includes field \'name\' but not \'age\'', function(done) {
799 const fieldsFilter = {
800 fields: {name: true, age: false},
801 order: 'sequence',
802 };
803 physician.patients(fieldsFilter, function(err, ch) {
804 if (err) return done(err);
805 should.exist(ch);
806 should.exist(ch[0].name);
807 if (connectorCapabilities.adhocSort !== false) {
808 ch[0].name.should.eql('a');
809 } else {
810 ch[0].name.should.be.oneOf(['a', 'c', 'z']);
811 }
812 should.not.exist(ch[0].age);
813 done();
814 });
815 });
816 });
817 context('with filter include', function() {
818 it('returns physicians included in patient', function(done) {
819 const includeFilter = {include: 'physicians'};
820 physician.patients(includeFilter, function(err, ch) {
821 if (err) return done(err);
822 ch.should.have.lengthOf(3);
823 should.exist(ch[0].physicians);
824 done();
825 });
826 });
827 });
828 context('with filter where', function() {
829 it('returns patient where id equal to samplePatientId', function(done) {
830 const whereFilter = {where: {id: samplePatientId}};
831 physician.patients(whereFilter, function(err, ch) {
832 if (err) return done(err);
833 should.exist(ch);
834 ch.should.have.lengthOf(1);
835 ch[0].id.should.eql(samplePatientId);
836 done();
837 });
838 });
839 it('returns patient where name equal to samplePatient name', function(done) {
840 const whereFilter = {where: {name: 'a'}};
841 physician.patients(whereFilter, function(err, ch) {
842 if (err) return done(err);
843 should.exist(ch);
844 ch.should.have.lengthOf(1);
845 ch[0].name.should.eql('a');
846 done();
847 });
848 });
849 it('returns patients where id in an array', function(done) {
850 const idArr = [];
851 let whereFilter;
852 physician.patients.create({name: 'b'}, function(err, p) {
853 idArr.push(samplePatientId, p.id);
854 whereFilter = {where: {id: {inq: idArr}}};
855 physician.patients(whereFilter, function(err, ch) {
856 if (err) return done(err);
857 should.exist(ch);
858 ch.should.have.lengthOf(2);
859 if (typeof idArr[0] === 'object') {
860 // mongodb returns `id` as an object
861 idArr[0] = idArr[0].toString();
862 idArr[1] = idArr[1].toString();
863 idArr.indexOf(ch[0].id.toString()).should.not.equal(-1);
864 idArr.indexOf(ch[1].id.toString()).should.not.equal(-1);
865 } else {
866 idArr.indexOf(ch[0].id).should.not.equal(-1);
867 idArr.indexOf(ch[1].id).should.not.equal(-1);
868 }
869 done();
870 });
871 });
872 });
873 it('returns empty result when patientId does not belongs to physician', function(done) {
874 Patient.create({name: 'x'}, function(err, p) {
875 if (err) return done(err);
876 should.exist(p);
877
878 const wrongWhereFilter = {where: {id: p.id}};
879 physician.patients(wrongWhereFilter, function(err, ch) {
880 if (err) return done(err);
881 should.exist(ch);
882 ch.should.have.lengthOf(0);
883 done();
884 });
885 });
886 });
887 });
888 context('findById with filter include', function() {
889 it('returns patient where id equal to \'samplePatientId\'' +
890 'with included physicians', function(done) {
891 const includeFilter = {include: 'physicians'};
892 physician.patients.findById(samplePatientId,
893 includeFilter, function(err, ch) {
894 if (err) return done(err);
895 should.exist(ch);
896 ch.id.should.eql(samplePatientId);
897 should.exist(ch.physicians);
898 done();
899 });
900 });
901 });
902 context('findById with filter fields', function() {
903 it('returns patient where id equal to \'samplePatientId\'' +
904 'with field \'name\' but not \'age\'', function(done) {
905 const fieldsFilter = {fields: {name: true, age: false}};
906 physician.patients.findById(samplePatientId,
907 fieldsFilter, function(err, ch) {
908 if (err) return done(err);
909 should.exist(ch);
910 should.exist(ch.name);
911 ch.name.should.eql('a');
912 should.not.exist(ch.age);
913 done();
914 });
915 });
916 });
917 context('findById with include filter that contains string fields', function() {
918 it('should accept string and convert it to array', function(done) {
919 const includeFilter = {include: {relation: 'patients', scope: {fields: 'name'}}};
920 const physicianId = physician.id;
921 Physician.findById(physicianId, includeFilter, function(err, result) {
922 if (err) return done(err);
923 should.exist(result);
924 result.id.should.eql(physicianId);
925 should.exist(result.patients);
926 result.patients().should.be.an.instanceOf(Array);
927 should.exist(result.patients()[0]);
928 should.exist(result.patients()[0].name);
929 should.not.exist(result.patients()[0].age);
930 done();
931 });
932 });
933 });
934
935 function createSampleData(done) {
936 Physician.create(function(err, result) {
937 result.patients.create({name: 'a', age: '10', sequence: 1},
938 function(err, p) {
939 samplePatientId = p.id;
940 result.patients.create({name: 'z', age: '20', sequence: 2},
941 function() {
942 result.patients.create({name: 'c', sequence: 3}, function() {
943 physician = result;
944 done();
945 });
946 });
947 });
948 });
949 }
950 });
951
952 describe('find over related model with options', function() {
953 after(function() {
954 Physician.clearObservers('access');
955 Patient.clearObservers('access');
956 });
957 before(function() {
958 Physician.observe('access', beforeAccessFn);
959 Patient.observe('access', beforeAccessFn);
960
961 function beforeAccessFn(ctx, next) {
962 ctx.query.where.realm = ctx.options.realm;
963 next();
964 }
965 });
966 it('should find be filtered from option', function(done) {
967 let id;
968 Physician.create(function(err, physician) {
969 if (err) return done(err);
970 physician.patients.create({name: 'a', realm: 'test'}, function(err, ch) {
971 if (err) return done(err);
972 id = ch.id;
973 physician.patients.create({name: 'z', realm: 'test'}, function(err) {
974 if (err) return done(err);
975 physician.patients.create({name: 'c', realm: 'anotherRealm'}, function(err) {
976 if (err) return done(err);
977 verify(physician);
978 });
979 });
980 });
981 });
982
983 function verify(physician) {
984 physician.patients({order: 'name ASC'}, {realm: 'test'}, function(err, records) {
985 if (err) return done(err);
986 should.exist(records);
987 records.length.should.eql(2);
988 const expected = ['a:test', 'z:test'];
989 const actual = records.map(function(r) { return r.name + ':' + r.realm; });
990 actual.sort().should.eql(expected.sort());
991 done();
992 });
993 }
994 });
995 });
996
997 it('should find scoped record', function(done) {
998 let id;
999 Physician.create(function(err, physician) {
1000 physician.patients.create({name: 'a'}, function(err, ch) {
1001 id = ch.id;
1002 physician.patients.create({name: 'z'}, function() {
1003 physician.patients.create({name: 'c'}, function() {
1004 verify(physician);
1005 });
1006 });
1007 });
1008 });
1009
1010 function verify(physician) {
1011 physician.patients.findById(id, function(err, ch) {
1012 if (err) return done(err);
1013 should.exist(ch);
1014 ch.id.should.eql(id);
1015 done();
1016 });
1017 }
1018 });
1019
1020 it('should find scoped record with promises', function(done) {
1021 let id;
1022 Physician.create()
1023 .then(function(physician) {
1024 return physician.patients.create({name: 'a'})
1025 .then(function(ch) {
1026 id = ch.id;
1027 return physician.patients.create({name: 'z'});
1028 })
1029 .then(function() {
1030 return physician.patients.create({name: 'c'});
1031 })
1032 .then(function() {
1033 return verify(physician);
1034 });
1035 }).catch(done);
1036
1037 function verify(physician) {
1038 return physician.patients.findById(id, function(err, ch) {
1039 if (err) return done(err);
1040 should.exist(ch);
1041 ch.id.should.eql(id);
1042 done();
1043 });
1044 }
1045 });
1046
1047 it('should allow to use include syntax on related data', function(done) {
1048 Physician.create(function(err, physician) {
1049 physician.patients.create({name: 'a'}, function(err, patient) {
1050 Address.create({name: 'z'}, function(err, address) {
1051 if (err) return done(err);
1052 patient.address(address);
1053 patient.save(function() {
1054 verify(physician, address.id);
1055 });
1056 });
1057 });
1058 });
1059 function verify(physician, addressId) {
1060 physician.patients({include: 'address'}, function(err, ch) {
1061 if (err) return done(err);
1062 should.exist(ch);
1063 ch.should.have.lengthOf(1);
1064 ch[0].addressId.should.eql(addressId);
1065 const address = ch[0].address();
1066 should.exist(address);
1067 address.should.be.an.instanceof(Address);
1068 address.name.should.equal('z');
1069 done();
1070 });
1071 }
1072 });
1073
1074 it('should allow to use include syntax on related data with promises', function(done) {
1075 Physician.create()
1076 .then(function(physician) {
1077 return physician.patients.create({name: 'a'})
1078 .then(function(patient) {
1079 return Address.create({name: 'z'})
1080 .then(function(address) {
1081 patient.address(address);
1082 return patient.save()
1083 .then(function() {
1084 return verify(physician, address.id);
1085 });
1086 });
1087 });
1088 }).catch(done);
1089
1090 function verify(physician, addressId) {
1091 return physician.patients.find({include: 'address'})
1092 .then(function(ch) {
1093 should.exist(ch);
1094 ch.should.have.lengthOf(1);
1095 ch[0].addressId.toString().should.eql(addressId.toString());
1096 const address = ch[0].address();
1097 should.exist(address);
1098 address.should.be.an.instanceof(Address);
1099 address.name.should.equal('z');
1100 done();
1101 });
1102 }
1103 });
1104
1105 it('should set targetClass on scope property', function() {
1106 should.equal(Physician.prototype.patients._targetClass, 'Patient');
1107 });
1108
1109 it('should update scoped record', function(done) {
1110 let id;
1111 Physician.create(function(err, physician) {
1112 physician.patients.create({name: 'a'}, function(err, ch) {
1113 id = ch.id;
1114 physician.patients.updateById(id, {name: 'aa'}, function(err, ch) {
1115 verify(physician);
1116 });
1117 });
1118 });
1119
1120 function verify(physician) {
1121 physician.patients.findById(id, function(err, ch) {
1122 if (err) return done(err);
1123 should.exist(ch);
1124 ch.id.should.eql(id);
1125 ch.name.should.equal('aa');
1126 done();
1127 });
1128 }
1129 });
1130
1131 it('should update scoped record with promises', function(done) {
1132 let id;
1133 Physician.create()
1134 .then(function(physician) {
1135 return physician.patients.create({name: 'a'})
1136 .then(function(ch) {
1137 id = ch.id;
1138 return physician.patients.updateById(id, {name: 'aa'})
1139 .then(function(ch) {
1140 return verify(physician);
1141 });
1142 });
1143 }).catch(done);
1144
1145 function verify(physician) {
1146 return physician.patients.findById(id)
1147 .then(function(ch) {
1148 should.exist(ch);
1149 ch.id.should.eql(id);
1150 ch.name.should.equal('aa');
1151 done();
1152 });
1153 }
1154 });
1155
1156 bdd.itIf(connectorCapabilities.deleteWithOtherThanId !== false,
1157 'should destroy scoped record', function(done) {
1158 let id;
1159 Physician.create(function(err, physician) {
1160 physician.patients.create({name: 'a'}, function(err, ch) {
1161 id = ch.id;
1162 physician.patients.destroy(id, function(err, ch) {
1163 verify(physician);
1164 });
1165 });
1166 });
1167
1168 function verify(physician) {
1169 physician.patients.findById(id, function(err, ch) {
1170 should.exist(err);
1171 done();
1172 });
1173 }
1174 });
1175
1176 bdd.itIf(connectorCapabilities.deleteWithOtherThanId !== false,
1177 'should destroy scoped record with promises', function(done) {
1178 let id;
1179 Physician.create()
1180 .then(function(physician) {
1181 return physician.patients.create({name: 'a'})
1182 .then(function(ch) {
1183 id = ch.id;
1184 return physician.patients.destroy(id)
1185 .then(function(ch) {
1186 return verify(physician);
1187 });
1188 });
1189 }).catch(done);
1190
1191 function verify(physician) {
1192 return physician.patients.findById(id)
1193 .then(function(ch) {
1194 should.not.exist(ch);
1195 done();
1196 })
1197 .catch(function(err) {
1198 should.exist(err);
1199 done();
1200 });
1201 }
1202 });
1203
1204 it('should check existence of a scoped record', function(done) {
1205 let id;
1206 Physician.create(function(err, physician) {
1207 physician.patients.create({name: 'a'}, function(err, ch) {
1208 if (err) return done(err);
1209 id = ch.id;
1210 physician.patients.create({name: 'z'}, function() {
1211 physician.patients.create({name: 'c'}, function() {
1212 verify(physician);
1213 });
1214 });
1215 });
1216 });
1217
1218 function verify(physician) {
1219 physician.patients.exists(id, function(err, flag) {
1220 if (err) return done(err);
1221 flag.should.be.eql(true);
1222 done();
1223 });
1224 }
1225 });
1226
1227 it('should check existence of a scoped record with promises', function(done) {
1228 let id;
1229 Physician.create()
1230 .then(function(physician) {
1231 return physician.patients.create({name: 'a'})
1232 .then(function(ch) {
1233 id = ch.id;
1234 return physician.patients.create({name: 'z'});
1235 })
1236 .then(function() {
1237 return physician.patients.create({name: 'c'});
1238 })
1239 .then(function() {
1240 return verify(physician);
1241 });
1242 }).catch(done);
1243
1244 function verify(physician) {
1245 return physician.patients.exists(id)
1246 .then(function(flag) {
1247 flag.should.be.eql(true);
1248 done();
1249 });
1250 }
1251 });
1252
1253 it('should allow to add connection with instance', function(done) {
1254 Physician.create({name: 'ph1'}, function(e, physician) {
1255 Patient.create({name: 'pa1'}, function(e, patient) {
1256 physician.patients.add(patient, function(e, app) {
1257 should.not.exist(e);
1258 should.exist(app);
1259 app.should.be.an.instanceOf(Appointment);
1260 app.physicianId.should.eql(physician.id);
1261 app.patientId.should.eql(patient.id);
1262 done();
1263 });
1264 });
1265 });
1266 });
1267
1268 it('should allow to add connection with instance with promises', function(done) {
1269 Physician.create({name: 'ph1'})
1270 .then(function(physician) {
1271 return Patient.create({name: 'pa1'})
1272 .then(function(patient) {
1273 return physician.patients.add(patient)
1274 .then(function(app) {
1275 should.exist(app);
1276 app.should.be.an.instanceOf(Appointment);
1277 app.physicianId.should.eql(physician.id);
1278 app.patientId.should.eql(patient.id);
1279 done();
1280 });
1281 });
1282 }).catch(done);
1283 });
1284
1285 it('should allow to add connection with through data', function(done) {
1286 Physician.create({name: 'ph1'}, function(e, physician) {
1287 Patient.create({name: 'pa1'}, function(e, patient) {
1288 const now = Date.now();
1289 physician.patients.add(patient, {date: new Date(now)}, function(e, app) {
1290 should.not.exist(e);
1291 should.exist(app);
1292 app.should.be.an.instanceOf(Appointment);
1293 app.physicianId.should.eql(physician.id);
1294 app.patientId.should.eql(patient.id);
1295 app.patientId.should.eql(patient.id);
1296 app.date.getTime().should.equal(now);
1297 done();
1298 });
1299 });
1300 });
1301 });
1302
1303 it('should allow to add connection with through data with promises', function(done) {
1304 Physician.create({name: 'ph1'})
1305 .then(function(physician) {
1306 return Patient.create({name: 'pa1'})
1307 .then(function(patient) {
1308 const now = Date.now();
1309 return physician.patients.add(patient, {date: new Date(now)})
1310 .then(function(app) {
1311 should.exist(app);
1312 app.should.be.an.instanceOf(Appointment);
1313 app.physicianId.should.eql(physician.id);
1314 app.patientId.should.eql(patient.id);
1315 app.patientId.should.eql(patient.id);
1316 app.date.getTime().should.equal(now);
1317 done();
1318 });
1319 });
1320 }).catch(done);
1321 });
1322
1323 bdd.itIf(connectorCapabilities.deleteWithOtherThanId !== false,
1324 'should allow to remove connection with instance', function(done) {
1325 let id;
1326 Physician.create(function(err, physician) {
1327 physician.patients.create({name: 'a'}, function(err, patient) {
1328 id = patient.id;
1329 physician.patients.remove(id, function(err, ch) {
1330 verify(physician);
1331 });
1332 });
1333 });
1334
1335 function verify(physician) {
1336 physician.patients.exists(id, function(err, flag) {
1337 if (err) return done(err);
1338 flag.should.be.eql(false);
1339 done();
1340 });
1341 }
1342 });
1343
1344 bdd.itIf(connectorCapabilities.deleteWithOtherThanId !== false,
1345 'should allow to remove connection with instance with promises', function(done) {
1346 let id;
1347 Physician.create()
1348 .then(function(physician) {
1349 return physician.patients.create({name: 'a'})
1350 .then(function(patient) {
1351 id = patient.id;
1352 return physician.patients.remove(id)
1353 .then(function(ch) {
1354 return verify(physician);
1355 });
1356 });
1357 }).catch(done);
1358
1359 function verify(physician) {
1360 return physician.patients.exists(id)
1361 .then(function(flag) {
1362 flag.should.be.eql(false);
1363 done();
1364 });
1365 }
1366 });
1367
1368 beforeEach(function(done) {
1369 Appointment.destroyAll(function(err) {
1370 Physician.destroyAll(function(err) {
1371 Patient.destroyAll(done);
1372 });
1373 });
1374 });
1375 });
1376
1377 describe('hasMany through - collect', function() {
1378 let Physician, Patient, Appointment, Address;
1379 let idPatient, idPhysician;
1380
1381 beforeEach(function(done) {
1382 idPatient = uid.fromConnector(db) || 1234;
1383 idPhysician = uid.fromConnector(db) || 2345;
1384 Physician = db.define('Physician', {name: String});
1385 Patient = db.define('Patient', {name: String});
1386 Appointment = db.define('Appointment', {date: {type: Date,
1387 default: function() {
1388 return new Date();
1389 }}});
1390 Address = db.define('Address', {name: String});
1391
1392 db.automigrate(['Physician', 'Patient', 'Appointment', 'Address'], done);
1393 });
1394
1395 describe('with default options', function() {
1396 it('can determine the collect by modelTo\'s name as default', function() {
1397 Physician.hasMany(Patient, {through: Appointment});
1398 Patient.hasMany(Physician, {through: Appointment, as: 'yyy'});
1399 Patient.belongsTo(Address);
1400 Appointment.belongsTo(Physician);
1401 Appointment.belongsTo(Patient);
1402 const physician = new Physician({id: idPhysician});
1403 const scope1 = physician.patients._scope;
1404 scope1.should.have.property('collect', 'patient');
1405 scope1.should.have.property('include', 'patient');
1406 const patient = new Patient({id: idPatient});
1407 const scope2 = patient.yyy._scope;
1408 scope2.should.have.property('collect', 'physician');
1409 scope2.should.have.property('include', 'physician');
1410 });
1411 });
1412
1413 describe('when custom reverse belongsTo names for both sides', function() {
1414 it('can determine the collect via keyThrough', function() {
1415 Physician.hasMany(Patient, {
1416 through: Appointment, foreignKey: 'fooId', keyThrough: 'barId',
1417 });
1418 Patient.hasMany(Physician, {
1419 through: Appointment, foreignKey: 'barId', keyThrough: 'fooId', as: 'yyy',
1420 });
1421 Appointment.belongsTo(Physician, {as: 'foo'});
1422 Appointment.belongsTo(Patient, {as: 'bar'});
1423 Patient.belongsTo(Address); // jam.
1424 Appointment.belongsTo(Patient, {as: 'car'}); // jam. Should we complain in this case???
1425
1426 const physician = new Physician({id: idPhysician});
1427 const scope1 = physician.patients._scope;
1428 scope1.should.have.property('collect', 'bar');
1429 scope1.should.have.property('include', 'bar');
1430 const patient = new Patient({id: idPatient});
1431 const scope2 = patient.yyy._scope;
1432 scope2.should.have.property('collect', 'foo');
1433 scope2.should.have.property('include', 'foo');
1434 });
1435
1436 it('can determine the collect via modelTo name', function() {
1437 Physician.hasMany(Patient, {through: Appointment});
1438 Patient.hasMany(Physician, {through: Appointment, as: 'yyy'});
1439 Appointment.belongsTo(Physician, {as: 'foo', foreignKey: 'physicianId'});
1440 Appointment.belongsTo(Patient, {as: 'bar', foreignKey: 'patientId'});
1441 Patient.belongsTo(Address); // jam.
1442
1443 const physician = new Physician({id: idPhysician});
1444 const scope1 = physician.patients._scope;
1445 scope1.should.have.property('collect', 'bar');
1446 scope1.should.have.property('include', 'bar');
1447 const patient = new Patient({id: idPatient});
1448 const scope2 = patient.yyy._scope;
1449 scope2.should.have.property('collect', 'foo');
1450 scope2.should.have.property('include', 'foo');
1451 });
1452
1453 it('can determine the collect via modelTo name (with jams)', function() {
1454 Physician.hasMany(Patient, {through: Appointment});
1455 Patient.hasMany(Physician, {through: Appointment, as: 'yyy'});
1456 Appointment.belongsTo(Physician, {as: 'foo', foreignKey: 'physicianId'});
1457 Appointment.belongsTo(Patient, {as: 'bar', foreignKey: 'patientId'});
1458 Patient.belongsTo(Address); // jam.
1459 Appointment.belongsTo(Physician, {as: 'goo', foreignKey: 'physicianId'}); // jam. Should we complain in this case???
1460 Appointment.belongsTo(Patient, {as: 'car', foreignKey: 'patientId'}); // jam. Should we complain in this case???
1461
1462 const physician = new Physician({id: idPhysician});
1463 const scope1 = physician.patients._scope;
1464 scope1.should.have.property('collect', 'bar');
1465 scope1.should.have.property('include', 'bar');
1466 const patient = new Patient({id: idPatient});
1467 const scope2 = patient.yyy._scope;
1468 scope2.should.have.property('collect', 'foo'); // first matched relation
1469 scope2.should.have.property('include', 'foo'); // first matched relation
1470 });
1471 });
1472
1473 describe('when custom reverse belongsTo name for one side only', function() {
1474 beforeEach(function() {
1475 Physician.hasMany(Patient, {as: 'xxx', through: Appointment, foreignKey: 'fooId'});
1476 Patient.hasMany(Physician, {as: 'yyy', through: Appointment, keyThrough: 'fooId'});
1477 Appointment.belongsTo(Physician, {as: 'foo'});
1478 Appointment.belongsTo(Patient);
1479 Patient.belongsTo(Address); // jam.
1480 Appointment.belongsTo(Physician, {as: 'bar'}); // jam. Should we complain in this case???
1481 });
1482
1483 it('can determine the collect via model name', function() {
1484 const physician = new Physician({id: idPhysician});
1485 const scope1 = physician.xxx._scope;
1486 scope1.should.have.property('collect', 'patient');
1487 scope1.should.have.property('include', 'patient');
1488 });
1489
1490 it('can determine the collect via keyThrough', function() {
1491 const patient = new Patient({id: idPatient});
1492 const scope2 = patient.yyy._scope;
1493 scope2.should.have.property('collect', 'foo');
1494 scope2.should.have.property('include', 'foo');
1495 });
1496 });
1497 });
1498
1499 describe('hasMany through - customized relation name and foreign key', function() {
1500 let Physician, Patient, Appointment;
1501
1502 beforeEach(function(done) {
1503 Physician = db.define('Physician', {name: String});
1504 Patient = db.define('Patient', {name: String});
1505 Appointment = db.define('Appointment', {date: {type: Date, defaultFn: 'now'}});
1506
1507 db.automigrate(['Physician', 'Patient', 'Appointment'], done);
1508 });
1509
1510 it('should use real target class', function() {
1511 Physician.hasMany(Patient, {through: Appointment, as: 'xxx', foreignKey: 'aaaId', keyThrough: 'bbbId'});
1512 Patient.hasMany(Physician, {through: Appointment, as: 'yyy', foreignKey: 'bbbId', keyThrough: 'aaaId'});
1513 Appointment.belongsTo(Physician, {as: 'aaa', foreignKey: 'aaaId'});
1514 Appointment.belongsTo(Patient, {as: 'bbb', foreignKey: 'bbbId'});
1515 const physician = new Physician({id: 1});
1516 physician.xxx.should.have.property('_targetClass', 'Patient');
1517 const patient = new Patient({id: 1});
1518 patient.yyy.should.have.property('_targetClass', 'Physician');
1519 });
1520 });
1521
1522 describe('hasMany through bi-directional relations on the same model', function() {
1523 let User, Follow, Address;
1524 let idFollower, idFollowee;
1525
1526 before(function(done) {
1527 idFollower = uid.fromConnector(db) || 3456;
1528 idFollowee = uid.fromConnector(db) || 4567;
1529 User = db.define('User', {name: String});
1530 Follow = db.define('Follow', {date: {type: Date,
1531 default: function() {
1532 return new Date();
1533 }}});
1534 Address = db.define('Address', {name: String});
1535
1536 User.hasMany(User, {
1537 as: 'followers', foreignKey: 'followeeId', keyThrough: 'followerId', through: Follow,
1538 });
1539 User.hasMany(User, {
1540 as: 'following', foreignKey: 'followerId', keyThrough: 'followeeId', through: Follow,
1541 });
1542 User.belongsTo(Address);
1543 Follow.belongsTo(User, {as: 'follower'});
1544 Follow.belongsTo(User, {as: 'followee'});
1545 db.automigrate(['User', 'Follow'], done);
1546 });
1547
1548 it('should set foreignKeys of through model correctly in first relation',
1549 function(done) {
1550 const follower = new User({id: idFollower});
1551 const followee = new User({id: idFollowee});
1552 followee.followers.add(follower, function(err, throughInst) {
1553 if (err) return done(err);
1554 should.exist(throughInst);
1555 throughInst.followerId.should.eql(follower.id);
1556 throughInst.followeeId.should.eql(followee.id);
1557 done();
1558 });
1559 });
1560
1561 it('should set foreignKeys of through model correctly in second relation',
1562 function(done) {
1563 const follower = new User({id: idFollower});
1564 const followee = new User({id: idFollowee});
1565 follower.following.add(followee, function(err, throughInst) {
1566 if (err) return done(err);
1567 should.exist(throughInst);
1568 throughInst.followeeId.toString().should.eql(followee.id.toString());
1569 throughInst.followerId.toString().should.eql(follower.id.toString());
1570 done();
1571 });
1572 });
1573 });
1574
1575 describe('hasMany through - between same models', function() {
1576 let User, Follow, Address;
1577 let idFollower, idFollowee;
1578
1579 before(function(done) {
1580 idFollower = uid.fromConnector(db) || 3456;
1581 idFollowee = uid.fromConnector(db) || 4567;
1582 User = db.define('User', {name: String});
1583 Follow = db.define('Follow', {date: {type: Date,
1584 default: function() {
1585 return new Date();
1586 }}});
1587 Address = db.define('Address', {name: String});
1588
1589 User.hasMany(User, {
1590 as: 'followers', foreignKey: 'followeeId', keyThrough: 'followerId', through: Follow,
1591 });
1592 User.hasMany(User, {
1593 as: 'following', foreignKey: 'followerId', keyThrough: 'followeeId', through: Follow,
1594 });
1595 User.belongsTo(Address);
1596 Follow.belongsTo(User, {as: 'follower'});
1597 Follow.belongsTo(User, {as: 'followee'});
1598 db.automigrate(['User', 'Follow', 'Address'], done);
1599 });
1600
1601 it('should set the keyThrough and the foreignKey', function(done) {
1602 const user = new User({id: idFollower});
1603 const user2 = new User({id: idFollowee});
1604 user.following.add(user2, function(err, f) {
1605 if (err) return done(err);
1606 should.exist(f);
1607 f.followeeId.should.eql(user2.id);
1608 f.followerId.should.eql(user.id);
1609 done();
1610 });
1611 });
1612
1613 it('can determine the collect via keyThrough for each side', function() {
1614 const user = new User({id: idFollower});
1615 const scope1 = user.followers._scope;
1616 scope1.should.have.property('collect', 'follower');
1617 scope1.should.have.property('include', 'follower');
1618 const scope2 = user.following._scope;
1619 scope2.should.have.property('collect', 'followee');
1620 scope2.should.have.property('include', 'followee');
1621 });
1622 });
1623
1624 describe('hasMany with properties', function() {
1625 before(function(done) {
1626 Book = db.define('Book', {name: String, type: String});
1627 Chapter = db.define('Chapter', {name: {type: String, index: true},
1628 bookType: String});
1629 Book.hasMany(Chapter, {properties: {type: 'bookType'}});
1630 db.automigrate(['Book', 'Chapter'], done);
1631 });
1632
1633 it('should create record on scope', function(done) {
1634 Book.create({type: 'fiction'}, function(err, book) {
1635 book.chapters.create(function(err, c) {
1636 if (err) return done(err);
1637 should.exist(c);
1638 c.bookId.should.eql(book.id);
1639 c.bookType.should.equal('fiction');
1640 done();
1641 });
1642 });
1643 });
1644
1645 it('should create record on scope with promises', function(done) {
1646 Book.create({type: 'fiction'})
1647 .then(function(book) {
1648 return book.chapters.create()
1649 .then(function(c) {
1650 should.exist(c);
1651 c.bookId.should.eql(book.id);
1652 c.bookType.should.equal('fiction');
1653 done();
1654 });
1655 }).catch(done);
1656 });
1657 });
1658
1659 describe('hasMany with scope and properties', function() {
1660 it('can be declared with properties', function(done) {
1661 Category = db.define('Category', {name: String, jobType: String});
1662 Job = db.define('Job', {name: String, type: String});
1663
1664 Category.hasMany(Job, {
1665 properties: function(inst, target) {
1666 if (!inst.jobType) return; // skip
1667 return {type: inst.jobType};
1668 },
1669 scope: function(inst, filter) {
1670 const m = this.properties(inst); // re-use properties
1671 if (m) return {where: m};
1672 },
1673 });
1674 db.automigrate(['Category', 'Job'], done);
1675 });
1676
1677 it('should create record on scope', function(done) {
1678 Category.create(function(err, c) {
1679 should.not.exists(err);
1680 c.jobs.create({type: 'book'}, function(err, p) {
1681 should.not.exists(err);
1682 p.categoryId.should.eql(c.id);
1683 p.type.should.equal('book');
1684 c.jobs.create({type: 'widget'}, function(err, p) {
1685 should.not.exists(err);
1686 p.categoryId.should.eql(c.id);
1687 p.type.should.equal('widget');
1688 done();
1689 });
1690 });
1691 });
1692 });
1693
1694 it('should create record on scope with promises', function(done) {
1695 Category.create()
1696 .then(function(c) {
1697 return c.jobs.create({type: 'book'})
1698 .then(function(p) {
1699 p.categoryId.should.eql(c.id);
1700 p.type.should.equal('book');
1701 return c.jobs.create({type: 'widget'})
1702 .then(function(p) {
1703 p.categoryId.should.eql(c.id);
1704 p.type.should.equal('widget');
1705 done();
1706 });
1707 });
1708 }).catch(done);
1709 });
1710
1711 it('should find records on scope', function(done) {
1712 Category.findOne(function(err, c) {
1713 should.not.exists(err);
1714 c.jobs(function(err, jobs) {
1715 should.not.exists(err);
1716 jobs.should.have.length(2);
1717 done();
1718 });
1719 });
1720 });
1721
1722 it('should find records on scope with promises', function(done) {
1723 Category.findOne()
1724 .then(function(c) {
1725 return c.jobs.find();
1726 })
1727 .then(function(jobs) {
1728 jobs.should.have.length(2);
1729 done();
1730 })
1731 .catch(done);
1732 });
1733
1734 it('should find record on scope - filtered', function(done) {
1735 Category.findOne(function(err, c) {
1736 should.not.exists(err);
1737 c.jobs({where: {type: 'book'}}, function(err, jobs) {
1738 should.not.exists(err);
1739 jobs.should.have.length(1);
1740 jobs[0].type.should.equal('book');
1741 done();
1742 });
1743 });
1744 });
1745
1746 it('should find record on scope with promises - filtered', function(done) {
1747 Category.findOne()
1748 .then(function(c) {
1749 return c.jobs.find({where: {type: 'book'}});
1750 })
1751 .then(function(jobs) {
1752 jobs.should.have.length(1);
1753 jobs[0].type.should.equal('book');
1754 done();
1755 })
1756 .catch(done);
1757 });
1758
1759 // So why not just do the above? In LoopBack, the context
1760 // that gets passed into a beforeRemote handler contains
1761 // a reference to the parent scope/instance: ctx.instance
1762 // in order to enforce a (dynamic scope) at runtime
1763 // a temporary property can be set in the beforeRemoting
1764 // handler. Optionally,properties dynamic properties can be declared.
1765 //
1766 // The code below simulates this.
1767
1768 it('should create record on scope - properties', function(done) {
1769 Category.findOne(function(err, c) {
1770 should.not.exists(err);
1771 c.jobType = 'tool'; // temporary
1772 c.jobs.create(function(err, p) {
1773 p.categoryId.should.eql(c.id);
1774 p.type.should.equal('tool');
1775 done();
1776 });
1777 });
1778 });
1779
1780 // eslint-disable-next-line mocha/no-identical-title
1781 it('should find records on scope', function(done) {
1782 Category.findOne(function(err, c) {
1783 should.not.exists(err);
1784 c.jobs(function(err, jobs) {
1785 should.not.exists(err);
1786 jobs.should.have.length(3);
1787 done();
1788 });
1789 });
1790 });
1791
1792 it('should find record on scope - scoped', function(done) {
1793 Category.findOne(function(err, c) {
1794 should.not.exists(err);
1795 c.jobType = 'book'; // temporary, for scoping
1796 c.jobs(function(err, jobs) {
1797 should.not.exists(err);
1798 jobs.should.have.length(1);
1799 jobs[0].type.should.equal('book');
1800 done();
1801 });
1802 });
1803 });
1804
1805 // eslint-disable-next-line mocha/no-identical-title
1806 it('should find record on scope - scoped', function(done) {
1807 Category.findOne(function(err, c) {
1808 should.not.exists(err);
1809 c.jobType = 'tool'; // temporary, for scoping
1810 c.jobs(function(err, jobs) {
1811 should.not.exists(err);
1812 jobs.should.have.length(1);
1813 jobs[0].type.should.equal('tool');
1814 done();
1815 });
1816 });
1817 });
1818
1819 it('should find count of records on scope - scoped', function(done) {
1820 Category.findOne(function(err, c) {
1821 should.not.exists(err);
1822 c.jobType = 'tool'; // temporary, for scoping
1823 c.jobs.count(function(err, count) {
1824 should.not.exists(err);
1825 count.should.equal(1);
1826 done();
1827 });
1828 });
1829 });
1830
1831 bdd.itIf(connectorCapabilities.deleteWithOtherThanId !== false,
1832 'should delete records on scope - scoped', function(done) {
1833 Category.findOne(function(err, c) {
1834 should.not.exists(err);
1835 c.jobType = 'tool'; // temporary, for scoping
1836 c.jobs.destroyAll(function(err, result) {
1837 done(err);
1838 });
1839 });
1840 });
1841
1842 bdd.itIf(connectorCapabilities.deleteWithOtherThanId !== false,
1843 'should find record on scope - verify', function(done) {
1844 Category.findOne(function(err, c) {
1845 should.not.exists(err);
1846 c.jobs(function(err, jobs) {
1847 should.not.exists(err);
1848 jobs.should.have.length(2);
1849 done(err);
1850 });
1851 });
1852 });
1853 });
1854
1855 describe('relations validation', function() {
1856 let validationError;
1857 // define a mockup getRelationValidationMsg() method to log the validation error
1858 const logRelationValidationError = function(code, rType, rName) {
1859 validationError = {code, rType, rName};
1860 };
1861
1862 it('rejects belongsTo relation if `model` is not provided', function() {
1863 try {
1864 const Picture = db.define('Picture', {name: String}, {relations: {
1865 author: {
1866 type: 'belongsTo',
1867 foreignKey: 'authorId'},
1868 }});
1869 should.not.exist(Picture, 'relation validation should have thrown');
1870 } catch (err) {
1871 err.details.should.eql({
1872 code: 'BELONGS_TO_MISSING_MODEL',
1873 rType: 'belongsTo',
1874 rName: 'author'});
1875 }
1876 });
1877
1878 it('rejects polymorphic belongsTo relation if `model` is provided', function() {
1879 try {
1880 const Picture = db.define('Picture', {name: String}, {relations: {
1881 imageable: {
1882 type: 'belongsTo',
1883 model: 'Picture',
1884 polymorphic: true},
1885 }});
1886 should.not.exist(Picture, 'relation validation should have thrown');
1887 } catch (err) {
1888 err.details.should.eql({
1889 code: 'POLYMORPHIC_BELONGS_TO_MODEL',
1890 rType: 'belongsTo',
1891 rName: 'imageable'});
1892 }
1893 });
1894
1895 it('rejects polymorphic non belongsTo relation if `model` is not provided', function() {
1896 try {
1897 const Article = db.define('Picture', {name: String}, {relations: {
1898 pictures: {
1899 type: 'hasMany',
1900 polymorphic: 'imageable'},
1901 }});
1902 should.not.exist(Picture, 'relation validation should have thrown');
1903 } catch (err) {
1904 err.details.should.eql({
1905 code: 'POLYMORPHIC_NOT_BELONGS_TO_MISSING_MODEL',
1906 rType: 'hasMany',
1907 rName: 'pictures'});
1908 }
1909 });
1910
1911 it('rejects polymorphic relation if `foreignKey` is provided but discriminator ' +
1912 'is missing', function() {
1913 try {
1914 const Article = db.define('Picture', {name: String}, {relations: {
1915 pictures: {
1916 type: 'hasMany',
1917 model: 'Picture',
1918 polymorphic: {foreignKey: 'imageableId'}},
1919 }});
1920 should.not.exist(Picture, 'relation validation should have thrown');
1921 } catch (err) {
1922 err.details.should.eql({
1923 code: 'POLYMORPHIC_MISSING_DISCRIMINATOR',
1924 rType: 'hasMany',
1925 rName: 'pictures'});
1926 }
1927 });
1928
1929 it('rejects polymorphic relation if `discriminator` is provided but foreignKey ' +
1930 'is missing', function() {
1931 try {
1932 const Article = db.define('Picture', {name: String}, {relations: {
1933 pictures: {
1934 type: 'hasMany',
1935 model: 'Picture',
1936 polymorphic: {discriminator: 'imageableType'}},
1937 }});
1938 should.not.exist(Picture, 'relation validation should have thrown');
1939 } catch (err) {
1940 err.details.should.eql({
1941 code: 'POLYMORPHIC_MISSING_FOREIGN_KEY',
1942 rType: 'hasMany',
1943 rName: 'pictures'});
1944 }
1945 });
1946
1947 it('rejects polymorphic relation if `polymorphic.as` is provided along ' +
1948 'with custom foreignKey/discriminator', function() {
1949 try {
1950 const Article = db.define('Picture', {name: String}, {relations: {
1951 pictures: {
1952 type: 'hasMany',
1953 model: 'Picture',
1954 polymorphic: {
1955 as: 'image',
1956 foreignKey: 'imageableId',
1957 discriminator: 'imageableType',
1958 }},
1959 }});
1960 should.not.exist(Picture, 'relation validation should have thrown');
1961 } catch (err) {
1962 err.details.should.eql({
1963 code: 'POLYMORPHIC_EXTRANEOUS_AS',
1964 rType: 'hasMany',
1965 rName: 'pictures'});
1966 }
1967 });
1968
1969 it('rejects polymorphic relation if `polymorphic.selector` is provided along ' +
1970 'with custom foreignKey/discriminator', function() {
1971 try {
1972 const Article = db.define('Picture', {name: String}, {relations: {
1973 pictures: {
1974 type: 'hasMany',
1975 model: 'Picture',
1976 polymorphic: {
1977 selector: 'image',
1978 foreignKey: 'imageableId',
1979 discriminator: 'imageableType',
1980 }},
1981 }});
1982 should.not.exist(Picture, 'relation validation should have thrown');
1983 } catch (err) {
1984 err.details.should.eql({
1985 code: 'POLYMORPHIC_EXTRANEOUS_SELECTOR',
1986 rType: 'hasMany',
1987 rName: 'pictures'});
1988 }
1989 });
1990
1991 it('warns on use of deprecated `polymorphic.as` keyword in polymorphic relation', function() {
1992 let message = 'deprecation not reported';
1993 process.once('deprecation', function(err) { message = err.message; });
1994
1995 const Article = db.define('Picture', {name: String}, {relations: {
1996 pictures: {type: 'hasMany', model: 'Picture', polymorphic: {as: 'imageable'}},
1997 }});
1998
1999 message.should.match(/keyword `polymorphic.as` which will be DEPRECATED in LoopBack.next/);
2000 });
2001 });
2002
2003 describe('polymorphic hasOne', function() {
2004 before(function(done) {
2005 Picture = db.define('Picture', {name: String});
2006 Article = db.define('Article', {name: String});
2007 Employee = db.define('Employee', {name: String});
2008
2009 db.automigrate(['Picture', 'Article', 'Employee'], done);
2010 });
2011
2012 it('can be declared using default polymorphic selector', function(done) {
2013 Article.hasOne(Picture, {as: 'packshot', polymorphic: 'imageable'});
2014 Employee.hasOne(Picture, {as: 'mugshot', polymorphic: 'imageable'});
2015 Picture.belongsTo('imageable', {polymorphic: true});
2016
2017 Article.relations['packshot'].toJSON().should.eql({
2018 name: 'packshot',
2019 type: 'hasOne',
2020 modelFrom: 'Article',
2021 keyFrom: 'id',
2022 modelTo: 'Picture',
2023 keyTo: 'imageableId',
2024 multiple: false,
2025 polymorphic: {
2026 selector: 'imageable',
2027 foreignKey: 'imageableId',
2028 discriminator: 'imageableType',
2029 },
2030 });
2031
2032 Picture.relations['imageable'].toJSON().should.eql({
2033 name: 'imageable',
2034 type: 'belongsTo',
2035 modelFrom: 'Picture',
2036 keyFrom: 'imageableId',
2037 modelTo: '<polymorphic>',
2038 keyTo: 'id',
2039 multiple: false,
2040 polymorphic: {
2041 selector: 'imageable',
2042 foreignKey: 'imageableId',
2043 discriminator: 'imageableType',
2044 },
2045 });
2046
2047 db.automigrate(['Picture', 'Article', 'Employee'], done);
2048 });
2049
2050 it('should create polymorphic relation - Article', function(done) {
2051 Article.create({name: 'Article 1'}, function(err, article) {
2052 should.not.exists(err);
2053 article.packshot.create({name: 'Packshot'}, function(err, pic) {
2054 if (err) return done(err);
2055 should.exist(pic);
2056 pic.imageableId.should.eql(article.id);
2057 pic.imageableType.should.equal('Article');
2058 done();
2059 });
2060 });
2061 });
2062
2063 it('should create polymorphic relation with promises - article', function(done) {
2064 Article.create({name: 'Article 1'})
2065 .then(function(article) {
2066 return article.packshot.create({name: 'Packshot'})
2067 .then(function(pic) {
2068 should.exist(pic);
2069 pic.imageableId.should.eql(article.id);
2070 pic.imageableType.should.equal('Article');
2071 done();
2072 });
2073 }).catch(done);
2074 });
2075
2076 it('should create polymorphic relation - reader', function(done) {
2077 Employee.create({name: 'Employee 1'}, function(err, employee) {
2078 should.not.exists(err);
2079 employee.mugshot.create({name: 'Mugshot'}, function(err, pic) {
2080 if (err) return done(err);
2081 should.exist(pic);
2082 pic.imageableId.should.eql(employee.id);
2083 pic.imageableType.should.equal('Employee');
2084 done();
2085 });
2086 });
2087 });
2088
2089 it('should find polymorphic relation - article', function(done) {
2090 Article.findOne(function(err, article) {
2091 should.not.exists(err);
2092 article.packshot(function(err, pic) {
2093 if (err) return done(err);
2094
2095 const packshot = article.packshot();
2096 packshot.should.equal(pic);
2097
2098 pic.name.should.equal('Packshot');
2099 pic.imageableId.toString().should.eql(article.id.toString());
2100 pic.imageableType.should.equal('Article');
2101 done();
2102 });
2103 });
2104 });
2105
2106 it('should find polymorphic relation - employee', function(done) {
2107 Employee.findOne(function(err, employee) {
2108 should.not.exists(err);
2109 employee.mugshot(function(err, mugshot) {
2110 if (err) return done(err);
2111 mugshot.name.should.equal('Mugshot');
2112 mugshot.imageableId.toString().should.eql(employee.id.toString());
2113 mugshot.imageableType.should.equal('Employee');
2114 done();
2115 });
2116 });
2117 });
2118
2119 it('should include polymorphic relation - article', function(done) {
2120 Article.findOne({include: 'packshot'}, function(err, article) {
2121 should.not.exists(err);
2122 const packshot = article.packshot();
2123 should.exist(packshot);
2124 packshot.name.should.equal('Packshot');
2125 done();
2126 });
2127 });
2128
2129 it('should find polymorphic relation with promises - employee', function(done) {
2130 Employee.findOne()
2131 .then(function(employee) {
2132 return employee.mugshot.get()
2133 .then(function(pic) {
2134 pic.name.should.equal('Mugshot');
2135 pic.imageableId.toString().should.eql(employee.id.toString());
2136 pic.imageableType.should.equal('Employee');
2137 done();
2138 });
2139 }).catch(done);
2140 });
2141
2142 it('should find inverse polymorphic relation - article', function(done) {
2143 Picture.findOne({where: {name: 'Packshot'}}, function(err, pic) {
2144 should.not.exists(err);
2145 pic.imageable(function(err, imageable) {
2146 if (err) return done(err);
2147 imageable.should.be.instanceof(Article);
2148 imageable.name.should.equal('Article 1');
2149 done();
2150 });
2151 });
2152 });
2153
2154 it('should include inverse polymorphic relation - article', function(done) {
2155 Picture.findOne({where: {name: 'Packshot'}, include: 'imageable'},
2156 function(err, pic) {
2157 should.not.exists(err);
2158 const imageable = pic.imageable();
2159 should.exist(imageable);
2160 imageable.should.be.instanceof(Article);
2161 imageable.name.should.equal('Article 1');
2162 done();
2163 });
2164 });
2165
2166 it('should find inverse polymorphic relation - employee', function(done) {
2167 Picture.findOne({where: {name: 'Mugshot'}}, function(err, pic) {
2168 should.not.exists(err);
2169 pic.imageable(function(err, imageable) {
2170 if (err) return done(err);
2171 imageable.should.be.instanceof(Employee);
2172 imageable.name.should.equal('Employee 1');
2173 done();
2174 });
2175 });
2176 });
2177 });
2178
2179 describe('polymorphic hasOne with non standard ids', function() {
2180 before(function(done) {
2181 Picture = db.define('Picture', {name: String});
2182 Article = db.define('Article', {
2183 username: {type: String, id: true, generated: true},
2184 name: String,
2185 });
2186 Employee = db.define('Employee', {
2187 username: {type: String, id: true, generated: true},
2188 name: String,
2189 });
2190
2191 db.automigrate(['Picture', 'Article', 'Employee'], done);
2192 });
2193
2194 it('can be declared using custom foreignKey/discriminator', function(done) {
2195 Article.hasOne(Picture, {
2196 as: 'packshot',
2197 polymorphic: {
2198 foreignKey: 'oid',
2199 discriminator: 'type',
2200 },
2201 });
2202 Employee.hasOne(Picture, {
2203 as: 'mugshot',
2204 polymorphic: {
2205 foreignKey: 'oid',
2206 discriminator: 'type',
2207 },
2208 });
2209 Picture.belongsTo('imageable', {
2210 idName: 'username',
2211 polymorphic: {
2212 idType: Article.definition.properties.username.type,
2213 foreignKey: 'oid',
2214 discriminator: 'type',
2215 },
2216 });
2217
2218 Article.relations['packshot'].toJSON().should.eql({
2219 name: 'packshot',
2220 type: 'hasOne',
2221 modelFrom: 'Article',
2222 keyFrom: 'username',
2223 modelTo: 'Picture',
2224 keyTo: 'oid',
2225 multiple: false,
2226 polymorphic: {
2227 selector: 'packshot',
2228 foreignKey: 'oid',
2229 discriminator: 'type',
2230 },
2231 });
2232
2233 const imageableRel = Picture.relations['imageable'].toJSON();
2234
2235 // assert idType independantly
2236 assert(typeof imageableRel.polymorphic.idType == 'function');
2237
2238 // backup idType and remove it temporarily from the relation
2239 // object to ease the test
2240 const idType = imageableRel.polymorphic.idType;
2241 delete imageableRel.polymorphic.idType;
2242
2243 imageableRel.should.eql({
2244 name: 'imageable',
2245 type: 'belongsTo',
2246 modelFrom: 'Picture',
2247 keyFrom: 'oid',
2248 modelTo: '<polymorphic>',
2249 keyTo: 'username',
2250 multiple: false,
2251 polymorphic: {
2252 selector: 'imageable',
2253 foreignKey: 'oid',
2254 discriminator: 'type',
2255 },
2256 });
2257
2258 // restore idType for next tests
2259 imageableRel.polymorphic.idType = idType;
2260
2261 db.automigrate(['Picture', 'Article', 'Employee'], done);
2262 });
2263
2264 it('should create polymorphic relation - article', function(done) {
2265 Article.create({name: 'Article 1'}, function(err, article) {
2266 should.not.exists(err);
2267 article.packshot.create({name: 'Packshot'}, function(err, pic) {
2268 if (err) return done(err);
2269 should.exist(pic);
2270 pic.oid.toString().should.equal(article.username.toString());
2271 pic.type.should.equal('Article');
2272 done();
2273 });
2274 });
2275 });
2276
2277 it('should create polymorphic relation with promises - article', function(done) {
2278 Article.create({name: 'Article 1'})
2279 .then(function(article) {
2280 return article.packshot.create({name: 'Packshot'})
2281 .then(function(pic) {
2282 should.exist(pic);
2283 pic.oid.toString().should.equal(article.username.toString());
2284 pic.type.should.equal('Article');
2285 done();
2286 });
2287 }).catch(done);
2288 });
2289
2290 it('should create polymorphic relation - employee', function(done) {
2291 Employee.create({name: 'Employee 1'}, function(err, employee) {
2292 should.not.exists(err);
2293 employee.mugshot.create({name: 'Mugshot'}, function(err, pic) {
2294 if (err) return done(err);
2295 should.exist(pic);
2296 pic.oid.toString().should.equal(employee.username.toString());
2297 pic.type.should.equal('Employee');
2298 done();
2299 });
2300 });
2301 });
2302
2303 it('should find polymorphic relation - article', function(done) {
2304 Article.findOne(function(err, article) {
2305 should.not.exists(err);
2306 article.packshot(function(err, pic) {
2307 if (err) return done(err);
2308
2309 const packshot = article.packshot();
2310 packshot.should.equal(pic);
2311
2312 pic.name.should.equal('Packshot');
2313 pic.oid.toString().should.equal(article.username.toString());
2314 pic.type.should.equal('Article');
2315 done();
2316 });
2317 });
2318 });
2319
2320 it('should find polymorphic relation - employee', function(done) {
2321 Employee.findOne(function(err, employee) {
2322 should.not.exists(err);
2323 employee.mugshot(function(err, pic) {
2324 if (err) return done(err);
2325 pic.name.should.equal('Mugshot');
2326 pic.oid.toString().should.equal(employee.username.toString());
2327 pic.type.should.equal('Employee');
2328 done();
2329 });
2330 });
2331 });
2332
2333 it('should find inverse polymorphic relation - article', function(done) {
2334 Picture.findOne({where: {name: 'Packshot'}}, function(err, pic) {
2335 should.not.exists(err);
2336 pic.imageable(function(err, imageable) {
2337 if (err) return done(err);
2338 imageable.should.be.instanceof(Article);
2339 imageable.name.should.equal('Article 1');
2340 done();
2341 });
2342 });
2343 });
2344
2345 it('should find inverse polymorphic relation - employee', function(done) {
2346 Picture.findOne({where: {name: 'Mugshot'}}, function(err, p) {
2347 should.not.exists(err);
2348 p.imageable(function(err, imageable) {
2349 if (err) return done(err);
2350 imageable.should.be.instanceof(Employee);
2351 imageable.name.should.equal('Employee 1');
2352 done();
2353 });
2354 });
2355 });
2356
2357 it('should include polymorphic relation - employee', function(done) {
2358 Employee.findOne({include: 'mugshot'},
2359 function(err, employee) {
2360 should.not.exists(err);
2361 const mugshot = employee.mugshot();
2362 should.exist(mugshot);
2363 mugshot.name.should.equal('Mugshot');
2364 done();
2365 });
2366 });
2367
2368 it('should include inverse polymorphic relation - employee', function(done) {
2369 Picture.findOne({where: {name: 'Mugshot'}, include: 'imageable'},
2370 function(err, pic) {
2371 should.not.exists(err);
2372 const imageable = pic.imageable();
2373 should.exist(imageable);
2374 imageable.should.be.instanceof(Employee);
2375 imageable.name.should.equal('Employee 1');
2376 done();
2377 });
2378 });
2379 });
2380
2381 describe('polymorphic hasMany', function() {
2382 before(function(done) {
2383 Picture = db.define('Picture', {name: String});
2384 Article = db.define('Article', {name: String});
2385 Employee = db.define('Employee', {name: String});
2386
2387 db.automigrate(['Picture', 'Article', 'Employee'], done);
2388 });
2389
2390 it('can be declared with model JSON definition when related model is already attached', function(done) {
2391 const ds = new DataSource('memory');
2392
2393 // by defining Picture model before Article model we make sure Picture IS
2394 // already attached when defining Article. This way, datasource.defineRelations
2395 // WILL NOT use the async listener to call hasMany relation method
2396 const Picture = ds.define('Picture', {name: String}, {relations: {
2397 imageable: {type: 'belongsTo', polymorphic: true},
2398 }});
2399 const Article = ds.define('Article', {name: String}, {relations: {
2400 pictures: {type: 'hasMany', model: 'Picture', polymorphic: 'imageable'},
2401 }});
2402
2403 assert(Article.relations['pictures']);
2404 assert.deepEqual(Article.relations['pictures'].toJSON(), {
2405 name: 'pictures',
2406 type: 'hasMany',
2407 modelFrom: 'Article',
2408 keyFrom: 'id',
2409 modelTo: 'Picture',
2410 keyTo: 'imageableId',
2411 multiple: true,
2412 polymorphic: {
2413 selector: 'imageable',
2414 foreignKey: 'imageableId',
2415 discriminator: 'imageableType',
2416 },
2417 });
2418
2419 assert(Picture.relations['imageable']);
2420 assert.deepEqual(Picture.relations['imageable'].toJSON(), {
2421 name: 'imageable',
2422 type: 'belongsTo',
2423 modelFrom: 'Picture',
2424 keyFrom: 'imageableId',
2425 modelTo: '<polymorphic>',
2426 keyTo: 'id',
2427 multiple: false,
2428 polymorphic: {
2429 selector: 'imageable',
2430 foreignKey: 'imageableId',
2431 discriminator: 'imageableType',
2432 },
2433 });
2434 done();
2435 });
2436
2437 it('can be declared with model JSON definition when related model is not yet attached', function(done) {
2438 const ds = new DataSource('memory');
2439
2440 // by defining Author model before Picture model we make sure Picture IS NOT
2441 // already attached when defining Author. This way, datasource.defineRelations
2442 // WILL use the async listener to call hasMany relation method
2443 const Author = ds.define('Author', {name: String}, {relations: {
2444 pictures: {type: 'hasMany', model: 'Picture', polymorphic: 'imageable'},
2445 }});
2446 const Picture = ds.define('Picture', {name: String}, {relations: {
2447 imageable: {type: 'belongsTo', polymorphic: true},
2448 }});
2449
2450 assert(Author.relations['pictures']);
2451 assert.deepEqual(Author.relations['pictures'].toJSON(), {
2452 name: 'pictures',
2453 type: 'hasMany',
2454 modelFrom: 'Author',
2455 keyFrom: 'id',
2456 modelTo: 'Picture',
2457 keyTo: 'imageableId',
2458 multiple: true,
2459 polymorphic: {
2460 selector: 'imageable',
2461 foreignKey: 'imageableId',
2462 discriminator: 'imageableType',
2463 },
2464 });
2465
2466 assert(Picture.relations['imageable']);
2467 assert.deepEqual(Picture.relations['imageable'].toJSON(), {
2468 name: 'imageable',
2469 type: 'belongsTo',
2470 modelFrom: 'Picture',
2471 keyFrom: 'imageableId',
2472 modelTo: '<polymorphic>',
2473 keyTo: 'id',
2474 multiple: false,
2475 polymorphic: {
2476 selector: 'imageable',
2477 foreignKey: 'imageableId',
2478 discriminator: 'imageableType',
2479 },
2480 });
2481 done();
2482 });
2483
2484 it('can be declared using default polymorphic selector', function(done) {
2485 Article.hasMany(Picture, {polymorphic: 'imageable'});
2486 Employee.hasMany(Picture, {polymorphic: { // alt syntax
2487 foreignKey: 'imageableId',
2488 discriminator: 'imageableType',
2489 }});
2490 Picture.belongsTo('imageable', {polymorphic: true});
2491
2492 Article.relations['pictures'].toJSON().should.eql({
2493 name: 'pictures',
2494 type: 'hasMany',
2495 modelFrom: 'Article',
2496 keyFrom: 'id',
2497 modelTo: 'Picture',
2498 keyTo: 'imageableId',
2499 multiple: true,
2500 polymorphic: {
2501 selector: 'imageable',
2502 foreignKey: 'imageableId',
2503 discriminator: 'imageableType',
2504 },
2505 });
2506
2507 Picture.relations['imageable'].toJSON().should.eql({
2508 name: 'imageable',
2509 type: 'belongsTo',
2510 modelFrom: 'Picture',
2511 keyFrom: 'imageableId',
2512 modelTo: '<polymorphic>',
2513 keyTo: 'id',
2514 multiple: false,
2515 polymorphic: {
2516 selector: 'imageable',
2517 foreignKey: 'imageableId',
2518 discriminator: 'imageableType',
2519 },
2520 });
2521
2522 db.automigrate(['Picture', 'Article', 'Employee'], done);
2523 });
2524
2525 it('should create polymorphic relation - article', function(done) {
2526 Article.create({name: 'Article 1'}, function(err, article) {
2527 should.not.exists(err);
2528 article.pictures.create({name: 'Article Pic'}, function(err, pics) {
2529 if (err) return done(err);
2530 should.exist(pics);
2531 pics.imageableId.should.eql(article.id);
2532 pics.imageableType.should.equal('Article');
2533 done();
2534 });
2535 });
2536 });
2537
2538 it('should create polymorphic relation - employee', function(done) {
2539 Employee.create({name: 'Employee 1'}, function(err, employee) {
2540 should.not.exists(err);
2541 employee.pictures.create({name: 'Employee Pic'}, function(err, pics) {
2542 if (err) return done(err);
2543 should.exist(pics);
2544 pics.imageableId.should.eql(employee.id);
2545 pics.imageableType.should.equal('Employee');
2546 done();
2547 });
2548 });
2549 });
2550
2551 it('should find polymorphic items - article', function(done) {
2552 Article.findOne(function(err, article) {
2553 should.not.exists(err);
2554 if (!article) return done();
2555 article.pictures(function(err, pics) {
2556 if (err) return done(err);
2557
2558 const pictures = article.pictures();
2559 pictures.should.eql(pics);
2560
2561 pics.should.have.length(1);
2562 pics[0].name.should.equal('Article Pic');
2563 done();
2564 });
2565 });
2566 });
2567
2568 it('should find polymorphic items - employee', function(done) {
2569 Employee.findOne(function(err, employee) {
2570 should.not.exists(err);
2571 employee.pictures(function(err, pics) {
2572 if (err) return done(err);
2573 pics.should.have.length(1);
2574 pics[0].name.should.equal('Employee Pic');
2575 done();
2576 });
2577 });
2578 });
2579
2580 it('should find the inverse of polymorphic relation - article', function(done) {
2581 Picture.findOne({where: {name: 'Article Pic'}}, function(err, pics) {
2582 if (err) return done(err);
2583 pics.imageableType.should.equal('Article');
2584 pics.imageable(function(err, imageable) {
2585 if (err) return done(err);
2586 imageable.should.be.instanceof(Article);
2587 imageable.name.should.equal('Article 1');
2588 done();
2589 });
2590 });
2591 });
2592
2593 it('should find the inverse of polymorphic relation - employee', function(done) {
2594 Picture.findOne({where: {name: 'Employee Pic'}}, function(err, pics) {
2595 if (err) return done(err);
2596 pics.imageableType.should.equal('Employee');
2597 pics.imageable(function(err, imageable) {
2598 if (err) return done(err);
2599 imageable.should.be.instanceof(Employee);
2600 imageable.name.should.equal('Employee 1');
2601 done();
2602 });
2603 });
2604 });
2605
2606 bdd.itIf(connectorCapabilities.adhocSort !== false,
2607 'should include the inverse of polymorphic relation', function(done) {
2608 Picture.find({include: 'imageable'}, function(err, pics) {
2609 if (err) return done(err);
2610 pics.should.have.length(2);
2611
2612 const actual = pics.map(
2613 function(pic) {
2614 return {imageName: pic.name, name: pic.imageable().name};
2615 },
2616 );
2617
2618 actual.should.containDeep([
2619 {name: 'Article 1', imageName: 'Article Pic'},
2620 {name: 'Employee 1', imageName: 'Employee Pic'},
2621 ]);
2622
2623 done();
2624 });
2625 });
2626
2627 bdd.itIf(connectorCapabilities.adhocSort === false,
2628 'should include the inverse of polymorphic relation w/o adhocSort', function(done) {
2629 Picture.find({include: 'imageable'}, function(err, pics) {
2630 if (err) return done(err);
2631 pics.should.have.length(2);
2632 const names = ['Article Pic', 'Employee Pic'];
2633 const imageables = ['Article 1', 'Employee 1'];
2634 names.should.containEql(pics[0].name);
2635 names.should.containEql(pics[1].name);
2636 imageables.should.containEql(pics[0].imageable().name);
2637 imageables.should.containEql(pics[1].imageable().name);
2638 done();
2639 });
2640 });
2641
2642 it('should assign a polymorphic relation', function(done) {
2643 Article.create({name: 'Article 2'}, function(err, article) {
2644 should.not.exists(err);
2645 const p = new Picture({name: 'Sample'});
2646 p.imageable(article); // assign
2647 p.imageableId.should.eql(article.id);
2648 p.imageableType.should.equal('Article');
2649 p.save(done);
2650 });
2651 });
2652
2653 // eslint-disable-next-line mocha/no-identical-title
2654 it('should find polymorphic items - article', function(done) {
2655 Article.findOne({where: {name: 'Article 2'}}, function(err, article) {
2656 should.not.exists(err);
2657 article.pictures(function(err, pics) {
2658 if (err) return done(err);
2659 pics.should.have.length(1);
2660 pics[0].name.should.equal('Sample');
2661 done();
2662 });
2663 });
2664 });
2665
2666 // eslint-disable-next-line mocha/no-identical-title
2667 it('should find the inverse of polymorphic relation - article', function(done) {
2668 Picture.findOne({where: {name: 'Sample'}}, function(err, p) {
2669 if (err) return done(err);
2670 p.imageableType.should.equal('Article');
2671 p.imageable(function(err, imageable) {
2672 if (err) return done(err);
2673 imageable.should.be.instanceof(Article);
2674 imageable.name.should.equal('Article 2');
2675 done();
2676 });
2677 });
2678 });
2679
2680 it('should include the inverse of polymorphic relation - article',
2681 function(done) {
2682 Picture.findOne({where: {name: 'Sample'}, include: 'imageable'},
2683 function(err, p) {
2684 if (err) return done(err);
2685 const imageable = p.imageable();
2686 should.exist(imageable);
2687 imageable.should.be.instanceof(Article);
2688 imageable.name.should.equal('Article 2');
2689 done();
2690 });
2691 });
2692
2693 it('can be declared using custom foreignKey/discriminator', function(done) {
2694 Article.hasMany(Picture, {polymorphic: {
2695 foreignKey: 'imageId',
2696 discriminator: 'imageType',
2697 }});
2698 Employee.hasMany(Picture, {polymorphic: { // alt syntax
2699 foreignKey: 'imageId',
2700 discriminator: 'imageType',
2701 }});
2702 Picture.belongsTo('imageable', {polymorphic: {
2703 foreignKey: 'imageId',
2704 discriminator: 'imageType',
2705 }});
2706
2707 Article.relations['pictures'].toJSON().should.eql({
2708 name: 'pictures',
2709 type: 'hasMany',
2710 modelFrom: 'Article',
2711 keyFrom: 'id',
2712 modelTo: 'Picture',
2713 keyTo: 'imageId',
2714 multiple: true,
2715 polymorphic: {
2716 selector: 'pictures',
2717 foreignKey: 'imageId',
2718 discriminator: 'imageType',
2719 },
2720 });
2721
2722 Picture.relations['imageable'].toJSON().should.eql({
2723 name: 'imageable',
2724 type: 'belongsTo',
2725 modelFrom: 'Picture',
2726 keyFrom: 'imageId',
2727 modelTo: '<polymorphic>',
2728 keyTo: 'id',
2729 multiple: false,
2730 polymorphic: {
2731 selector: 'imageable',
2732 foreignKey: 'imageId',
2733 discriminator: 'imageType',
2734 },
2735 });
2736
2737 db.automigrate(['Picture', 'Article', 'Employee'], done);
2738 });
2739 });
2740
2741 describe('polymorphic hasAndBelongsToMany through', function() {
2742 let idArticle, idEmployee;
2743
2744 before(function(done) {
2745 idArticle = uid.fromConnector(db) || 3456;
2746 idEmployee = uid.fromConnector(db) || 4567;
2747 Picture = db.define('Picture', {name: String});
2748 Article = db.define('Article', {name: String});
2749 Employee = db.define('Employee', {name: String});
2750 PictureLink = db.define('PictureLink', {});
2751
2752 db.automigrate(['Picture', 'Article', 'Employee', 'PictureLink'], done);
2753 });
2754
2755 it('can be declared using default polymorphic selector', function(done) {
2756 Article.hasAndBelongsToMany(Picture, {through: PictureLink, polymorphic: 'imageable'});
2757 Employee.hasAndBelongsToMany(Picture, {through: PictureLink, polymorphic: 'imageable'});
2758 // Optionally, define inverse relations:
2759 Picture.hasMany(Article, {through: PictureLink, polymorphic: 'imageable', invert: true});
2760 Picture.hasMany(Employee, {through: PictureLink, polymorphic: 'imageable', invert: true});
2761 db.automigrate(['Picture', 'Article', 'Employee', 'PictureLink'], done);
2762 });
2763
2764 it('can determine the collect via modelTo name', function() {
2765 Article.hasAndBelongsToMany(Picture, {through: PictureLink, polymorphic: 'imageable'});
2766 Employee.hasAndBelongsToMany(Picture, {through: PictureLink, polymorphic: 'imageable'});
2767 // Optionally, define inverse relations:
2768 Picture.hasMany(Article, {through: PictureLink, polymorphic: 'imageable', invert: true});
2769 Picture.hasMany(Employee, {through: PictureLink, polymorphic: 'imageable', invert: true});
2770 const article = new Article({id: idArticle});
2771 const scope1 = article.pictures._scope;
2772 scope1.should.have.property('collect', 'picture');
2773 scope1.should.have.property('include', 'picture');
2774 const employee = new Employee({id: idEmployee});
2775 const scope2 = employee.pictures._scope;
2776 scope2.should.have.property('collect', 'picture');
2777 scope2.should.have.property('include', 'picture');
2778 const picture = new Picture({id: idArticle});
2779 const scope3 = picture.articles._scope;
2780 scope3.should.have.property('collect', 'imageable');
2781 scope3.should.have.property('include', 'imageable');
2782 const scope4 = picture.employees._scope;
2783 scope4.should.have.property('collect', 'imageable');
2784 scope4.should.have.property('include', 'imageable');
2785 });
2786
2787 let article, employee;
2788 const pictures = [];
2789 it('should create polymorphic relation - article', function(done) {
2790 Article.create({name: 'Article 1'}, function(err, a) {
2791 if (err) return done(err);
2792 article = a;
2793 article.pictures.create({name: 'Article Pic 1'}, function(err, pic) {
2794 if (err) return done(err);
2795 pictures.push(pic);
2796 article.pictures.create({name: 'Article Pic 2'}, function(err, pic) {
2797 if (err) return done(err);
2798 pictures.push(pic);
2799 done();
2800 });
2801 });
2802 });
2803 });
2804
2805 it('should create polymorphic relation - employee', function(done) {
2806 Employee.create({name: 'Employee 1'}, function(err, r) {
2807 if (err) return done(err);
2808 employee = r;
2809 employee.pictures.create({name: 'Employee Pic 1'}, function(err, pic) {
2810 if (err) return done(err);
2811 pictures.push(pic);
2812 done();
2813 });
2814 });
2815 });
2816
2817 it('should create polymorphic through model', function(done) {
2818 PictureLink.findOne(function(err, link) {
2819 if (err) return done(err);
2820 if (connectorCapabilities.adhocSort !== false) {
2821 link.pictureId.should.eql(pictures[0].id);
2822 link.imageableId.should.eql(article.id);
2823 link.imageableType.should.equal('Article');
2824 link.imageable(function(err, imageable) {
2825 imageable.should.be.instanceof(Article);
2826 imageable.id.should.eql(article.id);
2827 done();
2828 });
2829 } else {
2830 const picIds = pictures.map(pic => pic.id.toString());
2831 picIds.should.containEql(link.pictureId.toString());
2832 link.imageableType.should.be.oneOf('Article', 'Employee');
2833 link.imageable(function(err, imageable) {
2834 imageable.id.should.be.oneOf(article.id, employee.id);
2835 done();
2836 });
2837 }
2838 });
2839 });
2840
2841 it('should get polymorphic relation through model - article', function(done) {
2842 if (!article) return done();
2843 Article.findById(article.id, function(err, article) {
2844 if (err) return done(err);
2845 article.name.should.equal('Article 1');
2846 article.pictures(function(err, pics) {
2847 if (err) return done(err);
2848 pics.should.have.length(2);
2849 const names = pics.map(p => p.name);
2850 const expected = ['Article Pic 1', 'Article Pic 2'];
2851 if (connectorCapabilities.adhocSort !== false) {
2852 names.should.eql(expected);
2853 } else {
2854 names.should.containDeep(expected);
2855 }
2856 done();
2857 });
2858 });
2859 });
2860
2861 it('should get polymorphic relation through model - employee', function(done) {
2862 Employee.findById(employee.id, function(err, employee) {
2863 if (err) return done(err);
2864 employee.name.should.equal('Employee 1');
2865 employee.pictures(function(err, pics) {
2866 if (err) return done(err);
2867 pics.should.have.length(1);
2868 pics[0].name.should.equal('Employee Pic 1');
2869 done();
2870 });
2871 });
2872 });
2873
2874 it('should include polymorphic items', function(done) {
2875 Article.find({include: 'pictures'}, function(err, articles) {
2876 articles.should.have.length(1);
2877 if (!articles) return done();
2878 articles[0].pictures(function(err, pics) {
2879 pics.should.have.length(2);
2880 const names = pics.map(p => p.name);
2881 const expected = ['Article Pic 1', 'Article Pic 2'];
2882 if (connectorCapabilities.adhocSort !== false) {
2883 names.should.eql(expected);
2884 } else {
2885 names.should.containDeep(expected);
2886 }
2887 done();
2888 });
2889 });
2890 });
2891
2892 let anotherPicture;
2893 it('should add to a polymorphic relation - article', function(done) {
2894 if (!article) return done();
2895 Article.findById(article.id, function(err, article) {
2896 Picture.create({name: 'Example'}, function(err, pic) {
2897 if (err) return done(err);
2898 pictures.push(pic);
2899 anotherPicture = pic;
2900 article.pictures.add(pic, function(err, link) {
2901 link.should.be.instanceof(PictureLink);
2902 link.pictureId.should.eql(pic.id);
2903 link.imageableId.should.eql(article.id);
2904 link.imageableType.should.equal('Article');
2905 done();
2906 });
2907 });
2908 });
2909 });
2910
2911 // eslint-disable-next-line mocha/no-identical-title
2912 it('should create polymorphic through model', function(done) {
2913 if (!anotherPicture) return done();
2914 PictureLink.findOne({where: {pictureId: anotherPicture.id, imageableType: 'Article'}},
2915 function(err, link) {
2916 if (err) return done(err);
2917 link.pictureId.toString().should.eql(anotherPicture.id.toString());
2918 link.imageableId.toString().should.eql(article.id.toString());
2919 link.imageableType.should.equal('Article');
2920 done();
2921 });
2922 });
2923
2924 let anotherArticle, anotherEmployee;
2925 // eslint-disable-next-line mocha/no-identical-title
2926 it('should add to a polymorphic relation - article', function(done) {
2927 Article.create({name: 'Article 2'}, function(err, article) {
2928 if (err) return done(err);
2929 anotherArticle = article;
2930 if (!anotherPicture) return done();
2931 article.pictures.add(anotherPicture.id, function(err, pic) {
2932 if (err) return done(err);
2933 done();
2934 });
2935 });
2936 });
2937
2938 // eslint-disable-next-line mocha/no-identical-title
2939 it('should add to a polymorphic relation - article', function(done) {
2940 Employee.create({name: 'Employee 2'}, function(err, reader) {
2941 if (err) return done(err);
2942 anotherEmployee = reader;
2943 if (!anotherPicture) return done();
2944 reader.pictures.add(anotherPicture.id, function(err, pic) {
2945 if (err) return done(err);
2946 done();
2947 });
2948 });
2949 });
2950
2951 it('should get the inverse polymorphic relation - article', function(done) {
2952 if (!anotherPicture) return done();
2953 Picture.findById(anotherPicture.id, function(err, pic) {
2954 pic.articles(function(err, articles) {
2955 articles.should.have.length(2);
2956 const names = articles.map(pic => pic.name);
2957 const expected = ['Article 1', 'Article 2'];
2958 if (connectorCapabilities.adhocSort !== false) {
2959 names.should.eql(expected);
2960 } else {
2961 names.should.containDeep(expected);
2962 }
2963 done();
2964 });
2965 });
2966 });
2967
2968 it('should get the inverse polymorphic relation - reader', function(done) {
2969 if (!anotherPicture) return done();
2970 Picture.findById(anotherPicture.id, function(err, pic) {
2971 pic.employees(function(err, employees) {
2972 employees.should.have.length(1);
2973 if (connectorCapabilities.adhocSort !== false) {
2974 employees[0].name.should.equal('Employee 2');
2975 } else {
2976 const employeeNames = ['Employee 1', 'Employee 2'];
2977 employees[0].name.should.be.oneOf(employeeNames);
2978 }
2979 done();
2980 });
2981 });
2982 });
2983
2984 it('should find polymorphic items - article', function(done) {
2985 if (!article) return done();
2986 Article.findById(article.id, function(err, article) {
2987 article.pictures(function(err, pics) {
2988 pics.should.have.length(3);
2989 const names = pics.map(pic => pic.name);
2990 const expected = ['Article Pic 1', 'Article Pic 2', 'Example'];
2991 if (connectorCapabilities.adhocSort !== false) {
2992 names.should.eql(expected);
2993 } else {
2994 names.should.containDeep(expected);
2995 }
2996 done();
2997 });
2998 });
2999 });
3000
3001 it('should check if polymorphic relation exists - article', function(done) {
3002 if (!article) return done();
3003 Article.findById(article.id, function(err, article) {
3004 article.pictures.exists(anotherPicture.id, function(err, exists) {
3005 exists.should.be.true;
3006 done();
3007 });
3008 });
3009 });
3010
3011 bdd.itIf(connectorCapabilities.deleteWithOtherThanId !== false,
3012 'should remove from a polymorphic relation - article', function(done) {
3013 if (!article || !anotherPicture) return done();
3014 Article.findById(article.id, function(err, article) {
3015 article.pictures.remove(anotherPicture.id, function(err) {
3016 if (err) return done(err);
3017 done();
3018 });
3019 });
3020 });
3021
3022 bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
3023 'should find polymorphic items - article', function(done) {
3024 if (!article) return done();
3025 Article.findById(article.id, function(err, article) {
3026 article.pictures(function(err, pics) {
3027 // If deleteWithOtherThanId is not implemented, the above test is skipped and
3028 // the remove did not take place. Thus +1.
3029 const expectedLength = connectorCapabilities.deleteWithOtherThanId !== false ?
3030 2 : 3;
3031 pics.should.have.length(expectedLength);
3032
3033 const names = pics.map(p => p.name);
3034 if (connectorCapabilities.adhocSort !== false) {
3035 names.should.eql(['Article Pic 1', 'Article Pic 2']);
3036 } else {
3037 names.should.containDeep(['Article Pic 1', 'Article Pic 2', 'Example']);
3038 }
3039 done();
3040 });
3041 });
3042 });
3043
3044 // eslint-disable-next-line mocha/no-identical-title
3045 it('should check if polymorphic relation exists - article', function(done) {
3046 if (!article) return done();
3047 Article.findById(article.id, function(err, article) {
3048 article.pictures.exists(7, function(err, exists) {
3049 exists.should.be.false;
3050 done();
3051 });
3052 });
3053 });
3054
3055 it('should create polymorphic item through relation scope', function(done) {
3056 if (!anotherPicture) return done();
3057 Picture.findById(anotherPicture.id, function(err, pic) {
3058 pic.articles.create({name: 'Article 3'}, function(err, prd) {
3059 if (err) return done(err);
3060 article = prd;
3061 should.equal(article.name, 'Article 3');
3062 done();
3063 });
3064 });
3065 });
3066
3067 it('should create polymorphic through model - new article', function(done) {
3068 if (!article || !anotherPicture) return done();
3069 PictureLink.findOne({where: {
3070 pictureId: anotherPicture.id, imageableId: article.id, imageableType: 'Article',
3071 }}, function(err, link) {
3072 if (err) return done(err);
3073 link.pictureId.toString().should.eql(anotherPicture.id.toString());
3074 link.imageableId.toString().should.eql(article.id.toString());
3075 link.imageableType.should.equal('Article');
3076 done();
3077 });
3078 });
3079
3080 it('should find polymorphic items - new article', function(done) {
3081 if (!article) return done();
3082 Article.findById(article.id, function(err, article) {
3083 article.pictures(function(err, pics) {
3084 pics.should.have.length(1);
3085 pics[0].id.should.eql(anotherPicture.id);
3086 pics[0].name.should.equal('Example');
3087 done();
3088 });
3089 });
3090 });
3091
3092 it('should use author_pictures as modelThrough', function(done) {
3093 Article.hasAndBelongsToMany(Picture, {throughTable: 'article_pictures'});
3094 Article.relations['pictures'].toJSON().should.eql({
3095 name: 'pictures',
3096 type: 'hasMany',
3097 modelFrom: 'Article',
3098 keyFrom: 'id',
3099 modelTo: 'Picture',
3100 keyTo: 'articleId',
3101 multiple: true,
3102 modelThrough: 'article_pictures',
3103 keyThrough: 'pictureId',
3104 });
3105 done();
3106 });
3107
3108 it('can be declared using custom foreignKey/discriminator', function(done) {
3109 Article.hasAndBelongsToMany(Picture, {through: PictureLink, polymorphic: {
3110 foreignKey: 'imageId',
3111 discriminator: 'imageType',
3112 }});
3113 Employee.hasAndBelongsToMany(Picture, {through: PictureLink, polymorphic: {
3114 foreignKey: 'imageId',
3115 discriminator: 'imageType',
3116 }});
3117 // Optionally, define inverse relations:
3118 Picture.hasMany(Article, {through: PictureLink, polymorphic: {
3119 foreignKey: 'imageId',
3120 discriminator: 'imageType',
3121 }, invert: true});
3122 Picture.hasMany(Employee, {through: PictureLink, polymorphic: {
3123 foreignKey: 'imageId',
3124 discriminator: 'imageType',
3125 }, invert: true});
3126
3127 Article.relations['pictures'].toJSON().should.eql({
3128 name: 'pictures',
3129 type: 'hasMany',
3130 modelFrom: 'Article',
3131 keyFrom: 'id',
3132 modelTo: 'Picture',
3133 keyTo: 'imageId',
3134 multiple: true,
3135 modelThrough: 'PictureLink',
3136 keyThrough: 'pictureId',
3137 polymorphic: {
3138 selector: 'pictures',
3139 foreignKey: 'imageId',
3140 discriminator: 'imageType',
3141 },
3142 });
3143
3144 Picture.relations['articles'].toJSON().should.eql({
3145 name: 'articles',
3146 type: 'hasMany',
3147 modelFrom: 'Picture',
3148 keyFrom: 'id',
3149 modelTo: 'Article',
3150 keyTo: 'pictureId',
3151 multiple: true,
3152 modelThrough: 'PictureLink',
3153 keyThrough: 'imageId',
3154 polymorphic: {
3155 foreignKey: 'imageId',
3156 discriminator: 'imageType',
3157 selector: 'articles',
3158 invert: true,
3159 },
3160 });
3161
3162 db.automigrate(['Picture', 'Article', 'Employee', 'PictureLink'], done);
3163 });
3164 });
3165
3166 describe('belongsTo', function() {
3167 let List, Item, Fear, Mind;
3168
3169 let listId, itemId;
3170
3171 it('can be declared in different ways', function() {
3172 List = db.define('List', {name: String});
3173 Item = db.define('Item', {name: String});
3174 Fear = db.define('Fear');
3175 Mind = db.define('Mind');
3176
3177 // syntax 1 (old)
3178 Item.belongsTo(List);
3179 Object.keys((new Item).toObject()).should.containEql('listId');
3180 (new Item).list.should.be.an.instanceOf(Function);
3181
3182 // syntax 2 (new)
3183 Fear.belongsTo('mind', {
3184 methods: {check: function() { return true; }},
3185 });
3186
3187 Object.keys((new Fear).toObject()).should.containEql('mindId');
3188 (new Fear).mind.should.be.an.instanceOf(Function);
3189 // (new Fear).mind.build().should.be.an.instanceOf(Mind);
3190 });
3191
3192 it('should setup a custom method on accessor', function() {
3193 const rel = Fear.relations['mind'];
3194 rel.defineMethod('other', function() {
3195 return true;
3196 });
3197 });
3198
3199 it('should have setup a custom method on accessor', function() {
3200 const f = new Fear();
3201 f.mind.check.should.be.a.function;
3202 f.mind.check().should.be.true;
3203 f.mind.other.should.be.a.function;
3204 f.mind.other().should.be.true;
3205 });
3206
3207 it('can be used to query data', function(done) {
3208 List.hasMany('todos', {model: Item});
3209 db.automigrate(['List', 'Item', 'Fear', 'Mind'], function() {
3210 List.create({name: 'List 1'}, function(e, list) {
3211 listId = list.id;
3212 should.not.exist(e);
3213 should.exist(list);
3214 list.todos.create({name: 'Item 1'}, function(err, todo) {
3215 itemId = todo.id;
3216 todo.list(function(e, l) {
3217 should.not.exist(e);
3218 should.exist(l);
3219 l.should.be.an.instanceOf(List);
3220 todo.list().id.should.eql(l.id);
3221 todo.list().name.should.equal('List 1');
3222 done();
3223 });
3224 });
3225 });
3226 });
3227 });
3228
3229 it('can be used to query data with get() with callback', function(done) {
3230 List.hasMany('todos', {model: Item});
3231 db.automigrate(['List', 'Item', 'Fear', 'Find'], function() {
3232 List.create({name: 'List 1'}, function(e, list) {
3233 listId = list.id;
3234 should.not.exist(e);
3235 should.exist(list);
3236 list.todos.create({name: 'Item 1'}, function(err, todo) {
3237 itemId = todo.id;
3238 todo.list.get(function(e, l) {
3239 should.not.exist(e);
3240 should.exist(l);
3241 l.should.be.an.instanceOf(List);
3242 todo.list().id.should.eql(l.id);
3243 todo.list().name.should.equal('List 1');
3244 done();
3245 });
3246 });
3247 });
3248 });
3249 });
3250
3251 it('can be used to query data with promises', function(done) {
3252 List.hasMany('todos', {model: Item});
3253 db.automigrate(['List', 'Item', 'Fear', 'Find'], function() {
3254 List.create({name: 'List 1'})
3255 .then(function(list) {
3256 listId = list.id;
3257 should.exist(list);
3258 return list.todos.create({name: 'Item 1'});
3259 })
3260 .then(function(todo) {
3261 itemId = todo.id;
3262 return todo.list.get()
3263 .then(function(l) {
3264 should.exist(l);
3265 l.should.be.an.instanceOf(List);
3266 todo.list().id.should.eql(l.id);
3267 todo.list().name.should.equal('List 1');
3268 done();
3269 });
3270 })
3271 .catch(done);
3272 });
3273 });
3274
3275 it('could accept objects when creating on scope', function(done) {
3276 List.create(function(e, list) {
3277 should.not.exist(e);
3278 should.exist(list);
3279 Item.create({list: list}, function(err, item) {
3280 if (err) return done(err);
3281 should.exist(item);
3282 should.exist(item.listId);
3283 item.listId.should.eql(list.id);
3284 item.__cachedRelations.list.should.equal(list);
3285 done();
3286 });
3287 });
3288 });
3289
3290 it('should update related item on scope', function(done) {
3291 Item.findById(itemId, function(e, todo) {
3292 todo.list.update({name: 'List A'}, function(err, list) {
3293 if (err) return done(err);
3294 should.exist(list);
3295 list.name.should.equal('List A');
3296 done();
3297 });
3298 });
3299 });
3300
3301 it('should not update related item FK on scope', function(done) {
3302 Item.findById(itemId, function(e, todo) {
3303 if (e) return done(e);
3304 todo.list.update({id: 10}, function(err, list) {
3305 should.exist(err);
3306 err.message.should.startWith('Cannot override foreign key');
3307 done();
3308 });
3309 });
3310 });
3311
3312 it('should get related item on scope', function(done) {
3313 Item.findById(itemId, function(e, todo) {
3314 todo.list(function(err, list) {
3315 if (err) return done(err);
3316 should.exist(list);
3317 list.name.should.equal('List A');
3318 done();
3319 });
3320 });
3321 });
3322
3323 it('should destroy related item on scope', function(done) {
3324 Item.findById(itemId, function(e, todo) {
3325 todo.list.destroy(function(err) {
3326 if (err) return done(err);
3327 done();
3328 });
3329 });
3330 });
3331
3332 it('should get related item on scope - verify', function(done) {
3333 Item.findById(itemId, function(e, todo) {
3334 todo.list(function(err, list) {
3335 if (err) return done(err);
3336 should.not.exist(list);
3337 done();
3338 });
3339 });
3340 });
3341
3342 it('should not have deleted related item', function(done) {
3343 List.findById(listId, function(e, list) {
3344 should.not.exist(e);
3345 should.exist(list);
3346 done();
3347 });
3348 });
3349
3350 it('should allow to create belongsTo model in beforeCreate hook', function(done) {
3351 let mind;
3352 Fear.beforeCreate = function(next) {
3353 this.mind.create(function(err, m) {
3354 mind = m;
3355 if (err) next(err); else next();
3356 });
3357 };
3358 Fear.create(function(err, fear) {
3359 should.not.exists(err);
3360 should.exists(fear);
3361 fear.mindId.should.eql(mind.id);
3362 should.exists(fear.mind());
3363 done();
3364 });
3365 });
3366
3367 it('should allow to create belongsTo model in beforeCreate hook with promises', function(done) {
3368 let mind;
3369 Fear.beforeCreate = function(next) {
3370 this.mind.create()
3371 .then(function(m) {
3372 mind = m;
3373 next();
3374 }).catch(next);
3375 };
3376 Fear.create()
3377 .then(function(fear) {
3378 should.exists(fear);
3379 fear.mindId.should.eql(mind.id);
3380 should.exists(fear.mind());
3381 done();
3382 }).catch(done);
3383 });
3384 });
3385
3386 describe('belongsTo with scope', function() {
3387 let Person, Passport;
3388
3389 it('can be declared with scope and properties', function(done) {
3390 Person = db.define('Person', {name: String, age: Number, passportNotes: String});
3391 Passport = db.define('Passport', {name: String, notes: String});
3392 Passport.belongsTo(Person, {
3393 properties: {notes: 'passportNotes'},
3394 scope: {fields: {id: true, name: true}},
3395 });
3396 db.automigrate(['Person', 'Passport'], done);
3397 });
3398
3399 let personCreated;
3400 it('should create record on scope', function(done) {
3401 const p = new Passport({name: 'Passport', notes: 'Some notes...'});
3402 p.person.create({name: 'Fred', age: 36}, function(err, person) {
3403 personCreated = person;
3404 p.personId.toString().should.eql(person.id.toString());
3405 person.name.should.equal('Fred');
3406 person.passportNotes.should.equal('Some notes...');
3407 p.save(function(err, passport) {
3408 should.not.exists(err);
3409 done();
3410 });
3411 });
3412 });
3413
3414 it('should find record on scope', function(done) {
3415 Passport.findOne(function(err, p) {
3416 p.personId.toString().should.eql(personCreated.id.toString());
3417 p.person(function(err, person) {
3418 person.name.should.equal('Fred');
3419 person.should.have.property('age', undefined);
3420 person.should.have.property('passportNotes', undefined);
3421 done();
3422 });
3423 });
3424 });
3425
3426 it('should create record on scope with promises', function(done) {
3427 const p = new Passport({name: 'Passport', notes: 'Some notes...'});
3428 p.person.create({name: 'Fred', age: 36})
3429 .then(function(person) {
3430 p.personId.should.eql(person.id);
3431 person.name.should.equal('Fred');
3432 person.passportNotes.should.equal('Some notes...');
3433 return p.save();
3434 })
3435 .then(function(passport) {
3436 done();
3437 })
3438 .catch(done);
3439 });
3440
3441 it('should find record on scope with promises', function(done) {
3442 Passport.findOne()
3443 .then(function(p) {
3444 if (connectorCapabilities.adhocSort !== false) {
3445 // We skip the check if adhocSort is not supported because
3446 // the first row returned may or may not be the same
3447 p.personId.should.eql(personCreated.id);
3448 }
3449 return p.person.get();
3450 })
3451 .then(function(person) {
3452 person.name.should.equal('Fred');
3453 person.should.have.property('age', undefined);
3454 person.should.have.property('passportNotes', undefined);
3455 done();
3456 })
3457 .catch(done);
3458 });
3459 });
3460
3461 // Disable the tests until the issue in
3462 // https://github.com/strongloop/loopback-datasource-juggler/pull/399
3463 // is fixed
3464 describe.skip('belongsTo with embed', function() {
3465 let Person, Passport;
3466
3467 it('can be declared with embed and properties', function(done) {
3468 Person = db.define('Person', {name: String, age: Number});
3469 Passport = db.define('Passport', {name: String, notes: String});
3470 Passport.belongsTo(Person, {
3471 properties: ['name'],
3472 options: {embedsProperties: true, invertProperties: true},
3473 });
3474 db.automigrate(['Person', 'Passport'], done);
3475 });
3476
3477 it('should create record with embedded data', function(done) {
3478 Person.create({name: 'Fred', age: 36}, function(err, person) {
3479 const p = new Passport({name: 'Passport', notes: 'Some notes...'});
3480 p.person(person);
3481 p.personId.should.eql(person.id);
3482 const data = p.toObject(true);
3483 data.person.id.should.eql(person.id);
3484 data.person.name.should.equal('Fred');
3485 p.save(function(err) {
3486 should.not.exists(err);
3487 done();
3488 });
3489 });
3490 });
3491
3492 it('should find record with embedded data', function(done) {
3493 Passport.findOne(function(err, p) {
3494 should.not.exists(err);
3495 const data = p.toObject(true);
3496 data.person.id.should.eql(p.personId);
3497 data.person.name.should.equal('Fred');
3498 done();
3499 });
3500 });
3501
3502 it('should find record with embedded data with promises', function(done) {
3503 Passport.findOne()
3504 .then(function(p) {
3505 const data = p.toObject(true);
3506 data.person.id.should.eql(p.personId);
3507 data.person.name.should.equal('Fred');
3508 done();
3509 }).catch(done);
3510 });
3511 });
3512
3513 describe('hasOne', function() {
3514 let Supplier, Account;
3515 let supplierId, accountId;
3516
3517 before(function() {
3518 Supplier = db.define('Supplier', {name: String});
3519 Account = db.define('Account', {accountNo: String, supplierName: String});
3520 });
3521
3522 it('can be declared using hasOne method', function() {
3523 Supplier.hasOne(Account, {
3524 properties: {name: 'supplierName'},
3525 methods: {check: function() { return true; }},
3526 });
3527 Object.keys((new Account()).toObject()).should.containEql('supplierId');
3528 (new Supplier()).account.should.be.an.instanceOf(Function);
3529 });
3530
3531 it('should setup a custom method on accessor', function() {
3532 const rel = Supplier.relations['account'];
3533 rel.defineMethod('other', function() {
3534 return true;
3535 });
3536 });
3537
3538 it('should have setup a custom method on accessor', function() {
3539 const s = new Supplier();
3540 s.account.check.should.be.a.function;
3541 s.account.check().should.be.true;
3542 s.account.other.should.be.a.function;
3543 s.account.other().should.be.true;
3544 });
3545
3546 it('can be used to query data', function(done) {
3547 db.automigrate(['Supplier', 'Account'], function() {
3548 Supplier.create({name: 'Supplier 1'}, function(e, supplier) {
3549 supplierId = supplier.id;
3550 should.not.exist(e);
3551 should.exist(supplier);
3552 supplier.account.create({accountNo: 'a01'}, function(err, account) {
3553 supplier.account(function(e, act) {
3554 accountId = act.id;
3555 should.not.exist(e);
3556 should.exist(act);
3557 act.should.be.an.instanceOf(Account);
3558 supplier.account().id.should.eql(act.id);
3559 act.supplierName.should.equal(supplier.name);
3560 done();
3561 });
3562 });
3563 });
3564 });
3565 });
3566
3567 it('can be used to query data with get() with callback', function(done) {
3568 db.automigrate(['Supplier', 'Account'], function() {
3569 Supplier.create({name: 'Supplier 1'}, function(e, supplier) {
3570 supplierId = supplier.id;
3571 should.not.exist(e);
3572 should.exist(supplier);
3573 supplier.account.create({accountNo: 'a01'}, function(err, account) {
3574 supplier.account.get(function(e, act) {
3575 accountId = act.id;
3576 should.not.exist(e);
3577 should.exist(act);
3578 act.should.be.an.instanceOf(Account);
3579 supplier.account().id.should.eql(act.id);
3580 act.supplierName.should.equal(supplier.name);
3581 done();
3582 });
3583 });
3584 });
3585 });
3586 });
3587
3588 it('can be used to query data with promises', function(done) {
3589 db.automigrate(['Supplier', 'Account'], function() {
3590 Supplier.create({name: 'Supplier 1'})
3591 .then(function(supplier) {
3592 supplierId = supplier.id;
3593 should.exist(supplier);
3594 return supplier.account.create({accountNo: 'a01'})
3595 .then(function(account) {
3596 return supplier.account.get();
3597 })
3598 .then(function(act) {
3599 accountId = act.id;
3600 should.exist(act);
3601 act.should.be.an.instanceOf(Account);
3602 supplier.account().id.should.eql(act.id);
3603 act.supplierName.should.equal(supplier.name);
3604 done();
3605 });
3606 })
3607 .catch(done);
3608 });
3609 });
3610
3611 it('should set targetClass on scope property', function() {
3612 should.equal(Supplier.prototype.account._targetClass, 'Account');
3613 });
3614
3615 it('should update the related item on scope', function(done) {
3616 Supplier.findById(supplierId, function(e, supplier) {
3617 should.not.exist(e);
3618 should.exist(supplier);
3619 supplier.account.update({supplierName: 'Supplier A'}, function(err, act) {
3620 should.not.exist(e);
3621 act.supplierName.should.equal('Supplier A');
3622 done();
3623 });
3624 });
3625 });
3626
3627 it('should not update the related item FK on scope', function(done) {
3628 Supplier.findById(supplierId, function(err, supplier) {
3629 if (err) return done(err);
3630 should.exist(supplier);
3631 supplier.account.update({supplierName: 'Supplier A', supplierId: 10}, function(err, acct) {
3632 should.exist(err);
3633 err.message.should.containEql('Cannot override foreign key');
3634 done();
3635 });
3636 });
3637 });
3638
3639 it('should update the related item on scope with promises', function(done) {
3640 Supplier.findById(supplierId)
3641 .then(function(supplier) {
3642 should.exist(supplier);
3643 return supplier.account.update({supplierName: 'Supplier B'});
3644 })
3645 .then(function(act) {
3646 act.supplierName.should.equal('Supplier B');
3647 done();
3648 })
3649 .catch(done);
3650 });
3651
3652 it('should error trying to change the foreign key in the update', function(done) {
3653 Supplier.create({name: 'Supplier 2'}, function(e, supplier) {
3654 const sid = supplier.id;
3655 Supplier.findById(supplierId, function(e, supplier) {
3656 should.not.exist(e);
3657 should.exist(supplier);
3658 supplier.account.update({supplierName: 'Supplier A',
3659 supplierId: sid},
3660 function(err, act) {
3661 should.exist(err);
3662 err.message.should.startWith('Cannot override foreign key');
3663 done();
3664 });
3665 });
3666 });
3667 });
3668
3669 it('should update the related item on scope with same foreign key', function(done) {
3670 Supplier.create({name: 'Supplier 2'}, function(err, supplier) {
3671 Supplier.findById(supplierId, function(err, supplier) {
3672 if (err) return done(err);
3673 should.exist(supplier);
3674 supplier.account.update({supplierName: 'Supplier A',
3675 supplierId: supplierId},
3676 function(err, act) {
3677 if (err) return done(err);
3678 act.supplierName.should.equal('Supplier A');
3679 act.supplierId.toString().should.eql(supplierId.toString());
3680 done();
3681 });
3682 });
3683 });
3684 });
3685
3686 it('should get the related item on scope', function(done) {
3687 Supplier.findById(supplierId, function(e, supplier) {
3688 should.not.exist(e);
3689 should.exist(supplier);
3690 supplier.account(function(err, act) {
3691 should.not.exist(e);
3692 should.exist(act);
3693 act.supplierName.should.equal('Supplier A');
3694 done();
3695 });
3696 });
3697 });
3698
3699 it('should get the related item on scope with promises', function(done) {
3700 Supplier.findById(supplierId)
3701 .then(function(supplier) {
3702 should.exist(supplier);
3703 return supplier.account.get();
3704 })
3705 .then(function(act) {
3706 should.exist(act);
3707 act.supplierName.should.equal('Supplier A');
3708 done();
3709 })
3710 .catch(done);
3711 });
3712
3713 it('should destroy the related item on scope', function(done) {
3714 Supplier.findById(supplierId, function(e, supplier) {
3715 should.not.exist(e);
3716 should.exist(supplier);
3717 supplier.account.destroy(function(err) {
3718 should.not.exist(e);
3719 done();
3720 });
3721 });
3722 });
3723
3724 it('should destroy the related item on scope with promises', function(done) {
3725 Supplier.findById(supplierId)
3726 .then(function(supplier) {
3727 should.exist(supplier);
3728 return supplier.account.create({accountNo: 'a01'})
3729 .then(function(account) {
3730 return supplier.account.destroy();
3731 })
3732 .then(function(err) {
3733 done();
3734 });
3735 })
3736 .catch(done);
3737 });
3738
3739 it('should get the related item on scope - verify', function(done) {
3740 Supplier.findById(supplierId, function(e, supplier) {
3741 should.not.exist(e);
3742 should.exist(supplier);
3743 supplier.account(function(err, act) {
3744 should.not.exist(e);
3745 should.not.exist(act);
3746 done();
3747 });
3748 });
3749 });
3750
3751 it('should get the related item on scope with promises - verify', function(done) {
3752 Supplier.findById(supplierId)
3753 .then(function(supplier) {
3754 should.exist(supplier);
3755 return supplier.account.get();
3756 })
3757 .then(function(act) {
3758 should.not.exist(act);
3759 done();
3760 })
3761 .catch(done);
3762 });
3763
3764 it('should have deleted related item', function(done) {
3765 Supplier.findById(supplierId, function(e, supplier) {
3766 should.not.exist(e);
3767 should.exist(supplier);
3768 done();
3769 });
3770 });
3771 });
3772
3773 describe('hasOne with scope', function() {
3774 let Supplier, Account;
3775 let supplierId, accountId;
3776
3777 before(function() {
3778 Supplier = db.define('Supplier', {name: String});
3779 Account = db.define('Account', {accountNo: String, supplierName: String, block: Boolean});
3780 Supplier.hasOne(Account, {scope: {where: {block: false}}, properties: {name: 'supplierName'}});
3781 });
3782
3783 it('can be used to query data', function(done) {
3784 db.automigrate(['Supplier', 'Account'], function() {
3785 Supplier.create({name: 'Supplier 1'}, function(e, supplier) {
3786 supplierId = supplier.id;
3787 should.not.exist(e);
3788 should.exist(supplier);
3789 supplier.account.create({accountNo: 'a01', block: false}, function(err, account) {
3790 supplier.account(function(e, act) {
3791 accountId = act.id;
3792 should.not.exist(e);
3793 should.exist(act);
3794 act.should.be.an.instanceOf(Account);
3795 should.exist(act.block);
3796 act.block.should.be.false;
3797 supplier.account().id.should.eql(act.id);
3798 act.supplierName.should.equal(supplier.name);
3799 done();
3800 });
3801 });
3802 });
3803 });
3804 });
3805
3806 it('should include record that matches scope', function(done) {
3807 Supplier.findById(supplierId, {include: 'account'}, function(err, supplier) {
3808 should.exists(supplier.toJSON().account);
3809 supplier.account(function(err, account) {
3810 should.exists(account);
3811 done();
3812 });
3813 });
3814 });
3815
3816 bdd.itIf(connectorCapabilities.supportUpdateWithoutId !== false,
3817 'should not find record that does not match scope', function(done) {
3818 Account.updateAll({block: true}, function(err) {
3819 if (err) return done(err);
3820 Supplier.findById(supplierId, function(err, supplier) {
3821 supplier.account(function(err, account) {
3822 should.not.exists(account);
3823 done();
3824 });
3825 });
3826 });
3827 });
3828
3829 bdd.itIf(connectorCapabilities.supportUpdateWithoutId !== false,
3830 'should not include record that does not match scope', function(done) {
3831 Account.updateAll({block: true}, function(err) {
3832 if (err) return done(err);
3833 Supplier.findById(supplierId, {include: 'account'}, function(err, supplier) {
3834 should.not.exists(supplier.toJSON().account);
3835 supplier.account(function(err, account) {
3836 should.not.exists(account);
3837 done();
3838 });
3839 });
3840 });
3841 });
3842
3843 it('can be used to query data with promises', function(done) {
3844 db.automigrate(['Supplier', 'Account'], function() {
3845 Supplier.create({name: 'Supplier 1'})
3846 .then(function(supplier) {
3847 supplierId = supplier.id;
3848 should.exist(supplier);
3849 return supplier.account.create({accountNo: 'a01', block: false})
3850 .then(function(account) {
3851 return supplier.account.get();
3852 })
3853 .then(function(act) {
3854 accountId = act.id;
3855 should.exist(act);
3856 act.should.be.an.instanceOf(Account);
3857 should.exist(act.block);
3858 act.block.should.be.false;
3859 supplier.account().id.should.eql(act.id);
3860 act.supplierName.should.equal(supplier.name);
3861 done();
3862 });
3863 })
3864 .catch(done);
3865 });
3866 });
3867
3868 bdd.itIf(connectorCapabilities.supportUpdateWithoutId !== false,
3869 'should find record that match scope with promises', function(done) {
3870 Account.updateAll({block: true})
3871 .then(function() {
3872 return Supplier.findById(supplierId);
3873 })
3874 .then(function(supplier) {
3875 return supplier.account.get();
3876 })
3877 .then(function(account) {
3878 should.not.exist(account);
3879 done();
3880 })
3881 .catch(function(err) {
3882 done();
3883 });
3884 });
3885 });
3886
3887 describe('hasOne with non standard id', function() {
3888 let Supplier, Account;
3889 let supplierId, accountId;
3890
3891 before(function() {
3892 Supplier = db.define('Supplier', {
3893 sid: {
3894 type: String,
3895 id: true,
3896 generated: true,
3897 },
3898 name: String,
3899 });
3900 Account = db.define('Account', {
3901 accid: {
3902 type: String,
3903 id: true,
3904 generated: false,
3905 },
3906 supplierName: String,
3907 });
3908 });
3909
3910 it('can be declared with non standard foreignKey', function() {
3911 Supplier.hasOne(Account, {
3912 properties: {name: 'supplierName'},
3913 foreignKey: 'sid',
3914 });
3915 Object.keys((new Account()).toObject()).should.containEql('sid');
3916 (new Supplier()).account.should.be.an.instanceOf(Function);
3917 });
3918
3919 it('can be used to query data', function(done) {
3920 db.automigrate(['Supplier', 'Account'], function() {
3921 Supplier.create({name: 'Supplier 1'}, function(e, supplier) {
3922 supplierId = supplier.sid;
3923 should.not.exist(e);
3924 should.exist(supplier);
3925 supplier.account.create({accid: 'a01'}, function(err, account) {
3926 supplier.account(function(e, act) {
3927 accountId = act.accid;
3928 should.not.exist(e);
3929 should.exist(act);
3930 act.should.be.an.instanceOf(Account);
3931 supplier.account().accid.should.eql(act.accid);
3932 act.supplierName.should.equal(supplier.name);
3933 done();
3934 });
3935 });
3936 });
3937 });
3938 });
3939
3940 it('should destroy the related item on scope', function(done) {
3941 Supplier.findById(supplierId, function(e, supplier) {
3942 should.not.exist(e);
3943 should.exist(supplier);
3944 supplier.account.destroy(function(err) {
3945 should.not.exist(e);
3946 done();
3947 });
3948 });
3949 });
3950
3951 bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
3952 'should get the related item on scope - verify', function(done) {
3953 Supplier.findById(supplierId, function(e, supplier) {
3954 should.not.exist(e);
3955 should.exist(supplier);
3956 supplier.account(function(err, act) {
3957 should.not.exist(e);
3958 should.not.exist(act);
3959 done();
3960 });
3961 });
3962 });
3963
3964 it('should have deleted related item', function(done) {
3965 Supplier.findById(supplierId, function(e, supplier) {
3966 should.not.exist(e);
3967 should.exist(supplier);
3968 done();
3969 });
3970 });
3971 });
3972
3973 describe('hasOne with primaryKey different from model PK', function() {
3974 let CompanyBoard, Boss;
3975 let companyBoardId, bossId;
3976
3977 before(function() {
3978 CompanyBoard = db.define('CompanyBoard', {
3979 membersNumber: Number,
3980 companyId: String,
3981 });
3982 Boss = db.define('Boss', {
3983 id: {type: String, id: true, generated: false},
3984 boardMembersNumber: Number,
3985 companyId: String,
3986 });
3987 });
3988
3989 it('relation can be declared with primaryKey', function() {
3990 CompanyBoard.hasOne(Boss, {
3991 properties: {membersNumber: 'boardMembersNumber'},
3992 primaryKey: 'companyId',
3993 foreignKey: 'companyId',
3994 });
3995 Object.keys((new Boss()).toObject()).should.containEql('companyId');
3996 (new CompanyBoard()).boss.should.be.an.instanceOf(Function);
3997 });
3998
3999 it('can be used to query data', function(done) {
4000 db.automigrate(['CompanyBoard', 'Boss'], function() {
4001 CompanyBoard.create({membersNumber: 7, companyId: 'Company1'}, function(e, companyBoard) {
4002 companyBoardId = companyBoard.id;
4003 should.not.exist(e);
4004 should.exist(companyBoard);
4005 companyBoard.boss.create({id: 'bossa01'}, function(err, account) {
4006 companyBoard.boss(function(e, boss) {
4007 bossId = boss.id;
4008 should.not.exist(e);
4009 should.exist(boss);
4010 boss.should.be.an.instanceOf(Boss);
4011 companyBoard.boss().id.should.eql(boss.id);
4012 boss.boardMembersNumber.should.eql(companyBoard.membersNumber);
4013 boss.companyId.should.eql(companyBoard.companyId);
4014 done();
4015 });
4016 });
4017 });
4018 });
4019 });
4020
4021 it('should destroy the related item on scope', function(done) {
4022 CompanyBoard.findById(companyBoardId, function(e, companyBoard) {
4023 should.not.exist(e);
4024 should.exist(companyBoard);
4025 companyBoard.boss.destroy(function(err) {
4026 should.not.exist(e);
4027 done();
4028 });
4029 });
4030 });
4031
4032 it('should get the related item on scope - verify', function(done) {
4033 CompanyBoard.findById(companyBoardId, function(e, companyBoard) {
4034 should.not.exist(e);
4035 should.exist(companyBoard);
4036 companyBoard.boss(function(err, act) {
4037 should.not.exist(e);
4038 should.not.exist(act);
4039 done();
4040 });
4041 });
4042 });
4043 });
4044
4045 describe('hasMany with primaryKey different from model PK', function() {
4046 let Employee, Boss;
4047 const COMPANY_ID = 'Company1';
4048
4049 before(function() {
4050 Employee = db.define('Employee', {name: String, companyId: String});
4051 Boss = db.define('Boss', {address: String, companyId: String});
4052 });
4053
4054 it('relation can be declared with primaryKey', function() {
4055 Boss.hasMany(Employee, {
4056 primaryKey: 'companyId',
4057 foreignKey: 'companyId',
4058 });
4059 (new Boss()).employees.should.be.an.instanceOf(Function);
4060 });
4061
4062 it('can be used to query employees for boss', function() {
4063 return db.automigrate(['Employee', 'Boss']).then(function() {
4064 return Boss.create({address: 'testAddress', companyId: COMPANY_ID})
4065 .then(function(boss) {
4066 should.exist(boss);
4067 should.exist(boss.employees);
4068 return boss.employees.create([{name: 'a01'}, {name: 'a02'}])
4069 .then(function(employees) {
4070 should.exists(employees);
4071 return boss.employees();
4072 }).then(function(employees) {
4073 const employee = employees[0];
4074 should.exist(employee);
4075 employees.length.should.equal(2);
4076 employee.should.be.an.instanceOf(Employee);
4077 employee.companyId.should.eql(boss.companyId);
4078 return employees;
4079 });
4080 });
4081 });
4082 });
4083
4084 it('can be used to query employees for boss2', function() {
4085 return db.automigrate(['Employee', 'Boss']).then(function() {
4086 return Boss.create({address: 'testAddress', companyId: COMPANY_ID})
4087 .then(function(boss) {
4088 return Employee.create({name: 'a01', companyId: COMPANY_ID})
4089 .then(function(employee) {
4090 should.exist(employee);
4091 return boss.employees.find();
4092 }).then(function(employees) {
4093 should.exists(employees);
4094 employees.length.should.equal(1);
4095 });
4096 });
4097 });
4098 });
4099 });
4100
4101 describe('belongsTo with primaryKey different from model PK', function() {
4102 let Employee, Boss;
4103 const COMPANY_ID = 'Company1';
4104 let bossId;
4105
4106 before(function() {
4107 Employee = db.define('Employee', {name: String, companyId: String});
4108 Boss = db.define('Boss', {address: String, companyId: String});
4109 });
4110
4111 it('relation can be declared with primaryKey', function() {
4112 Employee.belongsTo(Boss, {
4113 primaryKey: 'companyId',
4114 foreignKey: 'companyId',
4115 });
4116 (new Employee()).boss.should.be.an.instanceOf(Function);
4117 });
4118
4119 it('can be used to query data', function() {
4120 return db.automigrate(['Employee', 'Boss']).then(function() {
4121 return Boss.create({address: 'testAddress', companyId: COMPANY_ID})
4122 .then(function(boss) {
4123 bossId = boss.id;
4124 return Employee.create({name: 'a', companyId: COMPANY_ID});
4125 })
4126 .then(function(employee) {
4127 should.exists(employee);
4128 return employee.boss.get();
4129 })
4130 .then(function(boss) {
4131 should.exists(boss);
4132 boss.id.should.eql(bossId);
4133 });
4134 });
4135 });
4136 });
4137
4138 describe('hasAndBelongsToMany', function() {
4139 let Article, TagName, ArticleTag;
4140 it('can be declared', function(done) {
4141 Article = db.define('Article', {title: String});
4142 TagName = db.define('TagName', {name: String, flag: String});
4143 Article.hasAndBelongsToMany('tagNames');
4144 ArticleTag = db.models.ArticleTagName;
4145 db.automigrate(['Article', 'TagName', 'ArticleTagName'], done);
4146 });
4147
4148 it('should allow to create instances on scope', function(done) {
4149 Article.create(function(e, article) {
4150 article.tagNames.create({name: 'popular'}, function(e, t) {
4151 t.should.be.an.instanceOf(TagName);
4152 ArticleTag.findOne(function(e, at) {
4153 should.exist(at);
4154 at.tagNameId.toString().should.eql(t.id.toString());
4155 at.articleId.toString().should.eql(article.id.toString());
4156 done();
4157 });
4158 });
4159 });
4160 });
4161
4162 it('should allow to fetch scoped instances', function(done) {
4163 Article.findOne(function(e, article) {
4164 article.tagNames(function(e, tags) {
4165 should.not.exist(e);
4166 should.exist(tags);
4167
4168 article.tagNames().should.eql(tags);
4169
4170 done();
4171 });
4172 });
4173 });
4174
4175 bdd.itIf(connectorCapabilities.deleteWithOtherThanId !== false,
4176 'should destroy all related instances', function(done) {
4177 Article.create(function(err, article) {
4178 if (err) return done(err);
4179 article.tagNames.create({name: 'popular'}, function(err, t) {
4180 if (err) return done(err);
4181 article.tagNames.destroyAll(function(err) {
4182 if (err) return done(err);
4183 article.tagNames(true, function(err, list) {
4184 if (err) return done(err);
4185 list.should.have.length(0);
4186 done();
4187 });
4188 });
4189 });
4190 });
4191 });
4192
4193 it('should allow to add connection with instance', function(done) {
4194 Article.findOne(function(e, article) {
4195 TagName.create({name: 'awesome'}, function(e, tag) {
4196 article.tagNames.add(tag, function(e, at) {
4197 should.not.exist(e);
4198 should.exist(at);
4199 at.should.be.an.instanceOf(ArticleTag);
4200 at.tagNameId.should.eql(tag.id);
4201 at.articleId.should.eql(article.id);
4202 done();
4203 });
4204 });
4205 });
4206 });
4207
4208 bdd.itIf(connectorCapabilities.deleteWithOtherThanId !== false,
4209 'should allow to remove connection with instance', function(done) {
4210 Article.findOne(function(e, article) {
4211 article.tagNames(function(e, tags) {
4212 const len = tags.length;
4213 tags.should.not.be.empty;
4214 article.tagNames.remove(tags[0], function(e) {
4215 should.not.exist(e);
4216 article.tagNames(true, function(e, tags) {
4217 tags.should.have.lengthOf(len - 1);
4218 done();
4219 });
4220 });
4221 });
4222 });
4223 });
4224
4225 it('should allow to create instances on scope with promises', function(done) {
4226 db.automigrate(['Article', 'TagName', 'ArticleTagName'], function() {
4227 Article.create()
4228 .then(function(article) {
4229 return article.tagNames.create({name: 'popular'})
4230 .then(function(t) {
4231 t.should.be.an.instanceOf(TagName);
4232 return ArticleTag.findOne()
4233 .then(function(at) {
4234 should.exist(at);
4235 at.tagNameId.toString().should.eql(t.id.toString());
4236 at.articleId.toString().should.eql(article.id.toString());
4237 done();
4238 });
4239 });
4240 }).catch(done);
4241 });
4242 });
4243
4244 it('should allow to fetch scoped instances with promises', function(done) {
4245 Article.findOne()
4246 .then(function(article) {
4247 return article.tagNames.find()
4248 .then(function(tags) {
4249 should.exist(tags);
4250 article.tagNames().should.eql(tags);
4251 done();
4252 });
4253 }).catch(done);
4254 });
4255
4256 it('should allow to add connection with instance with promises', function(done) {
4257 Article.findOne()
4258 .then(function(article) {
4259 return TagName.create({name: 'awesome'})
4260 .then(function(tag) {
4261 return article.tagNames.add(tag)
4262 .then(function(at) {
4263 should.exist(at);
4264 at.should.be.an.instanceOf(ArticleTag);
4265 at.tagNameId.should.eql(tag.id);
4266 at.articleId.should.eql(article.id);
4267 done();
4268 });
4269 });
4270 })
4271 .catch(done);
4272 });
4273
4274 bdd.itIf(connectorCapabilities.deleteWithOtherThanId !== false,
4275 'should allow to remove connection with instance with promises', function(done) {
4276 Article.findOne()
4277 .then(function(article) {
4278 return article.tagNames.find()
4279 .then(function(tags) {
4280 const len = tags.length;
4281 tags.should.not.be.empty;
4282 return article.tagNames.remove(tags[0])
4283 .then(function() {
4284 return article.tagNames.find();
4285 })
4286 .then(function(tags) {
4287 tags.should.have.lengthOf(len - 1);
4288 done();
4289 });
4290 });
4291 })
4292 .catch(done);
4293 });
4294
4295 it('should set targetClass on scope property', function() {
4296 should.equal(Article.prototype.tagNames._targetClass, 'TagName');
4297 });
4298
4299 it('should apply inclusion fields to the target model', function(done) {
4300 Article.create({title: 'a1'}, function(e, article) {
4301 should.not.exist(e);
4302 article.tagNames.create({name: 't1', flag: '1'}, function(e, t) {
4303 should.not.exist(e);
4304 Article.find({
4305 where: {id: article.id},
4306 include: {relation: 'tagNames', scope: {fields: ['name']}}},
4307 function(e, articles) {
4308 should.not.exist(e);
4309 articles.should.have.property('length', 1);
4310 const a = articles[0].toJSON();
4311 a.should.have.property('title', 'a1');
4312 a.should.have.property('tagNames');
4313 a.tagNames.should.have.property('length', 1);
4314 const n = a.tagNames[0];
4315 n.should.have.property('name', 't1');
4316 n.should.have.property('flag', undefined);
4317 n.id.should.eql(t.id);
4318 done();
4319 });
4320 });
4321 });
4322 });
4323
4324 it('should apply inclusion where to the target model', function(done) {
4325 Article.create({title: 'a2'}, function(e, article) {
4326 should.not.exist(e);
4327 article.tagNames.create({name: 't2', flag: '2'}, function(e, t2) {
4328 should.not.exist(e);
4329 article.tagNames.create({name: 't3', flag: '3'}, function(e, t3) {
4330 Article.find({
4331 where: {id: article.id},
4332 include: {relation: 'tagNames', scope: {where: {flag: '2'}}}},
4333 function(e, articles) {
4334 should.not.exist(e);
4335 articles.should.have.property('length', 1);
4336 const a = articles[0].toJSON();
4337 a.should.have.property('title', 'a2');
4338 a.should.have.property('tagNames');
4339 a.tagNames.should.have.property('length', 1);
4340 const n = a.tagNames[0];
4341 n.should.have.property('name', 't2');
4342 n.should.have.property('flag', '2');
4343 n.id.should.eql(t2.id);
4344 done();
4345 });
4346 });
4347 });
4348 });
4349 });
4350 });
4351
4352 describe('embedsOne', function() {
4353 let person;
4354 let Passport;
4355 let Other;
4356
4357 before(function() {
4358 tmp = getTransientDataSource();
4359 Person = db.define('Person', {name: String});
4360 Passport = tmp.define('Passport',
4361 {name: {type: 'string', required: true}},
4362 {idInjection: false});
4363 Address = tmp.define('Address', {street: String}, {idInjection: false});
4364 Other = db.define('Other', {name: String});
4365 Person.embedsOne(Passport, {
4366 default: {name: 'Anonymous'}, // a bit contrived
4367 methods: {check: function() { return true; }},
4368 options: {
4369 property: {
4370 postgresql: {
4371 columnName: 'passport_item',
4372 },
4373 },
4374 },
4375 });
4376 });
4377
4378 it('can be declared using embedsOne method', function(done) {
4379 Person.embedsOne(Address); // all by default
4380 db.automigrate(['Person'], done);
4381 });
4382
4383 it('should have setup a property and accessor', function() {
4384 const p = new Person();
4385 p.passport.should.be.an.object; // because of default
4386 p.passportItem.should.be.a.function;
4387 p.passportItem.create.should.be.a.function;
4388 p.passportItem.build.should.be.a.function;
4389 p.passportItem.destroy.should.be.a.function;
4390 });
4391
4392 it('respects property options on the embedded property', function() {
4393 Person.definition.properties.passport.should.have.property('postgresql');
4394 Person.definition.properties.passport.postgresql.should.eql({columnName: 'passport_item'});
4395 });
4396
4397 it('should setup a custom method on accessor', function() {
4398 const rel = Person.relations['passportItem'];
4399 rel.defineMethod('other', function() {
4400 return true;
4401 });
4402 });
4403
4404 it('should have setup a custom method on accessor', function() {
4405 const p = new Person();
4406 p.passportItem.check.should.be.a.function;
4407 p.passportItem.check().should.be.true;
4408 p.passportItem.other.should.be.a.function;
4409 p.passportItem.other().should.be.true;
4410 });
4411
4412 it('should behave properly without default or being set', function(done) {
4413 const p = new Person();
4414 should.not.exist(p.address);
4415 const a = p.addressItem();
4416 should.not.exist(a);
4417 Person.create({}, function(err, p) {
4418 should.not.exist(p.address);
4419 const a = p.addressItem();
4420 should.not.exist(a);
4421 done();
4422 });
4423 });
4424
4425 it('should return an instance with default values', function() {
4426 const p = new Person();
4427 p.passport.toObject().should.eql({name: 'Anonymous'});
4428 p.passportItem().should.equal(p.passport);
4429 p.passportItem(function(err, passport) {
4430 should.not.exist(err);
4431 passport.should.equal(p.passport);
4432 });
4433 });
4434
4435 it('should embed a model instance', function() {
4436 const p = new Person();
4437 p.passportItem(new Passport({name: 'Fred'}));
4438 p.passport.toObject().should.eql({name: 'Fred'});
4439 p.passport.should.be.an.instanceOf(Passport);
4440 });
4441
4442 it('should not embed an invalid model type', function() {
4443 const p = new Person();
4444 p.passportItem(new Other());
4445 p.passport.toObject().should.eql({name: 'Anonymous'});
4446 p.passport.should.be.an.instanceOf(Passport);
4447 });
4448
4449 let personId;
4450 it('should create an embedded item on scope', function(done) {
4451 Person.create({name: 'Fred'}, function(err, p) {
4452 if (err) return done(err);
4453 personId = p.id;
4454 p.passportItem.create({name: 'Fredric'}, function(err, passport) {
4455 if (err) return done(err);
4456 p.passport.toObject().should.eql({name: 'Fredric'});
4457 p.passport.should.be.an.instanceOf(Passport);
4458 done();
4459 });
4460 });
4461 });
4462
4463 it('should get an embedded item on scope', function(done) {
4464 Person.findById(personId, function(err, p) {
4465 if (err) return done(err);
4466 const passport = p.passportItem();
4467 passport.toObject().should.eql({name: 'Fredric'});
4468 passport.should.be.an.instanceOf(Passport);
4469 passport.should.equal(p.passport);
4470 passport.should.equal(p.passportItem.value());
4471 done();
4472 });
4473 });
4474
4475 it('should validate an embedded item on scope - on creation', function(done) {
4476 const p = new Person({name: 'Fred'});
4477 p.passportItem.create({}, function(err, passport) {
4478 should.exist(err);
4479 err.name.should.equal('ValidationError');
4480 err.details.messages.name.should.eql(['can\'t be blank']);
4481 done();
4482 });
4483 });
4484
4485 it('should validate an embedded item on scope - on update', function(done) {
4486 Person.findById(personId, function(err, p) {
4487 const passport = p.passportItem();
4488 passport.name = null;
4489 p.save(function(err) {
4490 should.exist(err);
4491 err.name.should.equal('ValidationError');
4492 err.details.messages.passportItem
4493 .should.eql(['is invalid: `name` can\'t be blank']);
4494 done();
4495 });
4496 });
4497 });
4498
4499 it('should update an embedded item on scope', function(done) {
4500 Person.findById(personId, function(err, p) {
4501 p.passportItem.update({name: 'Freddy'}, function(err, passport) {
4502 if (err) return done(err);
4503 passport = p.passportItem();
4504 passport.toObject().should.eql({name: 'Freddy'});
4505 passport.should.be.an.instanceOf(Passport);
4506 passport.should.equal(p.passport);
4507 done();
4508 });
4509 });
4510 });
4511
4512 it('should get an embedded item on scope - verify', function(done) {
4513 Person.findById(personId, function(err, p) {
4514 if (err) return done(err);
4515 const passport = p.passportItem();
4516 passport.toObject().should.eql({name: 'Freddy'});
4517 done();
4518 });
4519 });
4520
4521 it('should destroy an embedded item on scope', function(done) {
4522 Person.findById(personId, function(err, p) {
4523 p.passportItem.destroy(function(err) {
4524 if (err) return done(err);
4525 should.equal(p.passport, null);
4526 done();
4527 });
4528 });
4529 });
4530
4531 // eslint-disable-next-line mocha/no-identical-title
4532 it('should get an embedded item on scope - verify', function(done) {
4533 Person.findById(personId, function(err, p) {
4534 if (err) return done(err);
4535 should.equal(p.passport, null);
4536 done();
4537 });
4538 });
4539
4540 it('should save an unsaved model', function(done) {
4541 const p = new Person({name: 'Fred'});
4542 p.isNewRecord().should.be.true;
4543 p.passportItem.create({name: 'Fredric'}, function(err, passport) {
4544 if (err) return done(err);
4545 p.passport.should.equal(passport);
4546 p.isNewRecord().should.be.false;
4547 done();
4548 });
4549 });
4550
4551 it('should create an embedded item on scope with promises', function(done) {
4552 Person.create({name: 'Fred'})
4553 .then(function(p) {
4554 personId = p.id;
4555 p.passportItem.create({name: 'Fredric'})
4556 .then(function(passport) {
4557 p.passport.toObject().should.eql({name: 'Fredric'});
4558 p.passport.should.be.an.instanceOf(Passport);
4559 done();
4560 });
4561 }).catch(done);
4562 });
4563
4564 it('should get an embedded item on scope with promises', function(done) {
4565 Person.findById(personId)
4566 .then(function(p) {
4567 const passport = p.passportItem();
4568 passport.toObject().should.eql({name: 'Fredric'});
4569 passport.should.be.an.instanceOf(Passport);
4570 passport.should.equal(p.passport);
4571 passport.should.equal(p.passportItem.value());
4572 done();
4573 }).catch(done);
4574 });
4575
4576 it('should validate an embedded item on scope with promises - on creation', function(done) {
4577 const p = new Person({name: 'Fred'});
4578 p.passportItem.create({})
4579 .then(function(passport) {
4580 should.not.exist(passport);
4581 done();
4582 })
4583 .catch(function(err) {
4584 should.exist(err);
4585 err.name.should.equal('ValidationError');
4586 err.details.messages.name.should.eql(['can\'t be blank']);
4587 done();
4588 }).catch(done);
4589 });
4590
4591 it('should validate an embedded item on scope with promises - on update', function(done) {
4592 Person.findById(personId)
4593 .then(function(p) {
4594 const passport = p.passportItem();
4595 passport.name = null;
4596 return p.save()
4597 .then(function(p) {
4598 should.not.exist(p);
4599 done();
4600 })
4601 .catch(function(err) {
4602 should.exist(err);
4603 err.name.should.equal('ValidationError');
4604 err.details.messages.passportItem
4605 .should.eql(['is invalid: `name` can\'t be blank']);
4606 done();
4607 });
4608 }).catch(done);
4609 });
4610
4611 it('should update an embedded item on scope with promises', function(done) {
4612 Person.findById(personId)
4613 .then(function(p) {
4614 return p.passportItem.update({name: 'Jason'})
4615 .then(function(passport) {
4616 passport = p.passportItem();
4617 passport.toObject().should.eql({name: 'Jason'});
4618 passport.should.be.an.instanceOf(Passport);
4619 passport.should.equal(p.passport);
4620 done();
4621 });
4622 }).catch(done);
4623 });
4624
4625 it('should get an embedded item on scope with promises - verify', function(done) {
4626 Person.findById(personId)
4627 .then(function(p) {
4628 const passport = p.passportItem();
4629 passport.toObject().should.eql({name: 'Jason'});
4630 done();
4631 }).catch(done);
4632 });
4633
4634 it('should destroy an embedded item on scope with promises', function(done) {
4635 Person.findById(personId)
4636 .then(function(p) {
4637 return p.passportItem.destroy()
4638 .then(function() {
4639 should.equal(p.passport, null);
4640 done();
4641 });
4642 }).catch(done);
4643 });
4644
4645 // eslint-disable-next-line mocha/no-identical-title
4646 it('should get an embedded item on scope with promises - verify', function(done) {
4647 Person.findById(personId)
4648 .then(function(p) {
4649 should.equal(p.passport, null);
4650 done();
4651 }).catch(done);
4652 });
4653
4654 it('should also save changes when directly saving the embedded model', function(done) {
4655 // Passport should normally have an id for the direct save to work. For now override the check
4656 const originalHasPK = Passport.definition.hasPK;
4657 Passport.definition.hasPK = function() { return true; };
4658 Person.findById(personId)
4659 .then(function(p) {
4660 return p.passportItem.create({name: 'Mitsos'});
4661 })
4662 .then(function(passport) {
4663 passport.name = 'Jim';
4664 return passport.save();
4665 })
4666 .then(function() {
4667 return Person.findById(personId);
4668 })
4669 .then(function(person) {
4670 person.passportItem().toObject().should.eql({name: 'Jim'});
4671 // restore original hasPk
4672 Passport.definition.hasPK = originalHasPK;
4673 done();
4674 })
4675 .catch(function(err) {
4676 Passport.definition.hasPK = originalHasPK;
4677 done(err);
4678 });
4679 });
4680
4681 it('should delete the embedded document and also update parent', function(done) {
4682 const originalHasPK = Passport.definition.hasPK;
4683 Passport.definition.hasPK = function() { return true; };
4684 Person.findById(personId)
4685 .then(function(p) {
4686 return p.passportItem().destroy();
4687 })
4688 .then(function() {
4689 return Person.findById(personId);
4690 })
4691 .then(function(person) {
4692 person.should.have.property('passport', null);
4693 done();
4694 })
4695 .catch(function(err) {
4696 Passport.definition.hasPK = originalHasPK;
4697 done(err);
4698 });
4699 });
4700 });
4701
4702 describe('embedsOne - persisted model', function() {
4703 // This test spefically uses the Memory connector
4704 // in order to test the use of the auto-generated
4705 // id, in the sequence of the related model.
4706 let Passport, Person;
4707 before(function() {
4708 db = getMemoryDataSource();
4709 Person = db.define('Person', {name: String});
4710 Passport = db.define('Passport',
4711 {name: {type: 'string', required: true}});
4712 });
4713
4714 it('can be declared using embedsOne method', function(done) {
4715 Person.embedsOne(Passport, {
4716 options: {persistent: true},
4717 });
4718 db.automigrate(['Person', 'Passport'], done);
4719 });
4720
4721 it('should create an item - to offset id', function(done) {
4722 Passport.create({name: 'Wilma'}, function(err, p) {
4723 if (err) return done(err);
4724 p.id.should.equal(1);
4725 p.name.should.equal('Wilma');
4726 done();
4727 });
4728 });
4729
4730 it('should create an embedded item on scope', function(done) {
4731 Person.create({name: 'Fred'}, function(err, p) {
4732 if (err) return done(err);
4733 p.passportItem.create({name: 'Fredric'}, function(err, passport) {
4734 if (err) return done(err);
4735 p.passport.id.should.eql(2);
4736 p.passport.name.should.equal('Fredric');
4737 done();
4738 });
4739 });
4740 });
4741
4742 it('should create an embedded item on scope with promises', function(done) {
4743 Person.create({name: 'Barney'})
4744 .then(function(p) {
4745 return p.passportItem.create({name: 'Barnabus'})
4746 .then(function(passport) {
4747 p.passport.id.should.eql(3);
4748 p.passport.name.should.equal('Barnabus');
4749 done();
4750 });
4751 }).catch(done);
4752 });
4753 });
4754
4755 describe('embedsOne - generated id', function() {
4756 let Passport;
4757 before(function() {
4758 tmp = getTransientDataSource();
4759 Person = db.define('Person', {name: String});
4760 Passport = tmp.define('Passport',
4761 {
4762 id: {type: 'string', id: true, generated: true},
4763 name: {type: 'string', required: true},
4764 });
4765 });
4766
4767 it('can be declared using embedsOne method', function(done) {
4768 Person.embedsOne(Passport);
4769 db.automigrate(['Person'], done);
4770 });
4771
4772 it('should create an embedded item on scope', function(done) {
4773 Person.create({name: 'Fred'}, function(err, p) {
4774 if (err) return done(err);
4775 p.passportItem.create({name: 'Fredric'}, function(err, passport) {
4776 if (err) return done(err);
4777 passport.id.should.match(/^[0-9a-fA-F]{24}$/);
4778 p.passport.name.should.equal('Fredric');
4779 done();
4780 });
4781 });
4782 });
4783 });
4784
4785 describe('embedsMany', function() {
4786 let address1, address2;
4787
4788 before(function(done) {
4789 tmp = getTransientDataSource({defaultIdType: Number});
4790 Person = db.define('Person', {name: String});
4791 Address = tmp.define('Address', {street: String});
4792 Address.validatesPresenceOf('street');
4793
4794 db.automigrate(['Person'], done);
4795 });
4796
4797 it('can be declared', function(done) {
4798 Person.embedsMany(Address, {
4799 options: {
4800 property: {
4801 postgresql: {
4802 dataType: 'json',
4803 },
4804 },
4805 },
4806 });
4807 db.automigrate(['Person'], done);
4808 });
4809
4810 it('should have setup embedded accessor/scope', function() {
4811 const p = new Person({name: 'Fred'});
4812 p.addresses.should.be.an.array;
4813 p.addresses.should.have.length(0);
4814 p.addressList.should.be.a.function;
4815 p.addressList.findById.should.be.a.function;
4816 p.addressList.updateById.should.be.a.function;
4817 p.addressList.destroy.should.be.a.function;
4818 p.addressList.exists.should.be.a.function;
4819 p.addressList.create.should.be.a.function;
4820 p.addressList.build.should.be.a.function;
4821 });
4822
4823 it('should create embedded items on scope', function(done) {
4824 Person.create({name: 'Fred'}, function(err, p) {
4825 p.addressList.create({street: 'Street 1'}, function(err, address) {
4826 if (err) return done(err);
4827 address1 = address;
4828 should.exist(address1.id);
4829 address1.street.should.equal('Street 1');
4830 done();
4831 });
4832 });
4833 });
4834
4835 it('respects property options on the embedded property', function() {
4836 Person.definition.properties.addresses.should.have.property('postgresql');
4837 Person.definition.properties.addresses.postgresql.should.eql({dataType: 'json'});
4838 });
4839
4840 // eslint-disable-next-line mocha/no-identical-title
4841 it('should create embedded items on scope', function(done) {
4842 Person.findOne(function(err, p) {
4843 p.addressList.create({street: 'Street 2'}, function(err, address) {
4844 if (err) return done(err);
4845 address2 = address;
4846 should.exist(address2.id);
4847 address2.street.should.equal('Street 2');
4848 done();
4849 });
4850 });
4851 });
4852
4853 it('should return embedded items from scope', function(done) {
4854 Person.findOne(function(err, p) {
4855 p.addressList(function(err, addresses) {
4856 if (err) return done(err);
4857
4858 const list = p.addressList();
4859 list.should.equal(addresses);
4860 list.should.equal(p.addresses);
4861
4862 p.addressList.value().should.equal(list);
4863
4864 addresses.should.have.length(2);
4865 addresses[0].id.should.eql(address1.id);
4866 addresses[0].street.should.equal('Street 1');
4867 addresses[1].id.should.eql(address2.id);
4868 addresses[1].street.should.equal('Street 2');
4869 done();
4870 });
4871 });
4872 });
4873
4874 it('should filter embedded items on scope', function(done) {
4875 Person.findOne(function(err, p) {
4876 p.addressList({where: {street: 'Street 2'}}, function(err, addresses) {
4877 if (err) return done(err);
4878 addresses.should.have.length(1);
4879 addresses[0].id.should.eql(address2.id);
4880 addresses[0].street.should.equal('Street 2');
4881 done();
4882 });
4883 });
4884 });
4885
4886 it('should validate embedded items', function(done) {
4887 Person.findOne(function(err, p) {
4888 p.addressList.create({}, function(err, address) {
4889 should.exist(err);
4890 should.not.exist(address);
4891 err.name.should.equal('ValidationError');
4892 err.details.codes.street.should.eql(['presence']);
4893 done();
4894 });
4895 });
4896 });
4897
4898 it('should find embedded items by id', function(done) {
4899 Person.findOne(function(err, p) {
4900 p.addressList.findById(address2.id, function(err, address) {
4901 address.should.be.instanceof(Address);
4902 address.id.should.eql(address2.id);
4903 address.street.should.equal('Street 2');
4904 done();
4905 });
4906 });
4907 });
4908
4909 it('should check if item exists', function(done) {
4910 Person.findOne(function(err, p) {
4911 p.addressList.exists(address2.id, function(err, exists) {
4912 if (err) return done(err);
4913 exists.should.be.true;
4914 done();
4915 });
4916 });
4917 });
4918
4919 it('should update embedded items by id', function(done) {
4920 Person.findOne(function(err, p) {
4921 p.addressList.updateById(address2.id, {street: 'New Street'}, function(err, address) {
4922 address.should.be.instanceof(Address);
4923 address.id.should.eql(address2.id);
4924 address.street.should.equal('New Street');
4925 done();
4926 });
4927 });
4928 });
4929
4930 it('should validate the update of embedded items', function(done) {
4931 Person.findOne(function(err, p) {
4932 p.addressList.updateById(address2.id, {street: null}, function(err, address) {
4933 err.name.should.equal('ValidationError');
4934 err.details.codes.street.should.eql(['presence']);
4935 done();
4936 });
4937 });
4938 });
4939
4940 it('should find embedded items by id - verify', function(done) {
4941 Person.findOne(function(err, p) {
4942 p.addressList.findById(address2.id, function(err, address) {
4943 address.should.be.instanceof(Address);
4944 address.id.should.eql(address2.id);
4945 address.street.should.equal('New Street');
4946 done();
4947 });
4948 });
4949 });
4950
4951 it('should have accessors: at, get, set', function(done) {
4952 Person.findOne(function(err, p) {
4953 p.addressList.at(0).id.should.eql(address1.id);
4954 p.addressList.get(address1.id).id.should.eql(address1.id);
4955 p.addressList.set(address1.id, {street: 'Changed 1'});
4956 p.addresses[0].street.should.equal('Changed 1');
4957 p.addressList.at(1).id.should.eql(address2.id);
4958 p.addressList.get(address2.id).id.should.eql(address2.id);
4959 p.addressList.set(address2.id, {street: 'Changed 2'});
4960 p.addresses[1].street.should.equal('Changed 2');
4961 done();
4962 });
4963 });
4964
4965 it('should remove embedded items by id', function(done) {
4966 Person.findOne(function(err, p) {
4967 p.addresses.should.have.length(2);
4968 p.addressList.destroy(address1.id, function(err) {
4969 if (err) return done(err);
4970 p.addresses.should.have.length(1);
4971 done();
4972 });
4973 });
4974 });
4975
4976 it('should have removed embedded items - verify', function(done) {
4977 Person.findOne(function(err, p) {
4978 p.addresses.should.have.length(1);
4979 done();
4980 });
4981 });
4982
4983 it('should pass options when removed by id', function(done) {
4984 const verifyOptions = function(ctx, next) {
4985 if (!ctx.options || !ctx.options.verify) {
4986 return next(new Error('options or options.verify is missing'));
4987 }
4988 return next();
4989 };
4990 Person.observe('before save', verifyOptions);
4991 Person.findOne(function(err, p) {
4992 p.addressList.create({street: 'options 1'}, {verify: true}, function(err, address) {
4993 if (err) {
4994 Person.clearObservers('before save');
4995 return done(err);
4996 }
4997 p.addressList.destroy(address.id, {verify: true}, function(err) {
4998 if (err) {
4999 Person.clearObservers('before save');
5000 return done(err);
5001 }
5002 Person.findById(p.id, function(err, verify) {
5003 if (err) {
5004 Person.clearObservers('before save');
5005 return done(err);
5006 }
5007 verify.addresses.should.have.length(1);
5008 Person.clearObservers('before save');
5009 done();
5010 });
5011 });
5012 });
5013 });
5014 });
5015
5016 it('should pass options when removed by where', function(done) {
5017 const verifyOptions = function(ctx, next) {
5018 if (!ctx.options || !ctx.options.verify) {
5019 return next(new Error('options or options.verify is missing'));
5020 }
5021 return next();
5022 };
5023 Person.observe('before save', verifyOptions);
5024 Person.findOne(function(err, p) {
5025 p.addressList.create({street: 'options 2'}, {verify: true}, function(err, address) {
5026 if (err) {
5027 Person.clearObservers('before save');
5028 return done(err);
5029 }
5030 p.addressList.destroyAll({street: 'options 2'}, {verify: true}, function(err) {
5031 if (err) {
5032 Person.clearObservers('before save');
5033 return done(err);
5034 }
5035 Person.findById(p.id, function(err, verify) {
5036 if (err) {
5037 Person.clearObservers('before save');
5038 return done(err);
5039 }
5040 verify.addresses.should.have.length(1);
5041 Person.clearObservers('before save');
5042 done();
5043 });
5044 });
5045 });
5046 });
5047 });
5048
5049 // eslint-disable-next-line mocha/no-identical-title
5050 it('should create embedded items on scope', function(done) {
5051 Person.findOne(function(err, p) {
5052 p.addressList.create({street: 'Street 3'}, function(err, address) {
5053 if (err) return done(err);
5054 address.street.should.equal('Street 3');
5055 done();
5056 });
5057 });
5058 });
5059
5060 it('should remove embedded items - filtered', function(done) {
5061 Person.findOne(function(err, p) {
5062 p.addresses.should.have.length(2);
5063 p.addressList.destroyAll({street: 'Street 3'}, function(err) {
5064 if (err) return done(err);
5065 p.addresses.should.have.length(1);
5066 done();
5067 });
5068 });
5069 });
5070
5071 it('should remove all embedded items', function(done) {
5072 Person.findOne(function(err, p) {
5073 p.addresses.should.have.length(1);
5074 p.addressList.destroyAll(function(err) {
5075 if (err) return done(err);
5076 p.addresses.should.have.length(0);
5077 done();
5078 });
5079 });
5080 });
5081
5082 it('should have removed all embedded items - verify', function(done) {
5083 Person.findOne(function(err, p) {
5084 p.addresses.should.have.length(0);
5085 done();
5086 });
5087 });
5088
5089 it('should save an unsaved model', function(done) {
5090 const p = new Person({name: 'Fred'});
5091 p.isNewRecord().should.be.true;
5092 p.addressList.create({street: 'Street 4'}, function(err, address) {
5093 if (err) return done(err);
5094 address.street.should.equal('Street 4');
5095 p.isNewRecord().should.be.false;
5096 done();
5097 });
5098 });
5099 });
5100
5101 describe('embedsMany - omit default value for embedded item', function() {
5102 before(function(done) {
5103 tmp = getTransientDataSource({defaultIdType: Number});
5104 Person = db.define('Person', {name: String});
5105 Address = tmp.define('Address', {street: String});
5106 Address.validatesPresenceOf('street');
5107
5108 db.automigrate(['Person'], done);
5109 });
5110
5111 it('can be declared', function(done) {
5112 Person.embedsMany(Address, {
5113 options: {
5114 omitDefaultEmbeddedItem: true,
5115 property: {
5116 postgresql: {
5117 dataType: 'json',
5118 },
5119 },
5120 },
5121 });
5122 db.automigrate(['Person'], done);
5123 });
5124
5125 it('should not set default value for embedded item', function() {
5126 const p = new Person({name: 'Fred'});
5127 p.should.have.property('addresses', undefined);
5128 });
5129
5130 it('should create embedded items on scope', function(done) {
5131 Person.create({name: 'Fred'}, function(err, p) {
5132 p.addressList.create({street: 'Street 1'}, function(err, address) {
5133 if (err) return done(err);
5134 should.exist(address.id);
5135 address.street.should.equal('Street 1');
5136 p.addresses.should.be.array;
5137 p.addresses.should.have.length(1);
5138 done();
5139 });
5140 });
5141 });
5142
5143 it('should build embedded items', function(done) {
5144 Person.findOne(function(err, p) {
5145 p.addresses.should.have.length(1);
5146 p.addressList.build({id: 'home', street: 'Home'});
5147 p.addressList.build({id: 'work', street: 'Work'});
5148 p.addresses.should.have.length(3);
5149 done();
5150 });
5151 });
5152
5153 it('should not create embedded from attributes - relation name', function(done) {
5154 const addresses = [
5155 {id: 'home', street: 'Home Street'},
5156 {id: 'work', street: 'Work Street'},
5157 ];
5158 Person.create({name: 'Wilma', addressList: addresses}, function(err, p) {
5159 if (err) return done(err);
5160 p.should.have.property('addresses', undefined);
5161 done();
5162 });
5163 });
5164 });
5165
5166 describe('embedsMany - numeric ids + forceId', function() {
5167 before(function(done) {
5168 tmp = getTransientDataSource();
5169 Person = db.define('Person', {name: String});
5170 Address = tmp.define('Address', {
5171 id: {type: Number, id: true},
5172 street: String,
5173 });
5174
5175 db.automigrate(['Person'], done);
5176 });
5177
5178 it('can be declared', function(done) {
5179 Person.embedsMany(Address, {options: {forceId: true}});
5180 db.automigrate(['Person'], done);
5181 });
5182
5183 it('should create embedded items on scope', function(done) {
5184 Person.create({name: 'Fred'}, function(err, p) {
5185 p.addressList.create({street: 'Street 1'}, function(err, address) {
5186 if (err) return done(err);
5187 address.id.should.equal(1);
5188 p.addressList.create({street: 'Street 2'}, function(err, address) {
5189 address.id.should.equal(2);
5190 p.addressList.create({id: 12345, street: 'Street 3'}, function(err, address) {
5191 address.id.should.equal(3);
5192 done();
5193 });
5194 });
5195 });
5196 });
5197 });
5198 });
5199
5200 describe('embedsMany - explicit ids', function() {
5201 before(function(done) {
5202 tmp = getTransientDataSource();
5203 Person = db.define('Person', {name: String});
5204 Address = tmp.define('Address', {street: String}, {forceId: false});
5205 Address.validatesPresenceOf('street');
5206
5207 db.automigrate(['Person'], done);
5208 });
5209
5210 it('can be declared', function(done) {
5211 Person.embedsMany(Address);
5212 db.automigrate(['Person'], done);
5213 });
5214
5215 it('should create embedded items on scope', function(done) {
5216 Person.create({name: 'Fred'}, function(err, p) {
5217 p.addressList.create({id: 'home', street: 'Street 1'}, function(err, address) {
5218 if (err) return done(err);
5219 p.addressList.create({id: 'work', street: 'Work Street 2'}, function(err, address) {
5220 if (err) return done(err);
5221 address.id.should.equal('work');
5222 address.street.should.equal('Work Street 2');
5223 done();
5224 });
5225 });
5226 });
5227 });
5228
5229 it('should find embedded items by id', function(done) {
5230 Person.findOne(function(err, p) {
5231 p.addressList.findById('work', function(err, address) {
5232 address.should.be.instanceof(Address);
5233 address.id.should.equal('work');
5234 address.street.should.equal('Work Street 2');
5235 done();
5236 });
5237 });
5238 });
5239
5240 it('should check for duplicate ids', function(done) {
5241 Person.findOne(function(err, p) {
5242 p.addressList.create({id: 'home', street: 'Invalid'}, function(err, addresses) {
5243 should.exist(err);
5244 err.name.should.equal('ValidationError');
5245 err.details.codes.addresses.should.eql(['uniqueness']);
5246 done();
5247 });
5248 });
5249 });
5250
5251 it('should update embedded items by id', function(done) {
5252 Person.findOne(function(err, p) {
5253 p.addressList.updateById('home', {street: 'New Street'}, function(err, address) {
5254 address.should.be.instanceof(Address);
5255 address.id.should.equal('home');
5256 address.street.should.equal('New Street');
5257 done();
5258 });
5259 });
5260 });
5261
5262 it('should remove embedded items by id', function(done) {
5263 Person.findOne(function(err, p) {
5264 p.addresses.should.have.length(2);
5265 p.addressList.destroy('home', function(err) {
5266 if (err) return done(err);
5267 p.addresses.should.have.length(1);
5268 done();
5269 });
5270 });
5271 });
5272
5273 it('should have embedded items - verify', function(done) {
5274 Person.findOne(function(err, p) {
5275 p.addresses.should.have.length(1);
5276 done();
5277 });
5278 });
5279
5280 it('should validate all embedded items', function(done) {
5281 const addresses = [];
5282 addresses.push({id: 'home', street: 'Home Street'});
5283 addresses.push({id: 'work', street: ''});
5284 Person.create({name: 'Wilma', addresses: addresses}, function(err, p) {
5285 err.name.should.equal('ValidationError');
5286 err.details.messages.addresses.should.eql([
5287 'contains invalid item: `work` (`street` can\'t be blank)',
5288 ]);
5289 done();
5290 });
5291 });
5292
5293 it('should build embedded items', function(done) {
5294 Person.create({name: 'Wilma'}, function(err, p) {
5295 p.addressList.build({id: 'home', street: 'Home'});
5296 p.addressList.build({id: 'work', street: 'Work'});
5297 p.addresses.should.have.length(2);
5298 p.save(function(err, p) {
5299 done();
5300 });
5301 });
5302 });
5303
5304 // eslint-disable-next-line mocha/no-identical-title
5305 it('should have embedded items - verify', function(done) {
5306 Person.findOne({where: {name: 'Wilma'}}, function(err, p) {
5307 p.name.should.equal('Wilma');
5308 p.addresses.should.have.length(2);
5309 p.addresses[0].id.should.equal('home');
5310 p.addresses[0].street.should.equal('Home');
5311 p.addresses[1].id.should.equal('work');
5312 p.addresses[1].street.should.equal('Work');
5313 done();
5314 });
5315 });
5316
5317 it('should have accessors: at, get, set', function(done) {
5318 Person.findOne({where: {name: 'Wilma'}}, function(err, p) {
5319 p.name.should.equal('Wilma');
5320 p.addresses.should.have.length(2);
5321 p.addressList.at(0).id.should.equal('home');
5322 p.addressList.get('home').id.should.equal('home');
5323 p.addressList.set('home', {id: 'den'}).id.should.equal('den');
5324 p.addressList.at(1).id.should.equal('work');
5325 p.addressList.get('work').id.should.equal('work');
5326 p.addressList.set('work', {id: 'factory'}).id.should.equal('factory');
5327 done();
5328 });
5329 });
5330
5331 it('should create embedded from attributes - property name', function(done) {
5332 const addresses = [
5333 {id: 'home', street: 'Home Street'},
5334 {id: 'work', street: 'Work Street'},
5335 ];
5336 Person.create({name: 'Wilma', addresses: addresses}, function(err, p) {
5337 if (err) return done(err);
5338 p.addressList.at(0).id.should.equal('home');
5339 p.addressList.at(1).id.should.equal('work');
5340 done();
5341 });
5342 });
5343
5344 it('should not create embedded from attributes - relation name', function(done) {
5345 const addresses = [
5346 {id: 'home', street: 'Home Street'},
5347 {id: 'work', street: 'Work Street'},
5348 ];
5349 Person.create({name: 'Wilma', addressList: addresses}, function(err, p) {
5350 if (err) return done(err);
5351 p.addresses.should.have.length(0);
5352 done();
5353 });
5354 });
5355
5356 it('should create embedded items with auto-generated id', function(done) {
5357 Person.create({name: 'Wilma'}, function(err, p) {
5358 p.addressList.create({street: 'Home Street 1'}, function(err, address) {
5359 if (err) return done(err);
5360 address.id.should.match(/^[0-9a-fA-F]{24}$/);
5361 address.street.should.equal('Home Street 1');
5362 done();
5363 });
5364 });
5365 });
5366 });
5367
5368 describe('embedsMany - persisted model', function() {
5369 let address0, address1, address2;
5370 let person;
5371
5372 // This test spefically uses the Memory connector
5373 // in order to test the use of the auto-generated
5374 // id, in the sequence of the related model.
5375
5376 before(function(done) {
5377 db = getMemoryDataSource();
5378 Person = db.define('Person', {name: String});
5379 Address = db.define('Address', {street: String});
5380 Address.validatesPresenceOf('street');
5381
5382 db.automigrate(['Person', 'Address'], done);
5383 });
5384
5385 it('can be declared', function(done) {
5386 // to save related model itself, set
5387 // persistent: true
5388 Person.embedsMany(Address, {
5389 scope: {order: 'street'},
5390 options: {persistent: true},
5391 });
5392 db.automigrate(['Person', 'Address'], done);
5393 });
5394
5395 it('should create individual items (0)', function(done) {
5396 Address.create({street: 'Street 0'}, function(err, inst) {
5397 inst.id.should.equal(1); // offset sequence
5398 address0 = inst;
5399 done();
5400 });
5401 });
5402
5403 it('should create individual items (1)', function(done) {
5404 Address.create({street: 'Street 1'}, function(err, inst) {
5405 inst.id.should.equal(2);
5406 address1 = inst;
5407 done();
5408 });
5409 });
5410
5411 it('should create individual items (2)', function(done) {
5412 Address.create({street: 'Street 2'}, function(err, inst) {
5413 inst.id.should.equal(3);
5414 address2 = inst;
5415 done();
5416 });
5417 });
5418
5419 it('should create individual items (3)', function(done) {
5420 Address.create({street: 'Street 3'}, function(err, inst) {
5421 inst.id.should.equal(4); // offset sequence
5422 done();
5423 });
5424 });
5425
5426 it('should add embedded items on scope', function(done) {
5427 Person.create({name: 'Fred'}, function(err, p) {
5428 person = p;
5429 p.addressList.create(address1.toObject(), function(err, address) {
5430 if (err) return done(err);
5431 address.id.should.eql(2);
5432 address.street.should.equal('Street 1');
5433 p.addressList.create(address2.toObject(), function(err, address) {
5434 if (err) return done(err);
5435 address.id.should.eql(3);
5436 address.street.should.equal('Street 2');
5437 done();
5438 });
5439 });
5440 });
5441 });
5442
5443 it('should create embedded items on scope', function(done) {
5444 Person.findById(person.id, function(err, p) {
5445 p.addressList.create({street: 'Street 4'}, function(err, address) {
5446 if (err) return done(err);
5447 address.id.should.equal(5); // in Address sequence, correct offset
5448 address.street.should.equal('Street 4');
5449 done();
5450 });
5451 });
5452 });
5453
5454 it('should have embedded items on scope', function(done) {
5455 Person.findById(person.id, function(err, p) {
5456 p.addressList(function(err, addresses) {
5457 if (err) return done(err);
5458 addresses.should.have.length(3);
5459 addresses[0].street.should.equal('Street 1');
5460 addresses[1].street.should.equal('Street 2');
5461 addresses[2].street.should.equal('Street 4');
5462 done();
5463 });
5464 });
5465 });
5466
5467 it('should validate embedded items on scope - id', function(done) {
5468 Person.create({name: 'Wilma'}, function(err, p) {
5469 p.addressList.create({id: null, street: 'Street 1'}, function(err, address) {
5470 if (err) return done(err);
5471 address.street.should.equal('Street 1');
5472 done();
5473 });
5474 });
5475 });
5476
5477 it('should validate embedded items on scope - street', function(done) {
5478 const newId = uid.fromConnector(db) || 1234;
5479 Person.create({name: 'Wilma'}, function(err, p) {
5480 p.addressList.create({id: newId}, function(err, address) {
5481 should.exist(err);
5482 err.name.should.equal('ValidationError');
5483 err.details.codes.street.should.eql(['presence']);
5484 let expected = 'The `Address` instance is not valid. ';
5485 expected += 'Details: `street` can\'t be blank (value: undefined).';
5486 err.message.should.equal(expected);
5487 done();
5488 });
5489 });
5490 });
5491 });
5492
5493 describe('embedsMany - relations, scope and properties', function() {
5494 let category, job1, job2, job3;
5495
5496 before(function() {
5497 Category = db.define('Category', {name: String});
5498 Job = db.define('Job', {name: String});
5499 Link = db.define('Link', {name: String, notes: String}, {forceId: false});
5500 });
5501
5502 it('can be declared', function(done) {
5503 Category.embedsMany(Link, {
5504 as: 'items', // rename
5505 scope: {include: 'job'}, // always include
5506 options: {belongsTo: 'job'}, // optional, for add()/remove()
5507 });
5508 Link.belongsTo(Job, {
5509 foreignKey: 'id', // re-use the actual job id
5510 properties: {id: 'id', name: 'name'}, // denormalize, transfer id
5511 options: {invertProperties: true},
5512 });
5513 db.automigrate(['Category', 'Job', 'Link'], function() {
5514 Job.create({name: 'Job 0'}, done); // offset ids for tests
5515 });
5516 });
5517
5518 it('should setup related items', function(done) {
5519 Job.create({name: 'Job 1'}, function(err, p) {
5520 if (err) return done(err);
5521 job1 = p;
5522 Job.create({name: 'Job 2'}, function(err, p) {
5523 if (err) return done(err);
5524 job2 = p;
5525 Job.create({name: 'Job 3'}, function(err, p) {
5526 if (err) return done(err);
5527 job3 = p;
5528 done();
5529 });
5530 });
5531 });
5532 });
5533
5534 it('should associate items on scope', function(done) {
5535 Category.create({name: 'Category A'}, function(err, cat) {
5536 if (err) return done(err);
5537 let link = cat.items.build();
5538 link.job(job1);
5539 link = cat.items.build();
5540 link.job(job2);
5541 cat.save(function(err, cat) {
5542 if (err) return done(err);
5543 let job = cat.items.at(0);
5544 job.should.be.instanceof(Link);
5545 job.should.not.have.property('jobId');
5546 job.id.should.eql(job1.id);
5547 job.name.should.equal(job1.name);
5548 job = cat.items.at(1);
5549 job.id.should.eql(job2.id);
5550 job.name.should.equal(job2.name);
5551 done();
5552 });
5553 });
5554 });
5555
5556 it('should include related items on scope', function(done) {
5557 Category.findOne(function(err, cat) {
5558 if (err) return done(err);
5559 cat.links.should.have.length(2);
5560
5561 // denormalized properties:
5562 cat.items.at(0).should.be.instanceof(Link);
5563 cat.items.at(0).id.should.eql(job1.id);
5564 cat.items.at(0).name.should.equal(job1.name);
5565 cat.items.at(1).id.should.eql(job2.id);
5566 cat.items.at(1).name.should.equal(job2.name);
5567
5568 // lazy-loaded relations
5569 should.not.exist(cat.items.at(0).job());
5570 should.not.exist(cat.items.at(1).job());
5571
5572 cat.items(function(err, items) {
5573 if (err) return done(err);
5574 cat.items.at(0).job().should.be.instanceof(Job);
5575 cat.items.at(1).job().should.be.instanceof(Job);
5576 cat.items.at(1).job().name.should.equal('Job 2');
5577 done();
5578 });
5579 });
5580 });
5581
5582 it('should remove embedded items by id', function(done) {
5583 Category.findOne(function(err, cat) {
5584 if (err) return done(err);
5585 cat.links.should.have.length(2);
5586 cat.items.destroy(job1.id, function(err) {
5587 if (err) return done(err);
5588 if (err) return done(err);
5589 cat.links.should.have.length(1);
5590 done();
5591 });
5592 });
5593 });
5594
5595 it('should find items on scope', function(done) {
5596 Category.findOne(function(err, cat) {
5597 if (err) return done(err);
5598 cat.links.should.have.length(1);
5599 cat.items.at(0).id.should.eql(job2.id);
5600 cat.items.at(0).name.should.equal(job2.name);
5601
5602 // lazy-loaded relations
5603 should.not.exist(cat.items.at(0).job());
5604
5605 cat.items(function(err, items) {
5606 if (err) return done(err);
5607 cat.items.at(0).job().should.be.instanceof(Job);
5608 cat.items.at(0).job().name.should.equal('Job 2');
5609 done();
5610 });
5611 });
5612 });
5613
5614 it('should add related items to scope', function(done) {
5615 Category.findOne(function(err, cat) {
5616 if (err) return done(err);
5617 cat.links.should.have.length(1);
5618 cat.items.add(job3, function(err, link) {
5619 if (err) return done(err);
5620 link.should.be.instanceof(Link);
5621 link.id.should.eql(job3.id);
5622 link.name.should.equal('Job 3');
5623
5624 cat.links.should.have.length(2);
5625 done();
5626 });
5627 });
5628 });
5629
5630 // eslint-disable-next-line mocha/no-identical-title
5631 it('should find items on scope', function(done) {
5632 Category.findOne(function(err, cat) {
5633 if (err) return done(err);
5634 cat.links.should.have.length(2);
5635
5636 cat.items.at(0).should.be.instanceof(Link);
5637 cat.items.at(0).id.should.eql(job2.id);
5638 cat.items.at(0).name.should.equal(job2.name);
5639 cat.items.at(1).id.should.eql(job3.id);
5640 cat.items.at(1).name.should.equal(job3.name);
5641
5642 done();
5643 });
5644 });
5645
5646 it('should remove embedded items by reference id', function(done) {
5647 Category.findOne(function(err, cat) {
5648 if (err) return done(err);
5649 cat.links.should.have.length(2);
5650 cat.items.remove(job2.id, function(err) {
5651 if (err) return done(err);
5652 if (err) return done(err);
5653 cat.links.should.have.length(1);
5654 done();
5655 });
5656 });
5657 });
5658
5659 it('should have removed embedded items by reference id', function(done) {
5660 Category.findOne(function(err, cat) {
5661 if (err) return done(err);
5662 cat.links.should.have.length(1);
5663 done();
5664 });
5665 });
5666
5667 let jobId;
5668
5669 it('should create items on scope', function(done) {
5670 Category.create({name: 'Category B'}, function(err, cat) {
5671 if (err) return done(err);
5672 category = cat;
5673 const link = cat.items.build({notes: 'Some notes...'});
5674 link.job.create({name: 'Job 1'}, function(err, p) {
5675 if (err) return done(err);
5676 jobId = p.id;
5677 cat.links[0].id.should.eql(p.id);
5678 cat.links[0].name.should.equal('Job 1'); // denormalized
5679 cat.links[0].notes.should.equal('Some notes...');
5680 cat.items.at(0).should.equal(cat.links[0]);
5681 done();
5682 });
5683 });
5684 });
5685
5686 // eslint-disable-next-line mocha/no-identical-title
5687 it('should find items on scope', function(done) {
5688 Category.findById(category.id, function(err, cat) {
5689 if (err) return done(err);
5690 cat.name.should.equal('Category B');
5691 cat.links.toObject().should.eql([
5692 {id: jobId, name: 'Job 1', notes: 'Some notes...'},
5693 ]);
5694 cat.items.at(0).should.equal(cat.links[0]);
5695 cat.items(function(err, items) { // alternative access
5696 if (err) return done(err);
5697 items.should.be.an.array;
5698 items.should.have.length(1);
5699 items[0].job(function(err, p) {
5700 p.name.should.equal('Job 1'); // actual value
5701 done();
5702 });
5703 });
5704 });
5705 });
5706
5707 it('should update items on scope - and save parent', function(done) {
5708 Category.findById(category.id, function(err, cat) {
5709 if (err) return done(err);
5710 const link = cat.items.at(0);
5711 // use 'updateById' instead as a replacement as it is one of the embedsMany methods,
5712 // that works with all connectors. `updateAttributes` does not recognize the query done on
5713 // the Category Model, resulting with an error in three connectors: mssql, oracle, postgresql
5714 cat.items.updateById(link.id, {notes: 'Updated notes...'}, function(err, link) {
5715 if (err) return done(err);
5716 link.notes.should.equal('Updated notes...');
5717 done();
5718 });
5719 });
5720 });
5721
5722 it('should find items on scope - verify update', function(done) {
5723 Category.findById(category.id, function(err, cat) {
5724 if (err) return done(err);
5725 cat.name.should.equal('Category B');
5726 cat.links.toObject().should.eql([
5727 {id: jobId, name: 'Job 1', notes: 'Updated notes...'},
5728 ]);
5729 done();
5730 });
5731 });
5732
5733 it('should remove items from scope - and save parent', function(done) {
5734 Category.findById(category.id, function(err, cat) {
5735 if (err) return done(err);
5736 cat.items.at(0).destroy(function(err, link) {
5737 if (err) return done(err);
5738 cat.links.should.have.lengthOf(0);
5739 done();
5740 });
5741 });
5742 });
5743
5744 it('should find items on scope - verify destroy', function(done) {
5745 Category.findById(category.id, function(err, cat) {
5746 if (err) return done(err);
5747 cat.name.should.equal('Category B');
5748 cat.links.should.have.lengthOf(0);
5749 done();
5750 });
5751 });
5752 });
5753
5754 describe('embedsMany - polymorphic relations', function() {
5755 let person1, person2;
5756
5757 before(function(done) {
5758 tmp = getTransientDataSource();
5759
5760 Book = db.define('Book', {name: String});
5761 Author = db.define('Author', {name: String});
5762 Reader = db.define('Reader', {name: String});
5763
5764 Link = tmp.define('Link', {
5765 id: {type: Number, id: true},
5766 name: String, notes: String,
5767 }); // generic model
5768 Link.validatesPresenceOf('linkedId');
5769 Link.validatesPresenceOf('linkedType');
5770
5771 db.automigrate(['Book', 'Author', 'Reader'], done);
5772 });
5773
5774 it('can be declared', function(done) {
5775 const idType = db.connector.getDefaultIdType();
5776
5777 Book.embedsMany(Link, {as: 'people',
5778 polymorphic: 'linked',
5779 scope: {include: 'linked'},
5780 });
5781 Link.belongsTo('linked', {
5782 polymorphic: {idType: idType}, // native type
5783 properties: {name: 'name'}, // denormalized
5784 options: {invertProperties: true},
5785 });
5786 db.automigrate(['Book', 'Author', 'Reader'], done);
5787 });
5788
5789 it('should setup related items', function(done) {
5790 Author.create({name: 'Author 1'}, function(err, p) {
5791 person1 = p;
5792 Reader.create({name: 'Reader 1'}, function(err, p) {
5793 person2 = p;
5794 done();
5795 });
5796 });
5797 });
5798
5799 it('should create items on scope', function(done) {
5800 Book.create({name: 'Book'}, function(err, book) {
5801 let link = book.people.build({notes: 'Something ...'});
5802 link.linked(person1);
5803 link = book.people.build();
5804 link.linked(person2);
5805 book.save(function(err, book) {
5806 if (err) return done(err);
5807
5808 let link = book.people.at(0);
5809 link.should.be.instanceof(Link);
5810 link.id.should.equal(1);
5811 link.linkedId.should.eql(person1.id);
5812 link.linkedType.should.equal('Author');
5813 link.name.should.equal('Author 1');
5814
5815 link = book.people.at(1);
5816 link.should.be.instanceof(Link);
5817 link.id.should.equal(2);
5818 link.linkedId.should.eql(person2.id);
5819 link.linkedType.should.equal('Reader');
5820 link.name.should.equal('Reader 1');
5821
5822 done();
5823 });
5824 });
5825 });
5826
5827 it('should include related items on scope', function(done) {
5828 Book.findOne(function(err, book) {
5829 book.links.should.have.length(2);
5830
5831 let link = book.people.at(0);
5832 link.should.be.instanceof(Link);
5833 link.id.should.eql(1);
5834 link.linkedId.should.eql(person1.id);
5835 link.linkedType.should.equal('Author');
5836 link.notes.should.equal('Something ...');
5837
5838 link = book.people.at(1);
5839 link.should.be.instanceof(Link);
5840 link.id.should.eql(2);
5841 link.linkedId.should.eql(person2.id);
5842 link.linkedType.should.equal('Reader');
5843
5844 // lazy-loaded relations
5845 should.not.exist(book.people.at(0).linked());
5846 should.not.exist(book.people.at(1).linked());
5847
5848 book.people(function(err, people) {
5849 people[0].linked().should.be.instanceof(Author);
5850 people[0].linked().name.should.equal('Author 1');
5851 people[1].linked().should.be.instanceof(Reader);
5852 people[1].linked().name.should.equal('Reader 1');
5853 done();
5854 });
5855 });
5856 });
5857
5858 bdd.itIf(connectorCapabilities.supportInclude === true,
5859 'should include nested related items on scope', function(done) {
5860 // There's some date duplication going on, so it might
5861 // make sense to override toObject on a case-by-case basis
5862 // to sort this out (delete links, keep people).
5863 // In loopback, an afterRemote filter could do this as well.
5864
5865 Book.find({include: 'people'}, function(err, books) {
5866 const obj = books[0].toObject();
5867
5868 obj.should.have.property('links');
5869 obj.should.have.property('people');
5870
5871 obj.links.should.have.length(2);
5872 obj.links[0].name.should.be.oneOf('Author 1', 'Reader 1');
5873 obj.links[1].name.should.be.oneOf('Author 1', 'Reader 1');
5874
5875 obj.people.should.have.length(2);
5876
5877 obj.people[0].name.should.equal('Author 1');
5878 obj.people[0].notes.should.equal('Something ...');
5879
5880 obj.people[0].linked.name.should.equal('Author 1');
5881 obj.people[1].linked.name.should.equal('Reader 1');
5882
5883 done();
5884 });
5885 });
5886 });
5887
5888 describe('referencesMany', function() {
5889 let job1, job2, job3;
5890
5891 before(function(done) {
5892 Category = db.define('Category', {name: String});
5893 Job = db.define('Job', {name: String});
5894
5895 db.automigrate(['Job', 'Category'], done);
5896 });
5897
5898 it('can be declared', function(done) {
5899 const reverse = function(cb) {
5900 cb = cb || createPromiseCallback();
5901 const modelInstance = this.modelInstance;
5902 const fk = this.definition.keyFrom;
5903 const ids = modelInstance[fk] || [];
5904 modelInstance.updateAttribute(fk, ids.reverse(), function(err, inst) {
5905 cb(err, inst[fk] || []);
5906 });
5907 return cb.promise;
5908 };
5909
5910 reverse.shared = true; // remoting
5911 reverse.http = {verb: 'put', path: '/jobs/reverse'};
5912
5913 Category.referencesMany(Job, {scopeMethods: {
5914 reverse: reverse,
5915 }});
5916
5917 Category.prototype['__reverse__jobs'].should.be.a.function;
5918 should.exist(Category.prototype['__reverse__jobs'].shared);
5919 Category.prototype['__reverse__jobs'].http.should.eql(reverse.http);
5920
5921 db.automigrate(['Job', 'Category'], done);
5922 });
5923
5924 it('should setup test records', function(done) {
5925 Job.create({name: 'Job 1'}, function(err, p) {
5926 job1 = p;
5927 Job.create({name: 'Job 3'}, function(err, p) {
5928 job3 = p;
5929 done();
5930 });
5931 });
5932 });
5933
5934 it('should create record on scope', function(done) {
5935 Category.create({name: 'Category A'}, function(err, cat) {
5936 cat.jobIds.should.be.an.array;
5937 cat.jobIds.should.have.length(0);
5938 cat.jobs.create({name: 'Job 2'}, function(err, p) {
5939 if (err) return done(err);
5940 cat.jobIds.should.have.lengthOf(1);
5941 cat.jobIds[0].should.eql(p.id);
5942 p.name.should.equal('Job 2');
5943 job2 = p;
5944 done();
5945 });
5946 });
5947 });
5948
5949 it('should not allow duplicate record on scope', function(done) {
5950 Category.findOne(function(err, cat) {
5951 cat.jobIds = [job2.id, job2.id];
5952 cat.save(function(err, p) {
5953 should.exist(err);
5954 err.name.should.equal('ValidationError');
5955 err.details.codes.jobs.should.eql(['uniqueness']);
5956 done();
5957 });
5958 });
5959 });
5960
5961 it('should find items on scope', function(done) {
5962 Category.findOne(function(err, cat) {
5963 cat.jobIds.should.have.lengthOf(1);
5964 cat.jobIds[0].should.eql(job2.id);
5965 cat.jobs(function(err, jobs) {
5966 if (err) return done(err);
5967 const p = jobs[0];
5968 p.id.should.eql(job2.id);
5969 p.name.should.equal('Job 2');
5970 done();
5971 });
5972 });
5973 });
5974
5975 it('should find items on scope - findById', function(done) {
5976 Category.findOne(function(err, cat) {
5977 cat.jobIds.should.have.lengthOf(1);
5978 cat.jobIds[0].should.eql(job2.id);
5979 cat.jobs.findById(job2.id, function(err, p) {
5980 if (err) return done(err);
5981 p.should.be.instanceof(Job);
5982 p.id.should.eql(job2.id);
5983 p.name.should.equal('Job 2');
5984 done();
5985 });
5986 });
5987 });
5988
5989 it('should check if a record exists on scope', function(done) {
5990 Category.findOne(function(err, cat) {
5991 cat.jobs.exists(job2.id, function(err, exists) {
5992 if (err) return done(err);
5993 should.exist(exists);
5994 done();
5995 });
5996 });
5997 });
5998
5999 it('should update a record on scope', function(done) {
6000 Category.findOne(function(err, cat) {
6001 const attrs = {name: 'Job 2 - edit'};
6002 cat.jobs.updateById(job2.id, attrs, function(err, p) {
6003 if (err) return done(err);
6004 p.name.should.equal(attrs.name);
6005 done();
6006 });
6007 });
6008 });
6009
6010 it('should get a record by index - at', function(done) {
6011 Category.findOne(function(err, cat) {
6012 cat.jobs.at(0, function(err, p) {
6013 if (err) return done(err);
6014 p.should.be.instanceof(Job);
6015 p.id.should.eql(job2.id);
6016 p.name.should.equal('Job 2 - edit');
6017 done();
6018 });
6019 });
6020 });
6021
6022 it('should add a record to scope - object', function(done) {
6023 Category.findOne(function(err, cat) {
6024 cat.jobs.add(job1, function(err, prod) {
6025 if (err) return done(err);
6026 cat.jobIds[0].should.eql(job2.id);
6027 cat.jobIds[1].should.eql(job1.id);
6028 prod.id.should.eql(job1.id);
6029 prod.should.have.property('name');
6030 done();
6031 });
6032 });
6033 });
6034
6035 // eslint-disable-next-line mocha/no-identical-title
6036 it('should add a record to scope - object', function(done) {
6037 Category.findOne(function(err, cat) {
6038 cat.jobs.add(job3.id, function(err, prod) {
6039 if (err) return done(err);
6040 const expected = [job2.id, job1.id, job3.id];
6041 cat.jobIds[0].should.eql(expected[0]);
6042 cat.jobIds[1].should.eql(expected[1]);
6043 cat.jobIds[2].should.eql(expected[2]);
6044 prod.id.should.eql(job3.id);
6045 prod.should.have.property('name');
6046 done();
6047 });
6048 });
6049 });
6050
6051 // eslint-disable-next-line mocha/no-identical-title
6052 it('should find items on scope - findById', function(done) {
6053 Category.findOne(function(err, cat) {
6054 cat.jobs.findById(job3.id, function(err, p) {
6055 if (err) return done(err);
6056 p.id.should.eql(job3.id);
6057 p.name.should.equal('Job 3');
6058 done();
6059 });
6060 });
6061 });
6062
6063 it('should find items on scope - filter', function(done) {
6064 Category.findOne(function(err, cat) {
6065 const filter = {where: {name: 'Job 1'}};
6066 cat.jobs(filter, function(err, jobs) {
6067 if (err) return done(err);
6068 jobs.should.have.length(1);
6069 const p = jobs[0];
6070 p.id.should.eql(job1.id);
6071 p.name.should.equal('Job 1');
6072 done();
6073 });
6074 });
6075 });
6076
6077 it('should remove items from scope', function(done) {
6078 Category.findOne(function(err, cat) {
6079 cat.jobs.remove(job1.id, function(err, ids) {
6080 if (err) return done(err);
6081 const expected = [job2.id, job3.id];
6082 cat.jobIds[0].should.eql(expected[0]);
6083 cat.jobIds[1].should.eql(expected[1]);
6084 cat.jobIds[0].should.eql(ids[0]);
6085 cat.jobIds[1].should.eql(ids[1]);
6086 done();
6087 });
6088 });
6089 });
6090
6091 it('should find items on scope - verify', function(done) {
6092 Category.findOne(function(err, cat) {
6093 const expected = [job2.id, job3.id];
6094 cat.jobIds[0].should.eql(expected[0]);
6095 cat.jobIds[1].should.eql(expected[1]);
6096 cat.jobs(function(err, jobs) {
6097 if (err) return done(err);
6098 jobs.should.have.length(2);
6099 jobs[0].id.should.eql(job2.id);
6100 jobs[1].id.should.eql(job3.id);
6101 done();
6102 });
6103 });
6104 });
6105
6106 bdd.itIf(connectorCapabilities.adhocSort !== false,
6107 'should find items on scope and ordered them by name DESC', function(done) {
6108 Category.find(function(err, categories) {
6109 categories.should.have.length(1);
6110 categories[0].jobs({order: 'name DESC'}, function(err, jobs) {
6111 if (err) return done(err);
6112 jobs.should.have.length(2);
6113 jobs[0].id.should.eql(job3.id);
6114 jobs[1].id.should.eql(job2.id);
6115 done();
6116 });
6117 });
6118 });
6119
6120 bdd.itIf(connectorCapabilities.adhocSort !== false,
6121 'should allow custom scope methods - reverse', function(done) {
6122 Category.findOne(function(err, cat) {
6123 cat.jobs.reverse(function(err, ids) {
6124 const expected = [job3.id, job2.id];
6125 ids.toArray().should.eql(expected);
6126 cat.jobIds.toArray().should.eql(expected);
6127 done();
6128 });
6129 });
6130 });
6131
6132 bdd.itIf(connectorCapabilities.adhocSort === false,
6133 'should allow custom scope methods - reverse', function(done) {
6134 Category.findOne(function(err, cat) {
6135 cat.jobs.reverse(function(err, ids) {
6136 const expected = [job3.id, job2.id];
6137 ids[0].should.be.oneOf(expected);
6138 ids[1].should.be.oneOf(expected);
6139 cat.jobIds[0].should.be.oneOf(expected);
6140 cat.jobIds[1].should.be.oneOf(expected);
6141 done();
6142 });
6143 });
6144 });
6145
6146 bdd.itIf(connectorCapabilities.supportInclude === true,
6147 'should include related items from scope', function(done) {
6148 Category.find({include: 'jobs'}, function(err, categories) {
6149 categories.should.have.length(1);
6150 const cat = categories[0].toObject();
6151 cat.name.should.equal('Category A');
6152 cat.jobs.should.have.length(2);
6153 cat.jobs[0].id.should.eql(job3.id);
6154 cat.jobs[1].id.should.eql(job2.id);
6155 done();
6156 });
6157 });
6158
6159 it('should destroy items from scope - destroyById', function(done) {
6160 Category.findOne(function(err, cat) {
6161 cat.jobs.destroy(job2.id, function(err) {
6162 if (err) return done(err);
6163 cat.jobIds.should.have.lengthOf(1);
6164 cat.jobIds[0].should.eql(job3.id);
6165 Job.exists(job2.id, function(err, exists) {
6166 if (err) return done(err);
6167 should.exist(exists);
6168 exists.should.be.false;
6169 done();
6170 });
6171 });
6172 });
6173 });
6174
6175 // eslint-disable-next-line mocha/no-identical-title
6176 it('should find items on scope - verify', function(done) {
6177 Category.findOne(function(err, cat) {
6178 cat.jobIds.should.have.lengthOf(1);
6179 cat.jobIds[0].should.eql(job3.id);
6180 cat.jobs(function(err, jobs) {
6181 if (err) return done(err);
6182 jobs.should.have.length(1);
6183 jobs[0].id.should.eql(job3.id);
6184 done();
6185 });
6186 });
6187 });
6188
6189 it('should setup test records with promises', function(done) {
6190 db.automigrate(['Job', 'Category'], function() {
6191 return Job.create({name: 'Job 1'})
6192 .then(function(p) {
6193 job1 = p;
6194 return Job.create({name: 'Job 3'});
6195 })
6196 .then(function(p) {
6197 job3 = p;
6198 done();
6199 }).catch(done);
6200 });
6201 });
6202
6203 it('should create record on scope with promises', function(done) {
6204 Category.create({name: 'Category A'})
6205 .then(function(cat) {
6206 cat.jobIds.should.be.an.array;
6207 cat.jobIds.should.have.length(0);
6208 return cat.jobs.create({name: 'Job 2'})
6209 .then(function(p) {
6210 cat.jobIds.should.have.length(1);
6211 cat.jobIds[0].should.eql(p.id);
6212 p.name.should.equal('Job 2');
6213 job2 = p;
6214 done();
6215 });
6216 }).catch(done);
6217 });
6218
6219 it('should not allow duplicate record on scope with promises', function(done) {
6220 Category.findOne()
6221 .then(function(cat) {
6222 cat.jobIds = [job2.id, job2.id];
6223 return cat.save();
6224 })
6225 .then(
6226 function(p) { done(new Error('save() should have failed')); },
6227 function(err) {
6228 err.name.should.equal('ValidationError');
6229 err.details.codes.jobs.should.eql(['uniqueness']);
6230 done();
6231 },
6232 );
6233 });
6234
6235 bdd.itIf(connectorCapabilities.adhocSort !== false,
6236 'should find items on scope with promises', function(done) {
6237 Category.findOne()
6238 .then(function(cat) {
6239 cat.jobIds.toArray().should.eql([job2.id]);
6240 return cat.jobs.find();
6241 })
6242 .then(function(jobs) {
6243 const p = jobs[0];
6244 p.id.should.eql(job2.id);
6245 p.name.should.equal('Job 2');
6246 done();
6247 })
6248 .catch(done);
6249 });
6250
6251 bdd.itIf(connectorCapabilities.adhocSort === false,
6252 'should find items on scope with promises', function(done) {
6253 const theExpectedIds = [job1.id, job2.id, job3.id];
6254 const theExpectedNames = ['Job 1', 'Job 2', 'Job 3'];
6255 Category.findOne()
6256 .then(function(cat) {
6257 cat.jobIds[0].should.be.oneOf(theExpectedIds);
6258 return cat.jobs.find();
6259 })
6260 .then(function(jobs) {
6261 const p = jobs[0];
6262 p.id.should.be.oneOf(theExpectedIds);
6263 p.name.should.be.oneOf(theExpectedNames);
6264 done();
6265 })
6266 .catch(done);
6267 });
6268
6269 it('should find items on scope with promises - findById', function(done) {
6270 Category.findOne()
6271 .then(function(cat) {
6272 cat.jobIds.should.have.lengthOf(1);
6273 cat.jobIds[0].should.eql(job2.id);
6274 return cat.jobs.findById(job2.id);
6275 })
6276 .then(function(p) {
6277 p.should.be.instanceof(Job);
6278 p.id.should.eql(job2.id);
6279 p.name.should.equal('Job 2');
6280 done();
6281 })
6282 .catch(done);
6283 });
6284
6285 it('should check if a record exists on scope with promises', function(done) {
6286 Category.findOne()
6287 .then(function(cat) {
6288 return cat.jobs.exists(job2.id)
6289 .then(function(exists) {
6290 should.exist(exists);
6291 done();
6292 });
6293 }).catch(done);
6294 });
6295
6296 it('should update a record on scope with promises', function(done) {
6297 Category.findOne()
6298 .then(function(cat) {
6299 const attrs = {name: 'Job 2 - edit'};
6300 return cat.jobs.updateById(job2.id, attrs)
6301 .then(function(p) {
6302 p.name.should.equal(attrs.name);
6303 done();
6304 });
6305 })
6306 .catch(done);
6307 });
6308
6309 it('should get a record by index with promises - at', function(done) {
6310 Category.findOne()
6311 .then(function(cat) {
6312 return cat.jobs.at(0);
6313 })
6314 .then(function(p) {
6315 p.should.be.instanceof(Job);
6316 p.id.should.eql(job2.id);
6317 p.name.should.equal('Job 2 - edit');
6318 done();
6319 })
6320 .catch(done);
6321 });
6322
6323 it('should add a record to scope with promises - object', function(done) {
6324 Category.findOne()
6325 .then(function(cat) {
6326 return cat.jobs.add(job1)
6327 .then(function(prod) {
6328 const expected = [job2.id, job1.id];
6329 cat.jobIds.should.have.lengthOf(expected.length);
6330 cat.jobIds.should.containDeep(expected);
6331 prod.id.should.eql(job1.id);
6332 prod.should.have.property('name');
6333 done();
6334 });
6335 })
6336 .catch(done);
6337 });
6338
6339 // eslint-disable-next-line mocha/no-identical-title
6340 it('should add a record to scope with promises - object', function(done) {
6341 Category.findOne()
6342 .then(function(cat) {
6343 return cat.jobs.add(job3.id)
6344 .then(function(prod) {
6345 const expected = [job2.id, job1.id, job3.id];
6346 cat.jobIds.should.have.lengthOf(expected.length);
6347 cat.jobIds.should.containDeep(expected);
6348 prod.id.should.eql(job3.id);
6349 prod.should.have.property('name');
6350 done();
6351 });
6352 })
6353 .catch(done);
6354 });
6355
6356 // eslint-disable-next-line mocha/no-identical-title
6357 it('should find items on scope with promises - findById', function(done) {
6358 Category.findOne()
6359 .then(function(cat) {
6360 return cat.jobs.findById(job3.id);
6361 })
6362 .then(function(p) {
6363 p.id.should.eql(job3.id);
6364 p.name.should.equal('Job 3');
6365 done();
6366 })
6367 .catch(done);
6368 });
6369
6370 it('should find items on scope with promises - filter', function(done) {
6371 Category.findOne()
6372 .then(function(cat) {
6373 const filter = {where: {name: 'Job 1'}};
6374 return cat.jobs.find(filter);
6375 })
6376 .then(function(jobs) {
6377 jobs.should.have.length(1);
6378 const p = jobs[0];
6379 p.id.should.eql(job1.id);
6380 p.name.should.equal('Job 1');
6381 done();
6382 })
6383 .catch(done);
6384 });
6385
6386 it('should remove items from scope with promises', function(done) {
6387 Category.findOne()
6388 .then(function(cat) {
6389 return cat.jobs.remove(job1.id)
6390 .then(function(ids) {
6391 const expected = [job2.id, job3.id];
6392 cat.jobIds.should.have.lengthOf(expected.length);
6393 cat.jobIds.should.containDeep(expected);
6394 cat.jobIds.should.eql(ids);
6395 done();
6396 });
6397 })
6398 .catch(done);
6399 });
6400
6401 it('should find items on scope with promises - verify', function(done) {
6402 Category.findOne()
6403 .then(function(cat) {
6404 const expected = [job2.id, job3.id];
6405 cat.jobIds.should.have.lengthOf(expected.length);
6406 cat.jobIds.should.containDeep(expected);
6407 return cat.jobs.find();
6408 })
6409 .then(function(jobs) {
6410 jobs.should.have.length(2);
6411 jobs[0].id.should.eql(job2.id);
6412 jobs[1].id.should.eql(job3.id);
6413 done();
6414 })
6415 .catch(done);
6416 });
6417
6418 bdd.itIf(connectorCapabilities.adhocSort !== false,
6419 'should find items on scope and ordered them by name DESC', function(done) {
6420 Category.find()
6421 .then(function(categories) {
6422 categories.should.have.length(1);
6423 return categories[0].jobs.find({order: 'name DESC'});
6424 })
6425 .then(function(jobs) {
6426 jobs.should.have.length(2);
6427 jobs[0].id.should.eql(job3.id);
6428 jobs[1].id.should.eql(job2.id);
6429 done();
6430 })
6431 .catch(done);
6432 });
6433
6434 bdd.itIf(connectorCapabilities.adhocSort !== false,
6435 'should allow custom scope methods with promises - reverse', function(done) {
6436 Category.findOne()
6437 .then(function(cat) {
6438 return cat.jobs.reverse()
6439 .then(function(ids) {
6440 const expected = [job3.id, job2.id];
6441 ids.toArray().should.eql(expected);
6442 cat.jobIds.toArray().should.eql(expected);
6443 done();
6444 });
6445 })
6446 .catch(done);
6447 });
6448
6449 bdd.itIf(connectorCapabilities.adhocSort === true &&
6450 connectorCapabilities.supportInclude === true,
6451 'should include related items from scope with promises', function(done) {
6452 Category.find({include: 'jobs'})
6453 .then(function(categories) {
6454 categories.should.have.length(1);
6455 const cat = categories[0].toObject();
6456 cat.name.should.equal('Category A');
6457 cat.jobs.should.have.length(2);
6458 cat.jobs[0].id.should.eql(job3.id);
6459 cat.jobs[1].id.should.eql(job2.id);
6460 done();
6461 }).catch(done);
6462 });
6463
6464 it('should destroy items from scope with promises - destroyById', function(done) {
6465 Category.findOne()
6466 .then(function(cat) {
6467 return cat.jobs.destroy(job2.id)
6468 .then(function() {
6469 const expected = [job3.id];
6470 if (connectorCapabilities.adhocSort !== false) {
6471 cat.jobIds.toArray().should.eql(expected);
6472 } else {
6473 cat.jobIds.toArray().should.containDeep(expected);
6474 }
6475 return Job.exists(job2.id);
6476 })
6477 .then(function(exists) {
6478 should.exist(exists);
6479 exists.should.be.false;
6480 done();
6481 });
6482 })
6483 .catch(done);
6484 });
6485
6486 // eslint-disable-next-line mocha/no-identical-title
6487 it('should find items on scope with promises - verify', function(done) {
6488 Category.findOne()
6489 .then(function(cat) {
6490 const expected = [job3.id];
6491 cat.jobIds.should.have.lengthOf(expected.length);
6492 cat.jobIds.should.containDeep(expected);
6493 return cat.jobs.find();
6494 })
6495 .then(function(jobs) {
6496 jobs.should.have.length(1);
6497 jobs[0].id.should.eql(job3.id);
6498 done();
6499 })
6500 .catch(done);
6501 });
6502 });
6503
6504 describe('custom relation/scope methods', function() {
6505 let categoryId;
6506
6507 before(function(done) {
6508 Category = db.define('Category', {name: String});
6509 Job = db.define('Job', {name: String});
6510
6511 db.automigrate(['Job', 'Category'], done);
6512 });
6513
6514 it('can be declared', function(done) {
6515 const relation = Category.hasMany(Job);
6516
6517 const summarize = function(cb) {
6518 cb = cb || createPromiseCallback();
6519 const modelInstance = this.modelInstance;
6520 this.fetch(function(err, items) {
6521 if (err) return cb(err, []);
6522 const summary = items.map(function(item) {
6523 const obj = item.toObject();
6524 obj.categoryName = modelInstance.name;
6525 return obj;
6526 });
6527 cb(null, summary);
6528 });
6529 return cb.promise;
6530 };
6531
6532 summarize.shared = true; // remoting
6533 summarize.http = {verb: 'get', path: '/jobs/summary'};
6534
6535 relation.defineMethod('summarize', summarize);
6536
6537 Category.prototype['__summarize__jobs'].should.be.a.function;
6538 should.exist(Category.prototype['__summarize__jobs'].shared);
6539 Category.prototype['__summarize__jobs'].http.should.eql(summarize.http);
6540
6541 db.automigrate(['Job', 'Category'], done);
6542 });
6543
6544 it('should setup test records', function(done) {
6545 Category.create({name: 'Category A'}, function(err, cat) {
6546 categoryId = cat.id;
6547 cat.jobs.create({name: 'Job 1'}, function(err, p) {
6548 cat.jobs.create({name: 'Job 2'}, function(err, p) {
6549 done();
6550 });
6551 });
6552 });
6553 });
6554
6555 it('should allow custom scope methods - summarize', function(done) {
6556 const categoryIdStr = categoryId.toString();
6557 const expected = [
6558 {name: 'Job 1', categoryId: categoryIdStr, categoryName: 'Category A'},
6559 {name: 'Job 2', categoryId: categoryIdStr, categoryName: 'Category A'},
6560 ];
6561
6562 Category.findOne(function(err, cat) {
6563 cat.jobs.summarize(function(err, summary) {
6564 if (err) return done(err);
6565 const result = summary.map(function(item) {
6566 delete item.id;
6567 item.categoryId = item.categoryId.toString();
6568 return item;
6569 });
6570 // order-independent match
6571 result.should.containDeep(expected);
6572 expected.should.containDeep(result);
6573 done();
6574 });
6575 });
6576 });
6577
6578 it('should allow custom scope methods with promises - summarize', function(done) {
6579 const categoryIdStr = categoryId.toString();
6580 const expected = [
6581 {name: 'Job 1', categoryId: categoryIdStr, categoryName: 'Category A'},
6582 {name: 'Job 2', categoryId: categoryIdStr, categoryName: 'Category A'},
6583 ];
6584
6585 Category.findOne()
6586 .then(function(cat) {
6587 return cat.jobs.summarize();
6588 })
6589 .then(function(summary) {
6590 const result = summary.map(function(item) {
6591 delete item.id;
6592 item.categoryId = item.categoryId.toString();
6593 return item;
6594 });
6595 // order-independent match
6596 result.should.containDeep(expected);
6597 expected.should.containDeep(result);
6598 done();
6599 })
6600 .catch(done);
6601 });
6602 });
6603
6604 describe('relation names', function() {
6605 it('throws error when a relation name is `trigger`', function() {
6606 Chapter = db.define('Chapter', {name: String});
6607
6608 (function() {
6609 db.define(
6610 'Book',
6611 {name: String},
6612 {
6613 relations: {
6614 trigger: {
6615 model: 'Chapter',
6616 type: 'hasMany',
6617 },
6618 },
6619 },
6620 );
6621 }).should.throw('Invalid relation name: trigger');
6622 });
6623 });
6624
6625 describe('polymorphic hasMany - revert', function() {
6626 before(function(done) {
6627 Picture = db.define('Picture', {name: String});
6628 Author = db.define('Author', {name: String});
6629 PictureLink = db.define('PictureLink', {});
6630 Author.hasMany(Picture, {through: PictureLink, polymorphic: 'imageable', invert: true});
6631 Picture.hasMany(Author, {through: PictureLink, polymorphic: 'imageable'});
6632 db.automigrate(['Picture', 'Author', 'PictureLink'], done);
6633 });
6634 it('should properly query through an inverted relationship', function(done) {
6635 Author.create({name: 'Steve'}, function(err, author) {
6636 if (err) {
6637 return done(err);
6638 }
6639 author.pictures.create({name: 'Steve pic 1'}, function(err, pic) {
6640 if (err) {
6641 return done(err);
6642 }
6643 Author.findOne({include: 'pictures'}, function(err, author) {
6644 if (err) {
6645 return done(err);
6646 }
6647 author.pictures().length.should.eql(1);
6648 author.pictures()[0].name.should.eql('Steve pic 1');
6649 done();
6650 });
6651 });
6652 });
6653 });
6654 });
6655});