UNPKG

18.9 kBJavaScriptView Raw
1// Copyright IBM Corp. 2017,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 is written in mocha+should.js
7
8'use strict';
9
10const should = require('./init.js');
11const assert = require('assert');
12
13const jdb = require('../');
14const ModelBuilder = jdb.ModelBuilder;
15const DataSource = jdb.DataSource;
16const Memory = require('../lib/connectors/memory');
17
18const ModelDefinition = require('../lib/model-definition');
19
20describe('Model class inheritance', function() {
21 let memory;
22 beforeEach(function() {
23 memory = new DataSource({connector: Memory});
24 });
25
26 describe('ModelBaseClass.getMergePolicy()', function() {
27 const legacyMergePolicy = {
28 description: {replace: true},
29 properties: {patch: true},
30 hidden: {replace: false},
31 protected: {replace: false},
32 relations: {patch: true},
33 acls: {rank: true},
34 };
35
36 const recommendedMergePolicy = {
37 description: {replace: true},
38 options: {patch: true},
39 hidden: {replace: false},
40 protected: {replace: false},
41 indexes: {patch: true},
42 methods: {patch: true},
43 mixins: {patch: true},
44 relations: {patch: true},
45 scope: {replace: true},
46 scopes: {patch: true},
47 acls: {rank: true},
48 __delete: null,
49 __default: {replace: true},
50 };
51
52 let modelBuilder, base;
53
54 beforeEach(function() {
55 modelBuilder = memory.modelBuilder;
56 base = modelBuilder.define('base');
57 });
58
59 it('returns legacy merge policy by default', function() {
60 const mergePolicy = base.getMergePolicy();
61 should.deepEqual(mergePolicy, legacyMergePolicy);
62 });
63
64 it('returns recommended merge policy when called with option ' +
65 '`{configureModelMerge: true}`', function() {
66 const mergePolicy = base.getMergePolicy({configureModelMerge: true});
67 should.deepEqual(mergePolicy, recommendedMergePolicy);
68 });
69
70 it('handles custom merge policy defined via model.settings', function() {
71 let mergePolicy;
72 const newMergePolicy = {
73 relations: {patch: true},
74 };
75
76 // saving original getMergePolicy method
77 const originalGetMergePolicy = base.getMergePolicy;
78
79 // the injected getMergePolicy method captures the provided configureModelMerge option
80 base.getMergePolicy = function(options) {
81 mergePolicy = options && options.configureModelMerge;
82 return originalGetMergePolicy(options);
83 };
84
85 // calling extend() on base model calls base.getMergePolicy() internally
86 // child model settings are passed as 3rd parameter
87 const child = base.extend('child', {}, {configureModelMerge: newMergePolicy});
88
89 should.deepEqual(mergePolicy, newMergePolicy);
90
91 // restoring original getMergePolicy method
92 base.getMergePolicy = originalGetMergePolicy;
93 });
94
95 it('can be extended by user', function() {
96 const alteredMergePolicy = Object.assign({}, recommendedMergePolicy, {
97 __delete: false,
98 });
99 // extending the builtin getMergePolicy function
100 base.getMergePolicy = function(options) {
101 const origin = base.base.getMergePolicy(options);
102 return Object.assign({}, origin, {
103 __delete: false,
104 });
105 };
106 const mergePolicy = base.getMergePolicy({configureModelMerge: true});
107 should.deepEqual(mergePolicy, alteredMergePolicy);
108 });
109
110 it('is inherited by child model', function() {
111 const child = base.extend('child', {}, {configureModelMerge: true});
112 // get mergePolicy from child
113 const mergePolicy = child.getMergePolicy({configureModelMerge: true});
114 should.deepEqual(mergePolicy, recommendedMergePolicy);
115 });
116 });
117
118 describe('Merge policy WITHOUT flag `configureModelMerge`', function() {
119 it('inherits prototype using option.base', function() {
120 const modelBuilder = memory.modelBuilder;
121 const parent = memory.createModel('parent', {}, {
122 relations: {
123 children: {
124 type: 'hasMany',
125 model: 'anotherChild',
126 },
127 },
128 });
129 const baseChild = modelBuilder.define('baseChild');
130 baseChild.attachTo(memory);
131 // the name of this must begin with a letter < b
132 // for this test to fail
133 const anotherChild = baseChild.extend('anotherChild');
134
135 assert(anotherChild.prototype instanceof baseChild);
136 });
137
138 it('ignores inherited options.base', function() {
139 const modelBuilder = memory.modelBuilder;
140 const base = modelBuilder.define('base');
141 const child = base.extend('child', {}, {base: 'base'});
142 const grandChild = child.extend('grand-child');
143 assert.equal('child', grandChild.base.modelName);
144 assert(grandChild.prototype instanceof child);
145 });
146
147 it('ignores inherited options.super', function() {
148 const modelBuilder = memory.modelBuilder;
149 const base = modelBuilder.define('base');
150 const child = base.extend('child', {}, {super: 'base'});
151 const grandChild = child.extend('grand-child');
152 assert.equal('child', grandChild.base.modelName);
153 assert(grandChild.prototype instanceof child);
154 });
155
156 it('allows model extension', function(done) {
157 const modelBuilder = new ModelBuilder();
158
159 const User = modelBuilder.define('User', {
160 name: String,
161 bio: ModelBuilder.Text,
162 approved: Boolean,
163 joinedAt: Date,
164 age: Number,
165 });
166
167 const Customer = User.extend('Customer', {customerId: {type: String, id: true}});
168
169 const customer = new Customer({name: 'Joe', age: 20, customerId: 'c01'});
170
171 customer.should.be.type('object').and.have.property('name', 'Joe');
172 customer.should.have.property('name', 'Joe');
173 customer.should.have.property('age', 20);
174 customer.should.have.property('customerId', 'c01');
175 customer.should.have.property('bio', undefined);
176
177 // The properties are defined at prototype level
178 assert.equal(Object.keys(customer).filter(function(k) {
179 // Remove internal properties
180 return k.indexOf('__') === -1;
181 }).length, 0);
182 let count = 0;
183 for (const p in customer.toObject()) {
184 if (p.indexOf('__') === 0) {
185 continue;
186 }
187 if (typeof customer[p] !== 'function') {
188 count++;
189 }
190 }
191 assert.equal(count, 6);
192 assert.equal(Object.keys(customer.toObject()).filter(function(k) {
193 // Remove internal properties
194 return k.indexOf('__') === -1;
195 }).length, 6);
196
197 done(null, customer);
198 });
199
200 it('allows model extension with merged settings', function(done) {
201 const modelBuilder = new ModelBuilder();
202
203 const User = modelBuilder.define('User', {
204 name: String,
205 }, {
206 defaultPermission: 'ALLOW',
207 acls: [
208 {
209 principalType: 'ROLE',
210 principalId: '$everyone',
211 permission: 'ALLOW',
212 },
213 ],
214 relations: {
215 posts: {
216 type: 'hasMany',
217 model: 'Post',
218 },
219 },
220 });
221
222 const Customer = User.extend('Customer',
223 {customerId: {type: String, id: true}}, {
224 defaultPermission: 'DENY',
225 acls: [
226 {
227 principalType: 'ROLE',
228 principalId: '$unauthenticated',
229 permission: 'DENY',
230 },
231 ],
232 relations: {
233 orders: {
234 type: 'hasMany',
235 model: 'Order',
236 },
237 },
238 });
239
240 assert.deepEqual(User.settings, {
241 // forceId is set to 'auto' in memory if idProp.generated && forceId !== false
242 forceId: 'auto',
243 defaultPermission: 'ALLOW',
244 acls: [
245 {
246 principalType: 'ROLE',
247 principalId: '$everyone',
248 permission: 'ALLOW',
249 },
250 ],
251 relations: {
252 posts: {
253 type: 'hasMany',
254 model: 'Post',
255 },
256 },
257 strict: false,
258 });
259
260 assert.deepEqual(Customer.settings, {
261 forceId: false,
262 defaultPermission: 'DENY',
263 acls: [
264 {
265 principalType: 'ROLE',
266 principalId: '$everyone',
267 permission: 'ALLOW',
268 },
269 {
270 principalType: 'ROLE',
271 principalId: '$unauthenticated',
272 permission: 'DENY',
273 },
274 ],
275 relations: {
276 posts: {
277 type: 'hasMany',
278 model: 'Post',
279 },
280 orders: {
281 type: 'hasMany',
282 model: 'Order',
283 },
284 },
285 strict: false,
286 base: User,
287 });
288
289 done();
290 });
291
292 it('defines rank of ACLs according to model\'s inheritance rank', function() {
293 // a simple test is enough as we already fully tested option `{rank: true}`
294 // in tests with flag `configureModelMerge`
295 const modelBuilder = memory.modelBuilder;
296 const base = modelBuilder.define('base', {}, {acls: [
297 {
298 principalType: 'ROLE',
299 principalId: '$everyone',
300 property: 'oneMethod',
301 permission: 'ALLOW',
302 },
303 ]});
304 const childRank1 = modelBuilder.define('childRank1', {}, {
305 base: base,
306 acls: [
307 {
308 principalType: 'ROLE',
309 principalId: '$everyone',
310 property: 'oneMethod',
311 permission: 'DENY',
312 },
313 ],
314 });
315
316 const expectedSettings = {
317 acls: [
318 {
319 principalType: 'ROLE',
320 principalId: '$everyone',
321 property: 'oneMethod',
322 permission: 'ALLOW',
323 __rank: 1,
324 },
325 {
326 principalType: 'ROLE',
327 principalId: '$everyone',
328 property: 'oneMethod',
329 permission: 'DENY',
330 __rank: 2,
331 },
332 ],
333 };
334 should.deepEqual(childRank1.settings.acls, expectedSettings.acls);
335 });
336
337 it('replaces baseClass relations with matching subClass relations', function() {
338 // merge policy of settings.relations is {patch: true}
339 const modelBuilder = memory.modelBuilder;
340 const base = modelBuilder.define('base', {}, {
341 relations: {
342 user: {
343 type: 'belongsTo',
344 model: 'User',
345 foreignKey: 'userId',
346 },
347 },
348 });
349 const child = base.extend('child', {}, {
350 relations: {
351 user: {
352 type: 'belongsTo',
353 idName: 'id',
354 polymorphic: {
355 idType: 'string',
356 foreignKey: 'userId',
357 discriminator: 'principalType',
358 },
359 },
360 },
361 });
362
363 const expectedSettings = {
364 relations: {
365 user: {
366 type: 'belongsTo',
367 idName: 'id',
368 polymorphic: {
369 idType: 'string',
370 foreignKey: 'userId',
371 discriminator: 'principalType',
372 },
373 },
374 },
375 };
376
377 should.deepEqual(child.settings.relations, expectedSettings.relations);
378 });
379 });
380
381 describe('Merge policy WITH flag `configureModelMerge: true`', function() {
382 it('`{__delete: null}` allows deleting base model settings by assigning ' +
383 'null value at sub model level', function() {
384 const modelBuilder = memory.modelBuilder;
385 const base = modelBuilder.define('base', {}, {
386 anyParam: {oneKey: 'this should be removed'},
387 });
388 const child = base.extend('child', {}, {
389 anyParam: null,
390 configureModelMerge: true,
391 });
392
393 const expectedSettings = {};
394
395 should.deepEqual(child.settings.description, expectedSettings.description);
396 });
397
398 it('`{rank: true}` defines rank of array elements ' +
399 'according to model\'s inheritance rank', function() {
400 const modelBuilder = memory.modelBuilder;
401 const base = modelBuilder.define('base', {}, {acls: [
402 {
403 principalType: 'ROLE',
404 principalId: '$everyone',
405 property: 'oneMethod',
406 permission: 'ALLOW',
407 },
408 ]});
409 const childRank1 = modelBuilder.define('childRank1', {}, {
410 base: base,
411 acls: [
412 {
413 principalType: 'ROLE',
414 principalId: '$owner',
415 property: 'anotherMethod',
416 permission: 'ALLOW',
417 },
418 ],
419 configureModelMerge: true,
420 });
421 const childRank2 = childRank1.extend('childRank2', {}, {});
422 const childRank3 = childRank2.extend('childRank3', {}, {
423 acls: [
424 {
425 principalType: 'ROLE',
426 principalId: '$everyone',
427 property: 'oneMethod',
428 permission: 'DENY',
429 },
430 ],
431 configureModelMerge: true,
432 });
433
434 const expectedSettings = {
435 acls: [
436 {
437 principalType: 'ROLE',
438 principalId: '$everyone',
439 property: 'oneMethod',
440 permission: 'ALLOW',
441 __rank: 1,
442 },
443 {
444 principalType: 'ROLE',
445 principalId: '$owner',
446 property: 'anotherMethod',
447 permission: 'ALLOW',
448 __rank: 2,
449 },
450 {
451 principalType: 'ROLE',
452 principalId: '$everyone',
453 property: 'oneMethod',
454 permission: 'DENY',
455 __rank: 4,
456 },
457 ],
458 };
459 should.deepEqual(childRank3.settings.acls, expectedSettings.acls);
460 });
461
462 it('`{replace: true}` replaces base model array with sub model matching ' +
463 'array', function() {
464 // merge policy of settings.description is {replace: true}
465 const modelBuilder = memory.modelBuilder;
466 const base = modelBuilder.define('base', {}, {
467 description: ['base', 'model', 'description'],
468 });
469 const child = base.extend('child', {}, {
470 description: ['this', 'is', 'child', 'model', 'description'],
471 configureModelMerge: true,
472 });
473
474 const expectedSettings = {
475 description: ['this', 'is', 'child', 'model', 'description'],
476 };
477
478 should.deepEqual(child.settings.description, expectedSettings.description);
479 });
480
481 it('`{replace:true}` is applied on array parameters not defined in merge policy', function() {
482 const modelBuilder = memory.modelBuilder;
483 const base = modelBuilder.define('base', {}, {
484 unknownArrayParam: ['this', 'should', 'be', 'replaced'],
485 });
486 const child = base.extend('child', {}, {
487 unknownArrayParam: ['this', 'should', 'remain', 'after', 'merge'],
488 configureModelMerge: true,
489 });
490
491 const expectedSettings = {
492 unknownArrayParam: ['this', 'should', 'remain', 'after', 'merge'],
493 };
494
495 should.deepEqual(child.settings.description, expectedSettings.description);
496 });
497
498 it('`{replace:true}` is applied on object {} parameters not defined in mergePolicy', function() {
499 const modelBuilder = memory.modelBuilder;
500 const base = modelBuilder.define('base', {}, {
501 unknownObjectParam: {oneKey: 'this should be replaced'},
502 });
503 const child = base.extend('child', {}, {
504 unknownObjectParam: {anotherKey: 'this should remain after merge'},
505 configureModelMerge: true,
506 });
507
508 const expectedSettings = {
509 unknownObjectParam: {anotherKey: 'this should remain after merge'},
510 };
511
512 should.deepEqual(child.settings.description, expectedSettings.description);
513 });
514
515 it('`{replace: false}` adds distinct members of matching arrays from ' +
516 'base model and sub model', function() {
517 // merge policy of settings.hidden is {replace: false}
518 const modelBuilder = memory.modelBuilder;
519 const base = modelBuilder.define('base', {}, {
520 hidden: ['firstProperty', 'secondProperty'],
521 });
522 const child = base.extend('child', {}, {
523 hidden: ['secondProperty', 'thirdProperty'],
524 configureModelMerge: true,
525 });
526
527 const expectedSettings = {
528 hidden: ['firstProperty', 'secondProperty', 'thirdProperty'],
529 };
530
531 should.deepEqual(child.settings.hidden, expectedSettings.hidden);
532 });
533
534 it('`{patch: true}` adds distinct inner properties of matching objects ' +
535 'from base model and sub model', function() {
536 // merge policy of settings.relations is {patch: true}
537 const modelBuilder = memory.modelBuilder;
538 const base = modelBuilder.define('base', {}, {
539 relations: {
540 someOtherRelation: {
541 type: 'hasMany',
542 model: 'someOtherModel',
543 foreignKey: 'otherModelId',
544 },
545 },
546 });
547 const child = base.extend('child', {}, {
548 relations: {
549 someRelation: {
550 type: 'belongsTo',
551 model: 'someModel',
552 foreignKey: 'modelId',
553 },
554 },
555 configureModelMerge: true,
556 });
557
558 const expectedSettings = {
559 relations: {
560 someRelation: {
561 type: 'belongsTo',
562 model: 'someModel',
563 foreignKey: 'modelId',
564 },
565 someOtherRelation: {
566 type: 'hasMany',
567 model: 'someOtherModel',
568 foreignKey: 'otherModelId',
569 },
570 },
571 };
572
573 should.deepEqual(child.settings.relations, expectedSettings.relations);
574 });
575
576 it('`{patch: true}` replaces baseClass inner properties with matching ' +
577 'subClass inner properties', function() {
578 // merge policy of settings.relations is {patch: true}
579 const modelBuilder = memory.modelBuilder;
580 const base = modelBuilder.define('base', {}, {
581 relations: {
582 user: {
583 type: 'belongsTo',
584 model: 'User',
585 foreignKey: 'userId',
586 },
587 },
588 });
589 const child = base.extend('child', {}, {
590 relations: {
591 user: {
592 type: 'belongsTo',
593 idName: 'id',
594 polymorphic: {
595 idType: 'string',
596 foreignKey: 'userId',
597 discriminator: 'principalType',
598 },
599 },
600 },
601 configureModelMerge: true,
602 });
603
604 const expectedSettings = {
605 relations: {
606 user: {
607 type: 'belongsTo',
608 idName: 'id',
609 polymorphic: {
610 idType: 'string',
611 foreignKey: 'userId',
612 discriminator: 'principalType',
613 },
614 },
615 },
616 };
617
618 should.deepEqual(child.settings.relations, expectedSettings.relations);
619 });
620 });
621});