UNPKG

62.8 kBJavaScriptView Raw
1// Copyright IBM Corp. 2013,2019. All Rights Reserved.
2// Node module: loopback-datasource-juggler
3// This file is licensed under the MIT License.
4// License text available at https://opensource.org/licenses/MIT
5
6// This test written in mocha+should.js
7'use strict';
8const should = require('./init.js');
9const assert = require('assert');
10const async = require('async');
11
12const jdb = require('../');
13const ModelBuilder = jdb.ModelBuilder;
14const DataSource = jdb.DataSource;
15
16describe('ModelBuilder', function() {
17 it('supports plain models', function(done) {
18 const modelBuilder = new ModelBuilder();
19
20 const User = modelBuilder.define('User', {
21 name: String,
22 bio: ModelBuilder.Text,
23 approved: Boolean,
24 joinedAt: Date,
25 age: Number,
26 });
27
28 // define any custom method
29 User.prototype.getNameAndAge = function() {
30 return this.name + ', ' + this.age;
31 };
32
33 modelBuilder.models.should.be.type('object').and.have.property('User').exactly(User);
34 modelBuilder.definitions.should.be.type('object').and.have.property('User');
35
36 const user = new User({name: 'Joe', age: 20, xyz: false});
37
38 User.modelName.should.equal('User');
39 user.should.be.type('object').and.have.property('name', 'Joe');
40 user.should.have.property('name', 'Joe');
41 user.should.have.property('age', 20);
42 user.should.have.property('xyz', false);
43 user.should.have.property('bio', undefined);
44 done(null, User);
45 });
46
47 it('ignores unknown properties in strict mode', function(done) {
48 const modelBuilder = new ModelBuilder();
49
50 const User = modelBuilder.define('User', {name: String, bio: String}, {strict: true});
51
52 const user = new User({name: 'Joe', age: 20});
53
54 User.modelName.should.equal('User');
55 user.should.be.type('object');
56 user.should.have.property('name', 'Joe');
57 user.should.not.have.property('age');
58 user.toObject().should.not.have.property('age');
59 user.toObject(true).should.not.have.property('age');
60 user.should.have.property('bio', undefined);
61 done(null, User);
62 });
63
64 it('ignores non-predefined properties in strict mode', function(done) {
65 const modelBuilder = new ModelBuilder();
66
67 const User = modelBuilder.define('User', {name: String, bio: String}, {strict: true});
68
69 const user = new User({name: 'Joe'});
70 user.age = 10;
71 user.bio = 'me';
72
73 user.should.have.property('name', 'Joe');
74 user.should.have.property('bio', 'me');
75
76 // Non predefined property age should be ignored in strict mode if schemaOnly parameter is not false
77 user.toObject().should.not.have.property('age');
78 user.toObject(true).should.not.have.property('age');
79 user.toObject(false).should.have.property('age', 10);
80
81 // Predefined property bio should be kept in strict mode
82 user.toObject().should.have.property('bio', 'me');
83 user.toObject(true).should.have.property('bio', 'me');
84 user.toObject(false).should.have.property('bio', 'me');
85 done(null, User);
86 });
87
88 it('throws an error when unknown properties are used if strict=throw', function(done) {
89 const modelBuilder = new ModelBuilder();
90
91 const User = modelBuilder.define('User', {name: String, bio: String}, {strict: 'throw'});
92
93 try {
94 const user = new User({name: 'Joe', age: 20});
95 assert(false, 'The code should have thrown an error');
96 } catch (e) {
97 assert(true, 'The code is expected to throw an error');
98 }
99 done(null, User);
100 });
101
102 it('supports open models', function(done) {
103 const modelBuilder = new ModelBuilder();
104
105 const User = modelBuilder.define('User', {}, {strict: false});
106
107 const user = new User({name: 'Joe', age: 20});
108
109 User.modelName.should.equal('User');
110 user.should.be.type('object').and.have.property('name', 'Joe');
111 user.should.have.property('name', 'Joe');
112 user.should.have.property('age', 20);
113 user.should.not.have.property('bio');
114 done(null, User);
115 });
116
117 it('accepts non-predefined properties in non-strict mode', function(done) {
118 const modelBuilder = new ModelBuilder();
119
120 const User = modelBuilder.define('User', {name: String, bio: String}, {strict: false});
121
122 const user = new User({name: 'Joe'});
123 user.age = 10;
124 user.bio = 'me';
125
126 user.should.have.property('name', 'Joe');
127 user.should.have.property('bio', 'me');
128
129 // Non predefined property age should be kept in non-strict mode
130 user.toObject().should.have.property('age', 10);
131 user.toObject(true).should.have.property('age', 10);
132 user.toObject(false).should.have.property('age', 10);
133
134 // Predefined property bio should be kept
135 user.toObject().should.have.property('bio', 'me');
136 user.toObject({onlySchema: true}).should.have.property('bio', 'me');
137 user.toObject({onlySchema: false}).should.have.property('bio', 'me');
138
139 done(null, User);
140 });
141
142 it('uses non-strict mode by default', function(done) {
143 const modelBuilder = new ModelBuilder();
144
145 const User = modelBuilder.define('User', {});
146
147 const user = new User({name: 'Joe', age: 20});
148
149 User.modelName.should.equal('User');
150 user.should.be.type('object').and.have.property('name', 'Joe');
151 user.should.have.property('name', 'Joe');
152 user.should.have.property('age', 20);
153 user.should.not.have.property('bio');
154 done(null, User);
155 });
156
157 it('supports nested model definitions', function(done) {
158 const modelBuilder = new ModelBuilder();
159
160 // simplier way to describe model
161 const User = modelBuilder.define('User', {
162 name: String,
163 bio: ModelBuilder.Text,
164 approved: Boolean,
165 joinedAt: Date,
166 age: Number,
167 address: {
168 street: String,
169 city: String,
170 state: String,
171 zipCode: String,
172 country: String,
173 },
174 emails: [
175 {
176 label: String,
177 email: String,
178 },
179 ],
180 friends: [String],
181 });
182
183 // define any custom method
184 User.prototype.getNameAndAge = function() {
185 return this.name + ', ' + this.age;
186 };
187
188 modelBuilder.models.should.be.type('object').and.have.property('User', User);
189 modelBuilder.definitions.should.be.type('object').and.have.property('User');
190
191 let user = new User({
192 name: 'Joe', age: 20,
193 address: {street: '123 Main St', 'city': 'San Jose', state: 'CA'},
194 emails: [
195 {label: 'work', email: 'xyz@sample.com'},
196 ],
197 friends: ['Mary', 'John'],
198 });
199
200 User.modelName.should.equal('User');
201 user.should.be.type('object').and.have.property('name', 'Joe');
202 user.should.have.property('name', 'Joe');
203 user.should.have.property('age', 20);
204 user.should.have.property('bio', undefined);
205 user.should.have.property('address');
206 user.address.should.have.property('city', 'San Jose');
207 user.address.should.have.property('state', 'CA');
208
209 user = user.toObject();
210 user.emails.should.have.property('length', 1);
211 user.emails[0].should.have.property('label', 'work');
212 user.emails[0].should.have.property('email', 'xyz@sample.com');
213 user.friends.should.have.property('length', 2);
214 assert.equal(user.friends[0], 'Mary');
215 assert.equal(user.friends[1], 'John');
216 done(null, User);
217 });
218
219 it('allows models to be referenced by name before they are defined', function(done) {
220 const modelBuilder = new ModelBuilder();
221
222 const User = modelBuilder.define('User', {name: String, address: 'Address'});
223
224 let user;
225 try {
226 user = new User({name: 'Joe', address: {street: '123 Main St', 'city': 'San Jose', state: 'CA'}});
227 assert(false, 'An exception should have been thrown');
228 } catch (e) {
229 // Ignore
230 }
231
232 const Address = modelBuilder.define('Address', {
233 street: String,
234 city: String,
235 state: String,
236 zipCode: String,
237 country: String,
238 });
239
240 user = new User({name: 'Joe', address: {street: '123 Main St', 'city': 'San Jose', state: 'CA'}});
241
242 User.modelName.should.equal('User');
243 User.definition.properties.address.should.have.property('type', Address);
244 user.should.be.type('object');
245 assert(user.name === 'Joe');
246 user.address.should.have.property('city', 'San Jose');
247 user.address.should.have.property('state', 'CA');
248 done(null, User);
249 });
250
251 it('defines an id property for composite ids', function() {
252 const modelBuilder = new ModelBuilder();
253 const Follow = modelBuilder.define('Follow', {
254 followerId: {type: String, id: 1},
255 followeeId: {type: String, id: 2},
256 followAt: Date,
257 });
258 const follow = new Follow({followerId: 1, followeeId: 2});
259
260 follow.should.have.property('id');
261 assert.deepEqual(follow.id, {followerId: 1, followeeId: 2});
262 });
263
264 it('instantiates model from data with no constructor', function(done) {
265 const modelBuilder = new ModelBuilder();
266
267 const User = modelBuilder.define('User', {name: String, age: Number});
268
269 try {
270 const data = Object.create(null);
271 data.name = 'Joe';
272 data.age = 20;
273 const user = new User(data);
274 assert(true, 'The code is expected to pass');
275 } catch (e) {
276 assert(false, 'The code should have not thrown an error');
277 }
278 done();
279 });
280
281 it('instantiates model from data with non function constructor', function(done) {
282 const modelBuilder = new ModelBuilder();
283
284 const User = modelBuilder.define('User', {name: String, age: Number});
285
286 try {
287 const Person = function(name, age) {
288 this.name = name;
289 this.age = age;
290 };
291
292 Person.prototype.constructor = 'constructor';
293
294 const data = new Person('Joe', 20);
295
296 const user = new User(data);
297 assert(false, 'The code should have thrown an error');
298 } catch (e) {
299 e.message.should.equal('Property name "constructor" is not allowed in User data');
300 assert(true, 'The code is expected to throw an error');
301 }
302 done();
303 });
304});
305
306describe('DataSource ping', function() {
307 const ds = new DataSource('memory');
308 ds.settings.connectionTimeout = 50; // ms
309 ds.connector.connect = function(cb) {
310 // Mock up the long delay
311 setTimeout(cb, 100);
312 };
313 ds.connector.ping = function(cb) {
314 cb(new Error('bad connection 2'));
315 };
316
317 it('reports connection errors during ping', function(done) {
318 ds.ping(function(err) {
319 (!!err).should.be.true;
320 err.message.should.be.eql('bad connection 2');
321 done();
322 });
323 });
324
325 it('cancels invocation after timeout', function(done) {
326 ds.connected = false; // Force connect
327 const Post = ds.define('Post', {
328 title: {type: String, length: 255},
329 });
330 Post.create(function(err) {
331 (!!err).should.be.true;
332 err.message.should.be.eql('Timeout in connecting after 50 ms');
333 done();
334 });
335 });
336});
337
338describe('DataSource define model', function() {
339 it('supports plain model definitions', function() {
340 const ds = new DataSource('memory');
341
342 // define models
343 const Post = ds.define('Post', {
344 title: {type: String, length: 255},
345 content: {type: ModelBuilder.Text},
346 date: {type: Date, default: function() {
347 return new Date();
348 }},
349 timestamp: {type: Number, default: Date.now},
350 published: {type: Boolean, default: false, index: true},
351 });
352
353 // simpler way to describe model
354 const User = ds.define('User', {
355 name: String,
356 bio: ModelBuilder.Text,
357 approved: Boolean,
358 joinedAt: {type: Date, default: Date},
359 age: Number,
360 });
361
362 const Group = ds.define('Group', {group: String});
363 User.mixin(Group);
364
365 // define any custom method
366 User.prototype.getNameAndAge = function() {
367 return this.name + ', ' + this.age;
368 };
369
370 const user = new User({name: 'Joe', group: 'G1'});
371 assert.equal(user.name, 'Joe');
372 assert.equal(user.group, 'G1');
373
374 assert(user.joinedAt instanceof Date);
375
376 // setup relationships
377 User.hasMany(Post, {as: 'posts', foreignKey: 'userId'});
378
379 Post.belongsTo(User, {as: 'author', foreignKey: 'userId'});
380
381 User.hasAndBelongsToMany('groups');
382
383 const user2 = new User({name: 'Smith'});
384 user2.save(function(err) {
385 const post = user2.posts.build({title: 'Hello world'});
386 post.save(function(err, data) {
387 // console.log(err ? err : data);
388 });
389 });
390
391 Post.findOne({where: {published: false}, order: 'date DESC'}, function(err, data) {
392 // console.log(data);
393 });
394
395 User.create({name: 'Jeff'}, function(err, data) {
396 if (err) {
397 return;
398 }
399 const post = data.posts.build({title: 'My Post'});
400 });
401
402 User.create({name: 'Ray'}, function(err, data) {
403 // console.log(data);
404 });
405
406 const Article = ds.define('Article', {title: String});
407 const Tag = ds.define('Tag', {name: String});
408 Article.hasAndBelongsToMany('tags');
409
410 Article.create(function(e, article) {
411 article.tags.create({name: 'popular'}, function(err, data) {
412 Article.findOne(function(e, article) {
413 article.tags(function(e, tags) {
414 // console.log(tags);
415 });
416 });
417 });
418 });
419
420 // should be able to attach a data source to an existing model
421 const modelBuilder = new ModelBuilder();
422
423 const Color = modelBuilder.define('Color', {
424 name: String,
425 });
426
427 Color.should.not.have.property('create');
428
429 // attach
430 ds.attach(Color);
431 Color.should.have.property('create');
432
433 Color.create({name: 'red'});
434 Color.create({name: 'green'});
435 Color.create({name: 'blue'});
436
437 Color.all(function(err, colors) {
438 colors.should.have.lengthOf(3);
439 });
440 });
441
442 it('emits events during attach', function() {
443 const ds = new DataSource('memory');
444 const modelBuilder = new ModelBuilder();
445
446 const User = modelBuilder.define('User', {
447 name: String,
448 });
449
450 let seq = 0;
451 let dataAccessConfigured = -1;
452 let dataSourceAttached = -1;
453
454 User.on('dataAccessConfigured', function(model) {
455 dataAccessConfigured = seq++;
456 assert(User.create);
457 assert(User.hasMany);
458 });
459
460 User.on('dataSourceAttached', function(model) {
461 assert(User.dataSource instanceof DataSource);
462 dataSourceAttached = seq++;
463 });
464
465 ds.attach(User);
466 assert.equal(dataAccessConfigured, 0);
467 assert.equal(dataSourceAttached, 1);
468 });
469
470 it('ignores unknown properties in strict mode', function(done) {
471 const ds = new DataSource('memory');
472
473 const User = ds.define('User', {name: String, bio: String}, {strict: true});
474
475 User.create({name: 'Joe', age: 20}, function(err, user) {
476 User.modelName.should.equal('User');
477 user.should.be.type('object');
478 assert(user.name === 'Joe');
479 assert(user.age === undefined);
480 assert(user.toObject().age === undefined);
481 assert(user.toObject(true).age === undefined);
482 assert(user.bio === undefined);
483 done(null, User);
484 });
485 });
486
487 it('throws an error when unknown properties are used if strict=throw', function(done) {
488 const ds = new DataSource('memory');
489
490 const User = ds.define('User', {name: String, bio: String}, {strict: 'throw'});
491
492 try {
493 const user = new User({name: 'Joe', age: 20});
494 assert(false, 'The code should have thrown an error');
495 } catch (e) {
496 assert(true, 'The code is expected to throw an error');
497 }
498 done(null, User);
499 });
500
501 describe('strict mode "validate"', function() {
502 it('reports validation errors for unknown properties', function() {
503 const ds = new DataSource('memory');
504 const User = ds.define('User', {name: String}, {strict: 'validate'});
505 const user = new User({name: 'Joe', age: 20});
506 user.isValid().should.be.false;
507 const codes = user.errors && user.errors.codes || {};
508 codes.should.have.property('age').eql(['unknown-property']);
509 });
510 });
511
512 it('supports open model definitions', function(done) {
513 const ds = new DataSource('memory');
514
515 const User = ds.define('User', {}, {strict: false});
516 User.modelName.should.equal('User');
517
518 User.create({name: 'Joe', age: 20}, function(err, user) {
519 user.should.be.type('object').and.have.property('name', 'Joe');
520 user.should.have.property('name', 'Joe');
521 user.should.have.property('age', 20);
522 user.should.not.have.property('bio');
523
524 User.findById(user.id, function(err, user) {
525 user.should.be.type('object').and.have.property('name', 'Joe');
526 user.should.have.property('name', 'Joe');
527 user.should.have.property('age', 20);
528 user.should.not.have.property('bio');
529 done(null, User);
530 });
531 });
532 });
533
534 it('uses non-strict mode by default', function(done) {
535 const ds = new DataSource('memory');
536
537 const User = ds.define('User', {});
538
539 User.create({name: 'Joe', age: 20}, function(err, user) {
540 User.modelName.should.equal('User');
541 user.should.be.type('object').and.have.property('name', 'Joe');
542 user.should.have.property('name', 'Joe');
543 user.should.have.property('age', 20);
544 user.should.not.have.property('bio');
545 done(null, User);
546 });
547 });
548
549 it('uses strict mode by default for relational DBs', function(done) {
550 const ds = new DataSource('memory');
551 ds.connector.relational = true; // HACK
552
553 const User = ds.define('User', {name: String, bio: String}, {strict: true});
554
555 const user = new User({name: 'Joe', age: 20});
556
557 User.modelName.should.equal('User');
558 user.should.be.type('object');
559 assert(user.name === 'Joe');
560 assert(user.age === undefined);
561 assert(user.toObject().age === undefined);
562 assert(user.toObject(true).age === undefined);
563 assert(user.bio === undefined);
564 done(null, User);
565 });
566
567 it('throws an error with unknown properties in non-strict mode for relational DBs', function(done) {
568 const ds = new DataSource('memory');
569 ds.connector.relational = true; // HACK
570
571 const User = ds.define('User', {name: String, bio: String}, {strict: 'throw'});
572
573 try {
574 const user = new User({name: 'Joe', age: 20});
575 assert(false, 'The code should have thrown an error');
576 } catch (e) {
577 assert(true, 'The code is expected to throw an error');
578 }
579 done(null, User);
580 });
581
582 it('changes the property value for save in non-strict mode', function(done) {
583 const ds = new DataSource('memory');// define models
584 const Post = ds.define('Post');
585
586 Post.create({price: 900}, function(err, post) {
587 assert.equal(post.price, 900);
588 post.price = 1000;
589 post.save(function(err, result) {
590 assert.equal(1000, result.price);
591 done(err, result);
592 });
593 });
594 });
595
596 it('supports instance level strict mode', function() {
597 const ds = new DataSource('memory');
598
599 const User = ds.define('User', {name: String, bio: String}, {strict: true});
600
601 const user = new User({name: 'Joe', age: 20}, {strict: false});
602
603 user.should.have.property('__strict', false);
604 user.should.be.type('object');
605 user.should.have.property('name', 'Joe');
606 user.should.have.property('age', 20);
607 user.toObject().should.have.property('age', 20);
608 user.toObject(true).should.have.property('age', 20);
609
610 user.setStrict(true);
611 user.toObject().should.not.have.property('age');
612 user.toObject(true).should.not.have.property('age');
613 user.toObject(false).should.have.property('age', 20);
614 });
615
616 it('updates instances with unknown properties in non-strict mode', function(done) {
617 const ds = new DataSource('memory');// define models
618 const Post = ds.define('Post', {
619 title: {type: String, length: 255, index: true},
620 content: {type: String},
621 });
622
623 Post.create({title: 'a', content: 'AAA'}, function(err, post) {
624 post.updateAttributes({title: 'b', xyz: 'xyz'}, function(err, p) {
625 should.not.exist(err);
626 p.id.should.be.equal(post.id);
627 p.content.should.be.equal(post.content);
628 p.xyz.should.be.equal('xyz');
629
630 Post.findById(post.id, function(err, p) {
631 p.id.should.be.equal(post.id);
632 p.content.should.be.equal(post.content);
633 p.xyz.should.be.equal('xyz');
634 p.title.should.be.equal('b');
635 done();
636 });
637 });
638 });
639 });
640
641 it('injects id by default', function(done) {
642 const ds = new ModelBuilder();
643
644 const User = ds.define('User', {});
645 assert.deepEqual(User.definition.properties.id,
646 {type: Number, id: 1, generated: true, updateOnly: true});
647
648 done();
649 });
650
651 it('injects id with useDefaultIdType to false', function(done) {
652 const ds = new ModelBuilder();
653
654 const User = ds.define('User', {id: {type: String, generated: true, id: true, useDefaultIdType: false}});
655 assert.deepEqual(User.definition.properties.id,
656 {type: String, id: true, generated: true, updateOnly: true, useDefaultIdType: false});
657
658 done();
659 });
660
661 it('disables idInjection if the value is false', function(done) {
662 const ds = new ModelBuilder();
663
664 const User1 = ds.define('User', {}, {idInjection: false});
665 assert(!User1.definition.properties.id);
666 done();
667 });
668
669 it('updates generated id type by the connector', function(done) {
670 const builder = new ModelBuilder();
671
672 const User = builder.define('User', {id: {type: String, generated: true, id: true}});
673 assert.deepEqual(User.definition.properties.id,
674 {type: String, id: 1, generated: true, updateOnly: true});
675
676 const ds = new DataSource('memory');// define models
677 User.attachTo(ds);
678
679 assert.deepEqual(User.definition.properties.id,
680 {type: Number, id: 1, generated: true, updateOnly: true});
681
682 done();
683 });
684
685 it('allows an explicit remoting path', function() {
686 const ds = new DataSource('memory');
687
688 const User = ds.define('User', {name: String, bio: String}, {
689 http: {path: 'accounts'},
690 });
691 User.http.path.should.equal('/accounts');
692 });
693
694 it('allows an explicit remoting path with leading /', function() {
695 const ds = new DataSource('memory');
696
697 const User = ds.define('User', {name: String, bio: String}, {
698 http: {path: '/accounts'},
699 });
700 User.http.path.should.equal('/accounts');
701 });
702});
703
704describe('Model loaded with a base', function() {
705 it('has a base class according to the base option', function() {
706 const ds = new ModelBuilder();
707
708 const User = ds.define('User', {name: String});
709
710 User.staticMethod = function staticMethod() {
711 };
712 User.prototype.instanceMethod = function instanceMethod() {
713 };
714
715 const Customer = ds.define('Customer', {vip: Boolean}, {base: 'User'});
716
717 assert(Customer.prototype instanceof User);
718 assert(Customer.staticMethod === User.staticMethod);
719 assert(Customer.prototype.instanceMethod === User.prototype.instanceMethod);
720 assert.equal(Customer.base, User);
721 assert.equal(Customer.base, Customer.super_);
722
723 try {
724 const Customer1 = ds.define('Customer1', {vip: Boolean}, {base: 'User1'});
725 } catch (e) {
726 assert(e);
727 }
728 });
729
730 it('inherits properties from base model', function() {
731 const ds = new ModelBuilder();
732
733 const User = ds.define('User', {name: String});
734
735 const Customer = ds.define('Customer', {vip: Boolean}, {base: 'User'});
736
737 Customer.definition.properties.should.have.property('name');
738 Customer.definition.properties.name.should.have.property('type', String);
739 });
740
741 it('inherits properties by clone from base model', function() {
742 const ds = new ModelBuilder();
743
744 const User = ds.define('User', {name: String});
745
746 const Customer1 = ds.define('Customer1', {vip: Boolean}, {base: 'User'});
747 const Customer2 = ds.define('Customer2', {vip: Boolean}, {base: 'User'});
748
749 Customer1.definition.properties.should.have.property('name');
750 Customer2.definition.properties.should.have.property('name');
751 Customer1.definition.properties.name.should.not.be.equal(
752 Customer2.definition.properties.name,
753 );
754 Customer1.definition.properties.name.should.eql(
755 Customer2.definition.properties.name,
756 );
757 });
758
759 it('can remove properties from base model', function() {
760 const ds = new ModelBuilder();
761
762 const User = ds.define('User', {username: String, email: String});
763
764 const Customer = ds.define('Customer',
765 {name: String, username: null, email: false},
766 {base: 'User'});
767
768 Customer.definition.properties.should.have.property('name');
769 // username/email are now shielded
770 Customer.definition.properties.should.not.have.property('username');
771 Customer.definition.properties.should.not.have.property('email');
772 const c = new Customer({name: 'John'});
773 c.should.have.property('username', undefined);
774 c.should.have.property('email', undefined);
775 c.should.have.property('name', 'John');
776 const u = new User({username: 'X', email: 'x@y.com'});
777 u.should.not.have.property('name');
778 u.should.have.property('username', 'X');
779 u.should.have.property('email', 'x@y.com');
780 });
781
782 it('can configure base class via parent argument', function() {
783 const ds = new ModelBuilder();
784
785 const User = ds.define('User', {name: String});
786
787 User.staticMethod = function staticMethod() {
788 };
789 User.prototype.instanceMethod = function instanceMethod() {
790 };
791
792 const Customer = ds.define('Customer', {vip: Boolean}, {}, User);
793
794 Customer.definition.properties.should.have.property('name');
795 Customer.definition.properties.name.should.have.property('type', String);
796
797 assert(Customer.prototype instanceof User);
798 assert(Customer.staticMethod === User.staticMethod);
799 assert(Customer.prototype.instanceMethod === User.prototype.instanceMethod);
800 assert.equal(Customer.base, User);
801 assert.equal(Customer.base, Customer.super_);
802 });
803});
804
805describe('Models attached to a dataSource', function() {
806 let Post;
807 before(function() {
808 const ds = new DataSource('memory');// define models
809 Post = ds.define('Post', {
810 title: {type: String, length: 255, index: true},
811 content: {type: String},
812 comments: [String],
813 }, {forceId: false});
814 });
815
816 beforeEach(function(done) {
817 Post.destroyAll(done);
818 });
819
820 describe('updateOrCreate', function() {
821 it('updates instances', function(done) {
822 Post.create({title: 'a', content: 'AAA'}, function(err, post) {
823 post.title = 'b';
824 Post.updateOrCreate(post, function(err, p) {
825 should.not.exist(err);
826 p.id.should.be.equal(post.id);
827 p.content.should.be.equal(post.content);
828 should.not.exist(p._id);
829
830 Post.findById(post.id, function(err, p) {
831 p.id.should.be.equal(post.id);
832 should.not.exist(p._id);
833 p.content.should.be.equal(post.content);
834 p.title.should.be.equal('b');
835 done();
836 });
837 });
838 });
839 });
840
841 it('updates instances without removing existing properties', function(done) {
842 Post.create({title: 'a', content: 'AAA', comments: ['Comment1']}, function(err, post) {
843 post = post.toObject();
844 delete post.title;
845 delete post.comments;
846 Post.updateOrCreate(post, function(err, p) {
847 should.not.exist(err);
848 p.id.should.be.equal(post.id);
849 p.content.should.be.equal(post.content);
850 should.not.exist(p._id);
851
852 Post.findById(post.id, function(err, p) {
853 p.id.should.be.equal(post.id);
854 should.not.exist(p._id);
855 p.content.should.be.equal(post.content);
856 p.title.should.be.equal('a');
857 p.comments.length.should.be.equal(1);
858 p.comments[0].should.be.equal('Comment1');
859 done();
860 });
861 });
862 });
863 });
864
865 it('creates a new instance if it does not exist', function(done) {
866 const post = {id: 123, title: 'a', content: 'AAA'};
867 Post.updateOrCreate(post, function(err, p) {
868 should.not.exist(err);
869 p.title.should.be.equal(post.title);
870 p.content.should.be.equal(post.content);
871 p.id.should.be.equal(post.id);
872
873 Post.findById(p.id, function(err, p) {
874 p.id.should.be.equal(post.id);
875 should.not.exist(p._id);
876 p.content.should.be.equal(post.content);
877 p.title.should.be.equal(post.title);
878 p.id.should.be.equal(post.id);
879 done();
880 });
881 });
882 });
883 });
884
885 describe('save', function() {
886 it('updates instance with the same id', function(done) {
887 Post.create({title: 'a', content: 'AAA'}, function(err, post) {
888 post.title = 'b';
889 post.save(function(err, p) {
890 should.not.exist(err);
891 p.id.should.be.equal(post.id);
892 p.content.should.be.equal(post.content);
893 should.not.exist(p._id);
894
895 Post.findById(post.id, function(err, p) {
896 p.id.should.be.equal(post.id);
897 should.not.exist(p._id);
898 p.content.should.be.equal(post.content);
899 p.title.should.be.equal('b');
900 done();
901 });
902 });
903 });
904 });
905
906 it('updates the instance without removing existing properties', function(done) {
907 Post.create({title: 'a', content: 'AAA'}, function(err, post) {
908 delete post.title;
909 post.save(function(err, p) {
910 should.not.exist(err);
911 p.id.should.be.equal(post.id);
912 p.content.should.be.equal(post.content);
913 should.not.exist(p._id);
914
915 Post.findById(post.id, function(err, p) {
916 p.id.should.be.equal(post.id);
917 should.not.exist(p._id);
918 p.content.should.be.equal(post.content);
919 p.title.should.be.equal('a');
920 done();
921 });
922 });
923 });
924 });
925
926 it('creates a new instance if it does not exist', function(done) {
927 const post = new Post({id: '123', title: 'a', content: 'AAA'});
928 post.save(post, function(err, p) {
929 should.not.exist(err);
930 p.title.should.be.equal(post.title);
931 p.content.should.be.equal(post.content);
932 p.id.should.be.equal(post.id);
933
934 Post.findById(p.id, function(err, p) {
935 p.id.should.be.equal(post.id);
936 should.not.exist(p._id);
937 p.content.should.be.equal(post.content);
938 p.title.should.be.equal(post.title);
939 p.id.should.be.equal(post.id);
940 done();
941 });
942 });
943 });
944 });
945});
946
947describe('DataSource connector types', function() {
948 it('returns an array of types using getTypes', function() {
949 const ds = new DataSource('memory');
950 const types = ds.getTypes();
951 assert.deepEqual(types, ['db', 'nosql', 'memory']);
952 });
953
954 describe('supportTypes', function() {
955 it('tests supported types by string', function() {
956 const ds = new DataSource('memory');
957 const result = ds.supportTypes('db');
958 assert(result);
959 });
960
961 it('tests supported types by array', function() {
962 const ds = new DataSource('memory');
963 const result = ds.supportTypes(['db', 'memory']);
964 assert(result);
965 });
966
967 it('tests unsupported types by string', function() {
968 const ds = new DataSource('memory');
969 const result = ds.supportTypes('rdbms');
970 assert(!result);
971 });
972
973 it('tests unsupported types by array', function() {
974 const ds = new DataSource('memory');
975 let result = ds.supportTypes(['rdbms', 'memory']);
976 assert(!result);
977
978 result = ds.supportTypes(['rdbms']);
979 assert(!result);
980 });
981 });
982});
983
984describe('DataSource._resolveConnector', function() {
985 // Mocked require
986 const loader = function(name) {
987 if (name.indexOf('./connectors/') !== -1) {
988 // ./connectors/<name> doesn't exist
989 return null;
990 }
991 if (name === 'loopback-connector-abc') {
992 // Assume loopback-connector-abc doesn't exist
993 return null;
994 }
995 return {
996 name: name,
997 };
998 };
999
1000 it('resolves connector by path', function() {
1001 const connector = DataSource._resolveConnector(__dirname + '/../lib/connectors/memory');
1002 assert(connector.connector);
1003 });
1004 it('resolves connector by internal name', function() {
1005 const connector = DataSource._resolveConnector('memory');
1006 assert(connector.connector);
1007 });
1008 it('resolves connector by module name starting with loopback-connector-', function() {
1009 const connector = DataSource._resolveConnector('loopback-connector-xyz', loader);
1010 assert(connector.connector);
1011 });
1012 it('resolves connector by short module name with full name first', function() {
1013 const connector = DataSource._resolveConnector('xyz', loader);
1014 assert(connector.connector);
1015 assert.equal(connector.connector.name, 'loopback-connector-xyz');
1016 });
1017 it('resolves connector by short module name', function() {
1018 const connector = DataSource._resolveConnector('abc', loader);
1019 assert(connector.connector);
1020 assert.equal(connector.connector.name, 'abc');
1021 });
1022 it('resolves connector by short module name for known connectors', function() {
1023 const connector = DataSource._resolveConnector('oracle', loader);
1024 assert(connector.connector);
1025 assert.equal(connector.connector.name, 'loopback-connector-oracle');
1026 });
1027 it('resolves connector by full module name', function() {
1028 const connector = DataSource._resolveConnector('loopback-xyz', loader);
1029 assert(connector.connector);
1030 });
1031 it('fails to resolve connector by module name starting with loopback-connector-', function() {
1032 const connector = DataSource._resolveConnector('loopback-connector-xyz');
1033 assert(!connector.connector);
1034 assert(connector.error.indexOf('loopback-connector-xyz') !== -1);
1035 });
1036 it('fails resolve invalid connector by short module name', function() {
1037 const connector = DataSource._resolveConnector('xyz');
1038 assert(!connector.connector);
1039 assert(connector.error.indexOf('loopback-connector-xyz') !== -1);
1040 });
1041 it('fails to resolve invalid connector by full module name', function() {
1042 const connector = DataSource._resolveConnector('loopback-xyz');
1043 assert(!connector.connector);
1044 assert(connector.error.indexOf('loopback-connector-loopback-xyz') !== -1);
1045 });
1046});
1047
1048describe('Model define with relations configuration', function() {
1049 it('sets up hasMany relations', function(done) {
1050 const ds = new DataSource('memory');
1051
1052 const Post = ds.define('Post', {userId: Number, content: String});
1053 const User = ds.define('User', {name: String}, {
1054 relations: {posts: {type: 'hasMany', model: 'Post'}},
1055 });
1056
1057 assert(User.relations['posts']);
1058 done();
1059 });
1060
1061 it('sets up belongsTo relations', function(done) {
1062 const ds = new DataSource('memory');
1063
1064 const User = ds.define('User', {name: String});
1065 const Post = ds.define('Post', {userId: Number, content: String}, {
1066 relations: {user: {type: 'belongsTo', model: 'User'}},
1067 });
1068
1069 assert(Post.relations['user']);
1070 done();
1071 });
1072
1073 it('sets up referencesMany relations', function(done) {
1074 const ds = new DataSource('memory');
1075
1076 const Post = ds.define('Post', {userId: Number, content: String});
1077 const User = ds.define('User', {name: String}, {
1078 relations: {posts: {type: 'referencesMany', model: 'Post'}},
1079 });
1080
1081 assert(User.relations['posts']);
1082 done();
1083 });
1084
1085 it('sets up embedsMany relations', function(done) {
1086 const ds = new DataSource('memory');
1087
1088 const Post = ds.define('Post', {userId: Number, content: String});
1089 const User = ds.define('User', {name: String}, {
1090 relations: {posts: {type: 'embedsMany', model: 'Post'}},
1091 });
1092
1093 assert(User.relations['posts']);
1094 done();
1095 });
1096
1097 it('sets up belongsTo polymorphic relation with `{polymorphic: true}`', function(done) {
1098 const ds = new DataSource('memory');
1099
1100 const Product = ds.define('Product', {name: String}, {relations: {
1101 pictures: {type: 'hasMany', model: 'Picture', polymorphic: 'imageable'},
1102 }});
1103 const Picture = ds.define('Picture', {name: String}, {relations: {
1104 imageable: {type: 'belongsTo', polymorphic: true},
1105 }});
1106
1107 assert(Picture.relations['imageable']);
1108 assert.deepEqual(Picture.relations['imageable'].toJSON(), {
1109 name: 'imageable',
1110 type: 'belongsTo',
1111 modelFrom: 'Picture',
1112 keyFrom: 'imageableId',
1113 modelTo: '<polymorphic>',
1114 keyTo: 'id',
1115 multiple: false,
1116 polymorphic: {
1117 selector: 'imageable',
1118 foreignKey: 'imageableId',
1119 discriminator: 'imageableType',
1120 },
1121 });
1122 done();
1123 });
1124
1125 it('sets up hasMany polymorphic relation with `{polymorphic: belongsToRelationName}`', function(done) {
1126 const ds = new DataSource('memory');
1127
1128 const Picture = ds.define('Picture', {name: String}, {relations: {
1129 imageable: {type: 'belongsTo', polymorphic: true},
1130 }});
1131 const Product = ds.define('Product', {name: String}, {relations: {
1132 pictures: {type: 'hasMany', model: 'Picture', polymorphic: 'imageable'},
1133 }});
1134
1135 assert(Product.relations['pictures']);
1136 assert.deepEqual(Product.relations['pictures'].toJSON(), {
1137 name: 'pictures',
1138 type: 'hasMany',
1139 modelFrom: 'Product',
1140 keyFrom: 'id',
1141 modelTo: 'Picture',
1142 keyTo: 'imageableId',
1143 multiple: true,
1144 polymorphic: {
1145 selector: 'imageable',
1146 foreignKey: 'imageableId',
1147 discriminator: 'imageableType',
1148 },
1149 });
1150 done();
1151 });
1152
1153 it('creates a foreign key with the correct type', function(done) {
1154 const ds = new DataSource('memory');
1155
1156 const User = ds.define('User', {name: String, id: {type: String, id: true}});
1157 const Post = ds.define('Post', {content: String}, {relations: {
1158 user: {type: 'belongsTo', model: 'User'}},
1159 });
1160
1161 const fk = Post.definition.properties['userId'];
1162 assert(fk, 'The foreign key should be added');
1163 assert(fk.type === String, 'The foreign key should be the same type as primary key');
1164 assert(Post.relations['user'], 'User relation should be set');
1165 done();
1166 });
1167
1168 it('sets up related hasMany and belongsTo relations', function(done) {
1169 const ds = new DataSource('memory');
1170
1171 const User = ds.define('User', {name: String}, {
1172 relations: {
1173 posts: {type: 'hasMany', model: 'Post'},
1174 accounts: {type: 'hasMany', model: 'Account'},
1175 },
1176 });
1177
1178 assert(!User.relations['posts']);
1179 assert(!User.relations['accounts']);
1180
1181 const Post = ds.define('Post', {userId: Number, content: String}, {
1182 relations: {user: {type: 'belongsTo', model: 'User'}},
1183 });
1184
1185 const Account = ds.define('Account', {userId: Number, type: String}, {
1186 relations: {user: {type: 'belongsTo', model: 'User'}},
1187 });
1188
1189 assert(Post.relations['user']);
1190 assert.deepEqual(Post.relations['user'].toJSON(), {
1191 name: 'user',
1192 type: 'belongsTo',
1193 modelFrom: 'Post',
1194 keyFrom: 'userId',
1195 modelTo: 'User',
1196 keyTo: 'id',
1197 multiple: false,
1198 });
1199 assert(User.relations['posts']);
1200 assert.deepEqual(User.relations['posts'].toJSON(), {
1201 name: 'posts',
1202 type: 'hasMany',
1203 modelFrom: 'User',
1204 keyFrom: 'id',
1205 modelTo: 'Post',
1206 keyTo: 'userId',
1207 multiple: true,
1208 });
1209 assert(User.relations['accounts']);
1210 assert.deepEqual(User.relations['accounts'].toJSON(), {
1211 name: 'accounts',
1212 type: 'hasMany',
1213 modelFrom: 'User',
1214 keyFrom: 'id',
1215 modelTo: 'Account',
1216 keyTo: 'userId',
1217 multiple: true,
1218 });
1219
1220 done();
1221 });
1222
1223 it('throws an error if a relation is missing type', function(done) {
1224 const ds = new DataSource('memory');
1225
1226 const Post = ds.define('Post', {userId: Number, content: String});
1227
1228 try {
1229 const User = ds.define('User', {name: String}, {
1230 relations: {posts: {model: 'Post'}},
1231 });
1232 } catch (e) {
1233 done();
1234 }
1235 });
1236
1237 it('throws an error if a relation type is invalid', function(done) {
1238 const ds = new DataSource('memory');
1239
1240 const Post = ds.define('Post', {userId: Number, content: String});
1241
1242 try {
1243 const User = ds.define('User', {name: String}, {
1244 relations: {posts: {type: 'hasXYZ', model: 'Post'}},
1245 });
1246 } catch (e) {
1247 done();
1248 }
1249 });
1250
1251 it('sets up hasMany through relations', function(done) {
1252 const ds = new DataSource('memory');
1253 const Physician = ds.createModel('Physician', {
1254 name: String,
1255 }, {
1256 relations: {
1257 patients: {model: 'Patient', type: 'hasMany', through: 'Appointment'},
1258 },
1259 });
1260
1261 const Patient = ds.createModel('Patient', {
1262 name: String,
1263 }, {
1264 relations: {
1265 physicians: {model: 'Physician', type: 'hasMany', through: 'Appointment'},
1266 },
1267 });
1268
1269 assert(!Physician.relations['patients']); // Appointment hasn't been resolved yet
1270 assert(!Patient.relations['physicians']); // Appointment hasn't been resolved yet
1271
1272 const Appointment = ds.createModel('Appointment', {
1273 physicianId: Number,
1274 patientId: Number,
1275 appointmentDate: Date,
1276 }, {
1277 relations: {
1278 patient: {type: 'belongsTo', model: 'Patient'},
1279 physician: {type: 'belongsTo', model: 'Physician'},
1280 },
1281 });
1282
1283 assert(Physician.relations['patients']);
1284 assert(Patient.relations['physicians']);
1285 done();
1286 });
1287
1288 it('sets up hasMany through relations with options', function(done) {
1289 const ds = new DataSource('memory');
1290 const Physician = ds.createModel('Physician', {
1291 name: String,
1292 }, {
1293 relations: {
1294 patients: {model: 'Patient', type: 'hasMany', foreignKey: 'leftId', through: 'Appointment'},
1295 },
1296 });
1297
1298 const Patient = ds.createModel('Patient', {
1299 name: String,
1300 }, {
1301 relations: {
1302 physicians: {model: 'Physician', type: 'hasMany', foreignKey: 'rightId', through: 'Appointment'},
1303 },
1304 });
1305
1306 const Appointment = ds.createModel('Appointment', {
1307 physicianId: Number,
1308 patientId: Number,
1309 appointmentDate: Date,
1310 }, {
1311 relations: {
1312 patient: {type: 'belongsTo', model: 'Patient'},
1313 physician: {type: 'belongsTo', model: 'Physician'},
1314 },
1315 });
1316
1317 assert(Physician.relations['patients'].keyTo === 'leftId');
1318 assert(Patient.relations['physicians'].keyTo === 'rightId');
1319 done();
1320 });
1321
1322 it('sets up relations after attach', function(done) {
1323 const ds = new DataSource('memory');
1324 const modelBuilder = new ModelBuilder();
1325
1326 const Post = modelBuilder.define('Post', {userId: Number, content: String});
1327 const User = modelBuilder.define('User', {name: String}, {
1328 relations: {posts: {type: 'hasMany', model: 'Post'},
1329 }});
1330
1331 assert(!User.relations['posts']);
1332 Post.attachTo(ds);
1333 User.attachTo(ds);
1334 assert(User.relations['posts']);
1335 done();
1336 });
1337});
1338
1339describe('Model define with scopes configuration', function() {
1340 it('creates scopes', function(done) {
1341 const ds = new DataSource('memory');
1342 const User = ds.define('User', {name: String, vip: Boolean, age: Number},
1343 {scopes: {vips: {where: {vip: true}}, top5: {limit: 5, order: 'age'}}});
1344
1345 const users = [];
1346 for (let i = 0; i < 10; i++) {
1347 users.push({name: 'User' + i, vip: i % 3 === 0, age: 20 + i * 2});
1348 }
1349 async.each(users, function(user, callback) {
1350 User.create(user, callback);
1351 }, function(err) {
1352 User.vips(function(err, vips) {
1353 if (err) {
1354 return done(err);
1355 }
1356 assert.equal(vips.length, 4);
1357 User.top5(function(err, top5) {
1358 assert.equal(top5.length, 5);
1359 done(err);
1360 });
1361 });
1362 });
1363 });
1364});
1365
1366describe('DataAccessObject', function() {
1367 let ds, model, where, error, filter;
1368
1369 before(function() {
1370 ds = new DataSource('memory');
1371 model = ds.createModel('M1', {
1372 id: {type: String, id: true},
1373 age: Number,
1374 string: 'string',
1375 vip: Boolean,
1376 date: Date,
1377 location: 'GeoPoint',
1378 scores: [Number],
1379 array: 'array',
1380 object: 'object',
1381 });
1382 });
1383
1384 beforeEach(function() {
1385 error = null;
1386 });
1387
1388 it('coerces where clause for string types', function() {
1389 where = model._coerce({id: 1});
1390 assert.deepEqual(where, {id: '1'});
1391 where = model._coerce({id: '1'});
1392 assert.deepEqual(where, {id: '1'});
1393
1394 // Mockup MongoDB ObjectID
1395 function ObjectID(id) {
1396 this.id = id;
1397 }
1398
1399 ObjectID.prototype.toString = function() {
1400 return this.id;
1401 };
1402
1403 where = model._coerce({id: new ObjectID('1')});
1404 assert.deepEqual(where, {id: '1'});
1405 });
1406
1407 it('coerces where clause for number types', function() {
1408 where = model._coerce({age: '10'});
1409 assert.deepEqual(where, {age: 10});
1410
1411 where = model._coerce({age: 10});
1412 assert.deepEqual(where, {age: 10});
1413
1414 where = model._coerce({age: {gt: 10}});
1415 assert.deepEqual(where, {age: {gt: 10}});
1416
1417 where = model._coerce({age: {gt: '10'}});
1418 assert.deepEqual(where, {age: {gt: 10}});
1419
1420 where = model._coerce({age: {between: ['10', '20']}});
1421 assert.deepEqual(where, {age: {between: [10, 20]}});
1422 });
1423
1424 it('coerces where clause for array types', function() {
1425 where = model._coerce({scores: ['10', '20']});
1426 assert.deepEqual(where, {scores: [10, 20]});
1427 });
1428
1429 it('coerces where clause for date types', function() {
1430 const d = new Date();
1431 where = model._coerce({date: d});
1432 assert.deepEqual(where, {date: d});
1433
1434 where = model._coerce({date: d.toISOString()});
1435 assert.deepEqual(where, {date: d});
1436 });
1437
1438 it('coerces where clause for boolean types', function() {
1439 where = model._coerce({vip: 'true'});
1440 assert.deepEqual(where, {vip: true});
1441
1442 where = model._coerce({vip: true});
1443 assert.deepEqual(where, {vip: true});
1444
1445 where = model._coerce({vip: 'false'});
1446 assert.deepEqual(where, {vip: false});
1447
1448 where = model._coerce({vip: false});
1449 assert.deepEqual(where, {vip: false});
1450
1451 where = model._coerce({vip: '1'});
1452 assert.deepEqual(where, {vip: true});
1453
1454 where = model._coerce({vip: 0});
1455 assert.deepEqual(where, {vip: false});
1456
1457 where = model._coerce({vip: ''});
1458 assert.deepEqual(where, {vip: false});
1459 });
1460
1461 it('coerces where clause with and operators', function() {
1462 where = model._coerce({and: [{age: '10'}, {vip: 'true'}]});
1463 assert.deepEqual(where, {and: [{age: 10}, {vip: true}]});
1464 });
1465
1466 it('coerces where clause with or operators', function() {
1467 where = model._coerce({or: [{age: '10'}, {vip: 'true'}]});
1468 assert.deepEqual(where, {or: [{age: 10}, {vip: true}]});
1469 });
1470
1471 it('continues to coerce properties after a logical operator', function() {
1472 const clause = {and: [{age: '10'}], vip: 'true'};
1473
1474 // Key order is predictable but not guaranteed. We prefer false negatives (failure) to false positives.
1475 assert(Object.keys(clause)[0] === 'and', 'Unexpected key order.');
1476
1477 where = model._coerce(clause);
1478 assert.deepEqual(where, {and: [{age: 10}], vip: true});
1479 });
1480
1481 const COERCIONS = [
1482 {
1483 in: {scores: {0: '10', 1: '20'}},
1484 out: {scores: [10, 20]},
1485 },
1486 {
1487 in: {and: {0: {age: '10'}, 1: {vip: 'true'}}},
1488 out: {and: [{age: 10}, {vip: true}]},
1489 },
1490 {
1491 in: {or: {0: {age: '10'}, 1: {vip: 'true'}}},
1492 out: {or: [{age: 10}, {vip: true}]},
1493 },
1494 {
1495 in: {id: {inq: {0: 'aaa', 1: 'bbb'}}},
1496 out: {id: {inq: ['aaa', 'bbb']}},
1497 },
1498 {
1499 in: {id: {nin: {0: 'aaa', 1: 'bbb'}}},
1500 out: {id: {nin: ['aaa', 'bbb']}},
1501 },
1502 {
1503 in: {scores: {between: {0: '0', 1: '42'}}},
1504 out: {scores: {between: [0, 42]}},
1505 },
1506 ];
1507
1508 COERCIONS.forEach(coercion => {
1509 const inStr = JSON.stringify(coercion.in);
1510 it('coerces where clause with array-like objects ' + inStr, () => {
1511 assert.deepEqual(model._coerce(coercion.in), coercion.out);
1512 });
1513 });
1514
1515 const INVALID_CLAUSES = [
1516 {scores: {inq: {0: '10', 1: '20', 4: '30'}}},
1517 {scores: {inq: {0: '10', 1: '20', bogus: 'true'}}},
1518 {scores: {between: {0: '10', 1: '20', 2: '30'}}},
1519 ];
1520
1521 INVALID_CLAUSES.forEach((where) => {
1522 const whereStr = JSON.stringify(where);
1523 it('throws an error on malformed array-like object ' + whereStr, () => {
1524 assert.throws(() => model._coerce(where), /property has invalid clause/);
1525 });
1526 });
1527
1528 it('throws an error if the where property is not an object', function() {
1529 try {
1530 // The where clause has to be an object
1531 model._coerce('abc');
1532 } catch (err) {
1533 error = err;
1534 }
1535 assert(error, 'An error should have been thrown');
1536 });
1537
1538 it('throws an error if the where property is an array', function() {
1539 try {
1540 // The where clause cannot be an array
1541 model._coerce([
1542 {vip: true},
1543 ]);
1544 } catch (err) {
1545 error = err;
1546 }
1547 assert(error, 'An error should have been thrown');
1548 });
1549
1550 it('throws an error if the and operator is not configured with an array', function() {
1551 try {
1552 // The and operator only takes an array of objects
1553 model._coerce({and: {x: 1}});
1554 } catch (err) {
1555 error = err;
1556 }
1557 assert(error, 'An error should have been thrown');
1558 });
1559
1560 it('throws an error if the or operator does not take an array', function() {
1561 try {
1562 // The or operator only takes an array of objects
1563 model._coerce({or: {x: 1}});
1564 } catch (err) {
1565 error = err;
1566 }
1567 assert(error, 'An error should have been thrown');
1568 });
1569
1570 it('throws an error if the or operator not configured with an array of objects', function() {
1571 try {
1572 // The or operator only takes an array of objects
1573 model._coerce({or: ['x']});
1574 } catch (err) {
1575 error = err;
1576 }
1577 assert(error, 'An error should have been thrown');
1578 });
1579
1580 it('throws an error when malformed logical operators follow valid logical clauses', function() {
1581 const invalid = {and: [{x: 1}], or: 'bogus'};
1582
1583 // Key order is predictable but not guaranteed. We prefer false negatives (failure) to false positives.
1584 assert(Object.keys(invalid)[0] !== 'or', 'Unexpected key order.');
1585
1586 try {
1587 model._coerce(invalid);
1588 } catch (err) {
1589 error = err;
1590 }
1591 assert(error, 'An error should have been thrown');
1592 });
1593
1594 it('throws an error if the filter property is not an object', function() {
1595 let filter = null;
1596 try {
1597 // The filter clause has to be an object
1598 filter = model._normalize('abc');
1599 } catch (err) {
1600 error = err;
1601 }
1602 assert(error, 'An error should have been thrown');
1603 });
1604
1605 it('throws an error if the filter.limit property is not a number', function() {
1606 try {
1607 // The limit param must be a valid number
1608 filter = model._normalize({limit: 'x'});
1609 } catch (err) {
1610 error = err;
1611 }
1612 assert(error, 'An error should have been thrown');
1613 });
1614
1615 it('throws an error if the filter.limit property is negative', function() {
1616 try {
1617 // The limit param must be a valid number
1618 filter = model._normalize({limit: -1});
1619 } catch (err) {
1620 error = err;
1621 }
1622 assert(error, 'An error should have been thrown');
1623 });
1624
1625 it('throws an error if the filter.limit property is not an integer', function() {
1626 try {
1627 // The limit param must be a valid number
1628 filter = model._normalize({limit: 5.8});
1629 } catch (err) {
1630 error = err;
1631 }
1632 assert(error, 'An error should have been thrown');
1633 });
1634
1635 it('throws an error if filter.offset property is not a number', function() {
1636 try {
1637 // The limit param must be a valid number
1638 filter = model._normalize({offset: 'x'});
1639 } catch (err) {
1640 error = err;
1641 }
1642 assert(error, 'An error should have been thrown');
1643 });
1644
1645 it('throws an error if the filter.skip property is not a number', function() {
1646 try {
1647 // The limit param must be a valid number
1648 filter = model._normalize({skip: '_'});
1649 } catch (err) {
1650 error = err;
1651 }
1652 assert(error, 'An error should have been thrown');
1653 });
1654
1655 it('normalizes limit/offset/skip', function() {
1656 filter = model._normalize({limit: '10', skip: 5});
1657 assert.deepEqual(filter, {limit: 10, offset: 5, skip: 5});
1658 });
1659
1660 it('uses a default value for limit', function() {
1661 filter = model._normalize({skip: 5});
1662 assert.deepEqual(filter, {limit: 100, offset: 5, skip: 5});
1663 });
1664
1665 it('applies settings for handling undefined', function() {
1666 filter = model._normalize({filter: {x: undefined}});
1667 assert.deepEqual(filter, {filter: {}});
1668
1669 ds.settings.normalizeUndefinedInQuery = 'ignore';
1670 filter = model._normalize({filter: {x: undefined}});
1671 assert.deepEqual(filter, {filter: {}}, 'Should ignore undefined');
1672
1673 ds.settings.normalizeUndefinedInQuery = 'nullify';
1674 filter = model._normalize({filter: {x: undefined}});
1675 assert.deepEqual(filter, {filter: {x: null}}, 'Should nullify undefined');
1676
1677 ds.settings.normalizeUndefinedInQuery = 'throw';
1678 (function() { model._normalize({filter: {x: undefined}}); }).should.throw(/`undefined` in query/);
1679 });
1680
1681 it('does not coerce GeoPoint', function() {
1682 where = model._coerce({location: {near: {lng: 10, lat: 20}, maxDistance: 20}});
1683 assert.deepEqual(where, {location: {near: {lng: 10, lat: 20}, maxDistance: 20}});
1684 });
1685
1686 it('does not coerce null values', function() {
1687 where = model._coerce({date: null});
1688 assert.deepEqual(where, {date: null});
1689 });
1690
1691 it('does not coerce undefined values', function() {
1692 where = model._coerce({date: undefined});
1693 assert.deepEqual(where, {date: undefined});
1694 });
1695
1696 it('does not coerce empty objects to arrays', function() {
1697 where = model._coerce({object: {}});
1698 where.object.should.not.be.an.Array();
1699 where.object.should.be.an.Object();
1700 });
1701
1702 it('does not coerce an empty array', function() {
1703 where = model._coerce({array: []});
1704 where.array.should.be.an.Array();
1705 where.array.should.have.length(0);
1706 });
1707
1708 it('does not coerce to a number for a simple value that produces NaN',
1709 function() {
1710 where = model._coerce({age: 'xyz'});
1711 assert.deepEqual(where, {age: 'xyz'});
1712 });
1713
1714 it('does not coerce to a number for a simple value in an array that produces NaN',
1715 function() {
1716 where = model._coerce({age: {inq: ['xyz', '12']}});
1717 assert.deepEqual(where, {age: {inq: ['xyz', 12]}});
1718 });
1719
1720 it('does not coerce to a string for a regexp value in an array ',
1721 function() {
1722 where = model._coerce({string: {inq: [/xyz/i, new RegExp(/xyz/i)]}});
1723 assert.deepEqual(where, {string: {inq: [/xyz/i, /xyz/i]}});
1724 });
1725
1726 // settings
1727 it('gets settings in priority',
1728 function() {
1729 ds.settings.test = 'test';
1730 assert.equal(model._getSetting('test'), ds.settings.test, 'Should get datasource setting');
1731 ds.settings.test = undefined;
1732
1733 model.settings.test = 'test';
1734 assert.equal(model._getSetting('test'), model.settings.test, 'Should get model settings');
1735
1736 ds.settings.test = 'willNotGet';
1737 assert.notEqual(model._getSetting('test'), ds.settings.test, 'Should not get datasource setting');
1738 });
1739});
1740
1741describe('ModelBuilder processing json files', function() {
1742 const path = require('path'),
1743 fs = require('fs');
1744
1745 /**
1746 * Load LDL schemas from a json doc
1747 * @param schemaFile The dataSource json file
1748 * @returns A map of schemas keyed by name
1749 */
1750 function loadSchemasSync(schemaFile, dataSource) {
1751 let modelBuilder, createModel;
1752 // Set up the data source
1753 if (!dataSource) {
1754 modelBuilder = new ModelBuilder();
1755 } else {
1756 modelBuilder = dataSource.modelBuilder;
1757 createModel = dataSource.createModel.bind(dataSource);
1758 }
1759
1760 // Read the dataSource JSON file
1761 const schemas = JSON.parse(fs.readFileSync(schemaFile));
1762 return modelBuilder.buildModels(schemas, createModel);
1763 }
1764
1765 it('defines models', function() {
1766 let models = loadSchemasSync(path.join(__dirname, 'test1-schemas.json'));
1767
1768 models.should.have.property('AnonymousModel_0');
1769 models.AnonymousModel_0.should.have.property('modelName', 'AnonymousModel_0');
1770
1771 const m1 = new models.AnonymousModel_0({title: 'Test'});
1772 m1.should.have.property('title', 'Test');
1773 m1.should.have.property('author', 'Raymond');
1774
1775 models = loadSchemasSync(path.join(__dirname, 'test2-schemas.json'));
1776 models.should.have.property('Address');
1777 models.should.have.property('Account');
1778 models.should.have.property('Customer');
1779 for (const s in models) {
1780 const m = models[s];
1781 assert(new m());
1782 }
1783 });
1784
1785 it('attaches models to a specified dataSource', function() {
1786 const ds = new DataSource('memory');
1787
1788 const models = loadSchemasSync(path.join(__dirname, 'test2-schemas.json'), ds);
1789 models.should.have.property('Address');
1790 models.should.have.property('Account');
1791 models.should.have.property('Customer');
1792 assert.equal(models.Address.dataSource, ds);
1793 });
1794
1795 it('allows customization of default model base class', function() {
1796 const modelBuilder = new ModelBuilder();
1797
1798 const User = modelBuilder.define('User', {
1799 name: String,
1800 bio: ModelBuilder.Text,
1801 approved: Boolean,
1802 joinedAt: Date,
1803 age: Number,
1804 });
1805
1806 modelBuilder.defaultModelBaseClass = User;
1807
1808 const Customer = modelBuilder.define('Customer', {customerId: {type: String, id: true}});
1809 assert(Customer.prototype instanceof User);
1810 });
1811
1812 it('accepts a model base class', function() {
1813 const modelBuilder = new ModelBuilder();
1814
1815 const User = modelBuilder.define('User', {
1816 name: String,
1817 bio: ModelBuilder.Text,
1818 approved: Boolean,
1819 joinedAt: Date,
1820 age: Number,
1821 });
1822
1823 const Customer = modelBuilder.define('Customer',
1824 {customerId: {type: String, id: true}}, {}, User);
1825 assert(Customer.prototype instanceof User);
1826 });
1827});
1828
1829describe('DataSource constructor', function() {
1830 it('takes url as the settings', function() {
1831 const ds = new DataSource('memory://localhost/mydb?x=1');
1832 assert.equal(ds.connector.name, 'memory');
1833 });
1834
1835 it('takes connector name', function() {
1836 const ds = new DataSource('memory');
1837 assert.equal(ds.connector.name, 'memory');
1838 });
1839
1840 it('takes settings object', function() {
1841 const ds = new DataSource({connector: 'memory'});
1842 assert.equal(ds.connector.name, 'memory');
1843 });
1844
1845 it('takes settings object and name', function() {
1846 const ds = new DataSource('x', {connector: 'memory'});
1847 assert.equal(ds.connector.name, 'memory');
1848 });
1849});
1850
1851describe('ModelBuilder options.models', function() {
1852 it('injects model classes from models', function() {
1853 const builder = new ModelBuilder();
1854 const M1 = builder.define('M1');
1855 const M2 = builder.define('M2', {}, {models: {
1856 'M1': M1,
1857 }});
1858
1859 assert.equal(M2.M1, M1, 'M1 should be injected to M2');
1860 });
1861
1862 it('injects model classes by name in the models', function() {
1863 const builder = new ModelBuilder();
1864 const M1 = builder.define('M1');
1865 const M2 = builder.define('M2', {}, {models: {
1866 'M1': 'M1',
1867 }});
1868
1869 assert.equal(M2.M1, M1, 'M1 should be injected to M2');
1870 });
1871
1872 it('injects model classes by name in the models before the class is defined',
1873 function() {
1874 const builder = new ModelBuilder();
1875 const M2 = builder.define('M2', {}, {models: {
1876 'M1': 'M1',
1877 }});
1878 assert(M2.M1, 'M1 should be injected to M2');
1879 assert(M2.M1.settings.unresolved, 'M1 is still a proxy');
1880 const M1 = builder.define('M1');
1881 assert.equal(M2.M1, M1, 'M1 should be injected to M2');
1882 });
1883
1884 it('uses non-strict mode for embedded models by default', function() {
1885 const builder = new ModelBuilder();
1886 const M1 = builder.define('testEmbedded', {
1887 name: 'string',
1888 address: {
1889 street: 'string',
1890 },
1891 });
1892 const m1 = new M1({
1893 name: 'Jim',
1894 address: {
1895 street: 'washington st',
1896 number: 5512,
1897 },
1898 });
1899 assert.equal(m1.address.number, 5512, 'm1 should contain number property in address');
1900 });
1901
1902 it('uses the strictEmbeddedModels setting (true) when applied on modelBuilder', function() {
1903 const builder = new ModelBuilder();
1904 builder.settings.strictEmbeddedModels = true;
1905 const M1 = builder.define('testEmbedded', {
1906 name: 'string',
1907 address: {
1908 street: 'string',
1909 },
1910 });
1911 const m1 = new M1({
1912 name: 'Jim',
1913 address: {
1914 street: 'washington st',
1915 number: 5512,
1916 },
1917 });
1918 assert.equal(m1.address.number, undefined, 'm1 should not contain number property in address');
1919 assert.equal(m1.address.isValid(), false, 'm1 address should not validate with extra property');
1920 const codes = m1.address.errors && m1.address.errors.codes || {};
1921 assert.deepEqual(codes.number, ['unknown-property']);
1922 });
1923});
1924
1925describe('updateOnly', function() {
1926 it('sets forceId to true when model id is generated', function(done) {
1927 const ds = new DataSource('memory');
1928 const Post = ds.define('Post', {
1929 title: {type: String, length: 255},
1930 date: {type: Date, default: function() {
1931 return new Date();
1932 }},
1933 });
1934 // check if forceId is added as true in ModelClass's settings[] explicitly,
1935 // if id a generated (default) and forceId in from the model is
1936 // true(unspecified is 'true' which is the default).
1937 Post.settings.should.have.property('forceId').eql('auto');
1938 done();
1939 });
1940
1941 it('flags id as updateOnly when forceId is undefined', function(done) {
1942 const ds = new DataSource('memory');
1943 const Post = ds.define('Post', {
1944 title: {type: String, length: 255},
1945 date: {type: Date, default: function() {
1946 return new Date();
1947 }},
1948 });
1949 // check if method getUpdateOnlyProperties exist in ModelClass and check if
1950 // the Post has 'id' in updateOnlyProperties list
1951 Post.should.have.property('getUpdateOnlyProperties');
1952 Post.getUpdateOnlyProperties().should.eql(['id']);
1953 done();
1954 });
1955
1956 it('does not flag id as updateOnly when forceId is false', function(done) {
1957 const ds = new DataSource('memory');
1958 const Person = ds.define('Person', {
1959 name: String,
1960 gender: String,
1961 }, {forceId: false});
1962 // id should not be there in updateOnly properties list if forceId is set
1963 // to false
1964 Person.should.have.property('getUpdateOnlyProperties');
1965 Person.getUpdateOnlyProperties().should.eql([]);
1966 done();
1967 });
1968
1969 it('flags id as updateOnly when forceId is true', function(done) {
1970 const ds = new DataSource('memory');
1971 const Person = ds.define('Person', {
1972 name: String,
1973 gender: String,
1974 }, {forceId: true});
1975 // id should be there in updateOnly properties list if forceId is set
1976 // to true
1977 Person.should.have.property('getUpdateOnlyProperties');
1978 Person.getUpdateOnlyProperties().should.eql(['id']);
1979 done();
1980 });
1981});