1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | 'use strict';
|
9 |
|
10 | const should = require('./init.js');
|
11 | const assert = require('assert');
|
12 |
|
13 | const jdb = require('../');
|
14 | const ModelBuilder = jdb.ModelBuilder;
|
15 | const DataSource = jdb.DataSource;
|
16 | const Memory = require('../lib/connectors/memory');
|
17 |
|
18 | const ModelDefinition = require('../lib/model-definition');
|
19 |
|
20 | describe('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 |
|
77 | const originalGetMergePolicy = base.getMergePolicy;
|
78 |
|
79 |
|
80 | base.getMergePolicy = function(options) {
|
81 | mergePolicy = options && options.configureModelMerge;
|
82 | return originalGetMergePolicy(options);
|
83 | };
|
84 |
|
85 |
|
86 |
|
87 | const child = base.extend('child', {}, {configureModelMerge: newMergePolicy});
|
88 |
|
89 | should.deepEqual(mergePolicy, newMergePolicy);
|
90 |
|
91 |
|
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 |
|
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 |
|
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 |
|
132 |
|
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 |
|
178 | assert.equal(Object.keys(customer).filter(function(k) {
|
179 |
|
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 |
|
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 |
|
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 |
|
294 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 | });
|