Merge pull request #1289 from strongloop/fix/preventRelationsMerge
review model settings merge/inheritance policy (util/mergeSettings() ) : relations, acls, ...
This commit is contained in:
commit
d375d61519
|
@ -17,7 +17,9 @@ var deprecated = require('depd')('loopback-datasource-juggler');
|
||||||
var DefaultModelBaseClass = require('./model.js');
|
var DefaultModelBaseClass = require('./model.js');
|
||||||
var List = require('./list.js');
|
var List = require('./list.js');
|
||||||
var ModelDefinition = require('./model-definition.js');
|
var ModelDefinition = require('./model-definition.js');
|
||||||
var mergeSettings = require('./utils').mergeSettings;
|
var deepMerge = require('./utils').deepMerge;
|
||||||
|
var deepMergeProperty = require('./utils').deepMergeProperty;
|
||||||
|
var rankArrayElements = require('./utils').rankArrayElements;
|
||||||
var MixinProvider = require('./mixins');
|
var MixinProvider = require('./mixins');
|
||||||
|
|
||||||
// Set up types
|
// Set up types
|
||||||
|
@ -162,6 +164,15 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Assert current model's base class provides method `getMergePolicy()`.
|
||||||
|
assert(ModelBaseClass.getMergePolicy, `Base class ${ModelBaseClass.modelName}
|
||||||
|
does not provide method getMergePolicy(). Most likely it is not inheriting
|
||||||
|
from datasource-juggler's built-in default ModelBaseClass, which is an
|
||||||
|
incorrect usage of the framework.`);
|
||||||
|
|
||||||
|
// Initialize base model inheritance rank if not set already
|
||||||
|
ModelBaseClass.__rank = ModelBaseClass.__rank || 1;
|
||||||
|
|
||||||
// Make sure base properties are inherited
|
// Make sure base properties are inherited
|
||||||
// See https://github.com/strongloop/loopback-datasource-juggler/issues/293
|
// See https://github.com/strongloop/loopback-datasource-juggler/issues/293
|
||||||
if ((parent && !settings.base) || (!parent && settings.base)) {
|
if ((parent && !settings.base) || (!parent && settings.base)) {
|
||||||
|
@ -197,6 +208,9 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
||||||
hiddenProperty(ModelClass, 'modelName', className);
|
hiddenProperty(ModelClass, 'modelName', className);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Iterate sub model inheritance rank over base model rank
|
||||||
|
ModelClass.__rank = ModelBaseClass.__rank + 1;
|
||||||
|
|
||||||
util.inherits(ModelClass, ModelBaseClass);
|
util.inherits(ModelClass, ModelBaseClass);
|
||||||
|
|
||||||
// store class in model pool
|
// store class in model pool
|
||||||
|
@ -348,59 +362,61 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
||||||
* ```
|
* ```
|
||||||
*
|
*
|
||||||
* @param {String} className Name of the new model being defined.
|
* @param {String} className Name of the new model being defined.
|
||||||
* @options {Object} properties Properties to define for the model, added to properties of model being extended.
|
* @options {Object} subClassProperties child model properties, added to base model
|
||||||
* @options {Object} settings Model settings, such as relations and acls.
|
* properties.
|
||||||
*
|
* @options {Object} subClassSettings child model settings such as relations and acls,
|
||||||
|
* merged with base model settings.
|
||||||
*/
|
*/
|
||||||
ModelClass.extend = function(className, subclassProperties, subclassSettings) {
|
ModelClass.extend = function(className, subClassProperties, subClassSettings) {
|
||||||
var properties = ModelClass.definition.properties;
|
var baseClassProperties = ModelClass.definition.properties;
|
||||||
var settings = ModelClass.definition.settings;
|
var baseClassSettings = ModelClass.definition.settings;
|
||||||
|
|
||||||
subclassProperties = subclassProperties || {};
|
subClassProperties = subClassProperties || {};
|
||||||
subclassSettings = subclassSettings || {};
|
subClassSettings = subClassSettings || {};
|
||||||
|
|
||||||
// Check if subclass redefines the ids
|
// Check if subclass redefines the ids
|
||||||
var idFound = false;
|
var idFound = false;
|
||||||
for (var k in subclassProperties) {
|
for (var k in subClassProperties) {
|
||||||
if (subclassProperties[k] && subclassProperties[k].id) {
|
if (subClassProperties[k] && subClassProperties[k].id) {
|
||||||
idFound = true;
|
idFound = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merging the properties
|
// Merging the properties
|
||||||
var keys = Object.keys(properties);
|
var keys = Object.keys(baseClassProperties);
|
||||||
for (var i = 0, n = keys.length; i < n; i++) {
|
for (var i = 0, n = keys.length; i < n; i++) {
|
||||||
var key = keys[i];
|
var key = keys[i];
|
||||||
|
|
||||||
if (idFound && properties[key].id) {
|
if (idFound && baseClassProperties[key].id) {
|
||||||
// don't inherit id properties
|
// don't inherit id properties
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (subclassProperties[key] === undefined) {
|
if (subClassProperties[key] === undefined) {
|
||||||
var baseProp = properties[key];
|
var baseProp = baseClassProperties[key];
|
||||||
var basePropCopy = baseProp;
|
var basePropCopy = baseProp;
|
||||||
if (baseProp && typeof baseProp === 'object') {
|
if (baseProp && typeof baseProp === 'object') {
|
||||||
// Deep clone the base prop
|
// Deep clone the base properties
|
||||||
basePropCopy = mergeSettings(null, baseProp);
|
basePropCopy = deepMerge(baseProp);
|
||||||
}
|
}
|
||||||
subclassProperties[key] = basePropCopy;
|
subClassProperties[key] = basePropCopy;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge the settings
|
// Merging the settings
|
||||||
var originalSubclassSettings = subclassSettings;
|
var originalSubclassSettings = subClassSettings;
|
||||||
subclassSettings = mergeSettings(settings, subclassSettings);
|
let mergePolicy = ModelClass.getMergePolicy(subClassSettings);
|
||||||
|
subClassSettings = mergeSettings(baseClassSettings, subClassSettings, mergePolicy);
|
||||||
|
|
||||||
// Ensure 'base' is not inherited. Note we don't have to delete 'super'
|
// Ensure 'base' is not inherited. Note we don't have to delete 'super'
|
||||||
// as that is removed from settings by modelBuilder.define and thus
|
// as that is removed from settings by modelBuilder.define and thus
|
||||||
// it is never inherited
|
// it is never inherited
|
||||||
if (!originalSubclassSettings.base) {
|
if (!originalSubclassSettings.base) {
|
||||||
subclassSettings.base = ModelClass;
|
subClassSettings.base = ModelClass;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the subclass
|
// Define the subclass
|
||||||
var subClass = modelBuilder.define(className, subclassProperties, subclassSettings, ModelClass);
|
var subClass = modelBuilder.define(className, subClassProperties, subClassSettings, ModelClass);
|
||||||
|
|
||||||
// Calling the setup function
|
// Calling the setup function
|
||||||
if (typeof subClass.setup === 'function') {
|
if (typeof subClass.setup === 'function') {
|
||||||
|
@ -410,6 +426,92 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
||||||
return subClass;
|
return subClass;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Merge parent and child model settings according to the provided merge policy.
|
||||||
|
*
|
||||||
|
* Below is presented the expected merge behaviour for each option of the policy.
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* Here is an example of merge policy:
|
||||||
|
* ```
|
||||||
|
* {
|
||||||
|
* description: {replace: true}, // string or array
|
||||||
|
* properties: {patch: true}, // object
|
||||||
|
* hidden: {replace: false}, // array
|
||||||
|
* protected: {replace: false}, // array
|
||||||
|
* relations: {acls: true}, // object
|
||||||
|
* acls: {rank: true}, // array
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param {Object} baseClassSettings parent model settings.
|
||||||
|
* @param {Object} subClassSettings child model settings.
|
||||||
|
* @param {Object} mergePolicy merge policy, as defined in `ModelClass.getMergePolicy()`
|
||||||
|
* @return {Object} mergedSettings merged parent and child models settings.
|
||||||
|
*/
|
||||||
|
function mergeSettings(baseClassSettings, subClassSettings, mergePolicy) {
|
||||||
|
// deep clone base class settings
|
||||||
|
let mergedSettings = deepMerge(baseClassSettings);
|
||||||
|
|
||||||
|
Object.keys(baseClassSettings).forEach(function(key) {
|
||||||
|
// rank base class settings arrays elements where required
|
||||||
|
if (mergePolicy[key] && mergePolicy[key].rank) {
|
||||||
|
baseClassSettings[key] = rankArrayElements(baseClassSettings[key], ModelBaseClass.__rank);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(subClassSettings).forEach(function(key) {
|
||||||
|
// assign default merge policy to unknown settings if specified
|
||||||
|
// if none specified, a deep merge will be applied eventually
|
||||||
|
if (mergePolicy[key] == null) { // undefined or null
|
||||||
|
mergePolicy[key] = mergePolicy.__default || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// allow null value to remove unwanted settings from base class settings
|
||||||
|
if (subClassSettings[key] === mergePolicy.__delete) {
|
||||||
|
delete mergedSettings[key];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// rank sub class settings arrays elements where required
|
||||||
|
if (mergePolicy[key].rank) {
|
||||||
|
subClassSettings[key] = rankArrayElements(subClassSettings[key], ModelBaseClass.__rank + 1);
|
||||||
|
}
|
||||||
|
// replace base class settings where required
|
||||||
|
if (mergePolicy[key].replace) {
|
||||||
|
mergedSettings[key] = subClassSettings[key];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// patch inner properties of base class settings where required
|
||||||
|
if (mergePolicy[key].patch) {
|
||||||
|
// mergedSettings[key] might not be initialized
|
||||||
|
mergedSettings[key] = mergedSettings[key] || {};
|
||||||
|
Object.keys(subClassSettings[key]).forEach(function(innerKey) {
|
||||||
|
mergedSettings[key][innerKey] = subClassSettings[key][innerKey];
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// in case no merge policy matched, apply a deep merge
|
||||||
|
// this for example handles {replace: false} and {rank: true}
|
||||||
|
mergedSettings[key] = deepMergeProperty(baseClassSettings[key], subClassSettings[key]);
|
||||||
|
});
|
||||||
|
|
||||||
|
return mergedSettings;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a property for the model class
|
* Register a property for the model class
|
||||||
* @param {String} propertyName Name of the property.
|
* @param {String} propertyName Name of the property.
|
||||||
|
|
153
lib/model.js
153
lib/model.js
|
@ -674,6 +674,159 @@ ModelBaseClass.prototype.setStrict = function(strict) {
|
||||||
this.__strict = strict;
|
this.__strict = strict;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* `getMergePolicy()` provides model merge policies to apply when extending
|
||||||
|
* a child model from a base model. Such a policy drives the way parent/child model
|
||||||
|
* properties/settings are merged/mixed-in together.
|
||||||
|
*
|
||||||
|
* Below 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 built-in merge policy is as follows. It is returned by getMergePolicy()
|
||||||
|
* when calling the method with option `{configureModelMerge: true}`.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* {
|
||||||
|
* description: {replace: true}, // string or array
|
||||||
|
* options: {patch: true}, // object
|
||||||
|
* hidden: {replace: false}, // array
|
||||||
|
* protected: {replace: false}, // array
|
||||||
|
* indexes: {patch: true}, // object
|
||||||
|
* methods: {patch: true}, // object
|
||||||
|
* mixins: {patch: true}, // object
|
||||||
|
* relations: {patch: true}, // object
|
||||||
|
* scope: {replace: true}, // object
|
||||||
|
* scopes: {patch: true}, // object
|
||||||
|
* acls: {rank: true}, // array
|
||||||
|
* // this setting controls which child model property's value allows deleting
|
||||||
|
* // a base model's property
|
||||||
|
* __delete: null,
|
||||||
|
* // this setting controls the default merge behaviour for settings not defined
|
||||||
|
* // in the mergePolicy specification
|
||||||
|
* __default: {replace: true},
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* The legacy built-in merge policy is as follows, it is retuned by `getMergePolicy()`
|
||||||
|
* when avoiding option `configureModelMerge`.
|
||||||
|
* NOTE: it also provides the ACLs ranking in addition to the legacy behaviour, as well
|
||||||
|
* as fixes for settings 'description' and 'relations': matching relations from child
|
||||||
|
* replace relations from parents.
|
||||||
|
*
|
||||||
|
* ```
|
||||||
|
* {
|
||||||
|
* description: {replace: true}, // string or array
|
||||||
|
* properties: {patch: true}, // object
|
||||||
|
* hidden: {replace: false}, // array
|
||||||
|
* protected: {replace: false}, // array
|
||||||
|
* relations: {acls: true}, // object
|
||||||
|
* acls: {rank: true}, // array
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* `getMergePolicy()` can be customized using model's setting `configureModelMerge` as follows:
|
||||||
|
*
|
||||||
|
* ``` json
|
||||||
|
* {
|
||||||
|
* // ..
|
||||||
|
* options: {
|
||||||
|
* configureModelMerge: {
|
||||||
|
* // merge options
|
||||||
|
* }
|
||||||
|
* }
|
||||||
|
* // ..
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* NOTE: mergePolicy parameter can also defined at JSON model definition root
|
||||||
|
*
|
||||||
|
* `getMergePolicy()` method can also be extended programmatically as follows:
|
||||||
|
*
|
||||||
|
* ``` js
|
||||||
|
* myModel.getMergePolicy = function(options) {
|
||||||
|
* const origin = myModel.base.getMergePolicy(options);
|
||||||
|
* return Object.assign({}, origin, {
|
||||||
|
* // new/overriding options
|
||||||
|
* });
|
||||||
|
* };
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param {Object} options option `configureModelMerge` can be used to alter the
|
||||||
|
* returned merge policy:
|
||||||
|
* - `configureModelMerge: true` will have the method return the recommended merge policy.
|
||||||
|
* - `configureModelMerge: {..}` will actually have the method return the provided object.
|
||||||
|
* - not providing this options will have the method return a merge policy emulating the
|
||||||
|
* the model merge behaviour up to datasource-juggler v3.6.1, as well as the ACLs ranking.
|
||||||
|
* @returns {Object} mergePolicy The model merge policy to apply when using the
|
||||||
|
* current model as base class for a child model
|
||||||
|
*/
|
||||||
|
ModelBaseClass.getMergePolicy = function(options) {
|
||||||
|
// NOTE: merge policy equivalent to datasource-juggler behaviour up to v3.6.1
|
||||||
|
// + fix for description arrays that should not be merged
|
||||||
|
// + fix for relations that should patch matching relations
|
||||||
|
// + ranking of ACLs
|
||||||
|
var mergePolicy = {
|
||||||
|
description: {replace: true}, // string or array
|
||||||
|
properties: {patch: true}, // object
|
||||||
|
hidden: {replace: false}, // array
|
||||||
|
protected: {replace: false}, // array
|
||||||
|
relations: {patch: true}, // object
|
||||||
|
acls: {rank: true}, // array
|
||||||
|
};
|
||||||
|
|
||||||
|
var config = (options || {}).configureModelMerge;
|
||||||
|
|
||||||
|
if (config === true) {
|
||||||
|
// NOTE: recommended merge policy from datasource-juggler v3.6.2
|
||||||
|
mergePolicy = {
|
||||||
|
description: {replace: true}, // string or array
|
||||||
|
options: {patch: true}, // object
|
||||||
|
// properties: {patch: true}, // object // NOTE: not part of configurable merge
|
||||||
|
hidden: {replace: false}, // array
|
||||||
|
protected: {replace: false}, // array
|
||||||
|
indexes: {patch: true}, // object
|
||||||
|
methods: {patch: true}, // object
|
||||||
|
mixins: {patch: true}, // object
|
||||||
|
// validations: {patch: true}, // object // NOTE: not implemented
|
||||||
|
relations: {patch: true}, // object
|
||||||
|
scope: {replace: true}, // object
|
||||||
|
scopes: {patch: true}, // object
|
||||||
|
acls: {rank: true}, // array
|
||||||
|
// this option controls which value assigned on child model allows deleting
|
||||||
|
// a base model's setting
|
||||||
|
__delete: null,
|
||||||
|
// this option controls the default merge behaviour for settings not defined
|
||||||
|
// in the mergePolicy specification
|
||||||
|
__default: {replace: true},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// override mergePolicy with provided model setting if required
|
||||||
|
if (config && typeof config === 'object' && !Array.isArray(config)) {
|
||||||
|
// config is an object
|
||||||
|
mergePolicy = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mergePolicy;
|
||||||
|
};
|
||||||
|
|
||||||
// Mixin observer
|
// Mixin observer
|
||||||
jutil.mixin(ModelBaseClass, require('./observer'));
|
jutil.mixin(ModelBaseClass, require('./observer'));
|
||||||
|
|
||||||
|
|
123
lib/utils.js
123
lib/utils.js
|
@ -9,7 +9,8 @@ exports.fieldsToArray = fieldsToArray;
|
||||||
exports.selectFields = selectFields;
|
exports.selectFields = selectFields;
|
||||||
exports.removeUndefined = removeUndefined;
|
exports.removeUndefined = removeUndefined;
|
||||||
exports.parseSettings = parseSettings;
|
exports.parseSettings = parseSettings;
|
||||||
exports.mergeSettings = exports.deepMerge = mergeSettings;
|
exports.mergeSettings = exports.deepMerge = deepMerge;
|
||||||
|
exports.deepMergeProperty = deepMergeProperty;
|
||||||
exports.isPlainObject = isPlainObject;
|
exports.isPlainObject = isPlainObject;
|
||||||
exports.defineCachedRelations = defineCachedRelations;
|
exports.defineCachedRelations = defineCachedRelations;
|
||||||
exports.sortObjectsByIds = sortObjectsByIds;
|
exports.sortObjectsByIds = sortObjectsByIds;
|
||||||
|
@ -24,6 +25,7 @@ exports.idEquals = idEquals;
|
||||||
exports.findIndexOf = findIndexOf;
|
exports.findIndexOf = findIndexOf;
|
||||||
exports.collectTargetIds = collectTargetIds;
|
exports.collectTargetIds = collectTargetIds;
|
||||||
exports.idName = idName;
|
exports.idName = idName;
|
||||||
|
exports.rankArrayElements = rankArrayElements;
|
||||||
|
|
||||||
var g = require('strong-globalize')();
|
var g = require('strong-globalize')();
|
||||||
var traverse = require('traverse');
|
var traverse = require('traverse');
|
||||||
|
@ -367,52 +369,64 @@ function parseSettings(urlStr) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Merge model settings
|
* Objects deep merge
|
||||||
*
|
*
|
||||||
* Folked from https://github.com/nrf110/deepmerge/blob/master/index.js
|
* Forked from https://github.com/nrf110/deepmerge/blob/master/index.js
|
||||||
*
|
*
|
||||||
* The original function tries to merge array items if they are objects
|
* The original function tries to merge array items if they are objects, this
|
||||||
|
* was changed to always push new items in arrays, independently of their type.
|
||||||
*
|
*
|
||||||
* @param {Object} target The target settings object
|
* NOTE: The function operates as a deep clone when called with a single object
|
||||||
* @param {Object} src The source settings object
|
* argument.
|
||||||
* @returns {Object} The merged settings object
|
*
|
||||||
|
* @param {Object} base The base object
|
||||||
|
* @param {Object} extras The object to merge with base
|
||||||
|
* @returns {Object} The merged object
|
||||||
*/
|
*/
|
||||||
function mergeSettings(target, src) {
|
function deepMerge(base, extras) {
|
||||||
var array = Array.isArray(src);
|
// deepMerge allows undefined extras to allow deep cloning of arrays
|
||||||
|
var array = Array.isArray(base) && (Array.isArray(extras) || !extras);
|
||||||
var dst = array && [] || {};
|
var dst = array && [] || {};
|
||||||
|
|
||||||
if (array) {
|
if (array) {
|
||||||
target = target || [];
|
// extras or base is an array
|
||||||
// Add items from target into dst
|
extras = extras || [];
|
||||||
dst = dst.concat(target);
|
// Add items from base into dst
|
||||||
// Add non-existent items from source into dst
|
dst = dst.concat(base);
|
||||||
src.forEach(function(e) {
|
// Add non-existent items from extras into dst
|
||||||
|
extras.forEach(function(e) {
|
||||||
if (dst.indexOf(e) === -1) {
|
if (dst.indexOf(e) === -1) {
|
||||||
dst.push(e);
|
dst.push(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
if (target != null && typeof target === 'object') {
|
if (base != null && typeof base === 'object') {
|
||||||
// Add properties from target to dst
|
// Add properties from base to dst
|
||||||
Object.keys(target).forEach(function(key) {
|
Object.keys(base).forEach(function(key) {
|
||||||
dst[key] = target[key];
|
if (base[key] && typeof base[key] === 'object') {
|
||||||
|
// call deepMerge on nested object to operate a deep clone
|
||||||
|
dst[key] = deepMerge(base[key]);
|
||||||
|
} else {
|
||||||
|
dst[key] = base[key];
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (src != null && typeof src === 'object') {
|
if (extras != null && typeof extras === 'object') {
|
||||||
// Source is an object
|
// extras is an object {}
|
||||||
Object.keys(src).forEach(function(key) {
|
Object.keys(extras).forEach(function(key) {
|
||||||
var srcValue = src[key];
|
var extra = extras[key];
|
||||||
if (srcValue == null || typeof srcValue !== 'object') {
|
if (extra == null || typeof extra !== 'object') {
|
||||||
// The source item value is null, undefined or not an object
|
// extra item value is null, undefined or not an object
|
||||||
dst[key] = srcValue;
|
dst[key] = extra;
|
||||||
} else {
|
} else {
|
||||||
// The source item value is an object
|
// The extra item value is an object
|
||||||
if (target == null || typeof target !== 'object' ||
|
if (base == null || typeof base !== 'object' ||
|
||||||
target[key] == null) {
|
base[key] == null) {
|
||||||
// If target is not an object or target item value
|
// base is not an object or base item value is undefined or null
|
||||||
dst[key] = srcValue;
|
dst[key] = extra;
|
||||||
} else {
|
} else {
|
||||||
dst[key] = mergeSettings(target[key], src[key]);
|
// call deepMerge on nested object
|
||||||
|
dst[key] = deepMerge(base[key], extra);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -422,6 +436,53 @@ function mergeSettings(target, src) {
|
||||||
return dst;
|
return dst;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties deep merge
|
||||||
|
* Similar as deepMerge but also works on single properties of any type
|
||||||
|
*
|
||||||
|
* @param {Object} base The base property
|
||||||
|
* @param {Object} extras The property to merge with base
|
||||||
|
* @returns {Object} The merged property
|
||||||
|
*/
|
||||||
|
function deepMergeProperty(base, extras) {
|
||||||
|
let mergedObject = deepMerge({key: base}, {key: extras});
|
||||||
|
let mergedProperty = mergedObject.key;
|
||||||
|
return mergedProperty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a property __rank to array elements of type object {}
|
||||||
|
* If an inner element already has the __rank property it is not altered
|
||||||
|
* NOTE: the function mutates the provided array
|
||||||
|
*
|
||||||
|
* @param array The original array
|
||||||
|
* @param rank The rank to apply to array elements
|
||||||
|
* @return rankedArray The original array with newly ranked elements
|
||||||
|
*/
|
||||||
|
function rankArrayElements(array, rank) {
|
||||||
|
if (!Array.isArray(array) || !Number.isFinite(rank))
|
||||||
|
return array;
|
||||||
|
|
||||||
|
array.forEach(function(el) {
|
||||||
|
// only apply ranking on objects {} in array
|
||||||
|
if (!el || typeof el != 'object' || Array.isArray(el))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// property rank is already defined for array element
|
||||||
|
if (el.__rank)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// define rank property as non-enumerable and read-only
|
||||||
|
Object.defineProperty(el, '__rank', {
|
||||||
|
writable: false,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: false,
|
||||||
|
value: rank,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Define an non-enumerable __cachedRelations property
|
* Define an non-enumerable __cachedRelations property
|
||||||
* @param {Object} obj The obj to receive the __cachedRelations
|
* @param {Object} obj The obj to receive the __cachedRelations
|
||||||
|
|
|
@ -1802,140 +1802,6 @@ describe('ModelBuilder processing json files', function() {
|
||||||
{customerId: {type: String, id: true}}, {}, User);
|
{customerId: {type: String, id: true}}, {}, User);
|
||||||
assert(Customer.prototype instanceof User);
|
assert(Customer.prototype instanceof User);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('allows model extension', function(done) {
|
|
||||||
var modelBuilder = new ModelBuilder();
|
|
||||||
|
|
||||||
var User = modelBuilder.define('User', {
|
|
||||||
name: String,
|
|
||||||
bio: ModelBuilder.Text,
|
|
||||||
approved: Boolean,
|
|
||||||
joinedAt: Date,
|
|
||||||
age: Number,
|
|
||||||
});
|
|
||||||
|
|
||||||
var Customer = User.extend('Customer', {customerId: {type: String, id: true}});
|
|
||||||
|
|
||||||
var customer = new Customer({name: 'Joe', age: 20, customerId: 'c01'});
|
|
||||||
|
|
||||||
customer.should.be.type('object').and.have.property('name', 'Joe');
|
|
||||||
customer.should.have.property('name', 'Joe');
|
|
||||||
customer.should.have.property('age', 20);
|
|
||||||
customer.should.have.property('customerId', 'c01');
|
|
||||||
customer.should.have.property('bio', undefined);
|
|
||||||
|
|
||||||
// The properties are defined at prototype level
|
|
||||||
assert.equal(Object.keys(customer).filter(function(k) {
|
|
||||||
// Remove internal properties
|
|
||||||
return k.indexOf('__') === -1;
|
|
||||||
}).length, 0);
|
|
||||||
var count = 0;
|
|
||||||
for (var p in customer) {
|
|
||||||
if (p.indexOf('__') === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (typeof customer[p] !== 'function') {
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert.equal(count, 7); // Please note there is an injected id from User prototype
|
|
||||||
assert.equal(Object.keys(customer.toObject()).filter(function(k) {
|
|
||||||
// Remove internal properties
|
|
||||||
return k.indexOf('__') === -1;
|
|
||||||
}).length, 6);
|
|
||||||
|
|
||||||
done(null, customer);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('allows model extension with merged settings', function(done) {
|
|
||||||
var modelBuilder = new ModelBuilder();
|
|
||||||
|
|
||||||
var User = modelBuilder.define('User', {
|
|
||||||
name: String,
|
|
||||||
}, {
|
|
||||||
defaultPermission: 'ALLOW',
|
|
||||||
acls: [
|
|
||||||
{
|
|
||||||
principalType: 'ROLE',
|
|
||||||
principalId: '$everyone',
|
|
||||||
permission: 'ALLOW',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
relations: {
|
|
||||||
posts: {
|
|
||||||
type: 'hasMany',
|
|
||||||
model: 'Post',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
var Customer = User.extend('Customer',
|
|
||||||
{customerId: {type: String, id: true}}, {
|
|
||||||
defaultPermission: 'DENY',
|
|
||||||
acls: [
|
|
||||||
{
|
|
||||||
principalType: 'ROLE',
|
|
||||||
principalId: '$unauthenticated',
|
|
||||||
permission: 'DENY',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
relations: {
|
|
||||||
orders: {
|
|
||||||
type: 'hasMany',
|
|
||||||
model: 'Order',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
assert.deepEqual(User.settings, {
|
|
||||||
defaultPermission: 'ALLOW',
|
|
||||||
acls: [
|
|
||||||
{
|
|
||||||
principalType: 'ROLE',
|
|
||||||
principalId: '$everyone',
|
|
||||||
permission: 'ALLOW',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
relations: {
|
|
||||||
posts: {
|
|
||||||
type: 'hasMany',
|
|
||||||
model: 'Post',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
strict: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.deepEqual(Customer.settings, {
|
|
||||||
defaultPermission: 'DENY',
|
|
||||||
acls: [
|
|
||||||
{
|
|
||||||
principalType: 'ROLE',
|
|
||||||
principalId: '$everyone',
|
|
||||||
permission: 'ALLOW',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
principalType: 'ROLE',
|
|
||||||
principalId: '$unauthenticated',
|
|
||||||
permission: 'DENY',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
relations: {
|
|
||||||
posts: {
|
|
||||||
type: 'hasMany',
|
|
||||||
model: 'Post',
|
|
||||||
},
|
|
||||||
orders: {
|
|
||||||
type: 'hasMany',
|
|
||||||
model: 'Order',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
strict: false,
|
|
||||||
base: User,
|
|
||||||
});
|
|
||||||
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('DataSource constructor', function() {
|
describe('DataSource constructor', function() {
|
||||||
|
|
|
@ -257,43 +257,6 @@ describe('ModelDefinition class', function() {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should inherit prototype using option.base', function() {
|
|
||||||
var modelBuilder = memory.modelBuilder;
|
|
||||||
var parent = memory.createModel('parent', {}, {
|
|
||||||
relations: {
|
|
||||||
children: {
|
|
||||||
type: 'hasMany',
|
|
||||||
model: 'anotherChild',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
var baseChild = modelBuilder.define('baseChild');
|
|
||||||
baseChild.attachTo(memory);
|
|
||||||
// the name of this must begin with a letter < b
|
|
||||||
// for this test to fail
|
|
||||||
var anotherChild = baseChild.extend('anotherChild');
|
|
||||||
|
|
||||||
assert(anotherChild.prototype instanceof baseChild);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore inherited options.base', function() {
|
|
||||||
var modelBuilder = memory.modelBuilder;
|
|
||||||
var base = modelBuilder.define('base');
|
|
||||||
var child = base.extend('child', {}, {base: 'base'});
|
|
||||||
var grandChild = child.extend('grand-child');
|
|
||||||
assert.equal('child', grandChild.base.modelName);
|
|
||||||
assert(grandChild.prototype instanceof child);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore inherited options.super', function() {
|
|
||||||
var modelBuilder = memory.modelBuilder;
|
|
||||||
var base = modelBuilder.define('base');
|
|
||||||
var child = base.extend('child', {}, {super: 'base'});
|
|
||||||
var grandChild = child.extend('grand-child');
|
|
||||||
assert.equal('child', grandChild.base.modelName);
|
|
||||||
assert(grandChild.prototype instanceof child);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should serialize protected properties into JSON', function() {
|
it('should serialize protected properties into JSON', function() {
|
||||||
var modelBuilder = memory.modelBuilder;
|
var modelBuilder = memory.modelBuilder;
|
||||||
var ProtectedModel = memory.createModel('protected', {}, {
|
var ProtectedModel = memory.createModel('protected', {}, {
|
||||||
|
|
|
@ -0,0 +1,619 @@
|
||||||
|
// Copyright IBM Corp. 2013,2016. All Rights Reserved.
|
||||||
|
// Node module: loopback-datasource-juggler
|
||||||
|
// This file is licensed under the MIT License.
|
||||||
|
// License text available at https://opensource.org/licenses/MIT
|
||||||
|
|
||||||
|
// This test is written in mocha+should.js
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const should = require('./init.js');
|
||||||
|
const assert = require('assert');
|
||||||
|
|
||||||
|
const jdb = require('../');
|
||||||
|
const ModelBuilder = jdb.ModelBuilder;
|
||||||
|
const DataSource = jdb.DataSource;
|
||||||
|
const Memory = require('../lib/connectors/memory');
|
||||||
|
|
||||||
|
const ModelDefinition = require('../lib/model-definition');
|
||||||
|
|
||||||
|
describe('Model class inheritance', function() {
|
||||||
|
let memory;
|
||||||
|
beforeEach(function() {
|
||||||
|
memory = new DataSource({connector: Memory});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ModelBaseClass.getMergePolicy()', function() {
|
||||||
|
const legacyMergePolicy = {
|
||||||
|
description: {replace: true},
|
||||||
|
properties: {patch: true},
|
||||||
|
hidden: {replace: false},
|
||||||
|
protected: {replace: false},
|
||||||
|
relations: {patch: true},
|
||||||
|
acls: {rank: true},
|
||||||
|
};
|
||||||
|
|
||||||
|
const recommendedMergePolicy = {
|
||||||
|
description: {replace: true},
|
||||||
|
options: {patch: true},
|
||||||
|
hidden: {replace: false},
|
||||||
|
protected: {replace: false},
|
||||||
|
indexes: {patch: true},
|
||||||
|
methods: {patch: true},
|
||||||
|
mixins: {patch: true},
|
||||||
|
relations: {patch: true},
|
||||||
|
scope: {replace: true},
|
||||||
|
scopes: {patch: true},
|
||||||
|
acls: {rank: true},
|
||||||
|
__delete: null,
|
||||||
|
__default: {replace: true},
|
||||||
|
};
|
||||||
|
|
||||||
|
let modelBuilder, base;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
modelBuilder = memory.modelBuilder;
|
||||||
|
base = modelBuilder.define('base');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns legacy merge policy by default', function() {
|
||||||
|
const mergePolicy = base.getMergePolicy();
|
||||||
|
should.deepEqual(mergePolicy, legacyMergePolicy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns recommended merge policy when called with option ' +
|
||||||
|
'`{configureModelMerge: true}`', function() {
|
||||||
|
const mergePolicy = base.getMergePolicy({configureModelMerge: true});
|
||||||
|
should.deepEqual(mergePolicy, recommendedMergePolicy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles custom merge policy defined via model.settings', function() {
|
||||||
|
let mergePolicy;
|
||||||
|
const newMergePolicy = {
|
||||||
|
relations: {patch: true},
|
||||||
|
};
|
||||||
|
|
||||||
|
// saving original getMergePolicy method
|
||||||
|
let originalGetMergePolicy = base.getMergePolicy;
|
||||||
|
|
||||||
|
// the injected getMergePolicy method captures the provided configureModelMerge option
|
||||||
|
base.getMergePolicy = function(options) {
|
||||||
|
mergePolicy = options && options.configureModelMerge;
|
||||||
|
return originalGetMergePolicy(options);
|
||||||
|
};
|
||||||
|
|
||||||
|
// calling extend() on base model calls base.getMergePolicy() internally
|
||||||
|
// child model settings are passed as 3rd parameter
|
||||||
|
const child = base.extend('child', {}, {configureModelMerge: newMergePolicy});
|
||||||
|
|
||||||
|
should.deepEqual(mergePolicy, newMergePolicy);
|
||||||
|
|
||||||
|
// restoring original getMergePolicy method
|
||||||
|
base.getMergePolicy = originalGetMergePolicy;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can be extended by user', function() {
|
||||||
|
const alteredMergePolicy = Object.assign({}, recommendedMergePolicy, {
|
||||||
|
__delete: false,
|
||||||
|
});
|
||||||
|
// extending the builtin getMergePolicy function
|
||||||
|
base.getMergePolicy = function(options) {
|
||||||
|
const origin = base.base.getMergePolicy(options);
|
||||||
|
return Object.assign({}, origin, {
|
||||||
|
__delete: false,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const mergePolicy = base.getMergePolicy({configureModelMerge: true});
|
||||||
|
should.deepEqual(mergePolicy, alteredMergePolicy);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is inherited by child model', function() {
|
||||||
|
const child = base.extend('child', {}, {configureModelMerge: true});
|
||||||
|
// get mergePolicy from child
|
||||||
|
const mergePolicy = child.getMergePolicy({configureModelMerge: true});
|
||||||
|
should.deepEqual(mergePolicy, recommendedMergePolicy);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Merge policy WITHOUT flag `configureModelMerge`', function() {
|
||||||
|
it('inherits prototype using option.base', function() {
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const parent = memory.createModel('parent', {}, {
|
||||||
|
relations: {
|
||||||
|
children: {
|
||||||
|
type: 'hasMany',
|
||||||
|
model: 'anotherChild',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const baseChild = modelBuilder.define('baseChild');
|
||||||
|
baseChild.attachTo(memory);
|
||||||
|
// the name of this must begin with a letter < b
|
||||||
|
// for this test to fail
|
||||||
|
const anotherChild = baseChild.extend('anotherChild');
|
||||||
|
|
||||||
|
assert(anotherChild.prototype instanceof baseChild);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores inherited options.base', function() {
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const base = modelBuilder.define('base');
|
||||||
|
const child = base.extend('child', {}, {base: 'base'});
|
||||||
|
const grandChild = child.extend('grand-child');
|
||||||
|
assert.equal('child', grandChild.base.modelName);
|
||||||
|
assert(grandChild.prototype instanceof child);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores inherited options.super', function() {
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const base = modelBuilder.define('base');
|
||||||
|
const child = base.extend('child', {}, {super: 'base'});
|
||||||
|
const grandChild = child.extend('grand-child');
|
||||||
|
assert.equal('child', grandChild.base.modelName);
|
||||||
|
assert(grandChild.prototype instanceof child);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows model extension', function(done) {
|
||||||
|
var modelBuilder = new ModelBuilder();
|
||||||
|
|
||||||
|
var User = modelBuilder.define('User', {
|
||||||
|
name: String,
|
||||||
|
bio: ModelBuilder.Text,
|
||||||
|
approved: Boolean,
|
||||||
|
joinedAt: Date,
|
||||||
|
age: Number,
|
||||||
|
});
|
||||||
|
|
||||||
|
var Customer = User.extend('Customer', {customerId: {type: String, id: true}});
|
||||||
|
|
||||||
|
var customer = new Customer({name: 'Joe', age: 20, customerId: 'c01'});
|
||||||
|
|
||||||
|
customer.should.be.type('object').and.have.property('name', 'Joe');
|
||||||
|
customer.should.have.property('name', 'Joe');
|
||||||
|
customer.should.have.property('age', 20);
|
||||||
|
customer.should.have.property('customerId', 'c01');
|
||||||
|
customer.should.have.property('bio', undefined);
|
||||||
|
|
||||||
|
// The properties are defined at prototype level
|
||||||
|
assert.equal(Object.keys(customer).filter(function(k) {
|
||||||
|
// Remove internal properties
|
||||||
|
return k.indexOf('__') === -1;
|
||||||
|
}).length, 0);
|
||||||
|
var count = 0;
|
||||||
|
for (var p in customer) {
|
||||||
|
if (p.indexOf('__') === 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (typeof customer[p] !== 'function') {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.equal(count, 7); // Please note there is an injected id from User prototype
|
||||||
|
assert.equal(Object.keys(customer.toObject()).filter(function(k) {
|
||||||
|
// Remove internal properties
|
||||||
|
return k.indexOf('__') === -1;
|
||||||
|
}).length, 6);
|
||||||
|
|
||||||
|
done(null, customer);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows model extension with merged settings', function(done) {
|
||||||
|
var modelBuilder = new ModelBuilder();
|
||||||
|
|
||||||
|
var User = modelBuilder.define('User', {
|
||||||
|
name: String,
|
||||||
|
}, {
|
||||||
|
defaultPermission: 'ALLOW',
|
||||||
|
acls: [
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone',
|
||||||
|
permission: 'ALLOW',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
relations: {
|
||||||
|
posts: {
|
||||||
|
type: 'hasMany',
|
||||||
|
model: 'Post',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
var Customer = User.extend('Customer',
|
||||||
|
{customerId: {type: String, id: true}}, {
|
||||||
|
defaultPermission: 'DENY',
|
||||||
|
acls: [
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$unauthenticated',
|
||||||
|
permission: 'DENY',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
relations: {
|
||||||
|
orders: {
|
||||||
|
type: 'hasMany',
|
||||||
|
model: 'Order',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(User.settings, {
|
||||||
|
defaultPermission: 'ALLOW',
|
||||||
|
acls: [
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone',
|
||||||
|
permission: 'ALLOW',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
relations: {
|
||||||
|
posts: {
|
||||||
|
type: 'hasMany',
|
||||||
|
model: 'Post',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
strict: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(Customer.settings, {
|
||||||
|
defaultPermission: 'DENY',
|
||||||
|
acls: [
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone',
|
||||||
|
permission: 'ALLOW',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$unauthenticated',
|
||||||
|
permission: 'DENY',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
relations: {
|
||||||
|
posts: {
|
||||||
|
type: 'hasMany',
|
||||||
|
model: 'Post',
|
||||||
|
},
|
||||||
|
orders: {
|
||||||
|
type: 'hasMany',
|
||||||
|
model: 'Order',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
strict: false,
|
||||||
|
base: User,
|
||||||
|
});
|
||||||
|
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defines rank of ACLs according to model\'s inheritance rank', function() {
|
||||||
|
// a simple test is enough as we already fully tested option `{rank: true}`
|
||||||
|
// in tests with flag `configureModelMerge`
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const base = modelBuilder.define('base', {}, {acls: [
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone',
|
||||||
|
property: 'oneMethod',
|
||||||
|
permission: 'ALLOW',
|
||||||
|
},
|
||||||
|
]});
|
||||||
|
const childRank1 = modelBuilder.define('childRank1', {}, {
|
||||||
|
base: base,
|
||||||
|
acls: [
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone',
|
||||||
|
property: 'oneMethod',
|
||||||
|
permission: 'DENY',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedSettings = {
|
||||||
|
acls: [
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone',
|
||||||
|
property: 'oneMethod',
|
||||||
|
permission: 'ALLOW',
|
||||||
|
__rank: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone',
|
||||||
|
property: 'oneMethod',
|
||||||
|
permission: 'DENY',
|
||||||
|
__rank: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
should.deepEqual(childRank1.settings.acls, expectedSettings.acls);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('replaces baseClass relations with matching subClass relations', function() {
|
||||||
|
// merge policy of settings.relations is {patch: true}
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const base = modelBuilder.define('base', {}, {
|
||||||
|
relations: {
|
||||||
|
user: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'User',
|
||||||
|
foreignKey: 'userId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const child = base.extend('child', {}, {
|
||||||
|
relations: {
|
||||||
|
user: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
idName: 'id',
|
||||||
|
polymorphic: {
|
||||||
|
idType: 'string',
|
||||||
|
foreignKey: 'userId',
|
||||||
|
discriminator: 'principalType',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedSettings = {
|
||||||
|
relations: {
|
||||||
|
user: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
idName: 'id',
|
||||||
|
polymorphic: {
|
||||||
|
idType: 'string',
|
||||||
|
foreignKey: 'userId',
|
||||||
|
discriminator: 'principalType',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
should.deepEqual(child.settings.relations, expectedSettings.relations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Merge policy WITH flag `configureModelMerge: true`', function() {
|
||||||
|
it('`{__delete: null}` allows deleting base model settings by assigning ' +
|
||||||
|
'null value at sub model level', function() {
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const base = modelBuilder.define('base', {}, {
|
||||||
|
anyParam: {oneKey: 'this should be removed'},
|
||||||
|
});
|
||||||
|
const child = base.extend('child', {}, {
|
||||||
|
anyParam: null,
|
||||||
|
configureModelMerge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedSettings = {};
|
||||||
|
|
||||||
|
should.deepEqual(child.settings.description, expectedSettings.description);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('`{rank: true}` defines rank of array elements ' +
|
||||||
|
'according to model\'s inheritance rank', function() {
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const base = modelBuilder.define('base', {}, {acls: [
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone',
|
||||||
|
property: 'oneMethod',
|
||||||
|
permission: 'ALLOW',
|
||||||
|
},
|
||||||
|
]});
|
||||||
|
const childRank1 = modelBuilder.define('childRank1', {}, {
|
||||||
|
base: base,
|
||||||
|
acls: [
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$owner',
|
||||||
|
property: 'anotherMethod',
|
||||||
|
permission: 'ALLOW',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
configureModelMerge: true,
|
||||||
|
});
|
||||||
|
const childRank2 = childRank1.extend('childRank2', {}, {});
|
||||||
|
const childRank3 = childRank2.extend('childRank3', {}, {
|
||||||
|
acls: [
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone',
|
||||||
|
property: 'oneMethod',
|
||||||
|
permission: 'DENY',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
configureModelMerge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedSettings = {
|
||||||
|
acls: [
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone',
|
||||||
|
property: 'oneMethod',
|
||||||
|
permission: 'ALLOW',
|
||||||
|
__rank: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$owner',
|
||||||
|
property: 'anotherMethod',
|
||||||
|
permission: 'ALLOW',
|
||||||
|
__rank: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone',
|
||||||
|
property: 'oneMethod',
|
||||||
|
permission: 'DENY',
|
||||||
|
__rank: 4,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
should.deepEqual(childRank3.settings.acls, expectedSettings.acls);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('`{replace: true}` replaces base model array with sub model matching ' +
|
||||||
|
'array', function() {
|
||||||
|
// merge policy of settings.description is {replace: true}
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const base = modelBuilder.define('base', {}, {
|
||||||
|
description: ['base', 'model', 'description'],
|
||||||
|
});
|
||||||
|
const child = base.extend('child', {}, {
|
||||||
|
description: ['this', 'is', 'child', 'model', 'description'],
|
||||||
|
configureModelMerge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedSettings = {
|
||||||
|
description: ['this', 'is', 'child', 'model', 'description'],
|
||||||
|
};
|
||||||
|
|
||||||
|
should.deepEqual(child.settings.description, expectedSettings.description);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('`{replace:true}` is applied on array parameters not defined in merge policy', function() {
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const base = modelBuilder.define('base', {}, {
|
||||||
|
unknownArrayParam: ['this', 'should', 'be', 'replaced'],
|
||||||
|
});
|
||||||
|
const child = base.extend('child', {}, {
|
||||||
|
unknownArrayParam: ['this', 'should', 'remain', 'after', 'merge'],
|
||||||
|
configureModelMerge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedSettings = {
|
||||||
|
unknownArrayParam: ['this', 'should', 'remain', 'after', 'merge'],
|
||||||
|
};
|
||||||
|
|
||||||
|
should.deepEqual(child.settings.description, expectedSettings.description);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('`{replace:true}` is applied on object {} parameters not defined in mergePolicy', function() {
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const base = modelBuilder.define('base', {}, {
|
||||||
|
unknownObjectParam: {oneKey: 'this should be replaced'},
|
||||||
|
});
|
||||||
|
const child = base.extend('child', {}, {
|
||||||
|
unknownObjectParam: {anotherKey: 'this should remain after merge'},
|
||||||
|
configureModelMerge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedSettings = {
|
||||||
|
unknownObjectParam: {anotherKey: 'this should remain after merge'},
|
||||||
|
};
|
||||||
|
|
||||||
|
should.deepEqual(child.settings.description, expectedSettings.description);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('`{replace: false}` adds distinct members of matching arrays from ' +
|
||||||
|
'base model and sub model', function() {
|
||||||
|
// merge policy of settings.hidden is {replace: false}
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const base = modelBuilder.define('base', {}, {
|
||||||
|
hidden: ['firstProperty', 'secondProperty'],
|
||||||
|
});
|
||||||
|
const child = base.extend('child', {}, {
|
||||||
|
hidden: ['secondProperty', 'thirdProperty'],
|
||||||
|
configureModelMerge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedSettings = {
|
||||||
|
hidden: ['firstProperty', 'secondProperty', 'thirdProperty'],
|
||||||
|
};
|
||||||
|
|
||||||
|
should.deepEqual(child.settings.hidden, expectedSettings.hidden);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('`{patch: true}` adds distinct inner properties of matching objects ' +
|
||||||
|
'from base model and sub model', function() {
|
||||||
|
// merge policy of settings.relations is {patch: true}
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const base = modelBuilder.define('base', {}, {
|
||||||
|
relations: {
|
||||||
|
someOtherRelation: {
|
||||||
|
type: 'hasMany',
|
||||||
|
model: 'someOtherModel',
|
||||||
|
foreignKey: 'otherModelId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const child = base.extend('child', {}, {
|
||||||
|
relations: {
|
||||||
|
someRelation: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'someModel',
|
||||||
|
foreignKey: 'modelId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configureModelMerge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedSettings = {
|
||||||
|
relations: {
|
||||||
|
someRelation: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'someModel',
|
||||||
|
foreignKey: 'modelId',
|
||||||
|
},
|
||||||
|
someOtherRelation: {
|
||||||
|
type: 'hasMany',
|
||||||
|
model: 'someOtherModel',
|
||||||
|
foreignKey: 'otherModelId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
should.deepEqual(child.settings.relations, expectedSettings.relations);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('`{patch: true}` replaces baseClass inner properties with matching ' +
|
||||||
|
'subClass inner properties', function() {
|
||||||
|
// merge policy of settings.relations is {patch: true}
|
||||||
|
const modelBuilder = memory.modelBuilder;
|
||||||
|
const base = modelBuilder.define('base', {}, {
|
||||||
|
relations: {
|
||||||
|
user: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
model: 'User',
|
||||||
|
foreignKey: 'userId',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const child = base.extend('child', {}, {
|
||||||
|
relations: {
|
||||||
|
user: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
idName: 'id',
|
||||||
|
polymorphic: {
|
||||||
|
idType: 'string',
|
||||||
|
foreignKey: 'userId',
|
||||||
|
discriminator: 'principalType',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
configureModelMerge: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expectedSettings = {
|
||||||
|
relations: {
|
||||||
|
user: {
|
||||||
|
type: 'belongsTo',
|
||||||
|
idName: 'id',
|
||||||
|
polymorphic: {
|
||||||
|
idType: 'string',
|
||||||
|
foreignKey: 'userId',
|
||||||
|
discriminator: 'principalType',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
should.deepEqual(child.settings.relations, expectedSettings.relations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -8,7 +8,8 @@ var should = require('./init.js');
|
||||||
var utils = require('../lib/utils');
|
var utils = require('../lib/utils');
|
||||||
var fieldsToArray = utils.fieldsToArray;
|
var fieldsToArray = utils.fieldsToArray;
|
||||||
var removeUndefined = utils.removeUndefined;
|
var removeUndefined = utils.removeUndefined;
|
||||||
var mergeSettings = utils.mergeSettings;
|
var deepMerge = utils.deepMerge;
|
||||||
|
var rankArrayElements = utils.rankArrayElements;
|
||||||
var mergeIncludes = utils.mergeIncludes;
|
var mergeIncludes = utils.mergeIncludes;
|
||||||
var sortObjectsByIds = utils.sortObjectsByIds;
|
var sortObjectsByIds = utils.sortObjectsByIds;
|
||||||
var uniq = utils.uniq;
|
var uniq = utils.uniq;
|
||||||
|
@ -138,9 +139,9 @@ describe('util.parseSettings', function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('mergeSettings', function() {
|
describe('util.deepMerge', function() {
|
||||||
it('should merge settings correctly', function() {
|
it('should deep merge objects', function() {
|
||||||
var src = {base: 'User',
|
var extras = {base: 'User',
|
||||||
relations: {accessTokens: {model: 'accessToken', type: 'hasMany',
|
relations: {accessTokens: {model: 'accessToken', type: 'hasMany',
|
||||||
foreignKey: 'userId'},
|
foreignKey: 'userId'},
|
||||||
account: {model: 'account', type: 'belongsTo'}},
|
account: {model: 'account', type: 'belongsTo'}},
|
||||||
|
@ -159,7 +160,7 @@ describe('mergeSettings', function() {
|
||||||
principalType: 'ROLE',
|
principalType: 'ROLE',
|
||||||
principalId: '$owner'},
|
principalId: '$owner'},
|
||||||
]};
|
]};
|
||||||
var tgt = {strict: false,
|
var base = {strict: false,
|
||||||
acls: [
|
acls: [
|
||||||
{principalType: 'ROLE',
|
{principalType: 'ROLE',
|
||||||
principalId: '$everyone',
|
principalId: '$everyone',
|
||||||
|
@ -173,7 +174,7 @@ describe('mergeSettings', function() {
|
||||||
maxTTL: 31556926,
|
maxTTL: 31556926,
|
||||||
ttl: 1209600};
|
ttl: 1209600};
|
||||||
|
|
||||||
var dst = mergeSettings(tgt, src);
|
var merged = deepMerge(base, extras);
|
||||||
|
|
||||||
var expected = {strict: false,
|
var expected = {strict: false,
|
||||||
acls: [
|
acls: [
|
||||||
|
@ -206,11 +207,41 @@ describe('mergeSettings', function() {
|
||||||
foreignKey: 'userId'},
|
foreignKey: 'userId'},
|
||||||
account: {model: 'account', type: 'belongsTo'}}};
|
account: {model: 'account', type: 'belongsTo'}}};
|
||||||
|
|
||||||
should.deepEqual(dst.acls, expected.acls, 'Merged settings should match the expectation');
|
should.deepEqual(merged, expected, 'Merged objects should match the expectation');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sortObjectsByIds', function() {
|
describe('util.rankArrayElements', function() {
|
||||||
|
it('should add property \'__rank\' to array elements of type object {}', function() {
|
||||||
|
var acls = [
|
||||||
|
{accessType: '*',
|
||||||
|
permission: 'DENY',
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone'},
|
||||||
|
];
|
||||||
|
|
||||||
|
var rankedAcls = rankArrayElements(acls, 2);
|
||||||
|
|
||||||
|
should.equal(rankedAcls[0].__rank, 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not replace existing \'__rank\' property of array elements', function() {
|
||||||
|
var acls = [
|
||||||
|
{accessType: '*',
|
||||||
|
permission: 'DENY',
|
||||||
|
principalType: 'ROLE',
|
||||||
|
principalId: '$everyone',
|
||||||
|
__rank: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
var rankedAcls = rankArrayElements(acls, 2);
|
||||||
|
|
||||||
|
should.equal(rankedAcls[0].__rank, 1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('util.sortObjectsByIds', function() {
|
||||||
var items = [
|
var items = [
|
||||||
{id: 1, name: 'a'},
|
{id: 1, name: 'a'},
|
||||||
{id: 2, name: 'b'},
|
{id: 2, name: 'b'},
|
||||||
|
|
Loading…
Reference in New Issue