From 9a7f13ff0f501879d69408dc6c55dfc5bbc45f0f Mon Sep 17 00:00:00 2001 From: ningsuhen Date: Sat, 25 Apr 2015 01:53:13 +0530 Subject: [PATCH] Add support for merging include filters --- lib/scope.js | 2 +- lib/utils.js | 98 ++++++++++++++++++++++++++++++- test/util.test.js | 144 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 240 insertions(+), 4 deletions(-) diff --git a/lib/scope.js b/lib/scope.js index c2dbd93a..c0b2a56d 100644 --- a/lib/scope.js +++ b/lib/scope.js @@ -60,7 +60,7 @@ ScopeDefinition.prototype.related = function(receiver, scopeParams, condOrRefres if (!self.__cachedRelations || self.__cachedRelations[name] === undefined || actualRefresh) { // It either doesn't hit the cache or refresh is required - var params = mergeQuery(actualCond, scopeParams); + var params = mergeQuery(actualCond, scopeParams, {nestedInclude: true}); var targetModel = this.targetModel(receiver); targetModel.find(params, function (err, data) { if (!err && saveOnCache) { diff --git a/lib/utils.js b/lib/utils.js index 6400e1ad..31acfb4b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -9,6 +9,7 @@ exports.defineCachedRelations = defineCachedRelations; exports.sortObjectsByIds = sortObjectsByIds; exports.setScopeValuesFromWhere = setScopeValuesFromWhere; exports.mergeQuery = mergeQuery; +exports.mergeIncludes = mergeIncludes; exports.createPromiseCallback = createPromiseCallback var traverse = require('traverse'); @@ -53,6 +54,87 @@ function setScopeValuesFromWhere(data, where, targetModel) { } } +/** + * Merge include options of default scope with runtime include option. + * exhibits the _.extend behaviour. Property value of source overrides + * property value of destination if property name collision occurs + * @param {String|Array|Object} destination The default value of `include` option + * @param {String|Array|Object} source The runtime value of `include` option + * @returns {Object} + */ +function mergeIncludes(destination, source) { + var destArray = convertToArray(destination); + var sourceArray = convertToArray(source); + if (destArray.length === 0) { + return sourceArray; + } + if (sourceArray.length === 0) { + return destArray; + } + var relationNames = []; + var resultArray = []; + for (var j in sourceArray) { + var sourceEntry = sourceArray[j]; + var sourceEntryRelationName = (typeof (sourceEntry.rel || sourceEntry.relation) === 'string') ? + sourceEntry.relation : Object.keys(sourceEntry)[0]; + relationNames.push(sourceEntryRelationName); + resultArray.push(sourceEntry); + } + for (var i in destArray) { + var destEntry = destArray[i]; + var destEntryRelationName = (typeof (destEntry.rel || destEntry.relation) === 'string') ? + destEntry.relation : Object.keys(destEntry)[0]; + if (relationNames.indexOf(destEntryRelationName) === -1) { + resultArray.push(destEntry); + } + } + return resultArray; +} + +/** + * Converts input parameter into array of objects which wraps the value. + * "someValue" is converted to [{"someValue":true}] + * ["someValue"] is converted to [{"someValue":true}] + * {"someValue":true} is converted to [{"someValue":true}] + * @param {String|Array|Object} param - Input parameter to be converted + * @returns {Array} + */ +function convertToArray(include) { + if (typeof include === 'string') { + var obj = {}; + obj[include] = true; + return [obj]; + } else if (isPlainObject(include)) { + //if include is of the form - {relation:'',scope:''} + if (include.rel || include.relation) { + return [include]; + } + // Build an array of key/value pairs + var newInclude = []; + for (var key in include) { + var obj = {}; + obj[key] = include[key]; + newInclude.push(obj); + } + return newInclude; + } else if (Array.isArray(include)) { + var normalized = []; + for (var i in include) { + var includeEntry = include[i]; + if (typeof includeEntry === 'string') { + var obj = {}; + obj[includeEntry] = true; + normalized.push(obj) + } + else{ + normalized.push(includeEntry); + } + } + return normalized; + } + return []; +} + /*! * Merge query parameters * @param {Object} base The base object to contain the merged results @@ -81,9 +163,19 @@ function mergeQuery(base, update, spec) { if (!base.include) { base.include = update.include; } else { - var saved = base.include; - base.include = {}; - base.include[update.include] = saved; + if (spec.nestedInclude === true){ + //specify nestedInclude=true to force nesting of inclusions on scoped + //queries. e.g. In physician.patients.getAsync({include: 'address'}), + //inclusion should be on patient model, not on physician model. + var saved = base.include; + base.include = {}; + base.include[update.include] = saved; + } + else{ + //default behaviour of inclusion merge - merge inclusions at the same + //level. - https://github.com/strongloop/loopback-datasource-juggler/pull/569#issuecomment-95310874 + base.include = mergeIncludes(base.include, update.include); + } } } diff --git a/test/util.test.js b/test/util.test.js index f933190a..24b9451d 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -3,6 +3,7 @@ var utils = require('../lib/utils'); var fieldsToArray = utils.fieldsToArray; var removeUndefined = utils.removeUndefined; var mergeSettings = utils.mergeSettings; +var mergeIncludes = utils.mergeIncludes; var sortObjectsByIds = utils.sortObjectsByIds; describe('util.fieldsToArray', function () { @@ -218,3 +219,146 @@ describe('sortObjectsByIds', function () { }); }); + +describe('util.mergeIncludes', function () { + + function checkInputOutput(baseInclude, updateInclude, expectedInclude) { + var mergedInclude = mergeIncludes(baseInclude, updateInclude); + should.deepEqual(mergedInclude, expectedInclude, + 'Merged include should match the expectation'); + } + + it('Merge string values to object', function () { + var baseInclude = 'relation1'; + var updateInclude = 'relation2'; + var expectedInclude = [ + {relation2: true}, + {relation1: true} + ]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + }); + + it('Merge string & array values to object', function () { + var baseInclude = 'relation1'; + var updateInclude = ['relation2']; + var expectedInclude = [ + {relation2: true}, + {relation1: true} + ]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + }); + + it('Merge string & object values to object', function () { + var baseInclude = ['relation1']; + var updateInclude = {relation2: 'relation2Include'}; + var expectedInclude = [ + {relation2: 'relation2Include'}, + {relation1: true} + ]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + }); + + it('Merge array & array values to object', function () { + var baseInclude = ['relation1']; + var updateInclude = ['relation2']; + var expectedInclude = [ + {relation2: true}, + {relation1: true} + ]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + }); + + it('Merge array & object values to object', function () { + var baseInclude = ['relation1']; + var updateInclude = {relation2: 'relation2Include'}; + var expectedInclude = [ + {relation2: 'relation2Include'}, + {relation1: true} + ]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + }); + + it('Merge object & object values to object', function () { + var baseInclude = {relation1: 'relation1Include'}; + var updateInclude = {relation2: 'relation2Include'}; + var expectedInclude = [ + {relation2: 'relation2Include'}, + {relation1: 'relation1Include'} + ]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + }); + + it('Override property collision with update value', function () { + var baseInclude = {relation1: 'baseValue'}; + var updateInclude = {relation1: 'updateValue'}; + var expectedInclude = [ + {relation1: 'updateValue'} + ]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + }); + + it('Merge string includes & include with relation syntax properly', + function () { + var baseInclude = 'relation1'; + var updateInclude = {relation: 'relation1'}; + var expectedInclude = [ + {relation: 'relation1'} + ]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + }); + + it('Merge string includes & include with scope properly', function () { + var baseInclude = 'relation1'; + var updateInclude = { + relation: 'relation1', + scope: {include: 'relation2'} + }; + var expectedInclude = [ + {relation: 'relation1', scope: {include: 'relation2'}} + ]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + }); + + it('Merge includes with and without relation syntax properly', + function () { + //w & w/o relation syntax - no collision + var baseInclude = ['relation2']; + var updateInclude = { + relation: 'relation1', + scope: {include: 'relation2'} + }; + var expectedInclude = [{ + relation: 'relation1', + scope: {include: 'relation2'} + }, {relation2: true}]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + + //w & w/o relation syntax - collision + baseInclude = ['relation1']; + updateInclude = {relation: 'relation1', scope: {include: 'relation2'}}; + expectedInclude = + [{relation: 'relation1', scope: {include: 'relation2'}}]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + + //w & w/o relation syntax - collision + baseInclude = {relation: 'relation1', scope: {include: 'relation2'}}; + updateInclude = ['relation1']; + expectedInclude = [{relation1: true}]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + }); + + it('Merge includes with mixture of strings, arrays & objects properly', function () { + var baseInclude = ['relation1', {relation2: true}, + {relation: 'relation3', scope: {where: {id: 'some id'}}}, + {relation: 'relation5', scope: {where: {id: 'some id'}}} + ]; + var updateInclude = ['relation4', {relation3: true}, + {relation: 'relation2', scope: {where: {id: 'some id'}}}]; + var expectedInclude = [{relation4: true}, {relation3: true}, + {relation: 'relation2', scope: {where: {id: 'some id'}}}, + {relation1: true}, + {relation: 'relation5', scope: {where: {id: 'some id'}}}]; + checkInputOutput(baseInclude, updateInclude, expectedInclude); + }); + +});