UNPKG

60 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'use strict';
7
8/* global getSchema:false, connectorCapabilities:false */
9const assert = require('assert');
10const async = require('async');
11const bdd = require('./helpers/bdd-if');
12const should = require('./init.js');
13
14const DataSource = require('../').DataSource;
15
16let db, User, Profile, AccessToken, Post, Passport, City, Street, Building, Assembly, Part;
17
18const knownUsers = ['User A', 'User B', 'User C', 'User D', 'User E'];
19const knownPassports = ['1', '2', '3', '4'];
20const knownPosts = ['Post A', 'Post B', 'Post C', 'Post D', 'Post E'];
21const knownProfiles = ['Profile A', 'Profile B', 'Profile Z'];
22
23describe('include', function() {
24 before(setup);
25
26 it('should fetch belongsTo relation', function(done) {
27 Passport.find({include: 'owner'}, function(err, passports) {
28 passports.length.should.be.ok;
29 passports.forEach(function(p) {
30 p.__cachedRelations.should.have.property('owner');
31
32 // The relation should be promoted as the 'owner' property
33 p.should.have.property('owner');
34 // The __cachedRelations should be removed from json output
35 p.toJSON().should.not.have.property('__cachedRelations');
36
37 const owner = p.__cachedRelations.owner;
38 if (!p.ownerId) {
39 should.not.exist(owner);
40 } else {
41 should.exist(owner);
42 owner.id.should.eql(p.ownerId);
43 }
44 });
45 done();
46 });
47 });
48
49 it('does not return included item if FK is excluded', function(done) {
50 Passport.find({include: 'owner', fields: 'number'}, function(err, passports) {
51 if (err) return done(err);
52 const owner = passports[0].toJSON().owner;
53 should.not.exist(owner);
54 done();
55 });
56 });
57
58 it('should fetch hasMany relation', function(done) {
59 User.find({include: 'posts'}, function(err, users) {
60 should.not.exist(err);
61 should.exist(users);
62 users.length.should.be.ok;
63 users.forEach(function(u) {
64 // The relation should be promoted as the 'owner' property
65 u.should.have.property('posts');
66 // The __cachedRelations should be removed from json output
67 u.toJSON().should.not.have.property('__cachedRelations');
68
69 u.__cachedRelations.should.have.property('posts');
70 u.__cachedRelations.posts.forEach(function(p) {
71 // FIXME There are cases that p.userId is string
72 p.userId.toString().should.eql(u.id.toString());
73 });
74 });
75 done();
76 });
77 });
78
79 it('should report errors if the PK is excluded', function(done) {
80 User.find({include: 'posts', fields: 'name'}, function(err) {
81 should.exist(err);
82 err.message.should.match(/ID property "id" is missing/);
83 done();
84 });
85 });
86
87 it('should not have changed the __strict flag of the model', function(done) {
88 const originalStrict = User.definition.settings.strict;
89 User.definition.settings.strict = true; // Change to test regression for issue #1252
90 const finish = (err) => {
91 // Restore original user strict property
92 User.definition.settings.strict = originalStrict;
93 done(err);
94 };
95 User.find({include: 'posts'}, function(err, users) {
96 if (err) return finish(err);
97 users.forEach(user => {
98 user.should.have.property('__strict', true); // we changed it
99 });
100 finish();
101 });
102 });
103
104 bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
105 'should not save in db included models, in query returned models',
106 function(done) {
107 const originalStrict = User.definition.settings.strict;
108 User.definition.settings.strict = true; // Change to test regression for issue #1252
109 const finish = (err) => {
110 // Restore original user strict property
111 User.definition.settings.strict = originalStrict;
112 done(err);
113 };
114 User.findOne({where: {name: 'User A'}, include: 'posts'}, function(err, user) {
115 if (err) return finish(err);
116 if (!user) return finish(new Error('User Not found to check relation not saved'));
117 user.save(function(err) { // save the returned user
118 if (err) return finish(err);
119 // should not store in db the posts
120 const dsName = User.dataSource.name;
121 if (dsName === 'memory') {
122 JSON.parse(User.dataSource.adapter.cache.User[1]).should.not.have.property('posts');
123 finish();
124 } else if (dsName === 'mongodb') { // Check native mongodb connector
125 // get hold of native mongodb collection
126 const dbCollection = User.dataSource.connector.collection(User.modelName);
127 dbCollection.findOne({_id: user.id})
128 .then(function(foundUser) {
129 if (!foundUser) {
130 finish(new Error('User not found to check posts not saved'));
131 }
132 foundUser.should.not.have.property('posts');
133 finish();
134 })
135 .catch(finish);
136 } else { // TODO make native checks for other connectors as well
137 finish();
138 }
139 });
140 });
141 });
142
143 it('should fetch Passport - Owner - Posts', function(done) {
144 Passport.find({include: {owner: 'posts'}}, function(err, passports) {
145 should.not.exist(err);
146 should.exist(passports);
147 passports.length.should.be.ok;
148 passports.forEach(function(p) {
149 p.__cachedRelations.should.have.property('owner');
150
151 // The relation should be promoted as the 'owner' property
152 p.should.have.property('owner');
153 // The __cachedRelations should be removed from json output
154 p.toJSON().should.not.have.property('__cachedRelations');
155
156 const user = p.__cachedRelations.owner;
157 if (!p.ownerId) {
158 should.not.exist(user);
159 } else {
160 should.exist(user);
161 user.id.should.eql(p.ownerId);
162 user.__cachedRelations.should.have.property('posts');
163 user.should.have.property('posts');
164 user.toJSON().should.have.property('posts').and.be.an.Array;
165 user.__cachedRelations.posts.forEach(function(pp) {
166 // FIXME There are cases that pp.userId is string
167 pp.userId.toString().should.eql(user.id.toString());
168 });
169 }
170 });
171 done();
172 });
173 });
174
175 it('should fetch Passport - Owner - empty Posts', function(done) {
176 Passport.findOne({where: {number: '4'}, include: {owner: 'posts'}}, function(err, passport) {
177 should.not.exist(err);
178 should.exist(passport);
179 passport.__cachedRelations.should.have.property('owner');
180
181 // The relation should be promoted as the 'owner' property
182 passport.should.have.property('owner');
183 // The __cachedRelations should be removed from json output
184 passport.toJSON().should.not.have.property('__cachedRelations');
185
186 const user = passport.__cachedRelations.owner;
187 should.exist(user);
188 user.id.should.eql(passport.ownerId);
189 user.__cachedRelations.should.have.property('posts');
190 user.should.have.property('posts');
191 user.toJSON().should.have.property('posts').and.be.an.Array().with
192 .length(0);
193 done();
194 });
195 });
196
197 it('should fetch Passport - Owner - Posts - alternate syntax', function(done) {
198 Passport.find({include: {owner: {relation: 'posts'}}}, function(err, passports) {
199 should.not.exist(err);
200 should.exist(passports);
201 passports.length.should.be.ok;
202 let posts;
203 if (connectorCapabilities.adhocSort !== false) {
204 posts = passports[0].owner().posts();
205 posts.should.have.length(3);
206 } else {
207 if (passports[0].owner()) {
208 posts = passports[0].owner().posts();
209 posts.length.should.be.belowOrEqual(3);
210 }
211 }
212 done();
213 });
214 });
215
216 it('should fetch Passports - User - Posts - User', function(done) {
217 Passport.find({
218 include: {owner: {posts: 'author'}},
219 }, function(err, passports) {
220 should.not.exist(err);
221 should.exist(passports);
222 passports.length.should.be.ok;
223 passports.forEach(function(p) {
224 p.__cachedRelations.should.have.property('owner');
225 const user = p.__cachedRelations.owner;
226 if (!p.ownerId) {
227 should.not.exist(user);
228 } else {
229 should.exist(user);
230 user.id.should.eql(p.ownerId);
231 user.__cachedRelations.should.have.property('posts');
232 user.__cachedRelations.posts.forEach(function(pp) {
233 pp.should.have.property('id');
234 // FIXME There are cases that pp.userId is string
235 pp.userId.toString().should.eql(user.id.toString());
236 pp.should.have.property('author');
237 pp.__cachedRelations.should.have.property('author');
238 const author = pp.__cachedRelations.author;
239 author.id.should.eql(user.id);
240 });
241 }
242 });
243 done();
244 });
245 });
246
247 it('should fetch Passports with include scope on Posts', function(done) {
248 Passport.find({
249 include: {owner: {relation: 'posts', scope: {
250 fields: ['title'], include: ['author'],
251 order: 'title DESC',
252 }}},
253 }, function(err, passports) {
254 should.not.exist(err);
255 should.exist(passports);
256 let passport, owner, posts;
257 if (connectorCapabilities.adhocSort !== false) {
258 passports.length.should.equal(4);
259
260 passport = passports[0];
261 passport.number.should.equal('1');
262 passport.owner().name.should.equal('User A');
263 owner = passport.owner().toObject();
264
265 posts = passport.owner().posts();
266 posts.should.be.an.array;
267 posts.should.have.length(3);
268
269 posts[0].title.should.equal('Post C');
270 posts[0].should.have.property('id', undefined); // omitted
271 posts[0].author().should.be.instanceOf(User);
272 posts[0].author().name.should.equal('User A');
273
274 posts[1].title.should.equal('Post B');
275 posts[1].author().name.should.equal('User A');
276
277 posts[2].title.should.equal('Post A');
278 posts[2].author().name.should.equal('User A');
279 } else {
280 passports.length.should.be.belowOrEqual(4);
281
282 passport = passports[0];
283 passport.number.should.be.oneOf(knownPassports);
284 if (passport.owner()) {
285 passport.owner().name.should.be.oneOf(knownUsers);
286 owner = passport.owner().toObject();
287
288 posts = passport.owner().posts();
289 posts.should.be.an.array;
290 posts.length.should.be.belowOrEqual(3);
291
292 if (posts[0]) {
293 posts[0].title.should.be.oneOf(knownPosts);
294 posts[0].author().should.be.instanceOf(User);
295 posts[0].author().name.should.be.oneOf(knownUsers);
296 }
297 }
298 }
299
300 done();
301 });
302 });
303
304 bdd.itIf(connectorCapabilities.adhocSort !== false,
305 'should support limit', function(done) {
306 Passport.find({
307 include: {
308 owner: {
309 relation: 'posts', scope: {
310 fields: ['title'], include: ['author'],
311 order: 'title DESC',
312 limit: 1,
313 },
314 },
315 },
316 limit: 2,
317 }, function(err, passports) {
318 if (err) return done(err);
319 passports.length.should.equal(2);
320 const posts1 = passports[0].toJSON().owner.posts;
321 posts1.length.should.equal(1);
322 posts1[0].title.should.equal('Post C');
323 const posts2 = passports[1].toJSON().owner.posts;
324 posts2.length.should.equal(1);
325 posts2[0].title.should.equal('Post D');
326
327 done();
328 });
329 });
330
331 bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
332 'should support limit - no sort', function(done) {
333 Passport.find({
334 include: {
335 owner: {
336 relation: 'posts', scope: {
337 fields: ['title'], include: ['author'],
338 order: 'title DESC',
339 limit: 1,
340 },
341 },
342 },
343 limit: 2,
344 }, function(err, passports) {
345 if (err) return done(err);
346 passports.length.should.equal(2);
347 let owner = passports[0].toJSON().owner;
348 if (owner) {
349 const posts1 = owner.posts;
350 posts1.length.should.belowOrEqual(1);
351 if (posts1.length === 1) {
352 posts1[0].title.should.be.oneOf(knownPosts);
353 }
354 }
355 owner = passports[1].toJSON().owner;
356 if (owner) {
357 const posts2 = owner.posts;
358 posts2.length.should.belowOrEqual(1);
359 if (posts2.length === 1) {
360 posts2[0].title.should.be.oneOf(knownPosts);
361 }
362 }
363 done();
364 });
365 });
366
367 bdd.describeIf(connectorCapabilities.adhocSort !== false,
368 'inq limit', function() {
369 before(function() {
370 Passport.dataSource.settings.inqLimit = 2;
371 });
372
373 after(function() {
374 delete Passport.dataSource.settings.inqLimit;
375 });
376
377 it('should support include by pagination', function(done) {
378 // `pagination` in this case is inside the implementation and set by
379 // `inqLimit = 2` in the before block. This will need to be reworked once
380 // we decouple `findWithForeignKeysByPage`.
381 //
382 // --superkhau
383 Passport.find({
384 include: {
385 owner: {
386 relation: 'posts',
387 scope: {
388 fields: ['title'], include: ['author'],
389 order: 'title ASC',
390 },
391 },
392 },
393 }, function(err, passports) {
394 if (err) return done(err);
395
396 passports.length.should.equal(4);
397 const posts1 = passports[0].toJSON().owner.posts;
398 posts1.length.should.equal(3);
399 posts1[0].title.should.equal('Post A');
400 const posts2 = passports[1].toJSON().owner.posts;
401 posts2.length.should.equal(1);
402 posts2[0].title.should.equal('Post D');
403
404 done();
405 });
406 });
407 });
408
409 bdd.describeIf(connectorCapabilities.adhocSort !== false,
410 'findWithForeignKeysByPage', function() {
411 context('filter', function() {
412 it('works when using a `where` with a foreign key', function(done) {
413 User.findOne({
414 include: {
415 relation: 'passports',
416 },
417 }, function(err, user) {
418 if (err) return done(err);
419
420 const passport = user.passports()[0];
421 // eql instead of equal because mongo uses object id type
422 passport.id.should.eql(createdPassports[0].id);
423 passport.ownerId.should.eql(createdPassports[0].ownerId);
424 passport.number.should.eql(createdPassports[0].number);
425
426 done();
427 });
428 });
429
430 it('works when using a `where` with `and`', function(done) {
431 User.findOne({
432 include: {
433 relation: 'posts',
434 scope: {
435 where: {
436 and: [
437 {id: createdPosts[0].id},
438 // Remove the duplicate userId to avoid Cassandra failure
439 // {userId: createdPosts[0].userId},
440 {title: 'Post A'},
441 ],
442 },
443 },
444 },
445 }, function(err, user) {
446 if (err) return done(err);
447
448 user.name.should.equal('User A');
449 user.age.should.equal(21);
450 user.id.should.eql(createdUsers[0].id);
451 const posts = user.posts();
452 posts.length.should.equal(1);
453 const post = posts[0];
454 post.title.should.equal('Post A');
455 // eql instead of equal because mongo uses object id type
456 post.userId.should.eql(createdPosts[0].userId);
457 post.id.should.eql(createdPosts[0].id);
458
459 done();
460 });
461 });
462
463 it('works when using `where` with `limit`', function(done) {
464 User.findOne({
465 include: {
466 relation: 'posts',
467 scope: {
468 limit: 1,
469 },
470 },
471 }, function(err, user) {
472 if (err) return done(err);
473
474 user.posts().length.should.equal(1);
475
476 done();
477 });
478 });
479
480 it('works when using `where` with `skip`', function(done) {
481 User.findOne({
482 include: {
483 relation: 'posts',
484 scope: {
485 skip: 1,
486 },
487 },
488 }, function(err, user) {
489 if (err) return done(err);
490
491 const ids = user.posts().map(function(p) { return p.id; });
492 ids.should.eql([createdPosts[1].id, createdPosts[2].id]);
493
494 done();
495 });
496 });
497
498 it('works when using `where` with `offset`', function(done) {
499 User.findOne({
500 include: {
501 relation: 'posts',
502 scope: {
503 offset: 1,
504 },
505 },
506 }, function(err, user) {
507 if (err) return done(err);
508
509 const ids = user.posts().map(function(p) { return p.id; });
510 ids.should.eql([createdPosts[1].id, createdPosts[2].id]);
511
512 done();
513 });
514 });
515
516 it('works when using `where` without `limit`, `skip` or `offset`',
517 function(done) {
518 User.findOne({include: {relation: 'posts'}}, function(err, user) {
519 if (err) return done(err);
520
521 const posts = user.posts();
522 const ids = posts.map(function(p) { return p.id; });
523 ids.should.eql([
524 createdPosts[0].id,
525 createdPosts[1].id,
526 createdPosts[2].id,
527 ]);
528
529 done();
530 });
531 });
532 });
533
534 context('pagination', function() {
535 it('works with the default page size (0) and `inqlimit` is exceeded',
536 function(done) {
537 // inqLimit modifies page size in the impl (there is no way to modify
538 // page size directly as it is hardcoded (once we decouple the func,
539 // we can use ctor injection to pass in whatever page size we want).
540 //
541 // --superkhau
542 Post.dataSource.settings.inqLimit = 2;
543
544 User.find({include: {relation: 'posts'}}, function(err, users) {
545 if (err) return done(err);
546
547 users.length.should.equal(5);
548
549 delete Post.dataSource.settings.inqLimit;
550
551 done();
552 });
553 });
554
555 it('works when page size is set to 0', function(done) {
556 Post.dataSource.settings.inqLimit = 0;
557
558 User.find({include: {relation: 'posts'}}, function(err, users) {
559 if (err) return done(err);
560
561 users.length.should.equal(5);
562
563 delete Post.dataSource.settings.inqLimit;
564
565 done();
566 });
567 });
568 });
569
570 context('relations', function() {
571 // WARNING
572 // The code paths for in this suite of tests were verified manually due to
573 // the tight coupling of the `findWithForeignKeys` in `include.js`.
574 //
575 // TODO
576 // Decouple the utility functions into their own modules and export each
577 // function individually to allow for unit testing via DI.
578 //
579 // --superkhau
580
581 it('works when hasOne is called', function(done) {
582 User.findOne({include: {relation: 'profile'}}, function(err, user) {
583 if (err) return done(err);
584
585 user.name.should.equal('User A');
586 user.age.should.equal(21);
587 // eql instead of equal because mongo uses object id type
588 user.id.should.eql(createdUsers[0].id);
589 const profile = user.profile();
590 profile.profileName.should.equal('Profile A');
591 // eql instead of equal because mongo uses object id type
592 profile.userId.should.eql(createdProfiles[0].userId);
593 profile.id.should.eql(createdProfiles[0].id);
594
595 done();
596 });
597 });
598
599 it('does not return included item if hasOne is missing the id property', function(done) {
600 User.findOne({include: {relation: 'profile'}, fields: 'name'}, function(err, user) {
601 if (err) return done(err);
602 should.exist(user);
603 // Convert to JSON as the user instance has `profile` as a relational method
604 should.not.exist(user.toJSON().profile);
605 done();
606 });
607 });
608
609 it('works when hasMany is called', function(done) {
610 User.findOne({include: {relation: 'posts'}}, function(err, user) {
611 if (err) return done();
612
613 user.name.should.equal('User A');
614 user.age.should.equal(21);
615 // eql instead of equal because mongo uses object id type
616 user.id.should.eql(createdUsers[0].id);
617 user.posts().length.should.equal(3);
618
619 done();
620 });
621 });
622
623 it('works when hasManyThrough is called', function(done) {
624 const Physician = db.define('Physician', {name: String});
625 const Patient = db.define('Patient', {name: String});
626 const Appointment = db.define('Appointment', {
627 date: {
628 type: Date,
629 default: function() {
630 return new Date();
631 },
632 },
633 });
634 const Address = db.define('Address', {name: String});
635
636 Physician.hasMany(Patient, {through: Appointment});
637 Patient.hasMany(Physician, {through: Appointment});
638 Patient.belongsTo(Address);
639 Appointment.belongsTo(Patient);
640 Appointment.belongsTo(Physician);
641
642 db.automigrate(['Physician', 'Patient', 'Appointment', 'Address'],
643 function() {
644 Physician.create(function(err, physician) {
645 physician.patients.create({name: 'a'}, function(err, patient) {
646 Address.create({name: 'z'}, function(err, address) {
647 patient.address(address);
648 patient.save(function() {
649 physician.patients({include: 'address'},
650 function(err, patients) {
651 if (err) return done(err);
652
653 patients.should.have.length(1);
654 const p = patients[0];
655 p.name.should.equal('a');
656 p.addressId.should.eql(patient.addressId);
657 p.address().id.should.eql(address.id);
658 p.address().name.should.equal('z');
659
660 done();
661 });
662 });
663 });
664 });
665 });
666 });
667 });
668
669 it('works when belongsTo is called', function(done) {
670 Profile.findOne({include: 'user'}, function(err, profile) {
671 if (err) return done(err);
672
673 profile.profileName.should.equal('Profile A');
674 profile.userId.should.eql(createdProfiles[0].userId);
675 profile.id.should.eql(createdProfiles[0].id);
676 const user = profile.user();
677 user.name.should.equal('User A');
678 user.age.should.equal(21);
679 user.id.should.eql(createdUsers[0].id);
680
681 done();
682 });
683 });
684 });
685 });
686
687 bdd.describeIf(connectorCapabilities.adhocSort === false,
688 'findWithForeignKeysByPage', function() {
689 // eslint-disable-next-line mocha/no-identical-title
690 context('filter', function() {
691 it('works when using a `where` with a foreign key', function(done) {
692 User.findOne({
693 include: {
694 relation: 'passports',
695 },
696 }, function(err, user) {
697 if (err) return done(err);
698
699 const passport = user.passports()[0];
700 if (passport) {
701 const knownPassportIds = [];
702 const knownOwnerIds = [];
703 createdPassports.forEach(function(p) {
704 if (p.id) knownPassportIds.push(p.id);
705 if (p.ownerId) knownOwnerIds.push(p.ownerId.toString());
706 });
707 passport.id.should.be.oneOf(knownPassportIds);
708 // FIXME passport.ownerId may be string
709 passport.ownerId.toString().should.be.oneOf(knownOwnerIds);
710 passport.number.should.be.oneOf(knownPassports);
711 }
712 done();
713 });
714 });
715
716 it('works when using a `where` with `and`', function(done) {
717 User.findOne({
718 include: {
719 relation: 'posts',
720 scope: {
721 where: {
722 and: [
723 {id: createdPosts[0].id},
724 // Remove the duplicate userId to avoid Cassandra failure
725 // {userId: createdPosts[0].userId},
726 {title: createdPosts[0].title},
727 ],
728 },
729 },
730 },
731 }, function(err, user) {
732 if (err) return done(err);
733
734 let posts, post;
735 if (connectorCapabilities.adhocSort !== false) {
736 user.name.should.equal('User A');
737 user.age.should.equal(21);
738 user.id.should.eql(createdUsers[0].id);
739 posts = user.posts();
740 posts.length.should.equal(1);
741 post = posts[0];
742 post.title.should.equal('Post A');
743 // eql instead of equal because mongo uses object id type
744 post.userId.should.eql(createdPosts[0].userId);
745 post.id.should.eql(createdPosts[0].id);
746 } else {
747 user.name.should.be.oneOf(knownUsers);
748 const knownUserIds = [];
749 createdUsers.forEach(function(u) {
750 knownUserIds.push(u.id.toString());
751 });
752 user.id.toString().should.be.oneOf(knownUserIds);
753 posts = user.posts();
754 if (posts && posts.length > 0) {
755 post = posts[0];
756 post.title.should.be.oneOf(knownPosts);
757 post.userId.toString().should.be.oneOf(knownUserIds);
758 const knownPostIds = [];
759 createdPosts.forEach(function(p) {
760 knownPostIds.push(p.id);
761 });
762 post.id.should.be.oneOf(knownPostIds);
763 }
764 }
765 done();
766 });
767 });
768
769 it('works when using `where` with `limit`', function(done) {
770 User.findOne({
771 include: {
772 relation: 'posts',
773 scope: {
774 limit: 1,
775 },
776 },
777 }, function(err, user) {
778 if (err) return done(err);
779
780 user.posts().length.should.belowOrEqual(1);
781
782 done();
783 });
784 });
785
786 it('works when using `where` with `skip`', function(done) {
787 User.findOne({
788 include: {
789 relation: 'posts',
790 scope: {
791 skip: 1, // will be ignored
792 },
793 },
794 }, function(err, user) {
795 if (err) return done(err);
796
797 const ids = user.posts().map(function(p) { return p.id; });
798 if (ids.length > 0) {
799 const knownPosts = [];
800 createdPosts.forEach(function(p) {
801 if (p.id) knownPosts.push(p.id);
802 });
803 ids.forEach(function(id) {
804 if (id) id.should.be.oneOf(knownPosts);
805 });
806 }
807
808 done();
809 });
810 });
811
812 it('works when using `where` with `offset`', function(done) {
813 User.findOne({
814 include: {
815 relation: 'posts',
816 scope: {
817 offset: 1, // will be ignored
818 },
819 },
820 }, function(err, user) {
821 if (err) return done(err);
822
823 const ids = user.posts().map(function(p) { return p.id; });
824 if (ids.length > 0) {
825 const knownPosts = [];
826 createdPosts.forEach(function(p) {
827 if (p.id) knownPosts.push(p.id);
828 });
829 ids.forEach(function(id) {
830 if (id) id.should.be.oneOf(knownPosts);
831 });
832 }
833
834 done();
835 });
836 });
837
838 it('works when using `where` without `limit`, `skip` or `offset`',
839 function(done) {
840 User.findOne({include: {relation: 'posts'}}, function(err, user) {
841 if (err) return done(err);
842
843 const posts = user.posts();
844 const ids = posts.map(function(p) { return p.id; });
845 if (ids.length > 0) {
846 const knownPosts = [];
847 createdPosts.forEach(function(p) {
848 if (p.id) knownPosts.push(p.id);
849 });
850 ids.forEach(function(id) {
851 if (id) id.should.be.oneOf(knownPosts);
852 });
853 }
854
855 done();
856 });
857 });
858 });
859
860 // eslint-disable-next-line mocha/no-identical-title
861 context('pagination', function() {
862 it('works with the default page size (0) and `inqlimit` is exceeded',
863 function(done) {
864 // inqLimit modifies page size in the impl (there is no way to modify
865 // page size directly as it is hardcoded (once we decouple the func,
866 // we can use ctor injection to pass in whatever page size we want).
867 //
868 // --superkhau
869 Post.dataSource.settings.inqLimit = 2;
870
871 User.find({include: {relation: 'posts'}}, function(err, users) {
872 if (err) return done(err);
873
874 users.length.should.equal(5);
875
876 delete Post.dataSource.settings.inqLimit;
877
878 done();
879 });
880 });
881
882 it('works when page size is set to 0', function(done) {
883 Post.dataSource.settings.inqLimit = 0;
884
885 User.find({include: {relation: 'posts'}}, function(err, users) {
886 if (err) return done(err);
887
888 users.length.should.equal(5);
889
890 delete Post.dataSource.settings.inqLimit;
891
892 done();
893 });
894 });
895 });
896
897 // eslint-disable-next-line mocha/no-identical-title
898 context('relations', function() {
899 // WARNING
900 // The code paths for in this suite of tests were verified manually due to
901 // the tight coupling of the `findWithForeignKeys` in `include.js`.
902 //
903 // TODO
904 // Decouple the utility functions into their own modules and export each
905 // function individually to allow for unit testing via DI.
906 //
907 // --superkhau
908
909 it('works when hasOne is called', function(done) {
910 User.findOne({include: {relation: 'profile'}}, function(err, user) {
911 if (err) return done(err);
912
913 const knownUserIds = [];
914 const knownProfileIds = [];
915 createdUsers.forEach(function(u) {
916 // FIXME user.id below might be string, so knownUserIds should match
917 knownUserIds.push(u.id.toString());
918 });
919 createdProfiles.forEach(function(p) {
920 // knownProfileIds.push(p.id ? p.id.toString() : '');
921 knownProfileIds.push(p.id);
922 });
923 if (user) {
924 user.name.should.be.oneOf(knownUsers);
925 // eql instead of equal because mongo uses object id type
926 user.id.toString().should.be.oneOf(knownUserIds);
927 const profile = user.profile();
928 if (profile) {
929 profile.profileName.should.be.oneOf(knownProfiles);
930 // eql instead of equal because mongo uses object id type
931 if (profile.userId) profile.userId.toString().should.be.oneOf(knownUserIds);
932 profile.id.should.be.oneOf(knownProfileIds);
933 }
934 }
935
936 done();
937 });
938 });
939
940 it('works when hasMany is called', function(done) {
941 User.findOne({include: {relation: 'posts'}}, function(err, user) {
942 if (err) return done();
943
944 const knownUserIds = [];
945 createdUsers.forEach(function(u) {
946 knownUserIds.push(u.id);
947 });
948 user.name.should.be.oneOf(knownUsers);
949 // eql instead of equal because mongo uses object id type
950 user.id.should.be.oneOf(knownUserIds);
951 user.posts().length.should.be.belowOrEqual(3);
952
953 done();
954 });
955 });
956
957 it('works when hasManyThrough is called', function(done) {
958 const Physician = db.define('Physician', {name: String});
959 const Patient = db.define('Patient', {name: String});
960 const Appointment = db.define('Appointment', {
961 date: {
962 type: Date,
963 default: function() {
964 return new Date();
965 },
966 },
967 });
968 const Address = db.define('Address', {name: String});
969
970 Physician.hasMany(Patient, {through: Appointment});
971 Patient.hasMany(Physician, {through: Appointment});
972 Patient.belongsTo(Address);
973 Appointment.belongsTo(Patient);
974 Appointment.belongsTo(Physician);
975
976 db.automigrate(['Physician', 'Patient', 'Appointment', 'Address'],
977 function() {
978 Physician.create(function(err, physician) {
979 physician.patients.create({name: 'a'}, function(err, patient) {
980 Address.create({name: 'z'}, function(err, address) {
981 patient.address(address);
982 patient.save(function() {
983 physician.patients({include: 'address'},
984 function(err, patients) {
985 if (err) return done(err);
986 patients.should.have.length(1);
987 const p = patients[0];
988 p.name.should.equal('a');
989 p.addressId.should.eql(patient.addressId);
990 p.address().id.should.eql(address.id);
991 p.address().name.should.equal('z');
992
993 done();
994 });
995 });
996 });
997 });
998 });
999 });
1000 });
1001
1002 it('works when belongsTo is called', function(done) {
1003 Profile.findOne({include: 'user'}, function(err, profile) {
1004 if (err) return done(err);
1005 if (!profile) return done(); // not every user has progile
1006
1007 const knownUserIds = [];
1008 const knownProfileIds = [];
1009 createdUsers.forEach(function(u) {
1010 knownUserIds.push(u.id.toString());
1011 });
1012 createdProfiles.forEach(function(p) {
1013 if (p.id) knownProfileIds.push(p.id.toString());
1014 });
1015 if (profile) {
1016 profile.profileName.should.be.oneOf(knownProfiles);
1017 if (profile.userId) profile.userId.toString().should.be.oneOf(knownUserIds);
1018 if (profile.id) profile.id.toString().should.be.oneOf(knownProfileIds);
1019 const user = profile.user();
1020 if (user) {
1021 user.name.should.be.oneOf(knownUsers);
1022 user.id.toString().should.be.oneOf(knownUserIds);
1023 }
1024 }
1025
1026 done();
1027 });
1028 });
1029 });
1030 });
1031
1032 bdd.itIf(connectorCapabilities.adhocSort !== false,
1033 'should fetch Users with include scope on Posts - belongsTo',
1034 function(done) {
1035 Post.find({include: {relation: 'author', scope: {fields: ['name']}}},
1036 function(err, posts) {
1037 should.not.exist(err);
1038 should.exist(posts);
1039 posts.length.should.equal(5);
1040
1041 const author = posts[0].author();
1042 author.name.should.equal('User A');
1043 author.should.have.property('id');
1044 author.should.have.property('age', undefined);
1045
1046 done();
1047 });
1048 });
1049
1050 bdd.itIf(connectorCapabilities.adhocSort === false,
1051 'should fetch Users with include scope on Posts - belongsTo - no sort',
1052 function(done) {
1053 Post.find({include: {relation: 'author', scope: {fields: ['name']}}},
1054 function(err, posts) {
1055 should.not.exist(err);
1056 should.exist(posts);
1057 posts.length.should.be.belowOrEqual(5);
1058
1059 const author = posts[0].author();
1060 if (author) {
1061 author.name.should.be.oneOf('User A', 'User B', 'User C', 'User D', 'User E');
1062 author.should.have.property('id');
1063 author.should.have.property('age', undefined);
1064 }
1065
1066 done();
1067 });
1068 });
1069
1070 it('should fetch Users with include scope on Posts - hasMany', function(done) {
1071 User.find({
1072 include: {relation: 'posts', scope: {
1073 order: 'title DESC',
1074 }},
1075 }, function(err, users) {
1076 should.not.exist(err);
1077 should.exist(users);
1078 users.length.should.equal(5);
1079
1080 if (connectorCapabilities.adhocSort !== false) {
1081 users[0].name.should.equal('User A');
1082 users[1].name.should.equal('User B');
1083
1084 let posts = users[0].posts();
1085 posts.should.be.an.array;
1086 posts.should.have.length(3);
1087
1088 posts[0].title.should.equal('Post C');
1089 posts[1].title.should.equal('Post B');
1090 posts[2].title.should.equal('Post A');
1091
1092 posts = users[1].posts();
1093 posts.should.be.an.array;
1094 posts.should.have.length(1);
1095 posts[0].title.should.equal('Post D');
1096 } else {
1097 users.forEach(function(u) {
1098 u.name.should.be.oneOf(knownUsers);
1099 const posts = u.posts();
1100 if (posts) {
1101 posts.should.be.an.array;
1102 posts.length.should.be.belowOrEqual(3);
1103 posts.forEach(function(p) {
1104 p.title.should.be.oneOf(knownPosts);
1105 });
1106 }
1107 });
1108 }
1109
1110 done();
1111 });
1112 });
1113
1114 it('should fetch User - Posts AND Passports', function(done) {
1115 User.find({include: ['posts', 'passports']}, function(err, users) {
1116 should.not.exist(err);
1117 should.exist(users);
1118 users.length.should.be.ok;
1119 users.forEach(function(user) {
1120 // The relation should be promoted as the 'owner' property
1121 user.should.have.property('posts');
1122 user.should.have.property('passports');
1123
1124 const userObj = user.toJSON();
1125 userObj.should.have.property('posts');
1126 userObj.should.have.property('passports');
1127 userObj.posts.should.be.an.instanceOf(Array);
1128 userObj.passports.should.be.an.instanceOf(Array);
1129
1130 // The __cachedRelations should be removed from json output
1131 userObj.should.not.have.property('__cachedRelations');
1132
1133 user.__cachedRelations.should.have.property('posts');
1134 user.__cachedRelations.should.have.property('passports');
1135 user.__cachedRelations.posts.forEach(function(p) {
1136 // FIXME there are cases that p.userId is string
1137 p.userId.toString().should.eql(user.id.toString());
1138 });
1139 user.__cachedRelations.passports.forEach(function(pp) {
1140 // FIXME there are cases that p.ownerId is string
1141 pp.ownerId.toString().should.eql(user.id.toString());
1142 });
1143 });
1144 done();
1145 });
1146 });
1147
1148 it('should fetch User - Posts AND Passports in relation syntax',
1149 function(done) {
1150 User.find({include: [
1151 {relation: 'posts', scope: {
1152 where: {title: 'Post A'},
1153 }},
1154 'passports',
1155 ]}, function(err, users) {
1156 should.not.exist(err);
1157 should.exist(users);
1158 users.length.should.be.ok;
1159 users.forEach(function(user) {
1160 // The relation should be promoted as the 'owner' property
1161 user.should.have.property('posts');
1162 user.should.have.property('passports');
1163
1164 const userObj = user.toJSON();
1165 userObj.should.have.property('posts');
1166 userObj.should.have.property('passports');
1167 userObj.posts.should.be.an.instanceOf(Array);
1168 userObj.passports.should.be.an.instanceOf(Array);
1169
1170 // The __cachedRelations should be removed from json output
1171 userObj.should.not.have.property('__cachedRelations');
1172
1173 user.__cachedRelations.should.have.property('posts');
1174 user.__cachedRelations.should.have.property('passports');
1175 user.__cachedRelations.posts.forEach(function(p) {
1176 // FIXME there are cases that p.userId is string
1177 p.userId.toString().should.eql(user.id.toString());
1178 p.title.should.be.equal('Post A');
1179 });
1180 user.__cachedRelations.passports.forEach(function(pp) {
1181 // FIXME there are cases that p.ownerId is string
1182 pp.ownerId.toString().should.eql(user.id.toString());
1183 });
1184 });
1185 done();
1186 });
1187 });
1188
1189 it('should not fetch User - AccessTokens', function(done) {
1190 User.find({include: ['accesstokens']}, function(err, users) {
1191 should.not.exist(err);
1192 should.exist(users);
1193 users.length.should.be.ok;
1194 users.forEach(function(user) {
1195 const userObj = user.toJSON();
1196 userObj.should.not.have.property('accesstokens');
1197 });
1198 done();
1199 });
1200 });
1201
1202 it('should support hasAndBelongsToMany', function(done) {
1203 Assembly.create({name: 'car'}, function(err, assembly) {
1204 Part.create({partNumber: 'engine'}, function(err, part) {
1205 assembly.parts.add(part, function(err, data) {
1206 assembly.parts(function(err, parts) {
1207 should.not.exist(err);
1208 should.exists(parts);
1209 parts.length.should.equal(1);
1210 parts[0].partNumber.should.equal('engine');
1211
1212 // Create a part
1213 assembly.parts.create({partNumber: 'door'}, function(err, part4) {
1214 Assembly.find({include: 'parts'}, function(err, assemblies) {
1215 assemblies.length.should.equal(1);
1216 assemblies[0].parts().length.should.equal(2);
1217 done();
1218 });
1219 });
1220 });
1221 });
1222 });
1223 });
1224 });
1225
1226 it('should fetch User - Profile (HasOne)', function(done) {
1227 User.find({include: ['profile']}, function(err, users) {
1228 should.not.exist(err);
1229 should.exist(users);
1230 users.length.should.be.ok;
1231 let usersWithProfile = 0;
1232 users.forEach(function(user) {
1233 // The relation should be promoted as the 'owner' property
1234 user.should.have.property('profile');
1235 const userObj = user.toJSON();
1236 const profile = user.profile();
1237 if (profile) {
1238 profile.should.be.an.instanceOf(Profile);
1239 usersWithProfile++;
1240 } else {
1241 (profile === null).should.be.true;
1242 }
1243 // The __cachedRelations should be removed from json output
1244 userObj.should.not.have.property('__cachedRelations');
1245 user.__cachedRelations.should.have.property('profile');
1246 if (user.__cachedRelations.profile) {
1247 // FIXME there are cases that profile.userId is string
1248 user.__cachedRelations.profile.userId.toString().should.eql(user.id.toString());
1249 usersWithProfile++;
1250 }
1251 });
1252 usersWithProfile.should.equal(2 * 2);
1253 done();
1254 });
1255 });
1256
1257 it('should not throw on fetch User if include is boolean equals true', function(done) {
1258 User.find({include: true}, function(err, users) {
1259 if (err) return done(err);
1260 should.exist(users);
1261 users.should.not.be.empty();
1262 done();
1263 });
1264 });
1265
1266 it('should not throw on fetch User if include is number', function(done) {
1267 User.find({include: 1}, function(err, users) {
1268 if (err) return done(err);
1269 should.exist(users);
1270 users.should.not.be.empty();
1271 done();
1272 });
1273 });
1274
1275 it('should not throw on fetch User if include is symbol', function(done) {
1276 User.find({include: Symbol('include')}, function(err, users) {
1277 if (err) return done(err);
1278 should.exist(users);
1279 users.should.not.be.empty();
1280 done();
1281 });
1282 });
1283
1284 it('should not throw on fetch User if include is function', function(done) {
1285 const include = () => {};
1286 User.find({include}, function(err, users) {
1287 if (err) return done(err);
1288 should.exist(users);
1289 users.should.not.be.empty();
1290 done();
1291 });
1292 });
1293
1294 // Not implemented correctly, see: loopback-datasource-juggler/issues/166
1295 // fixed by DB optimization
1296 it('should support include scope on hasAndBelongsToMany', function(done) {
1297 Assembly.find({include: {relation: 'parts', scope: {
1298 where: {partNumber: 'engine'},
1299 }}}, function(err, assemblies) {
1300 assemblies.length.should.equal(1);
1301 const parts = assemblies[0].parts();
1302 parts.should.have.length(1);
1303 parts[0].partNumber.should.equal('engine');
1304 done();
1305 });
1306 });
1307
1308 it('should save related items separately', function(done) {
1309 User.find({
1310 include: 'posts',
1311 })
1312 .then(function(users) {
1313 const posts = users[0].posts();
1314 if (connectorCapabilities.adhocSort !== false) {
1315 posts.should.have.length(3);
1316 } else {
1317 if (posts) posts.length.should.be.belowOrEqual(3);
1318 }
1319 return users[0].save();
1320 })
1321 .then(function(updatedUser) {
1322 return User.findById(updatedUser.id, {
1323 include: 'posts',
1324 });
1325 })
1326 .then(function(user) {
1327 const posts = user.posts();
1328 if (connectorCapabilities.adhocSort !== false) {
1329 posts.should.have.length(3);
1330 } else {
1331 if (posts) posts.length.should.be.belowOrEqual(3);
1332 }
1333 })
1334 .then(done)
1335 .catch(done);
1336 });
1337
1338 describe('performance', function() {
1339 let all;
1340 beforeEach(function() {
1341 this.called = 0;
1342 const self = this;
1343 all = db.connector.all;
1344 db.connector.all = function(model, filter, options, cb) {
1345 self.called++;
1346 return all.apply(db.connector, arguments);
1347 };
1348 });
1349 afterEach(function() {
1350 db.connector.all = all;
1351 });
1352
1353 const nDBCalls = connectorCapabilities.supportTwoOrMoreInq !== false ? 2 : 4;
1354 it('including belongsTo should make only ' + nDBCalls + ' db calls', function(done) {
1355 const self = this;
1356 Passport.find({include: 'owner'}, function(err, passports) {
1357 passports.length.should.be.ok;
1358 passports.forEach(function(p) {
1359 p.__cachedRelations.should.have.property('owner');
1360 // The relation should be promoted as the 'owner' property
1361 p.should.have.property('owner');
1362 // The __cachedRelations should be removed from json output
1363 p.toJSON().should.not.have.property('__cachedRelations');
1364 const owner = p.__cachedRelations.owner;
1365 if (!p.ownerId) {
1366 should.not.exist(owner);
1367 } else {
1368 should.exist(owner);
1369 owner.id.should.eql(p.ownerId);
1370 }
1371 });
1372 self.called.should.eql(nDBCalls);
1373 done();
1374 });
1375 });
1376
1377 it('including hasManyThrough should make only 3 db calls', function(done) {
1378 const self = this;
1379 Assembly.create([{name: 'sedan'}, {name: 'hatchback'},
1380 {name: 'SUV'}],
1381 function(err, assemblies) {
1382 Part.create([{partNumber: 'engine'}, {partNumber: 'bootspace'},
1383 {partNumber: 'silencer'}],
1384 function(err, parts) {
1385 async.each(parts, function(part, next) {
1386 async.each(assemblies, function(assembly, next) {
1387 if (assembly.name === 'SUV') {
1388 return next();
1389 }
1390 if (assembly.name === 'hatchback' &&
1391 part.partNumber === 'bootspace') {
1392 return next();
1393 }
1394 assembly.parts.add(part, function(err, data) {
1395 next();
1396 });
1397 }, next);
1398 }, function(err) {
1399 const autos = connectorCapabilities.supportTwoOrMoreInq !== false ?
1400 ['sedan', 'hatchback', 'SUV'] : ['sedan'];
1401 const resultLength = connectorCapabilities.supportTwoOrMoreInq !== false ? 3 : 1;
1402 const dbCalls = connectorCapabilities.supportTwoOrMoreInq !== false ? 3 : 5;
1403 self.called = 0;
1404 Assembly.find({
1405 where: {
1406 name: {
1407 inq: autos,
1408 },
1409 },
1410 include: 'parts',
1411 }, function(err, result) {
1412 should.not.exist(err);
1413 should.exists(result);
1414 result.length.should.equal(resultLength);
1415 // Please note the order of assemblies is random
1416 const assemblies = {};
1417 result.forEach(function(r) {
1418 assemblies[r.name] = r;
1419 });
1420 if (autos.indexOf('sedan') >= 0) assemblies.sedan.parts().should.have.length(3);
1421 if (autos.indexOf('hatchback') >= 0) assemblies.hatchback.parts().should.have.length(2);
1422 if (autos.indexOf('SUV') >= 0) assemblies.SUV.parts().should.have.length(0);
1423 self.called.should.eql(dbCalls);
1424 done();
1425 });
1426 });
1427 });
1428 });
1429 });
1430
1431 const dbCalls = connectorCapabilities.supportTwoOrMoreInq !== false ? 3 : 11;
1432 it('including hasMany should make only ' + dbCalls + ' db calls', function(done) {
1433 const self = this;
1434 User.find({include: ['posts', 'passports']}, function(err, users) {
1435 should.not.exist(err);
1436 should.exist(users);
1437 users.length.should.be.ok;
1438 users.forEach(function(user) {
1439 // The relation should be promoted as the 'owner' property
1440 user.should.have.property('posts');
1441 user.should.have.property('passports');
1442
1443 const userObj = user.toJSON();
1444 userObj.should.have.property('posts');
1445 userObj.should.have.property('passports');
1446 userObj.posts.should.be.an.instanceOf(Array);
1447 userObj.passports.should.be.an.instanceOf(Array);
1448
1449 // The __cachedRelations should be removed from json output
1450 userObj.should.not.have.property('__cachedRelations');
1451
1452 user.__cachedRelations.should.have.property('posts');
1453 user.__cachedRelations.should.have.property('passports');
1454 user.__cachedRelations.posts.forEach(function(p) {
1455 // FIXME p.userId is string in some cases.
1456 if (p.userId) p.userId.toString().should.eql(user.id.toString());
1457 });
1458 user.__cachedRelations.passports.forEach(function(pp) {
1459 // FIXME pp.owerId is string in some cases.
1460 if (pp.owerId) pp.ownerId.toString().should.eql(user.id.toString());
1461 });
1462 });
1463 self.called.should.eql(dbCalls);
1464 done();
1465 });
1466 });
1467
1468 it('should not make n+1 db calls in relation syntax',
1469 function(done) {
1470 const self = this;
1471 User.find({include: [{relation: 'posts', scope: {
1472 where: {title: 'Post A'},
1473 }}, 'passports']}, function(err, users) {
1474 should.not.exist(err);
1475 should.exist(users);
1476 users.length.should.be.ok;
1477 users.forEach(function(user) {
1478 // The relation should be promoted as the 'owner' property
1479 user.should.have.property('posts');
1480 user.should.have.property('passports');
1481
1482 const userObj = user.toJSON();
1483 userObj.should.have.property('posts');
1484 userObj.should.have.property('passports');
1485 userObj.posts.should.be.an.instanceOf(Array);
1486 userObj.passports.should.be.an.instanceOf(Array);
1487
1488 // The __cachedRelations should be removed from json output
1489 userObj.should.not.have.property('__cachedRelations');
1490
1491 user.__cachedRelations.should.have.property('posts');
1492 user.__cachedRelations.should.have.property('passports');
1493 user.__cachedRelations.posts.forEach(function(p) {
1494 // FIXME p.userId is string in some cases.
1495 p.userId.toString().should.eql(user.id.toString());
1496 p.title.should.be.equal('Post A');
1497 });
1498 user.__cachedRelations.passports.forEach(function(pp) {
1499 // FIXME p.userId is string in some cases.
1500 pp.ownerId.toString().should.eql(user.id.toString());
1501 });
1502 });
1503 self.called.should.eql(dbCalls);
1504 done();
1505 });
1506 });
1507 });
1508
1509 it('should support disableInclude for hasAndBelongsToMany', function() {
1510 const Patient = db.define('Patient', {name: String});
1511 const Doctor = db.define('Doctor', {name: String});
1512 const DoctorPatient = db.define('DoctorPatient');
1513 Doctor.hasAndBelongsToMany('patients', {
1514 model: 'Patient',
1515 options: {disableInclude: true},
1516 });
1517
1518 let doctor;
1519 return db.automigrate(['Patient', 'Doctor', 'DoctorPatient']).then(function() {
1520 return Doctor.create({name: 'Who'});
1521 }).then(function(inst) {
1522 doctor = inst;
1523 return doctor.patients.create({name: 'Lazarus'});
1524 }).then(function() {
1525 return Doctor.find({include: ['patients']});
1526 }).then(function(list) {
1527 list.should.have.length(1);
1528 list[0].toJSON().should.not.have.property('patients');
1529 });
1530 });
1531});
1532
1533let createdUsers = [];
1534let createdPassports = [];
1535let createdProfiles = [];
1536let createdPosts = [];
1537function setup(done) {
1538 db = getSchema();
1539 City = db.define('City');
1540 Street = db.define('Street');
1541 Building = db.define('Building');
1542 User = db.define('User', {
1543 name: String,
1544 age: Number,
1545 });
1546 Profile = db.define('Profile', {
1547 profileName: String,
1548 });
1549 AccessToken = db.define('AccessToken', {
1550 token: String,
1551 });
1552 Passport = db.define('Passport', {
1553 number: String,
1554 expirationDate: Date,
1555 });
1556 Post = db.define('Post', {
1557 title: {type: String, index: true},
1558 });
1559
1560 Passport.belongsTo('owner', {model: User});
1561 User.hasMany('passports', {foreignKey: 'ownerId'});
1562 User.hasMany('posts', {foreignKey: 'userId'});
1563 User.hasMany('accesstokens', {
1564 foreignKey: 'userId',
1565 options: {disableInclude: true},
1566 });
1567 Profile.belongsTo('user', {model: User});
1568 User.hasOne('profile', {foreignKey: 'userId'});
1569 Post.belongsTo('author', {model: User, foreignKey: 'userId'});
1570
1571 Assembly = db.define('Assembly', {
1572 name: String,
1573 });
1574
1575 Part = db.define('Part', {
1576 partNumber: String,
1577 });
1578
1579 Assembly.hasAndBelongsToMany(Part);
1580 Part.hasAndBelongsToMany(Assembly);
1581
1582 db.automigrate(function() {
1583 createUsers();
1584 function createUsers() {
1585 clearAndCreate(
1586 User,
1587 [
1588 {name: 'User A', age: 21},
1589 {name: 'User B', age: 22},
1590 {name: 'User C', age: 23},
1591 {name: 'User D', age: 24},
1592 {name: 'User E', age: 25},
1593 ],
1594
1595 function(items) {
1596 createdUsers = items;
1597 createPassports();
1598 createAccessTokens();
1599 },
1600 );
1601 }
1602 function createAccessTokens() {
1603 clearAndCreate(
1604 AccessToken,
1605 [
1606 {token: '1', userId: createdUsers[0].id},
1607 {token: '2', userId: createdUsers[1].id},
1608 ],
1609 function(items) {},
1610 );
1611 }
1612
1613 function createPassports() {
1614 clearAndCreate(
1615 Passport,
1616 [
1617 {number: '1', ownerId: createdUsers[0].id},
1618 {number: '2', ownerId: createdUsers[1].id},
1619 {number: '3'},
1620 {number: '4', ownerId: createdUsers[2].id},
1621 ],
1622 function(items) {
1623 createdPassports = items;
1624 createPosts();
1625 },
1626 );
1627 }
1628
1629 function createProfiles() {
1630 clearAndCreate(
1631 Profile,
1632 [
1633 {profileName: 'Profile A', userId: createdUsers[0].id},
1634 {profileName: 'Profile B', userId: createdUsers[1].id},
1635 {profileName: 'Profile Z'},
1636 ],
1637 function(items) {
1638 createdProfiles = items;
1639 done();
1640 },
1641 );
1642 }
1643
1644 function createPosts() {
1645 clearAndCreate(
1646 Post,
1647 [
1648 {title: 'Post A', userId: createdUsers[0].id},
1649 {title: 'Post B', userId: createdUsers[0].id},
1650 {title: 'Post C', userId: createdUsers[0].id},
1651 {title: 'Post D', userId: createdUsers[1].id},
1652 {title: 'Post E'},
1653 ],
1654 function(items) {
1655 createdPosts = items;
1656 createProfiles();
1657 },
1658 );
1659 }
1660 });
1661}
1662
1663function clearAndCreate(model, data, callback) {
1664 const createdItems = [];
1665 model.destroyAll(function() {
1666 nextItem(null, null);
1667 });
1668
1669 let itemIndex = 0;
1670
1671 function nextItem(err, lastItem) {
1672 if (lastItem !== null) {
1673 createdItems.push(lastItem);
1674 }
1675 if (itemIndex >= data.length) {
1676 callback(createdItems);
1677 return;
1678 }
1679 model.create(data[itemIndex], nextItem);
1680 itemIndex++;
1681 }
1682}
1683
1684describe('Model instance with included relation .toJSON()', function() {
1685 let db, ChallengerModel, GameParticipationModel, ResultModel;
1686
1687 before(function(done) {
1688 db = new DataSource({connector: 'memory'});
1689 ChallengerModel = db.createModel('Challenger',
1690 {
1691 name: String,
1692 },
1693 {
1694 relations: {
1695 gameParticipations: {
1696 type: 'hasMany',
1697 model: 'GameParticipation',
1698 foreignKey: '',
1699 },
1700 },
1701 });
1702 GameParticipationModel = db.createModel('GameParticipation',
1703 {
1704 date: Date,
1705 },
1706 {
1707 relations: {
1708 challenger: {
1709 type: 'belongsTo',
1710 model: 'Challenger',
1711 foreignKey: '',
1712 },
1713 results: {
1714 type: 'hasMany',
1715 model: 'Result',
1716 foreignKey: '',
1717 },
1718 },
1719 });
1720 ResultModel = db.createModel('Result', {
1721 points: Number,
1722 }, {
1723 relations: {
1724 gameParticipation: {
1725 type: 'belongsTo',
1726 model: 'GameParticipation',
1727 foreignKey: '',
1728 },
1729 },
1730 });
1731
1732 async.waterfall([
1733 createChallengers,
1734 createGameParticipations,
1735 createResults],
1736 function(err) {
1737 done(err);
1738 });
1739 });
1740
1741 function createChallengers(callback) {
1742 ChallengerModel.create([{name: 'challenger1'}, {name: 'challenger2'}], callback);
1743 }
1744
1745 function createGameParticipations(challengers, callback) {
1746 GameParticipationModel.create([
1747 {challengerId: challengers[0].id, date: Date.now()},
1748 {challengerId: challengers[0].id, date: Date.now()},
1749 ], callback);
1750 }
1751
1752 function createResults(gameParticipations, callback) {
1753 ResultModel.create([
1754 {gameParticipationId: gameParticipations[0].id, points: 10},
1755 {gameParticipationId: gameParticipations[0].id, points: 20},
1756 ], callback);
1757 }
1758
1759 it('should recursively serialize objects', function(done) {
1760 const filter = {include: {gameParticipations: 'results'}};
1761 ChallengerModel.find(filter, function(err, challengers) {
1762 const levelOneInclusion = challengers[0].toJSON().gameParticipations[0];
1763 assert(levelOneInclusion.__data === undefined, '.__data of a level 1 inclusion is undefined.');
1764
1765 const levelTwoInclusion = challengers[0].toJSON().gameParticipations[0].results[0];
1766 assert(levelTwoInclusion.__data === undefined, '__data of a level 2 inclusion is undefined.');
1767 done();
1768 });
1769 });
1770});