UNPKG

21.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');
10
11const jdb = require('../');
12const ModelBuilder = jdb.ModelBuilder;
13const DataSource = jdb.DataSource;
14const Memory = require('../lib/connectors/memory');
15
16const ModelDefinition = require('../lib/model-definition');
17
18describe('ModelDefinition class', function() {
19 let memory;
20 beforeEach(function() {
21 memory = new DataSource({connector: Memory});
22 });
23
24 it('should be able to define plain models', function(done) {
25 const modelBuilder = new ModelBuilder();
26
27 const User = new ModelDefinition(modelBuilder, 'User', {
28 name: 'string',
29 bio: ModelBuilder.Text,
30 approved: Boolean,
31 joinedAt: Date,
32 age: 'number',
33 });
34
35 User.build();
36 assert.equal(User.properties.name.type, String);
37 assert.equal(User.properties.bio.type, ModelBuilder.Text);
38 assert.equal(User.properties.approved.type, Boolean);
39 assert.equal(User.properties.joinedAt.type, Date);
40 assert.equal(User.properties.age.type, Number);
41
42 const json = User.toJSON();
43 assert.equal(json.name, 'User');
44 assert.equal(json.properties.name.type, 'String');
45 assert.equal(json.properties.bio.type, 'Text');
46 assert.equal(json.properties.approved.type, 'Boolean');
47 assert.equal(json.properties.joinedAt.type, 'Date');
48 assert.equal(json.properties.age.type, 'Number');
49
50 assert.deepEqual(User.toJSON(), json);
51
52 done();
53 });
54
55 it('should be able to define additional properties', function(done) {
56 const modelBuilder = new ModelBuilder();
57
58 const User = new ModelDefinition(modelBuilder, 'User', {
59 name: 'string',
60 bio: ModelBuilder.Text,
61 approved: Boolean,
62 joinedAt: Date,
63 age: 'number',
64 });
65
66 User.build();
67
68 let json = User.toJSON();
69
70 User.defineProperty('id', {type: 'number', id: true});
71 assert.equal(User.properties.name.type, String);
72 assert.equal(User.properties.bio.type, ModelBuilder.Text);
73 assert.equal(User.properties.approved.type, Boolean);
74 assert.equal(User.properties.joinedAt.type, Date);
75 assert.equal(User.properties.age.type, Number);
76
77 assert.equal(User.properties.id.type, Number);
78
79 json = User.toJSON();
80 assert.deepEqual(json.properties.id, {type: 'Number', id: true});
81
82 done();
83 });
84
85 it('should be able to define nesting models', function(done) {
86 const modelBuilder = new ModelBuilder();
87
88 const User = new ModelDefinition(modelBuilder, 'User', {
89 name: String,
90 bio: ModelBuilder.Text,
91 approved: Boolean,
92 joinedAt: Date,
93 age: Number,
94 address: {
95 street: String,
96 city: String,
97 zipCode: String,
98 state: String,
99 },
100 });
101
102 User.build();
103 assert.equal(User.properties.name.type, String);
104 assert.equal(User.properties.bio.type, ModelBuilder.Text);
105 assert.equal(User.properties.approved.type, Boolean);
106 assert.equal(User.properties.joinedAt.type, Date);
107 assert.equal(User.properties.age.type, Number);
108 assert.equal(typeof User.properties.address.type, 'function');
109
110 const json = User.toJSON();
111 assert.equal(json.name, 'User');
112 assert.equal(json.properties.name.type, 'String');
113 assert.equal(json.properties.bio.type, 'Text');
114 assert.equal(json.properties.approved.type, 'Boolean');
115 assert.equal(json.properties.joinedAt.type, 'Date');
116 assert.equal(json.properties.age.type, 'Number');
117
118 assert.deepEqual(json.properties.address.type, {street: {type: 'String'},
119 city: {type: 'String'},
120 zipCode: {type: 'String'},
121 state: {type: 'String'}});
122
123 done();
124 });
125
126 it('should be able to define referencing models', function(done) {
127 const modelBuilder = new ModelBuilder();
128
129 const Address = modelBuilder.define('Address', {
130 street: String,
131 city: String,
132 zipCode: String,
133 state: String,
134 });
135 const User = new ModelDefinition(modelBuilder, 'User', {
136 name: String,
137 bio: ModelBuilder.Text,
138 approved: Boolean,
139 joinedAt: Date,
140 age: Number,
141 address: Address,
142
143 });
144
145 User.build();
146 assert.equal(User.properties.name.type, String);
147 assert.equal(User.properties.bio.type, ModelBuilder.Text);
148 assert.equal(User.properties.approved.type, Boolean);
149 assert.equal(User.properties.joinedAt.type, Date);
150 assert.equal(User.properties.age.type, Number);
151 assert.equal(User.properties.address.type, Address);
152
153 const json = User.toJSON();
154 assert.equal(json.name, 'User');
155 assert.equal(json.properties.name.type, 'String');
156 assert.equal(json.properties.bio.type, 'Text');
157 assert.equal(json.properties.approved.type, 'Boolean');
158 assert.equal(json.properties.joinedAt.type, 'Date');
159 assert.equal(json.properties.age.type, 'Number');
160
161 assert.equal(json.properties.address.type, 'Address');
162
163 done();
164 });
165
166 it('should be able to define referencing models by name', function(done) {
167 const modelBuilder = new ModelBuilder();
168
169 const Address = modelBuilder.define('Address', {
170 street: String,
171 city: String,
172 zipCode: String,
173 state: String,
174 });
175 const User = new ModelDefinition(modelBuilder, 'User', {
176 name: String,
177 bio: ModelBuilder.Text,
178 approved: Boolean,
179 joinedAt: Date,
180 age: Number,
181 address: 'Address',
182
183 });
184
185 User.build();
186 assert.equal(User.properties.name.type, String);
187 assert.equal(User.properties.bio.type, ModelBuilder.Text);
188 assert.equal(User.properties.approved.type, Boolean);
189 assert.equal(User.properties.joinedAt.type, Date);
190 assert.equal(User.properties.age.type, Number);
191 assert.equal(User.properties.address.type, Address);
192
193 const json = User.toJSON();
194 assert.equal(json.name, 'User');
195 assert.equal(json.properties.name.type, 'String');
196 assert.equal(json.properties.bio.type, 'Text');
197 assert.equal(json.properties.approved.type, 'Boolean');
198 assert.equal(json.properties.joinedAt.type, 'Date');
199 assert.equal(json.properties.age.type, 'Number');
200
201 assert.equal(json.properties.address.type, 'Address');
202
203 done();
204 });
205
206 it('should report correct id names', function(done) {
207 const modelBuilder = new ModelBuilder();
208
209 const User = new ModelDefinition(modelBuilder, 'User', {
210 userId: {type: String, id: true},
211 name: 'string',
212 bio: ModelBuilder.Text,
213 approved: Boolean,
214 joinedAt: Date,
215 age: 'number',
216 });
217
218 assert.equal(User.idName(), 'userId');
219 assert.deepEqual(User.idNames(), ['userId']);
220 done();
221 });
222
223 it('should sort id properties by its index', function() {
224 const modelBuilder = new ModelBuilder();
225
226 const User = new ModelDefinition(modelBuilder, 'User', {
227 userId: {type: String, id: 2},
228 userType: {type: String, id: 1},
229 name: 'string',
230 bio: ModelBuilder.Text,
231 approved: Boolean,
232 joinedAt: Date,
233 age: 'number',
234 });
235
236 const ids = User.ids();
237 assert.ok(Array.isArray(ids));
238 assert.equal(ids.length, 2);
239 assert.equal(ids[0].id, 1);
240 assert.equal(ids[0].name, 'userType');
241 assert.equal(ids[1].id, 2);
242 assert.equal(ids[1].name, 'userId');
243 });
244
245 it('should report correct table/column names', function(done) {
246 const modelBuilder = new ModelBuilder();
247
248 const User = new ModelDefinition(modelBuilder, 'User', {
249 userId: {type: String, id: true, oracle: {column: 'ID'}},
250 name: 'string',
251 }, {oracle: {table: 'USER'}});
252
253 assert.equal(User.tableName('oracle'), 'USER');
254 assert.equal(User.tableName('mysql'), 'User');
255 assert.equal(User.columnName('oracle', 'userId'), 'ID');
256 assert.equal(User.columnName('mysql', 'userId'), 'userId');
257 done();
258 });
259
260 describe('maxDepthOfQuery', function() {
261 it('should report errors for deep query than maxDepthOfQuery', function(done) {
262 const MyModel = memory.createModel('my-model', {}, {
263 maxDepthOfQuery: 5,
264 });
265
266 const filter = givenComplexFilter();
267
268 MyModel.find(filter, function(err) {
269 should.exist(err);
270 err.message.should.match('The query object exceeds maximum depth 5');
271 done();
272 });
273 });
274
275 it('should honor maxDepthOfQuery setting', function(done) {
276 const MyModel = memory.createModel('my-model', {}, {
277 maxDepthOfQuery: 20,
278 });
279
280 const filter = givenComplexFilter();
281
282 MyModel.find(filter, function(err) {
283 should.not.exist(err);
284 done();
285 });
286 });
287
288 it('should honor maxDepthOfQuery in options', function(done) {
289 const MyModel = memory.createModel('my-model', {}, {
290 maxDepthOfQuery: 5,
291 });
292
293 const filter = givenComplexFilter();
294
295 MyModel.find(filter, {maxDepthOfQuery: 20}, function(err) {
296 should.not.exist(err);
297 done();
298 });
299 });
300
301 function givenComplexFilter() {
302 const filter = {where: {and: [{and: [{and: [{and: [{and: [{and:
303 [{and: [{and: [{and: [{x: 1}]}]}]}]}]}]}]}]}]}};
304 return filter;
305 }
306 });
307
308 it('should serialize protected properties into JSON', function() {
309 const ProtectedModel = memory.createModel('protected', {}, {
310 protected: ['protectedProperty'],
311 });
312 const pm = new ProtectedModel({
313 id: 1, foo: 'bar', protectedProperty: 'protected',
314 });
315 const serialized = pm.toJSON();
316 assert.deepEqual(serialized, {
317 id: 1, foo: 'bar', protectedProperty: 'protected',
318 });
319 });
320
321 it('should not serialize protected properties of nested models into JSON', function(done) {
322 const Parent = memory.createModel('parent');
323 const Child = memory.createModel('child', {}, {protected: ['protectedProperty']});
324 Parent.hasMany(Child);
325 Parent.create({
326 name: 'parent',
327 }, function(err, parent) {
328 if (err) return done(err);
329 parent.children.create({
330 name: 'child',
331 protectedProperty: 'protectedValue',
332 }, function(err, child{
333 if (err) return done(err);
334 Parent.find({include: 'children'}, function(err, parents) {
335 if (err) return done(err);
336 const serialized = parents[0].toJSON();
337 const child = serialized.children[0];
338 assert.equal(child.name, 'child');
339 assert.notEqual(child.protectedProperty, 'protectedValue');
340 done();
341 });
342 });
343 });
344 });
345
346 it('should not serialize hidden properties into JSON', function() {
347 const HiddenModel = memory.createModel('hidden', {}, {
348 hidden: ['secret'],
349 });
350 const hm = new HiddenModel({
351 id: 1,
352 foo: 'bar',
353 secret: 'secret',
354 });
355 const serialized = hm.toJSON();
356 assert.deepEqual(serialized, {
357 id: 1,
358 foo: 'bar',
359 });
360 });
361
362 it('should not serialize hidden properties of nested models into JSON', function(done) {
363 const Parent = memory.createModel('parent');
364 const Child = memory.createModel('child', {}, {hidden: ['secret']});
365 Parent.hasMany(Child);
366 Parent.create({
367 name: 'parent',
368 }, function(err, parent) {
369 if (err) return done(err);
370 parent.children.create({
371 name: 'child',
372 secret: 'secret',
373 }, function(err, child) {
374 if (err) return done(err);
375 Parent.find({include: 'children'}, function(err, parents) {
376 if (err) return done(err);
377 const serialized = parents[0].toJSON();
378 const child = serialized.children[0];
379 assert.equal(child.name, 'child');
380 assert.notEqual(child.secret, 'secret');
381 done();
382 });
383 });
384 });
385 });
386
387 describe('hidden properties', function() {
388 let Child;
389
390 describe('with hidden array', function() {
391 beforeEach(function() { givenChildren(); });
392
393 it('should be removed if used in where', function() {
394 return Child.find({
395 where: {secret: 'guess'},
396 }, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
397 });
398
399 it('should be removed if used in where.and', function() {
400 return Child.find({
401 where: {and: [{secret: 'guess'}]},
402 }, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
403 });
404
405 it('should be allowed for update', function() {
406 return Child.update({name: 'childA'}, {secret: 'new-secret'}, optionsFromRemoteReq).then(
407 function(result) {
408 result.count.should.equal(1);
409 },
410 );
411 });
412
413 it('should be allowed if prohibitHiddenPropertiesInQuery is `false`', function() {
414 Child.definition.settings.prohibitHiddenPropertiesInQuery = false;
415 return Child.find({
416 where: {secret: 'guess'},
417 }).then(function(children) {
418 children.length.should.equal(1);
419 children[0].secret.should.equal('guess');
420 });
421 });
422
423 it('should be allowed by default if not remote call', function() {
424 return Child.find({
425 where: {secret: 'guess'},
426 }).then(function(children) {
427 children.length.should.equal(1);
428 children[0].secret.should.equal('guess');
429 });
430 });
431
432 it('should be allowed if prohibitHiddenPropertiesInQuery is `false` in options', function() {
433 return Child.find({
434 where: {secret: 'guess'},
435 }, {
436 prohibitHiddenPropertiesInQuery: false,
437 }).then(function(children) {
438 children.length.should.equal(1);
439 children[0].secret.should.equal('guess');
440 });
441 });
442 });
443
444 describe('with hidden object', function() {
445 beforeEach(function() { givenChildren({hiddenProperties: {secret: true}}); });
446
447 it('should be removed if used in where', function() {
448 return Child.find({
449 where: {secret: 'guess'},
450 }, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
451 });
452
453 it('should be removed if used in where.and', function() {
454 return Child.find({
455 where: {and: [{secret: 'guess'}]},
456 }, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
457 });
458 });
459
460 /**
461 * Create two children with a hidden property, one with a matching
462 * value, the other with a non-matching value
463 */
464 function givenChildren(hiddenProps) {
465 hiddenProps = hiddenProps || {hidden: ['secret']};
466 Child = memory.createModel('child', {
467 name: String,
468 secret: String,
469 }, hiddenProps);
470 return Child.create([{
471 name: 'childA',
472 secret: 'secret',
473 }, {
474 name: 'childB',
475 secret: 'guess',
476 }]);
477 }
478
479 function assertHiddenPropertyIsIgnored(children) {
480 // All children are found whether the `secret` condition matches or not
481 // as the condition is removed because it's hidden
482 children.length.should.equal(2);
483 }
484 });
485
486 /**
487 * Mock up for default values set by the remote model
488 */
489 const optionsFromRemoteReq = {
490 prohibitHiddenPropertiesInQuery: true,
491 maxDepthOfQuery: 12,
492 maxDepthOfQuery: 32,
493 };
494
495 describe('hidden nested properties', function() {
496 let Child;
497 beforeEach(givenChildren);
498
499 it('should be removed if used in where as a composite key - x.secret', function() {
500 return Child.find({
501 where: {'x.secret': 'guess'},
502 }, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
503 });
504
505 it('should be removed if used in where as a composite key - secret.y', function() {
506 return Child.find({
507 where: {'secret.y': 'guess'},
508 }, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
509 });
510
511 it('should be removed if used in where as a composite key - a.secret.b', function() {
512 return Child.find({
513 where: {'a.secret.b': 'guess'},
514 }, optionsFromRemoteReq).then(assertHiddenPropertyIsIgnored);
515 });
516
517 function givenChildren() {
518 const hiddenProps = {hidden: ['secret']};
519 Child = memory.createModel('child', {
520 name: String,
521 x: {
522 secret: String,
523 },
524 secret: {
525 y: String,
526 },
527 a: {
528 secret: {
529 b: String,
530 },
531 },
532 }, hiddenProps);
533 return Child.create([{
534 name: 'childA',
535 x: {secret: 'secret'},
536 secret: {y: 'secret'},
537 a: {secret: {b: 'secret'}},
538 }, {
539 name: 'childB',
540 x: {secret: 'guess'},
541 secret: {y: 'guess'},
542 a: {secret: {b: 'guess'}},
543 }]);
544 }
545
546 function assertHiddenPropertyIsIgnored(children) {
547 // All children are found whether the `secret` condition matches or not
548 // as the condition is removed because it's hidden
549 children.length.should.equal(2);
550 }
551 });
552
553 function assertParentIncludeChildren(parents) {
554 parents[0].toJSON().children.length.should.equal(1);
555 }
556
557 describe('protected properties', function() {
558 let Parent;
559 let Child;
560 beforeEach(givenParentAndChild);
561
562 it('should be removed if used in include scope', function() {
563 Parent.find({
564 include: {
565 relation: 'children',
566 scope: {
567 where: {
568 secret: 'x',
569 },
570 },
571 },
572 }, optionsFromRemoteReq).then(assertParentIncludeChildren);
573 });
574
575 it('should be rejected if used in include scope.where.and', function() {
576 return Parent.find({
577 include: {
578 relation: 'children',
579 scope: {
580 where: {
581 and: [{secret: 'x'}],
582 },
583 },
584 },
585 }, optionsFromRemoteReq).then(assertParentIncludeChildren);
586 });
587
588 it('should be removed if a hidden property is used in include scope', function() {
589 return Parent.find({
590 include: {
591 relation: 'children',
592 scope: {
593 where: {
594 secret: 'x',
595 },
596 },
597 },
598 }, optionsFromRemoteReq).then(assertParentIncludeChildren);
599 });
600
601 function givenParentAndChild() {
602 Parent = memory.createModel('parent');
603 Child = memory.createModel('child', {}, {protected: ['secret']});
604 Parent.hasMany(Child);
605 return Parent.create({
606 name: 'parent',
607 }).then(parent => {
608 return parent.children.create({
609 name: 'child',
610 secret: 'secret',
611 });
612 });
613 }
614 });
615
616 describe('hidden properties in include', function() {
617 let Parent;
618 let Child;
619 beforeEach(givenParentAndChildWithHiddenProperty);
620
621 it('should be rejected if used in scope', function() {
622 return Parent.find({
623 include: {
624 relation: 'children',
625 scope: {
626 where: {
627 secret: 'x',
628 },
629 },
630 },
631 }, optionsFromRemoteReq).then(assertParentIncludeChildren);
632 });
633
634 function givenParentAndChildWithHiddenProperty() {
635 Parent = memory.createModel('parent');
636 Child = memory.createModel('child', {}, {hidden: ['secret']});
637 Parent.hasMany(Child);
638 return Parent.create({
639 name: 'parent',
640 }).then(parent => {
641 return parent.children.create({
642 name: 'child',
643 secret: 'secret',
644 });
645 });
646 }
647 });
648
649 it('should throw error for property names containing dot', function() {
650 (function() { memory.createModel('Dotted', {'dot.name': String}); })
651 .should
652 .throw(/dot\(s\).*Dotted.*dot\.name/);
653 });
654
655 it('should report deprecation warning for property named constructor', function() {
656 let message = 'deprecation not reported';
657 process.once('deprecation', function(err) { message = err.message; });
658
659 memory.createModel('Ctor', {'constructor': String});
660
661 message.should.match(/Property name should not be "constructor" in Model: Ctor/);
662 });
663
664 it('should throw error for dynamic property names containing dot',
665 function(done) {
666 const Model = memory.createModel('DynamicDotted');
667 Model.create({'dot.name': 'dot.value'}, function(err) {
668 err.should.be.instanceOf(Error);
669 err.message.should.match(/dot\(s\).*DynamicDotted.*dot\.name/);
670 done();
671 });
672 });
673
674 it('should throw error for dynamic property named constructor', function(done) {
675 const Model = memory.createModel('DynamicCtor');
676 Model.create({'constructor': 'myCtor'}, function(err) {
677 assert.equal(err.message, 'Property name "constructor" is not allowed in DynamicCtor data');
678 done();
679 });
680 });
681
682 it('should support "array" type shortcut', function() {
683 const Model = memory.createModel('TwoArrays', {
684 regular: Array,
685 sugar: 'array',
686 });
687
688 const props = Model.definition.properties;
689 props.regular.type.should.equal(props.sugar.type);
690 });
691
692 context('hasPK', function() {
693 context('with primary key defined', function() {
694 let Todo;
695 before(function prepModel() {
696 Todo = new ModelDefinition(new ModelBuilder(), 'Todo', {
697 content: 'string',
698 });
699 Todo.defineProperty('id', {
700 type: 'number',
701 id: true,
702 });
703 Todo.build();
704 });
705
706 it('should return true', function() {
707 Todo.hasPK().should.be.ok;
708 });
709 });
710
711 context('without primary key defined', function() {
712 let Todo;
713 before(function prepModel() {
714 Todo = new ModelDefinition(new ModelBuilder(), 'Todo', {
715 content: 'string',
716 });
717 Todo.build();
718 });
719
720 it('should return false', function() {
721 Todo.hasPK().should.not.be.ok;
722 });
723 });
724 });
725});