UNPKG

22.1 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'use strict';
7const should = require('./init.js');
8const utils = require('../lib/utils');
9const ObjectID = require('bson').ObjectID;
10const fieldsToArray = utils.fieldsToArray;
11const sanitizeQuery = utils.sanitizeQuery;
12const deepMerge = utils.deepMerge;
13const rankArrayElements = utils.rankArrayElements;
14const mergeIncludes = utils.mergeIncludes;
15const sortObjectsByIds = utils.sortObjectsByIds;
16const uniq = utils.uniq;
17
18describe('util.fieldsToArray', function() {
19 function sample(fields, excludeUnknown) {
20 const properties = ['foo', 'bar', 'bat', 'baz'];
21 return {
22 expect: function(arr) {
23 should.deepEqual(fieldsToArray(fields, properties, excludeUnknown), arr);
24 },
25 };
26 }
27
28 it('Turn objects and strings into an array of fields' +
29 ' to include when finding models', function() {
30 sample(false).expect(undefined);
31 sample(null).expect(undefined);
32 sample({}).expect(undefined);
33 sample('foo').expect(['foo']);
34 sample(['foo']).expect(['foo']);
35 sample({'foo': 1}).expect(['foo']);
36 sample({'bat': true}).expect(['bat']);
37 sample({'bat': 0}).expect(['foo', 'bar', 'baz']);
38 sample({'bat': false}).expect(['foo', 'bar', 'baz']);
39 });
40
41 it('should exclude unknown properties', function() {
42 sample(false, true).expect(undefined);
43 sample(null, true).expect(undefined);
44 sample({}, true).expect(undefined);
45 sample('foo', true).expect(['foo']);
46 sample(['foo', 'unknown'], true).expect(['foo']);
47 sample({'foo': 1, unknown: 1}, true).expect(['foo']);
48 sample({'bat': true, unknown: true}, true).expect(['bat']);
49 sample({'bat': 0}, true).expect(['foo', 'bar', 'baz']);
50 sample({'bat': false}, true).expect(['foo', 'bar', 'baz']);
51 sample({'other': false}, true).expect(['foo', 'bar', 'bat', 'baz']);
52 });
53});
54
55describe('util.sanitizeQuery', function() {
56 it('Remove undefined values from the query object', function() {
57 const q1 = {where: {x: 1, y: undefined}};
58 should.deepEqual(sanitizeQuery(q1), {where: {x: 1}});
59
60 const q2 = {where: {x: 1, y: 2}};
61 should.deepEqual(sanitizeQuery(q2), {where: {x: 1, y: 2}});
62
63 const q3 = {where: {x: 1, y: {in: [2, undefined]}}};
64 should.deepEqual(sanitizeQuery(q3), {where: {x: 1, y: {in: [2]}}});
65
66 should.equal(sanitizeQuery(null), null);
67
68 should.equal(sanitizeQuery(undefined), undefined);
69
70 should.equal(sanitizeQuery('x'), 'x');
71
72 const date = new Date();
73 const q4 = {where: {x: 1, y: date}};
74 should.deepEqual(sanitizeQuery(q4), {where: {x: 1, y: date}});
75
76 // test handling of undefined
77 let q5 = {where: {x: 1, y: undefined}};
78 should.deepEqual(sanitizeQuery(q5, 'nullify'), {where: {x: 1, y: null}});
79
80 q5 = {where: {x: 1, y: undefined}};
81 should.deepEqual(sanitizeQuery(q5, {normalizeUndefinedInQuery: 'nullify'}), {where: {x: 1, y: null}});
82
83 const q6 = {where: {x: 1, y: undefined}};
84 (function() { sanitizeQuery(q6, 'throw'); }).should.throw(/`undefined` in query/);
85 });
86
87 it('Report errors for circular or deep query objects', function() {
88 const q7 = {where: {x: 1}};
89 q7.where.y = q7;
90 (function() { sanitizeQuery(q7); }).should.throw(
91 /The query object is circular/,
92 );
93
94 const q8 = {where: {and: [{and: [{and: [{and: [{and: [{and:
95 [{and: [{and: [{and: [{x: 1}]}]}]}]}]}]}]}]}]}};
96 (function() { sanitizeQuery(q8, {maxDepth: 12}); }).should.throw(
97 /The query object exceeds maximum depth 12/,
98 );
99
100 // maxDepth is default to maximum integer
101 sanitizeQuery(q8).should.eql(q8);
102
103 const q9 = {where: {and: [{and: [{and: [{and: [{x: 1}]}]}]}]}};
104 (function() { sanitizeQuery(q8, {maxDepth: 4}); }).should.throw(
105 /The query object exceeds maximum depth 4/,
106 );
107 });
108
109 it('Removed prohibited properties in query objects', function() {
110 const q1 = {where: {secret: 'guess'}};
111 sanitizeQuery(q1, {prohibitedKeys: ['secret']});
112 q1.where.should.eql({});
113
114 const q2 = {and: [{secret: 'guess'}, {x: 1}]};
115 sanitizeQuery(q2, {prohibitedKeys: ['secret']});
116 q2.should.eql({and: [{}, {x: 1}]});
117 });
118
119 it('should allow proper structured regexp string', () => {
120 const q1 = {where: {name: {like: '^J'}}};
121 sanitizeQuery(q1).should.eql({where: {name: {like: '^J'}}});
122 });
123
124 it('should properly sanitize regexp string operators', () => {
125 const q1 = {where: {name: {like: '['}}};
126 sanitizeQuery(q1).should.eql({where: {name: {like: '\\['}}});
127
128 const q2 = {where: {name: {nlike: '['}}};
129 sanitizeQuery(q2).should.eql({where: {name: {nlike: '\\['}}});
130
131 const q3 = {where: {name: {ilike: '['}}};
132 sanitizeQuery(q3).should.eql({where: {name: {ilike: '\\['}}});
133
134 const q4 = {where: {name: {nilike: '['}}};
135 sanitizeQuery(q4).should.eql({where: {name: {nilike: '\\['}}});
136
137 const q5 = {where: {name: {regexp: '['}}};
138 sanitizeQuery(q5).should.eql({where: {name: {regexp: '\\['}}});
139 });
140});
141
142describe('util.parseSettings', function() {
143 it('Parse a full url into a settings object', function() {
144 const url = 'mongodb://x:y@localhost:27017/mydb?w=2';
145 const settings = utils.parseSettings(url);
146 should.equal(settings.hostname, 'localhost');
147 should.equal(settings.port, 27017);
148 should.equal(settings.host, 'localhost');
149 should.equal(settings.user, 'x');
150 should.equal(settings.password, 'y');
151 should.equal(settings.database, 'mydb');
152 should.equal(settings.connector, 'mongodb');
153 should.equal(settings.w, '2');
154 should.equal(settings.url, 'mongodb://x:y@localhost:27017/mydb?w=2');
155 });
156
157 it('Parse a url without auth into a settings object', function() {
158 const url = 'mongodb://localhost:27017/mydb/abc?w=2';
159 const settings = utils.parseSettings(url);
160 should.equal(settings.hostname, 'localhost');
161 should.equal(settings.port, 27017);
162 should.equal(settings.host, 'localhost');
163 should.equal(settings.user, undefined);
164 should.equal(settings.password, undefined);
165 should.equal(settings.database, 'mydb');
166 should.equal(settings.connector, 'mongodb');
167 should.equal(settings.w, '2');
168 should.equal(settings.url, 'mongodb://localhost:27017/mydb/abc?w=2');
169 });
170
171 it('Parse a url with complex query into a settings object', function() {
172 const url = 'mysql://127.0.0.1:3306/mydb?x[a]=1&x[b]=2&engine=InnoDB';
173 const settings = utils.parseSettings(url);
174 should.equal(settings.hostname, '127.0.0.1');
175 should.equal(settings.port, 3306);
176 should.equal(settings.host, '127.0.0.1');
177 should.equal(settings.user, undefined);
178 should.equal(settings.password, undefined);
179 should.equal(settings.database, 'mydb');
180 should.equal(settings.connector, 'mysql');
181 should.equal(settings.x.a, '1');
182 should.equal(settings.x.b, '2');
183 should.equal(settings.engine, 'InnoDB');
184 should.equal(settings.url, 'mysql://127.0.0.1:3306/mydb?x[a]=1&x[b]=2&engine=InnoDB');
185 });
186
187 it('Parse a Memory url without auth into a settings object', function() {
188 const url = 'memory://?x=1';
189 const settings = utils.parseSettings(url);
190 should.equal(settings.hostname, '');
191 should.equal(settings.user, undefined);
192 should.equal(settings.password, undefined);
193 should.equal(settings.database, undefined);
194 should.equal(settings.connector, 'memory');
195 should.equal(settings.x, '1');
196 should.equal(settings.url, 'memory://?x=1');
197 });
198});
199
200describe('util.deepMerge', function() {
201 it('should deep merge objects', function() {
202 const extras = {base: 'User',
203 relations: {accessTokens: {model: 'accessToken', type: 'hasMany',
204 foreignKey: 'userId'},
205 account: {model: 'account', type: 'belongsTo'}},
206 acls: [
207 {accessType: '*',
208 permission: 'DENY',
209 principalType: 'ROLE',
210 principalId: '$everyone'},
211 {accessType: '*',
212 permission: 'ALLOW',
213 principalType: 'ROLE',
214 property: 'login',
215 principalId: '$everyone'},
216 {permission: 'ALLOW',
217 property: 'findById',
218 principalType: 'ROLE',
219 principalId: '$owner'},
220 ]};
221 const base = {strict: false,
222 acls: [
223 {principalType: 'ROLE',
224 principalId: '$everyone',
225 permission: 'ALLOW',
226 property: 'create'},
227 {principalType: 'ROLE',
228 principalId: '$owner',
229 permission: 'ALLOW',
230 property: 'removeById'},
231 ],
232 maxTTL: 31556926,
233 ttl: 1209600};
234
235 const merged = deepMerge(base, extras);
236
237 const expected = {strict: false,
238 acls: [
239 {principalType: 'ROLE',
240 principalId: '$everyone',
241 permission: 'ALLOW',
242 property: 'create'},
243 {principalType: 'ROLE',
244 principalId: '$owner',
245 permission: 'ALLOW',
246 property: 'removeById'},
247 {accessType: '*',
248 permission: 'DENY',
249 principalType: 'ROLE',
250 principalId: '$everyone'},
251 {accessType: '*',
252 permission: 'ALLOW',
253 principalType: 'ROLE',
254 property: 'login',
255 principalId: '$everyone'},
256 {permission: 'ALLOW',
257 property: 'findById',
258 principalType: 'ROLE',
259 principalId: '$owner'},
260 ],
261 maxTTL: 31556926,
262 ttl: 1209600,
263 base: 'User',
264 relations: {accessTokens: {model: 'accessToken', type: 'hasMany',
265 foreignKey: 'userId'},
266 account: {model: 'account', type: 'belongsTo'}}};
267
268 should.deepEqual(merged, expected, 'Merged objects should match the expectation');
269 });
270});
271
272describe('util.rankArrayElements', function() {
273 it('should add property \'__rank\' to array elements of type object {}', function() {
274 const acls = [
275 {accessType: '*',
276 permission: 'DENY',
277 principalType: 'ROLE',
278 principalId: '$everyone'},
279 ];
280
281 const rankedAcls = rankArrayElements(acls, 2);
282
283 should.equal(rankedAcls[0].__rank, 2);
284 });
285
286 it('should not replace existing \'__rank\' property of array elements', function() {
287 const acls = [
288 {accessType: '*',
289 permission: 'DENY',
290 principalType: 'ROLE',
291 principalId: '$everyone',
292 __rank: 1,
293 },
294 ];
295
296 const rankedAcls = rankArrayElements(acls, 2);
297
298 should.equal(rankedAcls[0].__rank, 1);
299 });
300});
301
302describe('util.sortObjectsByIds', function() {
303 const items = [
304 {id: 1, name: 'a'},
305 {id: 2, name: 'b'},
306 {id: 3, name: 'c'},
307 {id: 4, name: 'd'},
308 {id: 5, name: 'e'},
309 {id: 6, name: 'f'},
310 ];
311
312 it('should sort', function() {
313 const sorted = sortObjectsByIds('id', [6, 5, 4, 3, 2, 1], items);
314 const names = sorted.map(function(u) { return u.name; });
315 should.deepEqual(names, ['f', 'e', 'd', 'c', 'b', 'a']);
316 });
317
318 it('should sort - partial ids', function() {
319 const sorted = sortObjectsByIds('id', [5, 3, 2], items);
320 const names = sorted.map(function(u) { return u.name; });
321 should.deepEqual(names, ['e', 'c', 'b', 'a', 'd', 'f']);
322 });
323
324 it('should sort - strict', function() {
325 const sorted = sortObjectsByIds('id', [5, 3, 2], items, true);
326 const names = sorted.map(function(u) { return u.name; });
327 should.deepEqual(names, ['e', 'c', 'b']);
328 });
329});
330
331describe('util.mergeIncludes', function() {
332 function checkInputOutput(baseInclude, updateInclude, expectedInclude) {
333 const mergedInclude = mergeIncludes(baseInclude, updateInclude);
334 should.deepEqual(mergedInclude, expectedInclude,
335 'Merged include should match the expectation');
336 }
337
338 it('Merge string values to object', function() {
339 const baseInclude = 'relation1';
340 const updateInclude = 'relation2';
341 const expectedInclude = [
342 {relation2: true},
343 {relation1: true},
344 ];
345 checkInputOutput(baseInclude, updateInclude, expectedInclude);
346 });
347
348 it('Merge string & array values to object', function() {
349 const baseInclude = 'relation1';
350 const updateInclude = ['relation2'];
351 const expectedInclude = [
352 {relation2: true},
353 {relation1: true},
354 ];
355 checkInputOutput(baseInclude, updateInclude, expectedInclude);
356 });
357
358 it('Merge string & object values to object', function() {
359 const baseInclude = ['relation1'];
360 const updateInclude = {relation2: 'relation2Include'};
361 const expectedInclude = [
362 {relation2: 'relation2Include'},
363 {relation1: true},
364 ];
365 checkInputOutput(baseInclude, updateInclude, expectedInclude);
366 });
367
368 it('Merge array & array values to object', function() {
369 const baseInclude = ['relation1'];
370 const updateInclude = ['relation2'];
371 const expectedInclude = [
372 {relation2: true},
373 {relation1: true},
374 ];
375 checkInputOutput(baseInclude, updateInclude, expectedInclude);
376 });
377
378 it('Merge array & object values to object', function() {
379 const baseInclude = ['relation1'];
380 const updateInclude = {relation2: 'relation2Include'};
381 const expectedInclude = [
382 {relation2: 'relation2Include'},
383 {relation1: true},
384 ];
385 checkInputOutput(baseInclude, updateInclude, expectedInclude);
386 });
387
388 it('Merge object & object values to object', function() {
389 const baseInclude = {relation1: 'relation1Include'};
390 const updateInclude = {relation2: 'relation2Include'};
391 const expectedInclude = [
392 {relation2: 'relation2Include'},
393 {relation1: 'relation1Include'},
394 ];
395 checkInputOutput(baseInclude, updateInclude, expectedInclude);
396 });
397
398 it('Override property collision with update value', function() {
399 const baseInclude = {relation1: 'baseValue'};
400 const updateInclude = {relation1: 'updateValue'};
401 const expectedInclude = [
402 {relation1: 'updateValue'},
403 ];
404 checkInputOutput(baseInclude, updateInclude, expectedInclude);
405 });
406
407 it('Merge string includes & include with relation syntax properly',
408 function() {
409 const baseInclude = 'relation1';
410 const updateInclude = {relation: 'relation1'};
411 const expectedInclude = [
412 {relation: 'relation1'},
413 ];
414 checkInputOutput(baseInclude, updateInclude, expectedInclude);
415 });
416
417 it('Merge string includes & include with scope properly', function() {
418 const baseInclude = 'relation1';
419 const updateInclude = {
420 relation: 'relation1',
421 scope: {include: 'relation2'},
422 };
423 const expectedInclude = [
424 {relation: 'relation1', scope: {include: 'relation2'}},
425 ];
426 checkInputOutput(baseInclude, updateInclude, expectedInclude);
427 });
428
429 it('Merge includes with and without relation syntax properly',
430 function() {
431 // w & w/o relation syntax - no collision
432 let baseInclude = ['relation2'];
433 let updateInclude = {
434 relation: 'relation1',
435 scope: {include: 'relation2'},
436 };
437 let expectedInclude = [{
438 relation: 'relation1',
439 scope: {include: 'relation2'},
440 }, {relation2: true}];
441 checkInputOutput(baseInclude, updateInclude, expectedInclude);
442
443 // w & w/o relation syntax - collision
444 baseInclude = ['relation1'];
445 updateInclude = {relation: 'relation1', scope: {include: 'relation2'}};
446 expectedInclude =
447 [{relation: 'relation1', scope: {include: 'relation2'}}];
448 checkInputOutput(baseInclude, updateInclude, expectedInclude);
449
450 // w & w/o relation syntax - collision
451 baseInclude = {relation: 'relation1', scope: {include: 'relation2'}};
452 updateInclude = ['relation1'];
453 expectedInclude = [{relation1: true}];
454 checkInputOutput(baseInclude, updateInclude, expectedInclude);
455 });
456
457 it('Merge includes with mixture of strings, arrays & objects properly', function() {
458 const baseInclude = ['relation1', {relation2: true},
459 {relation: 'relation3', scope: {where: {id: 'some id'}}},
460 {relation: 'relation5', scope: {where: {id: 'some id'}}},
461 ];
462 const updateInclude = ['relation4', {relation3: true},
463 {relation: 'relation2', scope: {where: {id: 'some id'}}}];
464 const expectedInclude = [{relation4: true}, {relation3: true},
465 {relation: 'relation2', scope: {where: {id: 'some id'}}},
466 {relation1: true},
467 {relation: 'relation5', scope: {where: {id: 'some id'}}}];
468 checkInputOutput(baseInclude, updateInclude, expectedInclude);
469 });
470});
471
472describe('util.uniq', function() {
473 it('should dedupe an array with duplicate number entries', function() {
474 const a = [1, 2, 1, 3];
475 const b = uniq(a);
476 b.should.eql([1, 2, 3]);
477 });
478
479 it('should dedupe an array with duplicate string entries', function() {
480 const a = ['a', 'a', 'b', 'a'];
481 const b = uniq(a);
482 b.should.eql(['a', 'b']);
483 });
484
485 it('should dedupe an array with duplicate bson entries', function() {
486 const idOne = new ObjectID('59f9ec5dc7d59a00042f7c62');
487 const idTwo = new ObjectID('59f9ec5dc7d59a00042f7c63');
488 const a = [idOne, idTwo, new ObjectID('59f9ec5dc7d59a00042f7c62'),
489 new ObjectID('59f9ec5dc7d59a00042f7c62')];
490 const b = uniq(a);
491 b.should.eql([idOne, idTwo]);
492 });
493
494 it('should dedupe an array without duplicate number entries', function() {
495 const a = [1, 3, 2];
496 const b = uniq(a);
497 b.should.eql([1, 3, 2]);
498 });
499
500 it('should dedupe an array without duplicate string entries', function() {
501 const a = ['a', 'c', 'b'];
502 const b = uniq(a);
503 b.should.eql(['a', 'c', 'b']);
504 });
505
506 it('should dedupe an array without duplicate bson entries', function() {
507 const idOne = new ObjectID('59f9ec5dc7d59a00042f7c62');
508 const idTwo = new ObjectID('59f9ec5dc7d59a00042f7c63');
509 const idThree = new ObjectID('59f9ec5dc7d59a00042f7c64');
510 const a = [idOne, idTwo, idThree];
511 const b = uniq(a);
512 b.should.eql([idOne, idTwo, idThree]);
513 });
514
515 it('should allow null/undefined array', function() {
516 const a = null;
517 const b = uniq(a);
518 b.should.eql([]);
519 });
520
521 it('should report error for non-array arg', function() {
522 const a = '1';
523 try {
524 const b = uniq(a);
525 throw new Error('The test should have thrown an error');
526 } catch (err) {
527 err.should.be.instanceof(Error);
528 }
529 });
530});
531
532describe('util.toRegExp', function() {
533 let invalidDataTypes;
534 let validDataTypes;
535
536 before(function() {
537 invalidDataTypes = [0, true, {}, [], Function, null];
538 validDataTypes = ['string', /^regex/, new RegExp(/^regex/)];
539 });
540
541 it('should not accept invalid data types', function() {
542 invalidDataTypes.forEach(function(invalid) {
543 utils.toRegExp(invalid).should.be.an.Error;
544 });
545 });
546
547 it('should accept valid data types', function() {
548 validDataTypes.forEach(function(valid) {
549 utils.toRegExp(valid).should.not.be.an.Error;
550 });
551 });
552
553 context('with a regex string', function() {
554 it('should return a RegExp object when no regex flags are provided',
555 function() {
556 utils.toRegExp('^regex$').should.be.an.instanceOf(RegExp);
557 });
558
559 it('should throw an error when invalid regex flags are provided',
560 function() {
561 utils.toRegExp('^regex$/abc').should.be.an.Error;
562 });
563
564 it('should return a RegExp object when valid flags are provided',
565 function() {
566 utils.toRegExp('regex/igm').should.be.an.instanceOf(RegExp);
567 });
568 });
569
570 context('with a regex literal', function() {
571 it('should return a RegExp object', function() {
572 utils.toRegExp(/^regex$/igm).should.be.an.instanceOf(RegExp);
573 });
574 });
575
576 context('with a regex object', function() {
577 it('should return a RegExp object', function() {
578 utils.toRegExp(new RegExp('^regex$', 'igm')).should.be.an.instanceOf(RegExp);
579 });
580 });
581});
582
583describe('util.hasRegExpFlags', function() {
584 context('with a regex string', function() {
585 it('should be true when the regex has invalid flags', function() {
586 utils.hasRegExpFlags('^regex$/abc').should.be.ok;
587 });
588
589 it('should be true when the regex has valid flags', function() {
590 utils.hasRegExpFlags('^regex$/igm').should.be.ok;
591 });
592
593 it('should be false when the regex has no flags', function() {
594 utils.hasRegExpFlags('^regex$').should.not.be.ok;
595 utils.hasRegExpFlags('^regex$/').should.not.be.ok;
596 });
597 });
598
599 context('with a regex literal', function() {
600 it('should be true when the regex has valid flags', function() {
601 utils.hasRegExpFlags(/^regex$/igm).should.be.ok;
602 });
603
604 it('should be false when the regex has no flags', function() {
605 utils.hasRegExpFlags(/^regex$/).should.not.be.ok;
606 });
607 });
608
609 context('with a regex object', function() {
610 it('should be true when the regex has valid flags', function() {
611 utils.hasRegExpFlags(new RegExp(/^regex$/igm)).should.be.ok;
612 });
613
614 it('should be false when the regex has no flags', function() {
615 utils.hasRegExpFlags(new RegExp(/^regex$/)).should.not.be.ok;
616 });
617 });
618});
619
620describe('util.idsHaveDuplicates', function() {
621 context('with string IDs', function() {
622 it('should be true with a duplicate present', function() {
623 utils.idsHaveDuplicates(['a', 'b', 'a']).should.be.ok;
624 });
625
626 it('should be false when no duplicates are present', function() {
627 utils.idsHaveDuplicates(['a', 'b', 'c']).should.not.be.ok;
628 });
629 });
630
631 context('with numeric IDs', function() {
632 it('should be true with a duplicate present', function() {
633 utils.idsHaveDuplicates([1, 2, 1]).should.be.ok;
634 });
635
636 it('should be false when no duplicates are present', function() {
637 utils.idsHaveDuplicates([1, 2, 3]).should.not.be.ok;
638 });
639 });
640
641 context('with complex IDs', function() {
642 it('should be true with a duplicate present', function() {
643 utils.idsHaveDuplicates(['a', 'b', 'a'].map(id => ({id}))).should.be.ok;
644 });
645
646 it('should be false when no duplicates are present', function() {
647 utils.idsHaveDuplicates(['a', 'b', 'c'].map(id => ({id}))).should.not.be.ok;
648 });
649 });
650});