loopback-datasource-juggler/test/util.test.js

651 lines
22 KiB
JavaScript
Raw Normal View History

// Copyright IBM Corp. 2013,2019. All Rights Reserved.
2016-04-01 22:25:16 +00:00
// Node module: loopback-datasource-juggler
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT
2016-08-22 19:55:22 +00:00
'use strict';
2018-12-07 14:54:29 +00:00
const should = require('./init.js');
const utils = require('../lib/utils');
const ObjectID = require('bson').ObjectID;
const fieldsToArray = utils.fieldsToArray;
const sanitizeQuery = utils.sanitizeQuery;
const deepMerge = utils.deepMerge;
const rankArrayElements = utils.rankArrayElements;
const mergeIncludes = utils.mergeIncludes;
const sortObjectsByIds = utils.sortObjectsByIds;
const uniq = utils.uniq;
2016-04-01 11:48:17 +00:00
describe('util.fieldsToArray', function() {
function sample(fields, excludeUnknown) {
2018-12-07 14:54:29 +00:00
const properties = ['foo', 'bar', 'bat', 'baz'];
return {
2016-04-01 11:48:17 +00:00
expect: function(arr) {
should.deepEqual(fieldsToArray(fields, properties, excludeUnknown), arr);
2016-04-01 11:48:17 +00:00
},
};
}
it('Turn objects and strings into an array of fields' +
2016-04-01 11:48:17 +00:00
' to include when finding models', function() {
sample(false).expect(undefined);
sample(null).expect(undefined);
sample({}).expect(undefined);
sample('foo').expect(['foo']);
sample(['foo']).expect(['foo']);
2016-08-19 17:46:59 +00:00
sample({'foo': 1}).expect(['foo']);
sample({'bat': true}).expect(['bat']);
sample({'bat': 0}).expect(['foo', 'bar', 'baz']);
sample({'bat': false}).expect(['foo', 'bar', 'baz']);
});
2016-04-01 11:48:17 +00:00
it('should exclude unknown properties', function() {
sample(false, true).expect(undefined);
sample(null, true).expect(undefined);
sample({}, true).expect(undefined);
sample('foo', true).expect(['foo']);
sample(['foo', 'unknown'], true).expect(['foo']);
2016-08-19 17:46:59 +00:00
sample({'foo': 1, unknown: 1}, true).expect(['foo']);
sample({'bat': true, unknown: true}, true).expect(['bat']);
sample({'bat': 0}, true).expect(['foo', 'bar', 'baz']);
sample({'bat': false}, true).expect(['foo', 'bar', 'baz']);
sample({'other': false}, true).expect(['foo', 'bar', 'bat', 'baz']);
});
});
2018-10-19 18:56:51 +00:00
describe('util.sanitizeQuery', function() {
2016-04-01 11:48:17 +00:00
it('Remove undefined values from the query object', function() {
2018-12-07 14:54:29 +00:00
const q1 = {where: {x: 1, y: undefined}};
2018-10-19 18:56:51 +00:00
should.deepEqual(sanitizeQuery(q1), {where: {x: 1}});
2018-12-07 14:54:29 +00:00
const q2 = {where: {x: 1, y: 2}};
2018-10-19 18:56:51 +00:00
should.deepEqual(sanitizeQuery(q2), {where: {x: 1, y: 2}});
2018-12-07 14:54:29 +00:00
const q3 = {where: {x: 1, y: {in: [2, undefined]}}};
2018-10-19 18:56:51 +00:00
should.deepEqual(sanitizeQuery(q3), {where: {x: 1, y: {in: [2]}}});
2018-10-19 18:56:51 +00:00
should.equal(sanitizeQuery(null), null);
2018-10-19 18:56:51 +00:00
should.equal(sanitizeQuery(undefined), undefined);
2018-10-19 18:56:51 +00:00
should.equal(sanitizeQuery('x'), 'x');
2018-12-07 14:54:29 +00:00
const date = new Date();
const q4 = {where: {x: 1, y: date}};
2018-10-19 18:56:51 +00:00
should.deepEqual(sanitizeQuery(q4), {where: {x: 1, y: date}});
// test handling of undefined
2018-12-07 14:54:29 +00:00
let q5 = {where: {x: 1, y: undefined}};
2018-10-19 18:56:51 +00:00
should.deepEqual(sanitizeQuery(q5, 'nullify'), {where: {x: 1, y: null}});
q5 = {where: {x: 1, y: undefined}};
should.deepEqual(sanitizeQuery(q5, {normalizeUndefinedInQuery: 'nullify'}), {where: {x: 1, y: null}});
2018-12-07 14:54:29 +00:00
const q6 = {where: {x: 1, y: undefined}};
2018-10-19 18:56:51 +00:00
(function() { sanitizeQuery(q6, 'throw'); }).should.throw(/`undefined` in query/);
});
it('Report errors for circular or deep query objects', function() {
2018-12-07 14:54:29 +00:00
const q7 = {where: {x: 1}};
2018-10-19 18:56:51 +00:00
q7.where.y = q7;
(function() { sanitizeQuery(q7); }).should.throw(
/The query object is circular/,
2018-10-19 18:56:51 +00:00
);
2018-12-07 14:54:29 +00:00
const q8 = {where: {and: [{and: [{and: [{and: [{and: [{and:
2018-10-19 18:56:51 +00:00
[{and: [{and: [{and: [{x: 1}]}]}]}]}]}]}]}]}]}};
2018-11-12 21:54:22 +00:00
(function() { sanitizeQuery(q8, {maxDepth: 12}); }).should.throw(
/The query object exceeds maximum depth 12/,
2018-10-19 18:56:51 +00:00
);
2018-11-12 21:54:22 +00:00
// maxDepth is default to maximum integer
sanitizeQuery(q8).should.eql(q8);
2018-12-07 14:54:29 +00:00
const q9 = {where: {and: [{and: [{and: [{and: [{x: 1}]}]}]}]}};
2018-10-19 18:56:51 +00:00
(function() { sanitizeQuery(q8, {maxDepth: 4}); }).should.throw(
/The query object exceeds maximum depth 4/,
2018-10-19 18:56:51 +00:00
);
});
it('Removed prohibited properties in query objects', function() {
2018-12-07 14:54:29 +00:00
const q1 = {where: {secret: 'guess'}};
2018-10-19 18:56:51 +00:00
sanitizeQuery(q1, {prohibitedKeys: ['secret']});
q1.where.should.eql({});
2018-12-07 14:54:29 +00:00
const q2 = {and: [{secret: 'guess'}, {x: 1}]};
2018-10-19 18:56:51 +00:00
sanitizeQuery(q2, {prohibitedKeys: ['secret']});
q2.should.eql({and: [{}, {x: 1}]});
2014-01-24 17:09:53 +00:00
});
2019-10-21 16:55:14 +00:00
it('should allow proper structured regexp string', () => {
const q1 = {where: {name: {like: '^J'}}};
sanitizeQuery(q1).should.eql({where: {name: {like: '^J'}}});
});
it('should properly sanitize regexp string operators', () => {
const q1 = {where: {name: {like: '['}}};
sanitizeQuery(q1).should.eql({where: {name: {like: '\\['}}});
const q2 = {where: {name: {nlike: '['}}};
sanitizeQuery(q2).should.eql({where: {name: {nlike: '\\['}}});
const q3 = {where: {name: {ilike: '['}}};
sanitizeQuery(q3).should.eql({where: {name: {ilike: '\\['}}});
const q4 = {where: {name: {nilike: '['}}};
sanitizeQuery(q4).should.eql({where: {name: {nilike: '\\['}}});
const q5 = {where: {name: {regexp: '['}}};
sanitizeQuery(q5).should.eql({where: {name: {regexp: '\\['}}});
});
});
2016-04-01 11:48:17 +00:00
describe('util.parseSettings', function() {
it('Parse a full url into a settings object', function() {
2018-12-07 14:54:29 +00:00
const url = 'mongodb://x:y@localhost:27017/mydb?w=2';
const settings = utils.parseSettings(url);
2014-01-24 17:09:53 +00:00
should.equal(settings.hostname, 'localhost');
should.equal(settings.port, 27017);
should.equal(settings.host, 'localhost');
should.equal(settings.user, 'x');
should.equal(settings.password, 'y');
should.equal(settings.database, 'mydb');
should.equal(settings.connector, 'mongodb');
should.equal(settings.w, '2');
should.equal(settings.url, 'mongodb://x:y@localhost:27017/mydb?w=2');
});
2016-04-01 11:48:17 +00:00
it('Parse a url without auth into a settings object', function() {
2018-12-07 14:54:29 +00:00
const url = 'mongodb://localhost:27017/mydb/abc?w=2';
const settings = utils.parseSettings(url);
2014-01-24 17:09:53 +00:00
should.equal(settings.hostname, 'localhost');
should.equal(settings.port, 27017);
should.equal(settings.host, 'localhost');
should.equal(settings.user, undefined);
should.equal(settings.password, undefined);
should.equal(settings.database, 'mydb');
should.equal(settings.connector, 'mongodb');
should.equal(settings.w, '2');
should.equal(settings.url, 'mongodb://localhost:27017/mydb/abc?w=2');
});
2016-04-01 11:48:17 +00:00
it('Parse a url with complex query into a settings object', function() {
2018-12-07 14:54:29 +00:00
const url = 'mysql://127.0.0.1:3306/mydb?x[a]=1&x[b]=2&engine=InnoDB';
const settings = utils.parseSettings(url);
2014-01-24 17:09:53 +00:00
should.equal(settings.hostname, '127.0.0.1');
should.equal(settings.port, 3306);
should.equal(settings.host, '127.0.0.1');
should.equal(settings.user, undefined);
should.equal(settings.password, undefined);
should.equal(settings.database, 'mydb');
should.equal(settings.connector, 'mysql');
should.equal(settings.x.a, '1');
should.equal(settings.x.b, '2');
should.equal(settings.engine, 'InnoDB');
should.equal(settings.url, 'mysql://127.0.0.1:3306/mydb?x[a]=1&x[b]=2&engine=InnoDB');
});
2018-06-12 07:13:32 +00:00
it('Parse a Memory url without auth into a settings object', function() {
2018-12-07 14:54:29 +00:00
const url = 'memory://?x=1';
const settings = utils.parseSettings(url);
2014-01-24 17:09:53 +00:00
should.equal(settings.hostname, '');
should.equal(settings.user, undefined);
should.equal(settings.password, undefined);
should.equal(settings.database, undefined);
should.equal(settings.connector, 'memory');
should.equal(settings.x, '1');
should.equal(settings.url, 'memory://?x=1');
});
2013-12-17 01:14:56 +00:00
});
configurable model merge The PR superseeds the existing deepMerge algorithm used to merge settings of parent and child models with a new algorithm that allows to specify the way each setting is merged or mixed-in. This configuration of this algorithm uses a merge policy specification. The `getMergePolicy()` helper of BaseModelClass can be used to ease model merge configuration. Next is presented the expected merge behaviour for each option. NOTE: This applies to top-level settings properties - Any - `{replace: true}` (default): child replaces the value from parent - assignin `null` on child setting deletes the inherited setting - Arrays - `{replace: false}`: unique elements of parent and child cumulate - `{rank: true}` adds the model inheritance rank to array elements of type Object {} as internal property `__rank` - Object {}: - `{replace: false}`: deep merges parent and child objects - `{patch: true}`: child replaces inner properties from parent The recommended merge policy is returned by getMergePolicy() when calling the method with option `{configureModelMerge: true}`. The legacy built-in merge policy is returned by `getMergePolicy()` when avoiding option `configureModelMerge`. NOTE: it also delivers ACLs ranking in addition to the legacy behaviour as well as fixes for settings `description` and `relations` `getMergePolicy()` can be customized using model's setting `configureModelMerge` as follows: ``` { // .. options: { configureModelMerge: { // merge options } } // .. } ``` `getMergePolicy()` method can also be extended programmatically as follows: ``` myModel.getMergePolicy = function(options) { const origin = myModel.base.getMergePolicy(options); return Object.assign({}, origin, { // new/overriding options }); }; ```
2017-03-27 22:30:29 +00:00
describe('util.deepMerge', function() {
it('should deep merge objects', function() {
2018-12-07 14:54:29 +00:00
const extras = {base: 'User',
2016-08-19 17:46:59 +00:00
relations: {accessTokens: {model: 'accessToken', type: 'hasMany',
foreignKey: 'userId'},
2018-06-12 07:13:32 +00:00
account: {model: 'account', type: 'belongsTo'}},
2013-12-17 01:14:56 +00:00
acls: [
2016-08-19 17:46:59 +00:00
{accessType: '*',
2013-12-17 01:14:56 +00:00
permission: 'DENY',
principalType: 'ROLE',
2016-08-19 17:46:59 +00:00
principalId: '$everyone'},
{accessType: '*',
2013-12-17 01:14:56 +00:00
permission: 'ALLOW',
principalType: 'ROLE',
property: 'login',
2016-08-19 17:46:59 +00:00
principalId: '$everyone'},
{permission: 'ALLOW',
2013-12-17 01:14:56 +00:00
property: 'findById',
principalType: 'ROLE',
2016-08-19 17:46:59 +00:00
principalId: '$owner'},
]};
2018-12-07 14:54:29 +00:00
const base = {strict: false,
2013-12-17 01:14:56 +00:00
acls: [
2016-08-19 17:46:59 +00:00
{principalType: 'ROLE',
2013-12-17 01:14:56 +00:00
principalId: '$everyone',
permission: 'ALLOW',
2016-08-19 17:46:59 +00:00
property: 'create'},
{principalType: 'ROLE',
2013-12-17 01:14:56 +00:00
principalId: '$owner',
permission: 'ALLOW',
2016-08-19 17:46:59 +00:00
property: 'removeById'},
2013-12-17 01:14:56 +00:00
],
maxTTL: 31556926,
2016-08-19 17:46:59 +00:00
ttl: 1209600};
2013-12-17 01:14:56 +00:00
2018-12-07 14:54:29 +00:00
const merged = deepMerge(base, extras);
2013-12-17 01:14:56 +00:00
2018-12-07 14:54:29 +00:00
const expected = {strict: false,
2013-12-17 01:14:56 +00:00
acls: [
2016-08-19 17:46:59 +00:00
{principalType: 'ROLE',
2013-12-17 01:14:56 +00:00
principalId: '$everyone',
permission: 'ALLOW',
2016-08-19 17:46:59 +00:00
property: 'create'},
{principalType: 'ROLE',
2013-12-17 01:14:56 +00:00
principalId: '$owner',
permission: 'ALLOW',
2016-08-19 17:46:59 +00:00
property: 'removeById'},
{accessType: '*',
2013-12-17 01:14:56 +00:00
permission: 'DENY',
principalType: 'ROLE',
2016-08-19 17:46:59 +00:00
principalId: '$everyone'},
{accessType: '*',
2013-12-17 01:14:56 +00:00
permission: 'ALLOW',
principalType: 'ROLE',
property: 'login',
2016-08-19 17:46:59 +00:00
principalId: '$everyone'},
{permission: 'ALLOW',
2013-12-17 01:14:56 +00:00
property: 'findById',
principalType: 'ROLE',
2016-08-19 17:46:59 +00:00
principalId: '$owner'},
2013-12-17 01:14:56 +00:00
],
maxTTL: 31556926,
ttl: 1209600,
base: 'User',
2016-08-19 17:46:59 +00:00
relations: {accessTokens: {model: 'accessToken', type: 'hasMany',
foreignKey: 'userId'},
2018-06-12 07:13:32 +00:00
account: {model: 'account', type: 'belongsTo'}}};
2013-12-17 01:14:56 +00:00
configurable model merge The PR superseeds the existing deepMerge algorithm used to merge settings of parent and child models with a new algorithm that allows to specify the way each setting is merged or mixed-in. This configuration of this algorithm uses a merge policy specification. The `getMergePolicy()` helper of BaseModelClass can be used to ease model merge configuration. Next is presented the expected merge behaviour for each option. NOTE: This applies to top-level settings properties - Any - `{replace: true}` (default): child replaces the value from parent - assignin `null` on child setting deletes the inherited setting - Arrays - `{replace: false}`: unique elements of parent and child cumulate - `{rank: true}` adds the model inheritance rank to array elements of type Object {} as internal property `__rank` - Object {}: - `{replace: false}`: deep merges parent and child objects - `{patch: true}`: child replaces inner properties from parent The recommended merge policy is returned by getMergePolicy() when calling the method with option `{configureModelMerge: true}`. The legacy built-in merge policy is returned by `getMergePolicy()` when avoiding option `configureModelMerge`. NOTE: it also delivers ACLs ranking in addition to the legacy behaviour as well as fixes for settings `description` and `relations` `getMergePolicy()` can be customized using model's setting `configureModelMerge` as follows: ``` { // .. options: { configureModelMerge: { // merge options } } // .. } ``` `getMergePolicy()` method can also be extended programmatically as follows: ``` myModel.getMergePolicy = function(options) { const origin = myModel.base.getMergePolicy(options); return Object.assign({}, origin, { // new/overriding options }); }; ```
2017-03-27 22:30:29 +00:00
should.deepEqual(merged, expected, 'Merged objects should match the expectation');
2013-12-17 01:14:56 +00:00
});
});
configurable model merge The PR superseeds the existing deepMerge algorithm used to merge settings of parent and child models with a new algorithm that allows to specify the way each setting is merged or mixed-in. This configuration of this algorithm uses a merge policy specification. The `getMergePolicy()` helper of BaseModelClass can be used to ease model merge configuration. Next is presented the expected merge behaviour for each option. NOTE: This applies to top-level settings properties - Any - `{replace: true}` (default): child replaces the value from parent - assignin `null` on child setting deletes the inherited setting - Arrays - `{replace: false}`: unique elements of parent and child cumulate - `{rank: true}` adds the model inheritance rank to array elements of type Object {} as internal property `__rank` - Object {}: - `{replace: false}`: deep merges parent and child objects - `{patch: true}`: child replaces inner properties from parent The recommended merge policy is returned by getMergePolicy() when calling the method with option `{configureModelMerge: true}`. The legacy built-in merge policy is returned by `getMergePolicy()` when avoiding option `configureModelMerge`. NOTE: it also delivers ACLs ranking in addition to the legacy behaviour as well as fixes for settings `description` and `relations` `getMergePolicy()` can be customized using model's setting `configureModelMerge` as follows: ``` { // .. options: { configureModelMerge: { // merge options } } // .. } ``` `getMergePolicy()` method can also be extended programmatically as follows: ``` myModel.getMergePolicy = function(options) { const origin = myModel.base.getMergePolicy(options); return Object.assign({}, origin, { // new/overriding options }); }; ```
2017-03-27 22:30:29 +00:00
describe('util.rankArrayElements', function() {
it('should add property \'__rank\' to array elements of type object {}', function() {
2018-12-07 14:54:29 +00:00
const acls = [
configurable model merge The PR superseeds the existing deepMerge algorithm used to merge settings of parent and child models with a new algorithm that allows to specify the way each setting is merged or mixed-in. This configuration of this algorithm uses a merge policy specification. The `getMergePolicy()` helper of BaseModelClass can be used to ease model merge configuration. Next is presented the expected merge behaviour for each option. NOTE: This applies to top-level settings properties - Any - `{replace: true}` (default): child replaces the value from parent - assignin `null` on child setting deletes the inherited setting - Arrays - `{replace: false}`: unique elements of parent and child cumulate - `{rank: true}` adds the model inheritance rank to array elements of type Object {} as internal property `__rank` - Object {}: - `{replace: false}`: deep merges parent and child objects - `{patch: true}`: child replaces inner properties from parent The recommended merge policy is returned by getMergePolicy() when calling the method with option `{configureModelMerge: true}`. The legacy built-in merge policy is returned by `getMergePolicy()` when avoiding option `configureModelMerge`. NOTE: it also delivers ACLs ranking in addition to the legacy behaviour as well as fixes for settings `description` and `relations` `getMergePolicy()` can be customized using model's setting `configureModelMerge` as follows: ``` { // .. options: { configureModelMerge: { // merge options } } // .. } ``` `getMergePolicy()` method can also be extended programmatically as follows: ``` myModel.getMergePolicy = function(options) { const origin = myModel.base.getMergePolicy(options); return Object.assign({}, origin, { // new/overriding options }); }; ```
2017-03-27 22:30:29 +00:00
{accessType: '*',
permission: 'DENY',
principalType: 'ROLE',
principalId: '$everyone'},
];
2018-12-07 14:54:29 +00:00
const rankedAcls = rankArrayElements(acls, 2);
configurable model merge The PR superseeds the existing deepMerge algorithm used to merge settings of parent and child models with a new algorithm that allows to specify the way each setting is merged or mixed-in. This configuration of this algorithm uses a merge policy specification. The `getMergePolicy()` helper of BaseModelClass can be used to ease model merge configuration. Next is presented the expected merge behaviour for each option. NOTE: This applies to top-level settings properties - Any - `{replace: true}` (default): child replaces the value from parent - assignin `null` on child setting deletes the inherited setting - Arrays - `{replace: false}`: unique elements of parent and child cumulate - `{rank: true}` adds the model inheritance rank to array elements of type Object {} as internal property `__rank` - Object {}: - `{replace: false}`: deep merges parent and child objects - `{patch: true}`: child replaces inner properties from parent The recommended merge policy is returned by getMergePolicy() when calling the method with option `{configureModelMerge: true}`. The legacy built-in merge policy is returned by `getMergePolicy()` when avoiding option `configureModelMerge`. NOTE: it also delivers ACLs ranking in addition to the legacy behaviour as well as fixes for settings `description` and `relations` `getMergePolicy()` can be customized using model's setting `configureModelMerge` as follows: ``` { // .. options: { configureModelMerge: { // merge options } } // .. } ``` `getMergePolicy()` method can also be extended programmatically as follows: ``` myModel.getMergePolicy = function(options) { const origin = myModel.base.getMergePolicy(options); return Object.assign({}, origin, { // new/overriding options }); }; ```
2017-03-27 22:30:29 +00:00
should.equal(rankedAcls[0].__rank, 2);
});
it('should not replace existing \'__rank\' property of array elements', function() {
2018-12-07 14:54:29 +00:00
const acls = [
configurable model merge The PR superseeds the existing deepMerge algorithm used to merge settings of parent and child models with a new algorithm that allows to specify the way each setting is merged or mixed-in. This configuration of this algorithm uses a merge policy specification. The `getMergePolicy()` helper of BaseModelClass can be used to ease model merge configuration. Next is presented the expected merge behaviour for each option. NOTE: This applies to top-level settings properties - Any - `{replace: true}` (default): child replaces the value from parent - assignin `null` on child setting deletes the inherited setting - Arrays - `{replace: false}`: unique elements of parent and child cumulate - `{rank: true}` adds the model inheritance rank to array elements of type Object {} as internal property `__rank` - Object {}: - `{replace: false}`: deep merges parent and child objects - `{patch: true}`: child replaces inner properties from parent The recommended merge policy is returned by getMergePolicy() when calling the method with option `{configureModelMerge: true}`. The legacy built-in merge policy is returned by `getMergePolicy()` when avoiding option `configureModelMerge`. NOTE: it also delivers ACLs ranking in addition to the legacy behaviour as well as fixes for settings `description` and `relations` `getMergePolicy()` can be customized using model's setting `configureModelMerge` as follows: ``` { // .. options: { configureModelMerge: { // merge options } } // .. } ``` `getMergePolicy()` method can also be extended programmatically as follows: ``` myModel.getMergePolicy = function(options) { const origin = myModel.base.getMergePolicy(options); return Object.assign({}, origin, { // new/overriding options }); }; ```
2017-03-27 22:30:29 +00:00
{accessType: '*',
permission: 'DENY',
principalType: 'ROLE',
principalId: '$everyone',
__rank: 1,
},
];
2018-12-07 14:54:29 +00:00
const rankedAcls = rankArrayElements(acls, 2);
configurable model merge The PR superseeds the existing deepMerge algorithm used to merge settings of parent and child models with a new algorithm that allows to specify the way each setting is merged or mixed-in. This configuration of this algorithm uses a merge policy specification. The `getMergePolicy()` helper of BaseModelClass can be used to ease model merge configuration. Next is presented the expected merge behaviour for each option. NOTE: This applies to top-level settings properties - Any - `{replace: true}` (default): child replaces the value from parent - assignin `null` on child setting deletes the inherited setting - Arrays - `{replace: false}`: unique elements of parent and child cumulate - `{rank: true}` adds the model inheritance rank to array elements of type Object {} as internal property `__rank` - Object {}: - `{replace: false}`: deep merges parent and child objects - `{patch: true}`: child replaces inner properties from parent The recommended merge policy is returned by getMergePolicy() when calling the method with option `{configureModelMerge: true}`. The legacy built-in merge policy is returned by `getMergePolicy()` when avoiding option `configureModelMerge`. NOTE: it also delivers ACLs ranking in addition to the legacy behaviour as well as fixes for settings `description` and `relations` `getMergePolicy()` can be customized using model's setting `configureModelMerge` as follows: ``` { // .. options: { configureModelMerge: { // merge options } } // .. } ``` `getMergePolicy()` method can also be extended programmatically as follows: ``` myModel.getMergePolicy = function(options) { const origin = myModel.base.getMergePolicy(options); return Object.assign({}, origin, { // new/overriding options }); }; ```
2017-03-27 22:30:29 +00:00
should.equal(rankedAcls[0].__rank, 1);
});
});
describe('util.sortObjectsByIds', function() {
2018-12-07 14:54:29 +00:00
const items = [
2016-08-19 17:46:59 +00:00
{id: 1, name: 'a'},
{id: 2, name: 'b'},
{id: 3, name: 'c'},
{id: 4, name: 'd'},
{id: 5, name: 'e'},
{id: 6, name: 'f'},
];
it('should sort', function() {
2018-12-07 14:54:29 +00:00
const sorted = sortObjectsByIds('id', [6, 5, 4, 3, 2, 1], items);
const names = sorted.map(function(u) { return u.name; });
should.deepEqual(names, ['f', 'e', 'd', 'c', 'b', 'a']);
});
it('should sort - partial ids', function() {
2018-12-07 14:54:29 +00:00
const sorted = sortObjectsByIds('id', [5, 3, 2], items);
const names = sorted.map(function(u) { return u.name; });
should.deepEqual(names, ['e', 'c', 'b', 'a', 'd', 'f']);
});
it('should sort - strict', function() {
2018-12-07 14:54:29 +00:00
const sorted = sortObjectsByIds('id', [5, 3, 2], items, true);
const names = sorted.map(function(u) { return u.name; });
should.deepEqual(names, ['e', 'c', 'b']);
});
});
2016-04-01 11:48:17 +00:00
describe('util.mergeIncludes', function() {
function checkInputOutput(baseInclude, updateInclude, expectedInclude) {
2018-12-07 14:54:29 +00:00
const mergedInclude = mergeIncludes(baseInclude, updateInclude);
should.deepEqual(mergedInclude, expectedInclude,
'Merged include should match the expectation');
}
2015-05-29 17:50:37 +00:00
it('Merge string values to object', function() {
2018-12-07 14:54:29 +00:00
const baseInclude = 'relation1';
const updateInclude = 'relation2';
const expectedInclude = [
2016-08-19 17:46:59 +00:00
{relation2: true},
{relation1: true},
];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
});
2015-05-29 17:50:37 +00:00
it('Merge string & array values to object', function() {
2018-12-07 14:54:29 +00:00
const baseInclude = 'relation1';
const updateInclude = ['relation2'];
const expectedInclude = [
2016-08-19 17:46:59 +00:00
{relation2: true},
{relation1: true},
];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
});
2015-05-29 17:50:37 +00:00
it('Merge string & object values to object', function() {
2018-12-07 14:54:29 +00:00
const baseInclude = ['relation1'];
const updateInclude = {relation2: 'relation2Include'};
const expectedInclude = [
2016-08-19 17:46:59 +00:00
{relation2: 'relation2Include'},
{relation1: true},
];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
});
2015-05-29 17:50:37 +00:00
it('Merge array & array values to object', function() {
2018-12-07 14:54:29 +00:00
const baseInclude = ['relation1'];
const updateInclude = ['relation2'];
const expectedInclude = [
2016-08-19 17:46:59 +00:00
{relation2: true},
{relation1: true},
];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
});
2015-05-29 17:50:37 +00:00
it('Merge array & object values to object', function() {
2018-12-07 14:54:29 +00:00
const baseInclude = ['relation1'];
const updateInclude = {relation2: 'relation2Include'};
const expectedInclude = [
2016-08-19 17:46:59 +00:00
{relation2: 'relation2Include'},
{relation1: true},
];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
});
2015-05-29 17:50:37 +00:00
it('Merge object & object values to object', function() {
2018-12-07 14:54:29 +00:00
const baseInclude = {relation1: 'relation1Include'};
const updateInclude = {relation2: 'relation2Include'};
const expectedInclude = [
2016-08-19 17:46:59 +00:00
{relation2: 'relation2Include'},
{relation1: 'relation1Include'},
];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
});
2015-05-29 17:50:37 +00:00
it('Override property collision with update value', function() {
2018-12-07 14:54:29 +00:00
const baseInclude = {relation1: 'baseValue'};
const updateInclude = {relation1: 'updateValue'};
const expectedInclude = [
2016-08-19 17:46:59 +00:00
{relation1: 'updateValue'},
];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
});
it('Merge string includes & include with relation syntax properly',
2015-05-29 17:50:37 +00:00
function() {
2018-12-07 14:54:29 +00:00
const baseInclude = 'relation1';
const updateInclude = {relation: 'relation1'};
const expectedInclude = [
2016-08-19 17:46:59 +00:00
{relation: 'relation1'},
];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
});
2015-05-29 17:50:37 +00:00
it('Merge string includes & include with scope properly', function() {
2018-12-07 14:54:29 +00:00
const baseInclude = 'relation1';
const updateInclude = {
relation: 'relation1',
2016-08-19 17:46:59 +00:00
scope: {include: 'relation2'},
};
2018-12-07 14:54:29 +00:00
const expectedInclude = [
2016-08-19 17:46:59 +00:00
{relation: 'relation1', scope: {include: 'relation2'}},
];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
});
it('Merge includes with and without relation syntax properly',
2015-05-29 17:50:37 +00:00
function() {
// w & w/o relation syntax - no collision
2018-12-07 14:54:29 +00:00
let baseInclude = ['relation2'];
let updateInclude = {
relation: 'relation1',
2016-08-19 17:46:59 +00:00
scope: {include: 'relation2'},
};
2018-12-07 14:54:29 +00:00
let expectedInclude = [{
relation: 'relation1',
2016-08-19 17:46:59 +00:00
scope: {include: 'relation2'},
}, {relation2: true}];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
// w & w/o relation syntax - collision
baseInclude = ['relation1'];
2016-08-19 17:46:59 +00:00
updateInclude = {relation: 'relation1', scope: {include: 'relation2'}};
expectedInclude =
2016-08-19 17:46:59 +00:00
[{relation: 'relation1', scope: {include: 'relation2'}}];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
// w & w/o relation syntax - collision
2016-08-19 17:46:59 +00:00
baseInclude = {relation: 'relation1', scope: {include: 'relation2'}};
updateInclude = ['relation1'];
2016-08-19 17:46:59 +00:00
expectedInclude = [{relation1: true}];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
});
2015-05-29 17:50:37 +00:00
it('Merge includes with mixture of strings, arrays & objects properly', function() {
2018-12-07 14:54:29 +00:00
const baseInclude = ['relation1', {relation2: true},
2016-08-19 17:46:59 +00:00
{relation: 'relation3', scope: {where: {id: 'some id'}}},
{relation: 'relation5', scope: {where: {id: 'some id'}}},
];
2018-12-07 14:54:29 +00:00
const updateInclude = ['relation4', {relation3: true},
2016-08-19 17:46:59 +00:00
{relation: 'relation2', scope: {where: {id: 'some id'}}}];
2018-12-07 14:54:29 +00:00
const expectedInclude = [{relation4: true}, {relation3: true},
2016-08-19 17:46:59 +00:00
{relation: 'relation2', scope: {where: {id: 'some id'}}},
{relation1: true},
{relation: 'relation5', scope: {where: {id: 'some id'}}}];
checkInputOutput(baseInclude, updateInclude, expectedInclude);
});
2015-05-29 17:50:37 +00:00
});
describe('util.uniq', function() {
it('should dedupe an array with duplicate number entries', function() {
2018-12-07 14:54:29 +00:00
const a = [1, 2, 1, 3];
const b = uniq(a);
2015-05-29 17:50:37 +00:00
b.should.eql([1, 2, 3]);
});
it('should dedupe an array with duplicate string entries', function() {
2018-12-07 14:54:29 +00:00
const a = ['a', 'a', 'b', 'a'];
const b = uniq(a);
2015-05-29 17:50:37 +00:00
b.should.eql(['a', 'b']);
});
it('should dedupe an array with duplicate bson entries', function() {
2018-12-07 14:54:29 +00:00
const idOne = new ObjectID('59f9ec5dc7d59a00042f7c62');
const idTwo = new ObjectID('59f9ec5dc7d59a00042f7c63');
const a = [idOne, idTwo, new ObjectID('59f9ec5dc7d59a00042f7c62'),
new ObjectID('59f9ec5dc7d59a00042f7c62')];
2018-12-07 14:54:29 +00:00
const b = uniq(a);
b.should.eql([idOne, idTwo]);
});
2015-05-29 17:50:37 +00:00
it('should dedupe an array without duplicate number entries', function() {
2018-12-07 14:54:29 +00:00
const a = [1, 3, 2];
const b = uniq(a);
2015-05-29 17:50:37 +00:00
b.should.eql([1, 3, 2]);
});
it('should dedupe an array without duplicate string entries', function() {
2018-12-07 14:54:29 +00:00
const a = ['a', 'c', 'b'];
const b = uniq(a);
2015-05-29 17:50:37 +00:00
b.should.eql(['a', 'c', 'b']);
});
it('should dedupe an array without duplicate bson entries', function() {
2018-12-07 14:54:29 +00:00
const idOne = new ObjectID('59f9ec5dc7d59a00042f7c62');
const idTwo = new ObjectID('59f9ec5dc7d59a00042f7c63');
const idThree = new ObjectID('59f9ec5dc7d59a00042f7c64');
const a = [idOne, idTwo, idThree];
const b = uniq(a);
b.should.eql([idOne, idTwo, idThree]);
});
2015-05-29 17:50:37 +00:00
it('should allow null/undefined array', function() {
2018-12-07 14:54:29 +00:00
const a = null;
const b = uniq(a);
2015-05-29 17:50:37 +00:00
b.should.eql([]);
});
it('should report error for non-array arg', function() {
2018-12-07 14:54:29 +00:00
const a = '1';
2015-05-29 17:50:37 +00:00
try {
2018-12-07 14:54:29 +00:00
const b = uniq(a);
2015-05-29 17:50:37 +00:00
throw new Error('The test should have thrown an error');
} catch (err) {
err.should.be.instanceof(Error);
}
});
});
2015-07-24 19:56:31 +00:00
describe('util.toRegExp', function() {
2018-12-07 14:54:29 +00:00
let invalidDataTypes;
let validDataTypes;
2015-07-24 19:56:31 +00:00
before(function() {
invalidDataTypes = [0, true, {}, [], Function, null];
validDataTypes = ['string', /^regex/, new RegExp(/^regex/)];
});
it('should not accept invalid data types', function() {
invalidDataTypes.forEach(function(invalid) {
utils.toRegExp(invalid).should.be.an.Error;
});
});
it('should accept valid data types', function() {
validDataTypes.forEach(function(valid) {
utils.toRegExp(valid).should.not.be.an.Error;
});
});
context('with a regex string', function() {
it('should return a RegExp object when no regex flags are provided',
2018-06-12 07:13:32 +00:00
function() {
utils.toRegExp('^regex$').should.be.an.instanceOf(RegExp);
});
2015-07-24 19:56:31 +00:00
it('should throw an error when invalid regex flags are provided',
2018-06-12 07:13:32 +00:00
function() {
utils.toRegExp('^regex$/abc').should.be.an.Error;
});
2015-07-24 19:56:31 +00:00
it('should return a RegExp object when valid flags are provided',
2018-06-12 07:13:32 +00:00
function() {
utils.toRegExp('regex/igm').should.be.an.instanceOf(RegExp);
});
2015-07-24 19:56:31 +00:00
});
context('with a regex literal', function() {
it('should return a RegExp object', function() {
utils.toRegExp(/^regex$/igm).should.be.an.instanceOf(RegExp);
});
});
context('with a regex object', function() {
it('should return a RegExp object', function() {
utils.toRegExp(new RegExp('^regex$', 'igm')).should.be.an.instanceOf(RegExp);
});
});
});
describe('util.hasRegExpFlags', function() {
context('with a regex string', function() {
it('should be true when the regex has invalid flags', function() {
utils.hasRegExpFlags('^regex$/abc').should.be.ok;
});
it('should be true when the regex has valid flags', function() {
utils.hasRegExpFlags('^regex$/igm').should.be.ok;
});
it('should be false when the regex has no flags', function() {
utils.hasRegExpFlags('^regex$').should.not.be.ok;
utils.hasRegExpFlags('^regex$/').should.not.be.ok;
});
});
context('with a regex literal', function() {
it('should be true when the regex has valid flags', function() {
utils.hasRegExpFlags(/^regex$/igm).should.be.ok;
});
it('should be false when the regex has no flags', function() {
utils.hasRegExpFlags(/^regex$/).should.not.be.ok;
});
});
context('with a regex object', function() {
it('should be true when the regex has valid flags', function() {
utils.hasRegExpFlags(new RegExp(/^regex$/igm)).should.be.ok;
});
it('should be false when the regex has no flags', function() {
utils.hasRegExpFlags(new RegExp(/^regex$/)).should.not.be.ok;
});
});
});
describe('util.idsHaveDuplicates', function() {
context('with string IDs', function() {
it('should be true with a duplicate present', function() {
utils.idsHaveDuplicates(['a', 'b', 'a']).should.be.ok;
});
it('should be false when no duplicates are present', function() {
utils.idsHaveDuplicates(['a', 'b', 'c']).should.not.be.ok;
});
});
context('with numeric IDs', function() {
it('should be true with a duplicate present', function() {
utils.idsHaveDuplicates([1, 2, 1]).should.be.ok;
});
it('should be false when no duplicates are present', function() {
utils.idsHaveDuplicates([1, 2, 3]).should.not.be.ok;
});
});
context('with complex IDs', function() {
it('should be true with a duplicate present', function() {
utils.idsHaveDuplicates(['a', 'b', 'a'].map(id => ({id}))).should.be.ok;
});
it('should be false when no duplicates are present', function() {
utils.idsHaveDuplicates(['a', 'b', 'c'].map(id => ({id}))).should.not.be.ok;
});
});
});