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 List = require('./list.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');
|
||||
|
||||
// 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
|
||||
// See https://github.com/strongloop/loopback-datasource-juggler/issues/293
|
||||
if ((parent && !settings.base) || (!parent && settings.base)) {
|
||||
|
@ -197,6 +208,9 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
hiddenProperty(ModelClass, 'modelName', className);
|
||||
}
|
||||
|
||||
// Iterate sub model inheritance rank over base model rank
|
||||
ModelClass.__rank = ModelBaseClass.__rank + 1;
|
||||
|
||||
util.inherits(ModelClass, ModelBaseClass);
|
||||
|
||||
// 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.
|
||||
* @options {Object} properties Properties to define for the model, added to properties of model being extended.
|
||||
* @options {Object} settings Model settings, such as relations and acls.
|
||||
*
|
||||
* @options {Object} subClassProperties child model properties, added to base model
|
||||
* properties.
|
||||
* @options {Object} subClassSettings child model settings such as relations and acls,
|
||||
* merged with base model settings.
|
||||
*/
|
||||
ModelClass.extend = function(className, subclassProperties, subclassSettings) {
|
||||
var properties = ModelClass.definition.properties;
|
||||
var settings = ModelClass.definition.settings;
|
||||
ModelClass.extend = function(className, subClassProperties, subClassSettings) {
|
||||
var baseClassProperties = ModelClass.definition.properties;
|
||||
var baseClassSettings = ModelClass.definition.settings;
|
||||
|
||||
subclassProperties = subclassProperties || {};
|
||||
subclassSettings = subclassSettings || {};
|
||||
subClassProperties = subClassProperties || {};
|
||||
subClassSettings = subClassSettings || {};
|
||||
|
||||
// Check if subclass redefines the ids
|
||||
var idFound = false;
|
||||
for (var k in subclassProperties) {
|
||||
if (subclassProperties[k] && subclassProperties[k].id) {
|
||||
for (var k in subClassProperties) {
|
||||
if (subClassProperties[k] && subClassProperties[k].id) {
|
||||
idFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Merging the properties
|
||||
var keys = Object.keys(properties);
|
||||
var keys = Object.keys(baseClassProperties);
|
||||
for (var i = 0, n = keys.length; i < n; i++) {
|
||||
var key = keys[i];
|
||||
|
||||
if (idFound && properties[key].id) {
|
||||
if (idFound && baseClassProperties[key].id) {
|
||||
// don't inherit id properties
|
||||
continue;
|
||||
}
|
||||
if (subclassProperties[key] === undefined) {
|
||||
var baseProp = properties[key];
|
||||
if (subClassProperties[key] === undefined) {
|
||||
var baseProp = baseClassProperties[key];
|
||||
var basePropCopy = baseProp;
|
||||
if (baseProp && typeof baseProp === 'object') {
|
||||
// Deep clone the base prop
|
||||
basePropCopy = mergeSettings(null, baseProp);
|
||||
// Deep clone the base properties
|
||||
basePropCopy = deepMerge(baseProp);
|
||||
}
|
||||
subclassProperties[key] = basePropCopy;
|
||||
subClassProperties[key] = basePropCopy;
|
||||
}
|
||||
}
|
||||
|
||||
// Merge the settings
|
||||
var originalSubclassSettings = subclassSettings;
|
||||
subclassSettings = mergeSettings(settings, subclassSettings);
|
||||
// Merging the settings
|
||||
var originalSubclassSettings = subClassSettings;
|
||||
let mergePolicy = ModelClass.getMergePolicy(subClassSettings);
|
||||
subClassSettings = mergeSettings(baseClassSettings, subClassSettings, mergePolicy);
|
||||
|
||||
// Ensure 'base' is not inherited. Note we don't have to delete 'super'
|
||||
// as that is removed from settings by modelBuilder.define and thus
|
||||
// it is never inherited
|
||||
if (!originalSubclassSettings.base) {
|
||||
subclassSettings.base = ModelClass;
|
||||
subClassSettings.base = ModelClass;
|
||||
}
|
||||
|
||||
// Define the subclass
|
||||
var subClass = modelBuilder.define(className, subclassProperties, subclassSettings, ModelClass);
|
||||
var subClass = modelBuilder.define(className, subClassProperties, subClassSettings, ModelClass);
|
||||
|
||||
// Calling the setup function
|
||||
if (typeof subClass.setup === 'function') {
|
||||
|
@ -410,6 +426,92 @@ ModelBuilder.prototype.define = function defineClass(className, properties, sett
|
|||
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
|
||||
* @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;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* `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
|
||||
jutil.mixin(ModelBaseClass, require('./observer'));
|
||||
|
||||
|
|
123
lib/utils.js
123
lib/utils.js
|
@ -9,7 +9,8 @@ exports.fieldsToArray = fieldsToArray;
|
|||
exports.selectFields = selectFields;
|
||||
exports.removeUndefined = removeUndefined;
|
||||
exports.parseSettings = parseSettings;
|
||||
exports.mergeSettings = exports.deepMerge = mergeSettings;
|
||||
exports.mergeSettings = exports.deepMerge = deepMerge;
|
||||
exports.deepMergeProperty = deepMergeProperty;
|
||||
exports.isPlainObject = isPlainObject;
|
||||
exports.defineCachedRelations = defineCachedRelations;
|
||||
exports.sortObjectsByIds = sortObjectsByIds;
|
||||
|
@ -24,6 +25,7 @@ exports.idEquals = idEquals;
|
|||
exports.findIndexOf = findIndexOf;
|
||||
exports.collectTargetIds = collectTargetIds;
|
||||
exports.idName = idName;
|
||||
exports.rankArrayElements = rankArrayElements;
|
||||
|
||||
var g = require('strong-globalize')();
|
||||
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
|
||||
* @param {Object} src The source settings object
|
||||
* @returns {Object} The merged settings object
|
||||
* NOTE: The function operates as a deep clone when called with a single object
|
||||
* argument.
|
||||
*
|
||||
* @param {Object} base The base object
|
||||
* @param {Object} extras The object to merge with base
|
||||
* @returns {Object} The merged object
|
||||
*/
|
||||
function mergeSettings(target, src) {
|
||||
var array = Array.isArray(src);
|
||||
function deepMerge(base, extras) {
|
||||
// deepMerge allows undefined extras to allow deep cloning of arrays
|
||||
var array = Array.isArray(base) && (Array.isArray(extras) || !extras);
|
||||
var dst = array && [] || {};
|
||||
|
||||
if (array) {
|
||||
target = target || [];
|
||||
// Add items from target into dst
|
||||
dst = dst.concat(target);
|
||||
// Add non-existent items from source into dst
|
||||
src.forEach(function(e) {
|
||||
// extras or base is an array
|
||||
extras = extras || [];
|
||||
// Add items from base into dst
|
||||
dst = dst.concat(base);
|
||||
// Add non-existent items from extras into dst
|
||||
extras.forEach(function(e) {
|
||||
if (dst.indexOf(e) === -1) {
|
||||
dst.push(e);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (target != null && typeof target === 'object') {
|
||||
// Add properties from target to dst
|
||||
Object.keys(target).forEach(function(key) {
|
||||
dst[key] = target[key];
|
||||
if (base != null && typeof base === 'object') {
|
||||
// Add properties from base to dst
|
||||
Object.keys(base).forEach(function(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') {
|
||||
// Source is an object
|
||||
Object.keys(src).forEach(function(key) {
|
||||
var srcValue = src[key];
|
||||
if (srcValue == null || typeof srcValue !== 'object') {
|
||||
// The source item value is null, undefined or not an object
|
||||
dst[key] = srcValue;
|
||||
if (extras != null && typeof extras === 'object') {
|
||||
// extras is an object {}
|
||||
Object.keys(extras).forEach(function(key) {
|
||||
var extra = extras[key];
|
||||
if (extra == null || typeof extra !== 'object') {
|
||||
// extra item value is null, undefined or not an object
|
||||
dst[key] = extra;
|
||||
} else {
|
||||
// The source item value is an object
|
||||
if (target == null || typeof target !== 'object' ||
|
||||
target[key] == null) {
|
||||
// If target is not an object or target item value
|
||||
dst[key] = srcValue;
|
||||
// The extra item value is an object
|
||||
if (base == null || typeof base !== 'object' ||
|
||||
base[key] == null) {
|
||||
// base is not an object or base item value is undefined or null
|
||||
dst[key] = extra;
|
||||
} 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @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);
|
||||
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() {
|
||||
|
|
|
@ -257,43 +257,6 @@ describe('ModelDefinition class', function() {
|
|||
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() {
|
||||
var modelBuilder = memory.modelBuilder;
|
||||
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 fieldsToArray = utils.fieldsToArray;
|
||||
var removeUndefined = utils.removeUndefined;
|
||||
var mergeSettings = utils.mergeSettings;
|
||||
var deepMerge = utils.deepMerge;
|
||||
var rankArrayElements = utils.rankArrayElements;
|
||||
var mergeIncludes = utils.mergeIncludes;
|
||||
var sortObjectsByIds = utils.sortObjectsByIds;
|
||||
var uniq = utils.uniq;
|
||||
|
@ -138,9 +139,9 @@ describe('util.parseSettings', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('mergeSettings', function() {
|
||||
it('should merge settings correctly', function() {
|
||||
var src = {base: 'User',
|
||||
describe('util.deepMerge', function() {
|
||||
it('should deep merge objects', function() {
|
||||
var extras = {base: 'User',
|
||||
relations: {accessTokens: {model: 'accessToken', type: 'hasMany',
|
||||
foreignKey: 'userId'},
|
||||
account: {model: 'account', type: 'belongsTo'}},
|
||||
|
@ -159,7 +160,7 @@ describe('mergeSettings', function() {
|
|||
principalType: 'ROLE',
|
||||
principalId: '$owner'},
|
||||
]};
|
||||
var tgt = {strict: false,
|
||||
var base = {strict: false,
|
||||
acls: [
|
||||
{principalType: 'ROLE',
|
||||
principalId: '$everyone',
|
||||
|
@ -173,7 +174,7 @@ describe('mergeSettings', function() {
|
|||
maxTTL: 31556926,
|
||||
ttl: 1209600};
|
||||
|
||||
var dst = mergeSettings(tgt, src);
|
||||
var merged = deepMerge(base, extras);
|
||||
|
||||
var expected = {strict: false,
|
||||
acls: [
|
||||
|
@ -206,11 +207,41 @@ describe('mergeSettings', function() {
|
|||
foreignKey: 'userId'},
|
||||
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 = [
|
||||
{id: 1, name: 'a'},
|
||||
{id: 2, name: 'b'},
|
||||
|
|
Loading…
Reference in New Issue