UNPKG

45.4 kBJavaScriptView Raw
1// Copyright IBM Corp. 2013,2019. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6// This test written in mocha+should.js
7'use strict';
8
9/* global getSchema:false, connectorCapabilities:false */
10const async = require('async');
11const bdd = require('./helpers/bdd-if');
12const should = require('./init.js');
13const uid = require('./helpers/uid-generator');
14const createTestSetupForParentRef = require('./helpers/setup-parent-ref');
15
16let db, User;
17
18describe('basic-querying', function() {
19 before(function(done) {
20 const userModelDef = {
21 seq: {type: Number, index: true},
22 name: {type: String, index: true, sort: true},
23 email: {type: String, index: true},
24 birthday: {type: Date, index: true},
25 role: {type: String, index: true},
26 order: {type: Number, index: true, sort: true},
27 tag: {type: String, index: true},
28 vip: {type: Boolean},
29 address: {
30 street: String,
31 city: String,
32 state: String,
33 zipCode: String,
34 tags: [
35 {
36 tag: String,
37 },
38 ],
39 },
40 friends: [
41 {
42 name: String,
43 },
44 ],
45 addressLoc: {
46 lat: Number,
47 lng: Number,
48 },
49 };
50
51 db = getSchema();
52 // connectors that do not support geo-point types
53 connectorCapabilities.geoPoint = (db.adapter.name != 'dashdb') && (db.adapter.name != 'db2') &&
54 (db.adapter.name != 'informix') && (db.adapter.name != 'cassandra');
55 if (connectorCapabilities.geoPoint) userModelDef.addressLoc = {type: 'GeoPoint'};
56 User = db.define('User', userModelDef);
57 db.automigrate(done);
58 });
59
60 describe('ping', function() {
61 it('should be able to test connections', function(done) {
62 db.ping(function(err) {
63 should.not.exist(err);
64 done();
65 });
66 });
67 });
68
69 describe('findById', function() {
70 before(function(done) {
71 db = getSchema();
72 User.destroyAll(done);
73 });
74
75 it('should query by id: not found', function(done) {
76 const unknownId = uid.fromConnector(db) || 1;
77 User.findById(unknownId, function(err, u) {
78 should.not.exist(u);
79 should.not.exist(err);
80 done();
81 });
82 });
83
84 it('should query by id: found', function(done) {
85 User.create(function(err, u) {
86 should.not.exist(err);
87 should.exist(u.id);
88 User.findById(u.id, function(err, u) {
89 should.exist(u);
90 should.not.exist(err);
91 u.should.be.an.instanceOf(User);
92 done();
93 });
94 });
95 });
96 });
97
98 describe('findByIds', function() {
99 let createdUsers;
100 before(function(done) {
101 db = getSchema();
102 const people = [
103 {name: 'a', vip: true},
104 {name: 'b', vip: null},
105 {name: 'c'},
106 {name: 'd', vip: true},
107 {name: 'e'},
108 {name: 'f'},
109 ];
110 db.automigrate(['User'], function(err) {
111 User.create(people, function(err, users) {
112 should.not.exist(err);
113 // Users might be created in parallel and the generated ids can be
114 // out of sequence
115 createdUsers = users;
116 done();
117 });
118 });
119 });
120
121 it('should query by ids', function(done) {
122 User.findByIds(
123 [createdUsers[2].id, createdUsers[1].id, createdUsers[0].id],
124 function(err, users) {
125 should.exist(users);
126 should.not.exist(err);
127 const names = users.map(function(u) {
128 return u.name;
129 });
130 names.should.eql(
131 [createdUsers[2].name, createdUsers[1].name, createdUsers[0].name],
132 );
133 done();
134 },
135 );
136 });
137
138 it('should query by ids and condition', function(done) {
139 User.findByIds([
140 createdUsers[0].id,
141 createdUsers[1].id,
142 createdUsers[2].id,
143 createdUsers[3].id],
144 {where: {vip: true}}, function(err, users) {
145 should.exist(users);
146 should.not.exist(err);
147 const names = users.map(function(u) {
148 return u.name;
149 });
150 names.should.eql(createdUsers.slice(0, 4).
151 filter(function(u) {
152 return u.vip;
153 }).map(function(u) {
154 return u.name;
155 }));
156 done();
157 });
158 });
159
160 bdd.itIf(connectorCapabilities.nullDataValueExists !== false,
161 'should query by ids to check null property', function(done) {
162 User.findByIds([
163 createdUsers[0].id,
164 createdUsers[1].id],
165 {where: {vip: null}}, function(err, users) {
166 should.not.exist(err);
167 should.exist(users);
168 users.length.should.eql(1);
169 users[0].name.should.eql(createdUsers[1].name);
170 done();
171 });
172 });
173 });
174
175 describe('find', function() {
176 before(seed);
177
178 before(function setupDelayingLoadedHook() {
179 User.observe('loaded', nextAfterDelay);
180 });
181
182 after(function removeDelayingLoadHook() {
183 User.removeObserver('loaded', nextAfterDelay);
184 });
185
186 it('should query collection', function(done) {
187 User.find(function(err, users) {
188 should.exists(users);
189 should.not.exists(err);
190 users.should.have.lengthOf(6);
191 done();
192 });
193 });
194
195 it('should query limited collection', function(done) {
196 User.find({limit: 3}, function(err, users) {
197 should.exists(users);
198 should.not.exists(err);
199 users.should.have.lengthOf(3);
200 done();
201 });
202 });
203
204 bdd.itIf(connectorCapabilities.supportPagination !== false, 'should query collection with skip & ' +
205 'limit', function(done) {
206 User.find({skip: 1, limit: 4, order: 'seq'}, function(err, users) {
207 should.exists(users);
208 should.not.exists(err);
209 users[0].seq.should.be.eql(1);
210 users.should.have.lengthOf(4);
211 done();
212 });
213 });
214
215 bdd.itIf(connectorCapabilities.supportPagination !== false, 'should query collection with offset & ' +
216 'limit', function(done) {
217 User.find({offset: 2, limit: 3, order: 'seq'}, function(err, users) {
218 should.exists(users);
219 should.not.exists(err);
220 users[0].seq.should.be.eql(2);
221 users.should.have.lengthOf(3);
222 done();
223 });
224 });
225
226 it('should query filtered collection', function(done) {
227 User.find({where: {role: 'lead'}}, function(err, users) {
228 should.exists(users);
229 should.not.exists(err);
230 users.should.have.lengthOf(2);
231 done();
232 });
233 });
234
235 bdd.itIf(connectorCapabilities.adhocSort !== false, 'should query collection sorted by numeric ' +
236 'field', function(done) {
237 User.find({order: 'order'}, function(err, users) {
238 should.exists(users);
239 should.not.exists(err);
240 users.forEach(function(u, i) {
241 u.order.should.eql(i + 1);
242 });
243 done();
244 });
245 });
246
247 bdd.itIf(connectorCapabilities.adhocSort !== false, 'should query collection desc sorted by ' +
248 'numeric field', function(done) {
249 User.find({order: 'order DESC'}, function(err, users) {
250 should.exists(users);
251 should.not.exists(err);
252 users.forEach(function(u, i) {
253 u.order.should.eql(users.length - i);
254 });
255 done();
256 });
257 });
258
259 bdd.itIf(connectorCapabilities.adhocSort !== false, 'should query collection sorted by string ' +
260 'field', function(done) {
261 User.find({order: 'name'}, function(err, users) {
262 should.exists(users);
263 should.not.exists(err);
264 users.shift().name.should.equal('George Harrison');
265 users.shift().name.should.equal('John Lennon');
266 users.pop().name.should.equal('Stuart Sutcliffe');
267 done();
268 });
269 });
270
271 bdd.itIf(connectorCapabilities.adhocSort !== false, 'should query collection desc sorted by ' +
272 'string field', function(done) {
273 User.find({order: 'name DESC'}, function(err, users) {
274 should.exists(users);
275 should.not.exists(err);
276 users.pop().name.should.equal('George Harrison');
277 users.pop().name.should.equal('John Lennon');
278 users.shift().name.should.equal('Stuart Sutcliffe');
279 done();
280 });
281 });
282
283 bdd.itIf(connectorCapabilities.adhocSort !== false, 'should query sorted desc by order integer field' +
284 ' even though there is an async model loaded hook', function(done) {
285 User.find({order: 'order DESC'}, function(err, users) {
286 if (err) return done(err);
287 should.exists(users);
288 const order = users.map(function(u) { return u.order; });
289 order.should.eql([6, 5, 4, 3, 2, 1]);
290 done();
291 });
292 });
293
294 it('should support "and" operator that is satisfied', function(done) {
295 User.find({where: {and: [
296 {name: 'John Lennon'},
297 {role: 'lead'},
298 ]}}, function(err, users) {
299 should.not.exist(err);
300 users.should.have.property('length', 1);
301 done();
302 });
303 });
304
305 it('should support "and" operator that is not satisfied', function(done) {
306 User.find({where: {and: [
307 {name: 'John Lennon'},
308 {role: 'member'},
309 ]}}, function(err, users) {
310 should.not.exist(err);
311 users.should.have.property('length', 0);
312 done();
313 });
314 });
315
316 bdd.itIf(connectorCapabilities.supportOrOperator !== false, 'should support "or" that is ' +
317 'satisfied', function(done) {
318 User.find({where: {or: [
319 {name: 'John Lennon'},
320 {role: 'lead'},
321 ]}}, function(err, users) {
322 should.not.exist(err);
323 users.should.have.property('length', 2);
324 done();
325 });
326 });
327
328 bdd.itIf(connectorCapabilities.supportOrOperator !== false, 'should support "or" operator that is ' +
329 'not satisfied', function(done) {
330 User.find({where: {or: [
331 {name: 'XYZ'},
332 {role: 'Hello1'},
333 ]}}, function(err, users) {
334 should.not.exist(err);
335 users.should.have.property('length', 0);
336 done();
337 });
338 });
339
340 bdd.itIf(connectorCapabilities.nullDataValueExists !== false,
341 'should support where date "neq" null', function(done) {
342 User.find({where: {birthday: {'neq': null},
343 }}, function(err, users) {
344 should.not.exist(err);
345 should.exist(users);
346 users.should.have.property('length', 2);
347 should(users[0].name).be.oneOf('John Lennon', 'Paul McCartney');
348 should(users[1].name).be.oneOf('John Lennon', 'Paul McCartney');
349 done();
350 });
351 });
352
353 bdd.itIf(connectorCapabilities.nullDataValueExists !== false,
354 'should support where date is null', function(done) {
355 User.find({where: {birthday: null,
356 }}, function(err, users) {
357 should.not.exist(err);
358 should.exist(users);
359 users.should.have.property('length', 4);
360 done();
361 });
362 });
363
364 it('should support date "gte" that is satisfied', function(done) {
365 User.find({where: {birthday: {'gte': new Date('1980-12-08')},
366 }}, function(err, users) {
367 should.not.exist(err);
368 users.should.have.property('length', 1);
369 users[0].name.should.equal('John Lennon');
370 done();
371 });
372 });
373
374 it('should support date "gt" that is not satisfied', function(done) {
375 User.find({where: {birthday: {'gt': new Date('1980-12-08')},
376 }}, function(err, users) {
377 should.not.exist(err);
378 users.should.have.property('length', 0);
379 done();
380 });
381 });
382
383 it('should support date "gt" that is satisfied', function(done) {
384 User.find({where: {birthday: {'gt': new Date('1980-12-07')},
385 }}, function(err, users) {
386 should.not.exist(err);
387 users.should.have.property('length', 1);
388 users[0].name.should.equal('John Lennon');
389 done();
390 });
391 });
392
393 bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
394 'should support date "lt" that is satisfied', function(done) {
395 User.find({where: {birthday: {'lt': new Date('1980-12-07')},
396 }}, function(err, users) {
397 should.not.exist(err);
398 users.should.have.property('length', 1);
399 users[0].name.should.equal('Paul McCartney');
400 done();
401 });
402 });
403
404 it('should support number "gte" that is satisfied', function(done) {
405 User.find({where: {order: {'gte': 3}}}, function(err, users) {
406 should.not.exist(err);
407 users.should.have.property('length', 4);
408 users.map(u => u.name).should.containDeep([
409 'George Harrison', 'Ringo Starr', 'Pete Best', 'Stuart Sutcliffe',
410 ]);
411 done();
412 });
413 });
414
415 it('should support number "gt" that is not satisfied', function(done) {
416 User.find({where: {order: {'gt': 6},
417 }}, function(err, users) {
418 should.not.exist(err);
419 users.should.have.property('length', 0);
420 done();
421 });
422 });
423
424 it('should support number "gt" that is satisfied', function(done) {
425 User.find({where: {order: {'gt': 5},
426 }}, function(err, users) {
427 should.not.exist(err);
428 users.should.have.property('length', 1);
429 users[0].name.should.equal('Ringo Starr');
430 done();
431 });
432 });
433
434 it('should support number "lt" that is satisfied', function(done) {
435 User.find({where: {order: {'lt': 2},
436 }}, function(err, users) {
437 should.not.exist(err);
438 users.should.have.property('length', 1);
439 users[0].name.should.equal('Paul McCartney');
440 done();
441 });
442 });
443
444 bdd.itIf(connectorCapabilities.ignoreUndefinedConditionValue !== false, 'should support number "gt" ' +
445 'that is satisfied by null value', function(done) {
446 User.find({order: 'seq', where: {order: {'gt': null}}}, function(err, users) {
447 should.not.exist(err);
448 users.should.have.property('length', 0);
449 done();
450 });
451 });
452
453 bdd.itIf(connectorCapabilities.ignoreUndefinedConditionValue !== false, 'should support number "lt" ' +
454 'that is not satisfied by null value', function(done) {
455 User.find({where: {order: {'lt': null}}}, function(err, users) {
456 should.not.exist(err);
457 users.should.have.property('length', 0);
458 done();
459 });
460 });
461
462 bdd.itIf(connectorCapabilities.ignoreUndefinedConditionValue !== false, 'should support string "gte" ' +
463 'that is satisfied by null value', function(done) {
464 User.find({order: 'seq', where: {name: {'gte': null}}}, function(err, users) {
465 should.not.exist(err);
466 users.should.have.property('length', 0);
467 done();
468 });
469 });
470
471 bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
472 'should support string "gte" that is satisfied', function(done) {
473 User.find({where: {name: {'gte': 'Paul McCartney'}}}, function(err, users) {
474 should.not.exist(err);
475 users.should.have.property('length', 4);
476 for (let ix = 0; ix < users.length; ix++) {
477 users[ix].name.should.be.greaterThanOrEqual('Paul McCartney');
478 }
479 done();
480 });
481 });
482
483 it('should support string "gt" that is not satisfied', function(done) {
484 User.find({where: {name: {'gt': 'xyz'},
485 }}, function(err, users) {
486 should.not.exist(err);
487 users.should.have.property('length', 0);
488 done();
489 });
490 });
491
492 bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
493 'should support string "gt" that is satisfied', function(done) {
494 User.find({where: {name: {'gt': 'Paul McCartney'},
495 }}, function(err, users) {
496 should.not.exist(err);
497 users.should.have.property('length', 3);
498 for (let ix = 0; ix < users.length; ix++) {
499 users[ix].name.should.be.greaterThan('Paul McCartney');
500 }
501 done();
502 });
503 });
504
505 bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
506 'should support string "lt" that is satisfied', function(done) {
507 User.find({where: {name: {'lt': 'Paul McCartney'},
508 }}, function(err, users) {
509 should.not.exist(err);
510 users.should.have.property('length', 2);
511 for (let ix = 0; ix < users.length; ix++) {
512 users[ix].name.should.be.lessThan('Paul McCartney');
513 }
514 done();
515 });
516 });
517
518 it('should support boolean "gte" that is satisfied', function(done) {
519 User.find({where: {vip: {'gte': true},
520 }}, function(err, users) {
521 should.not.exist(err);
522 users.should.have.property('length', 3);
523 for (let ix = 0; ix < users.length; ix++) {
524 users[ix].name.should.be.oneOf(['John Lennon', 'Stuart Sutcliffe', 'Paul McCartney']);
525 users[ix].vip.should.be.true();
526 }
527 done();
528 });
529 });
530
531 it('should support boolean "gt" that is not satisfied', function(done) {
532 User.find({where: {vip: {'gt': true},
533 }}, function(err, users) {
534 should.not.exist(err);
535 users.should.have.property('length', 0);
536 done();
537 });
538 });
539
540 it('should support boolean "gt" that is satisfied', function(done) {
541 User.find({where: {vip: {'gt': false},
542 }}, function(err, users) {
543 should.not.exist(err);
544 users.should.have.property('length', 3);
545 for (let ix = 0; ix < users.length; ix++) {
546 users[ix].name.should.be.oneOf(['John Lennon', 'Stuart Sutcliffe', 'Paul McCartney']);
547 users[ix].vip.should.be.true(users[ix].name + ' should be VIP');
548 }
549 done();
550 });
551 });
552
553 it('should support boolean "lt" that is satisfied', function(done) {
554 User.find({where: {vip: {'lt': true},
555 }}, function(err, users) {
556 should.not.exist(err);
557 users.should.have.property('length', 2);
558 for (let ix = 0; ix < users.length; ix++) {
559 users[ix].name.should.be.oneOf(['Ringo Starr', 'George Harrison']);
560 users[ix].vip.should.be.false(users[ix].name + ' should not be VIP');
561 }
562 done();
563 });
564 });
565
566 bdd.itIf(connectorCapabilities.supportInq, 'supports non-empty inq', function() {
567 // note there is no record with seq=100
568 return User.find({where: {seq: {inq: [0, 1, 100]}}})
569 .then(result => {
570 const seqsFound = result.map(r => r.seq);
571 should(seqsFound).eql([0, 1]);
572 });
573 });
574
575 bdd.itIf(connectorCapabilities.supportInq, 'supports empty inq', function() {
576 return User.find({where: {seq: {inq: []}}})
577 .then(result => {
578 const seqsFound = result.map(r => r.seq);
579 should(seqsFound).eql([]);
580 });
581 });
582
583 const itWhenIlikeSupported = connectorCapabilities.ilike;
584 bdd.describeIf(itWhenIlikeSupported, 'ilike', function() {
585 it('should support "like" that is satisfied',
586 function(done) {
587 User.find({where: {name: {like: 'John'}}},
588 function(err, users) {
589 if (err) return done(err);
590 users.length.should.equal(1);
591 users[0].name.should.equal('John Lennon');
592 done();
593 });
594 });
595
596 it('should sanitize invalid usage of like', async () => {
597 const users = await User.find({where: {tag: {like: '['}}});
598 users.should.have.length(1);
599 users[0].should.have.property('name', 'John Lennon');
600 });
601
602 it('should support "like" that is not satisfied',
603 function(done) {
604 User.find({where: {name: {like: 'Bob'}}},
605 function(err, users) {
606 if (err) return done(err);
607 users.length.should.equal(0);
608 done();
609 });
610 });
611 it('should support "ilike" that is satisfied', function(done) {
612 User.find({where: {name: {ilike: 'john'}}},
613 function(err, users) {
614 if (err) return done(err);
615 users.length.should.equal(1);
616 users[0].name.should.equal('John Lennon');
617 done();
618 });
619 });
620 it('should support "ilike" that is not satisfied', function(done) {
621 User.find({where: {name: {ilike: 'bob'}}}, function(err, users) {
622 if (err) return done(err);
623 users.length.should.equal(0);
624 done();
625 });
626 });
627
628 it('should properly sanitize invalid ilike filter', async () => {
629 const users = await User.find({where: {name: {ilike: '['}}});
630 users.should.be.empty();
631 });
632 });
633
634 const itWhenNilikeSupported = connectorCapabilities.nilike !== false;
635 bdd.describeIf(itWhenNilikeSupported, 'nilike', function() {
636 it('should support "nlike" that is satisfied', function(done) {
637 User.find({where: {name: {nlike: 'John'}}},
638 function(err, users) {
639 if (err) return done(err);
640 users.length.should.equal(5);
641 users[0].name.should.equal('Paul McCartney');
642 done();
643 });
644 });
645
646 it('should support "nilike" that is satisfied', function(done) {
647 User.find({where: {name: {nilike: 'john'}}},
648 function(err, users) {
649 if (err) return done(err);
650 users.length.should.equal(5);
651 users[0].name.should.equal('Paul McCartney');
652 done();
653 });
654 });
655 });
656
657 describe('geo queries', function() {
658 describe('near filter', function() {
659 it('supports a basic "near" query', function(done) {
660 User.find({
661 where: {
662 addressLoc: {
663 near: {lat: 29.9, lng: -90.07},
664 },
665 },
666 }, function(err, users) {
667 if (err) done(err);
668 users.should.have.property('length', 3);
669 users[0].name.should.equal('John Lennon');
670 users[0].should.be.instanceOf(User);
671 users[0].addressLoc.should.not.be.null();
672 done();
673 });
674 });
675
676 it('supports "near" inside a coumpound query with "and"', function(done) {
677 User.find({
678 where: {
679 and: [
680 {
681 addressLoc: {
682 near: {lat: 29.9, lng: -90.07},
683 },
684 },
685 {
686 vip: true,
687 },
688 ],
689 },
690 }, function(err, users) {
691 if (err) done(err);
692 users.should.have.property('length', 2);
693 users[0].name.should.equal('John Lennon');
694 users[0].should.be.instanceOf(User);
695 users[0].addressLoc.should.not.be.null();
696 users[0].vip.should.be.true();
697 done();
698 });
699 });
700
701 it('supports "near" inside a complex coumpound query with multiple "and"', function(done) {
702 User.find({
703 where: {
704 and: [
705 {
706 and: [
707 {
708 addressLoc: {
709 near: {lat: 29.9, lng: -90.07},
710 },
711 },
712 {
713 order: 2,
714 },
715 ],
716 },
717 {
718 vip: true,
719 },
720 ],
721 },
722 }, function(err, users) {
723 if (err) done(err);
724 users.should.have.property('length', 1);
725 users[0].name.should.equal('John Lennon');
726 users[0].should.be.instanceOf(User);
727 users[0].addressLoc.should.not.be.null();
728 users[0].vip.should.be.true();
729 users[0].order.should.equal(2);
730 done();
731 });
732 });
733
734 it('supports multiple "near" queries with "or"', function(done) {
735 User.find({
736 where: {
737 or: [
738 {
739 addressLoc: {
740 near: {lat: 29.9, lng: -90.04},
741 maxDistance: 300,
742 },
743 },
744 {
745 addressLoc: {
746 near: {lat: 22.97, lng: -88.03},
747 maxDistance: 50,
748 },
749 },
750 ],
751 },
752 }, function(err, users) {
753 if (err) done(err);
754 users.should.have.property('length', 2);
755 users[0].addressLoc.should.not.be.null();
756 users[0].name.should.equal('Paul McCartney');
757 users[0].should.be.instanceOf(User);
758 users[1].addressLoc.should.not.equal(null);
759 users[1].name.should.equal('John Lennon');
760 done();
761 });
762 });
763
764 it('supports multiple "near" queries with "or" ' +
765 'inside a coumpound query with "and"', function(done) {
766 User.find({
767 where: {
768 and: [
769 {
770 or: [
771 {
772 addressLoc: {
773 near: {lat: 29.9, lng: -90.04},
774 maxDistance: 300,
775 },
776 },
777 {
778 addressLoc: {
779 near: {lat: 22.7, lng: -89.03},
780 maxDistance: 50,
781 },
782 },
783 ],
784 },
785 {
786 vip: true,
787 },
788 ],
789 },
790 }, function(err, users) {
791 if (err) done(err);
792 users.should.have.property('length', 1);
793 users[0].addressLoc.should.not.be.null();
794 users[0].name.should.equal('John Lennon');
795 users[0].should.be.instanceOf(User);
796 users[0].vip.should.be.true();
797 done();
798 });
799 });
800 });
801 });
802
803 it('should only include fields as specified', function(done) {
804 let remaining = 0;
805
806 function sample(fields) {
807 return {
808 expect: function(arr) {
809 remaining++;
810 User.find({fields: fields}, function(err, users) {
811 remaining--;
812 if (err) return done(err);
813
814 should.exists(users);
815
816 if (remaining === 0) {
817 done();
818 }
819
820 users.forEach(function(user) {
821 const obj = user.toObject();
822
823 Object.keys(obj)
824 .forEach(function(key) {
825 // if the obj has an unexpected value
826 if (obj[key] !== undefined && arr.indexOf(key) === -1) {
827 console.log('Given fields:', fields);
828 console.log('Got:', key, obj[key]);
829 console.log('Expected:', arr);
830 throw new Error('should not include data for key: ' + key);
831 }
832 });
833 });
834 });
835 },
836 };
837 }
838
839 sample({name: true}).expect(['name']);
840 sample({name: false}).expect([
841 'id', 'seq', 'email', 'role', 'order', 'birthday', 'vip', 'address', 'friends', 'addressLoc', 'tag',
842 ]);
843 sample({name: false, id: true}).expect(['id']);
844 sample({id: true}).expect(['id']);
845 sample('id').expect(['id']);
846 sample(['id']).expect(['id']);
847 sample(['email']).expect(['email']);
848 });
849
850 it('should ignore non existing properties when excluding', function(done) {
851 return User.find({fields: {notExist: false}}, (err, users) => {
852 if (err) return done(err);
853 users.forEach(user => {
854 switch (user.seq) { // all fields depending on each document
855 case 0:
856 case 1:
857 Object.keys(user.__data).should.containDeep(['id', 'seq', 'name', 'order', 'role',
858 'birthday', 'vip', 'address', 'friends']);
859 break;
860 case 4: // seq 4
861 Object.keys(user.__data).should.containDeep(['id', 'seq', 'name', 'order']);
862 break;
863 default: // Other records, seq 2, 3, 5
864 Object.keys(user.__data).should.containDeep(['id', 'seq', 'name', 'order', 'vip']);
865 }
866 });
867 done();
868 });
869 });
870
871 const describeWhenNestedSupported = connectorCapabilities.nestedProperty;
872 bdd.describeIf(describeWhenNestedSupported, 'query with nested property', function() {
873 it('should support nested property in query', function(done) {
874 User.find({where: {'address.city': 'San Jose'}}, function(err, users) {
875 if (err) return done(err);
876 users.length.should.be.equal(1);
877 for (let i = 0; i < users.length; i++) {
878 users[i].address.city.should.be.eql('San Jose');
879 }
880 done();
881 });
882 });
883
884 it('should support nested property with regex over arrays in query', function(done) {
885 User.find({where: {'friends.name': {regexp: /^Ringo/}}}, function(err, users) {
886 if (err) return done(err);
887 users.length.should.be.equal(2);
888 const expectedUsers = ['John Lennon', 'Paul McCartney'];
889 expectedUsers.indexOf(users[0].name).should.not.equal(-1);
890 expectedUsers.indexOf(users[1].name).should.not.equal(-1);
891 done();
892 });
893 });
894
895 it('should support nested property with gt in query', function(done) {
896 User.find({where: {'address.city': {gt: 'San'}}}, function(err, users) {
897 if (err) return done(err);
898 users.length.should.be.equal(2);
899 for (let i = 0; i < users.length; i++) {
900 users[i].address.state.should.be.eql('CA');
901 }
902 done();
903 });
904 });
905
906 bdd.itIf(connectorCapabilities.adhocSort,
907 'should support nested property for order in query',
908 function(done) {
909 User.find({where: {'address.state': 'CA'}, order: 'address.city DESC'},
910 function(err, users) {
911 if (err) return done(err);
912 users.length.should.be.equal(2);
913 users[0].address.city.should.be.eql('San Mateo');
914 users[1].address.city.should.be.eql('San Jose');
915 done();
916 });
917 });
918
919 it('should support multi-level nested array property in query', function(done) {
920 User.find({where: {'address.tags.tag': 'business'}}, function(err, users) {
921 if (err) return done(err);
922 users.length.should.be.equal(1);
923 users[0].address.tags[0].tag.should.be.equal('business');
924 users[0].address.tags[1].tag.should.be.equal('rent');
925 done();
926 });
927 });
928
929 it('should fail when querying with an invalid value for a type',
930 function(done) {
931 User.find({where: {birthday: 'notadate'}}, function(err, users) {
932 should.exist(err);
933 err.message.should.equal('Invalid date: notadate');
934 done();
935 });
936 });
937 });
938
939 it('preserves empty values from the database', async () => {
940 // https://github.com/strongloop/loopback-datasource-juggler/issues/1692
941
942 // Initially, all Players were always active, no property was needed
943 const Player = db.define('Player', {name: String});
944
945 await db.automigrate('Player');
946 const created = await Player.create({name: 'Pen'});
947
948 // Later on, we decide to introduce `active` property
949 Player.defineProperty('active', {
950 type: Boolean,
951 default: false,
952 });
953 await db.autoupdate('Player');
954
955 // And query existing data
956 const found = await Player.findOne();
957 should(found.toObject().active).be.oneOf([
958 undefined, // databases supporting `undefined` value
959 null, // databases representing `undefined` as `null` (e.g. SQL)
960 ]);
961 });
962
963 describe('check __parent relationship in embedded models', () => {
964 createTestSetupForParentRef(() => User.modelBuilder);
965 it('should fill the parent in embedded model', async () => {
966 const user = await User.findOne({where: {name: 'John Lennon'}});
967 user.should.have.property('address');
968 should(user.address).have.property('__parent');
969 should(user.address.__parent).be.instanceof(User).and.equal(user);
970 });
971 it('should assign the container model as parent in list property', async () => {
972 const user = await User.findOne({where: {name: 'John Lennon'}});
973 user.should.have.property('friends');
974 should(user.friends).have.property('parent');
975 should(user.friends.parent).be.instanceof(User).and.equal(user);
976 });
977 it('should have the complete chain of parents available in embedded list element', async () => {
978 const user = await User.findOne({where: {name: 'John Lennon'}});
979 user.friends.forEach((userFriend) => {
980 userFriend.should.have.property('__parent');
981 should(userFriend.__parent).equal(user);
982 });
983 });
984 });
985 });
986
987 describe('count', function() {
988 before(seed);
989
990 it('should query total count', function(done) {
991 User.count(function(err, n) {
992 should.not.exist(err);
993 should.exist(n);
994 n.should.equal(6);
995 done();
996 });
997 });
998
999 it('should query filtered count', function(done) {
1000 User.count({role: 'lead'}, function(err, n) {
1001 should.not.exist(err);
1002 should.exist(n);
1003 n.should.equal(2);
1004 done();
1005 });
1006 });
1007 });
1008
1009 describe('findOne', function() {
1010 before(seed);
1011
1012 bdd.itIf(connectorCapabilities.cloudantCompatible !== false,
1013 'should find first record (default sort by id)', function(done) {
1014 User.all({order: 'id'}, function(err, users) {
1015 User.findOne(function(e, u) {
1016 should.not.exist(e);
1017 should.exist(u);
1018 u.id.toString().should.equal(users[0].id.toString());
1019 done();
1020 });
1021 });
1022 });
1023
1024 bdd.itIf(connectorCapabilities.adhocSort, 'should find first record', function(done) {
1025 User.findOne({order: 'order'}, function(e, u) {
1026 should.not.exist(e);
1027 should.exist(u);
1028 u.order.should.equal(1);
1029 u.name.should.equal('Paul McCartney');
1030 done();
1031 });
1032 });
1033
1034 bdd.itIf(connectorCapabilities.adhocSort, 'should find last record', function(done) {
1035 User.findOne({order: 'order DESC'}, function(e, u) {
1036 should.not.exist(e);
1037 should.exist(u);
1038 u.order.should.equal(6);
1039 u.name.should.equal('Ringo Starr');
1040 done();
1041 });
1042 });
1043
1044 bdd.itIf(connectorCapabilities.adhocSort, 'should find last record in filtered set', function(done) {
1045 User.findOne({
1046 where: {role: 'lead'},
1047 order: 'order DESC',
1048 }, function(e, u) {
1049 should.not.exist(e);
1050 should.exist(u);
1051 u.order.should.equal(2);
1052 u.name.should.equal('John Lennon');
1053 done();
1054 });
1055 });
1056
1057 it('should work even when find by id', function(done) {
1058 User.findOne(function(e, u) {
1059 User.findOne({where: {id: u.id}}, function(err, user) {
1060 should.not.exist(err);
1061 should.exist(user);
1062 done();
1063 });
1064 });
1065 });
1066 });
1067
1068 describe('exists', function() {
1069 before(seed);
1070
1071 it('should check whether record exist', function(done) {
1072 User.findOne(function(e, u) {
1073 User.exists(u.id, function(err, exists) {
1074 should.not.exist(err);
1075 should.exist(exists);
1076 exists.should.be.ok;
1077 done();
1078 });
1079 });
1080 });
1081
1082 it('should check whether record not exist', function(done) {
1083 const unknownId = uid.fromConnector(db) || 42;
1084 User.destroyAll(function() {
1085 User.exists(unknownId, function(err, exists) {
1086 should.not.exist(err);
1087 exists.should.not.be.ok;
1088 done();
1089 });
1090 });
1091 });
1092 });
1093
1094 describe('updateAll', function() {
1095 let numAndDateModel, numAndDateArrayModel;
1096
1097 before(function() {
1098 numAndDateModel = db.define('numAndDateModel', {
1099 dateProp: Date,
1100 numProp: Number,
1101 });
1102 // 'numAndDateArrayModel' is too long an identifier name for Oracle DB
1103 numAndDateArrayModel = db.define('numAndDateArrMod', {
1104 dateArray: [Date],
1105 numArray: [Number],
1106 });
1107 return db.automigrate(['numAndDateModel', 'numAndDateArrMod']);
1108 });
1109
1110 it('coerces primitive datatypes on update', async () => {
1111 const createDate = new Date('2019-02-21T12:00:00').toISOString();
1112 const createData = {
1113 dateProp: createDate,
1114 numProp: '1',
1115 };
1116 const updateDate = new Date('2019-04-15T12:00:00').toISOString();
1117 const updateData = {
1118 dateProp: updateDate,
1119 numProp: '3',
1120 };
1121 const created = await numAndDateModel.create(createData);
1122 const updated = await numAndDateModel.updateAll({id: created.id}, updateData);
1123 const found = await numAndDateModel.findById(created.id);
1124 found.dateProp.should.deepEqual(new Date(updateDate));
1125 found.numProp.should.equal(3);
1126 });
1127
1128 // PostgreSQL connector does not support arrays at the moment
1129 bdd.itIf(connectorCapabilities.supportsArrays !== false,
1130 'coerces primitive array datatypes on update', async () => {
1131 const createDate = new Date('2019-02-21T12:00:00').toISOString();
1132 const createData = {
1133 dateArray: [createDate, createDate],
1134 numArray: ['1', '2'],
1135 };
1136 const updateDate = new Date('2019-04-15T12:00:00').toISOString();
1137 const updateData = {
1138 dateArray: [updateDate, updateDate],
1139 numArray: ['3', '4'],
1140 };
1141 const created = await numAndDateArrayModel.create(createData);
1142 const updated = await numAndDateArrayModel.updateAll({id: created.id}, updateData);
1143 const found = await numAndDateArrayModel.findById(created.id);
1144 found.dateArray[0].should.deepEqual(new Date(updateDate));
1145 found.dateArray[1].should.deepEqual(new Date(updateDate));
1146 found.numArray[0].should.equal(3);
1147 found.numArray[1].should.equal(4);
1148 });
1149 });
1150
1151 context('regexp operator', function() {
1152 const invalidDataTypes = [0, true, {}, [], Function, null];
1153
1154 before(seed);
1155
1156 it('should return an error for invalid data types', function(done) {
1157 // `undefined` is not tested because the `removeUndefined` function
1158 // in `lib/dao.js` removes it before coercion
1159 async.each(invalidDataTypes, function(v, cb) {
1160 User.find({where: {name: {regexp: v}}}, function(err, users) {
1161 should.exist(err);
1162 cb();
1163 });
1164 }, done);
1165 });
1166 });
1167});
1168
1169// FIXME: This should either be re-enabled or removed.
1170describe.skip('queries', function() {
1171 let Todo;
1172
1173 before(function prepDb(done) {
1174 db = getSchema();
1175 Todo = db.define('Todo', {
1176 id: false,
1177 content: {type: 'string'},
1178 }, {
1179 idInjection: false,
1180 });
1181 db.automigrate(['Todo'], done);
1182 });
1183 beforeEach(function resetFixtures(done) {
1184 db = getSchema();
1185 Todo.destroyAll(function() {
1186 Todo.create([
1187 {content: 'Buy eggs'},
1188 {content: 'Buy milk'},
1189 {content: 'Buy sausages'},
1190 ], done);
1191 });
1192 });
1193
1194 context('that do not require an id', function() {
1195 it('should work for create', function(done) {
1196 Todo.create({content: 'Buy ham'}, function(err) {
1197 should.not.exist(err);
1198 done();
1199 });
1200 });
1201
1202 it('should work for updateOrCreate/upsert', function(done) {
1203 const aliases = ['updateOrCreate', 'upsert'];
1204 async.each(aliases, function(alias, cb) {
1205 Todo[alias]({content: 'Buy ham'}, function(err) {
1206 should.not.exist(err);
1207 cb();
1208 });
1209 }, done);
1210 });
1211
1212 it('should work for findOrCreate', function(done) {
1213 Todo.findOrCreate({content: 'Buy ham'}, function(err) {
1214 should.not.exist(err);
1215 done();
1216 });
1217 });
1218
1219 it('should work for exists', function(done) {
1220 Todo.exists({content: 'Buy ham'}, function(err) {
1221 should.not.exist(err);
1222 done();
1223 });
1224 });
1225
1226 it('should work for find', function(done) {
1227 Todo.find(function(err) {
1228 should.not.exist(err);
1229 done();
1230 });
1231 });
1232
1233 it('should work for findOne', function(done) {
1234 Todo.findOne(function(err) {
1235 should.not.exist(err);
1236 done();
1237 });
1238 });
1239
1240 it('should work for deleteAll/destroyAll/remove', function(done) {
1241 // FIXME: We should add a DAO.delete static method alias for consistency
1242 // (DAO.prototype.delete instance method already exists)
1243 const aliases = ['deleteAll', 'destroyAll', 'remove'];
1244 async.each(aliases, function(alias, cb) {
1245 Todo[alias](function(err) {
1246 should.not.exist(err);
1247 cb();
1248 });
1249 }, done);
1250 });
1251
1252 it('should work for update/updateAll', function(done) {
1253 Todo.update({content: 'Buy ham'}, function(err) {
1254 should.not.exist(err);
1255 done();
1256 });
1257 });
1258
1259 it('should work for count', function(done) {
1260 Todo.count({content: 'Buy eggs'}, function(err) {
1261 should.not.exist(err);
1262 done();
1263 });
1264 });
1265 });
1266
1267 context('that require an id', function() {
1268 const expectedErrMsg = 'Primary key is missing for the Todo model';
1269
1270 it('should return an error for findById', function(done) {
1271 Todo.findById(1, function(err) {
1272 should.exist(err);
1273 err.message.should.equal(expectedErrMsg);
1274 done();
1275 });
1276 });
1277
1278 it('should return an error for findByIds', function(done) {
1279 Todo.findByIds([1, 2], function(err) {
1280 should.exist(err);
1281 err.message.should.equal(expectedErrMsg);
1282 done();
1283 });
1284 });
1285
1286 it('should return an error for deleteById/destroyById/removeById',
1287 function(done) {
1288 const aliases = ['deleteById', 'destroyById', 'removeById'];
1289 async.each(aliases, function(alias, cb) {
1290 Todo[alias](1, function(err) {
1291 should.exist(err);
1292 err.message.should.equal(expectedErrMsg);
1293 cb();
1294 });
1295 }, done);
1296 });
1297
1298 it('should return an error for instance.save', function(done) {
1299 const todo = new Todo();
1300 todo.content = 'Buy ham';
1301 todo.save(function(err) {
1302 should.exist(err);
1303 err.message.should.equal(expectedErrMsg);
1304 done();
1305 });
1306 });
1307
1308 it('should return an error for instance.delete', function(done) {
1309 Todo.findOne(function(err, todo) {
1310 todo.delete(function(err) {
1311 should.exist(err);
1312 err.message.should.equal(expectedErrMsg);
1313 done();
1314 });
1315 });
1316 });
1317
1318 it('should return an error for instance.updateAttribute', function(done) {
1319 Todo.findOne(function(err, todo) {
1320 todo.updateAttribute('content', 'Buy ham', function(err) {
1321 should.exist(err);
1322 err.message.should.equal(expectedErrMsg);
1323 done();
1324 });
1325 });
1326 });
1327
1328 it('should return an error for instance.updateAttributes', function(done) {
1329 Todo.findOne(function(err, todo) {
1330 todo.updateAttributes({content: 'Buy ham'}, function(err) {
1331 should.exist(err);
1332 err.message.should.equal(expectedErrMsg);
1333 done();
1334 });
1335 });
1336 });
1337 });
1338});
1339
1340function seed(done) {
1341 const beatles = [
1342 {
1343 seq: 0,
1344 name: 'John Lennon',
1345 email: 'john@b3atl3s.co.uk',
1346 role: 'lead',
1347 birthday: new Date('1980-12-08'),
1348 order: 2,
1349 vip: true,
1350 tag: '[singer]',
1351 address: {
1352 street: '123 A St',
1353 city: 'San Jose',
1354 state: 'CA',
1355 zipCode: '95131',
1356 tags: [
1357 {tag: 'business'},
1358 {tag: 'rent'},
1359 ],
1360 },
1361 friends: [
1362 {name: 'Paul McCartney'},
1363 {name: 'George Harrison'},
1364 {name: 'Ringo Starr'},
1365 ],
1366 addressLoc: {lat: 29.97, lng: -90.03},
1367 },
1368 {
1369 seq: 1,
1370 name: 'Paul McCartney',
1371 email: 'paul@b3atl3s.co.uk',
1372 role: 'lead',
1373 birthday: new Date('1942-06-18'),
1374 order: 1,
1375 vip: true,
1376 address: {
1377 street: '456 B St',
1378 city: 'San Mateo',
1379 state: 'CA',
1380 zipCode: '94065',
1381 },
1382 friends: [
1383 {name: 'John Lennon'},
1384 {name: 'George Harrison'},
1385 {name: 'Ringo Starr'},
1386 ],
1387 addressLoc: {lat: 22.97, lng: -88.03},
1388 },
1389 {
1390 seq: 2,
1391 name: 'George Harrison',
1392 birthday: null,
1393 order: 5,
1394 vip: false,
1395 addressLoc: {lat: 22.7, lng: -89.03},
1396 },
1397 {seq: 3, name: 'Ringo Starr', order: 6, birthday: null, vip: false},
1398 {seq: 4, name: 'Pete Best', order: 4, birthday: null},
1399 {seq: 5, name: 'Stuart Sutcliffe', order: 3, birthday: null, vip: true},
1400 ];
1401
1402 async.series([
1403 User.destroyAll.bind(User),
1404 function(cb) {
1405 async.each(beatles, User.create.bind(User), cb);
1406 },
1407 ], done);
1408}
1409
1410function nextAfterDelay(ctx, next) {
1411 const randomTimeoutTrigger = Math.floor(Math.random() * 100);
1412 setTimeout(function() { process.nextTick(next); }, randomTimeoutTrigger);
1413}