From 7826a8c1233330bbf12919b6bc393814d9fe96f4 Mon Sep 17 00:00:00 2001 From: vamsee Date: Mon, 2 Mar 2020 15:39:28 +0530 Subject: [PATCH 01/80] remove oracledb tar for oracle ci --- .gitlab-ci.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 540f2bd..48af208 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -111,7 +111,7 @@ oracletest: # - time npm install git+http://evgit/oecloud.io/oe-connector-oracle.git#master --no-optional - export custom_function_path="${CI_PROJECT_DIR}/test/customFunction" - time npm install --no-optional - - mv /oracledb node_modules/ + # - mv /oracledb node_modules/ - node test/oracle-utility.js # - export ORACLE_USERNAME=${group}"_"${project} # - export ORACLE_USERNAME=$(echo $ORACLE_USERNAME | tr '[:lower:]' '[:upper:]') diff --git a/package.json b/package.json index ef2d4fd..b181134 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oe-service-personalization", - "version": "2.1.0", + "version": "2.2.0", "description": "oe-cloud modularization project", "engines": { "node": ">=6" From 572239507e0e0aa2bc54a2d9173f000ac753447d Mon Sep 17 00:00:00 2001 From: deostroll Date: Fri, 10 Apr 2020 19:22:00 +0530 Subject: [PATCH 02/80] support for the recent change in oe-cloud mixin behavior mixins are no more auto-loaded via oe-cloud; a special property needs to be added in app-list.json added the same --- test/app-list.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/app-list.json b/test/app-list.json index 973d628..ad5f6d0 100644 --- a/test/app-list.json +++ b/test/app-list.json @@ -5,7 +5,8 @@ }, { "path": "oe-personalization", - "enabled": true + "enabled": true, + "autoEnableMixins" : true }, { "path": "./", From ca60510e2f9364ecf5819e58ee5ff268519d0d4f Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 13 Apr 2020 12:29:29 +0530 Subject: [PATCH 03/80] modified the operations sorter function --- lib/service-personalizer.js | 4 ++-- server/boot/service-personalization.js | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index bf55e57..91528b4 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -30,7 +30,7 @@ var customFunction; * @returns {function} - comparator function to be used for sorting */ var sortFactoryFn = (reverse = false) => - (first, second) => first.type === 'fieldReplace' ? (reverse ? -1 : 1) : 0; + (first, second) => first.type === 'fieldReplace' ? (reverse ? -1 : 1) : (reverse ? 1 : -1); /** * @@ -214,7 +214,7 @@ var applyReversePersonalizationRule = function applyReversePersonalizationRuleFn } } } - return arr.sort(sortFactoryFn(true)).map(x => x.fn); + return arr.sort(sortFactoryFn(true)); }; /* eslint-enable no-loop-func */ diff --git a/server/boot/service-personalization.js b/server/boot/service-personalization.js index ffd47c2..2442250 100644 --- a/server/boot/service-personalization.js +++ b/server/boot/service-personalization.js @@ -389,6 +389,7 @@ function beforeRemotePersonalizationExec(model, ctx, next) { log.debug(ctx.req.callContext, 'beforeRemotePersonalizationExec personalization rule found , rule: ', rule); log.debug(ctx.req.callContext, 'applying PersonalizationRule now'); var fns = servicePersonalizer.applyReversePersonalizationRule(ctx, rule.personalizationRule); + fns = fns.map(x => x.fn) servicePersonalizer.execute(fns, function (err) { if (err) { return next(err); From 700204469be0c4d2b9c812e3cd7638ca86529c54 Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 13 Apr 2020 12:34:18 +0530 Subject: [PATCH 04/80] lint fixes --- server/boot/service-personalization.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/boot/service-personalization.js b/server/boot/service-personalization.js index 2442250..dbc8438 100644 --- a/server/boot/service-personalization.js +++ b/server/boot/service-personalization.js @@ -389,7 +389,7 @@ function beforeRemotePersonalizationExec(model, ctx, next) { log.debug(ctx.req.callContext, 'beforeRemotePersonalizationExec personalization rule found , rule: ', rule); log.debug(ctx.req.callContext, 'applying PersonalizationRule now'); var fns = servicePersonalizer.applyReversePersonalizationRule(ctx, rule.personalizationRule); - fns = fns.map(x => x.fn) + fns = fns.map(x => x.fn); servicePersonalizer.execute(fns, function (err) { if (err) { return next(err); From be7c0cf46b6d36cdc9931a2afeacb7d8857f952e Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 13 Apr 2020 12:43:56 +0530 Subject: [PATCH 05/80] v2.2.1 - fixed bug in sorting operations - fieldReplace/fieldValueReplace which were breaking postgres tests - incorporated oe-cloud update which required the tests to enable oe-personalization mixins --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b181134..0760aba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oe-service-personalization", - "version": "2.2.0", + "version": "2.2.1", "description": "oe-cloud modularization project", "engines": { "node": ">=6" From 5e05b28711dc663d6c32d4cbfe5f97fe5b690baf Mon Sep 17 00:00:00 2001 From: deostroll Date: Thu, 23 Apr 2020 13:56:00 +0530 Subject: [PATCH 06/80] new changes --- .../mixins/service-personalization-mixin.js | 97 +++++++++++ lib/service-personalizer.js | 158 +++++++++++++++++- package.json | 1 + server/boot/service-personalization.js | 3 + test/common/models/my-model.js | 81 +++++++++ test/common/models/my-model.json | 32 ++++ test/component-config.json | 3 + test/model-config.json | 4 + 8 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 common/mixins/service-personalization-mixin.js create mode 100644 test/common/models/my-model.js create mode 100644 test/common/models/my-model.json diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js new file mode 100644 index 0000000..6b4d5be --- /dev/null +++ b/common/mixins/service-personalization-mixin.js @@ -0,0 +1,97 @@ +/** + * + * ©2016-2020 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), + * Bangalore, India. All Rights Reserved. + * + */ + +/** + * This mixin will attach beforeRemote and afterRemote + * hooks and decide if the data needs to be service + * personalized. + * + * Therefore, it is necessary to enable the mixin + * configuration on the corresponding model definition, + * even if it does not directly participate in the + * service personalization (viz is the case with any + * form of relations - or related models). + * + * This will only personalize data for the remote endpoints. + */ + + +const logger = require('oe-logger'); +const log = logger('service-personalization-mixin'); +const { applyServicePersonalization } = require('./../../lib/service-personalizer'); + +const ALLOWED_METHOD = ['create', 'find', 'fineOne']; + +const parseMethodString = str => { + return str.split('.').reduce((obj, comp, idx, arr) => { + let ret = {}; + let length = arr.length; + if (idx === 0) { + ret.modelName = comp; + } + else if (length === 3 && idx !== length - 1) { + ret.isStatic = false; + } + else if (length === 3 && idx == length - 1) { + ret.methodName = comp; + } + else { + ret.isStatic = true; + ret.methodName = comp; + } + return Object.assign({}, obj, ret); + }, {}); +} +const slice = [].slice; +const nextTick = function () { + let args = slice.call(arguments); + let cb = args.shift(); + return process.nextTick(() => { + cb.apply(null, args); + }); +} +module.exports = function ServicePersonalizationMixin(TargetModel) { + TargetModel.beforeRemote('**', function () { + + let args = slice.call(arguments); + let ctx = args[0]; + let next = args.slice(-1); + let callCtx = ctx.req.callContext; + log.debug(callCtx, `MethodString: ${ctx.methodString}`); + + ctxInfo = parseMethodString(ctx.methodString); + if (ALLOWED_METHOD.includes(ctxInfo.methodName)) { + let data = null; + if (ctxInfo.isStatic) { + switch (ctxInfo.methodName) { + case 'create': + data = ctx.instance + break; + default: + log.debug(callCtx, `Unhandled: ${ctx.methodString}`); + data = {} + } + + let personalizationOptions = { + reverse: true, + context: callCtx + }; + + return applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function(err, personalizedData) { + if(err) { + next(err); + } + else { + next(); + } + }); + } + } + + nextTick(next); + }); +} \ No newline at end of file diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 91528b4..d6e3791 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -848,11 +848,167 @@ function maskCharacters(ctx, charMaskRules, cb) { return cb(); } +const p13nFunctions = { + fieldReplace(replacements) { + let replace = (data, oldField, newField) => { + let fieldValue = data[oldField]; + data[newField] = fieldValue; + delete data[oldField]; + } + return function(data, callback) { + Object.entries(replacements) + .forEach(([oldField, newField]) => { + if(Array.isArray(data)) { + data.forEach(dt => { + replace(dt, oldField, newField); + }) + } + else { + replace(data, oldField, newField); + } + }); + + process.nextTick(callback); + } + }, + + noop : function(data, cb) { + !(function(){})(data) + process.nextTick(cb); + } +} +/** + * Apply the personalization on the given data + * + * @param {bool} isReverse - flag indicating if reverse personalization is to be applied + * @param {object} instructions - object containing personalization instructions + * @param {List or Instance} data - data from the context. Could either be a List of model instances or a single model instance + * @param {function} done - the callback which receives the new data. First argument of function is an error object + */ +function personalize(isReverse, instructions, data, done) { + let tasks = Object.entries(instructions).map(([operation, instruction]) => { + switch(operation) { + case 'fieldReplace': + return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction) } + default: + return { type: operation, fn: p13nFunctions.noop } + } + }).sort(sortFactoryFn(isReverse)); + + let asyncIterator = function( { fn }, done) { + fn(data, function(err){ + done(err); + }); + }; + + async.each(tasks, asyncIterator, function asyncEachCb(err){ + if(err) { + done(err) + } + else { + done(); + } + }); +} + + +function checkRelationAndRecurse(Model, data, personalizationOptions, done) { + if(Model.definition.relations) { + relationEntries = Object.entries(Model.definitions.relations); + async.each(relationEntries, function asyncExecutor([relationName, relationDef], asyncExecutorDoneCb){ + let relatedModelName = relationDef.model; + //check if data has the relation + relData = data[relationName]; + if(typeof relData !== 'undefined') { + // apply personalization and exit function + return applyServicePersonalization(relatedModelName, relData, personalizationOptions, function(err, persData) { + if(err) { + asyncExecutorDoneCb(err); + } + else { + data[relationName] = persData; + asyncExecutorDoneCb(); + } + }); + } + // no related data - do nothing + process.nextTick(function() { + asyncExecutorDoneCb(); + }); + }, function asyncEachDone(err){ + + }); + } + else { + // ! no relations defined on model - exit function + process.nextTick(function() { + done(); + }); + } +} + +function applyServicePersonalization(modelName, data, personalizationOptions, done) { + let { reverse, context } = personalizationOptions; + let findQuery = { where: { modelName, enabled: true }}; + let Model = loopback.findModel(modelName); + + PersonalizationRule.find(findQuery, context, function(err, entries) { + if(err) { + done(err); + } + else { + // let { instructions } = entries[0]; + // personalize(reverse, instructions, data, done); + if(entries.length == 0) { + //! not needed to personalize here, + //! however we need to check for related + //! model + checkRelationAndRecurse(Model, data, personalizationOptions, function(err) { + if(err) { + done(err) + } + else { + done(); + } + }); + } + else { + personalize(reverse, entries[0].instructions, data, function(err) { + if(err) { + done(err); + } + else { + checkRelationAndRecurse(Model, data, personalizationOptions, function(err) { + if(err) { + done(err); + } + else { + done(); + } + }); + } + }); + } + } + }); //PersonalizationRule.find() - end +} + +let PersonalizationRule = null; +/** + * Initializes this module for service personalization + * + * @param {Application} app - The Loopback application object + */ +function init(app) { + PersonalizationRule = app.models['PersonalizationRule']; +} + module.exports = { getPersonalizationRuleForModel: getPersonalizationRuleForModel, applyPersonalizationRule: applyPersonalizationRule, applyReversePersonalizationRule: applyReversePersonalizationRule, execute: execute, loadCustomFunction, - getCustomFunction + getCustomFunction, + applyServicePersonalization }; diff --git a/package.json b/package.json index b181134..1bbcc93 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "grunt-contrib-clean": "2.0.0", "grunt-mocha-istanbul": "5.0.2", "istanbul": "0.4.5", + "loopback-component-explorer": "^6.5.1", "md5": "^2.2.1", "mocha": "5.2.0", "oe-connector-mongodb": "git+http://evgit/oecloud.io/oe-connector-mongodb.git#master", diff --git a/server/boot/service-personalization.js b/server/boot/service-personalization.js index dbc8438..345f3d8 100644 --- a/server/boot/service-personalization.js +++ b/server/boot/service-personalization.js @@ -156,6 +156,9 @@ function attachRemoteHooksToModel(modelName, options) { beforeRemoteFindHook(model); beforeRemoteFindOneHook(model); beforeRemoteFindByIdHook(model); + // model.beforeRemote('**', function(ctx, next) { + + // }); } } diff --git a/test/common/models/my-model.js b/test/common/models/my-model.js new file mode 100644 index 0000000..6eee421 --- /dev/null +++ b/test/common/models/my-model.js @@ -0,0 +1,81 @@ +module.exports = function myModelBoot(MyModel) { + // MyModel.beforeRemote('**', function(ctx, unused, next) { + // console.log(`BeforeRemote MethodString: ${ctx.methodString}`); + // process.nextTick(function(){ + // next(); + // }); + // }); + + // MyModel.afterRemote('**', function(ctx, unused, next) { + // console.log(`AfterRemote MethodString: ${ctx.methodString}`); + // process.nextTick(function(){ + // next(); + // }); + // }); + + MyModel.remoteMethod('exec', { + description: 'do something', + accessType: 'WRITE', + accepts: [ + { + arg: 'documentName', + type: 'string', + required: true, + http: { + source: 'path' + }, + description: 'Name of the Document to be fetched from db for rule engine' + }, + { + arg: 'data', + type: 'object', + required: true, + http: { + source: 'body' + }, + description: 'An object on which business rules should be applied' + } + ], + http: { + verb: 'post', + path: '/exec/:documentName' + }, + returns: { + arg: 'data', + type: 'object', + root: true + } + }); + + MyModel.exec = function(docName, data, callback) { + data.docName = docName; + process.nextTick(function(){ + callback(null, data); + }); + }; + + MyModel.remoteMethod('foo', { + description: "foo desc", + isStatic: false, + http: { path: '/foo', verb: 'get' }, + returns: { + arg:'name', type: 'string' + } + }); + + MyModel.prototype.foo = function() { + let args = [].slice.call(arguments); + let callback = args.find(arg => typeof arg === 'function'); + process.nextTick(function() { + callback(null, { "result": "foo"}) + }) + } + + // var toJSON = MyModel.prototype.toJSON + + // MyModel.prototype.toJSON = function() { + // var self = this; + // console.log('This is a patched toJSON call') + // return toJSON.call(self) + // } +} \ No newline at end of file diff --git a/test/common/models/my-model.json b/test/common/models/my-model.json new file mode 100644 index 0000000..4ab35dc --- /dev/null +++ b/test/common/models/my-model.json @@ -0,0 +1,32 @@ +{ + "name": "MyModel", + "base": "BaseEntity", + "idInjection": true, + "properties": { + "name": { + "type": "string" + }, + "category": { + "type": "string", + "require": true + }, + "desc": { + "type": "string" + }, + "price": { + "type": "object" + }, + "isAvailable": { + "type": "boolean" + }, + "modelNo": "string", + "keywords": [ + "string" + ] + }, + "validations": [], + "relations": {}, + "acls": [], + "methods": {}, + "strict": true +} \ No newline at end of file diff --git a/test/component-config.json b/test/component-config.json index 2c63c08..d6fd049 100644 --- a/test/component-config.json +++ b/test/component-config.json @@ -1,2 +1,5 @@ { + "loopback-component-explorer" : { + "mountPath":"/explorer" + } } diff --git a/test/model-config.json b/test/model-config.json index 6c83a4a..6844f04 100644 --- a/test/model-config.json +++ b/test/model-config.json @@ -39,5 +39,9 @@ "ProductOwner" :{ "dataSource": "db", "public": true + }, + "MyModel": { + "dataSource" : "db", + "public" : true } } From ed96afb0fbafb230ac5ac478cfcf67ad0013a493 Mon Sep 17 00:00:00 2001 From: deostroll Date: Thu, 23 Apr 2020 15:35:47 +0530 Subject: [PATCH 07/80] refactor: functions inside fieldReplace to utils --- lib/service-personalizer.js | 93 ++++++++++++++++++------------------- 1 file changed, 44 insertions(+), 49 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index d6e3791..cfda420 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -324,42 +324,8 @@ function fieldReplacementFn(ctx, replacements, cb) { return cb(); } - // replace field function to replace record wrt replacement object and value - function replaceField(record, replacement, value) { - var pos = replacement.indexOf('\uFF0E'); - var key; - var elsePart; - if (pos !== null && pos !== 'undefined' && pos !== -1) { - key = replacement.substr(0, pos); - elsePart = replacement.substr(pos + 1); - } else { - key = replacement; - } - - if (record[key] !== 'undefined' && typeof record[key] === 'object') { - replaceField(record[key], elsePart, value); - } else if (record[key] !== 'undefined' && typeof record[key] !== 'object') { - if (record[key]) { - if (typeof record.__data !== 'undefined') { - record.__data[value] = record[key]; - delete record.__data[key]; - } else { - record[value] = record[key]; - delete record[key]; - } - } - } - } - - function replaceRecord(record, replacements) { - var keys = Object.keys(JSON.parse(JSON.stringify(replacements))); - for (var attr in keys) { - if (keys.hasOwnProperty(attr)) { - replaceField(record, keys[attr], replacements[keys[attr]]); - } - } - return record; - } + // let replaceField = utils.replaceField; + let replaceRecord = utils.replaceRecord; /** * if input or result is array then iterates the process @@ -848,24 +814,53 @@ function maskCharacters(ctx, charMaskRules, cb) { return cb(); } +const utils = { + replaceField(record, replacement, value) { + var pos = replacement.indexOf('\uFF0E'); + var key; + var elsePart; + if (pos !== null && pos !== 'undefined' && pos !== -1) { + key = replacement.substr(0, pos); + elsePart = replacement.substr(pos + 1); + } else { + key = replacement; + } + + if (record[key] !== 'undefined' && typeof record[key] === 'object') { + utils.replaceField(record[key], elsePart, value); + } else if (record[key] !== 'undefined' && typeof record[key] !== 'object') { + if (record[key]) { + if (typeof record.__data !== 'undefined') { + record.__data[value] = record[key]; + delete record.__data[key]; + } else { + record[value] = record[key]; + delete record[key]; + } + } + } + }, + + replaceRecord(record, replacements) { + var keys = Object.keys(JSON.parse(JSON.stringify(replacements))); + for (var attr in keys) { + if (keys.hasOwnProperty(attr)) { + utils.replaceField(record, keys[attr], replacements[keys[attr]]); + } + } + return record; + } +}; + + +const ALT_DOT = '\uFF0E' const p13nFunctions = { fieldReplace(replacements) { - let replace = (data, oldField, newField) => { - let fieldValue = data[oldField]; - data[newField] = fieldValue; - delete data[oldField]; - } + return function(data, callback) { Object.entries(replacements) .forEach(([oldField, newField]) => { - if(Array.isArray(data)) { - data.forEach(dt => { - replace(dt, oldField, newField); - }) - } - else { - replace(data, oldField, newField); - } + }); process.nextTick(callback); From e38b0a91a71839d00658ee58b26047761a135ad8 Mon Sep 17 00:00:00 2001 From: deostroll Date: Thu, 23 Apr 2020 16:32:20 +0530 Subject: [PATCH 08/80] added support for field replace and basic structure for application --- .../mixins/service-personalization-mixin.js | 3 +- lib/service-personalizer.js | 17 +- server/boot/service-personalization.js | 391 +----------------- test/app-list.json | 3 +- test/common/models/product-catalog.json | 5 +- test/test.js | 2 +- 6 files changed, 25 insertions(+), 396 deletions(-) diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index 6b4d5be..a434e65 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -55,7 +55,8 @@ const nextTick = function () { }); } module.exports = function ServicePersonalizationMixin(TargetModel) { - TargetModel.beforeRemote('**', function () { + log.debug(log.defaultContext(), `Applying service personalization for ${TargetModel.definition.name}`); + TargetModel.afterRemote('**', function () { let args = slice.call(arguments); let ctx = args[0]; diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index cfda420..13f2ee0 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -853,15 +853,21 @@ const utils = { }; -const ALT_DOT = '\uFF0E' +// const ALT_DOT = '\uFF0E' + const p13nFunctions = { fieldReplace(replacements) { return function(data, callback) { - Object.entries(replacements) - .forEach(([oldField, newField]) => { - + if(Array.isArray(data)) { + let updatedResult = data.map(record => { + return utils.replaceRecord(record, replacements); }); + data = updatedResult; + } + else { + data = utils.replaceRecord(data, replacements); + } process.nextTick(callback); } @@ -1005,5 +1011,6 @@ module.exports = { execute: execute, loadCustomFunction, getCustomFunction, - applyServicePersonalization + applyServicePersonalization, + init }; diff --git a/server/boot/service-personalization.js b/server/boot/service-personalization.js index 345f3d8..f1e58e8 100644 --- a/server/boot/service-personalization.js +++ b/server/boot/service-personalization.js @@ -1,6 +1,6 @@ /** * - * ©2018-2019 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), + * ©2018-2020 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), * Bangalore, India. All Rights Reserved. * */ @@ -8,7 +8,7 @@ * This boot script brings the ability to apply personalization rules to the model. * * @memberof Boot Scripts - * @author Pradeep Kumar Tippa + * @author deostroll * @name Service Personalization */ // TODO: without clean db test cases are not passing, need to clean up test cases. @@ -19,390 +19,7 @@ var log = require('oe-logger')('service-personalization'); // var messaging = require('../../lib/common/global-messaging'); var servicePersonalizer = require('../../lib/service-personalizer'); -var personalizationRuleModel; - -module.exports = function ServicePersonalization(app, cb) { - log.debug(log.defaultContext(), 'In service-personalization.js boot script.'); - personalizationRuleModel = app.models.PersonalizationRule; - // requiring customFunction - let servicePersoConfig = app.get('servicePersonalization'); - if (servicePersoConfig && 'customFunctionPath' in servicePersoConfig) { - try { - servicePersonalizer.loadCustomFunction(require(servicePersoConfig.customFunctionPath)); - } catch (e) { - log.error(log.defaultContext(), 'require customFunction', e); - } - } - // Creating 'before save' and 'after save' observer hooks for PersonlizationRule model - personalizationRuleModel.observe('before save', personalizationRuleBeforeSave); - personalizationRuleModel.observe('after save', personalizationRuleAfterSave); - // Creating filter finding only records where disabled is false. - var filter = { - where: { - disabled: false - } - }; - // Creating options to fetch all records irrespective of scope. - var options = { - ignoreAutoScope: true, - fetchAllScopes: true - }; - // Using fetchAllScopes and ignoreAutoScope to retrieve all the records from DB. i.e. from all tenants. - personalizationRuleModel.find(filter, options, function (err, results) { - log.debug(log.defaultContext(), 'personalizationRuleModel.find executed.'); - if (err) { - log.error(log.defaultContext(), 'personalizationRuleModel.find error. Error', err); - cb(err); - } else if (results && results.length > 0) { - // The below code for the if clause will not executed for test cases with clean/empty DB. - // In order to execute the below code and get code coverage for it we should have - // some rules defined for some models in the database before running tests for coverage. - log.debug(log.defaultContext(), 'Some PersonalizationRules are present, on loading of this PersonalizationRule model'); - for (var i = 0; i < results.length; i++) { - // No need to publish the message to other nodes, since other nodes will attach the hooks on their boot. - // Attaching all models(PersonalizationRule.modelName) before save hooks when PersonalizationRule loads. - // Passing directly modelName without checking existence since it is a mandatory field for PersonalizationRule. - attachRemoteHooksToModel(results[i].modelName, { ctx: results[i]._autoScope }); - } - cb(); - } else { - cb(); - } - }); +module.exports = function ServicePersonalization(app) { + servicePersonalizer.init(app); }; -// Subscribing for messages to attach 'before save' hook for modelName model when POST/PUT to PersonalizationRule. -// messaging.subscribe('personalizationRuleAttachHook', function (modelName, options) { -// // TODO: need to enhance test cases for running in cluster and send/recieve messages in cluster. -// log.debug(log.defaultContext(), 'Got message to '); -// attachRemoteHooksToModel(modelName, options); -// }); - -/** - * This function is before save hook for PersonlizationRule model. - * - * @param {object} ctx - Model context - * @param {function} next - callback function - */ -function personalizationRuleBeforeSave(ctx, next) { - var data = ctx.data || ctx.instance; - // It is good to have if we have a declarative way of validating model existence. - var modelName = data.modelName; - if (loopback.findModel(modelName, ctx.options)) { - var nextFlag = true; - if (data.personalizationRule.postCustomFunction) { - if (!(Object.keys(servicePersonalizer.getCustomFunction()).indexOf(data.personalizationRule.postCustomFunction.functionName) > -1) && nextFlag) { - next(new Error('Module \'' + data.personalizationRule.postCustomFunction.functionName + '\' doesn\'t exists.')); - nextFlag = false; - } - } - if (data.personalizationRule.preCustomFunction) { - if (!(Object.keys(servicePersonalizer.getCustomFunction()).indexOf(data.personalizationRule.preCustomFunction.functionName) > -1) && nextFlag) { - next(new Error('Module \'' + data.personalizationRule.precustomFunction.functionName + '\' doesn\'t exists.')); - } - } - if (nextFlag) { - next(); - } - } else { - // Not sure it is the right way to construct error object to sent in the response. - var err = new Error('Model \'' + modelName + '\' doesn\'t exists.'); - next(err); - } -} - -/** - * This function is after save hook for PersonlizationRule model. - * - * @param {object} ctx - Model context - * @param {function} next - callback function - */ -function personalizationRuleAfterSave(ctx, next) { - log.debug(log.defaultContext(), 'personalizationRuleAfterSave method.'); - var data = ctx.data || ctx.instance; - // Publishing message to other nodes in cluster to attach the 'before save' hook for model. - // messaging.publish('personalizationRuleAttachHook', data.modelName, ctx.options); - log.debug(log.defaultContext(), 'personalizationRuleAfterSave data is present. calling attachBeforeSaveHookToModel'); - attachRemoteHooksToModel(data.modelName, ctx.options); - next(); -} - -/** - * This function is to attach remote hooks for given modelName to apply PersonalizationRule. - * - * @param {string} modelName - Model name - * @param {object} options - options - */ -function attachRemoteHooksToModel(modelName, options) { - // Can we avoid this step and get the ModelConstructor from context. - var model = loopback.findModel(modelName, options); - // Setting the flag that Personalization Rule exists, need to check where it will be used. - if (!model.settings._personalizationRuleExists) { - model.settings._personalizationRuleExists = true; - // We can put hook methods in an array an have single function to attach them. - // After Remote hooks - - afterRemoteFindHook(model); - afterRemoteFindByIdHook(model); - afterRemoteFindOneHook(model); - afterRemoteCreateHook(model); - afterRemoteUpsertHook(model); - afterRemoteUpdateAttributesHook(model); - - // Before Remote Hooks - beforeRemoteCreateHook(model); - beforeRemoteUpsertHook(model); - beforeRemoteUpdateAttributesHook(model); - beforeRemoteFindHook(model); - beforeRemoteFindOneHook(model); - beforeRemoteFindByIdHook(model); - // model.beforeRemote('**', function(ctx, next) { - - // }); - } -} - -/** - * This function is to attach after remote hook for find for given model. - * - * @param {object} model - Model constructor object. - */ -function afterRemoteFindHook(model) { - model.afterRemote('find', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'afterRemoteFindHook for ', model.modelName, ' called'); - afterRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach after remote hook for findById for given model. - * - * @param {object} model - Model constructor object. - */ -function afterRemoteFindByIdHook(model) { - model.afterRemote('findById', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'afterRemoteFindByIdHook for ', model.modelName, ' called'); - afterRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach after remote hook for findOne for given model. - * - * @param {object} model - Model constructor object. - */ -function afterRemoteFindOneHook(model) { - model.afterRemote('findOne', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'afterRemoteFindOneHook for ', model.modelName, ' called'); - afterRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach after remote hook for create for given model. - * - * @param {object} model - Model constructor object. - */ -function afterRemoteCreateHook(model) { - model.afterRemote('create', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'afterRemoteCreateHook for ', model.modelName, ' called'); - afterRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach after remote hook for upsert for given model. - * - * @param {object} model - Model constructor object. - */ -function afterRemoteUpsertHook(model) { - model.afterRemote('upsert', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'afterRemoteUpsertHook for ', model.modelName, ' called'); - afterRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach after remote hook for updateAttributes for given model. - * - * @param {object} model - Model constructor object. - */ -function afterRemoteUpdateAttributesHook(model) { - model.afterRemote('prototype.updateAttributes', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'afterRemoteUpdateAttributes for ', model.modelName, ' called'); - afterRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach before remote hook for create for given model. - * - * @param {object} model - Model constructor object. - */ -function beforeRemoteCreateHook(model) { - model.beforeRemote('create', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'beforeRemoteCreateHook for ', model.modelName, ' called'); - beforeRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach before remote hook for upsert for given model. - * - * @param {object} model - Model constructor object. - */ -function beforeRemoteUpsertHook(model) { - model.beforeRemote('upsert', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'beforeRemoteUpsertHook for ', model.modelName, ' called'); - beforeRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach before remote hook for updateAttributes for given model. - * - * @param {object} model - Model constructor object. - */ -function beforeRemoteUpdateAttributesHook(model) { - model.beforeRemote('prototype.updateAttributes', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'beforeRemoteUpdateAttributesHook for ', model.modelName, ' called'); - beforeRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach before remote hook for find for given model - * and modify ctx.args.filter if any corresponding personalization rule is there. - * - * @param {object} model - Model constructor object. - */ -function beforeRemoteFindHook(model) { - model.beforeRemote('find', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'beforeRemoteFindHook ', model.modelName, 'called'); - servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationAccessHookGetRuleCb(rule) { - if (rule !== null && typeof rule !== 'undefined') { - log.debug(ctx.req.callContext, 'beforeRemoteFindHook personalization rule found , rule: ', rule); - var fns = servicePersonalizer.applyPersonalizationRule(ctx, rule.personalizationRule); - fns = fns.filter(x => x.type !== 'postCustomFunction'); - fns = fns.map(x => x.fn); - servicePersonalizer.execute(fns, function (err) { - if (err) { - return next(err); - } - log.debug(ctx.req.callContext, 'filter', ctx.args.filter); - next(); - }); - } else { - log.debug(ctx.req.callContext, 'beforeRemoteFindHook no rules were found'); - next(); - } - }); - }); -} - -function beforeRemoteFindOneHook(model) { - model.beforeRemote('findOne', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'beforeRemoteFindOneHook ', model.modelName, 'called'); - servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationAccessHookGetRuleCb(rule) { - if (rule !== null && typeof rule !== 'undefined') { - log.debug(ctx.req.callContext, 'beforeRemoteFindOneHook personalization rule found , rule: ', rule); - var fns = servicePersonalizer.applyPersonalizationRule(ctx, rule.personalizationRule); - fns = fns.filter(x => x.type !== 'postCustomFunction'); - fns = fns.map(x => x.fn); - servicePersonalizer.execute(fns, function (err) { - if (err) { - return next(err); - } - log.debug(ctx.req.callContext, 'filter', ctx.args.filter); - next(); - }); - } else { - log.debug(ctx.req.callContext, 'beforeRemoteFindOneHook no rules were found'); - next(); - } - }); - }); -} - -function beforeRemoteFindByIdHook(model) { - model.beforeRemote('findById', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'beforeRemoteFindByIdHook ', model.modelName, 'called'); - servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationAccessHookGetRuleCb(rule) { - if (rule !== null && typeof rule !== 'undefined') { - log.debug(ctx.req.callContext, 'beforeRemoteFindByIdHook personalization rule found , rule: ', rule); - var fns = servicePersonalizer.applyPersonalizationRule(ctx, rule.personalizationRule); - fns = fns.filter(x => x.type !== 'postCustomFunction'); - fns = fns.map(x => x.fn); - servicePersonalizer.execute(fns, function (err) { - if (err) { - return next(err); - } - log.debug(ctx.req.callContext, 'filter', ctx.args.filter); - next(); - }); - } else { - log.debug(ctx.req.callContext, 'beforeRemoteFindByIdHook no rules were found'); - next(); - } - }); - }); -} - -/** - * This function is to do the execution personalization rules of after remote hook for given model. - * - * @param {object} model - Model constructor object. - * @param {object} ctx - context object. - * @param {function} next - callback function. - */ -function afterRemotePersonalizationExec(model, ctx, next) { - log.debug(ctx.req.callContext, 'afterRemotePersonalizationExec for ', model.modelName, ' called.'); - servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationMixinBeforeCreateGetReverse(rule) { - if (rule !== null && typeof rule !== 'undefined') { - log.debug(ctx.req.callContext, 'afterRemotePersonalizationExec personalization rule found , rule: ', rule); - log.debug(ctx.req.callContext, 'applying PersonalizationRule now'); - log.debug(ctx.req.callContext, 'beforeRemoteFindHook personalization rule found , rule: ', rule); - var fns = servicePersonalizer.applyPersonalizationRule(ctx, rule.personalizationRule); - fns = fns.map(x => x.fn); - servicePersonalizer.execute(fns, function (err) { - if (err) { - return next(err); - } - log.debug(ctx.req.callContext, 'filter', ctx.args.filter); - next(); - }); - } else { - log.debug(ctx.req.callContext, 'afterRemotePersonalizationExec no rules were found'); - next(); - } - }); -} - -/** - * This function is to do the execution personalization rules of before remote hook for given model. - * - * @param {object} model - Model constructor object. - * @param {object} ctx - context object. - * @param {function} next - callback function. - */ -function beforeRemotePersonalizationExec(model, ctx, next) { - log.debug(ctx.req.callContext, 'beforeRemotePersonalizationExec for ', model.modelName, ' called.'); - servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationMixinBeforeCreateGetReverse(rule) { - if (rule !== null && typeof rule !== 'undefined') { - log.debug(ctx.req.callContext, 'beforeRemotePersonalizationExec personalization rule found , rule: ', rule); - log.debug(ctx.req.callContext, 'applying PersonalizationRule now'); - var fns = servicePersonalizer.applyReversePersonalizationRule(ctx, rule.personalizationRule); - fns = fns.map(x => x.fn); - servicePersonalizer.execute(fns, function (err) { - if (err) { - return next(err); - } - log.debug(ctx.req.callContext, 'filter', ctx.args.filter); - next(); - }); - } else { - log.debug(ctx.req.callContext, 'beforeRemotePersonalizationExec no rules were found'); - next(); - } - }); -} diff --git a/test/app-list.json b/test/app-list.json index ad5f6d0..0b36f67 100644 --- a/test/app-list.json +++ b/test/app-list.json @@ -15,6 +15,7 @@ { "path": "./", "enabled": true, - "serverDir" : "test" + "serverDir" : "test", + "autoEnableMixins": true } ] diff --git a/test/common/models/product-catalog.json b/test/common/models/product-catalog.json index 12dd980..9606a0f 100644 --- a/test/common/models/product-catalog.json +++ b/test/common/models/product-catalog.json @@ -28,5 +28,8 @@ "relations": {}, "acls": [], "methods": {}, - "strict": true + "strict": true, + "mixins": { + "ServicePersonalizationMixin" : true + } } \ No newline at end of file diff --git a/test/test.js b/test/test.js index b75fa92..c9ee4c6 100755 --- a/test/test.js +++ b/test/test.js @@ -159,7 +159,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t1 should replace field names in response when fieldReplace personalization is configured', function (done) { + it.only('t1 should replace field names in response when fieldReplace personalization is configured', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', From 5196b99266ef8d9f7c836fe48743e445bbff608a Mon Sep 17 00:00:00 2001 From: deostroll Date: Fri, 24 Apr 2020 11:40:08 +0530 Subject: [PATCH 09/80] incomplete mask feature support --- .../mixins/service-personalization-mixin.js | 5 +- lib/service-personalizer.js | 310 +++++++++++++----- test/common/models/product-owner.json | 5 +- test/test.js | 6 +- 4 files changed, 233 insertions(+), 93 deletions(-) diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index a434e65..08c8f81 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -60,7 +60,7 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { let args = slice.call(arguments); let ctx = args[0]; - let next = args.slice(-1); + let next = args.slice(-1)[0]; let callCtx = ctx.req.callContext; log.debug(callCtx, `MethodString: ${ctx.methodString}`); @@ -72,6 +72,9 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { case 'create': data = ctx.instance break; + case 'find': + data = ctx.result; + break; default: log.debug(callCtx, `Unhandled: ${ctx.methodString}`); data = {} diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 13f2ee0..c10b7fa 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -494,52 +494,55 @@ function fieldValueReplacementFn(ctx, replacements, cb) { }); } - function replaceValue(record, replacement, value) { - var pos = replacement.indexOf('\uFF0E'); - var key; - var elsePart; - if (pos !== null && typeof pos !== 'undefined' && pos !== -1) { - key = replacement.substr(0, pos); - elsePart = replacement.substr(pos + 1); - } else { - key = replacement; - } - - - if (typeof record[key] !== 'undefined' && Array.isArray(record[key])) { - var newValue = record[key]; - record[key].forEach(function (element, index) { - if (value[element]) { - newValue[index] = value[element]; - } - }); - if (typeof record.__data !== 'undefined') { - record.__data[key] = newValue; - } else { - record[key] = newValue; - } - } else if (typeof record[key] !== 'undefined' && typeof record[key] === 'object') { - replaceValue(record[key], elsePart, value); - } else if (typeof record[key] !== 'undefined' && typeof record[key] !== 'object') { - if (value.hasOwnProperty(record[key])) { - if (typeof record.__data !== 'undefined') { - record.__data[key] = value[record[key]]; - } else { - record[key] = value[record[key]]; - } - } - } - } - function replaceRecord(record, replacements) { - var keys = Object.keys(JSON.parse(JSON.stringify(replacements))); - for (var attr in keys) { - if (keys.hasOwnProperty(attr)) { - replaceValue(record, keys[attr], replacements[keys[attr]]); - } - } - return record; - } + // function replaceValue(record, replacement, value) { + // var pos = replacement.indexOf('\uFF0E'); + // var key; + // var elsePart; + // if (pos !== null && typeof pos !== 'undefined' && pos !== -1) { + // key = replacement.substr(0, pos); + // elsePart = replacement.substr(pos + 1); + // } else { + // key = replacement; + // } + + + // if (typeof record[key] !== 'undefined' && Array.isArray(record[key])) { + // var newValue = record[key]; + // record[key].forEach(function (element, index) { + // if (value[element]) { + // newValue[index] = value[element]; + // } + // }); + // if (typeof record.__data !== 'undefined') { + // record.__data[key] = newValue; + // } else { + // record[key] = newValue; + // } + // } else if (typeof record[key] !== 'undefined' && typeof record[key] === 'object') { + // replaceValue(record[key], elsePart, value); + // } else if (typeof record[key] !== 'undefined' && typeof record[key] !== 'object') { + // if (value.hasOwnProperty(record[key])) { + // if (typeof record.__data !== 'undefined') { + // record.__data[key] = value[record[key]]; + // } else { + // record[key] = value[record[key]]; + // } + // } + // } + // } + + // function replaceRecord(record, replacements) { + // var keys = Object.keys(JSON.parse(JSON.stringify(replacements))); + // for (var attr in keys) { + // if (keys.hasOwnProperty(attr)) { + // replaceValue(record, keys[attr], replacements[keys[attr]]); + // } + // } + // return record; + // } + + let replaceRecord = utils.replaceRecordFactory(utils.replaceValue); if (Array.isArray(input)) { var updatedResult = []; @@ -814,7 +817,21 @@ function maskCharacters(ctx, charMaskRules, cb) { return cb(); } +/** + * queue the function to the runtime's next event loop + * + * @param {function} cb - the callback function + */ +const nextTick = cb => process.nextTick(cb); + const utils = { + /** + * field replacer function + * + * @param {instance or data} record - the model instance or plain data + * @param {object} replacement - personalization rule (for field name replace) + * @param {string} value - new field name + */ replaceField(record, replacement, value) { var pos = replacement.indexOf('\uFF0E'); var key; @@ -841,14 +858,63 @@ const utils = { } }, - replaceRecord(record, replacements) { - var keys = Object.keys(JSON.parse(JSON.stringify(replacements))); - for (var attr in keys) { - if (keys.hasOwnProperty(attr)) { - utils.replaceField(record, keys[attr], replacements[keys[attr]]); + /** + * field value replace function + * @param {instance or data} record - the model instance or data + * @param {object} replacement - the personalization rule (for field value replace) + * @param {string} value - the value to replace + */ + replaceValue(record, replacement, value) { + var pos = replacement.indexOf('\uFF0E'); + var key; + var elsePart; + if (pos !== null && typeof pos !== 'undefined' && pos !== -1) { + key = replacement.substr(0, pos); + elsePart = replacement.substr(pos + 1); + } else { + key = replacement; + } + + + if (typeof record[key] !== 'undefined' && Array.isArray(record[key])) { + var newValue = record[key]; + record[key].forEach(function (element, index) { + if (value[element]) { + newValue[index] = value[element]; + } + }); + if (typeof record.__data !== 'undefined') { + record.__data[key] = newValue; + } else { + record[key] = newValue; + } + } else if (typeof record[key] !== 'undefined' && typeof record[key] === 'object') { + replaceValue(record[key], elsePart, value); + } else if (typeof record[key] !== 'undefined' && typeof record[key] !== 'object') { + if (value.hasOwnProperty(record[key])) { + if (typeof record.__data !== 'undefined') { + record.__data[key] = value[record[key]]; + } else { + record[key] = value[record[key]]; + } + } + } + }, + + replaceRecordFactory(fn) { + return function replaceRecord(record, replacements) { + var keys = Object.keys(JSON.parse(JSON.stringify(replacements))); + for (var attr in keys) { + if (keys.hasOwnProperty(attr)) { + fn(record, keys[attr], replacements[keys[attr]]); + } } + return record; } - return record; + }, + + noop(){ + // do nothing } }; @@ -859,14 +925,33 @@ const p13nFunctions = { fieldReplace(replacements) { return function(data, callback) { + let replaceRecord = utils.replaceRecordFactory(utils.replaceField); + if(Array.isArray(data)) { + let updatedResult = data.map(record => { + return replaceRecord(record, replacements); + }); + data = updatedResult; + } + else { + data = replaceRecord(data, replacements); + } + + process.nextTick(callback); + } + }, + + fieldValueReplace(replacements) { + + return function(data, callback) { + let replaceRecord = utils.replaceRecordFactory(utils.replaceValue); if(Array.isArray(data)) { let updatedResult = data.map(record => { - return utils.replaceRecord(record, replacements); + return replaceRecord(record, replacements); }); data = updatedResult; } else { - data = utils.replaceRecord(data, replacements); + data = replaceRecord(data, replacements); } process.nextTick(callback); @@ -876,6 +961,72 @@ const p13nFunctions = { noop : function(data, cb) { !(function(){})(data) process.nextTick(cb); + }, + + addSort(ctx, instruction) { + return function(data, callback) { + + } + }, + + /** + * Mask helper function for masking a field + * + * @param {CallContext} ctx - the request context + * @param {object} instructions - personalization rule object + * + * Tests: t13 + * + * PreApplication: No + * PostApplication: Yes + * + * Example Rule - Masks a "category field" + * + * var rule = { + * "modelName": "ProductCatalog", + * "personalizationRule" : { + * "mask" : { + * "category": true + * } + * } + * }; + */ + mask(ctx, instructions) { + return function(data, callback) { + utils.noop(data); + let dsSupportMask = true; + if(dsSupportMask) { + ctx.args.filter = ctx.args.filter || {}; + let query = ctx.args.filter; + // TODO: (Arun - 2020-04-24 11:16:19) Don't we need to handle the alternate case? + // i.e. when there is no filter ? + if(!query) { + return nextTick(callback); + } + let keys = Object.keys(instructions); + let exp = {}; + if(typeof query.fields === 'undefined') { + for (var i = 0, length = keys.length; i < length; i++) { + var key = keys[i]; + key = key.indexOf('|') > -1 ? key.replace(/\|/g, '.') : key; + exp[key] = false; + } + query.fields = exp; + } + + //TODO: (Arun - 2020-04-24 11:17:11) shouldn't we uncomment the following? + + // else { + // var fieldList = query.fields; + // fieldList = _.filter(fieldList, function (item) { + // return keys.indexOf(item) === -1 + // }); + // query.fields = fieldList; + // } + } + + nextTick(callback); + }; } } /** @@ -886,11 +1037,17 @@ const p13nFunctions = { * @param {List or Instance} data - data from the context. Could either be a List of model instances or a single model instance * @param {function} done - the callback which receives the new data. First argument of function is an error object */ -function personalize(isReverse, instructions, data, done) { +function personalize(ctx, isReverse, instructions, data, done) { let tasks = Object.entries(instructions).map(([operation, instruction]) => { switch(operation) { case 'fieldReplace': - return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction) } + return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction) }; + case 'fieldValueReplace': + return { type: 'fieldValueReplace', fn: p13nFunctions.fieldValueReplace(instruction) }; + // case 'sort': + // return { type: 'sort', fn: p13nFunctions.addSort(ctx, instruction) }; + case 'mask': + return { type: 'mask', fn: p13nFunctions.mask(ctx, instruction) }; default: return { type: operation, fn: p13nFunctions.noop } } @@ -914,45 +1071,22 @@ function personalize(isReverse, instructions, data, done) { function checkRelationAndRecurse(Model, data, personalizationOptions, done) { - if(Model.definition.relations) { - relationEntries = Object.entries(Model.definitions.relations); - async.each(relationEntries, function asyncExecutor([relationName, relationDef], asyncExecutorDoneCb){ - let relatedModelName = relationDef.model; - //check if data has the relation - relData = data[relationName]; - if(typeof relData !== 'undefined') { - // apply personalization and exit function - return applyServicePersonalization(relatedModelName, relData, personalizationOptions, function(err, persData) { - if(err) { - asyncExecutorDoneCb(err); - } - else { - data[relationName] = persData; - asyncExecutorDoneCb(); - } - }); - } - // no related data - do nothing - process.nextTick(function() { - asyncExecutorDoneCb(); - }); - }, function asyncEachDone(err){ - }); - } - else { - // ! no relations defined on model - exit function - process.nextTick(function() { - done(); - }); + if(Model.definition.relations.length > 0) { + throw Error('not implemented'); } + + //! no relations + process.nextTick(function() { + done(); + }); } function applyServicePersonalization(modelName, data, personalizationOptions, done) { let { reverse, context } = personalizationOptions; - let findQuery = { where: { modelName, enabled: true }}; + let findQuery = { where: { modelName, disabled: false }}; let Model = loopback.findModel(modelName); - + // console.log(Model.definition) PersonalizationRule.find(findQuery, context, function(err, entries) { if(err) { done(err); @@ -974,7 +1108,7 @@ function applyServicePersonalization(modelName, data, personalizationOptions, do }); } else { - personalize(reverse, entries[0].instructions, data, function(err) { + personalize(context, reverse, entries[0].personalizationRule, data, function(err) { if(err) { done(err); } diff --git a/test/common/models/product-owner.json b/test/common/models/product-owner.json index 59658f6..4fb18ec 100644 --- a/test/common/models/product-owner.json +++ b/test/common/models/product-owner.json @@ -19,5 +19,8 @@ } }, "acls": [], - "methods": {} + "methods": {}, + "mixins": { + "ServicePersonalizationMixin" : true + } } diff --git a/test/test.js b/test/test.js index c9ee4c6..d249a2f 100755 --- a/test/test.js +++ b/test/test.js @@ -211,7 +211,7 @@ describe(chalk.blue('service personalization test started...'), function () { 'id': 2 }; - it('t2 create records in product owners', function (done) { + it.only('t2 create records in product owners', function (done) { ProductOwner = loopback.findModel('ProductOwner'); ProductOwner.create(owner1, function (err) { if (err) { @@ -223,7 +223,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t3 should replace field names in response when fieldReplace personalization is configured', function (done) { + it.only('t3 should replace field names in response when fieldReplace personalization is configured', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -599,7 +599,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t13 Mask:should mask the given fields and not send them to the response', function (done) { + it.only('t13 Mask:should mask the given fields and not send them to the response', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', From 37ccd31e41e505f2e3cf2191dbe042f8664cb71e Mon Sep 17 00:00:00 2001 From: deostroll Date: Fri, 24 Apr 2020 19:58:13 +0530 Subject: [PATCH 10/80] sort support --- .../mixins/service-personalization-mixin.js | 125 +++++++++-------- lib/service-personalizer.js | 132 +++++++++++++----- lib/utils.js | 35 +++++ 3 files changed, 197 insertions(+), 95 deletions(-) create mode 100644 lib/utils.js diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index 08c8f81..2c6029d 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -23,79 +23,84 @@ const logger = require('oe-logger'); const log = logger('service-personalization-mixin'); const { applyServicePersonalization } = require('./../../lib/service-personalizer'); +const { nextTick, parseMethodString, slice } = require('./../../lib/utils'); -const ALLOWED_METHOD = ['create', 'find', 'fineOne']; - -const parseMethodString = str => { - return str.split('.').reduce((obj, comp, idx, arr) => { - let ret = {}; - let length = arr.length; - if (idx === 0) { - ret.modelName = comp; - } - else if (length === 3 && idx !== length - 1) { - ret.isStatic = false; - } - else if (length === 3 && idx == length - 1) { - ret.methodName = comp; - } - else { - ret.isStatic = true; - ret.methodName = comp; - } - return Object.assign({}, obj, ret); - }, {}); -} -const slice = [].slice; -const nextTick = function () { - let args = slice.call(arguments); - let cb = args.shift(); - return process.nextTick(() => { - cb.apply(null, args); - }); -} module.exports = function ServicePersonalizationMixin(TargetModel) { log.debug(log.defaultContext(), `Applying service personalization for ${TargetModel.definition.name}`); TargetModel.afterRemote('**', function () { - let args = slice.call(arguments); + let args = slice(arguments); let ctx = args[0]; - let next = args.slice(-1)[0]; - let callCtx = ctx.req.callContext; - log.debug(callCtx, `MethodString: ${ctx.methodString}`); + let next = args[args.length - 1]; + // let callCtx = ctx.req.callContext; + log.debug(ctx, `afterRemote: MethodString: ${ctx.methodString}`); ctxInfo = parseMethodString(ctx.methodString); - if (ALLOWED_METHOD.includes(ctxInfo.methodName)) { - let data = null; - if (ctxInfo.isStatic) { - switch (ctxInfo.methodName) { - case 'create': - data = ctx.instance - break; - case 'find': - data = ctx.result; - break; - default: - log.debug(callCtx, `Unhandled: ${ctx.methodString}`); - data = {} + + let data = null; + if (ctxInfo.isStatic) { + switch (ctxInfo.methodName) { + case 'create': + data = ctx.instance + break; + case 'find': + data = ctx.result; + break; + default: + log.debug(ctx, `afterRemote: Unhandled: ${ctx.methodString}`); + data = {} + } + + let personalizationOptions = { + isBeforeRemote: false, + context: ctx + }; + + return applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function(err) { + if(err) { + next(err); } + else { + next(); + } + }); + } - let personalizationOptions = { - reverse: true, - context: callCtx - }; + log.debug(callCtx, `afterRemote: Unhandled non-static: ${ctx.methodString}`); + nextTick(next); + }); + + TargetModel.beforeRemote('**', function() { + let args = slice(arguments); + let ctx = args[0]; + let next = args[args.length - 1]; + // let callCtx = ctx.req.callContext; + + log.debug(ctx, `beforeRemote: MethodString: ${ctx.methodString}`); - return applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function(err, personalizedData) { - if(err) { - next(err); - } - else { - next(); - } - }); + ctxInfo = parseMethodString(ctx.methodString); + + if(ctxInfo.isStatic) { + switch(ctxInfo.methodString) { + case 'find': + data = {} + break; + default: + data = {} + log.debug(ctx, `beforeRemote: Unhandled ${ctx.methodString}`); } - } + let personalizationOptions = { + isBeforeRemote: true, + context: ctx + }; + + return applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function(err){ + next(err); + }); + } + + log.debug(ctx, `beforeRemote: Unhandled non-static ${ctx.methodString}`); nextTick(next); }); } \ No newline at end of file diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index c10b7fa..26b7193 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -19,7 +19,7 @@ var mergeQuery = require('loopback-datasource-juggler/lib/utils').mergeQuery; var logger = require('oe-logger'); var log = logger('service-personalizer'); var customFunction; - +const { nextTick } = require('./utils'); /** * This function returns the necessary sorting logic to @@ -817,13 +817,6 @@ function maskCharacters(ctx, charMaskRules, cb) { return cb(); } -/** - * queue the function to the runtime's next event loop - * - * @param {function} cb - the callback function - */ -const nextTick = cb => process.nextTick(cb); - const utils = { /** * field replacer function @@ -936,7 +929,7 @@ const p13nFunctions = { data = replaceRecord(data, replacements); } - process.nextTick(callback); + nextTick(callback); } }, @@ -954,18 +947,74 @@ const p13nFunctions = { data = replaceRecord(data, replacements); } - process.nextTick(callback); + nextTick(callback); } }, noop : function(data, cb) { - !(function(){})(data) - process.nextTick(cb); + utils.noop(data); + nextTick(cb); }, - addSort(ctx, instruction) { + /** + * Apply a sort. Mostly passed on to the Model.find() + * where the actual sorting is applied. (Provided the + * underlying datasource supports it) + * + * PreApplication: Yes + * PostApplication: No + * + * @param {HttpContext} ctx - the context object + * @param {object} instruction - the personalization sort rule + */ + addSort(ctx, instruction) { //Tests: t4, t5, t6, t7, t8, t10, t11 return function(data, callback) { - + utils.noop(data); + var dsSupportSort = true; + if (dsSupportSort) { + var query = ctx.args.filter || {}; + //TODO: (Arun 2020-04-24 19:43:38) - what if no filter? + if (query) { + if (typeof query.order === 'string') { + query.order = [query.order]; + } + + var tempKeys = []; + + if (query.order && query.order.length >= 1) { + query.order.forEach(function addSortQueryOrderForEachFn(item) { + tempKeys.push(item.split(' ')[0]); + }); + } + + // create the order expression based on the instruction passed + var orderExp = createOrderExp(instruction, tempKeys); + + if (typeof query.order === 'undefined') { + query.order = orderExp; + } else { + query.order = query.order.concat(orderExp); + } + query.order = _.uniq(query.order); + ctx.args.filter = ctx.args.filter || {}; + ctx.args.filter.order = query.order; + nextTick(callback); + } else { + cb(); + } + } + + /** + * TODO: (Arun 2020-04-24 19:37:19) we may need to enable + * the below lines if the datasource in consideration is + * service-oriented, like a web service (for e.g.), and, + * not a traditional db like mongo or postgres + */ + + // else { + // addPostProcessingFunction(ctx, 'sortInMemory', instruction, sortInMemory); + // cb(); + // } } }, @@ -1037,29 +1086,41 @@ const p13nFunctions = { * @param {List or Instance} data - data from the context. Could either be a List of model instances or a single model instance * @param {function} done - the callback which receives the new data. First argument of function is an error object */ -function personalize(ctx, isReverse, instructions, data, done) { - let tasks = Object.entries(instructions).map(([operation, instruction]) => { - switch(operation) { - case 'fieldReplace': - return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction) }; - case 'fieldValueReplace': - return { type: 'fieldValueReplace', fn: p13nFunctions.fieldValueReplace(instruction) }; - // case 'sort': - // return { type: 'sort', fn: p13nFunctions.addSort(ctx, instruction) }; - case 'mask': - return { type: 'mask', fn: p13nFunctions.mask(ctx, instruction) }; - default: - return { type: operation, fn: p13nFunctions.noop } - } - }).sort(sortFactoryFn(isReverse)); +function personalize(ctx, isBeforeRemote, instructions, data, done) { + let tasks = null; + if(isBeforeRemote) { + tasks = Object.entries(instructions).map(([operation, instruction]) => { + switch(operation) { + case 'mask': + return { type: 'mask' , fn: p13nFunctions.mask(ctx, instruction) }; + case 'sort': + return { type: 'mask', fn: p13nFunctions.addSort(ctx, instruction) }; + default: + return { type: `${operation}:noop`, fn: p13nFunctions.noop } + } + }); + } + else { + tasks = Object.entries(instructions).map(([operation, instruction]) => { + switch(operation) { + case 'fieldReplace': + return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction) }; + case 'fieldValueReplace': + return { type: 'fieldValueReplace', fn: p13nFunctions.fieldValueReplace(instruction) }; + default: + return { type: operation, fn: p13nFunctions.noop } + } + }); + } - let asyncIterator = function( { fn }, done) { + let asyncIterator = function( { type, fn }, done) { + log.debug(ctx, `${isBeforeRemote ? "beforeRemote" : "afterRemote"}: applying function - ${type}`); fn(data, function(err){ done(err); }); }; - async.each(tasks, asyncIterator, function asyncEachCb(err){ + async.each(tasks.sort(sortFactoryFn(isBeforeRemote)), asyncIterator, function asyncEachCb(err){ if(err) { done(err) } @@ -1077,17 +1138,18 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { } //! no relations - process.nextTick(function() { + nextTick(function() { done(); }); } function applyServicePersonalization(modelName, data, personalizationOptions, done) { - let { reverse, context } = personalizationOptions; + let { isBeforeRemote, context } = personalizationOptions; let findQuery = { where: { modelName, disabled: false }}; let Model = loopback.findModel(modelName); // console.log(Model.definition) - PersonalizationRule.find(findQuery, context, function(err, entries) { + let callContext = context.req.callContext; + PersonalizationRule.find(findQuery, callContext, function(err, entries) { if(err) { done(err); } @@ -1108,7 +1170,7 @@ function applyServicePersonalization(modelName, data, personalizationOptions, do }); } else { - personalize(context, reverse, entries[0].personalizationRule, data, function(err) { + personalize(context, isBeforeRemote, entries[0].personalizationRule, data, function(err) { if(err) { done(err); } diff --git a/lib/utils.js b/lib/utils.js new file mode 100644 index 0000000..f27f357 --- /dev/null +++ b/lib/utils.js @@ -0,0 +1,35 @@ +const _slice = [].slice; +module.exports = { + /** + * queue the function to the runtime's next event loop + * + * @param {function} cb - the callback function + */ + nextTick: cb => process.nextTick(cb), + + /** + * parses the context's method string + */ + parseMethodString : str => { + return str.split('.').reduce((obj, comp, idx, arr) => { + let ret = {}; + let length = arr.length; + if (idx === 0) { + ret.modelName = comp; + } + else if (length === 3 && idx !== length - 1) { + ret.isStatic = false; + } + else if (length === 3 && idx == length - 1) { + ret.methodName = comp; + } + else { + ret.isStatic = true; + ret.methodName = comp; + } + return Object.assign({}, obj, ret); + }, {}); + }, + + slice : arg => _slice.call(arg) +} \ No newline at end of file From 20f70cf21444daec699f9acf40511799ef6ea4af Mon Sep 17 00:00:00 2001 From: deostroll Date: Fri, 24 Apr 2020 19:58:47 +0530 Subject: [PATCH 11/80] sort support --- lib/service-personalizer.js | 2 +- test/test.js | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 26b7193..c23f577 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -1094,7 +1094,7 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { case 'mask': return { type: 'mask' , fn: p13nFunctions.mask(ctx, instruction) }; case 'sort': - return { type: 'mask', fn: p13nFunctions.addSort(ctx, instruction) }; + return { type: 'sort', fn: p13nFunctions.addSort(ctx, instruction) }; default: return { type: `${operation}:noop`, fn: p13nFunctions.noop } } diff --git a/test/test.js b/test/test.js index d249a2f..c6d3ec8 100755 --- a/test/test.js +++ b/test/test.js @@ -261,7 +261,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); // sort test cases - it('t4 single sort condition: should return the sorted result when sort personalization rule is configured.', function (done) { + it.only('t4 single sort condition: should return the sorted result when sort personalization rule is configured.', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -295,7 +295,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t5 single sort condition: should sort in ascending order when the sort order is not specified', function (done) { + it.only('t5 single sort condition: should sort in ascending order when the sort order is not specified', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -333,7 +333,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t6 single sort condition: should accept the keywords like asc,ascending,desc or descending as sort order', function (done) { + it.only('t6 single sort condition: should accept the keywords like asc,ascending,desc or descending as sort order', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -368,7 +368,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t7 smultiple sort condition: should return sorted result when personalization rule with multiple sort is configured', function (done) { + it.only('t7 smultiple sort condition: should return sorted result when personalization rule with multiple sort is configured', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -406,7 +406,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t8 multiple sort condition: should omit the sort expression whose order value(ASC|DSC) doesnt match the different cases', function (done) { + it.only('t8 multiple sort condition: should omit the sort expression whose order value(ASC|DSC) doesnt match the different cases', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -487,7 +487,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t10 multiple sort: should handle duplicate sort expressions', function (done) { + it.only('t10 multiple sort: should handle duplicate sort expressions', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -522,7 +522,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t11 multiple sort: should handle clashing sort expressions.(Eg:name ASC in personalization rule and name DESC from API, in this case consider name DESC from API)', + it.only('t11 multiple sort: should handle clashing sort expressions.(Eg:name ASC in personalization rule and name DESC from API, in this case consider name DESC from API)', function (done) { // Setup personalization rule var ruleForAndroid = { From 92551438ac079404ee609ff77a31667d5fd1f862 Mon Sep 17 00:00:00 2001 From: deostroll Date: Fri, 24 Apr 2020 20:15:35 +0530 Subject: [PATCH 12/80] support for filter --- lib/service-personalizer.js | 108 ++++++++++++++++++++++-------------- 1 file changed, 65 insertions(+), 43 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index c23f577..7ce67e2 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -832,7 +832,7 @@ const utils = { if (pos !== null && pos !== 'undefined' && pos !== -1) { key = replacement.substr(0, pos); elsePart = replacement.substr(pos + 1); - } else { + } else { key = replacement; } @@ -905,8 +905,8 @@ const utils = { return record; } }, - - noop(){ + + noop() { // do nothing } }; @@ -916,10 +916,10 @@ const utils = { const p13nFunctions = { fieldReplace(replacements) { - - return function(data, callback) { + + return function (data, callback) { let replaceRecord = utils.replaceRecordFactory(utils.replaceField); - if(Array.isArray(data)) { + if (Array.isArray(data)) { let updatedResult = data.map(record => { return replaceRecord(record, replacements); }); @@ -928,16 +928,16 @@ const p13nFunctions = { else { data = replaceRecord(data, replacements); } - + nextTick(callback); } }, fieldValueReplace(replacements) { - return function(data, callback) { + return function (data, callback) { let replaceRecord = utils.replaceRecordFactory(utils.replaceValue); - if(Array.isArray(data)) { + if (Array.isArray(data)) { let updatedResult = data.map(record => { return replaceRecord(record, replacements); }); @@ -946,12 +946,12 @@ const p13nFunctions = { else { data = replaceRecord(data, replacements); } - + nextTick(callback); } }, - noop : function(data, cb) { + noop: function (data, cb) { utils.noop(data); nextTick(cb); }, @@ -968,7 +968,7 @@ const p13nFunctions = { * @param {object} instruction - the personalization sort rule */ addSort(ctx, instruction) { //Tests: t4, t5, t6, t7, t8, t10, t11 - return function(data, callback) { + return function (data, callback) { utils.noop(data); var dsSupportSort = true; if (dsSupportSort) { @@ -978,18 +978,18 @@ const p13nFunctions = { if (typeof query.order === 'string') { query.order = [query.order]; } - + var tempKeys = []; - + if (query.order && query.order.length >= 1) { query.order.forEach(function addSortQueryOrderForEachFn(item) { tempKeys.push(item.split(' ')[0]); }); } - + // create the order expression based on the instruction passed var orderExp = createOrderExp(instruction, tempKeys); - + if (typeof query.order === 'undefined') { query.order = orderExp; } else { @@ -1002,8 +1002,8 @@ const p13nFunctions = { } else { cb(); } - } - + } + /** * TODO: (Arun 2020-04-24 19:37:19) we may need to enable * the below lines if the datasource in consideration is @@ -1041,20 +1041,20 @@ const p13nFunctions = { * }; */ mask(ctx, instructions) { - return function(data, callback) { + return function (data, callback) { utils.noop(data); let dsSupportMask = true; - if(dsSupportMask) { + if (dsSupportMask) { ctx.args.filter = ctx.args.filter || {}; let query = ctx.args.filter; // TODO: (Arun - 2020-04-24 11:16:19) Don't we need to handle the alternate case? // i.e. when there is no filter ? - if(!query) { + if (!query) { return nextTick(callback); } let keys = Object.keys(instructions); let exp = {}; - if(typeof query.fields === 'undefined') { + if (typeof query.fields === 'undefined') { for (var i = 0, length = keys.length; i < length; i++) { var key = keys[i]; key = key.indexOf('|') > -1 ? key.replace(/\|/g, '.') : key; @@ -1076,6 +1076,26 @@ const p13nFunctions = { nextTick(callback); }; + }, + + /** + * add a filter to Model.find() + * + * PreApplication: yes + * PostApplication: no + * + * @param {HttpContext} ctx - http context + * @param {object} instruction - personalization filter rule + */ + addFilter(ctx, instruction) {// Tests: t9 + return function (data, callback) { + utils.noop(data); + var dsSupportFilter = true; + + if (dsSupportFilter) { + addWhereClause(ctx, instruction, callback); + } + } } } /** @@ -1088,13 +1108,15 @@ const p13nFunctions = { */ function personalize(ctx, isBeforeRemote, instructions, data, done) { let tasks = null; - if(isBeforeRemote) { + if (isBeforeRemote) { tasks = Object.entries(instructions).map(([operation, instruction]) => { - switch(operation) { + switch (operation) { case 'mask': - return { type: 'mask' , fn: p13nFunctions.mask(ctx, instruction) }; + return { type: 'mask', fn: p13nFunctions.mask(ctx, instruction) }; case 'sort': return { type: 'sort', fn: p13nFunctions.addSort(ctx, instruction) }; + case 'filter': + return { type: 'filter', fn: p13nFunctions.addFilter(ctx, instruction) }; default: return { type: `${operation}:noop`, fn: p13nFunctions.noop } } @@ -1102,26 +1124,26 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { } else { tasks = Object.entries(instructions).map(([operation, instruction]) => { - switch(operation) { + switch (operation) { case 'fieldReplace': return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction) }; case 'fieldValueReplace': - return { type: 'fieldValueReplace', fn: p13nFunctions.fieldValueReplace(instruction) }; + return { type: 'fieldValueReplace', fn: p13nFunctions.fieldValueReplace(instruction) }; default: return { type: operation, fn: p13nFunctions.noop } } }); } - let asyncIterator = function( { type, fn }, done) { + let asyncIterator = function ({ type, fn }, done) { log.debug(ctx, `${isBeforeRemote ? "beforeRemote" : "afterRemote"}: applying function - ${type}`); - fn(data, function(err){ + fn(data, function (err) { done(err); }); }; - async.each(tasks.sort(sortFactoryFn(isBeforeRemote)), asyncIterator, function asyncEachCb(err){ - if(err) { + async.each(tasks.sort(sortFactoryFn(isBeforeRemote)), asyncIterator, function asyncEachCb(err) { + if (err) { done(err) } else { @@ -1133,35 +1155,35 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { function checkRelationAndRecurse(Model, data, personalizationOptions, done) { - if(Model.definition.relations.length > 0) { + if (Model.definition.relations.length > 0) { throw Error('not implemented'); } //! no relations - nextTick(function() { + nextTick(function () { done(); }); } function applyServicePersonalization(modelName, data, personalizationOptions, done) { let { isBeforeRemote, context } = personalizationOptions; - let findQuery = { where: { modelName, disabled: false }}; + let findQuery = { where: { modelName, disabled: false } }; let Model = loopback.findModel(modelName); // console.log(Model.definition) let callContext = context.req.callContext; - PersonalizationRule.find(findQuery, callContext, function(err, entries) { - if(err) { + PersonalizationRule.find(findQuery, callContext, function (err, entries) { + if (err) { done(err); } else { // let { instructions } = entries[0]; // personalize(reverse, instructions, data, done); - if(entries.length == 0) { + if (entries.length == 0) { //! not needed to personalize here, //! however we need to check for related //! model - checkRelationAndRecurse(Model, data, personalizationOptions, function(err) { - if(err) { + checkRelationAndRecurse(Model, data, personalizationOptions, function (err) { + if (err) { done(err) } else { @@ -1170,13 +1192,13 @@ function applyServicePersonalization(modelName, data, personalizationOptions, do }); } else { - personalize(context, isBeforeRemote, entries[0].personalizationRule, data, function(err) { - if(err) { + personalize(context, isBeforeRemote, entries[0].personalizationRule, data, function (err) { + if (err) { done(err); } else { - checkRelationAndRecurse(Model, data, personalizationOptions, function(err) { - if(err) { + checkRelationAndRecurse(Model, data, personalizationOptions, function (err) { + if (err) { done(err); } else { From 53d8806a9b9a152ce892883208e315a8ce8a6d62 Mon Sep 17 00:00:00 2001 From: deostroll Date: Fri, 24 Apr 2020 22:20:57 +0530 Subject: [PATCH 13/80] support for reverse field replace --- .../mixins/service-personalization-mixin.js | 9 ++-- lib/service-personalizer.js | 54 +++++++++++++++++-- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index 2c6029d..5ea637b 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -41,7 +41,7 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { if (ctxInfo.isStatic) { switch (ctxInfo.methodName) { case 'create': - data = ctx.instance + data = ctx.result; break; case 'find': data = ctx.result; @@ -66,7 +66,7 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { }); } - log.debug(callCtx, `afterRemote: Unhandled non-static: ${ctx.methodString}`); + log.debug(ctx, `afterRemote: Unhandled non-static: ${ctx.methodString}`); nextTick(next); }); @@ -81,10 +81,13 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { ctxInfo = parseMethodString(ctx.methodString); if(ctxInfo.isStatic) { - switch(ctxInfo.methodString) { + switch(ctxInfo.methodName) { case 'find': data = {} break; + case 'create': + data = ctx.req.body; + break; default: data = {} log.debug(ctx, `beforeRemote: Unhandled ${ctx.methodString}`); diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 7ce67e2..2bf67e4 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -915,10 +915,22 @@ const utils = { // const ALT_DOT = '\uFF0E' const p13nFunctions = { - fieldReplace(replacements) { - return function (data, callback) { - let replaceRecord = utils.replaceRecordFactory(utils.replaceField); + /** + * Does field replace. + * + * PreApplication: Yes + * PostApplication: Yes + * + * For pre-appilication a reverse rule is applied. + * @param {object} replacements + * @param {boolean} isBeforeRemote + */ + fieldReplace(replacements, isBeforeRemote = false) { //Tests: t1, t15 + + let replaceRecord = utils.replaceRecordFactory(utils.replaceField); + + let process = function(replacements, data, cb) { if (Array.isArray(data)) { let updatedResult = data.map(record => { return replaceRecord(record, replacements); @@ -929,7 +941,33 @@ const p13nFunctions = { data = replaceRecord(data, replacements); } - nextTick(callback); + nextTick(cb); + }; + + if(isBeforeRemote) { + return function(data, callback) { + var revInputJson = {}; + var rule = replacements; + for (var key in rule) { + if (rule.hasOwnProperty(key)) { + var pos = key.lastIndexOf('\uFF0E'); + if (pos !== -1) { + var replaceAttr = key.substr(pos + 1); + var elsePart = key.substr(0, pos + 1); + revInputJson[elsePart + rule[key]] = replaceAttr; + } else { + revInputJson[rule[key]] = key; + } + } + } + // fieldReplacementFn(ctx, revInputJson, cb); + process(revInputJson, data, callback); + } + } + + // ! for afterRemote case + return function (data, callback) { + process(replacements, data, callback); } }, @@ -1090,6 +1128,10 @@ const p13nFunctions = { addFilter(ctx, instruction) {// Tests: t9 return function (data, callback) { utils.noop(data); + + //TODO: (Arun 2020-04-24 20:16:02) - how to check for datasource support? + + //TODO: (Arun 2020-04-24 20:16:47) - implement in-memory filter if datasource unsupported var dsSupportFilter = true; if (dsSupportFilter) { @@ -1117,6 +1159,8 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { return { type: 'sort', fn: p13nFunctions.addSort(ctx, instruction) }; case 'filter': return { type: 'filter', fn: p13nFunctions.addFilter(ctx, instruction) }; + case 'fieldReplace': + return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction, true)} default: return { type: `${operation}:noop`, fn: p13nFunctions.noop } } @@ -1130,7 +1174,7 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { case 'fieldValueReplace': return { type: 'fieldValueReplace', fn: p13nFunctions.fieldValueReplace(instruction) }; default: - return { type: operation, fn: p13nFunctions.noop } + return { type: `${operation}:noop`, fn: p13nFunctions.noop } } }); } From 9643d853ed22d615979a65106cd7f58b9d9a9709 Mon Sep 17 00:00:00 2001 From: deostroll Date: Sat, 25 Apr 2020 08:45:17 +0530 Subject: [PATCH 14/80] - added lbFilter support - added findById static method in afterRemote - fixed a bug in field value replace --- .../mixins/service-personalization-mixin.js | 9 ++++-- lib/service-personalizer.js | 29 +++++++++++++++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index 5ea637b..39f94e8 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -46,6 +46,9 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { case 'find': data = ctx.result; break; + case 'findById': + data = ctx.result; + break; default: log.debug(ctx, `afterRemote: Unhandled: ${ctx.methodString}`); data = {} @@ -82,9 +85,9 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { if(ctxInfo.isStatic) { switch(ctxInfo.methodName) { - case 'find': - data = {} - break; + // case 'find': + // data = {} + // break; case 'create': data = ctx.req.body; break; diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 2bf67e4..2c01fc8 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -882,7 +882,7 @@ const utils = { record[key] = newValue; } } else if (typeof record[key] !== 'undefined' && typeof record[key] === 'object') { - replaceValue(record[key], elsePart, value); + utils.replaceValue(record[key], elsePart, value); } else if (typeof record[key] !== 'undefined' && typeof record[key] !== 'object') { if (value.hasOwnProperty(record[key])) { if (typeof record.__data !== 'undefined') { @@ -926,7 +926,7 @@ const p13nFunctions = { * @param {object} replacements * @param {boolean} isBeforeRemote */ - fieldReplace(replacements, isBeforeRemote = false) { //Tests: t1, t15 + fieldReplace(replacements, isBeforeRemote = false) { //Tests: t1, t15, t17 let replaceRecord = utils.replaceRecordFactory(utils.replaceField); @@ -971,7 +971,15 @@ const p13nFunctions = { } }, - fieldValueReplace(replacements) { + /** + * does a field value replace. + * + * PreApplication: no + * PostApplication: yes + * + * @param {object} replacements - replacement rule + */ + fieldValueReplace(replacements) { //Tests: t22, t20, t19, t18, t17, t16, t3, t23 return function (data, callback) { let replaceRecord = utils.replaceRecordFactory(utils.replaceValue); @@ -1138,6 +1146,19 @@ const p13nFunctions = { addWhereClause(ctx, instruction, callback); } } + }, + + /** + * Adds a loopback filter clause + * + * @param {HttpContext} ctx - context + * @param {object} instruction - the rule + */ + addLbFilter(ctx, instruction) { //Tests: t21 + return function(data, callback) { + utils.noop(data); + addLbFilter(ctx,instruction,callback); + } } } /** @@ -1161,6 +1182,8 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { return { type: 'filter', fn: p13nFunctions.addFilter(ctx, instruction) }; case 'fieldReplace': return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction, true)} + case 'lbFilter': + return { type: 'lbFilter', fn: p13nFunctions.addLbFilter(ctx, instruction) }; default: return { type: `${operation}:noop`, fn: p13nFunctions.noop } } From 1ff72a0ab119e102d2a8dd2c36900c1437d70f6b Mon Sep 17 00:00:00 2001 From: deostroll Date: Sat, 25 Apr 2020 20:33:35 +0530 Subject: [PATCH 15/80] initial support state --- lib/service-personalizer.js | 111 ++++- server/boot/service-personalization.js | 2 + server/boot/service-personalization.js.0.txt | 408 +++++++++++++++++++ test/test.js | 32 +- 4 files changed, 531 insertions(+), 22 deletions(-) create mode 100644 server/boot/service-personalization.js.0.txt diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 2c01fc8..fe75cba 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -927,10 +927,10 @@ const p13nFunctions = { * @param {boolean} isBeforeRemote */ fieldReplace(replacements, isBeforeRemote = false) { //Tests: t1, t15, t17 - + let replaceRecord = utils.replaceRecordFactory(utils.replaceField); - - let process = function(replacements, data, cb) { + + let process = function (replacements, data, cb) { if (Array.isArray(data)) { let updatedResult = data.map(record => { return replaceRecord(record, replacements); @@ -944,8 +944,8 @@ const p13nFunctions = { nextTick(cb); }; - if(isBeforeRemote) { - return function(data, callback) { + if (isBeforeRemote) { + return function (data, callback) { var revInputJson = {}; var rule = replacements; for (var key in rule) { @@ -1155,10 +1155,95 @@ const p13nFunctions = { * @param {object} instruction - the rule */ addLbFilter(ctx, instruction) { //Tests: t21 + return function (data, callback) { + utils.noop(data); + addLbFilter(ctx, instruction, callback); + } + }, + + /** + * applies masking values in a field. E.g + * masking first few digits of the phone + * number + * + * @param {object} instruction - mask instructions + */ + addFieldMask(charMaskRules) { //Test t24, t25, t26, t27, t28, t29 + return function (data, callback) { + var input = data; + + function modifyField(record, property, rule) { + var pos = property.indexOf('.'); + if (pos !== -1) { + var key = property.substr(0, pos); + var innerProp = property.substr(pos + 1); + } else { + key = property; + } + + if (record[key] && typeof record[key] === 'object') { + modifyField(record[key], innerProp, rule); + } else if (record[key] && typeof record[key] !== 'object') { + var char = rule.maskCharacter || 'X'; + var flags = rule.flags; + var regex = flags ? new RegExp(rule.pattern, flags) : new RegExp(rule.pattern); + var groups = record[key].match(regex) || []; + var masking = rule.mask || []; + var newVal = rule.format || []; + if (Array.isArray(newVal)) { + for (let i = 1; i < groups.length; i++) { + newVal.push('$' + i); + } + newVal = newVal.join(''); + } + masking.forEach(function (elem) { + newVal = newVal.replace(elem, new Array(groups[elem.substr(1)].length + 1).join(char)); + }); + for (let i = 0; i < groups.length; i++) { + newVal = newVal.replace('$' + i, groups[i]); + } + // normally we set __data but now, lb3!!! + record[key] = newVal; + } + } + + function applyRuleOnRecord(record, charMaskRules) { + Object.keys(charMaskRules).forEach(function (key) { + modifyField(record, key, charMaskRules[key]); + }); + return record; + } + + if (Array.isArray(input)) { + var updatedResult = []; + input.forEach(function (record) { + updatedResult.push(applyRuleOnRecord(record, charMaskRules)); + }); + input = updatedResult; + } else { + input = applyRuleOnRecord(input, charMaskRules); + } + + // return cb(); + nextTick(callback); + } + }, + + + /** + * adds the post custom function added via + * config.json to the after remote + * + * PreApplication: no + * PostApplication: yes + * @param {context} ctx + * @param {object} instruction + */ + addCustomFunction(ctx, instruction) { //Tests t35, t36 return function(data, callback) { utils.noop(data); - addLbFilter(ctx,instruction,callback); - } + executeCustomFunction(ctx, instruction, () => nextTick(callback)); + } } } /** @@ -1181,11 +1266,13 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { case 'filter': return { type: 'filter', fn: p13nFunctions.addFilter(ctx, instruction) }; case 'fieldReplace': - return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction, true)} + return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction, true) } case 'lbFilter': return { type: 'lbFilter', fn: p13nFunctions.addLbFilter(ctx, instruction) }; + case 'preCustomFunction': + return { type: 'preCustomFunction', fn: p13nFunctions.addCustomFunction(ctx, instruction)} ; default: - return { type: `${operation}:noop`, fn: p13nFunctions.noop } + return { type: `noop:${operation}`, fn: p13nFunctions.noop } } }); } @@ -1196,8 +1283,12 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction) }; case 'fieldValueReplace': return { type: 'fieldValueReplace', fn: p13nFunctions.fieldValueReplace(instruction) }; + case 'fieldMask': + return { type: 'fieldMask', fn: p13nFunctions.addFieldMask(instruction) }; + case 'postCustomFunction': + return { type: 'postCustomFunction', fn: p13nFunctions.addCustomFunction(ctx, instruction) }; default: - return { type: `${operation}:noop`, fn: p13nFunctions.noop } + return { type: `noop:${operation}`, fn: p13nFunctions.noop } } }); } diff --git a/server/boot/service-personalization.js b/server/boot/service-personalization.js index f1e58e8..dc04027 100644 --- a/server/boot/service-personalization.js +++ b/server/boot/service-personalization.js @@ -21,5 +21,7 @@ var servicePersonalizer = require('../../lib/service-personalizer'); module.exports = function ServicePersonalization(app) { servicePersonalizer.init(app); + let servicePersoConfig = app.get('servicePersonalization'); + servicePersonalizer.loadCustomFunction(require(servicePersoConfig.customFunctionPath)); }; diff --git a/server/boot/service-personalization.js.0.txt b/server/boot/service-personalization.js.0.txt new file mode 100644 index 0000000..345f3d8 --- /dev/null +++ b/server/boot/service-personalization.js.0.txt @@ -0,0 +1,408 @@ +/** + * + * ©2018-2019 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), + * Bangalore, India. All Rights Reserved. + * + */ +/** + * This boot script brings the ability to apply personalization rules to the model. + * + * @memberof Boot Scripts + * @author Pradeep Kumar Tippa + * @name Service Personalization + */ +// TODO: without clean db test cases are not passing, need to clean up test cases. + +var loopback = require('loopback'); +var log = require('oe-logger')('service-personalization'); + +// var messaging = require('../../lib/common/global-messaging'); +var servicePersonalizer = require('../../lib/service-personalizer'); + +var personalizationRuleModel; + +module.exports = function ServicePersonalization(app, cb) { + log.debug(log.defaultContext(), 'In service-personalization.js boot script.'); + personalizationRuleModel = app.models.PersonalizationRule; + // requiring customFunction + let servicePersoConfig = app.get('servicePersonalization'); + if (servicePersoConfig && 'customFunctionPath' in servicePersoConfig) { + try { + servicePersonalizer.loadCustomFunction(require(servicePersoConfig.customFunctionPath)); + } catch (e) { + log.error(log.defaultContext(), 'require customFunction', e); + } + } + // Creating 'before save' and 'after save' observer hooks for PersonlizationRule model + personalizationRuleModel.observe('before save', personalizationRuleBeforeSave); + personalizationRuleModel.observe('after save', personalizationRuleAfterSave); + // Creating filter finding only records where disabled is false. + var filter = { + where: { + disabled: false + } + }; + // Creating options to fetch all records irrespective of scope. + var options = { + ignoreAutoScope: true, + fetchAllScopes: true + }; + // Using fetchAllScopes and ignoreAutoScope to retrieve all the records from DB. i.e. from all tenants. + personalizationRuleModel.find(filter, options, function (err, results) { + log.debug(log.defaultContext(), 'personalizationRuleModel.find executed.'); + if (err) { + log.error(log.defaultContext(), 'personalizationRuleModel.find error. Error', err); + cb(err); + } else if (results && results.length > 0) { + // The below code for the if clause will not executed for test cases with clean/empty DB. + // In order to execute the below code and get code coverage for it we should have + // some rules defined for some models in the database before running tests for coverage. + log.debug(log.defaultContext(), 'Some PersonalizationRules are present, on loading of this PersonalizationRule model'); + for (var i = 0; i < results.length; i++) { + // No need to publish the message to other nodes, since other nodes will attach the hooks on their boot. + // Attaching all models(PersonalizationRule.modelName) before save hooks when PersonalizationRule loads. + // Passing directly modelName without checking existence since it is a mandatory field for PersonalizationRule. + attachRemoteHooksToModel(results[i].modelName, { ctx: results[i]._autoScope }); + } + cb(); + } else { + cb(); + } + }); +}; + +// Subscribing for messages to attach 'before save' hook for modelName model when POST/PUT to PersonalizationRule. +// messaging.subscribe('personalizationRuleAttachHook', function (modelName, options) { +// // TODO: need to enhance test cases for running in cluster and send/recieve messages in cluster. +// log.debug(log.defaultContext(), 'Got message to '); +// attachRemoteHooksToModel(modelName, options); +// }); + +/** + * This function is before save hook for PersonlizationRule model. + * + * @param {object} ctx - Model context + * @param {function} next - callback function + */ +function personalizationRuleBeforeSave(ctx, next) { + var data = ctx.data || ctx.instance; + // It is good to have if we have a declarative way of validating model existence. + var modelName = data.modelName; + if (loopback.findModel(modelName, ctx.options)) { + var nextFlag = true; + if (data.personalizationRule.postCustomFunction) { + if (!(Object.keys(servicePersonalizer.getCustomFunction()).indexOf(data.personalizationRule.postCustomFunction.functionName) > -1) && nextFlag) { + next(new Error('Module \'' + data.personalizationRule.postCustomFunction.functionName + '\' doesn\'t exists.')); + nextFlag = false; + } + } + if (data.personalizationRule.preCustomFunction) { + if (!(Object.keys(servicePersonalizer.getCustomFunction()).indexOf(data.personalizationRule.preCustomFunction.functionName) > -1) && nextFlag) { + next(new Error('Module \'' + data.personalizationRule.precustomFunction.functionName + '\' doesn\'t exists.')); + } + } + if (nextFlag) { + next(); + } + } else { + // Not sure it is the right way to construct error object to sent in the response. + var err = new Error('Model \'' + modelName + '\' doesn\'t exists.'); + next(err); + } +} + +/** + * This function is after save hook for PersonlizationRule model. + * + * @param {object} ctx - Model context + * @param {function} next - callback function + */ +function personalizationRuleAfterSave(ctx, next) { + log.debug(log.defaultContext(), 'personalizationRuleAfterSave method.'); + var data = ctx.data || ctx.instance; + // Publishing message to other nodes in cluster to attach the 'before save' hook for model. + // messaging.publish('personalizationRuleAttachHook', data.modelName, ctx.options); + log.debug(log.defaultContext(), 'personalizationRuleAfterSave data is present. calling attachBeforeSaveHookToModel'); + attachRemoteHooksToModel(data.modelName, ctx.options); + next(); +} + +/** + * This function is to attach remote hooks for given modelName to apply PersonalizationRule. + * + * @param {string} modelName - Model name + * @param {object} options - options + */ +function attachRemoteHooksToModel(modelName, options) { + // Can we avoid this step and get the ModelConstructor from context. + var model = loopback.findModel(modelName, options); + // Setting the flag that Personalization Rule exists, need to check where it will be used. + if (!model.settings._personalizationRuleExists) { + model.settings._personalizationRuleExists = true; + // We can put hook methods in an array an have single function to attach them. + // After Remote hooks + + afterRemoteFindHook(model); + afterRemoteFindByIdHook(model); + afterRemoteFindOneHook(model); + afterRemoteCreateHook(model); + afterRemoteUpsertHook(model); + afterRemoteUpdateAttributesHook(model); + + // Before Remote Hooks + beforeRemoteCreateHook(model); + beforeRemoteUpsertHook(model); + beforeRemoteUpdateAttributesHook(model); + beforeRemoteFindHook(model); + beforeRemoteFindOneHook(model); + beforeRemoteFindByIdHook(model); + // model.beforeRemote('**', function(ctx, next) { + + // }); + } +} + +/** + * This function is to attach after remote hook for find for given model. + * + * @param {object} model - Model constructor object. + */ +function afterRemoteFindHook(model) { + model.afterRemote('find', function (ctx, modelInstance, next) { + log.debug(ctx.req.callContext, 'afterRemoteFindHook for ', model.modelName, ' called'); + afterRemotePersonalizationExec(model, ctx, next); + }); +} + +/** + * This function is to attach after remote hook for findById for given model. + * + * @param {object} model - Model constructor object. + */ +function afterRemoteFindByIdHook(model) { + model.afterRemote('findById', function (ctx, modelInstance, next) { + log.debug(ctx.req.callContext, 'afterRemoteFindByIdHook for ', model.modelName, ' called'); + afterRemotePersonalizationExec(model, ctx, next); + }); +} + +/** + * This function is to attach after remote hook for findOne for given model. + * + * @param {object} model - Model constructor object. + */ +function afterRemoteFindOneHook(model) { + model.afterRemote('findOne', function (ctx, modelInstance, next) { + log.debug(ctx.req.callContext, 'afterRemoteFindOneHook for ', model.modelName, ' called'); + afterRemotePersonalizationExec(model, ctx, next); + }); +} + +/** + * This function is to attach after remote hook for create for given model. + * + * @param {object} model - Model constructor object. + */ +function afterRemoteCreateHook(model) { + model.afterRemote('create', function (ctx, modelInstance, next) { + log.debug(ctx.req.callContext, 'afterRemoteCreateHook for ', model.modelName, ' called'); + afterRemotePersonalizationExec(model, ctx, next); + }); +} + +/** + * This function is to attach after remote hook for upsert for given model. + * + * @param {object} model - Model constructor object. + */ +function afterRemoteUpsertHook(model) { + model.afterRemote('upsert', function (ctx, modelInstance, next) { + log.debug(ctx.req.callContext, 'afterRemoteUpsertHook for ', model.modelName, ' called'); + afterRemotePersonalizationExec(model, ctx, next); + }); +} + +/** + * This function is to attach after remote hook for updateAttributes for given model. + * + * @param {object} model - Model constructor object. + */ +function afterRemoteUpdateAttributesHook(model) { + model.afterRemote('prototype.updateAttributes', function (ctx, modelInstance, next) { + log.debug(ctx.req.callContext, 'afterRemoteUpdateAttributes for ', model.modelName, ' called'); + afterRemotePersonalizationExec(model, ctx, next); + }); +} + +/** + * This function is to attach before remote hook for create for given model. + * + * @param {object} model - Model constructor object. + */ +function beforeRemoteCreateHook(model) { + model.beforeRemote('create', function (ctx, modelInstance, next) { + log.debug(ctx.req.callContext, 'beforeRemoteCreateHook for ', model.modelName, ' called'); + beforeRemotePersonalizationExec(model, ctx, next); + }); +} + +/** + * This function is to attach before remote hook for upsert for given model. + * + * @param {object} model - Model constructor object. + */ +function beforeRemoteUpsertHook(model) { + model.beforeRemote('upsert', function (ctx, modelInstance, next) { + log.debug(ctx.req.callContext, 'beforeRemoteUpsertHook for ', model.modelName, ' called'); + beforeRemotePersonalizationExec(model, ctx, next); + }); +} + +/** + * This function is to attach before remote hook for updateAttributes for given model. + * + * @param {object} model - Model constructor object. + */ +function beforeRemoteUpdateAttributesHook(model) { + model.beforeRemote('prototype.updateAttributes', function (ctx, modelInstance, next) { + log.debug(ctx.req.callContext, 'beforeRemoteUpdateAttributesHook for ', model.modelName, ' called'); + beforeRemotePersonalizationExec(model, ctx, next); + }); +} + +/** + * This function is to attach before remote hook for find for given model + * and modify ctx.args.filter if any corresponding personalization rule is there. + * + * @param {object} model - Model constructor object. + */ +function beforeRemoteFindHook(model) { + model.beforeRemote('find', function (ctx, modelInstance, next) { + log.debug(ctx.req.callContext, 'beforeRemoteFindHook ', model.modelName, 'called'); + servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationAccessHookGetRuleCb(rule) { + if (rule !== null && typeof rule !== 'undefined') { + log.debug(ctx.req.callContext, 'beforeRemoteFindHook personalization rule found , rule: ', rule); + var fns = servicePersonalizer.applyPersonalizationRule(ctx, rule.personalizationRule); + fns = fns.filter(x => x.type !== 'postCustomFunction'); + fns = fns.map(x => x.fn); + servicePersonalizer.execute(fns, function (err) { + if (err) { + return next(err); + } + log.debug(ctx.req.callContext, 'filter', ctx.args.filter); + next(); + }); + } else { + log.debug(ctx.req.callContext, 'beforeRemoteFindHook no rules were found'); + next(); + } + }); + }); +} + +function beforeRemoteFindOneHook(model) { + model.beforeRemote('findOne', function (ctx, modelInstance, next) { + log.debug(ctx.req.callContext, 'beforeRemoteFindOneHook ', model.modelName, 'called'); + servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationAccessHookGetRuleCb(rule) { + if (rule !== null && typeof rule !== 'undefined') { + log.debug(ctx.req.callContext, 'beforeRemoteFindOneHook personalization rule found , rule: ', rule); + var fns = servicePersonalizer.applyPersonalizationRule(ctx, rule.personalizationRule); + fns = fns.filter(x => x.type !== 'postCustomFunction'); + fns = fns.map(x => x.fn); + servicePersonalizer.execute(fns, function (err) { + if (err) { + return next(err); + } + log.debug(ctx.req.callContext, 'filter', ctx.args.filter); + next(); + }); + } else { + log.debug(ctx.req.callContext, 'beforeRemoteFindOneHook no rules were found'); + next(); + } + }); + }); +} + +function beforeRemoteFindByIdHook(model) { + model.beforeRemote('findById', function (ctx, modelInstance, next) { + log.debug(ctx.req.callContext, 'beforeRemoteFindByIdHook ', model.modelName, 'called'); + servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationAccessHookGetRuleCb(rule) { + if (rule !== null && typeof rule !== 'undefined') { + log.debug(ctx.req.callContext, 'beforeRemoteFindByIdHook personalization rule found , rule: ', rule); + var fns = servicePersonalizer.applyPersonalizationRule(ctx, rule.personalizationRule); + fns = fns.filter(x => x.type !== 'postCustomFunction'); + fns = fns.map(x => x.fn); + servicePersonalizer.execute(fns, function (err) { + if (err) { + return next(err); + } + log.debug(ctx.req.callContext, 'filter', ctx.args.filter); + next(); + }); + } else { + log.debug(ctx.req.callContext, 'beforeRemoteFindByIdHook no rules were found'); + next(); + } + }); + }); +} + +/** + * This function is to do the execution personalization rules of after remote hook for given model. + * + * @param {object} model - Model constructor object. + * @param {object} ctx - context object. + * @param {function} next - callback function. + */ +function afterRemotePersonalizationExec(model, ctx, next) { + log.debug(ctx.req.callContext, 'afterRemotePersonalizationExec for ', model.modelName, ' called.'); + servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationMixinBeforeCreateGetReverse(rule) { + if (rule !== null && typeof rule !== 'undefined') { + log.debug(ctx.req.callContext, 'afterRemotePersonalizationExec personalization rule found , rule: ', rule); + log.debug(ctx.req.callContext, 'applying PersonalizationRule now'); + log.debug(ctx.req.callContext, 'beforeRemoteFindHook personalization rule found , rule: ', rule); + var fns = servicePersonalizer.applyPersonalizationRule(ctx, rule.personalizationRule); + fns = fns.map(x => x.fn); + servicePersonalizer.execute(fns, function (err) { + if (err) { + return next(err); + } + log.debug(ctx.req.callContext, 'filter', ctx.args.filter); + next(); + }); + } else { + log.debug(ctx.req.callContext, 'afterRemotePersonalizationExec no rules were found'); + next(); + } + }); +} + +/** + * This function is to do the execution personalization rules of before remote hook for given model. + * + * @param {object} model - Model constructor object. + * @param {object} ctx - context object. + * @param {function} next - callback function. + */ +function beforeRemotePersonalizationExec(model, ctx, next) { + log.debug(ctx.req.callContext, 'beforeRemotePersonalizationExec for ', model.modelName, ' called.'); + servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationMixinBeforeCreateGetReverse(rule) { + if (rule !== null && typeof rule !== 'undefined') { + log.debug(ctx.req.callContext, 'beforeRemotePersonalizationExec personalization rule found , rule: ', rule); + log.debug(ctx.req.callContext, 'applying PersonalizationRule now'); + var fns = servicePersonalizer.applyReversePersonalizationRule(ctx, rule.personalizationRule); + fns = fns.map(x => x.fn); + servicePersonalizer.execute(fns, function (err) { + if (err) { + return next(err); + } + log.debug(ctx.req.callContext, 'filter', ctx.args.filter); + next(); + }); + } else { + log.debug(ctx.req.callContext, 'beforeRemotePersonalizationExec no rules were found'); + next(); + } + }); +} diff --git a/test/test.js b/test/test.js index c6d3ec8..e817f51 100755 --- a/test/test.js +++ b/test/test.js @@ -159,7 +159,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it.only('t1 should replace field names in response when fieldReplace personalization is configured', function (done) { + it('t1 should replace field names in response when fieldReplace personalization is configured', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -211,7 +211,7 @@ describe(chalk.blue('service personalization test started...'), function () { 'id': 2 }; - it.only('t2 create records in product owners', function (done) { + it('t2 create records in product owners', function (done) { ProductOwner = loopback.findModel('ProductOwner'); ProductOwner.create(owner1, function (err) { if (err) { @@ -223,7 +223,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it.only('t3 should replace field names in response when fieldReplace personalization is configured', function (done) { + it('t3 should replace field names in response when fieldReplace personalization is configured', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -261,7 +261,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); // sort test cases - it.only('t4 single sort condition: should return the sorted result when sort personalization rule is configured.', function (done) { + it('t4 single sort condition: should return the sorted result when sort personalization rule is configured.', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -295,7 +295,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it.only('t5 single sort condition: should sort in ascending order when the sort order is not specified', function (done) { + it('t5 single sort condition: should sort in ascending order when the sort order is not specified', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -333,7 +333,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it.only('t6 single sort condition: should accept the keywords like asc,ascending,desc or descending as sort order', function (done) { + it('t6 single sort condition: should accept the keywords like asc,ascending,desc or descending as sort order', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -368,7 +368,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it.only('t7 smultiple sort condition: should return sorted result when personalization rule with multiple sort is configured', function (done) { + it('t7 smultiple sort condition: should return sorted result when personalization rule with multiple sort is configured', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -406,7 +406,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it.only('t8 multiple sort condition: should omit the sort expression whose order value(ASC|DSC) doesnt match the different cases', function (done) { + it('t8 multiple sort condition: should omit the sort expression whose order value(ASC|DSC) doesnt match the different cases', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -487,7 +487,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it.only('t10 multiple sort: should handle duplicate sort expressions', function (done) { + it('t10 multiple sort: should handle duplicate sort expressions', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -522,7 +522,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it.only('t11 multiple sort: should handle clashing sort expressions.(Eg:name ASC in personalization rule and name DESC from API, in this case consider name DESC from API)', + it('t11 multiple sort: should handle clashing sort expressions.(Eg:name ASC in personalization rule and name DESC from API, in this case consider name DESC from API)', function (done) { // Setup personalization rule var ruleForAndroid = { @@ -599,7 +599,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it.only('t13 Mask:should mask the given fields and not send them to the response', function (done) { + it('t13 Mask:should mask the given fields and not send them to the response', function (done) { // Setup personalization rule var ruleForAndroid = { 'modelName': 'ProductCatalog', @@ -721,7 +721,8 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t16 should replace field value names while posting when fieldValueReplace personalization is configured', + // TODO: (Arun - 2020-04-24 22:34:58) Is it meant to demonstrate reverse field value replace? + xit('t16 should replace field value names while posting when fieldValueReplace personalization is configured', function (done) { // Setup personalization rule var ruleForAndroid = { @@ -1057,6 +1058,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); + //TODO: (Arun 2020-04-24 22:40:29) - lbFilter should it be there in the first place? it('t21 should give filterd result when lbFilter is applied', function (done) { // Setup personalization rule var ruleForAndroid = { @@ -1437,6 +1439,9 @@ describe(chalk.blue('service personalization test started...'), function () { state: { type: 'string' } + }, + mixins: { + ServicePersonalizationMixin: true } }; @@ -1452,6 +1457,9 @@ describe(chalk.blue('service personalization test started...'), function () { model: 'Address', property: 'billingAddress' } + }, + mixins: { + ServicePersonalizationMixin: true } }; From bc54b2b0a9946fcbd18f36a8f7bde8aa81cc80de Mon Sep 17 00:00:00 2001 From: deostroll Date: Sat, 25 Apr 2020 20:34:51 +0530 Subject: [PATCH 16/80] removed unwanted file --- server/boot/service-personalization.js.0.txt | 408 ------------------- 1 file changed, 408 deletions(-) delete mode 100644 server/boot/service-personalization.js.0.txt diff --git a/server/boot/service-personalization.js.0.txt b/server/boot/service-personalization.js.0.txt deleted file mode 100644 index 345f3d8..0000000 --- a/server/boot/service-personalization.js.0.txt +++ /dev/null @@ -1,408 +0,0 @@ -/** - * - * ©2018-2019 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), - * Bangalore, India. All Rights Reserved. - * - */ -/** - * This boot script brings the ability to apply personalization rules to the model. - * - * @memberof Boot Scripts - * @author Pradeep Kumar Tippa - * @name Service Personalization - */ -// TODO: without clean db test cases are not passing, need to clean up test cases. - -var loopback = require('loopback'); -var log = require('oe-logger')('service-personalization'); - -// var messaging = require('../../lib/common/global-messaging'); -var servicePersonalizer = require('../../lib/service-personalizer'); - -var personalizationRuleModel; - -module.exports = function ServicePersonalization(app, cb) { - log.debug(log.defaultContext(), 'In service-personalization.js boot script.'); - personalizationRuleModel = app.models.PersonalizationRule; - // requiring customFunction - let servicePersoConfig = app.get('servicePersonalization'); - if (servicePersoConfig && 'customFunctionPath' in servicePersoConfig) { - try { - servicePersonalizer.loadCustomFunction(require(servicePersoConfig.customFunctionPath)); - } catch (e) { - log.error(log.defaultContext(), 'require customFunction', e); - } - } - // Creating 'before save' and 'after save' observer hooks for PersonlizationRule model - personalizationRuleModel.observe('before save', personalizationRuleBeforeSave); - personalizationRuleModel.observe('after save', personalizationRuleAfterSave); - // Creating filter finding only records where disabled is false. - var filter = { - where: { - disabled: false - } - }; - // Creating options to fetch all records irrespective of scope. - var options = { - ignoreAutoScope: true, - fetchAllScopes: true - }; - // Using fetchAllScopes and ignoreAutoScope to retrieve all the records from DB. i.e. from all tenants. - personalizationRuleModel.find(filter, options, function (err, results) { - log.debug(log.defaultContext(), 'personalizationRuleModel.find executed.'); - if (err) { - log.error(log.defaultContext(), 'personalizationRuleModel.find error. Error', err); - cb(err); - } else if (results && results.length > 0) { - // The below code for the if clause will not executed for test cases with clean/empty DB. - // In order to execute the below code and get code coverage for it we should have - // some rules defined for some models in the database before running tests for coverage. - log.debug(log.defaultContext(), 'Some PersonalizationRules are present, on loading of this PersonalizationRule model'); - for (var i = 0; i < results.length; i++) { - // No need to publish the message to other nodes, since other nodes will attach the hooks on their boot. - // Attaching all models(PersonalizationRule.modelName) before save hooks when PersonalizationRule loads. - // Passing directly modelName without checking existence since it is a mandatory field for PersonalizationRule. - attachRemoteHooksToModel(results[i].modelName, { ctx: results[i]._autoScope }); - } - cb(); - } else { - cb(); - } - }); -}; - -// Subscribing for messages to attach 'before save' hook for modelName model when POST/PUT to PersonalizationRule. -// messaging.subscribe('personalizationRuleAttachHook', function (modelName, options) { -// // TODO: need to enhance test cases for running in cluster and send/recieve messages in cluster. -// log.debug(log.defaultContext(), 'Got message to '); -// attachRemoteHooksToModel(modelName, options); -// }); - -/** - * This function is before save hook for PersonlizationRule model. - * - * @param {object} ctx - Model context - * @param {function} next - callback function - */ -function personalizationRuleBeforeSave(ctx, next) { - var data = ctx.data || ctx.instance; - // It is good to have if we have a declarative way of validating model existence. - var modelName = data.modelName; - if (loopback.findModel(modelName, ctx.options)) { - var nextFlag = true; - if (data.personalizationRule.postCustomFunction) { - if (!(Object.keys(servicePersonalizer.getCustomFunction()).indexOf(data.personalizationRule.postCustomFunction.functionName) > -1) && nextFlag) { - next(new Error('Module \'' + data.personalizationRule.postCustomFunction.functionName + '\' doesn\'t exists.')); - nextFlag = false; - } - } - if (data.personalizationRule.preCustomFunction) { - if (!(Object.keys(servicePersonalizer.getCustomFunction()).indexOf(data.personalizationRule.preCustomFunction.functionName) > -1) && nextFlag) { - next(new Error('Module \'' + data.personalizationRule.precustomFunction.functionName + '\' doesn\'t exists.')); - } - } - if (nextFlag) { - next(); - } - } else { - // Not sure it is the right way to construct error object to sent in the response. - var err = new Error('Model \'' + modelName + '\' doesn\'t exists.'); - next(err); - } -} - -/** - * This function is after save hook for PersonlizationRule model. - * - * @param {object} ctx - Model context - * @param {function} next - callback function - */ -function personalizationRuleAfterSave(ctx, next) { - log.debug(log.defaultContext(), 'personalizationRuleAfterSave method.'); - var data = ctx.data || ctx.instance; - // Publishing message to other nodes in cluster to attach the 'before save' hook for model. - // messaging.publish('personalizationRuleAttachHook', data.modelName, ctx.options); - log.debug(log.defaultContext(), 'personalizationRuleAfterSave data is present. calling attachBeforeSaveHookToModel'); - attachRemoteHooksToModel(data.modelName, ctx.options); - next(); -} - -/** - * This function is to attach remote hooks for given modelName to apply PersonalizationRule. - * - * @param {string} modelName - Model name - * @param {object} options - options - */ -function attachRemoteHooksToModel(modelName, options) { - // Can we avoid this step and get the ModelConstructor from context. - var model = loopback.findModel(modelName, options); - // Setting the flag that Personalization Rule exists, need to check where it will be used. - if (!model.settings._personalizationRuleExists) { - model.settings._personalizationRuleExists = true; - // We can put hook methods in an array an have single function to attach them. - // After Remote hooks - - afterRemoteFindHook(model); - afterRemoteFindByIdHook(model); - afterRemoteFindOneHook(model); - afterRemoteCreateHook(model); - afterRemoteUpsertHook(model); - afterRemoteUpdateAttributesHook(model); - - // Before Remote Hooks - beforeRemoteCreateHook(model); - beforeRemoteUpsertHook(model); - beforeRemoteUpdateAttributesHook(model); - beforeRemoteFindHook(model); - beforeRemoteFindOneHook(model); - beforeRemoteFindByIdHook(model); - // model.beforeRemote('**', function(ctx, next) { - - // }); - } -} - -/** - * This function is to attach after remote hook for find for given model. - * - * @param {object} model - Model constructor object. - */ -function afterRemoteFindHook(model) { - model.afterRemote('find', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'afterRemoteFindHook for ', model.modelName, ' called'); - afterRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach after remote hook for findById for given model. - * - * @param {object} model - Model constructor object. - */ -function afterRemoteFindByIdHook(model) { - model.afterRemote('findById', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'afterRemoteFindByIdHook for ', model.modelName, ' called'); - afterRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach after remote hook for findOne for given model. - * - * @param {object} model - Model constructor object. - */ -function afterRemoteFindOneHook(model) { - model.afterRemote('findOne', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'afterRemoteFindOneHook for ', model.modelName, ' called'); - afterRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach after remote hook for create for given model. - * - * @param {object} model - Model constructor object. - */ -function afterRemoteCreateHook(model) { - model.afterRemote('create', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'afterRemoteCreateHook for ', model.modelName, ' called'); - afterRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach after remote hook for upsert for given model. - * - * @param {object} model - Model constructor object. - */ -function afterRemoteUpsertHook(model) { - model.afterRemote('upsert', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'afterRemoteUpsertHook for ', model.modelName, ' called'); - afterRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach after remote hook for updateAttributes for given model. - * - * @param {object} model - Model constructor object. - */ -function afterRemoteUpdateAttributesHook(model) { - model.afterRemote('prototype.updateAttributes', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'afterRemoteUpdateAttributes for ', model.modelName, ' called'); - afterRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach before remote hook for create for given model. - * - * @param {object} model - Model constructor object. - */ -function beforeRemoteCreateHook(model) { - model.beforeRemote('create', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'beforeRemoteCreateHook for ', model.modelName, ' called'); - beforeRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach before remote hook for upsert for given model. - * - * @param {object} model - Model constructor object. - */ -function beforeRemoteUpsertHook(model) { - model.beforeRemote('upsert', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'beforeRemoteUpsertHook for ', model.modelName, ' called'); - beforeRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach before remote hook for updateAttributes for given model. - * - * @param {object} model - Model constructor object. - */ -function beforeRemoteUpdateAttributesHook(model) { - model.beforeRemote('prototype.updateAttributes', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'beforeRemoteUpdateAttributesHook for ', model.modelName, ' called'); - beforeRemotePersonalizationExec(model, ctx, next); - }); -} - -/** - * This function is to attach before remote hook for find for given model - * and modify ctx.args.filter if any corresponding personalization rule is there. - * - * @param {object} model - Model constructor object. - */ -function beforeRemoteFindHook(model) { - model.beforeRemote('find', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'beforeRemoteFindHook ', model.modelName, 'called'); - servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationAccessHookGetRuleCb(rule) { - if (rule !== null && typeof rule !== 'undefined') { - log.debug(ctx.req.callContext, 'beforeRemoteFindHook personalization rule found , rule: ', rule); - var fns = servicePersonalizer.applyPersonalizationRule(ctx, rule.personalizationRule); - fns = fns.filter(x => x.type !== 'postCustomFunction'); - fns = fns.map(x => x.fn); - servicePersonalizer.execute(fns, function (err) { - if (err) { - return next(err); - } - log.debug(ctx.req.callContext, 'filter', ctx.args.filter); - next(); - }); - } else { - log.debug(ctx.req.callContext, 'beforeRemoteFindHook no rules were found'); - next(); - } - }); - }); -} - -function beforeRemoteFindOneHook(model) { - model.beforeRemote('findOne', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'beforeRemoteFindOneHook ', model.modelName, 'called'); - servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationAccessHookGetRuleCb(rule) { - if (rule !== null && typeof rule !== 'undefined') { - log.debug(ctx.req.callContext, 'beforeRemoteFindOneHook personalization rule found , rule: ', rule); - var fns = servicePersonalizer.applyPersonalizationRule(ctx, rule.personalizationRule); - fns = fns.filter(x => x.type !== 'postCustomFunction'); - fns = fns.map(x => x.fn); - servicePersonalizer.execute(fns, function (err) { - if (err) { - return next(err); - } - log.debug(ctx.req.callContext, 'filter', ctx.args.filter); - next(); - }); - } else { - log.debug(ctx.req.callContext, 'beforeRemoteFindOneHook no rules were found'); - next(); - } - }); - }); -} - -function beforeRemoteFindByIdHook(model) { - model.beforeRemote('findById', function (ctx, modelInstance, next) { - log.debug(ctx.req.callContext, 'beforeRemoteFindByIdHook ', model.modelName, 'called'); - servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationAccessHookGetRuleCb(rule) { - if (rule !== null && typeof rule !== 'undefined') { - log.debug(ctx.req.callContext, 'beforeRemoteFindByIdHook personalization rule found , rule: ', rule); - var fns = servicePersonalizer.applyPersonalizationRule(ctx, rule.personalizationRule); - fns = fns.filter(x => x.type !== 'postCustomFunction'); - fns = fns.map(x => x.fn); - servicePersonalizer.execute(fns, function (err) { - if (err) { - return next(err); - } - log.debug(ctx.req.callContext, 'filter', ctx.args.filter); - next(); - }); - } else { - log.debug(ctx.req.callContext, 'beforeRemoteFindByIdHook no rules were found'); - next(); - } - }); - }); -} - -/** - * This function is to do the execution personalization rules of after remote hook for given model. - * - * @param {object} model - Model constructor object. - * @param {object} ctx - context object. - * @param {function} next - callback function. - */ -function afterRemotePersonalizationExec(model, ctx, next) { - log.debug(ctx.req.callContext, 'afterRemotePersonalizationExec for ', model.modelName, ' called.'); - servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationMixinBeforeCreateGetReverse(rule) { - if (rule !== null && typeof rule !== 'undefined') { - log.debug(ctx.req.callContext, 'afterRemotePersonalizationExec personalization rule found , rule: ', rule); - log.debug(ctx.req.callContext, 'applying PersonalizationRule now'); - log.debug(ctx.req.callContext, 'beforeRemoteFindHook personalization rule found , rule: ', rule); - var fns = servicePersonalizer.applyPersonalizationRule(ctx, rule.personalizationRule); - fns = fns.map(x => x.fn); - servicePersonalizer.execute(fns, function (err) { - if (err) { - return next(err); - } - log.debug(ctx.req.callContext, 'filter', ctx.args.filter); - next(); - }); - } else { - log.debug(ctx.req.callContext, 'afterRemotePersonalizationExec no rules were found'); - next(); - } - }); -} - -/** - * This function is to do the execution personalization rules of before remote hook for given model. - * - * @param {object} model - Model constructor object. - * @param {object} ctx - context object. - * @param {function} next - callback function. - */ -function beforeRemotePersonalizationExec(model, ctx, next) { - log.debug(ctx.req.callContext, 'beforeRemotePersonalizationExec for ', model.modelName, ' called.'); - servicePersonalizer.getPersonalizationRuleForModel(model.modelName, ctx, function servicePersonalizationMixinBeforeCreateGetReverse(rule) { - if (rule !== null && typeof rule !== 'undefined') { - log.debug(ctx.req.callContext, 'beforeRemotePersonalizationExec personalization rule found , rule: ', rule); - log.debug(ctx.req.callContext, 'applying PersonalizationRule now'); - var fns = servicePersonalizer.applyReversePersonalizationRule(ctx, rule.personalizationRule); - fns = fns.map(x => x.fn); - servicePersonalizer.execute(fns, function (err) { - if (err) { - return next(err); - } - log.debug(ctx.req.callContext, 'filter', ctx.args.filter); - next(); - }); - } else { - log.debug(ctx.req.callContext, 'beforeRemotePersonalizationExec no rules were found'); - next(); - } - }); -} From 08db35fb7a674ba942df6d245006970dd68983d3 Mon Sep 17 00:00:00 2001 From: deostroll Date: Sun, 26 Apr 2020 11:17:48 +0530 Subject: [PATCH 17/80] modified boot scripts and restored tests to original state --- lib/service-personalizer.js | 36 ++++++++++++++++++++++++++ server/boot/service-personalization.js | 2 -- test/test.js | 2 +- 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index fe75cba..5a51cf8 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -1373,11 +1373,47 @@ function applyServicePersonalization(modelName, data, personalizationOptions, do let PersonalizationRule = null; /** * Initializes this module for service personalization + * during applcation boot. Initializes observers on + * PersonalizationRule model. * * @param {Application} app - The Loopback application object */ function init(app) { PersonalizationRule = app.models['PersonalizationRule']; + let servicePersoConfig = app.get('servicePersonalization'); + loadCustomFunction(require(servicePersoConfig.customFunctionPath)); + PersonalizationRule.observe('before save', function(ctx, next){ + log.debug(ctx, 'PersonalizationRule: before save'); + let data = ctx.__data || ctx.instance || ctx.data; + + let model = loopback.findModel(data.modelName); + if(typeof model === 'undefined') { + log.error(ctx, `PersonalizationRule: before save - model "${data.modelName}" is not found`); + return nextTick(function() { + let error = new Error(`Model: ${data.modelName} is not found`); + next(error); + }); + } + let { personalizationRule : { postCustomFunction } } = data; + if(postCustomFunction) { + let { functionName } = postCustomFunction; + if(functionName) { + if(!Object.keys(getCustomFunction()).includes(functionName)) { + return nextTick(function() { + next(new Error(`The custom function with name "${functionName}" does not exist`)); + }) + } + } + else { + return nextTick(function() { + let error = new Error('postCustomFunction not defined with functionName'); + next(error); + }); + } + } + nextTick(next); + }); + } module.exports = { diff --git a/server/boot/service-personalization.js b/server/boot/service-personalization.js index dc04027..f1e58e8 100644 --- a/server/boot/service-personalization.js +++ b/server/boot/service-personalization.js @@ -21,7 +21,5 @@ var servicePersonalizer = require('../../lib/service-personalizer'); module.exports = function ServicePersonalization(app) { servicePersonalizer.init(app); - let servicePersoConfig = app.get('servicePersonalization'); - servicePersonalizer.loadCustomFunction(require(servicePersoConfig.customFunctionPath)); }; diff --git a/test/test.js b/test/test.js index e817f51..e626949 100755 --- a/test/test.js +++ b/test/test.js @@ -722,7 +722,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); // TODO: (Arun - 2020-04-24 22:34:58) Is it meant to demonstrate reverse field value replace? - xit('t16 should replace field value names while posting when fieldValueReplace personalization is configured', + it('t16 should replace field value names while posting when fieldValueReplace personalization is configured', function (done) { // Setup personalization rule var ruleForAndroid = { From 797aac0a1b89cf1ebacd6d304ffeea81bfda395b Mon Sep 17 00:00:00 2001 From: deostroll Date: Sun, 26 Apr 2020 18:43:30 +0530 Subject: [PATCH 18/80] removed unwanted files --- test/common/models/my-model.js | 81 -------------------------------- test/common/models/my-model.json | 32 ------------- 2 files changed, 113 deletions(-) delete mode 100644 test/common/models/my-model.js delete mode 100644 test/common/models/my-model.json diff --git a/test/common/models/my-model.js b/test/common/models/my-model.js deleted file mode 100644 index 6eee421..0000000 --- a/test/common/models/my-model.js +++ /dev/null @@ -1,81 +0,0 @@ -module.exports = function myModelBoot(MyModel) { - // MyModel.beforeRemote('**', function(ctx, unused, next) { - // console.log(`BeforeRemote MethodString: ${ctx.methodString}`); - // process.nextTick(function(){ - // next(); - // }); - // }); - - // MyModel.afterRemote('**', function(ctx, unused, next) { - // console.log(`AfterRemote MethodString: ${ctx.methodString}`); - // process.nextTick(function(){ - // next(); - // }); - // }); - - MyModel.remoteMethod('exec', { - description: 'do something', - accessType: 'WRITE', - accepts: [ - { - arg: 'documentName', - type: 'string', - required: true, - http: { - source: 'path' - }, - description: 'Name of the Document to be fetched from db for rule engine' - }, - { - arg: 'data', - type: 'object', - required: true, - http: { - source: 'body' - }, - description: 'An object on which business rules should be applied' - } - ], - http: { - verb: 'post', - path: '/exec/:documentName' - }, - returns: { - arg: 'data', - type: 'object', - root: true - } - }); - - MyModel.exec = function(docName, data, callback) { - data.docName = docName; - process.nextTick(function(){ - callback(null, data); - }); - }; - - MyModel.remoteMethod('foo', { - description: "foo desc", - isStatic: false, - http: { path: '/foo', verb: 'get' }, - returns: { - arg:'name', type: 'string' - } - }); - - MyModel.prototype.foo = function() { - let args = [].slice.call(arguments); - let callback = args.find(arg => typeof arg === 'function'); - process.nextTick(function() { - callback(null, { "result": "foo"}) - }) - } - - // var toJSON = MyModel.prototype.toJSON - - // MyModel.prototype.toJSON = function() { - // var self = this; - // console.log('This is a patched toJSON call') - // return toJSON.call(self) - // } -} \ No newline at end of file diff --git a/test/common/models/my-model.json b/test/common/models/my-model.json deleted file mode 100644 index 4ab35dc..0000000 --- a/test/common/models/my-model.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "MyModel", - "base": "BaseEntity", - "idInjection": true, - "properties": { - "name": { - "type": "string" - }, - "category": { - "type": "string", - "require": true - }, - "desc": { - "type": "string" - }, - "price": { - "type": "object" - }, - "isAvailable": { - "type": "boolean" - }, - "modelNo": "string", - "keywords": [ - "string" - ] - }, - "validations": [], - "relations": {}, - "acls": [], - "methods": {}, - "strict": true -} \ No newline at end of file From e1bca08daab7a2042ad4cd7bf47238f453829dc0 Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 27 Apr 2020 23:28:04 +0530 Subject: [PATCH 19/80] recursive application --- README.md | 55 ++++ .../mixins/service-personalization-mixin.js | 69 +++- lib/service-personalizer.js | 91 ++++- test/common/models/address-book.json | 19 ++ test/common/models/phone-number.json | 14 + test/common/models/product-catalog.json | 7 +- test/common/models/product-owner.json | 6 +- test/common/models/store-stock.json | 19 ++ test/common/models/store.json | 20 ++ test/model-config.json | 16 +- test/test.js | 311 +++++++++++++++++- 11 files changed, 599 insertions(+), 28 deletions(-) create mode 100644 test/common/models/address-book.json create mode 100644 test/common/models/phone-number.json create mode 100644 test/common/models/store-stock.json create mode 100644 test/common/models/store.json diff --git a/README.md b/README.md index b650d5b..a9dced6 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # oe-service-personalization +This module will apply personalizations such as field masking, hiding fields, sorting, etc on top of traditional remote endpoints. With these limited set of operations it is also possible to personalize data to a group of clients. In other words, the same data can appear (and/or behave differently) on, say, an android app, an ios app, and, a browser app . Such granular segementations are possible by describing them in an property called `scope` on the personalization rule. (This is made possible by the `oe-personalization` module). Further, for such segmented personalizations to take effect, we need the necessary header in the http request (as how it is in the `scope`). + ## dependency * oe-cloud * oe-logger @@ -17,3 +19,56 @@ $ npm run test $ # Run test cases along with code coverage - code coverage report will be available in coverage folder $ npm run grunt-cover ``` + +## Test Synopsis + +The following entity structure and relationships assumed for most of the tests. + +``` + ++-------------------+ +------------------------+ +------------------------+ +| | | | | | +| AddressBook | | PhoneNumber | | ProductCatalog | +| | | | | | ++-------------------+ +------------------------+ +------------------------+ +| line1 : string | | number : number (*) | | name : string | +| line2 : string | | firstName : string | | category : string | +| landmark : string | | lastName : string | | desc : string | +| pincode : string | | | | price : object | ++-------------------+ +------------------------+ | isAvailable : boolean | + | modelNo : string | + | keywords : [string] | + +------------------------+ + + + +-----------------+ +---------------+ + | | | | + | ProductOwner | | Store | + | | | | + +-----------------+ +---------------+ + | name : string | | name : string | + | city : string | | | + +-----------------+ +---------------+ + +========================================================================================= + + +--------------+ + +--------+ ProductOwner +----------+ + | +--------------+ | + (hasMany) (hasOne) + | | + v v ++-------+--------+ +----+----+ +-------------+ +| ProductCatalog | | Address +------(hasMany)----> | PhoneNumber | ++-------+--------+ +----+----+ +-------------+ + ^ ^ + | | + (hasMany) | + | (hasMany) + | +-------+ | + +--------->+ Store +---------------+ + +-------+ + +``` + +Note: All the models have the `ServicePersonalizationMixin` enabled. \ No newline at end of file diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index 39f94e8..7fa47c8 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -27,7 +27,7 @@ const { nextTick, parseMethodString, slice } = require('./../../lib/utils'); module.exports = function ServicePersonalizationMixin(TargetModel) { log.debug(log.defaultContext(), `Applying service personalization for ${TargetModel.definition.name}`); - TargetModel.afterRemote('**', function () { + TargetModel.afterRemote('**', function ServicePersonalizationAfterRemoteHook() { let args = slice(arguments); let ctx = args[0]; @@ -38,28 +38,44 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { ctxInfo = parseMethodString(ctx.methodString); let data = null; + let applyFlag = true; + if (ctxInfo.isStatic) { switch (ctxInfo.methodName) { case 'create': + case 'patchOrCreate': data = ctx.result; break; case 'find': + case 'findById': + case 'findOne': data = ctx.result; break; - case 'findById': + default: + log.debug(ctx, `afterRemote: Unhandled static - ${ctx.methodString}`); + data = {} + applyFlag = false; + } + } + else { + switch(ctxInfo.methodName) { + case 'patchAttributes': data = ctx.result; break; default: - log.debug(ctx, `afterRemote: Unhandled: ${ctx.methodString}`); + log.debug(ctx, `afterRemote: Unhandled non-static - ${ctx.methodString}`); data = {} + applyFlag = false; } + } + if(applyFlag) { let personalizationOptions = { isBeforeRemote: false, context: ctx }; - return applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function(err) { + applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function(err) { if(err) { next(err); } @@ -68,12 +84,13 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { } }); } - - log.debug(ctx, `afterRemote: Unhandled non-static: ${ctx.methodString}`); - nextTick(next); + else { + nextTick(next); + } + }); - TargetModel.beforeRemote('**', function() { + TargetModel.beforeRemote('**', function ServicePersonalizationBeforeRemoteHook() { let args = slice(arguments); let ctx = args[0]; let next = args[args.length - 1]; @@ -82,31 +99,49 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { log.debug(ctx, `beforeRemote: MethodString: ${ctx.methodString}`); ctxInfo = parseMethodString(ctx.methodString); - + let applyFlag = true; if(ctxInfo.isStatic) { switch(ctxInfo.methodName) { - // case 'find': - // data = {} - // break; case 'create': + case 'patchOrCreate': data = ctx.req.body; break; + case 'find': + case 'findById': + case 'findOne': + data = {}; + break; default: data = {} - log.debug(ctx, `beforeRemote: Unhandled ${ctx.methodString}`); + log.debug(ctx, `beforeRemote: Unhandled static: ${ctx.methodString}`); + applyFlag = false; + } + } + else { + switch(ctxInfo.methodName) { + case 'patchAttributes': + data = ctx.req.body; + break; + default: + data = {} + log.debug(ctx, `beforeRemote: Unhandled non-static: ${ctx.methodString}`); + applyFlag = false; } - + } + + if(applyFlag) { let personalizationOptions = { isBeforeRemote: true, context: ctx }; - return applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function(err){ + applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function(err){ next(err); }); } + else { + nextTick(next); + } - log.debug(ctx, `beforeRemote: Unhandled non-static ${ctx.methodString}`); - nextTick(next); }); } \ No newline at end of file diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 5a51cf8..6ea1032 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -1070,8 +1070,6 @@ const p13nFunctions = { * @param {CallContext} ctx - the request context * @param {object} instructions - personalization rule object * - * Tests: t13 - * * PreApplication: No * PostApplication: Yes * @@ -1087,7 +1085,7 @@ const p13nFunctions = { * }; */ mask(ctx, instructions) { - return function (data, callback) { + return function (data, callback) { //Tests: t13 utils.noop(data); let dsSupportMask = true; if (dsSupportMask) { @@ -1300,7 +1298,7 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { }); }; - async.each(tasks.sort(sortFactoryFn(isBeforeRemote)), asyncIterator, function asyncEachCb(err) { + async.eachSeries(tasks.sort(sortFactoryFn(isBeforeRemote)), asyncIterator, function asyncEachCb(err) { if (err) { done(err) } @@ -1312,9 +1310,90 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { function checkRelationAndRecurse(Model, data, personalizationOptions, done) { + let {settings: {relations}, definition: { name } } = Model; + let { isBeforeRemote, context } = personalizationOptions; + let prefix = isBeforeRemote ? 'beforeRemote' : 'afterRemote'; + + // let applyOnRecord = (data, relationName, relModel, done) => { + // let relData = null; + // if(data.__data) { + // relData = data.__data[relationName]; + // } + // else { + // relData = data[relationName]; + // } + // let logMessage = `process relation "${relationName}" data in model "${name}"`; + + // if(typeof relData !== 'undefined' && Array.isArray(relData)) { + // log.debug(context, `${prefix}: ${logMessage}`); + // async.eachSeries(relData, function relationRecordProcessor(relationRecord, cb){ + // applyServicePersonalization(relModel, relationRecord, personalizationOptions, cb); + // }, function relationListItemsAsyncDoneCb(err){ + // done(err); + // }); + // } + // else if(typeof relData !== 'undefined' && typeof relData === 'object') { + // log.debug(context, `${prefix}: ${logMessage}`); + // applyServicePersonalization(relModel, relData, personalizationOptions, done); + // } + // else if(typeof relData === 'undefined' && isBeforeRemote) { + // log.debug(context, `${prefix}: ${logMessage}`); + // applyServicePersonalization(relModel, {}, personalizationOptions, done); + // } + // else { + // log.debug(context, `${prefix}: no data for relation "${relationName}" of model "${name}"`) + // nextTick(done); + // } + // }; + + if (relations) { + // Object.entries(Model.setti) + let relationItems = Object.entries(relations); + let relationsIterator = function relationProcessor([ relationName, relation ], done) { + // if(typeof data !== 'undefined' && Array.isArray(data)) { + // async.eachSeries(data, function dataIterator(record, cb) { + // applyOnRecord(record, relationName, relation.model, cb); + // }, done); + // } + // else if(typeof data !== 'undefined' && typeof data === 'object') { + // applyOnRecord(data, relationName, relation.model, done); + // } + // else { + // nextTick(done); + // } - if (Model.definition.relations.length > 0) { - throw Error('not implemented'); + // check if the related model has personalization + let relData = undefined; + let relModel = relation.model; + let applyFlag = false; + if(Array.isArray(data)) { + relData = data.reduce((carrier, record) => { + if(record.__data && typeof record.__data[relationName] !== 'undefined') { + carrier.push(record.__data[relationName]) + } + return carrier; + }, []); + if(relData.length) { + applyFlag = true; + } + } + else if(data.__data) { + relData = data.__data[relationName]; + applyFlag = !!relData; + } + else if((relData = data[relationName])) { + applyFlag = true; + } + let callback = function (err) { + done(err); + }; + callback.__trace = `${name}_${relationName}`; + if(applyFlag) { + return applyServicePersonalization(relModel, relData, personalizationOptions, callback) + } + nextTick(done); + }; + return async.eachSeries(relationItems, relationsIterator, done); } //! no relations diff --git a/test/common/models/address-book.json b/test/common/models/address-book.json new file mode 100644 index 0000000..140877b --- /dev/null +++ b/test/common/models/address-book.json @@ -0,0 +1,19 @@ +{ + "name":"AddressBook", + "base":"BaseEntity", + "properties": { + "line1": "string", + "line2": "string", + "landmark": "string", + "pincode":"string" + }, + "relations": { + "phones" : { + "type": "hasMany", + "model":"PhoneNumber" + } + }, + "mixins": { + "ServicePersonalizationMixin": true + } +} \ No newline at end of file diff --git a/test/common/models/phone-number.json b/test/common/models/phone-number.json new file mode 100644 index 0000000..73334ec --- /dev/null +++ b/test/common/models/phone-number.json @@ -0,0 +1,14 @@ +{ + "name": "PhoneNumber", + "properties": { + "number": { + "type": "string", + "id": true + }, + "firstName": "string", + "lastName": "string" + }, + "mixins": { + "ServicePersonalizationMixin": true + } +} \ No newline at end of file diff --git a/test/common/models/product-catalog.json b/test/common/models/product-catalog.json index 9606a0f..0ad0b89 100644 --- a/test/common/models/product-catalog.json +++ b/test/common/models/product-catalog.json @@ -25,7 +25,12 @@ ] }, "validations": [], - "relations": {}, + "relations": { + "store":{ + "model":"StoreStock", + "type": "hasOne" + } + }, "acls": [], "methods": {}, "strict": true, diff --git a/test/common/models/product-owner.json b/test/common/models/product-owner.json index 4fb18ec..3bfe704 100644 --- a/test/common/models/product-owner.json +++ b/test/common/models/product-owner.json @@ -16,7 +16,11 @@ "ProductCatalog": { "type": "hasMany", "model": "ProductCatalog" - } + }, + "address": { + "type" : "hasOne", + "model" : "AddressBook" + } }, "acls": [], "methods": {}, diff --git a/test/common/models/store-stock.json b/test/common/models/store-stock.json new file mode 100644 index 0000000..fe1c083 --- /dev/null +++ b/test/common/models/store-stock.json @@ -0,0 +1,19 @@ +{ + "name": "StoreStock", + "description": "Inventory of what is sold in the store", + "properties": { + }, + "relations": { + "store":{ + "type":"belongsTo", + "model": "Store" + }, + "product": { + "type":"belongsTo", + "model":"ProductCatalog" + } + }, + "mixins": { + "ServicePersonalizationMixin": true + } +} \ No newline at end of file diff --git a/test/common/models/store.json b/test/common/models/store.json new file mode 100644 index 0000000..932e236 --- /dev/null +++ b/test/common/models/store.json @@ -0,0 +1,20 @@ +{ + "name": "Store", + "base":"BaseEntity", + "properties": { + "name": "string" + }, + "relations": { + "addresses": { + "model": "AddressBook", + "type": "hasMany" + }, + "products": { + "type":"hasMany", + "model": "ProductCatalog" + } + }, + "mixins": { + "ServicePersonalizationMixin": true + } +} \ No newline at end of file diff --git a/test/model-config.json b/test/model-config.json index 6844f04..7f45354 100644 --- a/test/model-config.json +++ b/test/model-config.json @@ -40,8 +40,20 @@ "dataSource": "db", "public": true }, - "MyModel": { + "AddressBook" : { "dataSource" : "db", - "public" : true + "public": true + }, + "PhoneNumber" : { + "dataSource" : "db", + "public": true + }, + "Store" : { + "dataSource" : "db", + "public": true + }, + "StoreStock" : { + "dataSource" : "db", + "public": true } } diff --git a/test/test.js b/test/test.js index e626949..5bab575 100755 --- a/test/test.js +++ b/test/test.js @@ -31,6 +31,7 @@ describe(chalk.blue('service personalization test started...'), function () { var accessToken; before('wait for boot scripts to complete', function (done) { app.on('test-start', function () { + console.log('booted'); ProductCatalog = loopback.findModel('ProductCatalog'); ProductCatalog.destroyAll(function (err, info) { return done(err); @@ -1421,7 +1422,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - describe('Relation Tests - ', function () { + describe('DOT Tests - ', function () { var AddressModel, CustomerModel; var defContext = {}; before('setup test data', done => { @@ -1723,6 +1724,314 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); }); + + describe('Updation scenarios', function () { + it('t37 should apply personalization during an upsert for the same scope', function (done) { + let putData = { + id: 'watch3', + name: 'MultiTrix - MODEL 1100', + modelNo: '7983211100' + }; + + api.put(productCatalogUrl + '?access_token=' + accessToken) + .set('Accept', 'application/json') + .set('REMOTE_USER', 'testUser') + .set('region', 'kl') + .send(putData) + .expect(200) + .end(function (err, resp) { + if (err) { + done(err) + } + else { + var result = resp.body; + expect(result.id).to.equal(putData.id); + expect(result.name).to.equal(putData.name); + expect(result.modelNo).to.equal('798321XXXX'); + done(); + } + }) + }); + + it('t38 should apply personalization to a single record that is being updated', function (done) { + let putData = { + modelNo: '1234560000' + }; + + let apiUrl = productCatalogUrl + '/watch3' + `?access_token=${accessToken}`; + api.put(apiUrl) + .set('Accept', 'application/json') + .set('REMOTE_USER', 'testUser') + .set('region', 'kl') + .send(putData) + .expect(200) + .end(function (err, resp) { + if (err) { + done(err) + } + else { + let result = resp.body; + expect(result.id).to.equal('watch3'); + expect(result.modelNo).to.equal('123456XXXX'); + done(); + } + }) + }); + }); + + describe.only('Relation tests', function () { + + before('creating the product owner', done => { + let data = { + "name": "Swamy Patanjali", + "city": "Lucknow", + "id": 12 + }; + ProductOwner = loopback.findModel('ProductOwner'); + ProductOwner.create(data, function(err){ + done(err); + }); + }); + + before('creating a catalog', done => { + let data = [ + { + "name" : "Patanjali Paste", + "category": "FMCG", + "desc": "Herbal paste that is all vegan", + "price": {"currency":"INR", "amount": 45 }, + "isAvailable": false, + "keywords": [ "toothpaste", "herbal" ], + "productOwnerId" : 12, + "id": "prod1" + }, + { + "name" : "Patanjali Facial", + "category": "Cosmetics", + "desc": "Ayurvedic cream to get rid of dark spots, pimples, etc", + "price": {"currency":"INR", "amount": 70 }, + "isAvailable": true, + "keywords": [ "face", "herbal", "cream" ], + "productOwnerId" : 12, + "id" : "prod2" + } + ]; + + ProductCatalog.create(data, function(err){ + done(err); + }); + }); + + before('creating stores', done => { + let data = [ + { + "name": "Patanjali Store 1", + "id": "store2" + }, + { + "name": "Patanjali Store 2", + "id": "store1" + } + ]; + let Store = loopback.findModel('Store'); + Store.create(data, function(err) { + done(err); + }); + }); + + before('creating store stock', done => { + let data = [ + {"storeId": "store1", "productCatalogId": "prod1" }, + {"storeId": "store2", "productCatalogId": "prod2" } + ]; + let StoreStock = loopback.findModel('StoreStock'); + StoreStock.create(data, function(err) { + done(err); + }); + }); + + before('create addresses', done => { + let data = [ + { + "line1": "5th ave", + "line2": "Richmond", + "landmark":"Siegel Building", + "pincode": "434532", + "id": "addr1", + "storeId":"store1" + }, + { + "line1": "7th ave", + "line2": "Wellington Broadway", + "landmark":"Carl Sagan's Office", + "pincode": "434543", + "id": "addr2", + "storeId":"store1" + }, + { + "line1": "Patanjali Lane", + "line2": "Patanjali Rd", + "landmark": "Near locality water tank", + "pincode": "473032", + "id": "addr3", + "productOwnerId": 12 + }, + { + "line1": "Orchard St", + "line2": "Blumingdale's", + "landmark":"Post Office", + "pincode": "673627", + "id": "addr4", + "storeId":"store2" + } + ]; + let AddressBook = loopback.findModel('AddressBook'); + AddressBook.create(data, function(err){ + done(err); + }); + }); + + before('creating phone numbers', done => { + let data = [ + { + "number": "2342229898", + "firstName": "Ethan", + "lastName": "Hunt", + "addressBookId": "addr1" + }, + { + "number": "2342229899", + "firstName": "Davy", + "lastName": "Jones", + "addressBookId": "addr1" + }, + { + "number": "2342222399", + "firstName": "Davy", + "lastName": "Jones", + "addressBookId": "addr2" + }, + { + "number": "8037894565", + "firstName": "Martha", + "lastName": "James", + "addressBookId": "addr3" + } + ]; + + let PhoneNumber = loopback.findModel('PhoneNumber'); + PhoneNumber.create(data, function(err){ + done(err); + }); + }); + + before('creating personalization rule', done => { + let data = [ + { + "modelName" : "AddressBook", + "personalizationRule": { + "mask": { + "landmark": true + } + } + }, + { + "modelName": "PhoneNumber", + "personalizationRule": { + "fieldMask": { + "number" : { + "pattern": "([0-9]{3})([0-9]{3})([0-9]{4})", + "maskCharacter": "X", + "format": "($1) $2-$3", + "mask": ["$1", "$2"] + } + } + } + } + ]; + + PersonalizationRule.create(data, err => done(err)); + }); + + it('t39 should apply child model personalization when included from parent with no personalization', done => { + let data = { + productOwnerId: 1 + }; + + let url = `${productCatalogUrl}/watch3?access_token=${accessToken}`; + api.put(url) + .set('Accept', 'application/json') + .set('REMOTE_USER', 'testUser') + .set('region', 'kl') + .send(data) + .expect(200) + .end((err, resp) => { + if (err) { + done(err) + } + else { + let filter = { include: ["ProductCatalog"] }; + let escapedFilter = encodeURIComponent(JSON.stringify(filter)); + let url2 = `${productOwnerUrl}/1?filter=${escapedFilter}&access_token=${accessToken}`; + let res = resp.body; + expect(res.modelNo).to.include("XXXX"); + api.get(url2) + .set('Accept', 'application/json') + .set('REMOTE_USER', 'testUser') + .set('region', 'kl') + .expect(200).end((err, resp) => { + if(err) { + done(err); + } + else { + let result = resp.body; + expect(result.ProductCatalog).to.be.array; + let watch3item = result.ProductCatalog.find(item => item.id === 'watch3'); + expect(watch3item.modelNo).to.equal('123456XXXX'); + done(); + } + }); + } + }); + }); + + it.only('t40 should demonstrate personalization is being applied recursively', done => { + let filter = { + "include": [ + { + "ProductCatalog" : { + "store": { + "store" : { + "addresses" : "phones" + } + } + } + }, + "address" + ], + "where": { "id": 12 } + }; + let filterString = encodeURIComponent(JSON.stringify(filter)); + let url = `${productOwnerUrl}/findOne?access_token=${accessToken}&&filter=${filterString}`; + api.get(url) + .set('Accept', 'application/json') + .set('REMOTE_USER', 'testUser') + .expect(200) + .end((err, resp) => { + if(err){ + done(err) + } + else { + let result = resp.body; + expect(result.ProductCatalog).to.be.array; + expect(result.address).to.be.object; + console.log(JSON.stringify(result,null, 2)); + done(); + } + }); + + }); + }); }); From 0362ddf77296c75e569f4eae64c190111a8ea8b2 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 28 Apr 2020 01:51:06 +0530 Subject: [PATCH 20/80] removed comments --- lib/service-personalizer.js | 50 ++--------- test/test.js | 161 ++++++++++++++++++++++-------------- 2 files changed, 105 insertions(+), 106 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 6ea1032..ff13f2b 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -1313,55 +1313,12 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { let {settings: {relations}, definition: { name } } = Model; let { isBeforeRemote, context } = personalizationOptions; let prefix = isBeforeRemote ? 'beforeRemote' : 'afterRemote'; - - // let applyOnRecord = (data, relationName, relModel, done) => { - // let relData = null; - // if(data.__data) { - // relData = data.__data[relationName]; - // } - // else { - // relData = data[relationName]; - // } - // let logMessage = `process relation "${relationName}" data in model "${name}"`; - - // if(typeof relData !== 'undefined' && Array.isArray(relData)) { - // log.debug(context, `${prefix}: ${logMessage}`); - // async.eachSeries(relData, function relationRecordProcessor(relationRecord, cb){ - // applyServicePersonalization(relModel, relationRecord, personalizationOptions, cb); - // }, function relationListItemsAsyncDoneCb(err){ - // done(err); - // }); - // } - // else if(typeof relData !== 'undefined' && typeof relData === 'object') { - // log.debug(context, `${prefix}: ${logMessage}`); - // applyServicePersonalization(relModel, relData, personalizationOptions, done); - // } - // else if(typeof relData === 'undefined' && isBeforeRemote) { - // log.debug(context, `${prefix}: ${logMessage}`); - // applyServicePersonalization(relModel, {}, personalizationOptions, done); - // } - // else { - // log.debug(context, `${prefix}: no data for relation "${relationName}" of model "${name}"`) - // nextTick(done); - // } - // }; if (relations) { // Object.entries(Model.setti) let relationItems = Object.entries(relations); let relationsIterator = function relationProcessor([ relationName, relation ], done) { - // if(typeof data !== 'undefined' && Array.isArray(data)) { - // async.eachSeries(data, function dataIterator(record, cb) { - // applyOnRecord(record, relationName, relation.model, cb); - // }, done); - // } - // else if(typeof data !== 'undefined' && typeof data === 'object') { - // applyOnRecord(data, relationName, relation.model, done); - // } - // else { - // nextTick(done); - // } - + // check if the related model has personalization let relData = undefined; let relModel = relation.model; @@ -1373,6 +1330,7 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { } return carrier; }, []); + relData = _.flatten(relData); if(relData.length) { applyFlag = true; } @@ -1384,13 +1342,17 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { else if((relData = data[relationName])) { applyFlag = true; } + let callback = function (err) { + log.debug(context, `${prefix}: processing relation "${relationName}"/"${name}" - finished`); done(err); }; callback.__trace = `${name}_${relationName}`; if(applyFlag) { + log.debug(context, `${prefix}: processing relation "${relationName}"/"${name}"`); return applyServicePersonalization(relModel, relData, personalizationOptions, callback) } + log.debug(context, `${prefix}: processing relation "${relationName}"/"${name}" - skipped`); nextTick(done); }; return async.eachSeries(relationItems, relationsIterator, done); diff --git a/test/test.js b/test/test.js index 5bab575..1322db1 100755 --- a/test/test.js +++ b/test/test.js @@ -1779,7 +1779,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - describe.only('Relation tests', function () { + describe('Relation tests', function () { before('creating the product owner', done => { let data = { @@ -1907,8 +1907,8 @@ describe(chalk.blue('service personalization test started...'), function () { }, { "number": "2342222399", - "firstName": "Davy", - "lastName": "Jones", + "firstName": "Jack", + "lastName": "Sparrow", "addressBookId": "addr2" }, { @@ -1916,7 +1916,13 @@ describe(chalk.blue('service personalization test started...'), function () { "firstName": "Martha", "lastName": "James", "addressBookId": "addr3" - } + }, + { + "number": "2340022399", + "firstName": "Antonio", + "lastName": "Bandaras", + "addressBookId": "addr4" + }, ]; let PhoneNumber = loopback.findModel('PhoneNumber'); @@ -1925,33 +1931,36 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - before('creating personalization rule', done => { - let data = [ - { - "modelName" : "AddressBook", - "personalizationRule": { - "mask": { - "landmark": true - } - } - }, - { - "modelName": "PhoneNumber", - "personalizationRule": { - "fieldMask": { - "number" : { - "pattern": "([0-9]{3})([0-9]{3})([0-9]{4})", - "maskCharacter": "X", - "format": "($1) $2-$3", - "mask": ["$1", "$2"] - } - } - } - } - ]; + // before('creating personalization rule', done => { + // let data = [ + // { + // "modelName" : "AddressBook", + // "personalizationRule": { + // "mask": { + // "landmark": true + // } + // } + // }, + // { + // "modelName": "PhoneNumber", + // "personalizationRule": { + // "fieldMask": { + // "number" : { + // "pattern": "([0-9]{3})([0-9]{3})([0-9]{4})", + // "maskCharacter": "X", + // "format": "($1) $2-$3", + // "mask": ["$1", "$2"] + // } + // } + // } + // } + // ]; - PersonalizationRule.create(data, err => done(err)); - }); + // PersonalizationRule.create(data, {}, function(err, res){ + // console.log(res); + // done(err) + // }); + // }); it('t39 should apply child model personalization when included from parent with no personalization', done => { let data = { @@ -1995,41 +2004,69 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it.only('t40 should demonstrate personalization is being applied recursively', done => { - let filter = { - "include": [ - { - "ProductCatalog" : { - "store": { - "store" : { - "addresses" : "phones" - } - } - } - }, - "address" - ], - "where": { "id": 12 } - }; - let filterString = encodeURIComponent(JSON.stringify(filter)); - let url = `${productOwnerUrl}/findOne?access_token=${accessToken}&&filter=${filterString}`; - api.get(url) - .set('Accept', 'application/json') - .set('REMOTE_USER', 'testUser') - .expect(200) - .end((err, resp) => { - if(err){ - done(err) + it('t40 should demonstrate personalization is being applied recursively', done => { + let data = [ + { + "modelName" : "AddressBook", + "personalizationRule": { + "mask": { + "landmark": true + } } - else { - let result = resp.body; - expect(result.ProductCatalog).to.be.array; - expect(result.address).to.be.object; - console.log(JSON.stringify(result,null, 2)); - done(); + }, + { + "modelName": "PhoneNumber", + "personalizationRule": { + "fieldMask": { + "number" : { + "pattern": "([0-9]{3})([0-9]{3})([0-9]{4})", + "maskCharacter": "X", + "format": "($1) $2-$3", + "mask": ["$1", "$2"] + } + } } - }); + } + ]; + PersonalizationRule.create(data, {}, function(err){ + if(err) { + return done(err) + } + let filter = { + "include": [ + { + "ProductCatalog" : { + "store": { + "store" : { + "addresses" : "phones" + } + } + } + }, + "address" + ], + "where": { "id": 12 } + }; + let filterString = encodeURIComponent(JSON.stringify(filter)); + let url = `${productOwnerUrl}/findOne?access_token=${accessToken}&&filter=${filterString}`; + api.get(url) + .set('Accept', 'application/json') + .set('REMOTE_USER', 'testUser') + .expect(200) + .end((err, resp) => { + if(err){ + done(err) + } + else { + let result = resp.body; + expect(result.ProductCatalog).to.be.array; + expect(result.address).to.be.object; + console.log(JSON.stringify(result,null, 2)); + done(); + } + }); + }); }); }); }); From cfb06c07eab6197d9ce3af579b91cf0be1687b4a Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 28 Apr 2020 02:10:29 +0530 Subject: [PATCH 21/80] updated readme --- README.md | 57 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index a9dced6..735b11d 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ The following entity structure and relationships assumed for most of the tests. | AddressBook | | PhoneNumber | | ProductCatalog | | | | | | | +-------------------+ +------------------------+ +------------------------+ -| line1 : string | | number : number (*) | | name : string | +| line1 : string | | number : string (PK)| | name : string | | line2 : string | | firstName : string | | category : string | | landmark : string | | lastName : string | | desc : string | | pincode : string | | | | price : object | @@ -41,33 +41,38 @@ The following entity structure and relationships assumed for most of the tests. +------------------------+ - +-----------------+ +---------------+ - | | | | - | ProductOwner | | Store | - | | | | - +-----------------+ +---------------+ - | name : string | | name : string | - | city : string | | | - +-----------------+ +---------------+ ++-----------------+ +---------------+ +--------------------------------+ +| | | | | | +| ProductOwner | | Store | | StoreStock | +| | | | | | ++-----------------+ +---------------+ +--------------------------------+ +| name : string | | name : string | | storeId : string (FK) | +| city : string | | | | productCatalogId : string (FK) | ++-----------------+ +---------------+ +--------------------------------+ -========================================================================================= - +--------------+ - +--------+ ProductOwner +----------+ - | +--------------+ | - (hasMany) (hasOne) - | | - v v -+-------+--------+ +----+----+ +-------------+ -| ProductCatalog | | Address +------(hasMany)----> | PhoneNumber | -+-------+--------+ +----+----+ +-------------+ - ^ ^ - | | - (hasMany) | - | (hasMany) - | +-------+ | - +--------->+ Store +---------------+ - +-------+ +========================================================================================================================== + + +--------------+ + +--------+ ProductOwner +----------+ + + +--------------+ + + (hasMany-ProductCatalog) (hasOne-address) + + + + v v + +-------+--------+ +----+----+ +-------------+ + +------------>+ ProductCatalog | | Address +-----+(hasMany-phones)+---> | PhoneNumber | + | +----------------+ +----+----+ +-------------+ + | ^ + | | +(belongsTo-product) + + | (hasMany-addresses) + | +-------+ + + | +-+(belongsTo-store)+->+ Store +---------------+ + | | +-------+ + | | + +-----+------+ | + | StoreStock +--+ + +------------+ ``` From f4b528c773ef60554aa236c93bd4da45a2d66ba6 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 28 Apr 2020 11:32:32 +0530 Subject: [PATCH 22/80] added a customRemote method based test --- test/common/models/product-owner.js | 64 ++++++++++++++++++ test/test.js | 100 +++++++++++++++++++--------- 2 files changed, 132 insertions(+), 32 deletions(-) create mode 100644 test/common/models/product-owner.js diff --git a/test/common/models/product-owner.js b/test/common/models/product-owner.js new file mode 100644 index 0000000..82788fe --- /dev/null +++ b/test/common/models/product-owner.js @@ -0,0 +1,64 @@ +const { applyServicePersonalization } = require('./../../../lib/service-personalizer'); // or require('oe-service-personalization/lib/service-personalizer'); + +module.exports = function(ProductOwner) { + ProductOwner.remoteMethod('demandchain', { + description: 'Gets the stores, store addresses, and, contacts of a product owner', + accepts: [ + { + arg: 'id', + type: 'number', + description: 'the unique id of the owner', + required: true + }, + { + arg: 'options', + type: 'object', + http:function(ctx) { + return ctx; + } + } + ], + returns: { + arg: 'chain', + root: true, + type: 'object' + }, + http: { path: '/:id/demandchain', verb: 'get' } + }); + + ProductOwner.demandchain = function(ownerId, options, done) { + if(typeof done === 'undefined' && typeof options === 'function') { + done = options; + options = {}; + }; + + let filter = { + "include": [ + { + "ProductCatalog" : { + "store": { + "store" : { + "addresses" : "phones" + } + } + } + }, + "address" + ], + "where": { "id": ownerId } + }; + ProductOwner.findOne(filter, options, function(err, result) { + if(err) { + done(err) + } + else { + let persOpts = { + isBeforeRemote: false, context: options + }; + applyServicePersonalization('ProductOwner', result, persOpts, function(err){ + done(err, result); + }); + } + }) + }; +} \ No newline at end of file diff --git a/test/test.js b/test/test.js index 1322db1..7624cec 100755 --- a/test/test.js +++ b/test/test.js @@ -14,6 +14,7 @@ var chalk = require('chalk'); var chai = require('chai'); chai.use(require('chai-things')); var expect = chai.expect; +var _ = require('lodash'); var ProductCatalog; var ProductOwner; @@ -1779,6 +1780,8 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); + var httpResult; + describe('Relation tests', function () { before('creating the product owner', done => { @@ -1931,36 +1934,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - // before('creating personalization rule', done => { - // let data = [ - // { - // "modelName" : "AddressBook", - // "personalizationRule": { - // "mask": { - // "landmark": true - // } - // } - // }, - // { - // "modelName": "PhoneNumber", - // "personalizationRule": { - // "fieldMask": { - // "number" : { - // "pattern": "([0-9]{3})([0-9]{3})([0-9]{4})", - // "maskCharacter": "X", - // "format": "($1) $2-$3", - // "mask": ["$1", "$2"] - // } - // } - // } - // } - // ]; - - // PersonalizationRule.create(data, {}, function(err, res){ - // console.log(res); - // done(err) - // }); - // }); + it('t39 should apply child model personalization when included from parent with no personalization', done => { let data = { @@ -2003,7 +1977,7 @@ describe(chalk.blue('service personalization test started...'), function () { } }); }); - + it('t40 should demonstrate personalization is being applied recursively', done => { let data = [ { @@ -2060,13 +2034,75 @@ describe(chalk.blue('service personalization test started...'), function () { } else { let result = resp.body; + httpResult = result; expect(result.ProductCatalog).to.be.array; expect(result.address).to.be.object; - console.log(JSON.stringify(result,null, 2)); + // console.log(JSON.stringify(result,null, 2)); + _.flatten( + _.flatten(result.ProductCatalog.map(item => item.store.store.addresses)) + .map(x => x.phones) + ).forEach(ph => { + let substr = ph.number.substr(0, 10); + expect(substr).to.equal('(XXX) XXX-'); + }); + + done(); } }); }); + }); + }); + + describe('Remote method tests', () => { + before('re-inserting the personalization rules', done => { + let data = [ + { + "modelName" : "AddressBook", + "personalizationRule": { + "mask": { + "landmark": true + } + } + }, + { + "modelName": "PhoneNumber", + "personalizationRule": { + "fieldMask": { + "number" : { + "pattern": "([0-9]{3})([0-9]{3})([0-9]{4})", + "maskCharacter": "X", + "format": "($1) $2-$3", + "mask": ["$1", "$2"] + } + } + } + } + ]; + + PersonalizationRule.create(data, {}, function(err){ + done(err); + }); + }); + + it('t41 should demonstrate data getting personalized via a remote method', done => { + + let ownerId = 12; + let url = `${productOwnerUrl}/${ownerId}/demandchain?access_token=${accessToken}`; + api.get(url) + .set('Accept', 'application/json') + .set('REMOTE_USER', 'testUser') + .expect(200) + .end((err, resp) => { + if(err) { + done(err); + } + else { + let result = resp.body; + expect(result).to.deep.equal(httpResult); + done(); + } + }) }); }); }); From 65d17d527479e25a8195fed46d51a19978786d59 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 28 Apr 2020 17:03:12 +0530 Subject: [PATCH 23/80] documentation --- README.md | 305 +++++++++++++++++++++++++++++++++++- lib/service-personalizer.js | 2 +- 2 files changed, 304 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 735b11d..491f919 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,30 @@ # oe-service-personalization -This module will apply personalizations such as field masking, hiding fields, sorting, etc on top of traditional remote endpoints. With these limited set of operations it is also possible to personalize data to a group of clients. In other words, the same data can appear (and/or behave differently) on, say, an android app, an ios app, and, a browser app . Such granular segementations are possible by describing them in an property called `scope` on the personalization rule. (This is made possible by the `oe-personalization` module). Further, for such segmented personalizations to take effect, we need the necessary header in the http request (as how it is in the `scope`). +This module will apply operations such as field masking, hiding +fields, sorting, etc on top of traditional remote endpoints, +thereby "personalizing" them. With these limited set of +operations it is also possible to personalize data to a group +of clients. In other words, the same data can appear (and/or +behave differently) on, say, an android app, an ios app, and, +a browser app. + +Such granular differenciations are possible by describing them +in an property called `scope` on the personalization rule. +(This is made possible by the `oe-personalization` module). +Further, for such segmented personalizations to take effect, we +need the necessary header in the http request (as how it is in +the `scope`). + +Such kind of personalizations allows for us to be able to +derive analytics on the groups or segmentations which would +in-turn allow us to focus more on servicing those groups better. +For e.g. assume there is an api. Further assume, it is +personalized for both a mobile client and a browser client. +Once deployed, we can derive the analytics by looking at +server logs for the same api and decide which platform that api +is frequently accessed. Such information can be used to improve +the user experience on that platform. However, this is not in +scope of this document. ## dependency * oe-cloud @@ -20,6 +44,272 @@ $ # Run test cases along with code coverage - code coverage report will be avail $ npm run grunt-cover ``` +## How to use + +1. Install the module to your application + +``` +npm install oe-service-personalization +``` +2. In your project's `app-list.json` file add the following config: + +``` + { + "path": "oe-service-personalization", + "enabled": true, + "autoEnableMixins": true + } +``` +3. Add `ServicePersonalizationMixin` mixin to the model declaration. + +Example: +```json +{ + "name": "ProductOwner", + "base": "BaseEntity", + "idInjection": true, + "properties": { + "name": { + "type": "string" + }, + "city": { + "type": "string", + "require" : true + } + }, + "validations": [], + "relations": { + "ProductCatalog": { + "type": "hasMany", + "model": "ProductCatalog" + }, + "address": { + "type" : "hasOne", + "model" : "AddressBook" + } + }, + "acls": [], + "methods": {}, + "mixins": { + "ServicePersonalizationMixin" : true + } +} + +``` +4. Insert rules into the `PersonalizationRule` model. + +Example: +```json +{ + "disabled" : false, + "modelName" : "ProductCatalog", + "personalizationRule" : { + "fieldValueReplace" : { + "keywords" : { + "Alpha" : "A", + "Bravo" : "B" + } + } + }, + "scope" : { + "device" : "mobile" + } +} +``` + +The above example adds a `fieldValueReplace` operation to the +`keywords` property of `ProductCatalog` model. Additionally +we have provided `scope` for this rule to take effect only +when it is specified in the http headers of the request; it +is always a simple key/value pair. + +5. _(Optional)_ If there are some custom function based +operations, add the path in the application's `config.json` +file. Alternatively, set the environment variable: +`custom_function_path` + +Example: (`config.json` snippet): + +```json + "servicePersonalization" : { + "customFunctionPath": "D:\\Repos\\oecloud.io\\oe-service-personalization_master\\test\\customFunction" + } +``` +Example: (via environment variable): +```bash +$ export custom_function_path="/project/customFuncDir" +``` +> Note: the full path to the directory is required. + +## Working Principle + +During application startup all the models which have the +above mixin applied will behave differently; we attach +`beforeRemote` and `afterRemote` hooks which determine if +there are personalization rules for the model, +and, then finally performs +the defined operations on the request/response (as per the +case). It also recursively does this in the case relational data +is included. The net effect of all the operations is the +"personalized" data. + +These steps personalize data as long as its accessed via a +remote endpoint (aka _remotes_). To do this in code, please +see the programmatic api. + +## Supported operations + +To keep this document brief, a short description about each +operation is only given. Please visit the tests to see +example usages. It is recommended to review the tests synopsis +section if debugging the tests are necessary. + +The one and only test file can be found in this project's +folder here: +``` +./test/test.js +``` + +Below is the list of all supported operations and their +corresponding tests: + +| Operation | Description | Aspect | Tests | +|--------------------|---------------------------------------------------------------------------------------------------------------|----------------------|---------------------------------------| +| lbFilter | This applies a loopback filter to the request; it can contain an _include_ clause or a _where_ clause. | Pre-apply | t21 | +| filter | Same as above, but only adds the where clause to the request i.e. a query-based filter | Pre-apply | t9 | +| sort | Performs a sort at the datasource level | Pre-apply | t4, t5, t6, t7, t8, t10, t11 | +| fieldReplace | Replaces the property name in the data with another text. (Not its value) | Pre-apply/Post-apply | t1, t15, t17 | +| fieldValueReplace | Replaces the property value in the data | Pre-apply/Post-apply | t22, t20, t19, t18, t17, t16, t3, t23 | +| fieldMask | Masks value in the field according to a regex pattern | Post-apply | t24, t25, t26, t27, t28, t29 | +| mask | Hides a field in the response | Pre-apply | t13 | +| hide | Same as _mask_ | Pre-apply | t13 | +| postCustomFunction | Adds a custom function which can add desired customization to response. Please see step #5 in how to use. | Post-apply | t35, t36 | +| preCustomFunction | Adds a custom function which can add desired customization to the request. Please see step #5 in how to use. | Pre-apply | t35, t36 | + +## Programmatic API + +To do personalization in a custom remote method, or, in unit +tests you need the following api. + +```JavaScript + +const { applyServicePersonalization } = require('oe-service-personalization/lib/service-personalizer'); + +// ... +var options = { + isBeforeRemote: false, // required + context: ctx //the http context +}; + +applyServicePersonalization(modelName, data, options, function(err){ + // nothing to access here since + // data gets mutated internally +}) +``` + +Example directly from our tests (test case `t41`): `./test/common/models/product-owner.js` + +```javascript +const { applyServicePersonalization } = require('./../../../lib/service-personalizer'); // or require('oe-service-personalization/lib/service-personalizer'); + +module.exports = function(ProductOwner) { + ProductOwner.remoteMethod('demandchain', { + description: 'Gets the stores, store addresses, and, contacts of a product owner', + accepts: [ + { + arg: 'id', + type: 'number', + description: 'the unique id of the owner', + required: true + }, + { + arg: 'options', + type: 'object', + http:function(ctx) { + return ctx; + } + } + ], + returns: { + arg: 'chain', + root: true, + type: 'object' + }, + http: { path: '/:id/demandchain', verb: 'get' } + }); + + ProductOwner.demandchain = function(ownerId, options, done) { + if(typeof done === 'undefined' && typeof options === 'function') { + done = options; + options = {}; + }; + + let filter = { + "include": [ + { + "ProductCatalog" : { + "store": { + "store" : { + "addresses" : "phones" + } + } + } + }, + "address" + ], + "where": { "id": ownerId } + }; + ProductOwner.findOne(filter, options, function(err, result) { + if(err) { + done(err) + } + else { + let persOpts = { + isBeforeRemote: false, context: options + }; + applyServicePersonalization('ProductOwner', result, persOpts, function(err){ + done(err, result); + }); + } + }) + }; +} +``` + +## Pre-apply/Post-apply & Relations + +All operations have two aspects. Some operations +modify the context of the http request (for e.g. +`lbFilter`, `sort`, etc). Some operations modify the response (e.g. `fieldMask`) +we can access in an `afterRemote` phase of a request/response +pipeline. Other operations (for e.g. `fieldReplace`) have to +take effect in both the stages. + +Pre-apply/Post-apply is the vocabulary adopted to distinguish +these aspects of an operation - namely how and when it is +applied in the request/response pipeline. + +Due to the way loopback relations are +implemented only operations that post-apply are honoured. +This is also the case when using the programmatic api +for service personalization (regardless of whether relations +are accessed or not). + +## Points to consider + +1. Datasource support. Datasources can be service-oriented. +(Such as a web service). +Hence support for sorting, filtering, etc may be limited. +Therefore operations which pre-apply may not give expected +results. + +2. Using custom functions (`postCustomFunction` or `preCustomFunction`). +Please honour the pipeline stage and use the correct +operation. No point in trying to modify `ctx.result` in a +`preCustomFunction`. Also ensure path to the directory where +the custom functions are stored is configured correctly. + +3. Pre-apply/post-apply and relations ## Test Synopsis The following entity structure and relationships assumed for most of the tests. @@ -76,4 +366,15 @@ The following entity structure and relationships assumed for most of the tests. ``` -Note: All the models have the `ServicePersonalizationMixin` enabled. \ No newline at end of file +Note: All the models have the `ServicePersonalizationMixin` enabled. + +The `test` folder is meant to emulate a small oe-cloud application. + +To run as an application server: + +``` +$ node test/server.js +``` + +It is also recommended to attach an explorer component (such as +loopback-component-explorer) when running as a standalone application. diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index ff13f2b..483f42c 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -449,7 +449,7 @@ function executeCustomFunction(ctx, instruction, cb) { * @param {object} instruction - instructions. * @param {function} cb - callback function. */ -function addFilter(ctx, instruction, cb) { +function addFilter(ctx, instruction, cb) { //Tests: t9 // TODO: Check the datasource to which this model is attached. // If the datasource is capable of doing filter queries add a where clause. From 12841c178cc48b28f91c9b56e092701a71767ec8 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 28 Apr 2020 20:55:11 +0530 Subject: [PATCH 24/80] partial code cleanup --- lib/service-personalizer.js | 1084 +++++++++++++++++------------------ 1 file changed, 540 insertions(+), 544 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 483f42c..9e3acf3 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -8,7 +8,7 @@ * Service personalization module. Optimizes and applies one personalization function. * * @module EV Service Personalizer - * @author Atul Pandit, gourav_gupta, pradeep_tippa + * @author Atul Pandit, gourav_gupta, pradeep_tippa, arun_jayapal (aka deostroll) */ var loopback = require('loopback'); @@ -42,34 +42,34 @@ var sortFactoryFn = (reverse = false) => * @function * @name getPersonalizationRuleForModel */ -var getPersonalizationRuleForModel = function getPersonalizationRuleForModelFn(modelName, ctx, callback) { - log.debug(ctx.req.callContext, 'getPersonalizationRuleForModel called for model - ', modelName); - - var PersonalizationRule = loopback.findModel('PersonalizationRule'); - - var findByModelNameQuery = { - where: { - modelName: modelName, - disabled: false - } - }; - - PersonalizationRule.find(findByModelNameQuery, ctx.req.callContext, function getPersonalizationRuleForModelFindCb(err, result) { - log.debug(ctx.req.callContext, 'Query result = ', result); - if (err) { - // TODO: Error getting personalization rule.. what should be done? Continue or stop? - log.debug(ctx.req.callContext, 'Error getting personalization rule for model [', modelName, ']. skipping personalization'); - return callback(null); - } - - if (result && result.length > 0) { - log.debug(ctx.req.callContext, 'Returning personzalition rule'); - return callback(result[0]); - } - log.debug(ctx.req.callContext, 'Personalization rules not defined for model [', modelName, ']. skipping personalization'); - return callback(null); - }); -}; +// var getPersonalizationRuleForModel = function getPersonalizationRuleForModelFn(modelName, ctx, callback) { +// log.debug(ctx.req.callContext, 'getPersonalizationRuleForModel called for model - ', modelName); + +// var PersonalizationRule = loopback.findModel('PersonalizationRule'); + +// var findByModelNameQuery = { +// where: { +// modelName: modelName, +// disabled: false +// } +// }; + +// PersonalizationRule.find(findByModelNameQuery, ctx.req.callContext, function getPersonalizationRuleForModelFindCb(err, result) { +// log.debug(ctx.req.callContext, 'Query result = ', result); +// if (err) { +// // TODO: Error getting personalization rule.. what should be done? Continue or stop? +// log.debug(ctx.req.callContext, 'Error getting personalization rule for model [', modelName, ']. skipping personalization'); +// return callback(null); +// } + +// if (result && result.length > 0) { +// log.debug(ctx.req.callContext, 'Returning personzalition rule'); +// return callback(result[0]); +// } +// log.debug(ctx.req.callContext, 'Personalization rules not defined for model [', modelName, ']. skipping personalization'); +// return callback(null); +// }); +// }; /** * @@ -83,148 +83,148 @@ var getPersonalizationRuleForModel = function getPersonalizationRuleForModelFn(m * @name applyPersonalizationRule */ -var applyPersonalizationRule = function applyPersonalizationRuleFn(ctx, p13nRule) { - var arr = []; - - log.debug(ctx.req.callContext, 'applying Personalizing ctx with function - ', p13nRule); - - var instructions = Object.keys(p13nRule); - - // TODO:Check if all instructions can be applied in parallel in asynch way. - // instructions.forEach(function (instruction) { - - for (var i in instructions) { - if (instructions.hasOwnProperty(i)) { - var instruction = instructions[i]; - switch (instruction) { - case 'lbFilter': - arr.push({ - type: 'lbFilter', - fn: async.apply(addLbFilter, ctx, p13nRule[instruction]) - }); - // arr.push(async.apply(addLbFilter, ctx, p13nRule[instruction])); - break; - case 'filter': - arr.push({ - type: 'filter', - fn: async.apply(addFilter, ctx, p13nRule[instruction]) - }); - // arr.push(async.apply(addFilter, ctx, p13nRule[instruction])); - break; - case 'fieldReplace': - arr.push({ - type: 'fieldReplace', - fn: async.apply(addFieldReplace, ctx, p13nRule[instruction]) - }); - // arr.push(async.apply(addFieldReplace, ctx, p13nRule[instruction])); - break; - case 'fieldValueReplace': - arr.push({ - type: 'fieldValueReplace', - fn: async.apply(addFieldValueReplace, ctx, p13nRule[instruction]) - }); - // arr.push(async.apply(addFieldValueReplace, ctx, p13nRule[instruction])); - break; - case 'sort': - arr.push({ - type: 'sort', - fn: async.apply(addSort, ctx, p13nRule[instruction]) - }); - // arr.push(async.apply(addSort, ctx, p13nRule[instruction])); - break; - case 'postCustomFunction': - arr.push({ - type: 'postCustomFunction', - fn: async.apply(executeCustomFunction, ctx, p13nRule[instruction]) - }); - break; - case 'union': - // unionResults(ctx, p13nRule[instruction]); - break; - case 'mask': - arr.push({ - type: 'mask', - fn: async.apply(maskFields, ctx, p13nRule[instruction]) - }); - // arr.push(async.apply(maskFields, ctx, p13nRule[instruction])); - break; - case 'fieldMask': - arr.push({ - type: 'fieldMask', - fn: async.apply(maskCharacters, ctx, p13nRule[instruction]) - }); - // arr.push(async.apply(maskCharacters, ctx, p13nRule[instruction])); - break; - default: - } - } - } - return arr.sort(sortFactoryFn()); -}; - -function execute(arr, callback) { - async.parallel(arr, function applyPersonalizationRuleAsyncParallelFn(err, results) { - if (err) { - return callback(err); - } - callback(); - }); -} - -function addFieldValueReplace(ctx, instruction, cb) { - return fieldValueReplacementFn(ctx, instruction, cb); -} +// var applyPersonalizationRule = function applyPersonalizationRuleFn(ctx, p13nRule) { +// var arr = []; + +// log.debug(ctx.req.callContext, 'applying Personalizing ctx with function - ', p13nRule); + +// var instructions = Object.keys(p13nRule); + +// // TODO:Check if all instructions can be applied in parallel in asynch way. +// // instructions.forEach(function (instruction) { + +// for (var i in instructions) { +// if (instructions.hasOwnProperty(i)) { +// var instruction = instructions[i]; +// switch (instruction) { +// case 'lbFilter': +// arr.push({ +// type: 'lbFilter', +// fn: async.apply(addLbFilter, ctx, p13nRule[instruction]) +// }); +// // arr.push(async.apply(addLbFilter, ctx, p13nRule[instruction])); +// break; +// case 'filter': +// arr.push({ +// type: 'filter', +// fn: async.apply(addFilter, ctx, p13nRule[instruction]) +// }); +// // arr.push(async.apply(addFilter, ctx, p13nRule[instruction])); +// break; +// case 'fieldReplace': +// arr.push({ +// type: 'fieldReplace', +// fn: async.apply(addFieldReplace, ctx, p13nRule[instruction]) +// }); +// // arr.push(async.apply(addFieldReplace, ctx, p13nRule[instruction])); +// break; +// case 'fieldValueReplace': +// arr.push({ +// type: 'fieldValueReplace', +// fn: async.apply(addFieldValueReplace, ctx, p13nRule[instruction]) +// }); +// // arr.push(async.apply(addFieldValueReplace, ctx, p13nRule[instruction])); +// break; +// case 'sort': +// arr.push({ +// type: 'sort', +// fn: async.apply(addSort, ctx, p13nRule[instruction]) +// }); +// // arr.push(async.apply(addSort, ctx, p13nRule[instruction])); +// break; +// case 'postCustomFunction': +// arr.push({ +// type: 'postCustomFunction', +// fn: async.apply(executeCustomFunction, ctx, p13nRule[instruction]) +// }); +// break; +// case 'union': +// // unionResults(ctx, p13nRule[instruction]); +// break; +// case 'mask': +// arr.push({ +// type: 'mask', +// fn: async.apply(maskFields, ctx, p13nRule[instruction]) +// }); +// // arr.push(async.apply(maskFields, ctx, p13nRule[instruction])); +// break; +// case 'fieldMask': +// arr.push({ +// type: 'fieldMask', +// fn: async.apply(maskCharacters, ctx, p13nRule[instruction]) +// }); +// // arr.push(async.apply(maskCharacters, ctx, p13nRule[instruction])); +// break; +// default: +// } +// } +// } +// return arr.sort(sortFactoryFn()); +// }; + +// function execute(arr, callback) { +// async.parallel(arr, function applyPersonalizationRuleAsyncParallelFn(err, results) { +// if (err) { +// return callback(err); +// } +// callback(); +// }); +// } + +// function addFieldValueReplace(ctx, instruction, cb) { +// return fieldValueReplacementFn(ctx, instruction, cb); +// } /* eslint-disable no-loop-func */ -var applyReversePersonalizationRule = function applyReversePersonalizationRuleFn(ctx, p13nRule, callback) { - log.debug(ctx.options, 'Reverse Personalizing ctx with function - ', p13nRule); - - var instructions = Object.keys(p13nRule); - - var arr = []; - // TODO:Check if all instructions can be applied in parallel in asynch way. - // instructions.forEach(function (instruction) { - - for (var i in instructions) { - if (instructions.hasOwnProperty(i)) { - var instruction = instructions[i]; - switch (instruction) { - case 'fieldReplace': - arr.push({ - type: 'fieldReplace', - fn: async.apply(addReverseFieldReplace, ctx, p13nRule[instruction]) - }); - // arr.push(async.apply(addReverseFieldReplace, ctx, p13nRule[instruction])); - break; - case 'fieldValueReplace': - arr.push({ - type: 'fieldValueReplace', - fn: async.apply(addReverseFieldValueReplace, ctx, p13nRule[instruction]) - }); - // arr.push(async.apply(addReverseFieldValueReplace, ctx, p13nRule[instruction])); - break; - case 'preCustomFunction': - arr.push({ - type: 'preCustomFunction', - fn: async.apply(executeCustomFunction, ctx, p13nRule[instruction]) - }); - break; - default: - } - } - } - return arr.sort(sortFactoryFn(true)); -}; +// var applyReversePersonalizationRule = function applyReversePersonalizationRuleFn(ctx, p13nRule, callback) { +// log.debug(ctx.options, 'Reverse Personalizing ctx with function - ', p13nRule); + +// var instructions = Object.keys(p13nRule); + +// var arr = []; +// // TODO:Check if all instructions can be applied in parallel in asynch way. +// // instructions.forEach(function (instruction) { + +// for (var i in instructions) { +// if (instructions.hasOwnProperty(i)) { +// var instruction = instructions[i]; +// switch (instruction) { +// case 'fieldReplace': +// arr.push({ +// type: 'fieldReplace', +// fn: async.apply(addReverseFieldReplace, ctx, p13nRule[instruction]) +// }); +// // arr.push(async.apply(addReverseFieldReplace, ctx, p13nRule[instruction])); +// break; +// case 'fieldValueReplace': +// arr.push({ +// type: 'fieldValueReplace', +// fn: async.apply(addReverseFieldValueReplace, ctx, p13nRule[instruction]) +// }); +// // arr.push(async.apply(addReverseFieldValueReplace, ctx, p13nRule[instruction])); +// break; +// case 'preCustomFunction': +// arr.push({ +// type: 'preCustomFunction', +// fn: async.apply(executeCustomFunction, ctx, p13nRule[instruction]) +// }); +// break; +// default: +// } +// } +// } +// return arr.sort(sortFactoryFn(true)); +// }; /* eslint-enable no-loop-func */ -function addReverseFieldValueReplace(ctx, instruction, cb) { - reverseFieldValueReplacementFn(ctx, instruction, cb); -} +// function addReverseFieldValueReplace(ctx, instruction, cb) { +// reverseFieldValueReplacementFn(ctx, instruction, cb); +// } -function addReverseFieldReplace(ctx, instruction, cb) { - reverseFieldReplacementFn(ctx, instruction, cb); -} +// function addReverseFieldReplace(ctx, instruction, cb) { +// reverseFieldReplacementFn(ctx, instruction, cb); +// } /** @@ -295,84 +295,84 @@ function addLbFiltertoCtx(ctx, filter, cb) { * */ -function ProcessingFunction(instruction, fn) { - this.instruction = instruction; - this.fn = fn; - - this.execute = function processingFunctionExecuteFn(ctx) { - // console.log('this.instruction = ' + JSON.stringify(this.instruction)); - // console.log('this.fn = ' + this.fn); - - this.fn(ctx, instruction); - }; -} - - -function fieldReplacementFn(ctx, replacements, cb) { - var input; - var result = ctx.result || ctx.accdata; - - if (typeof result !== 'undefined' && !_.isEmpty(result)) { - input = ctx.result || ctx.accdata; - log.debug(ctx.options, 'fieldValueReplacementFn called. Resultset = ', - input + ' Replacements = ' + replacements); - } else if (typeof ctx.req.body !== 'undefined' && !_.isEmpty(ctx.req.body)) { - input = ctx.req.body; - log.debug(ctx.req.callContext, 'reverseFieldValueReplacementFn called. Input = ', - input + ' Replacements = ' + replacements); - } else { - return cb(); - } - - // let replaceField = utils.replaceField; - let replaceRecord = utils.replaceRecord; - - /** - * if input or result is array then iterates the process - * otherwise once calls update record function. - */ - if (Array.isArray(input)) { - var updatedResult = []; - for (var i in input) { - if (input.hasOwnProperty(i)) { - var record = input[i]; - updatedResult.push(replaceRecord(record, replacements)); - } - } - input = updatedResult; - } else { - var updatedRecord = replaceRecord(input, replacements); - input = updatedRecord; - } - - process.nextTick(function () { - return cb(); - }); -} - -function reverseFieldReplacementFn(ctx, rule, cb) { - // var input = ctx.args.data; - - if (rule !== null && typeof rule !== 'undefined') { - var revInputJson = {}; - - for (var key in rule) { - if (rule.hasOwnProperty(key)) { - var pos = key.lastIndexOf('\uFF0E'); - if (pos !== -1) { - var replaceAttr = key.substr(pos + 1); - var elsePart = key.substr(0, pos + 1); - revInputJson[elsePart + rule[key]] = replaceAttr; - } else { - revInputJson[rule[key]] = key; - } - } - } - fieldReplacementFn(ctx, revInputJson, cb); - } else { - return cb(); - } -} +// function ProcessingFunction(instruction, fn) { +// this.instruction = instruction; +// this.fn = fn; + +// this.execute = function processingFunctionExecuteFn(ctx) { +// // console.log('this.instruction = ' + JSON.stringify(this.instruction)); +// // console.log('this.fn = ' + this.fn); + +// this.fn(ctx, instruction); +// }; +// } + + +// function fieldReplacementFn(ctx, replacements, cb) { +// var input; +// var result = ctx.result || ctx.accdata; + +// if (typeof result !== 'undefined' && !_.isEmpty(result)) { +// input = ctx.result || ctx.accdata; +// log.debug(ctx.options, 'fieldValueReplacementFn called. Resultset = ', +// input + ' Replacements = ' + replacements); +// } else if (typeof ctx.req.body !== 'undefined' && !_.isEmpty(ctx.req.body)) { +// input = ctx.req.body; +// log.debug(ctx.req.callContext, 'reverseFieldValueReplacementFn called. Input = ', +// input + ' Replacements = ' + replacements); +// } else { +// return cb(); +// } + +// // let replaceField = utils.replaceField; +// let replaceRecord = utils.replaceRecord; + +// /** +// * if input or result is array then iterates the process +// * otherwise once calls update record function. +// */ +// if (Array.isArray(input)) { +// var updatedResult = []; +// for (var i in input) { +// if (input.hasOwnProperty(i)) { +// var record = input[i]; +// updatedResult.push(replaceRecord(record, replacements)); +// } +// } +// input = updatedResult; +// } else { +// var updatedRecord = replaceRecord(input, replacements); +// input = updatedRecord; +// } + +// process.nextTick(function () { +// return cb(); +// }); +// } + +// function reverseFieldReplacementFn(ctx, rule, cb) { +// // var input = ctx.args.data; + +// if (rule !== null && typeof rule !== 'undefined') { +// var revInputJson = {}; + +// for (var key in rule) { +// if (rule.hasOwnProperty(key)) { +// var pos = key.lastIndexOf('\uFF0E'); +// if (pos !== -1) { +// var replaceAttr = key.substr(pos + 1); +// var elsePart = key.substr(0, pos + 1); +// revInputJson[elsePart + rule[key]] = replaceAttr; +// } else { +// revInputJson[rule[key]] = key; +// } +// } +// } +// fieldReplacementFn(ctx, revInputJson, cb); +// } else { +// return cb(); +// } +// } /** * Field value replacement function. To be used when datasource does not support field value replacements. @@ -395,43 +395,43 @@ function reverseFieldReplacementFn(ctx, rule, cb) { * @name reverseFieldValueReplacementFn */ -function reverseFieldValueReplacementFn(ctx, rule, cb) { - // var input = ctx.args.data; - - if (rule !== null && typeof rule !== 'undefined') { - var revInputJson = {}; - for (var field in rule) { - if (rule.hasOwnProperty(field)) { - var temp = {}; - var rf = rule[field]; - for (var key in rf) { - if (rf.hasOwnProperty(key)) { - temp[rf[key]] = key; - } - } - - revInputJson[field] = temp; - } - } - fieldValueReplacementFn(ctx, revInputJson, cb); - } else { - return process.nextTick(function () { - return cb(); - }); - } -} +// function reverseFieldValueReplacementFn(ctx, rule, cb) { +// // var input = ctx.args.data; + +// if (rule !== null && typeof rule !== 'undefined') { +// var revInputJson = {}; +// for (var field in rule) { +// if (rule.hasOwnProperty(field)) { +// var temp = {}; +// var rf = rule[field]; +// for (var key in rf) { +// if (rf.hasOwnProperty(key)) { +// temp[rf[key]] = key; +// } +// } + +// revInputJson[field] = temp; +// } +// } +// fieldValueReplacementFn(ctx, revInputJson, cb); +// } else { +// return process.nextTick(function () { +// return cb(); +// }); +// } +// } // old code -function executeCustomFunctionFn(ctx, customFunctionName) { - // TODO: Security check - // var custFn = new Function('ctx', customFunction); - var custFn = function customFnn(ctx, customFunction) { - customFunction(ctx); - }; +// function executeCustomFunctionFn(ctx, customFunctionName) { +// // TODO: Security check +// // var custFn = new Function('ctx', customFunction); +// var custFn = function customFnn(ctx, customFunction) { +// customFunction(ctx); +// }; - log.debug(ctx.options, 'function - ', customFunction); - custFn(ctx); -} +// log.debug(ctx.options, 'function - ', customFunction); +// custFn(ctx); +// } // execute custom function function executeCustomFunction(ctx, instruction, cb) { @@ -449,17 +449,17 @@ function executeCustomFunction(ctx, instruction, cb) { * @param {object} instruction - instructions. * @param {function} cb - callback function. */ -function addFilter(ctx, instruction, cb) { //Tests: t9 - // TODO: Check the datasource to which this model is attached. - // If the datasource is capable of doing filter queries add a where clause. +// function addFilter(ctx, instruction, cb) { //Tests: t9 +// // TODO: Check the datasource to which this model is attached. +// // If the datasource is capable of doing filter queries add a where clause. - var dsSupportFilter = true; +// var dsSupportFilter = true; - if (dsSupportFilter) { - addWhereClause(ctx, instruction, cb); - } - // else {} -} +// if (dsSupportFilter) { +// addWhereClause(ctx, instruction, cb); +// } +// // else {} +// } // Processes a filter instruction. filter instruction schema is same like loopback filter schema. function addLbFilter(ctx, instruction, cb) { @@ -475,138 +475,138 @@ function addLbFilter(ctx, instruction, cb) { * value replacements by iterating the results retrieved. */ -function fieldValueReplacementFn(ctx, replacements, cb) { - var input; - - var result = ctx.result || ctx.accdata; - - if (typeof result !== 'undefined' && !_.isEmpty(result)) { - input = ctx.result || ctx.accdata; - log.debug(ctx.options, 'fieldValueReplacementFn called. Resultset = ', - input + ' Replacements = ' + replacements); - } else if (typeof ctx.instance !== 'undefined' && !_.isEmpty(ctx.instance)) { - input = ctx.instance; - log.debug(ctx.options, 'reverseFieldValueReplacementFn called. Input = ', - input + ' Replacements = ' + replacements); - } else { - return process.nextTick(function () { - return cb(); - }); - } - - - // function replaceValue(record, replacement, value) { - // var pos = replacement.indexOf('\uFF0E'); - // var key; - // var elsePart; - // if (pos !== null && typeof pos !== 'undefined' && pos !== -1) { - // key = replacement.substr(0, pos); - // elsePart = replacement.substr(pos + 1); - // } else { - // key = replacement; - // } - - - // if (typeof record[key] !== 'undefined' && Array.isArray(record[key])) { - // var newValue = record[key]; - // record[key].forEach(function (element, index) { - // if (value[element]) { - // newValue[index] = value[element]; - // } - // }); - // if (typeof record.__data !== 'undefined') { - // record.__data[key] = newValue; - // } else { - // record[key] = newValue; - // } - // } else if (typeof record[key] !== 'undefined' && typeof record[key] === 'object') { - // replaceValue(record[key], elsePart, value); - // } else if (typeof record[key] !== 'undefined' && typeof record[key] !== 'object') { - // if (value.hasOwnProperty(record[key])) { - // if (typeof record.__data !== 'undefined') { - // record.__data[key] = value[record[key]]; - // } else { - // record[key] = value[record[key]]; - // } - // } - // } - // } - - // function replaceRecord(record, replacements) { - // var keys = Object.keys(JSON.parse(JSON.stringify(replacements))); - // for (var attr in keys) { - // if (keys.hasOwnProperty(attr)) { - // replaceValue(record, keys[attr], replacements[keys[attr]]); - // } - // } - // return record; - // } - - let replaceRecord = utils.replaceRecordFactory(utils.replaceValue); - - if (Array.isArray(input)) { - var updatedResult = []; - for (var i in input) { - if (input.hasOwnProperty(i)) { - var record = input[i]; - updatedResult.push(replaceRecord(record, replacements)); - } - } - input = updatedResult; - } else { - var updatedRecord = replaceRecord(input, replacements); - input = updatedRecord; - } - process.nextTick(function () { - return cb(); - }); -} - -function addFieldReplace(ctx, instruction, cb) { - fieldReplacementFn(ctx, instruction, cb); -} +// function fieldValueReplacementFn(ctx, replacements, cb) { +// var input; + +// var result = ctx.result || ctx.accdata; + +// if (typeof result !== 'undefined' && !_.isEmpty(result)) { +// input = ctx.result || ctx.accdata; +// log.debug(ctx.options, 'fieldValueReplacementFn called. Resultset = ', +// input + ' Replacements = ' + replacements); +// } else if (typeof ctx.instance !== 'undefined' && !_.isEmpty(ctx.instance)) { +// input = ctx.instance; +// log.debug(ctx.options, 'reverseFieldValueReplacementFn called. Input = ', +// input + ' Replacements = ' + replacements); +// } else { +// return process.nextTick(function () { +// return cb(); +// }); +// } + + +// // function replaceValue(record, replacement, value) { +// // var pos = replacement.indexOf('\uFF0E'); +// // var key; +// // var elsePart; +// // if (pos !== null && typeof pos !== 'undefined' && pos !== -1) { +// // key = replacement.substr(0, pos); +// // elsePart = replacement.substr(pos + 1); +// // } else { +// // key = replacement; +// // } + + +// // if (typeof record[key] !== 'undefined' && Array.isArray(record[key])) { +// // var newValue = record[key]; +// // record[key].forEach(function (element, index) { +// // if (value[element]) { +// // newValue[index] = value[element]; +// // } +// // }); +// // if (typeof record.__data !== 'undefined') { +// // record.__data[key] = newValue; +// // } else { +// // record[key] = newValue; +// // } +// // } else if (typeof record[key] !== 'undefined' && typeof record[key] === 'object') { +// // replaceValue(record[key], elsePart, value); +// // } else if (typeof record[key] !== 'undefined' && typeof record[key] !== 'object') { +// // if (value.hasOwnProperty(record[key])) { +// // if (typeof record.__data !== 'undefined') { +// // record.__data[key] = value[record[key]]; +// // } else { +// // record[key] = value[record[key]]; +// // } +// // } +// // } +// // } + +// // function replaceRecord(record, replacements) { +// // var keys = Object.keys(JSON.parse(JSON.stringify(replacements))); +// // for (var attr in keys) { +// // if (keys.hasOwnProperty(attr)) { +// // replaceValue(record, keys[attr], replacements[keys[attr]]); +// // } +// // } +// // return record; +// // } + +// let replaceRecord = utils.replaceRecordFactory(utils.replaceValue); + +// if (Array.isArray(input)) { +// var updatedResult = []; +// for (var i in input) { +// if (input.hasOwnProperty(i)) { +// var record = input[i]; +// updatedResult.push(replaceRecord(record, replacements)); +// } +// } +// input = updatedResult; +// } else { +// var updatedRecord = replaceRecord(input, replacements); +// input = updatedRecord; +// } +// process.nextTick(function () { +// return cb(); +// }); +// } + +// function addFieldReplace(ctx, instruction, cb) { +// fieldReplacementFn(ctx, instruction, cb); +// } // Function to add Sort (Order By) to the query. -function addSort(ctx, instruction, cb) { - // { order: 'propertyName ' } -- sort by single field - // { order: ['propertyName ', 'propertyName ',...] } --sort by mulitple fields - - var dsSupportSort = true; - if (dsSupportSort) { - var query = ctx.args.filter || {}; - if (query) { - if (typeof query.order === 'string') { - query.order = [query.order]; - } - - var tempKeys = []; - - if (query.order && query.order.length >= 1) { - query.order.forEach(function addSortQueryOrderForEachFn(item) { - tempKeys.push(item.split(' ')[0]); - }); - } - - // create the order expression based on the instruction passed - var orderExp = createOrderExp(instruction, tempKeys); - - if (typeof query.order === 'undefined') { - query.order = orderExp; - } else { - query.order = query.order.concat(orderExp); - } - query.order = _.uniq(query.order); - ctx.args.filter = ctx.args.filter || {}; - ctx.args.filter.order = query.order; - cb(); - } else { - cb(); - } - } else { - addPostProcessingFunction(ctx, 'sortInMemory', instruction, sortInMemory); - cb(); - } -} +// function addSort(ctx, instruction, cb) { +// // { order: 'propertyName ' } -- sort by single field +// // { order: ['propertyName ', 'propertyName ',...] } --sort by mulitple fields + +// var dsSupportSort = true; +// if (dsSupportSort) { +// var query = ctx.args.filter || {}; +// if (query) { +// if (typeof query.order === 'string') { +// query.order = [query.order]; +// } + +// var tempKeys = []; + +// if (query.order && query.order.length >= 1) { +// query.order.forEach(function addSortQueryOrderForEachFn(item) { +// tempKeys.push(item.split(' ')[0]); +// }); +// } + +// // create the order expression based on the instruction passed +// var orderExp = createOrderExp(instruction, tempKeys); + +// if (typeof query.order === 'undefined') { +// query.order = orderExp; +// } else { +// query.order = query.order.concat(orderExp); +// } +// query.order = _.uniq(query.order); +// ctx.args.filter = ctx.args.filter || {}; +// ctx.args.filter.order = query.order; +// cb(); +// } else { +// cb(); +// } +// } else { +// addPostProcessingFunction(ctx, 'sortInMemory', instruction, sortInMemory); +// cb(); +// } +// } /** * Custom function @@ -621,13 +621,13 @@ function getCustomFunction() { } /* eslint-disable */ -function addCustomFunction(ctx, instruction, cb) { - // instruction has the customFunction Name - // Datasource does not support field name replacement. Add it as a post processing function - addPostProcessingFunction(ctx, 'customFunction', instruction, executeCustomFunctionFn); - cb(); -} -/* eslint-enable */ +// function addCustomFunction(ctx, instruction, cb) { +// // instruction has the customFunction Name +// // Datasource does not support field name replacement. Add it as a post processing function +// addPostProcessingFunction(ctx, 'customFunction', instruction, executeCustomFunctionFn); +// cb(); +// } +// /* eslint-enable */ function createOrderExp(instruction, tempKeys) { if (!Array.isArray(instruction)) { @@ -699,123 +699,123 @@ function sortInMemory(ctx, options) { * Instantiate a new post processing function and adds to the request context. */ -function addPostProcessingFunction(ctx, func, instruction, fn) { - var callContext = ctx.req.callContext = ctx.req.callContext || {}; - - callContext.postProcessingFns = callContext.postProcessingFns || {}; - callContext.postProcessingFns[callContext.modelName] = callContext.postProcessingFns[callContext.modelName] || []; - var prcFn = new ProcessingFunction(instruction, fn); - var addPrcFn = callContext.postProcessingFns[callContext.modelName].some(function (processingFn) { - return processingFn.fn.name === prcFn.fn.name; - }); - if (!addPrcFn) { - if (func === 'fieldReplace') { - callContext.postProcessingFns[callContext.modelName].push(prcFn); - } else { - callContext.postProcessingFns[callContext.modelName].unshift(prcFn); - } - } - // console.log('callContext so far - ' + JSON.stringify(callContext)); -} - -/* - * Function to mask the certain fields from the output field List - * */ -function maskFields(ctx, instruction, cb) { - var dsSupportMask = true; - if (dsSupportMask) { - ctx.args.filter = ctx.args.filter || {}; - var query = ctx.args.filter; - if (!query) { - return cb(); - } - var keys = Object.keys(instruction); - var exp = {}; - if (typeof query.fields === 'undefined') { - for (var i = 0, length = keys.length; i < length; i++) { - var key = keys[i]; - key = key.indexOf('|') > -1 ? key.replace(/\|/g, '.') : key; - exp[key] = false; - } - query.fields = exp; - } - // else { - // var fieldList = query.fields; - // fieldList = _.filter(fieldList, function (item) { - // return keys.indexOf(item) === -1 - // }); - // query.fields = fieldList; - // } - } - cb(); -} - -function maskCharacters(ctx, charMaskRules, cb) { - var input; - var result = ctx.result || ctx.accdata; - - if (result && !_.isEmpty(result)) { - input = result; - } else if (ctx.req.body && !_.isEmpty(ctx.req.body)) { - input = ctx.req.body; - } else { - return cb(); - } - - function modifyField(record, property, rule) { - var pos = property.indexOf('.'); - if (pos !== -1) { - var key = property.substr(0, pos); - var innerProp = property.substr(pos + 1); - } else { - key = property; - } - - if (record[key] && typeof record[key] === 'object') { - modifyField(record[key], innerProp, rule); - } else if (record[key] && typeof record[key] !== 'object') { - var char = rule.maskCharacter || 'X'; - var flags = rule.flags; - var regex = flags ? new RegExp(rule.pattern, flags) : new RegExp(rule.pattern); - var groups = record[key].match(regex) || []; - var masking = rule.mask || []; - var newVal = rule.format || []; - if (Array.isArray(newVal)) { - for (let i = 1; i < groups.length; i++) { - newVal.push('$' + i); - } - newVal = newVal.join(''); - } - masking.forEach(function (elem) { - newVal = newVal.replace(elem, new Array(groups[elem.substr(1)].length + 1).join(char)); - }); - for (let i = 0; i < groups.length; i++) { - newVal = newVal.replace('$' + i, groups[i]); - } - // normally we set __data but now, lb3!!! - record[key] = newVal; - } - } - - function applyRuleOnRecord(record, charMaskRules) { - Object.keys(charMaskRules).forEach(function (key) { - modifyField(record, key, charMaskRules[key]); - }); - return record; - } - - if (Array.isArray(input)) { - var updatedResult = []; - input.forEach(function (record) { - updatedResult.push(applyRuleOnRecord(record, charMaskRules)); - }); - input = updatedResult; - } else { - input = applyRuleOnRecord(input, charMaskRules); - } - - return cb(); -} +// // function addPostProcessingFunction(ctx, func, instruction, fn) { +// // var callContext = ctx.req.callContext = ctx.req.callContext || {}; + +// // callContext.postProcessingFns = callContext.postProcessingFns || {}; +// // callContext.postProcessingFns[callContext.modelName] = callContext.postProcessingFns[callContext.modelName] || []; +// // var prcFn = new ProcessingFunction(instruction, fn); +// // var addPrcFn = callContext.postProcessingFns[callContext.modelName].some(function (processingFn) { +// // return processingFn.fn.name === prcFn.fn.name; +// // }); +// // if (!addPrcFn) { +// // if (func === 'fieldReplace') { +// // callContext.postProcessingFns[callContext.modelName].push(prcFn); +// // } else { +// // callContext.postProcessingFns[callContext.modelName].unshift(prcFn); +// // } +// // } +// // // console.log('callContext so far - ' + JSON.stringify(callContext)); +// // } + +// /* +// * Function to mask the certain fields from the output field List +// * */ +// function maskFields(ctx, instruction, cb) { +// var dsSupportMask = true; +// if (dsSupportMask) { +// ctx.args.filter = ctx.args.filter || {}; +// var query = ctx.args.filter; +// if (!query) { +// return cb(); +// } +// var keys = Object.keys(instruction); +// var exp = {}; +// if (typeof query.fields === 'undefined') { +// for (var i = 0, length = keys.length; i < length; i++) { +// var key = keys[i]; +// key = key.indexOf('|') > -1 ? key.replace(/\|/g, '.') : key; +// exp[key] = false; +// } +// query.fields = exp; +// } +// // else { +// // var fieldList = query.fields; +// // fieldList = _.filter(fieldList, function (item) { +// // return keys.indexOf(item) === -1 +// // }); +// // query.fields = fieldList; +// // } +// } +// cb(); +// } + +// function maskCharacters(ctx, charMaskRules, cb) { +// var input; +// var result = ctx.result || ctx.accdata; + +// if (result && !_.isEmpty(result)) { +// input = result; +// } else if (ctx.req.body && !_.isEmpty(ctx.req.body)) { +// input = ctx.req.body; +// } else { +// return cb(); +// } + +// function modifyField(record, property, rule) { +// var pos = property.indexOf('.'); +// if (pos !== -1) { +// var key = property.substr(0, pos); +// var innerProp = property.substr(pos + 1); +// } else { +// key = property; +// } + +// if (record[key] && typeof record[key] === 'object') { +// modifyField(record[key], innerProp, rule); +// } else if (record[key] && typeof record[key] !== 'object') { +// var char = rule.maskCharacter || 'X'; +// var flags = rule.flags; +// var regex = flags ? new RegExp(rule.pattern, flags) : new RegExp(rule.pattern); +// var groups = record[key].match(regex) || []; +// var masking = rule.mask || []; +// var newVal = rule.format || []; +// if (Array.isArray(newVal)) { +// for (let i = 1; i < groups.length; i++) { +// newVal.push('$' + i); +// } +// newVal = newVal.join(''); +// } +// masking.forEach(function (elem) { +// newVal = newVal.replace(elem, new Array(groups[elem.substr(1)].length + 1).join(char)); +// }); +// for (let i = 0; i < groups.length; i++) { +// newVal = newVal.replace('$' + i, groups[i]); +// } +// // normally we set __data but now, lb3!!! +// record[key] = newVal; +// } +// } + +// function applyRuleOnRecord(record, charMaskRules) { +// Object.keys(charMaskRules).forEach(function (key) { +// modifyField(record, key, charMaskRules[key]); +// }); +// return record; +// } + +// if (Array.isArray(input)) { +// var updatedResult = []; +// input.forEach(function (record) { +// updatedResult.push(applyRuleOnRecord(record, charMaskRules)); +// }); +// input = updatedResult; +// } else { +// input = applyRuleOnRecord(input, charMaskRules); +// } + +// return cb(); +// } const utils = { /** @@ -1458,10 +1458,6 @@ function init(app) { } module.exports = { - getPersonalizationRuleForModel: getPersonalizationRuleForModel, - applyPersonalizationRule: applyPersonalizationRule, - applyReversePersonalizationRule: applyReversePersonalizationRule, - execute: execute, loadCustomFunction, getCustomFunction, applyServicePersonalization, From 424e488111167f9718ab95c9db937e37b0a3c82f Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 28 Apr 2020 21:03:22 +0530 Subject: [PATCH 25/80] code cleanup --- lib/service-personalizer.js | 678 ++---------------------------------- 1 file changed, 24 insertions(+), 654 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 9e3acf3..0c8e3d3 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -1,13 +1,13 @@ /** * - * ©2018-2019 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), + * ©2018-2020 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), * Bangalore, India. All Rights Reserved. * */ /** * Service personalization module. Optimizes and applies one personalization function. * - * @module EV Service Personalizer + * @module oe-service-personalization/lib/service-personalizer * @author Atul Pandit, gourav_gupta, pradeep_tippa, arun_jayapal (aka deostroll) */ @@ -32,201 +32,6 @@ const { nextTick } = require('./utils'); var sortFactoryFn = (reverse = false) => (first, second) => first.type === 'fieldReplace' ? (reverse ? -1 : 1) : (reverse ? 1 : -1); -/** - * - * This function returns personalization rule for modelName if exists. - * - * @param {String} modelName - Model Name - * @param {object} ctx - context - * @param {callback} callback - callback function - * @function - * @name getPersonalizationRuleForModel - */ -// var getPersonalizationRuleForModel = function getPersonalizationRuleForModelFn(modelName, ctx, callback) { -// log.debug(ctx.req.callContext, 'getPersonalizationRuleForModel called for model - ', modelName); - -// var PersonalizationRule = loopback.findModel('PersonalizationRule'); - -// var findByModelNameQuery = { -// where: { -// modelName: modelName, -// disabled: false -// } -// }; - -// PersonalizationRule.find(findByModelNameQuery, ctx.req.callContext, function getPersonalizationRuleForModelFindCb(err, result) { -// log.debug(ctx.req.callContext, 'Query result = ', result); -// if (err) { -// // TODO: Error getting personalization rule.. what should be done? Continue or stop? -// log.debug(ctx.req.callContext, 'Error getting personalization rule for model [', modelName, ']. skipping personalization'); -// return callback(null); -// } - -// if (result && result.length > 0) { -// log.debug(ctx.req.callContext, 'Returning personzalition rule'); -// return callback(result[0]); -// } -// log.debug(ctx.req.callContext, 'Personalization rules not defined for model [', modelName, ']. skipping personalization'); -// return callback(null); -// }); -// }; - -/** - * - * This function add functions to an array postProcessingFunctions which will execute to - * apply personalization rules after getting result. - * - * @param {Object} ctx - loopback context - * @param {Object} p13nRule - Personalization Rule - * @param {callback} callback - callback function - * @function - * @name applyPersonalizationRule - */ - -// var applyPersonalizationRule = function applyPersonalizationRuleFn(ctx, p13nRule) { -// var arr = []; - -// log.debug(ctx.req.callContext, 'applying Personalizing ctx with function - ', p13nRule); - -// var instructions = Object.keys(p13nRule); - -// // TODO:Check if all instructions can be applied in parallel in asynch way. -// // instructions.forEach(function (instruction) { - -// for (var i in instructions) { -// if (instructions.hasOwnProperty(i)) { -// var instruction = instructions[i]; -// switch (instruction) { -// case 'lbFilter': -// arr.push({ -// type: 'lbFilter', -// fn: async.apply(addLbFilter, ctx, p13nRule[instruction]) -// }); -// // arr.push(async.apply(addLbFilter, ctx, p13nRule[instruction])); -// break; -// case 'filter': -// arr.push({ -// type: 'filter', -// fn: async.apply(addFilter, ctx, p13nRule[instruction]) -// }); -// // arr.push(async.apply(addFilter, ctx, p13nRule[instruction])); -// break; -// case 'fieldReplace': -// arr.push({ -// type: 'fieldReplace', -// fn: async.apply(addFieldReplace, ctx, p13nRule[instruction]) -// }); -// // arr.push(async.apply(addFieldReplace, ctx, p13nRule[instruction])); -// break; -// case 'fieldValueReplace': -// arr.push({ -// type: 'fieldValueReplace', -// fn: async.apply(addFieldValueReplace, ctx, p13nRule[instruction]) -// }); -// // arr.push(async.apply(addFieldValueReplace, ctx, p13nRule[instruction])); -// break; -// case 'sort': -// arr.push({ -// type: 'sort', -// fn: async.apply(addSort, ctx, p13nRule[instruction]) -// }); -// // arr.push(async.apply(addSort, ctx, p13nRule[instruction])); -// break; -// case 'postCustomFunction': -// arr.push({ -// type: 'postCustomFunction', -// fn: async.apply(executeCustomFunction, ctx, p13nRule[instruction]) -// }); -// break; -// case 'union': -// // unionResults(ctx, p13nRule[instruction]); -// break; -// case 'mask': -// arr.push({ -// type: 'mask', -// fn: async.apply(maskFields, ctx, p13nRule[instruction]) -// }); -// // arr.push(async.apply(maskFields, ctx, p13nRule[instruction])); -// break; -// case 'fieldMask': -// arr.push({ -// type: 'fieldMask', -// fn: async.apply(maskCharacters, ctx, p13nRule[instruction]) -// }); -// // arr.push(async.apply(maskCharacters, ctx, p13nRule[instruction])); -// break; -// default: -// } -// } -// } -// return arr.sort(sortFactoryFn()); -// }; - -// function execute(arr, callback) { -// async.parallel(arr, function applyPersonalizationRuleAsyncParallelFn(err, results) { -// if (err) { -// return callback(err); -// } -// callback(); -// }); -// } - -// function addFieldValueReplace(ctx, instruction, cb) { -// return fieldValueReplacementFn(ctx, instruction, cb); -// } - - -/* eslint-disable no-loop-func */ -// var applyReversePersonalizationRule = function applyReversePersonalizationRuleFn(ctx, p13nRule, callback) { -// log.debug(ctx.options, 'Reverse Personalizing ctx with function - ', p13nRule); - -// var instructions = Object.keys(p13nRule); - -// var arr = []; -// // TODO:Check if all instructions can be applied in parallel in asynch way. -// // instructions.forEach(function (instruction) { - -// for (var i in instructions) { -// if (instructions.hasOwnProperty(i)) { -// var instruction = instructions[i]; -// switch (instruction) { -// case 'fieldReplace': -// arr.push({ -// type: 'fieldReplace', -// fn: async.apply(addReverseFieldReplace, ctx, p13nRule[instruction]) -// }); -// // arr.push(async.apply(addReverseFieldReplace, ctx, p13nRule[instruction])); -// break; -// case 'fieldValueReplace': -// arr.push({ -// type: 'fieldValueReplace', -// fn: async.apply(addReverseFieldValueReplace, ctx, p13nRule[instruction]) -// }); -// // arr.push(async.apply(addReverseFieldValueReplace, ctx, p13nRule[instruction])); -// break; -// case 'preCustomFunction': -// arr.push({ -// type: 'preCustomFunction', -// fn: async.apply(executeCustomFunction, ctx, p13nRule[instruction]) -// }); -// break; -// default: -// } -// } -// } -// return arr.sort(sortFactoryFn(true)); -// }; -/* eslint-enable no-loop-func */ - -// function addReverseFieldValueReplace(ctx, instruction, cb) { -// reverseFieldValueReplacementFn(ctx, instruction, cb); -// } - -// function addReverseFieldReplace(ctx, instruction, cb) { -// reverseFieldReplacementFn(ctx, instruction, cb); -// } - - /** * Function to add 'where' clause in the datasource filter query. */ @@ -288,151 +93,6 @@ function addLbFiltertoCtx(ctx, filter, cb) { cb(); } -/* - * Object wrapper to add a processing function in the context. - * Wraps the instrunctions and reference to the actual function to be called. - * Processing functions are invoked before posting data or after data is retried by the API - * - */ - -// function ProcessingFunction(instruction, fn) { -// this.instruction = instruction; -// this.fn = fn; - -// this.execute = function processingFunctionExecuteFn(ctx) { -// // console.log('this.instruction = ' + JSON.stringify(this.instruction)); -// // console.log('this.fn = ' + this.fn); - -// this.fn(ctx, instruction); -// }; -// } - - -// function fieldReplacementFn(ctx, replacements, cb) { -// var input; -// var result = ctx.result || ctx.accdata; - -// if (typeof result !== 'undefined' && !_.isEmpty(result)) { -// input = ctx.result || ctx.accdata; -// log.debug(ctx.options, 'fieldValueReplacementFn called. Resultset = ', -// input + ' Replacements = ' + replacements); -// } else if (typeof ctx.req.body !== 'undefined' && !_.isEmpty(ctx.req.body)) { -// input = ctx.req.body; -// log.debug(ctx.req.callContext, 'reverseFieldValueReplacementFn called. Input = ', -// input + ' Replacements = ' + replacements); -// } else { -// return cb(); -// } - -// // let replaceField = utils.replaceField; -// let replaceRecord = utils.replaceRecord; - -// /** -// * if input or result is array then iterates the process -// * otherwise once calls update record function. -// */ -// if (Array.isArray(input)) { -// var updatedResult = []; -// for (var i in input) { -// if (input.hasOwnProperty(i)) { -// var record = input[i]; -// updatedResult.push(replaceRecord(record, replacements)); -// } -// } -// input = updatedResult; -// } else { -// var updatedRecord = replaceRecord(input, replacements); -// input = updatedRecord; -// } - -// process.nextTick(function () { -// return cb(); -// }); -// } - -// function reverseFieldReplacementFn(ctx, rule, cb) { -// // var input = ctx.args.data; - -// if (rule !== null && typeof rule !== 'undefined') { -// var revInputJson = {}; - -// for (var key in rule) { -// if (rule.hasOwnProperty(key)) { -// var pos = key.lastIndexOf('\uFF0E'); -// if (pos !== -1) { -// var replaceAttr = key.substr(pos + 1); -// var elsePart = key.substr(0, pos + 1); -// revInputJson[elsePart + rule[key]] = replaceAttr; -// } else { -// revInputJson[rule[key]] = key; -// } -// } -// } -// fieldReplacementFn(ctx, revInputJson, cb); -// } else { -// return cb(); -// } -// } - -/** - * Field value replacement function. To be used when datasource does not support field value replacements. - * It simply iterates over the resultset and carries our field value replacements. - * - * @param {Object} ctx - loopback context - * @param {Object} replacements - field value replacement rule - * @function - * @name fieldValueReplacementFn - */ - - -/** - * Reverse Field value replacement function. To be used for reverting field value replacements. - * It simply iterates over the posted data and reverts field value replacements. - * - * @param {Object} ctx - loopback context - * @param {Object} rule - field value replacement rule - * @function - * @name reverseFieldValueReplacementFn - */ - -// function reverseFieldValueReplacementFn(ctx, rule, cb) { -// // var input = ctx.args.data; - -// if (rule !== null && typeof rule !== 'undefined') { -// var revInputJson = {}; -// for (var field in rule) { -// if (rule.hasOwnProperty(field)) { -// var temp = {}; -// var rf = rule[field]; -// for (var key in rf) { -// if (rf.hasOwnProperty(key)) { -// temp[rf[key]] = key; -// } -// } - -// revInputJson[field] = temp; -// } -// } -// fieldValueReplacementFn(ctx, revInputJson, cb); -// } else { -// return process.nextTick(function () { -// return cb(); -// }); -// } -// } - -// old code -// function executeCustomFunctionFn(ctx, customFunctionName) { -// // TODO: Security check -// // var custFn = new Function('ctx', customFunction); -// var custFn = function customFnn(ctx, customFunction) { -// customFunction(ctx); -// }; - -// log.debug(ctx.options, 'function - ', customFunction); -// custFn(ctx); -// } - // execute custom function function executeCustomFunction(ctx, instruction, cb) { let customFunctionName = instruction.functionName; @@ -441,173 +101,11 @@ function executeCustomFunction(ctx, instruction, cb) { } -/** - * Processes a 'filter' instruction. This method checks if underlying datasource to which the model is attached to - * supports query based filtering. If yes, it adds a 'where' clause in the query. Otherwise creates a post processing - * function which performs filtering on the resultset retrieved. - * @param {object} ctx - context. - * @param {object} instruction - instructions. - * @param {function} cb - callback function. - */ -// function addFilter(ctx, instruction, cb) { //Tests: t9 -// // TODO: Check the datasource to which this model is attached. -// // If the datasource is capable of doing filter queries add a where clause. - -// var dsSupportFilter = true; - -// if (dsSupportFilter) { -// addWhereClause(ctx, instruction, cb); -// } -// // else {} -// } - // Processes a filter instruction. filter instruction schema is same like loopback filter schema. function addLbFilter(ctx, instruction, cb) { addLbFilterClause(ctx, instruction, cb); } -/** - * Processes a 'fieldValueReplace' instruction. - * This method checks if underlying datasource to which the model is attached - * to supports 'field value' replacements. - * If yes, it delegates it to datasource by modifying the query. - * Otherwise creates a post processing function which performs field - * value replacements by iterating the results retrieved. - */ - -// function fieldValueReplacementFn(ctx, replacements, cb) { -// var input; - -// var result = ctx.result || ctx.accdata; - -// if (typeof result !== 'undefined' && !_.isEmpty(result)) { -// input = ctx.result || ctx.accdata; -// log.debug(ctx.options, 'fieldValueReplacementFn called. Resultset = ', -// input + ' Replacements = ' + replacements); -// } else if (typeof ctx.instance !== 'undefined' && !_.isEmpty(ctx.instance)) { -// input = ctx.instance; -// log.debug(ctx.options, 'reverseFieldValueReplacementFn called. Input = ', -// input + ' Replacements = ' + replacements); -// } else { -// return process.nextTick(function () { -// return cb(); -// }); -// } - - -// // function replaceValue(record, replacement, value) { -// // var pos = replacement.indexOf('\uFF0E'); -// // var key; -// // var elsePart; -// // if (pos !== null && typeof pos !== 'undefined' && pos !== -1) { -// // key = replacement.substr(0, pos); -// // elsePart = replacement.substr(pos + 1); -// // } else { -// // key = replacement; -// // } - - -// // if (typeof record[key] !== 'undefined' && Array.isArray(record[key])) { -// // var newValue = record[key]; -// // record[key].forEach(function (element, index) { -// // if (value[element]) { -// // newValue[index] = value[element]; -// // } -// // }); -// // if (typeof record.__data !== 'undefined') { -// // record.__data[key] = newValue; -// // } else { -// // record[key] = newValue; -// // } -// // } else if (typeof record[key] !== 'undefined' && typeof record[key] === 'object') { -// // replaceValue(record[key], elsePart, value); -// // } else if (typeof record[key] !== 'undefined' && typeof record[key] !== 'object') { -// // if (value.hasOwnProperty(record[key])) { -// // if (typeof record.__data !== 'undefined') { -// // record.__data[key] = value[record[key]]; -// // } else { -// // record[key] = value[record[key]]; -// // } -// // } -// // } -// // } - -// // function replaceRecord(record, replacements) { -// // var keys = Object.keys(JSON.parse(JSON.stringify(replacements))); -// // for (var attr in keys) { -// // if (keys.hasOwnProperty(attr)) { -// // replaceValue(record, keys[attr], replacements[keys[attr]]); -// // } -// // } -// // return record; -// // } - -// let replaceRecord = utils.replaceRecordFactory(utils.replaceValue); - -// if (Array.isArray(input)) { -// var updatedResult = []; -// for (var i in input) { -// if (input.hasOwnProperty(i)) { -// var record = input[i]; -// updatedResult.push(replaceRecord(record, replacements)); -// } -// } -// input = updatedResult; -// } else { -// var updatedRecord = replaceRecord(input, replacements); -// input = updatedRecord; -// } -// process.nextTick(function () { -// return cb(); -// }); -// } - -// function addFieldReplace(ctx, instruction, cb) { -// fieldReplacementFn(ctx, instruction, cb); -// } - -// Function to add Sort (Order By) to the query. -// function addSort(ctx, instruction, cb) { -// // { order: 'propertyName ' } -- sort by single field -// // { order: ['propertyName ', 'propertyName ',...] } --sort by mulitple fields - -// var dsSupportSort = true; -// if (dsSupportSort) { -// var query = ctx.args.filter || {}; -// if (query) { -// if (typeof query.order === 'string') { -// query.order = [query.order]; -// } - -// var tempKeys = []; - -// if (query.order && query.order.length >= 1) { -// query.order.forEach(function addSortQueryOrderForEachFn(item) { -// tempKeys.push(item.split(' ')[0]); -// }); -// } - -// // create the order expression based on the instruction passed -// var orderExp = createOrderExp(instruction, tempKeys); - -// if (typeof query.order === 'undefined') { -// query.order = orderExp; -// } else { -// query.order = query.order.concat(orderExp); -// } -// query.order = _.uniq(query.order); -// ctx.args.filter = ctx.args.filter || {}; -// ctx.args.filter.order = query.order; -// cb(); -// } else { -// cb(); -// } -// } else { -// addPostProcessingFunction(ctx, 'sortInMemory', instruction, sortInMemory); -// cb(); -// } -// } - /** * Custom function */ @@ -620,15 +118,6 @@ function getCustomFunction() { return customFunction; } -/* eslint-disable */ -// function addCustomFunction(ctx, instruction, cb) { -// // instruction has the customFunction Name -// // Datasource does not support field name replacement. Add it as a post processing function -// addPostProcessingFunction(ctx, 'customFunction', instruction, executeCustomFunctionFn); -// cb(); -// } -// /* eslint-enable */ - function createOrderExp(instruction, tempKeys) { if (!Array.isArray(instruction)) { instruction = [instruction]; @@ -666,155 +155,36 @@ function createOrderExp(instruction, tempKeys) { return orderExp; } -/* To be used when database doesnt support sort OR sort needs to be done in memory*/ -function sortInMemory(ctx, options) { - var result = ctx.result; - if (typeof result === 'undefined') { - return; - } - if (!Array.isArray(options)) { - options = [options]; - } - var keys = []; - var values = []; - for (var index in options) { - if (options.hasOwnProperty(index)) { - var key = Object.keys(options[index])[0]; - values.push(options[index][key]); - key = key.indexOf('|') > -1 ? key.replace(/\|/g, '.') : key; - keys.push(key); - } - } - var updatedResults; - if (Array.isArray(result)) { - // lodash version 3.10.1 uses sortByOrder;version 4.0.0 uses OrderBy - updatedResults = _.orderBy(result, keys, values); - } - if (updatedResults) { - ctx.result = updatedResults; - } -} -/** - * Instantiate a new post processing function and adds to the request context. - */ +// (Arun 2020-04-28 21:01:57) - retaining the below function for future use -// // function addPostProcessingFunction(ctx, func, instruction, fn) { -// // var callContext = ctx.req.callContext = ctx.req.callContext || {}; - -// // callContext.postProcessingFns = callContext.postProcessingFns || {}; -// // callContext.postProcessingFns[callContext.modelName] = callContext.postProcessingFns[callContext.modelName] || []; -// // var prcFn = new ProcessingFunction(instruction, fn); -// // var addPrcFn = callContext.postProcessingFns[callContext.modelName].some(function (processingFn) { -// // return processingFn.fn.name === prcFn.fn.name; -// // }); -// // if (!addPrcFn) { -// // if (func === 'fieldReplace') { -// // callContext.postProcessingFns[callContext.modelName].push(prcFn); -// // } else { -// // callContext.postProcessingFns[callContext.modelName].unshift(prcFn); -// // } -// // } -// // // console.log('callContext so far - ' + JSON.stringify(callContext)); -// // } - -// /* -// * Function to mask the certain fields from the output field List -// * */ -// function maskFields(ctx, instruction, cb) { -// var dsSupportMask = true; -// if (dsSupportMask) { -// ctx.args.filter = ctx.args.filter || {}; -// var query = ctx.args.filter; -// if (!query) { -// return cb(); -// } -// var keys = Object.keys(instruction); -// var exp = {}; -// if (typeof query.fields === 'undefined') { -// for (var i = 0, length = keys.length; i < length; i++) { -// var key = keys[i]; -// key = key.indexOf('|') > -1 ? key.replace(/\|/g, '.') : key; -// exp[key] = false; -// } -// query.fields = exp; -// } -// // else { -// // var fieldList = query.fields; -// // fieldList = _.filter(fieldList, function (item) { -// // return keys.indexOf(item) === -1 -// // }); -// // query.fields = fieldList; -// // } +/* To be used when database doesnt support sort OR sort needs to be done in memory*/ +// function sortInMemory(ctx, options) { +// var result = ctx.result; +// if (typeof result === 'undefined') { +// return; // } -// cb(); -// } - -// function maskCharacters(ctx, charMaskRules, cb) { -// var input; -// var result = ctx.result || ctx.accdata; - -// if (result && !_.isEmpty(result)) { -// input = result; -// } else if (ctx.req.body && !_.isEmpty(ctx.req.body)) { -// input = ctx.req.body; -// } else { -// return cb(); +// if (!Array.isArray(options)) { +// options = [options]; // } - -// function modifyField(record, property, rule) { -// var pos = property.indexOf('.'); -// if (pos !== -1) { -// var key = property.substr(0, pos); -// var innerProp = property.substr(pos + 1); -// } else { -// key = property; -// } - -// if (record[key] && typeof record[key] === 'object') { -// modifyField(record[key], innerProp, rule); -// } else if (record[key] && typeof record[key] !== 'object') { -// var char = rule.maskCharacter || 'X'; -// var flags = rule.flags; -// var regex = flags ? new RegExp(rule.pattern, flags) : new RegExp(rule.pattern); -// var groups = record[key].match(regex) || []; -// var masking = rule.mask || []; -// var newVal = rule.format || []; -// if (Array.isArray(newVal)) { -// for (let i = 1; i < groups.length; i++) { -// newVal.push('$' + i); -// } -// newVal = newVal.join(''); -// } -// masking.forEach(function (elem) { -// newVal = newVal.replace(elem, new Array(groups[elem.substr(1)].length + 1).join(char)); -// }); -// for (let i = 0; i < groups.length; i++) { -// newVal = newVal.replace('$' + i, groups[i]); -// } -// // normally we set __data but now, lb3!!! -// record[key] = newVal; +// var keys = []; +// var values = []; +// for (var index in options) { +// if (options.hasOwnProperty(index)) { +// var key = Object.keys(options[index])[0]; +// values.push(options[index][key]); +// key = key.indexOf('|') > -1 ? key.replace(/\|/g, '.') : key; +// keys.push(key); // } // } - -// function applyRuleOnRecord(record, charMaskRules) { -// Object.keys(charMaskRules).forEach(function (key) { -// modifyField(record, key, charMaskRules[key]); -// }); -// return record; +// var updatedResults; +// if (Array.isArray(result)) { +// // lodash version 3.10.1 uses sortByOrder;version 4.0.0 uses OrderBy +// updatedResults = _.orderBy(result, keys, values); // } - -// if (Array.isArray(input)) { -// var updatedResult = []; -// input.forEach(function (record) { -// updatedResult.push(applyRuleOnRecord(record, charMaskRules)); -// }); -// input = updatedResult; -// } else { -// input = applyRuleOnRecord(input, charMaskRules); +// if (updatedResults) { +// ctx.result = updatedResults; // } - -// return cb(); // } const utils = { From 3c36d0a5848bf69ce5493f1e84a6a4e324be5442 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 28 Apr 2020 22:15:37 +0530 Subject: [PATCH 26/80] refactor and lint fixes --- .../mixins/service-personalization-mixin.js | 67 ++- lib/service-personalizer.js | 496 +++++++++--------- lib/utils.js | 22 +- server/boot/service-personalization.js | 4 - 4 files changed, 286 insertions(+), 303 deletions(-) diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index 7fa47c8..69f0f0d 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -9,13 +9,13 @@ * This mixin will attach beforeRemote and afterRemote * hooks and decide if the data needs to be service * personalized. - * + * * Therefore, it is necessary to enable the mixin * configuration on the corresponding model definition, - * even if it does not directly participate in the + * even if it does not directly participate in the * service personalization (viz is the case with any * form of relations - or related models). - * + * * This will only personalize data for the remote endpoints. */ @@ -28,15 +28,14 @@ const { nextTick, parseMethodString, slice } = require('./../../lib/utils'); module.exports = function ServicePersonalizationMixin(TargetModel) { log.debug(log.defaultContext(), `Applying service personalization for ${TargetModel.definition.name}`); TargetModel.afterRemote('**', function ServicePersonalizationAfterRemoteHook() { - let args = slice(arguments); let ctx = args[0]; let next = args[args.length - 1]; // let callCtx = ctx.req.callContext; log.debug(ctx, `afterRemote: MethodString: ${ctx.methodString}`); - ctxInfo = parseMethodString(ctx.methodString); - + let ctxInfo = parseMethodString(ctx.methodString); + let data = null; let applyFlag = true; @@ -53,41 +52,37 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { break; default: log.debug(ctx, `afterRemote: Unhandled static - ${ctx.methodString}`); - data = {} + data = {}; applyFlag = false; } - } - else { - switch(ctxInfo.methodName) { + } else { + switch (ctxInfo.methodName) { case 'patchAttributes': data = ctx.result; break; default: log.debug(ctx, `afterRemote: Unhandled non-static - ${ctx.methodString}`); - data = {} + data = {}; applyFlag = false; } } - if(applyFlag) { + if (applyFlag) { let personalizationOptions = { isBeforeRemote: false, context: ctx }; - applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function(err) { - if(err) { + applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function (err) { + if (err) { next(err); - } - else { + } else { next(); } }); - } - else { + } else { nextTick(next); } - }); TargetModel.beforeRemote('**', function ServicePersonalizationBeforeRemoteHook() { @@ -98,10 +93,11 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { log.debug(ctx, `beforeRemote: MethodString: ${ctx.methodString}`); - ctxInfo = parseMethodString(ctx.methodString); + let ctxInfo = parseMethodString(ctx.methodString); let applyFlag = true; - if(ctxInfo.isStatic) { - switch(ctxInfo.methodName) { + let data = null; + if (ctxInfo.isStatic) { + switch (ctxInfo.methodName) { case 'create': case 'patchOrCreate': data = ctx.req.body; @@ -112,36 +108,33 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { data = {}; break; default: - data = {} - log.debug(ctx, `beforeRemote: Unhandled static: ${ctx.methodString}`); + data = {}; + log.debug(ctx, `beforeRemote: Unhandled static: ${ctx.methodString}`); applyFlag = false; - } - } - else { - switch(ctxInfo.methodName) { + } + } else { + switch (ctxInfo.methodName) { case 'patchAttributes': data = ctx.req.body; break; default: - data = {} - log.debug(ctx, `beforeRemote: Unhandled non-static: ${ctx.methodString}`); + data = {}; + log.debug(ctx, `beforeRemote: Unhandled non-static: ${ctx.methodString}`); applyFlag = false; } } - - if(applyFlag) { + + if (applyFlag) { let personalizationOptions = { isBeforeRemote: true, context: ctx }; - applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function(err){ + applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function (err) { next(err); }); - } - else { + } else { nextTick(next); } - }); -} \ No newline at end of file +}; diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 0c8e3d3..fa4e1f2 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -32,67 +32,6 @@ const { nextTick } = require('./utils'); var sortFactoryFn = (reverse = false) => (first, second) => first.type === 'fieldReplace' ? (reverse ? -1 : 1) : (reverse ? 1 : -1); -/** - * Function to add 'where' clause in the datasource filter query. - */ - -function addWhereClause(ctx, instruction, cb) { - if (typeof instruction === 'string') { - exprLang(instruction).then(function addWhereClauseInstrResultCb(result) { - addWheretoCtx(ctx, result.value.where, cb); - }); - } else { - addWheretoCtx(ctx, instruction, cb); - } -} - -function addWheretoCtx(ctx, where, cb) { - var filter = ctx.args.filter; - // Shall we directly use mergeQuery util.? - if (filter) { - if (typeof filter === 'string') { - var filterQuery = JSON.parse(filter); - if (filterQuery && filterQuery.where) { - // not sure and | or ?. or will give more results, and will give none. - var newQuery = { - or: [where, filterQuery.where] - }; - filter = filterQuery; - filter.where = newQuery; - } else { - filter.where = where; - } - } else { - filter.where = { or: [where, filter.where] }; - } - } else { - filter = {}; - filter.where = where; - } - ctx.args.filter = filter; - cb(); -} - -/** - * Function to add filter clause in query. - */ - -function addLbFilterClause(ctx, instruction, cb) { - if (typeof instruction === 'string') { - exprLang(instruction).then(function addLbFilterInstrResultCb(result) { - addLbFiltertoCtx(ctx, result.value, cb); - }); - } else { - addLbFiltertoCtx(ctx, instruction, cb); - } -} - -function addLbFiltertoCtx(ctx, filter, cb) { - ctx.args.filter = ctx.args.filter || {}; - mergeQuery(ctx.args.filter, filter); - cb(); -} - // execute custom function function executeCustomFunction(ctx, instruction, cb) { let customFunctionName = instruction.functionName; @@ -100,12 +39,6 @@ function executeCustomFunction(ctx, instruction, cb) { cb(); } - -// Processes a filter instruction. filter instruction schema is same like loopback filter schema. -function addLbFilter(ctx, instruction, cb) { - addLbFilterClause(ctx, instruction, cb); -} - /** * Custom function */ @@ -118,43 +51,6 @@ function getCustomFunction() { return customFunction; } -function createOrderExp(instruction, tempKeys) { - if (!Array.isArray(instruction)) { - instruction = [instruction]; - } - - var orderExp = []; - - for (var i = 0; i < instruction.length; i++) { - var obj = instruction[i]; - var key = Object.keys(obj)[0]; - var val = obj[key]; - key = key.indexOf('|') > -1 ? key.replace(/\|/g, '.') : key; - - var index = tempKeys.length >= 1 ? tempKeys.indexOf(key) : -1; - - switch (val.toUpperCase()) { - case 'ASC': - case 'ASCENDING': - case '': - val = 'ASC'; - break; - case 'DESC': - case 'DESCENDING': - case 'DSC': - val = 'DESC'; - break; - default: - val = null; - } - if (val && index === -1) { - var value = key + ' ' + val; - orderExp.push(value); - } - } - - return orderExp; -} // (Arun 2020-04-28 21:01:57) - retaining the below function for future use @@ -188,12 +84,14 @@ function createOrderExp(instruction, tempKeys) { // } const utils = { + /** * field replacer function - * - * @param {instance or data} record - the model instance or plain data + * + * @param {instance} record - the model instance or plain data * @param {object} replacement - personalization rule (for field name replace) * @param {string} value - new field name + * @returns {void} nothing */ replaceField(record, replacement, value) { var pos = replacement.indexOf('\uFF0E'); @@ -223,9 +121,11 @@ const utils = { /** * field value replace function - * @param {instance or data} record - the model instance or data + * + * @param {instance} record - the model instance or data * @param {object} replacement - the personalization rule (for field value replace) * @param {string} value - the value to replace + * @returns {void} nothing */ replaceValue(record, replacement, value) { var pos = replacement.indexOf('\uFF0E'); @@ -273,31 +173,137 @@ const utils = { } } return record; - } + }; }, noop() { // do nothing - } -}; + }, + + // === BEGIN: query based filter functions === + + addWhereClause(ctx, instruction, cb) { + if (typeof instruction === 'string') { + exprLang(instruction).then(function addWhereClauseInstrResultCb(result) { + utils.addWheretoCtx(ctx, result.value.where, cb); + }); + } else { + utils.addWheretoCtx(ctx, instruction, cb); + } + }, + + addWheretoCtx(ctx, where, cb) { + var filter = ctx.args.filter; + // Shall we directly use mergeQuery util.? + if (filter) { + if (typeof filter === 'string') { + var filterQuery = JSON.parse(filter); + if (filterQuery && filterQuery.where) { + // not sure and | or ?. or will give more results, and will give none. + var newQuery = { + or: [where, filterQuery.where] + }; + filter = filterQuery; + filter.where = newQuery; + } else { + filter.where = where; + } + } else { + filter.where = { or: [where, filter.where] }; + } + } else { + filter = {}; + filter.where = where; + } + ctx.args.filter = filter; + cb(); + }, + + // === END: query based filter functions === + // === BEGIN: lbFilter functions === -// const ALT_DOT = '\uFF0E' + // Processes a filter instruction. filter instruction schema is same like loopback filter schema. + addLbFilter(ctx, instruction, cb) { + utils.addLbFilterClause(ctx, instruction, cb); + }, + + /** + * Function to add filter clause in query. + */ + + addLbFilterClause(ctx, instruction, cb) { + if (typeof instruction === 'string') { + exprLang(instruction).then(function addLbFilterInstrResultCb(result) { + utils.addLbFiltertoCtx(ctx, result.value, cb); + }); + } else { + utils.addLbFiltertoCtx(ctx, instruction, cb); + } + }, + + addLbFiltertoCtx(ctx, filter, cb) { + ctx.args.filter = ctx.args.filter || {}; + mergeQuery(ctx.args.filter, filter); + cb(); + }, + + // === END: lbFilter functions === + + + createOrderExp(instruction, tempKeys) { + if (!Array.isArray(instruction)) { + instruction = [instruction]; + } + + var orderExp = []; + + for (var i = 0; i < instruction.length; i++) { + var obj = instruction[i]; + var key = Object.keys(obj)[0]; + var val = obj[key]; + key = key.indexOf('|') > -1 ? key.replace(/\|/g, '.') : key; + + var index = tempKeys.length >= 1 ? tempKeys.indexOf(key) : -1; + + switch (val.toUpperCase()) { + case 'ASC': + case 'ASCENDING': + case '': + val = 'ASC'; + break; + case 'DESC': + case 'DESCENDING': + case 'DSC': + val = 'DESC'; + break; + default: + val = null; + } + if (val && index === -1) { + var value = key + ' ' + val; + orderExp.push(value); + } + } + + return orderExp; + } +}; const p13nFunctions = { /** * Does field replace. - * + * * PreApplication: Yes * PostApplication: Yes - * + * * For pre-appilication a reverse rule is applied. - * @param {object} replacements - * @param {boolean} isBeforeRemote + * @param {object} replacements + * @param {boolean} isBeforeRemote */ - fieldReplace(replacements, isBeforeRemote = false) { //Tests: t1, t15, t17 - + // eslint-disable-next-line no-inline-comments + fieldReplace(replacements, isBeforeRemote = false) { // Tests: t1, t15, t17 let replaceRecord = utils.replaceRecordFactory(utils.replaceField); let process = function (replacements, data, cb) { @@ -306,8 +312,7 @@ const p13nFunctions = { return replaceRecord(record, replacements); }); data = updatedResult; - } - else { + } else { data = replaceRecord(data, replacements); } @@ -330,27 +335,27 @@ const p13nFunctions = { } } } - // fieldReplacementFn(ctx, revInputJson, cb); + // fieldReplacementFn(ctx, revInputJson, cb); process(revInputJson, data, callback); - } + }; } // ! for afterRemote case return function (data, callback) { process(replacements, data, callback); - } + }; }, /** * does a field value replace. - * + * * PreApplication: no * PostApplication: yes - * + * * @param {object} replacements - replacement rule */ - fieldValueReplace(replacements) { //Tests: t22, t20, t19, t18, t17, t16, t3, t23 - + // eslint-disable-next-line no-inline-comments + fieldValueReplace(replacements) { // Tests: t22, t20, t19, t18, t17, t16, t3, t23 return function (data, callback) { let replaceRecord = utils.replaceRecordFactory(utils.replaceValue); if (Array.isArray(data)) { @@ -358,13 +363,12 @@ const p13nFunctions = { return replaceRecord(record, replacements); }); data = updatedResult; - } - else { + } else { data = replaceRecord(data, replacements); } nextTick(callback); - } + }; }, noop: function (data, cb) { @@ -376,20 +380,21 @@ const p13nFunctions = { * Apply a sort. Mostly passed on to the Model.find() * where the actual sorting is applied. (Provided the * underlying datasource supports it) - * + * * PreApplication: Yes * PostApplication: No - * + * * @param {HttpContext} ctx - the context object * @param {object} instruction - the personalization sort rule */ - addSort(ctx, instruction) { //Tests: t4, t5, t6, t7, t8, t10, t11 + // eslint-disable-next-line no-inline-comments + addSort(ctx, instruction) { // Tests: t4, t5, t6, t7, t8, t10, t11 return function (data, callback) { utils.noop(data); var dsSupportSort = true; if (dsSupportSort) { var query = ctx.args.filter || {}; - //TODO: (Arun 2020-04-24 19:43:38) - what if no filter? + // TODO: (Arun 2020-04-24 19:43:38) - what if no filter? if (query) { if (typeof query.order === 'string') { query.order = [query.order]; @@ -404,7 +409,7 @@ const p13nFunctions = { } // create the order expression based on the instruction passed - var orderExp = createOrderExp(instruction, tempKeys); + var orderExp = utils.createOrderExp(instruction, tempKeys); if (typeof query.order === 'undefined') { query.order = orderExp; @@ -415,8 +420,6 @@ const p13nFunctions = { ctx.args.filter = ctx.args.filter || {}; ctx.args.filter.order = query.order; nextTick(callback); - } else { - cb(); } } @@ -431,20 +434,20 @@ const p13nFunctions = { // addPostProcessingFunction(ctx, 'sortInMemory', instruction, sortInMemory); // cb(); // } - } + }; }, /** * Mask helper function for masking a field - * + * * @param {CallContext} ctx - the request context * @param {object} instructions - personalization rule object - * + * * PreApplication: No * PostApplication: Yes - * + * * Example Rule - Masks a "category field" - * + * * var rule = { * "modelName": "ProductCatalog", * "personalizationRule" : { @@ -454,15 +457,16 @@ const p13nFunctions = { * } * }; */ - mask(ctx, instructions) { - return function (data, callback) { //Tests: t13 + // eslint-disable-next-line no-inline-comments + mask(ctx, instructions) { // Tests: t13 + return function (data, callback) { utils.noop(data); let dsSupportMask = true; if (dsSupportMask) { ctx.args.filter = ctx.args.filter || {}; let query = ctx.args.filter; // TODO: (Arun - 2020-04-24 11:16:19) Don't we need to handle the alternate case? - // i.e. when there is no filter ? + // i.e. when there is no filter ? if (!query) { return nextTick(callback); } @@ -477,7 +481,7 @@ const p13nFunctions = { query.fields = exp; } - //TODO: (Arun - 2020-04-24 11:17:11) shouldn't we uncomment the following? + // TODO: (Arun - 2020-04-24 11:17:11) shouldn't we uncomment the following? // else { // var fieldList = query.fields; @@ -494,49 +498,47 @@ const p13nFunctions = { /** * add a filter to Model.find() - * + * * PreApplication: yes * PostApplication: no - * + * * @param {HttpContext} ctx - http context * @param {object} instruction - personalization filter rule */ + // eslint-disable-next-line no-inline-comments addFilter(ctx, instruction) {// Tests: t9 return function (data, callback) { utils.noop(data); - - //TODO: (Arun 2020-04-24 20:16:02) - how to check for datasource support? - - //TODO: (Arun 2020-04-24 20:16:47) - implement in-memory filter if datasource unsupported var dsSupportFilter = true; - if (dsSupportFilter) { - addWhereClause(ctx, instruction, callback); + utils.addWhereClause(ctx, instruction, callback); } - } + }; }, /** * Adds a loopback filter clause - * + * * @param {HttpContext} ctx - context * @param {object} instruction - the rule */ - addLbFilter(ctx, instruction) { //Tests: t21 + // eslint-disable-next-line no-inline-comments + addLbFilter(ctx, instruction) { // Tests: t21 return function (data, callback) { utils.noop(data); - addLbFilter(ctx, instruction, callback); - } + utils.addLbFilter(ctx, instruction, callback); + }; }, /** * applies masking values in a field. E.g - * masking first few digits of the phone + * masking first few digits of the phone * number - * + * * @param {object} instruction - mask instructions */ - addFieldMask(charMaskRules) { //Test t24, t25, t26, t27, t28, t29 + // eslint-disable-next-line no-inline-comments + addFieldMask(charMaskRules) { // Test t24, t25, t26, t27, t28, t29 return function (data, callback) { var input = data; @@ -594,33 +596,37 @@ const p13nFunctions = { // return cb(); nextTick(callback); - } + }; }, /** * adds the post custom function added via * config.json to the after remote - * + * * PreApplication: no * PostApplication: yes - * @param {context} ctx - * @param {object} instruction + * @param {context} ctx + * @param {object} instruction + * @returns {function} function that applies custom function (async iterator function) */ - addCustomFunction(ctx, instruction) { //Tests t35, t36 - return function(data, callback) { + // eslint-disable-next-line no-inline-comments + addCustomFunction(ctx, instruction) { // Tests t35, t36 + return function (data, callback) { utils.noop(data); executeCustomFunction(ctx, instruction, () => nextTick(callback)); - } + }; } -} +}; + /** - * Apply the personalization on the given data - * - * @param {bool} isReverse - flag indicating if reverse personalization is to be applied + * apply the personalization on the given data + * @param {object} ctx - http context object + * @param {bool} isBeforeRemote - flag indicating if this is invoked in a beforeRemote stage * @param {object} instructions - object containing personalization instructions - * @param {List or Instance} data - data from the context. Could either be a List of model instances or a single model instance + * @param {instance} data - data from the context. Could either be a List or a single model instance * @param {function} done - the callback which receives the new data. First argument of function is an error object + * @returns {void} nothing */ function personalize(ctx, isBeforeRemote, instructions, data, done) { let tasks = null; @@ -634,17 +640,16 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { case 'filter': return { type: 'filter', fn: p13nFunctions.addFilter(ctx, instruction) }; case 'fieldReplace': - return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction, true) } + return { type: 'fieldReplace', fn: p13nFunctions.fieldReplace(instruction, true) }; case 'lbFilter': return { type: 'lbFilter', fn: p13nFunctions.addLbFilter(ctx, instruction) }; case 'preCustomFunction': - return { type: 'preCustomFunction', fn: p13nFunctions.addCustomFunction(ctx, instruction)} ; + return { type: 'preCustomFunction', fn: p13nFunctions.addCustomFunction(ctx, instruction) }; default: - return { type: `noop:${operation}`, fn: p13nFunctions.noop } + return { type: `noop:${operation}`, fn: p13nFunctions.noop }; } }); - } - else { + } else { tasks = Object.entries(instructions).map(([operation, instruction]) => { switch (operation) { case 'fieldReplace': @@ -653,16 +658,16 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { return { type: 'fieldValueReplace', fn: p13nFunctions.fieldValueReplace(instruction) }; case 'fieldMask': return { type: 'fieldMask', fn: p13nFunctions.addFieldMask(instruction) }; - case 'postCustomFunction': + case 'postCustomFunction': return { type: 'postCustomFunction', fn: p13nFunctions.addCustomFunction(ctx, instruction) }; default: - return { type: `noop:${operation}`, fn: p13nFunctions.noop } + return { type: `noop:${operation}`, fn: p13nFunctions.noop }; } }); } let asyncIterator = function ({ type, fn }, done) { - log.debug(ctx, `${isBeforeRemote ? "beforeRemote" : "afterRemote"}: applying function - ${type}`); + log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: applying function - ${type}`); fn(data, function (err) { done(err); }); @@ -670,9 +675,8 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { async.eachSeries(tasks.sort(sortFactoryFn(isBeforeRemote)), asyncIterator, function asyncEachCb(err) { if (err) { - done(err) - } - else { + done(err); + } else { done(); } }); @@ -680,47 +684,44 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { function checkRelationAndRecurse(Model, data, personalizationOptions, done) { - let {settings: {relations}, definition: { name } } = Model; + let { settings: { relations }, definition: { name } } = Model; let { isBeforeRemote, context } = personalizationOptions; let prefix = isBeforeRemote ? 'beforeRemote' : 'afterRemote'; if (relations) { // Object.entries(Model.setti) let relationItems = Object.entries(relations); - let relationsIterator = function relationProcessor([ relationName, relation ], done) { - + let relationsIterator = function relationProcessor([relationName, relation], done) { // check if the related model has personalization - let relData = undefined; + let relData; let relModel = relation.model; let applyFlag = false; - if(Array.isArray(data)) { + if (Array.isArray(data)) { relData = data.reduce((carrier, record) => { - if(record.__data && typeof record.__data[relationName] !== 'undefined') { - carrier.push(record.__data[relationName]) + if (record.__data && typeof record.__data[relationName] !== 'undefined') { + carrier.push(record.__data[relationName]); } return carrier; }, []); relData = _.flatten(relData); - if(relData.length) { + if (relData.length) { applyFlag = true; } - } - else if(data.__data) { + } else if (data.__data) { relData = data.__data[relationName]; applyFlag = !!relData; - } - else if((relData = data[relationName])) { + } else if ((relData = data[relationName])) { // eslint-disable-line no-cond-assign applyFlag = true; } - + let callback = function (err) { log.debug(context, `${prefix}: processing relation "${relationName}"/"${name}" - finished`); done(err); }; callback.__trace = `${name}_${relationName}`; - if(applyFlag) { + if (applyFlag) { log.debug(context, `${prefix}: processing relation "${relationName}"/"${name}"`); - return applyServicePersonalization(relModel, relData, personalizationOptions, callback) + return applyServicePersonalization(relModel, relData, personalizationOptions, callback); } log.debug(context, `${prefix}: processing relation "${relationName}"/"${name}" - skipped`); nextTick(done); @@ -743,42 +744,37 @@ function applyServicePersonalization(modelName, data, personalizationOptions, do PersonalizationRule.find(findQuery, callContext, function (err, entries) { if (err) { done(err); + } else if (entries.length === 0) { + //! not needed to personalize here, + + //! however we need to check for related + //! model + checkRelationAndRecurse(Model, data, personalizationOptions, function (err) { + if (err) { + done(err); + } else { + done(); + } + }); + } else { + //! apply personalization, then check for related model + + personalize(context, isBeforeRemote, entries[0].personalizationRule, data, function (err) { + if (err) { + done(err); + } else { + checkRelationAndRecurse(Model, data, personalizationOptions, function (err) { + if (err) { + done(err); + } else { + done(); + } + }); + } + }); } - else { - // let { instructions } = entries[0]; - // personalize(reverse, instructions, data, done); - if (entries.length == 0) { - //! not needed to personalize here, - //! however we need to check for related - //! model - checkRelationAndRecurse(Model, data, personalizationOptions, function (err) { - if (err) { - done(err) - } - else { - done(); - } - }); - } - else { - personalize(context, isBeforeRemote, entries[0].personalizationRule, data, function (err) { - if (err) { - done(err); - } - else { - checkRelationAndRecurse(Model, data, personalizationOptions, function (err) { - if (err) { - done(err); - } - else { - done(); - } - }); - } - }); - } - } - }); //PersonalizationRule.find() - end + }); + // PersonalizationRule.find() - end } let PersonalizationRule = null; @@ -786,37 +782,36 @@ let PersonalizationRule = null; * Initializes this module for service personalization * during applcation boot. Initializes observers on * PersonalizationRule model. - * + * * @param {Application} app - The Loopback application object */ function init(app) { - PersonalizationRule = app.models['PersonalizationRule']; + PersonalizationRule = app.models.PersonalizationRule; let servicePersoConfig = app.get('servicePersonalization'); loadCustomFunction(require(servicePersoConfig.customFunctionPath)); - PersonalizationRule.observe('before save', function(ctx, next){ + PersonalizationRule.observe('before save', function (ctx, next) { log.debug(ctx, 'PersonalizationRule: before save'); let data = ctx.__data || ctx.instance || ctx.data; - + let model = loopback.findModel(data.modelName); - if(typeof model === 'undefined') { + if (typeof model === 'undefined') { log.error(ctx, `PersonalizationRule: before save - model "${data.modelName}" is not found`); - return nextTick(function() { + return nextTick(function () { let error = new Error(`Model: ${data.modelName} is not found`); next(error); }); } - let { personalizationRule : { postCustomFunction } } = data; - if(postCustomFunction) { + let { personalizationRule: { postCustomFunction } } = data; + if (postCustomFunction) { let { functionName } = postCustomFunction; - if(functionName) { - if(!Object.keys(getCustomFunction()).includes(functionName)) { - return nextTick(function() { + if (functionName) { + if (!Object.keys(getCustomFunction()).includes(functionName)) { + return nextTick(function () { next(new Error(`The custom function with name "${functionName}" does not exist`)); - }) - } - } - else { - return nextTick(function() { + }); + } + } else { + return nextTick(function () { let error = new Error('postCustomFunction not defined with functionName'); next(error); }); @@ -824,7 +819,6 @@ function init(app) { } nextTick(next); }); - } module.exports = { diff --git a/lib/utils.js b/lib/utils.js index f27f357..43dac25 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -2,28 +2,28 @@ const _slice = [].slice; module.exports = { /** * queue the function to the runtime's next event loop - * + * * @param {function} cb - the callback function + * @returns {void} */ nextTick: cb => process.nextTick(cb), /** - * parses the context's method string + * parses a context's methodString + * @param {string} str - method string + * @returns {object} - object representing the method string */ - parseMethodString : str => { + parseMethodString: str => { return str.split('.').reduce((obj, comp, idx, arr) => { let ret = {}; let length = arr.length; if (idx === 0) { ret.modelName = comp; - } - else if (length === 3 && idx !== length - 1) { + } else if (length === 3 && idx !== length - 1) { ret.isStatic = false; - } - else if (length === 3 && idx == length - 1) { + } else if (length === 3 && idx === length - 1) { ret.methodName = comp; - } - else { + } else { ret.isStatic = true; ret.methodName = comp; } @@ -31,5 +31,5 @@ module.exports = { }, {}); }, - slice : arg => _slice.call(arg) -} \ No newline at end of file + slice: arg => _slice.call(arg) +}; diff --git a/server/boot/service-personalization.js b/server/boot/service-personalization.js index f1e58e8..e74df87 100644 --- a/server/boot/service-personalization.js +++ b/server/boot/service-personalization.js @@ -11,10 +11,6 @@ * @author deostroll * @name Service Personalization */ -// TODO: without clean db test cases are not passing, need to clean up test cases. - -var loopback = require('loopback'); -var log = require('oe-logger')('service-personalization'); // var messaging = require('../../lib/common/global-messaging'); var servicePersonalizer = require('../../lib/service-personalizer'); From 86c23ede0c1544c335b229a4347a7e84b362dd37 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 28 Apr 2020 23:03:11 +0530 Subject: [PATCH 27/80] v2.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0760aba..76fc76f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oe-service-personalization", - "version": "2.2.1", + "version": "2.3.0", "description": "oe-cloud modularization project", "engines": { "node": ">=6" From 1e7f757ba46b42292a4f97cfaa87af890c4b71c9 Mon Sep 17 00:00:00 2001 From: deostroll Date: Thu, 30 Apr 2020 16:51:19 +0530 Subject: [PATCH 28/80] added a relation scenario --- .../mixins/service-personalization-mixin.js | 25 +++++++++++++++---- test/test.js | 18 +++++++++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index 69f0f0d..62d78ff 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -25,6 +25,11 @@ const log = logger('service-personalization-mixin'); const { applyServicePersonalization } = require('./../../lib/service-personalizer'); const { nextTick, parseMethodString, slice } = require('./../../lib/utils'); +const checkNonStaticMethod = ctxInfo => { + let { methodName } = ctxInfo; + return methodName.startsWith('__'); +}; + module.exports = function ServicePersonalizationMixin(TargetModel) { log.debug(log.defaultContext(), `Applying service personalization for ${TargetModel.definition.name}`); TargetModel.afterRemote('**', function ServicePersonalizationAfterRemoteHook() { @@ -61,9 +66,15 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { data = ctx.result; break; default: - log.debug(ctx, `afterRemote: Unhandled non-static - ${ctx.methodString}`); - data = {}; - applyFlag = false; + // log.debug(ctx, `afterRemote: Unhandled non-static - ${ctx.methodString}`); + // data = {}; + // applyFlag = false; + applyFlag = checkNonStaticMethod(ctxInfo); + + data = ctx.result; + if(!applyFlag){ + log.debug(ctx, `afterRemote: Unhandled non-static: ${ctx.methodString}`); + }; } } @@ -118,9 +129,13 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { data = ctx.req.body; break; default: + applyFlag = checkNonStaticMethod(ctxInfo); + data = {}; - log.debug(ctx, `beforeRemote: Unhandled non-static: ${ctx.methodString}`); - applyFlag = false; + if(!applyFlag){ + log.debug(ctx, `beforeRemote: Unhandled non-static: ${ctx.methodString}`); + }; + // applyFlag = false; } } diff --git a/test/test.js b/test/test.js index 7624cec..3b31d9d 100755 --- a/test/test.js +++ b/test/test.js @@ -2052,6 +2052,24 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); }); + + it('t40(b) should apply service personalization to a related model', function(done) { + let url = `${productOwnerUrl}/1/ProductCatalog?access_token=${accessToken}`; + api.get(url) + .set('Accept', 'application/json') + .set('REMOTE_USER', 'testUser') + .set('region', 'kl') + .expect(200) + .end((err, resp) => { + if(err){ + return done(err); + } + let result = resp.body; + // console.log(resp.body); + expect(result[0].modelNo).to.equal('123456XXXX'); + done(); + }) + }); }); describe('Remote method tests', () => { From 05c5a5aea1d81d1652846326d2d4a212d4340a7a Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 4 May 2020 14:25:05 +0530 Subject: [PATCH 29/80] handled remote calls that are relational --- .../mixins/service-personalization-mixin.js | 61 +++++++++++-------- test/test.js | 6 +- 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index 62d78ff..585da14 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -25,13 +25,21 @@ const log = logger('service-personalization-mixin'); const { applyServicePersonalization } = require('./../../lib/service-personalizer'); const { nextTick, parseMethodString, slice } = require('./../../lib/utils'); -const checkNonStaticMethod = ctxInfo => { - let { methodName } = ctxInfo; - return methodName.startsWith('__'); +const getRelationInfo = (parentModel, { methodName }) => { + const idx = 7; //__get__ + const REL_GET_STR = '__get__'; + if(methodName.substr(0,idx) === REL_GET_STR) { + let relName = methodName.substr(idx); + relationDef = parentModel.settings.relations[relName]; + return { isRelation: true, model: relationDef.model }; + } + + return { isRelation: false } }; module.exports = function ServicePersonalizationMixin(TargetModel) { log.debug(log.defaultContext(), `Applying service personalization for ${TargetModel.definition.name}`); + const TARGET_MODEL_NAME = TargetModel.definition.name; TargetModel.afterRemote('**', function ServicePersonalizationAfterRemoteHook() { let args = slice(arguments); let ctx = args[0]; @@ -43,13 +51,11 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { let data = null; let applyFlag = true; - + let toModel = TARGET_MODEL_NAME; if (ctxInfo.isStatic) { switch (ctxInfo.methodName) { case 'create': case 'patchOrCreate': - data = ctx.result; - break; case 'find': case 'findById': case 'findOne': @@ -66,15 +72,16 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { data = ctx.result; break; default: - // log.debug(ctx, `afterRemote: Unhandled non-static - ${ctx.methodString}`); - // data = {}; - // applyFlag = false; - applyFlag = checkNonStaticMethod(ctxInfo); - - data = ctx.result; - if(!applyFlag){ - log.debug(ctx, `afterRemote: Unhandled non-static: ${ctx.methodString}`); - }; + let relationInfo = getRelationInfo(TargetModel, ctxInfo); + if(relationInfo.isRelation) { + applyFlag = true; + toModel = relationInfo.model; + data = ctx.result; + } + else { + applyFlag = false; + log.debug(ctx, `afterRemote: Unhandled non-static - ${ctx.methodString}`); + } } } @@ -84,7 +91,7 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { context: ctx }; - applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function (err) { + applyServicePersonalization(toModel, data, personalizationOptions, function (err) { if (err) { next(err); } else { @@ -100,13 +107,14 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { let args = slice(arguments); let ctx = args[0]; let next = args[args.length - 1]; - // let callCtx = ctx.req.callContext; log.debug(ctx, `beforeRemote: MethodString: ${ctx.methodString}`); let ctxInfo = parseMethodString(ctx.methodString); let applyFlag = true; let data = null; + let toModel = TARGET_MODEL_NAME; + if (ctxInfo.isStatic) { switch (ctxInfo.methodName) { case 'create': @@ -129,13 +137,16 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { data = ctx.req.body; break; default: - applyFlag = checkNonStaticMethod(ctxInfo); - - data = {}; - if(!applyFlag){ - log.debug(ctx, `beforeRemote: Unhandled non-static: ${ctx.methodString}`); - }; - // applyFlag = false; + let relationInfo = getRelationInfo(TargetModel, ctxInfo); + if(relationInfo.isRelation) { + applyFlag = true; + toModel = relationInfo.model; + data = {}; + } + else { + applyFlag = false; + log.debug(ctx, `beforeRemote: Unhandled non-static - ${ctx.methodString}`); + } } } @@ -145,7 +156,7 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { context: ctx }; - applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function (err) { + applyServicePersonalization(toModel, data, personalizationOptions, function (err) { next(err); }); } else { diff --git a/test/test.js b/test/test.js index 3b31d9d..ddecfac 100755 --- a/test/test.js +++ b/test/test.js @@ -1978,7 +1978,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t40 should demonstrate personalization is being applied recursively', done => { + it('t40(a) should demonstrate personalization is being applied recursively', done => { let data = [ { "modelName" : "AddressBook", @@ -2053,7 +2053,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t40(b) should apply service personalization to a related model', function(done) { + it('t40(b) should apply service personalization to a related model invoked via remote call', function(done) { let url = `${productOwnerUrl}/1/ProductCatalog?access_token=${accessToken}`; api.get(url) .set('Accept', 'application/json') @@ -2068,7 +2068,7 @@ describe(chalk.blue('service personalization test started...'), function () { // console.log(resp.body); expect(result[0].modelNo).to.equal('123456XXXX'); done(); - }) + }); }); }); From 7d12bfbb84e77c553c176edc5e3ef199bc329b87 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 12 May 2020 19:01:08 +0530 Subject: [PATCH 30/80] untested changes --- .../mixins/service-personalization-mixin.js | 138 ++---------- lib/service-personalizer.js | 202 ++++++++++++++---- 2 files changed, 178 insertions(+), 162 deletions(-) diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index 585da14..774b3a6 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -22,85 +22,22 @@ const logger = require('oe-logger'); const log = logger('service-personalization-mixin'); -const { applyServicePersonalization } = require('./../../lib/service-personalizer'); -const { nextTick, parseMethodString, slice } = require('./../../lib/utils'); - -const getRelationInfo = (parentModel, { methodName }) => { - const idx = 7; //__get__ - const REL_GET_STR = '__get__'; - if(methodName.substr(0,idx) === REL_GET_STR) { - let relName = methodName.substr(idx); - relationDef = parentModel.settings.relations[relName]; - return { isRelation: true, model: relationDef.model }; - } - - return { isRelation: false } -}; +const { runPersonalizations } = require('./../../lib/service-personalizer'); +// const { nextTick, parseMethodString, slice } = require('./../../lib/utils'); module.exports = function ServicePersonalizationMixin(TargetModel) { log.debug(log.defaultContext(), `Applying service personalization for ${TargetModel.definition.name}`); - const TARGET_MODEL_NAME = TargetModel.definition.name; TargetModel.afterRemote('**', function ServicePersonalizationAfterRemoteHook() { let args = slice(arguments); let ctx = args[0]; let next = args[args.length - 1]; // let callCtx = ctx.req.callContext; - log.debug(ctx, `afterRemote: MethodString: ${ctx.methodString}`); - - let ctxInfo = parseMethodString(ctx.methodString); - - let data = null; - let applyFlag = true; - let toModel = TARGET_MODEL_NAME; - if (ctxInfo.isStatic) { - switch (ctxInfo.methodName) { - case 'create': - case 'patchOrCreate': - case 'find': - case 'findById': - case 'findOne': - data = ctx.result; - break; - default: - log.debug(ctx, `afterRemote: Unhandled static - ${ctx.methodString}`); - data = {}; - applyFlag = false; - } - } else { - switch (ctxInfo.methodName) { - case 'patchAttributes': - data = ctx.result; - break; - default: - let relationInfo = getRelationInfo(TargetModel, ctxInfo); - if(relationInfo.isRelation) { - applyFlag = true; - toModel = relationInfo.model; - data = ctx.result; - } - else { - applyFlag = false; - log.debug(ctx, `afterRemote: Unhandled non-static - ${ctx.methodString}`); - } - } - } - - if (applyFlag) { - let personalizationOptions = { - isBeforeRemote: false, - context: ctx - }; - - applyServicePersonalization(toModel, data, personalizationOptions, function (err) { - if (err) { - next(err); - } else { - next(); - } - }); - } else { - nextTick(next); - } + log.debug(ctx, `afterRemote: (enter) MethodString: ${ctx.methodString}`); + runPersonalizations(ctx, false, function(err){ + log.debug(ctx, `afterRemote: (leave) MethodString: ${ctx.methodString}`); + next(err); + }); + }); TargetModel.beforeRemote('**', function ServicePersonalizationBeforeRemoteHook() { @@ -108,59 +45,12 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { let ctx = args[0]; let next = args[args.length - 1]; - log.debug(ctx, `beforeRemote: MethodString: ${ctx.methodString}`); - - let ctxInfo = parseMethodString(ctx.methodString); - let applyFlag = true; - let data = null; - let toModel = TARGET_MODEL_NAME; - - if (ctxInfo.isStatic) { - switch (ctxInfo.methodName) { - case 'create': - case 'patchOrCreate': - data = ctx.req.body; - break; - case 'find': - case 'findById': - case 'findOne': - data = {}; - break; - default: - data = {}; - log.debug(ctx, `beforeRemote: Unhandled static: ${ctx.methodString}`); - applyFlag = false; - } - } else { - switch (ctxInfo.methodName) { - case 'patchAttributes': - data = ctx.req.body; - break; - default: - let relationInfo = getRelationInfo(TargetModel, ctxInfo); - if(relationInfo.isRelation) { - applyFlag = true; - toModel = relationInfo.model; - data = {}; - } - else { - applyFlag = false; - log.debug(ctx, `beforeRemote: Unhandled non-static - ${ctx.methodString}`); - } - } - } - - if (applyFlag) { - let personalizationOptions = { - isBeforeRemote: true, - context: ctx - }; + log.debug(ctx, `beforeRemote: (enter) MethodString: ${ctx.methodString}`); - applyServicePersonalization(toModel, data, personalizationOptions, function (err) { - next(err); - }); - } else { - nextTick(next); - } + // let ctxInfo = parseMethodString(ctx.methodString); + runPersonalizations(ctx, true, function(err){ + log.debug(ctx, `beforeRemote: (leave) MethodString: ${ctx.methodString}`); + next(err); + }); }); }; diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index fa4e1f2..0af9f5c 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -19,7 +19,7 @@ var mergeQuery = require('loopback-datasource-juggler/lib/utils').mergeQuery; var logger = require('oe-logger'); var log = logger('service-personalizer'); var customFunction; -const { nextTick } = require('./utils'); +const { nextTick, parseMethodString } = require('./utils'); /** * This function returns the necessary sorting logic to @@ -721,7 +721,15 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { callback.__trace = `${name}_${relationName}`; if (applyFlag) { log.debug(context, `${prefix}: processing relation "${relationName}"/"${name}"`); - return applyServicePersonalization(relModel, relData, personalizationOptions, callback); + let { context: { _personalizationCache : { records }} } = personalizationOptions; + // return applyServicePersonalization(relModel, relData, records, ) + // return applyServicePersonalization(relModel, relData, personalizationOptions, callback); + return fetchPersonalizationRecords(ctx, relModel, function(err, relModelP13nRecords){ + if(err) { + return done(err); + } + applyServicePersonalization(relModel, relData, relModelP13nRecords, personalizationOptions, done); + }); } log.debug(context, `${prefix}: processing relation "${relationName}"/"${name}" - skipped`); nextTick(done); @@ -735,46 +743,39 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { }); } -function applyServicePersonalization(modelName, data, personalizationOptions, done) { - let { isBeforeRemote, context } = personalizationOptions; - let findQuery = { where: { modelName, disabled: false } }; - let Model = loopback.findModel(modelName); - // console.log(Model.definition) - let callContext = context.req.callContext; - PersonalizationRule.find(findQuery, callContext, function (err, entries) { - if (err) { - done(err); - } else if (entries.length === 0) { - //! not needed to personalize here, - - //! however we need to check for related - //! model - checkRelationAndRecurse(Model, data, personalizationOptions, function (err) { - if (err) { - done(err); - } else { - done(); - } - }); - } else { - //! apply personalization, then check for related model +function fetchPersonalizationRecords(ctx, forModel, cb) { + let filter = { where: { modelName: forModel, disabled: false }}; + let { req: { callContext }} = ctx; + PersonalizationRule.find(filter, callContext, cb); +} - personalize(context, isBeforeRemote, entries[0].personalizationRule, data, function (err) { - if (err) { - done(err); - } else { - checkRelationAndRecurse(Model, data, personalizationOptions, function (err) { - if (err) { - done(err); - } else { - done(); - } - }); +function applyServicePersonalization(modelName, data, records, options, cb) { + let Model = loopback.findModel(modelName); + doMethodGating(ctx, records, function(isAllowedMethod){ + if(isAllowedMethod) { + return doRoleGating(ctx, records, function(isAllowedRole){ + if(isAllowedRole) { + if(records.length === 0) { + checkRelationAndRecurse(Model, data, options, cb); + } + else { + let entry = records[0]; + let personalizationRule = entry.personalizationRule; + let ctx = options.context; + let isBeforeRemote = options.isBeforeRemote; + personalize(ctx, isBeforeRemote, personalizationRule, data, function(err){ + if(err) { + return cb(err); + } + checkRelationAndRecurse(Model, data, options, cb); + }); + } } }); } + cb(); }); - // PersonalizationRule.find() - end + } let PersonalizationRule = null; @@ -821,9 +822,134 @@ function init(app) { }); } +function doMethodGating(ctx, records, cb) { + // let { _personalizationCache: { info, records }} = ctx; + // Assume all allowed for now + nextTick(() => cb(true)); +} + +function doRoleGating(ctx, records, cb) { + //Assume all allowed for now + nextTick(() => cb(null, true)); +} + +function getPersonalizationMeta(info, isBeforeRemote) { + let { methodName, isStatic, model } = info; + let data = null; + let applyPersonalizationFlag = true; + let theModel = model; + if(isStatic) { + switch(methodName) { + case 'create': + case 'patchOrCreate': + data = isBeforeRemote ? ctx.req.body : ctx.result; + break; + case 'find': + case 'findById': + case 'findOne': + data = isBeforeRemote ? {} : ctx.result; + break; + default: + //!some static method we missed... + let statInfo = getValidStaticMethod(info, isBeforeRemote); + if(!statInfo.isValid) { + applyPersonalizationFlag = false; + } + } + } + else { + switch(methodName) { + case 'patchAttributes': + data = isBeforeRemote ? ctx.req.body: ctx.result; + break; + default: + let instInfo = getValidInstanceInfo(info, isBeforeRemote); + if(!instInfo.isValid) { + applyPersonalizationFlag = false; + } + } + } + + return { applyPersonalizationFlag, model: theModel, data } +} +function runPersonalizationPipeline(ctx, isBeforeRemote, cb) { + let { _personalizationCache : { info, records }} = ctx; + // let { methodName, model, isStatic } = info; + let pMeta = getPersonalizationMeta(info, isBeforeRemote); + if(pMeta.applyPersonalizationFlag) { + let { data, model }; + let pOptions = { + isBeforeRemote, + context: ctx + } + return applyServicePersonalization(model, data, records, pOptions, cb); + } + next(cb); +} + +function performPersonalizations(ctx, isBeforeRemote, cb) { + // doMethodGating(ctx, isBeforeRemote, function(isAllowed) { + // if(isAllowed) { + // return doRoleGating(ctx, isBeforeRemote, function(err, applyPersonalization){ + // if(err) { + // return cb(err); + // } + + // if(applyPersonalization) { + // return runPersonalizationPipeline(ctx, isBeforeRemote, cb); + // } + // cb(); + // }); + // } + // cb(); + // }); + runPersonalizationPipeline(ctx, isBeforeRemote, cb); +} + +function fetchPersonalizationRoles(ctx, cb) { + //! doing nothing + next(cb); +} + +/** + * fetches the personalization rules + * and checks against methodName if + * personalizations are applicable + * + * @param {string} model + * @param {function} cb + */ +function runPersonalizations(ctx, isBeforeRemote, cb) { + if(isBeforeRemote) { + //! we have not fetched the personalization record(s) + + //fetch the personalization record and store it in ctx + let info = parseMethodString(ctx.methodString); + return fetchPersonalizationRecords(ctx, info.model, function(err, records) { + if(err) { + return cb(err); + } + ctx._personalizationCache = { + records, + info + }; + fetchPersonalizationRoles(ctx, function(err){ + if(err) { + return cb(err) + } + performPersonalizations(ctx, isBeforeRemote, cb); + }); + }); + } + else { + performPersonalizations(ctx, isBeforeRemote, cb); + } +}; + module.exports = { loadCustomFunction, getCustomFunction, applyServicePersonalization, - init + init, + runPersonalizations }; From 3145b2342f778b3d3633940ad890354f460dc987 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 12 May 2020 19:16:15 +0530 Subject: [PATCH 31/80] added fix for loading customFunctionPath --- lib/service-personalizer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index fa4e1f2..202b8cb 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -788,7 +788,9 @@ let PersonalizationRule = null; function init(app) { PersonalizationRule = app.models.PersonalizationRule; let servicePersoConfig = app.get('servicePersonalization'); - loadCustomFunction(require(servicePersoConfig.customFunctionPath)); + if(servicePersoConfig && 'customFunctionPath' in servicePersoConfig) { + loadCustomFunction(require(servicePersoConfig.customFunctionPath)); + } PersonalizationRule.observe('before save', function (ctx, next) { log.debug(ctx, 'PersonalizationRule: before save'); let data = ctx.__data || ctx.instance || ctx.data; From ee3ca43d44e898892ce5163dc49a55db8abb7a26 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 12 May 2020 19:19:41 +0530 Subject: [PATCH 32/80] lint fix --- lib/service-personalizer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 202b8cb..1759211 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -788,7 +788,7 @@ let PersonalizationRule = null; function init(app) { PersonalizationRule = app.models.PersonalizationRule; let servicePersoConfig = app.get('servicePersonalization'); - if(servicePersoConfig && 'customFunctionPath' in servicePersoConfig) { + if (servicePersoConfig && 'customFunctionPath' in servicePersoConfig) { loadCustomFunction(require(servicePersoConfig.customFunctionPath)); } PersonalizationRule.observe('before save', function (ctx, next) { From a717a1c136e6bbb0dccc08526e7c2ce83bed4343 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 12 May 2020 19:35:20 +0530 Subject: [PATCH 33/80] v2.3.1 - reverted commit to 86c23 - fixed loading for customFunction --- lib/service-personalizer.js | 4 +--- test/test.js | 20 +------------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 1759211..fa4e1f2 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -788,9 +788,7 @@ let PersonalizationRule = null; function init(app) { PersonalizationRule = app.models.PersonalizationRule; let servicePersoConfig = app.get('servicePersonalization'); - if (servicePersoConfig && 'customFunctionPath' in servicePersoConfig) { - loadCustomFunction(require(servicePersoConfig.customFunctionPath)); - } + loadCustomFunction(require(servicePersoConfig.customFunctionPath)); PersonalizationRule.observe('before save', function (ctx, next) { log.debug(ctx, 'PersonalizationRule: before save'); let data = ctx.__data || ctx.instance || ctx.data; diff --git a/test/test.js b/test/test.js index ddecfac..7624cec 100755 --- a/test/test.js +++ b/test/test.js @@ -1978,7 +1978,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t40(a) should demonstrate personalization is being applied recursively', done => { + it('t40 should demonstrate personalization is being applied recursively', done => { let data = [ { "modelName" : "AddressBook", @@ -2052,24 +2052,6 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); }); - - it('t40(b) should apply service personalization to a related model invoked via remote call', function(done) { - let url = `${productOwnerUrl}/1/ProductCatalog?access_token=${accessToken}`; - api.get(url) - .set('Accept', 'application/json') - .set('REMOTE_USER', 'testUser') - .set('region', 'kl') - .expect(200) - .end((err, resp) => { - if(err){ - return done(err); - } - let result = resp.body; - // console.log(resp.body); - expect(result[0].modelNo).to.equal('123456XXXX'); - done(); - }); - }); }); describe('Remote method tests', () => { From 6684d9f5f03242b9903147ed2eaa600989ebf12c Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 12 May 2020 19:50:53 +0530 Subject: [PATCH 34/80] reverted to state v2.3.0 --- .../mixins/service-personalization-mixin.js | 50 +++++-------------- 1 file changed, 12 insertions(+), 38 deletions(-) diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index 585da14..69f0f0d 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -25,21 +25,8 @@ const log = logger('service-personalization-mixin'); const { applyServicePersonalization } = require('./../../lib/service-personalizer'); const { nextTick, parseMethodString, slice } = require('./../../lib/utils'); -const getRelationInfo = (parentModel, { methodName }) => { - const idx = 7; //__get__ - const REL_GET_STR = '__get__'; - if(methodName.substr(0,idx) === REL_GET_STR) { - let relName = methodName.substr(idx); - relationDef = parentModel.settings.relations[relName]; - return { isRelation: true, model: relationDef.model }; - } - - return { isRelation: false } -}; - module.exports = function ServicePersonalizationMixin(TargetModel) { log.debug(log.defaultContext(), `Applying service personalization for ${TargetModel.definition.name}`); - const TARGET_MODEL_NAME = TargetModel.definition.name; TargetModel.afterRemote('**', function ServicePersonalizationAfterRemoteHook() { let args = slice(arguments); let ctx = args[0]; @@ -51,11 +38,13 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { let data = null; let applyFlag = true; - let toModel = TARGET_MODEL_NAME; + if (ctxInfo.isStatic) { switch (ctxInfo.methodName) { case 'create': case 'patchOrCreate': + data = ctx.result; + break; case 'find': case 'findById': case 'findOne': @@ -72,16 +61,9 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { data = ctx.result; break; default: - let relationInfo = getRelationInfo(TargetModel, ctxInfo); - if(relationInfo.isRelation) { - applyFlag = true; - toModel = relationInfo.model; - data = ctx.result; - } - else { - applyFlag = false; - log.debug(ctx, `afterRemote: Unhandled non-static - ${ctx.methodString}`); - } + log.debug(ctx, `afterRemote: Unhandled non-static - ${ctx.methodString}`); + data = {}; + applyFlag = false; } } @@ -91,7 +73,7 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { context: ctx }; - applyServicePersonalization(toModel, data, personalizationOptions, function (err) { + applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function (err) { if (err) { next(err); } else { @@ -107,14 +89,13 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { let args = slice(arguments); let ctx = args[0]; let next = args[args.length - 1]; + // let callCtx = ctx.req.callContext; log.debug(ctx, `beforeRemote: MethodString: ${ctx.methodString}`); let ctxInfo = parseMethodString(ctx.methodString); let applyFlag = true; let data = null; - let toModel = TARGET_MODEL_NAME; - if (ctxInfo.isStatic) { switch (ctxInfo.methodName) { case 'create': @@ -137,16 +118,9 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { data = ctx.req.body; break; default: - let relationInfo = getRelationInfo(TargetModel, ctxInfo); - if(relationInfo.isRelation) { - applyFlag = true; - toModel = relationInfo.model; - data = {}; - } - else { - applyFlag = false; - log.debug(ctx, `beforeRemote: Unhandled non-static - ${ctx.methodString}`); - } + data = {}; + log.debug(ctx, `beforeRemote: Unhandled non-static: ${ctx.methodString}`); + applyFlag = false; } } @@ -156,7 +130,7 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { context: ctx }; - applyServicePersonalization(toModel, data, personalizationOptions, function (err) { + applyServicePersonalization(ctxInfo.modelName, data, personalizationOptions, function (err) { next(err); }); } else { From 4819a52588a20c0fbd08ae4744a151fdc93c3fae Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 12 May 2020 19:52:53 +0530 Subject: [PATCH 35/80] v2.3.2 - fix for customFunctionPath load --- lib/service-personalizer.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index fa4e1f2..f5c3933 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -788,7 +788,9 @@ let PersonalizationRule = null; function init(app) { PersonalizationRule = app.models.PersonalizationRule; let servicePersoConfig = app.get('servicePersonalization'); - loadCustomFunction(require(servicePersoConfig.customFunctionPath)); + if (servicePersoConfig && servicePersoConfig.customFunctionPath) { + loadCustomFunction(require(servicePersoConfig.customFunctionPath)); + } PersonalizationRule.observe('before save', function (ctx, next) { log.debug(ctx, 'PersonalizationRule: before save'); let data = ctx.__data || ctx.instance || ctx.data; From 642cb8afbfcec9cdba9d3d30051d2190c76c40b6 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 12 May 2020 23:05:47 +0530 Subject: [PATCH 36/80] restored original test state --- .../mixins/service-personalization-mixin.js | 6 +- lib/service-personalizer.js | 105 ++++++++++++++---- test/common/models/product-owner.js | 23 ++-- test/test.js | 2 +- 4 files changed, 99 insertions(+), 37 deletions(-) diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index 774b3a6..c255e54 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -23,7 +23,7 @@ const logger = require('oe-logger'); const log = logger('service-personalization-mixin'); const { runPersonalizations } = require('./../../lib/service-personalizer'); -// const { nextTick, parseMethodString, slice } = require('./../../lib/utils'); +const { slice } = require('./../../lib/utils'); module.exports = function ServicePersonalizationMixin(TargetModel) { log.debug(log.defaultContext(), `Applying service personalization for ${TargetModel.definition.name}`); @@ -34,7 +34,7 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { // let callCtx = ctx.req.callContext; log.debug(ctx, `afterRemote: (enter) MethodString: ${ctx.methodString}`); runPersonalizations(ctx, false, function(err){ - log.debug(ctx, `afterRemote: (leave) MethodString: ${ctx.methodString}`); + log.debug(ctx, `afterRemote: (leave${err ? '- with error' : ''}) MethodString: ${ctx.methodString}`); next(err); }); @@ -49,7 +49,7 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { // let ctxInfo = parseMethodString(ctx.methodString); runPersonalizations(ctx, true, function(err){ - log.debug(ctx, `beforeRemote: (leave) MethodString: ${ctx.methodString}`); + log.debug(ctx, `beforeRemote: (leave${err ? '- with error' : ''}) MethodString: ${ctx.methodString}`); next(err); }); }); diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 40a4a29..f10477c 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -715,23 +715,23 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { } let callback = function (err) { - log.debug(context, `${prefix}: processing relation "${relationName}"/"${name}" - finished`); + log.debug(context, `${prefix}: (leave${err ? '- with error' : ''}) processing relation "${name}/${relationName}"`); done(err); }; callback.__trace = `${name}_${relationName}`; if (applyFlag) { - log.debug(context, `${prefix}: processing relation "${relationName}"/"${name}"`); + log.debug(context, `${prefix}: (enter) processing relation: ${name}/${relationName}`); let { context: { _personalizationCache : { records }} } = personalizationOptions; // return applyServicePersonalization(relModel, relData, records, ) // return applyServicePersonalization(relModel, relData, personalizationOptions, callback); - return fetchPersonalizationRecords(ctx, relModel, function(err, relModelP13nRecords){ + return fetchPersonalizationRecords(context, relModel, function(err, relModelP13nRecords){ if(err) { - return done(err); + return callback(err); } - applyServicePersonalization(relModel, relData, relModelP13nRecords, personalizationOptions, done); + applyServicePersonalization(relModel, relData, relModelP13nRecords, personalizationOptions, callback); }); } - log.debug(context, `${prefix}: processing relation "${relationName}"/"${name}" - skipped`); + log.debug(context, `${prefix}: (leave) processing relation "${relationName}"/"${name}" - skipped`); nextTick(done); }; return async.eachSeries(relationItems, relationsIterator, done); @@ -751,12 +751,19 @@ function fetchPersonalizationRecords(ctx, forModel, cb) { function applyServicePersonalization(modelName, data, records, options, cb) { let Model = loopback.findModel(modelName); + let ctx = options.context; + let isBeforeRemote = options.isBeforeRemote; + log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (enter) applying personalization for model: ${modelName}`); + let done = err => { + log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? ' - with error': ''}) applying personalization for model: ${modelName}`); + cb(err); + } doMethodGating(ctx, records, function(isAllowedMethod){ if(isAllowedMethod) { return doRoleGating(ctx, records, function(isAllowedRole){ if(isAllowedRole) { if(records.length === 0) { - checkRelationAndRecurse(Model, data, options, cb); + checkRelationAndRecurse(Model, data, options, done); } else { let entry = records[0]; @@ -765,17 +772,19 @@ function applyServicePersonalization(modelName, data, records, options, cb) { let isBeforeRemote = options.isBeforeRemote; personalize(ctx, isBeforeRemote, personalizationRule, data, function(err){ if(err) { - return cb(err); + return done(err); } - checkRelationAndRecurse(Model, data, options, cb); + checkRelationAndRecurse(Model, data, options, done); }); } } + else { + done(); + } }); } - cb(); - }); - + done(); + }); } let PersonalizationRule = null; @@ -832,14 +841,34 @@ function doMethodGating(ctx, records, cb) { function doRoleGating(ctx, records, cb) { //Assume all allowed for now - nextTick(() => cb(null, true)); + + //! we need to extract info from context cache + nextTick(() => cb(true)); +} + +function getValidStaticMethod(info, isBeforeRemote) { + let { modelName, methodName } = info; + let Model = loopback.findModel(modelName); + if(Model[methodName]) { + return { isValid: true}; + } + return { isValid: false }; +} + +function getValidInstanceMethod(info, isBeforeRemote) { + let { modelName, methodName } = info; + let Model = loopback.findModel(modelName); + if(Model.prototype[methodName]) { + return { isValid : true }; + } + return { isValid : false }; } -function getPersonalizationMeta(info, isBeforeRemote) { - let { methodName, isStatic, model } = info; +function getPersonalizationMeta(ctx, info, isBeforeRemote) { + let { methodName, isStatic, modelName } = info; let data = null; let applyPersonalizationFlag = true; - let theModel = model; + let theModel = modelName; if(isStatic) { switch(methodName) { case 'create': @@ -857,7 +886,16 @@ function getPersonalizationMeta(info, isBeforeRemote) { if(!statInfo.isValid) { applyPersonalizationFlag = false; } - } + else { + let httpMethod = ctx.req.method; + if(httpMethod === 'POST' || httpMethod === 'PUT') { + data = isBeforeRemote ? ctx.req.body : ctx.result; + } + else { + data = isBeforeRemote ? {} : ctx.result; + } + } + } // end switch() } else { switch(methodName) { @@ -865,10 +903,19 @@ function getPersonalizationMeta(info, isBeforeRemote) { data = isBeforeRemote ? ctx.req.body: ctx.result; break; default: - let instInfo = getValidInstanceInfo(info, isBeforeRemote); + let instInfo = getValidInstanceMethod(info, isBeforeRemote); if(!instInfo.isValid) { applyPersonalizationFlag = false; } + else { + let httpMethod = ctx.req.method; + if(httpMethod === 'POST' || httpMethod === 'PUT') { + data = isBeforeRemote ? ctx.req.body : ctx.result; + } + else { + data = isBeforeRemote ? {} : ctx.result; + } + } } } @@ -877,9 +924,9 @@ function getPersonalizationMeta(info, isBeforeRemote) { function runPersonalizationPipeline(ctx, isBeforeRemote, cb) { let { _personalizationCache : { info, records }} = ctx; // let { methodName, model, isStatic } = info; - let pMeta = getPersonalizationMeta(info, isBeforeRemote); + let pMeta = getPersonalizationMeta(ctx, info, isBeforeRemote); if(pMeta.applyPersonalizationFlag) { - let { data, model }; + let { data, model } = pMeta; let pOptions = { isBeforeRemote, context: ctx @@ -910,7 +957,7 @@ function performPersonalizations(ctx, isBeforeRemote, cb) { function fetchPersonalizationRoles(ctx, cb) { //! doing nothing - next(cb); + nextTick(cb); } /** @@ -948,10 +995,24 @@ function runPersonalizations(ctx, isBeforeRemote, cb) { } }; +function performServicePersonalizations(modelName, data, options, cb) { + let ctx = options.context; + log.debug(ctx, `performServicePersonalizations: (enter) model -> ${modelName}`); + let done = err => { + log.debug(ctx, `performServicePersonalizations: (leave${err ? '- with error': ''}) model -> ${modelName}`); + }; + fetchPersonalizationRecords(ctx, modelName, function(err, records){ + if(err){ + return done(err); + } + applyServicePersonalization(modelName, data, records, options, done); + }); +} module.exports = { loadCustomFunction, getCustomFunction, applyServicePersonalization, init, - runPersonalizations + runPersonalizations, + performServicePersonalizations }; diff --git a/test/common/models/product-owner.js b/test/common/models/product-owner.js index 82788fe..203be43 100644 --- a/test/common/models/product-owner.js +++ b/test/common/models/product-owner.js @@ -48,17 +48,18 @@ module.exports = function(ProductOwner) { "where": { "id": ownerId } }; ProductOwner.findOne(filter, options, function(err, result) { - if(err) { - done(err) - } - else { - let persOpts = { - isBeforeRemote: false, context: options - }; - applyServicePersonalization('ProductOwner', result, persOpts, function(err){ - done(err, result); - }); - } + // if(err) { + // done(err) + // } + // else { + // let persOpts = { + // isBeforeRemote: false, context: options + // }; + // applyServicePersonalization('ProductOwner', result, persOpts, function(err){ + // done(err, result); + // }); + // } + done(err, result); }) }; } \ No newline at end of file diff --git a/test/test.js b/test/test.js index ddecfac..b617758 100755 --- a/test/test.js +++ b/test/test.js @@ -2096,7 +2096,7 @@ describe(chalk.blue('service personalization test started...'), function () { } } } - ]; + ] PersonalizationRule.create(data, {}, function(err){ done(err); From 05131f882ad8676b79ea461c9662d1c04264545f Mon Sep 17 00:00:00 2001 From: deostroll Date: Wed, 13 May 2020 12:52:49 +0530 Subject: [PATCH 37/80] added code and test for concrete service personalization api --- lib/service-personalizer.js | 3 +- test/common/models/pseudo-product-owner.js | 64 ++++++++++++++++++++ test/common/models/pseudo-product-owner.json | 4 ++ test/model-config.json | 4 ++ test/test.js | 28 +++++++-- 5 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 test/common/models/pseudo-product-owner.js create mode 100644 test/common/models/pseudo-product-owner.json diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index f10477c..ac1642b 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -721,7 +721,7 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { callback.__trace = `${name}_${relationName}`; if (applyFlag) { log.debug(context, `${prefix}: (enter) processing relation: ${name}/${relationName}`); - let { context: { _personalizationCache : { records }} } = personalizationOptions; + // let { context: { _personalizationCache : { records }} } = personalizationOptions; // return applyServicePersonalization(relModel, relData, records, ) // return applyServicePersonalization(relModel, relData, personalizationOptions, callback); return fetchPersonalizationRecords(context, relModel, function(err, relModelP13nRecords){ @@ -1000,6 +1000,7 @@ function performServicePersonalizations(modelName, data, options, cb) { log.debug(ctx, `performServicePersonalizations: (enter) model -> ${modelName}`); let done = err => { log.debug(ctx, `performServicePersonalizations: (leave${err ? '- with error': ''}) model -> ${modelName}`); + cb(err); }; fetchPersonalizationRecords(ctx, modelName, function(err, records){ if(err){ diff --git a/test/common/models/pseudo-product-owner.js b/test/common/models/pseudo-product-owner.js new file mode 100644 index 0000000..67657f4 --- /dev/null +++ b/test/common/models/pseudo-product-owner.js @@ -0,0 +1,64 @@ +const { performServicePersonalizations } = require('./../../../lib/service-personalizer'); // or require('oe-service-personalization/lib/service-personalizer'); +const loopback = require('loopback'); + +module.exports = function(PseudoProductOwner) { + PseudoProductOwner.remoteMethod('demandchain', { + description: 'Gets the stores, store addresses, and, contacts of a product owner', + accepts: [ + { + arg: 'id', + type: 'number', + description: 'the unique id of the owner', + required: true + }, + { + arg: 'options', + type: 'object', + http:function(ctx) { + return ctx; + } + } + ], + returns: { + arg: 'chain', + root: true, + type: 'object' + }, + http: { path: '/:id/demandchain', verb: 'get' } + }); + + PseudoProductOwner.demandchain = function(ownerId, options, done) { + if(typeof done === 'undefined' && typeof options === 'function') { + done = options; + options = {}; + }; + let ProductOwner = loopback.findModel('ProductOwner'); + let filter = { + "include": [ + { + "ProductCatalog" : { + "store": { + "store" : { + "addresses" : "phones" + } + } + } + }, + "address" + ], + "where": { "id": ownerId } + }; + ProductOwner.findOne(filter, options, function(err, result) { + if(err) { + return done(err); + } + let persOptions = { + isBeforeRemote: false, + context: options + } + performServicePersonalizations(ProductOwner.definition.name, result, persOptions, function(err){ + done(err, result); + }) + }); + }; +} \ No newline at end of file diff --git a/test/common/models/pseudo-product-owner.json b/test/common/models/pseudo-product-owner.json new file mode 100644 index 0000000..18c9244 --- /dev/null +++ b/test/common/models/pseudo-product-owner.json @@ -0,0 +1,4 @@ +{ + "name": "PseudoProductOwner", + "base": "Model" +} diff --git a/test/model-config.json b/test/model-config.json index 7f45354..b926055 100644 --- a/test/model-config.json +++ b/test/model-config.json @@ -55,5 +55,9 @@ "StoreStock" : { "dataSource" : "db", "public": true + }, + "PseudoProductOwner" : { + "dataSource": "db", + "public": "true" } } diff --git a/test/test.js b/test/test.js index b617758..6b77e5a 100755 --- a/test/test.js +++ b/test/test.js @@ -28,11 +28,11 @@ var basePath = app.get('restApiRoot'); var productCatalogUrl = basePath + '/ProductCatalogs'; var productOwnerUrl = basePath + '/ProductOwners'; describe(chalk.blue('service personalization test started...'), function () { - this.timeout(10000); + // this.timeout(10000); var accessToken; before('wait for boot scripts to complete', function (done) { app.on('test-start', function () { - console.log('booted'); + // console.log('booted'); ProductCatalog = loopback.findModel('ProductCatalog'); ProductCatalog.destroyAll(function (err, info) { return done(err); @@ -2073,7 +2073,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); describe('Remote method tests', () => { - before('re-inserting the personalization rules', done => { + beforeEach('re-inserting the personalization rules', done => { let data = [ { "modelName" : "AddressBook", @@ -2103,7 +2103,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t41 should demonstrate data getting personalized via a remote method', done => { + it('t41(a) should demonstrate data getting personalized via a custom remote method of model that has mixin enabled', done => { let ownerId = 12; let url = `${productOwnerUrl}/${ownerId}/demandchain?access_token=${accessToken}`; @@ -2122,6 +2122,26 @@ describe(chalk.blue('service personalization test started...'), function () { } }) }); + + it('t41(b) should personalize the response of a custom remote method of model that does not have mixin enabled (api usage)', done => { + let ownerId = 12; + let pseudoProductOwnerUrl = '/api/PseudoProductOwners'; + let url = `${pseudoProductOwnerUrl}/${ownerId}/demandchain?access_token=${accessToken}`; + api.get(url) + .set('Accept', 'application/json') + .set('REMOTE_USER', 'testUser') + .expect(200) + .end((err, resp) => { + if(err) { + done(err); + } + else { + let result = resp.body; + expect(result).to.deep.equal(httpResult); + done(); + } + }) + }); }); }); From 543f80bea52b527dabeef4603a53ccea3266709f Mon Sep 17 00:00:00 2001 From: deostroll Date: Sun, 17 May 2020 11:15:13 +0530 Subject: [PATCH 38/80] impl for prop level personalization --- lib/service-personalizer.js | 144 ++++++++++++++++++++++++---- test/test.js | 185 +++++++++++++++++++++++++++++++++++- 2 files changed, 309 insertions(+), 20 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index ac1642b..5a3d6df 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -749,6 +749,116 @@ function fetchPersonalizationRecords(ctx, forModel, cb) { PersonalizationRule.find(filter, callContext, cb); } +function checkPropsAndRecurse(Model, data, options, cb) { + let ctx = options.context; + let { isBeforeRemote } = options; + let modelName = Model.definition.name; + let done = err => { + log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? '- with error' : ''}) applying personalization on properties of ${modelName}`); + cb(err); + }; + + let getModelCtorProps = lbModel => { + let propHash = lbModel.definition.properties; + let props = Object.entries(propHash); + let ctorTestPrimitive = ctor => ctor === String || ctor === Number || ctor === Date || ctor === Boolean; + let unAllowedCtors = ['ObjectID', 'Object']; + let ctorTestNames = ctor => unAllowedCtors.includes(ctor.name); + let ctorTest = ctor => ctorTestPrimitive(ctor) || ctorTestNames(ctor); + let modelProps = props.reduce((carrier, item) => { + let [fieldName, propDef] = item; + let { type } = propDef; + let addFlag = false; + if(fieldName === 'id') { + return carrier; + } + if(typeof type === 'function' && !ctorTest(type)) { + addFlag = true; + } + else if(typeof type === 'object' && Array.isArray(type)) { + addFlag = type.some(ctorFn => !ctorTest(ctorFn)); + } + if(addFlag){ + carrier.hasModelCtorProps = true; + carrier.modelProps.push(item); + } + return carrier; + }, { modelProps: [], hasModelCtorProps: false }); + return modelProps; + }; + + let { _personalizationCache: { info }} = ctx; + + let isRoot = false; + log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (enter) applying personalization on properties of ${modelName}`); + if(info && info.modelName === modelName) { + //! this is the root model in a remote call + isRoot = true; + } + let mInfo = getModelCtorProps(Model); + if(mInfo.hasModelCtorProps) { + return async.eachSeries(mInfo.modelProps, function propPersonalizerFn([key, propDef], cb) { + let { type: { name }} = propDef; + let relations = Model.settings.relations; + let done = err => { + log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? '- with error' : ''}) personalizing field ${modelName}/${key}`); + cb(err); + } + log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (enter) personalizing field ${modelName}/${key}`); + // begin - task01 + + // if the property is an embedsOne relation + // property, skip the processing + + //! Because they might already be personalized + + let isAnEmbedsOneRelationProp = Object.entries(relations).some(([relationName, relationDef]) => { + utils.noop(relationName); + // TODO: Add more checks here...(?) + return relationDef.type === 'embedsOne' && relationDef.property === key; + }); + // end - task01 + + + if(!isAnEmbedsOneRelationProp) { + let unpersonalizedData = null; + let modelCtorName = null; + + //begin - task02 - extract data + + if(typeof type === 'function') { + //! this is a plain model constructor + modelCtorName = modelCtor.name; + unpersonalizedData = data[key]; + } + else { + //! this is an array of model constructors + + //! Only one model constructor available(?) + let modelCtor = type[0]; + modelCtorName = modelCtor.name; + unpersonalizedData = data[key]; + }// end if-else block - if(typeof type === 'function') + + //end - task02 - extract data + + return fetchPersonalizationRecords(ctx, modelCtorName, function(err, records){ + if(err) { + return done(err); + } + applyServicePersonalization(modelCtorName, unpersonalizedData, records, options, done); + }); + + } //end if-block if(!isAnEmbedsOneRelationProp) + + nextTick(done); + }, function(err){ + done(err); + }); + } + nextTick(done); +} + function applyServicePersonalization(modelName, data, records, options, cb) { let Model = loopback.findModel(modelName); let ctx = options.context; @@ -763,7 +873,12 @@ function applyServicePersonalization(modelName, data, records, options, cb) { return doRoleGating(ctx, records, function(isAllowedRole){ if(isAllowedRole) { if(records.length === 0) { - checkRelationAndRecurse(Model, data, options, done); + checkRelationAndRecurse(Model, data, options, function(err){ + if(err) { + return done(err); + } + checkPropsAndRecurse(Model, data, options, done); + }); } else { let entry = records[0]; @@ -774,7 +889,12 @@ function applyServicePersonalization(modelName, data, records, options, cb) { if(err) { return done(err); } - checkRelationAndRecurse(Model, data, options, done); + checkRelationAndRecurse(Model, data, options, function(err) { + if(err) { + return done(err); + } + checkPropsAndRecurse(Model, data, options, done); + }); }); } } @@ -935,23 +1055,8 @@ function runPersonalizationPipeline(ctx, isBeforeRemote, cb) { } next(cb); } - -function performPersonalizations(ctx, isBeforeRemote, cb) { - // doMethodGating(ctx, isBeforeRemote, function(isAllowed) { - // if(isAllowed) { - // return doRoleGating(ctx, isBeforeRemote, function(err, applyPersonalization){ - // if(err) { - // return cb(err); - // } - - // if(applyPersonalization) { - // return runPersonalizationPipeline(ctx, isBeforeRemote, cb); - // } - // cb(); - // }); - // } - // cb(); - // }); +// FIXME: We don't need this method. +function performPersonalizations(ctx, isBeforeRemote, cb) { runPersonalizationPipeline(ctx, isBeforeRemote, cb); } @@ -1002,6 +1107,7 @@ function performServicePersonalizations(modelName, data, options, cb) { log.debug(ctx, `performServicePersonalizations: (leave${err ? '- with error': ''}) model -> ${modelName}`); cb(err); }; + ctx._personalizationCache = {}; fetchPersonalizationRecords(ctx, modelName, function(err, records){ if(err){ return done(err); diff --git a/test/test.js b/test/test.js index 6b77e5a..632644e 100755 --- a/test/test.js +++ b/test/test.js @@ -1,6 +1,6 @@ var oecloud = require('oe-cloud'); var loopback = require('loopback'); - +var async = require('async'); oecloud.boot(__dirname, function (err) { if (err) { console.log(err); @@ -2143,6 +2143,189 @@ describe(chalk.blue('service personalization test started...'), function () { }) }); }); + + /** + * These tests describe how the property + * level personalizations work + * + */ + describe('property level personalizations', () => { + let ModelDefinition = null; + before('deleting customer model created before', done => { + ModelDefinition = loopback.findModel('ModelDefinition'); + + let removeOldData = done => { + let Customer = loopback.findModel("Customer"); + Customer.destroyAll({}, {}, done); + }; + + let removeModelDef = function(done) { + ModelDefinition.findOne({ name: 'Customer'}, {}, function(err, def){ + if(err) { + return done(err); + } + let id = def.id; + ModelDefinition.destroyById( id, {}, function(err){ + done(err); + }); + }); + }; + + async.eachSeries([removeOldData, removeModelDef],function(fn, cb){ + fn(cb); + }, function(err){ + done(err); + }); + }); + + before('creating models dynamically', done => { + + let AccountModel = { + name:'Account', + properties: { + "accountType": "string", + "openedOn": "date" + }, + relations: { + "customer": { + type: "embedsOne", + model: "Customer", + property:"linkedCustomer" + } + } + }; + + let KycModel = { + name:'Kyc', + plural: 'Kyc', + properties: { + "criteria": "string", + "code": "string", + "remark":"string", + "score": "number" + } + }; + + let CustomerModel = { + name: "Customer", + base:"BaseEntity", + properties: { + firstName: "string", + lastName: "string", + salutation: "string", + dob: "date", + kycInfo: [ 'Kyc' ] + }, + relations: { + all_accounts : { + type: 'hasMany', + model: 'Account' + } + } + }; + + + async.eachSeries([KycModel, CustomerModel, AccountModel], function(spec, cb){ + spec.mixins = { ServicePersonalizationMixin: true }; //enabling service personalization mixin + ModelDefinition.create(spec, {}, function(err){ + cb(err); + }); + }, function(err){ + done(err); + }); + }); + + let Customer = null; + before('creating a new customers', done => { + Customer = loopback.findModel('Customer'); + let data = [ + { + id: 1, + firstName: 'Cust', + lastName:'One', + salutation: 'Mr', + kycInfo: [ + { + 'criteria': 'isEmployed', + 'code': 'BCODE-0056', + 'remark': 'SC Bank', + 'score': 56.23 + }, + { + 'criteria': 'allowedAgeLimit', + 'code': 'BCODE-0057', + 'remark': 'witin 25 to 35', + 'score': 76.24 + } + ], + dob: new Date(1987, 3, 12) + }, + { + id: 2, + firstName: 'Cust', + lastName:'Two', + salutation: 'Mrs', + kycInfo: [ + { + 'criteria': 'isEmployed', + 'code': 'BCODE-0056', + 'remark': 'Unemployed', + 'score': -23.23 + }, + { + 'criteria': 'allowedAgeLimit', + 'code': 'BCODE-0058', + 'remark': 'witin 25 to 35', + 'score': 76.24 + } + ], + dob: new Date(1989, 3, 12) + } + ]; + Customer.create(data, {}, function(err){ + done(err); + }); + }); + + before('creating personalization rules', done => { + let data = { + modelName: 'Kyc', + personalizationRule: { + fieldMask: { + code: { + 'pattern': '([A-Z]{4})\\-(\d{4})', + 'maskCharacter': '*', + 'format': '$1-$2', + 'mask': ['$1'] + } + } + } + }; + PersonalizationRule.create(data, {}, function(err){ + done(err); + }); + }); + + it('t42 when fetching a customer record the kycInfo field should also be personalized', done => { + let custUrl = `/api/Customers/1`; + api.get(custUrl) + .set('Accept', 'application/json') + .set('REMOTE_USER', 'testUser') + .expect(200) + .end((err, resp) =>{ + done(err); + let result = resp.body; + expect(result).to.be.object; + expect(result).to.have.property('firstName'); + expect(result.kycInfo).to.be.array; + result.kycInfo.forEach(kycItem => { + let lastFour = kycItem.code.substr(-4); + let expectedString = `*****-${lastFour}`; + expect(kycItem.code).to.equal(expectedString); + }); + }); + }); + }); }); From 0af015ec29f9a61a41beeeff54bb10a9bc6fc35b Mon Sep 17 00:00:00 2001 From: deostroll Date: Sun, 17 May 2020 12:59:29 +0530 Subject: [PATCH 39/80] support for property level personalizations: --- lib/service-personalizer.js | 30 +++++++++++++++-------------- test/test.js | 38 ++++++------------------------------- 2 files changed, 22 insertions(+), 46 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 5a3d6df..746f5bb 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -787,24 +787,26 @@ function checkPropsAndRecurse(Model, data, options, cb) { return modelProps; }; - let { _personalizationCache: { info }} = ctx; - - let isRoot = false; - log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (enter) applying personalization on properties of ${modelName}`); - if(info && info.modelName === modelName) { - //! this is the root model in a remote call - isRoot = true; - } + let extractData = (key, data) => { + if(data.__data) { + return data.__data[key]; + } + else { + return data[key] + } + }; + let mInfo = getModelCtorProps(Model); if(mInfo.hasModelCtorProps) { return async.eachSeries(mInfo.modelProps, function propPersonalizerFn([key, propDef], cb) { - let { type: { name }} = propDef; + let { type } = propDef; + let relations = Model.settings.relations; let done = err => { - log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? '- with error' : ''}) personalizing field ${modelName}/${key}`); + log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? '- with error' : ''}) personalizing field ${modelName}/${key}`); cb(err); } - log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (enter) personalizing field ${modelName}/${key}`); + log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (enter) personalizing field ${modelName}/${key}`); // begin - task01 // if the property is an embedsOne relation @@ -828,8 +830,8 @@ function checkPropsAndRecurse(Model, data, options, cb) { if(typeof type === 'function') { //! this is a plain model constructor - modelCtorName = modelCtor.name; - unpersonalizedData = data[key]; + modelCtorName = type.name; + unpersonalizedData = extractData(key, data); } else { //! this is an array of model constructors @@ -837,7 +839,7 @@ function checkPropsAndRecurse(Model, data, options, cb) { //! Only one model constructor available(?) let modelCtor = type[0]; modelCtorName = modelCtor.name; - unpersonalizedData = data[key]; + unpersonalizedData = extractData(key, data); }// end if-else block - if(typeof type === 'function') //end - task02 - extract data diff --git a/test/test.js b/test/test.js index 632644e..ec2e5eb 100755 --- a/test/test.js +++ b/test/test.js @@ -2150,36 +2150,10 @@ describe(chalk.blue('service personalization test started...'), function () { * */ describe('property level personalizations', () => { - let ModelDefinition = null; - before('deleting customer model created before', done => { - ModelDefinition = loopback.findModel('ModelDefinition'); - - let removeOldData = done => { - let Customer = loopback.findModel("Customer"); - Customer.destroyAll({}, {}, done); - }; - - let removeModelDef = function(done) { - ModelDefinition.findOne({ name: 'Customer'}, {}, function(err, def){ - if(err) { - return done(err); - } - let id = def.id; - ModelDefinition.destroyById( id, {}, function(err){ - done(err); - }); - }); - }; - - async.eachSeries([removeOldData, removeModelDef],function(fn, cb){ - fn(cb); - }, function(err){ - done(err); - }); - }); + let ModelDefinition = null; before('creating models dynamically', done => { - + ModelDefinition = loopback.findModel('ModelDefinition'); let AccountModel = { name:'Account', properties: { @@ -2207,7 +2181,7 @@ describe(chalk.blue('service personalization test started...'), function () { }; let CustomerModel = { - name: "Customer", + name: "XCustomer", base:"BaseEntity", properties: { firstName: "string", @@ -2237,7 +2211,7 @@ describe(chalk.blue('service personalization test started...'), function () { let Customer = null; before('creating a new customers', done => { - Customer = loopback.findModel('Customer'); + Customer = loopback.findModel('XCustomer'); let data = [ { id: 1, @@ -2293,7 +2267,7 @@ describe(chalk.blue('service personalization test started...'), function () { personalizationRule: { fieldMask: { code: { - 'pattern': '([A-Z]{4})\\-(\d{4})', + 'pattern': '([A-Z]{5})\\-(\\d{4})', 'maskCharacter': '*', 'format': '$1-$2', 'mask': ['$1'] @@ -2307,7 +2281,7 @@ describe(chalk.blue('service personalization test started...'), function () { }); it('t42 when fetching a customer record the kycInfo field should also be personalized', done => { - let custUrl = `/api/Customers/1`; + let custUrl = `/api/XCustomers/1`; api.get(custUrl) .set('Accept', 'application/json') .set('REMOTE_USER', 'testUser') From 1059c22e19fe5229271bbf1235236ab7fc92948b Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 18 May 2020 15:59:22 +0530 Subject: [PATCH 40/80] advanced fieldMask configuration --- lib/service-personalizer.js | 252 ++++++++++++++++++++------------ lib/utils.js | 37 ++++- package.json | 5 + test/test.js | 280 +++++++++++++++++++++++------------- 4 files changed, 387 insertions(+), 187 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 746f5bb..b3692d3 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -19,7 +19,7 @@ var mergeQuery = require('loopback-datasource-juggler/lib/utils').mergeQuery; var logger = require('oe-logger'); var log = logger('service-personalizer'); var customFunction; -const { nextTick, parseMethodString } = require('./utils'); +const { nextTick, parseMethodString, createError, formatDateTimeJoda, isDate, isString, isObject, isNumber } = require('./utils'); /** * This function returns the necessary sorting logic to @@ -83,6 +83,8 @@ function getCustomFunction() { // } // } + +//begin - task-utils - utility functions const utils = { /** @@ -287,9 +289,37 @@ const utils = { } return orderExp; + }, + + getMaskedString(stringRule, value) { + + let { pattern, flags, format, mask, maskCharacter } = stringRule; + let char = maskCharacter || 'X'; + let rgx = flags ? new RegExp(pattern, flags) : new RegExp(pattern); + let groups = value.match(rgx); + let masking = mask || []; + let newVal = format || []; //format can be an array or string + if(Array.isArray(newVal)) { + for(let i = 1, len = groups.length; i < len; i++) { + newVal.push(`$${i}`); + } + newVal = newVal.join(''); + } + masking.forEach(function(elem){ + newVal = newVal.replace(elem, new Array(groups[elem.substr(1)].length + 1).join(char)); + }); + for (let i = 0; i < groups.length; i++) { + newVal = newVal.replace('$' + i, groups[i]); + } + return newVal; + }, + + getMaskedDate(rule, value) { + let { format, locale } = rule; + return formatDateTimeJoda(value, format, locale); } }; - +//end - task-utils - utility functions const p13nFunctions = { /** @@ -551,30 +581,24 @@ const p13nFunctions = { key = property; } - if (record[key] && typeof record[key] === 'object') { - modifyField(record[key], innerProp, rule); - } else if (record[key] && typeof record[key] !== 'object') { - var char = rule.maskCharacter || 'X'; - var flags = rule.flags; - var regex = flags ? new RegExp(rule.pattern, flags) : new RegExp(rule.pattern); - var groups = record[key].match(regex) || []; - var masking = rule.mask || []; - var newVal = rule.format || []; - if (Array.isArray(newVal)) { - for (let i = 1; i < groups.length; i++) { - newVal.push('$' + i); - } - newVal = newVal.join(''); - } - masking.forEach(function (elem) { - newVal = newVal.replace(elem, new Array(groups[elem.substr(1)].length + 1).join(char)); - }); - for (let i = 0; i < groups.length; i++) { - newVal = newVal.replace('$' + i, groups[i]); - } - // normally we set __data but now, lb3!!! - record[key] = newVal; + let oldValue = record[key]; + + if(isString(oldValue) && 'stringMask' in rule) { + record[key] = utils.getMaskedString(rule.stringMask, oldValue); + } + else if(isDate(oldValue) && 'dateMask' in rule) { + record[`$${key}`] = utils.getMaskedDate(rule.dateMask, oldValue); + } + else if(isObject(oldValue)) { + modifyField(oldValue, innerProp, rule); + } + else if(isNumber(oldValue) && 'numberMask' in rule) { + record[key] = utils.getMaskedString(rule.numberMask, String(oldValue)); + } + else if(isString(oldValue)) { + record[key] = utils.getMaskedString(rule, oldValue); } + } function applyRuleOnRecord(record, charMaskRules) { @@ -724,8 +748,8 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { // let { context: { _personalizationCache : { records }} } = personalizationOptions; // return applyServicePersonalization(relModel, relData, records, ) // return applyServicePersonalization(relModel, relData, personalizationOptions, callback); - return fetchPersonalizationRecords(context, relModel, function(err, relModelP13nRecords){ - if(err) { + return fetchPersonalizationRecords(context, relModel, function (err, relModelP13nRecords) { + if (err) { return callback(err); } applyServicePersonalization(relModel, relData, relModelP13nRecords, personalizationOptions, callback); @@ -744,8 +768,8 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { } function fetchPersonalizationRecords(ctx, forModel, cb) { - let filter = { where: { modelName: forModel, disabled: false }}; - let { req: { callContext }} = ctx; + let filter = { where: { modelName: forModel, disabled: false } }; + let { req: { callContext } } = ctx; PersonalizationRule.find(filter, callContext, cb); } @@ -757,7 +781,7 @@ function checkPropsAndRecurse(Model, data, options, cb) { log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? '- with error' : ''}) applying personalization on properties of ${modelName}`); cb(err); }; - + let getModelCtorProps = lbModel => { let propHash = lbModel.definition.properties; let props = Object.entries(propHash); @@ -769,16 +793,16 @@ function checkPropsAndRecurse(Model, data, options, cb) { let [fieldName, propDef] = item; let { type } = propDef; let addFlag = false; - if(fieldName === 'id') { + if (fieldName === 'id') { return carrier; } - if(typeof type === 'function' && !ctorTest(type)) { + if (typeof type === 'function' && !ctorTest(type)) { addFlag = true; } - else if(typeof type === 'object' && Array.isArray(type)) { + else if (typeof type === 'object' && Array.isArray(type)) { addFlag = type.some(ctorFn => !ctorTest(ctorFn)); } - if(addFlag){ + if (addFlag) { carrier.hasModelCtorProps = true; carrier.modelProps.push(item); } @@ -788,7 +812,7 @@ function checkPropsAndRecurse(Model, data, options, cb) { }; let extractData = (key, data) => { - if(data.__data) { + if (data.__data) { return data.__data[key]; } else { @@ -797,18 +821,18 @@ function checkPropsAndRecurse(Model, data, options, cb) { }; let mInfo = getModelCtorProps(Model); - if(mInfo.hasModelCtorProps) { + if (mInfo.hasModelCtorProps) { return async.eachSeries(mInfo.modelProps, function propPersonalizerFn([key, propDef], cb) { let { type } = propDef; - + let relations = Model.settings.relations; let done = err => { - log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? '- with error' : ''}) personalizing field ${modelName}/${key}`); + log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? '- with error' : ''}) personalizing field ${modelName}/${key}`); cb(err); } log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (enter) personalizing field ${modelName}/${key}`); // begin - task01 - + // if the property is an embedsOne relation // property, skip the processing @@ -821,14 +845,14 @@ function checkPropsAndRecurse(Model, data, options, cb) { }); // end - task01 - - if(!isAnEmbedsOneRelationProp) { + + if (!isAnEmbedsOneRelationProp) { let unpersonalizedData = null; let modelCtorName = null; //begin - task02 - extract data - - if(typeof type === 'function') { + + if (typeof type === 'function') { //! this is a plain model constructor modelCtorName = type.name; unpersonalizedData = extractData(key, data); @@ -844,17 +868,17 @@ function checkPropsAndRecurse(Model, data, options, cb) { //end - task02 - extract data - return fetchPersonalizationRecords(ctx, modelCtorName, function(err, records){ - if(err) { + return fetchPersonalizationRecords(ctx, modelCtorName, function (err, records) { + if (err) { return done(err); } applyServicePersonalization(modelCtorName, unpersonalizedData, records, options, done); }); - + } //end if-block if(!isAnEmbedsOneRelationProp) - + nextTick(done); - }, function(err){ + }, function (err) { done(err); }); } @@ -867,16 +891,16 @@ function applyServicePersonalization(modelName, data, records, options, cb) { let isBeforeRemote = options.isBeforeRemote; log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (enter) applying personalization for model: ${modelName}`); let done = err => { - log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? ' - with error': ''}) applying personalization for model: ${modelName}`); + log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? ' - with error' : ''}) applying personalization for model: ${modelName}`); cb(err); } - doMethodGating(ctx, records, function(isAllowedMethod){ - if(isAllowedMethod) { - return doRoleGating(ctx, records, function(isAllowedRole){ - if(isAllowedRole) { - if(records.length === 0) { - checkRelationAndRecurse(Model, data, options, function(err){ - if(err) { + doMethodGating(ctx, records, function (isAllowedMethod) { + if (isAllowedMethod) { + return doRoleGating(ctx, records, function (isAllowedRole) { + if (isAllowedRole) { + if (records.length === 0) { + checkRelationAndRecurse(Model, data, options, function (err) { + if (err) { return done(err); } checkPropsAndRecurse(Model, data, options, done); @@ -887,12 +911,12 @@ function applyServicePersonalization(modelName, data, records, options, cb) { let personalizationRule = entry.personalizationRule; let ctx = options.context; let isBeforeRemote = options.isBeforeRemote; - personalize(ctx, isBeforeRemote, personalizationRule, data, function(err){ - if(err) { + personalize(ctx, isBeforeRemote, personalizationRule, data, function (err) { + if (err) { return done(err); } - checkRelationAndRecurse(Model, data, options, function(err) { - if(err) { + checkRelationAndRecurse(Model, data, options, function (err) { + if (err) { return done(err); } checkPropsAndRecurse(Model, data, options, done); @@ -906,7 +930,14 @@ function applyServicePersonalization(modelName, data, records, options, cb) { }); } done(); - }); + }); +} + +function isValidStringMask(data, fieldMaskDefinition) { + let lbModel = loopback.findModel(data.modelName); + if ('stringMask' in fieldMaskDefinition) { + + } } let PersonalizationRule = null; @@ -923,7 +954,7 @@ function init(app) { if (servicePersoConfig && servicePersoConfig.customFunctionPath) { loadCustomFunction(require(servicePersoConfig.customFunctionPath)); } - PersonalizationRule.observe('before save', function (ctx, next) { + PersonalizationRule.observe('before save', function PersonalizationRuleBeforeSave(ctx, next) { log.debug(ctx, 'PersonalizationRule: before save'); let data = ctx.__data || ctx.instance || ctx.data; @@ -935,7 +966,7 @@ function init(app) { next(error); }); } - let { personalizationRule: { postCustomFunction } } = data; + let { personalizationRule: { postCustomFunction, fieldMask } } = data; if (postCustomFunction) { let { functionName } = postCustomFunction; if (functionName) { @@ -951,6 +982,47 @@ function init(app) { }); } } + + if (fieldMask) { + let fMaskPropNames = Object.keys(fieldMask); // all the fields/properties for the fieldMask + + let fmError = msg => createError(`Invalid fieldMask: ${msg}`); + + let fMaskDef = fMaskPropNames.reduce((carrier, fieldName) => { + if (!carrier.hasInvalidFieldMaskDefinition) { + let maskDefAtRoot = fieldMask[fieldName]; + let { modelProps } = carrier; + let field = modelProps[fieldName]; + let fieldType = field.type; + let isConstructor = typeof field.type === 'function'; + let isStringMask = 'stringMask' in maskDefAtRoot; + let isNumberMask = 'numberMask' in maskDefAtRoot; + let isDateMask = 'dateMask' in maskDefAtRoot; + let isAMask = isStringMask || isDateMask || isStringMask; + if (isStringMask && isConstructor && fieldType !== String) { + carrier.hasInvalidFieldMaskDefinition = true; + carrier.error = fmError(`${fieldName} is of type ${fieldType.name}, expected ${String.name}`); + } + else if (isNumberMask && isConstructor && fieldType !== Number) { + carrier.hasInvalidFieldMaskDefinition = true; + carrier.error = fmError(`${fieldName} is of type ${fieldType.name}, expected ${Number.name}`); + } + else if (isDateMask && isConstructor && fieldType !== Date) { + carrier.hasInvalidFieldMaskDefinition = true; + carrier.error = fmError(`${fieldName} is of type ${fieldType.name}, expected ${Date.name}`); + } + else if (!isAMask && isConstructor && fieldType !== String) { + carrier.hasInvalidFieldMaskDefinition = true; + carrier.error = fmError(`${fieldName} is of type ${fieldType.name}, expected ${String.name}`); + } + } + return carrier; + }, { hasInvalidFieldMaskDefinition: false, modelProps: loopback.findModel(data.modelName).definition.properties }); + + if (fMaskDef.hasInvalidFieldMaskDefinition) { + return nextTick(() => next(fMaskDef.error)); + } + } nextTick(next); }); } @@ -971,8 +1043,8 @@ function doRoleGating(ctx, records, cb) { function getValidStaticMethod(info, isBeforeRemote) { let { modelName, methodName } = info; let Model = loopback.findModel(modelName); - if(Model[methodName]) { - return { isValid: true}; + if (Model[methodName]) { + return { isValid: true }; } return { isValid: false }; } @@ -980,10 +1052,10 @@ function getValidStaticMethod(info, isBeforeRemote) { function getValidInstanceMethod(info, isBeforeRemote) { let { modelName, methodName } = info; let Model = loopback.findModel(modelName); - if(Model.prototype[methodName]) { - return { isValid : true }; + if (Model.prototype[methodName]) { + return { isValid: true }; } - return { isValid : false }; + return { isValid: false }; } function getPersonalizationMeta(ctx, info, isBeforeRemote) { @@ -991,8 +1063,8 @@ function getPersonalizationMeta(ctx, info, isBeforeRemote) { let data = null; let applyPersonalizationFlag = true; let theModel = modelName; - if(isStatic) { - switch(methodName) { + if (isStatic) { + switch (methodName) { case 'create': case 'patchOrCreate': data = isBeforeRemote ? ctx.req.body : ctx.result; @@ -1005,12 +1077,12 @@ function getPersonalizationMeta(ctx, info, isBeforeRemote) { default: //!some static method we missed... let statInfo = getValidStaticMethod(info, isBeforeRemote); - if(!statInfo.isValid) { + if (!statInfo.isValid) { applyPersonalizationFlag = false; } else { let httpMethod = ctx.req.method; - if(httpMethod === 'POST' || httpMethod === 'PUT') { + if (httpMethod === 'POST' || httpMethod === 'PUT') { data = isBeforeRemote ? ctx.req.body : ctx.result; } else { @@ -1020,18 +1092,18 @@ function getPersonalizationMeta(ctx, info, isBeforeRemote) { } // end switch() } else { - switch(methodName) { + switch (methodName) { case 'patchAttributes': - data = isBeforeRemote ? ctx.req.body: ctx.result; + data = isBeforeRemote ? ctx.req.body : ctx.result; break; default: let instInfo = getValidInstanceMethod(info, isBeforeRemote); - if(!instInfo.isValid) { + if (!instInfo.isValid) { applyPersonalizationFlag = false; } else { let httpMethod = ctx.req.method; - if(httpMethod === 'POST' || httpMethod === 'PUT') { + if (httpMethod === 'POST' || httpMethod === 'PUT') { data = isBeforeRemote ? ctx.req.body : ctx.result; } else { @@ -1044,10 +1116,10 @@ function getPersonalizationMeta(ctx, info, isBeforeRemote) { return { applyPersonalizationFlag, model: theModel, data } } function runPersonalizationPipeline(ctx, isBeforeRemote, cb) { - let { _personalizationCache : { info, records }} = ctx; + let { _personalizationCache: { info, records } } = ctx; // let { methodName, model, isStatic } = info; let pMeta = getPersonalizationMeta(ctx, info, isBeforeRemote); - if(pMeta.applyPersonalizationFlag) { + if (pMeta.applyPersonalizationFlag) { let { data, model } = pMeta; let pOptions = { isBeforeRemote, @@ -1058,7 +1130,7 @@ function runPersonalizationPipeline(ctx, isBeforeRemote, cb) { next(cb); } // FIXME: We don't need this method. -function performPersonalizations(ctx, isBeforeRemote, cb) { +function performPersonalizations(ctx, isBeforeRemote, cb) { runPersonalizationPipeline(ctx, isBeforeRemote, cb); } @@ -1075,22 +1147,22 @@ function fetchPersonalizationRoles(ctx, cb) { * @param {string} model * @param {function} cb */ -function runPersonalizations(ctx, isBeforeRemote, cb) { - if(isBeforeRemote) { +function runPersonalizations(ctx, isBeforeRemote, cb) { + if (isBeforeRemote) { //! we have not fetched the personalization record(s) //fetch the personalization record and store it in ctx let info = parseMethodString(ctx.methodString); - return fetchPersonalizationRecords(ctx, info.model, function(err, records) { - if(err) { + return fetchPersonalizationRecords(ctx, info.model, function (err, records) { + if (err) { return cb(err); } ctx._personalizationCache = { records, info }; - fetchPersonalizationRoles(ctx, function(err){ - if(err) { + fetchPersonalizationRoles(ctx, function (err) { + if (err) { return cb(err) } performPersonalizations(ctx, isBeforeRemote, cb); @@ -1099,19 +1171,19 @@ function runPersonalizations(ctx, isBeforeRemote, cb) { } else { performPersonalizations(ctx, isBeforeRemote, cb); - } + } }; -function performServicePersonalizations(modelName, data, options, cb) { +function performServicePersonalizations(modelName, data, options, cb) { let ctx = options.context; log.debug(ctx, `performServicePersonalizations: (enter) model -> ${modelName}`); let done = err => { - log.debug(ctx, `performServicePersonalizations: (leave${err ? '- with error': ''}) model -> ${modelName}`); + log.debug(ctx, `performServicePersonalizations: (leave${err ? '- with error' : ''}) model -> ${modelName}`); cb(err); }; ctx._personalizationCache = {}; - fetchPersonalizationRecords(ctx, modelName, function(err, records){ - if(err){ + fetchPersonalizationRecords(ctx, modelName, function (err, records) { + if (err) { return done(err); } applyServicePersonalization(modelName, data, records, options, done); diff --git a/lib/utils.js b/lib/utils.js index 43dac25..6ca276f 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,4 +1,8 @@ const _slice = [].slice; +let { DateTimeFormatter, LocalDateTime, nativeJs } = require('@js-joda/core'); +let { Locale } = require('@js-joda/locale'); +let { prototype: {toString} } = Object; + module.exports = { /** * queue the function to the runtime's next event loop @@ -31,5 +35,36 @@ module.exports = { }, {}); }, - slice: arg => _slice.call(arg) + slice: arg => _slice.call(arg), + + createError: msg => new Error(msg), + + /** + * joda time formatter helper + * @param {Date} date + * @param {string} format - joda format string + * @param {string} locale - as per js-joda/locale api + */ + formatDateTimeJoda(date, format, locale = "US") { + let ldt = LocalDateTime.from(nativeJs(date)); + let pattern = DateTimeFormatter.ofPattern(format).withLocale(Locale[locale]) + return ldt.format(pattern); + }, + + + isDate(object) { + return toString.call(object) === '[object Date]'; + }, + + isString(object) { + return toString.call(object) === '[object String]'; + }, + + isObject(object) { + return toString.call(object) === '[object Object]'; + }, + + isNumber(object) { + return toString.call(object) === '[object Number]'; + } }; diff --git a/package.json b/package.json index 76fc76f..ad37d8f 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,12 @@ "grunt-cover": "grunt test-with-coverage" }, "dependencies": { + "@js-joda/core": "^2.0.0", + "@js-joda/locale": "^3.1.1", "assertion-error": "1.1.0", "async": "2.6.1", + "cldr-data": "^36.0.0", + "cldrjs": "^0.5.1", "lodash": "4.17.14", "oe-cloud": "git+http://evgit/oecloud.io/oe-cloud.git#master", "oe-expression": "git+http://evgit/oecloud.io/oe-expression.git#master", @@ -32,6 +36,7 @@ "grunt-contrib-clean": "2.0.0", "grunt-mocha-istanbul": "5.0.2", "istanbul": "0.4.5", + "loopback-component-explorer": "^6.5.1", "md5": "^2.2.1", "mocha": "5.2.0", "oe-connector-mongodb": "git+http://evgit/oecloud.io/oe-connector-mongodb.git#master", diff --git a/test/test.js b/test/test.js index ec2e5eb..780f49e 100755 --- a/test/test.js +++ b/test/test.js @@ -1791,7 +1791,7 @@ describe(chalk.blue('service personalization test started...'), function () { "id": 12 }; ProductOwner = loopback.findModel('ProductOwner'); - ProductOwner.create(data, function(err){ + ProductOwner.create(data, function (err) { done(err); }); }); @@ -1799,28 +1799,28 @@ describe(chalk.blue('service personalization test started...'), function () { before('creating a catalog', done => { let data = [ { - "name" : "Patanjali Paste", + "name": "Patanjali Paste", "category": "FMCG", "desc": "Herbal paste that is all vegan", - "price": {"currency":"INR", "amount": 45 }, + "price": { "currency": "INR", "amount": 45 }, "isAvailable": false, - "keywords": [ "toothpaste", "herbal" ], - "productOwnerId" : 12, + "keywords": ["toothpaste", "herbal"], + "productOwnerId": 12, "id": "prod1" }, { - "name" : "Patanjali Facial", + "name": "Patanjali Facial", "category": "Cosmetics", "desc": "Ayurvedic cream to get rid of dark spots, pimples, etc", - "price": {"currency":"INR", "amount": 70 }, + "price": { "currency": "INR", "amount": 70 }, "isAvailable": true, - "keywords": [ "face", "herbal", "cream" ], - "productOwnerId" : 12, - "id" : "prod2" + "keywords": ["face", "herbal", "cream"], + "productOwnerId": 12, + "id": "prod2" } ]; - ProductCatalog.create(data, function(err){ + ProductCatalog.create(data, function (err) { done(err); }); }); @@ -1837,18 +1837,18 @@ describe(chalk.blue('service personalization test started...'), function () { } ]; let Store = loopback.findModel('Store'); - Store.create(data, function(err) { + Store.create(data, function (err) { done(err); }); }); before('creating store stock', done => { let data = [ - {"storeId": "store1", "productCatalogId": "prod1" }, - {"storeId": "store2", "productCatalogId": "prod2" } + { "storeId": "store1", "productCatalogId": "prod1" }, + { "storeId": "store2", "productCatalogId": "prod2" } ]; let StoreStock = loopback.findModel('StoreStock'); - StoreStock.create(data, function(err) { + StoreStock.create(data, function (err) { done(err); }); }); @@ -1858,18 +1858,18 @@ describe(chalk.blue('service personalization test started...'), function () { { "line1": "5th ave", "line2": "Richmond", - "landmark":"Siegel Building", + "landmark": "Siegel Building", "pincode": "434532", "id": "addr1", - "storeId":"store1" + "storeId": "store1" }, { "line1": "7th ave", "line2": "Wellington Broadway", - "landmark":"Carl Sagan's Office", + "landmark": "Carl Sagan's Office", "pincode": "434543", "id": "addr2", - "storeId":"store1" + "storeId": "store1" }, { "line1": "Patanjali Lane", @@ -1882,14 +1882,14 @@ describe(chalk.blue('service personalization test started...'), function () { { "line1": "Orchard St", "line2": "Blumingdale's", - "landmark":"Post Office", + "landmark": "Post Office", "pincode": "673627", "id": "addr4", - "storeId":"store2" + "storeId": "store2" } ]; let AddressBook = loopback.findModel('AddressBook'); - AddressBook.create(data, function(err){ + AddressBook.create(data, function (err) { done(err); }); }); @@ -1929,12 +1929,12 @@ describe(chalk.blue('service personalization test started...'), function () { ]; let PhoneNumber = loopback.findModel('PhoneNumber'); - PhoneNumber.create(data, function(err){ + PhoneNumber.create(data, function (err) { done(err); }); }); - + it('t39 should apply child model personalization when included from parent with no personalization', done => { let data = { @@ -1963,7 +1963,7 @@ describe(chalk.blue('service personalization test started...'), function () { .set('REMOTE_USER', 'testUser') .set('region', 'kl') .expect(200).end((err, resp) => { - if(err) { + if (err) { done(err); } else { @@ -1977,11 +1977,11 @@ describe(chalk.blue('service personalization test started...'), function () { } }); }); - + it('t40(a) should demonstrate personalization is being applied recursively', done => { let data = [ { - "modelName" : "AddressBook", + "modelName": "AddressBook", "personalizationRule": { "mask": { "landmark": true @@ -1992,7 +1992,7 @@ describe(chalk.blue('service personalization test started...'), function () { "modelName": "PhoneNumber", "personalizationRule": { "fieldMask": { - "number" : { + "number": { "pattern": "([0-9]{3})([0-9]{3})([0-9]{4})", "maskCharacter": "X", "format": "($1) $2-$3", @@ -2003,24 +2003,24 @@ describe(chalk.blue('service personalization test started...'), function () { } ]; - PersonalizationRule.create(data, {}, function(err){ - if(err) { + PersonalizationRule.create(data, {}, function (err) { + if (err) { return done(err) } let filter = { - "include": [ + "include": [ { - "ProductCatalog" : { + "ProductCatalog": { "store": { - "store" : { - "addresses" : "phones" - } - } - } - }, - "address" + "store": { + "addresses": "phones" + } + } + } + }, + "address" ], - "where": { "id": 12 } + "where": { "id": 12 } }; let filterString = encodeURIComponent(JSON.stringify(filter)); let url = `${productOwnerUrl}/findOne?access_token=${accessToken}&&filter=${filterString}`; @@ -2029,7 +2029,7 @@ describe(chalk.blue('service personalization test started...'), function () { .set('REMOTE_USER', 'testUser') .expect(200) .end((err, resp) => { - if(err){ + if (err) { done(err) } else { @@ -2040,35 +2040,35 @@ describe(chalk.blue('service personalization test started...'), function () { // console.log(JSON.stringify(result,null, 2)); _.flatten( _.flatten(result.ProductCatalog.map(item => item.store.store.addresses)) - .map(x => x.phones) + .map(x => x.phones) ).forEach(ph => { let substr = ph.number.substr(0, 10); expect(substr).to.equal('(XXX) XXX-'); }); - - + + done(); } }); }); - }); + }); - it('t40(b) should apply service personalization to a related model invoked via remote call', function(done) { + it('t40(b) should apply service personalization to a related model invoked via remote call', function (done) { let url = `${productOwnerUrl}/1/ProductCatalog?access_token=${accessToken}`; api.get(url) - .set('Accept', 'application/json') - .set('REMOTE_USER', 'testUser') - .set('region', 'kl') - .expect(200) - .end((err, resp) => { - if(err){ - return done(err); - } - let result = resp.body; - // console.log(resp.body); - expect(result[0].modelNo).to.equal('123456XXXX'); - done(); - }); + .set('Accept', 'application/json') + .set('REMOTE_USER', 'testUser') + .set('region', 'kl') + .expect(200) + .end((err, resp) => { + if (err) { + return done(err); + } + let result = resp.body; + // console.log(resp.body); + expect(result[0].modelNo).to.equal('123456XXXX'); + done(); + }); }); }); @@ -2076,7 +2076,7 @@ describe(chalk.blue('service personalization test started...'), function () { beforeEach('re-inserting the personalization rules', done => { let data = [ { - "modelName" : "AddressBook", + "modelName": "AddressBook", "personalizationRule": { "mask": { "landmark": true @@ -2087,7 +2087,7 @@ describe(chalk.blue('service personalization test started...'), function () { "modelName": "PhoneNumber", "personalizationRule": { "fieldMask": { - "number" : { + "number": { "pattern": "([0-9]{3})([0-9]{3})([0-9]{4})", "maskCharacter": "X", "format": "($1) $2-$3", @@ -2098,13 +2098,13 @@ describe(chalk.blue('service personalization test started...'), function () { } ] - PersonalizationRule.create(data, {}, function(err){ + PersonalizationRule.create(data, {}, function (err) { done(err); }); }); it('t41(a) should demonstrate data getting personalized via a custom remote method of model that has mixin enabled', done => { - + let ownerId = 12; let url = `${productOwnerUrl}/${ownerId}/demandchain?access_token=${accessToken}`; api.get(url) @@ -2112,7 +2112,7 @@ describe(chalk.blue('service personalization test started...'), function () { .set('REMOTE_USER', 'testUser') .expect(200) .end((err, resp) => { - if(err) { + if (err) { done(err); } else { @@ -2132,7 +2132,7 @@ describe(chalk.blue('service personalization test started...'), function () { .set('REMOTE_USER', 'testUser') .expect(200) .end((err, resp) => { - if(err) { + if (err) { done(err); } else { @@ -2150,12 +2150,12 @@ describe(chalk.blue('service personalization test started...'), function () { * */ describe('property level personalizations', () => { - let ModelDefinition = null; + let ModelDefinition = null; before('creating models dynamically', done => { ModelDefinition = loopback.findModel('ModelDefinition'); let AccountModel = { - name:'Account', + name: 'Account', properties: { "accountType": "string", "openedOn": "date" @@ -2164,34 +2164,35 @@ describe(chalk.blue('service personalization test started...'), function () { "customer": { type: "embedsOne", model: "Customer", - property:"linkedCustomer" + property: "linkedCustomer" } } }; - + let KycModel = { - name:'Kyc', + name: 'Kyc', plural: 'Kyc', properties: { "criteria": "string", "code": "string", - "remark":"string", + "remark": "string", "score": "number" } }; let CustomerModel = { name: "XCustomer", - base:"BaseEntity", + base: "BaseEntity", properties: { firstName: "string", lastName: "string", salutation: "string", dob: "date", - kycInfo: [ 'Kyc' ] + kycInfo: ['Kyc'], + custRef: "string" }, relations: { - all_accounts : { + all_accounts: { type: 'hasMany', model: 'Account' } @@ -2199,12 +2200,12 @@ describe(chalk.blue('service personalization test started...'), function () { }; - async.eachSeries([KycModel, CustomerModel, AccountModel], function(spec, cb){ + async.eachSeries([KycModel, CustomerModel, AccountModel], function (spec, cb) { spec.mixins = { ServicePersonalizationMixin: true }; //enabling service personalization mixin - ModelDefinition.create(spec, {}, function(err){ + ModelDefinition.create(spec, {}, function (err) { cb(err); }); - }, function(err){ + }, function (err) { done(err); }); }); @@ -2216,7 +2217,7 @@ describe(chalk.blue('service personalization test started...'), function () { { id: 1, firstName: 'Cust', - lastName:'One', + lastName: 'One', salutation: 'Mr', kycInfo: [ { @@ -2232,12 +2233,13 @@ describe(chalk.blue('service personalization test started...'), function () { 'score': 76.24 } ], - dob: new Date(1987, 3, 12) + dob: new Date(1987, 3, 12), + custRef: "HDFC-VCHRY-12354" }, { id: 2, firstName: 'Cust', - lastName:'Two', + lastName: 'Two', salutation: 'Mrs', kycInfo: [ { @@ -2253,10 +2255,11 @@ describe(chalk.blue('service personalization test started...'), function () { 'score': 76.24 } ], - dob: new Date(1989, 3, 12) + dob: new Date(1989, 3, 12), + custRef: "ICICI-BLR-0056" } ]; - Customer.create(data, {}, function(err){ + Customer.create(data, {}, function (err) { done(err); }); }); @@ -2275,7 +2278,7 @@ describe(chalk.blue('service personalization test started...'), function () { } } }; - PersonalizationRule.create(data, {}, function(err){ + PersonalizationRule.create(data, {}, function (err) { done(err); }); }); @@ -2283,22 +2286,107 @@ describe(chalk.blue('service personalization test started...'), function () { it('t42 when fetching a customer record the kycInfo field should also be personalized', done => { let custUrl = `/api/XCustomers/1`; api.get(custUrl) - .set('Accept', 'application/json') - .set('REMOTE_USER', 'testUser') - .expect(200) - .end((err, resp) =>{ - done(err); - let result = resp.body; - expect(result).to.be.object; - expect(result).to.have.property('firstName'); - expect(result.kycInfo).to.be.array; - result.kycInfo.forEach(kycItem => { - let lastFour = kycItem.code.substr(-4); - let expectedString = `*****-${lastFour}`; - expect(kycItem.code).to.equal(expectedString); + .set('Accept', 'application/json') + .set('REMOTE_USER', 'testUser') + .expect(200) + .end((err, resp) => { + done(err); + let result = resp.body; + expect(result).to.be.object; + expect(result).to.have.property('firstName'); + expect(result.kycInfo).to.be.array; + result.kycInfo.forEach(kycItem => { + let lastFour = kycItem.code.substr(-4); + let expectedString = `*****-${lastFour}`; + expect(kycItem.code).to.equal(expectedString); + }); }); + }); + }); + + /** + * These set of test cases describe + * advanced configuration for fieldMask + * operation + */ + + describe('Advanced fieldMask configurations', () => { + it('t43 applying string mask on a dob field (date) should throw an error', done => { + let record = { + modelName: 'XCustomer', + personalizationRule: { + fieldMask: { + dob: { + 'pattern': '([0-9]{3})([0-9]{3})([0-9]{4})', + 'maskCharacter': 'X', + 'format': '($1) $2-$3', + 'mask': ['$3'] + } + } + } + }; + + PersonalizationRule.create(record, {}, function (err) { + if (err) { + return done(); + } + expect(false, 'Should not happen').to.be.ok; + }); + }); + + before('setup rules', done => { + let data = { + modelName: 'XCustomer', + personalizationRule: { + fieldMask: { + custRef: { + stringMask: { + 'pattern': '(\\w+)\\-(\\w+)\\-(\\d+)', + 'maskCharacter': 'X', + 'format': '$1-$2-$3', + 'mask': ['$3'] + } + }, + dob: { + dateMask: { + format: 'MMM/yyyy' + } + } + } + } + }; + + PersonalizationRule.create(data, {}, function (err) { + done(err); }); }); + let apiResponse; + before('fetch api response', done => { + let url = '/api/XCustomers/2'; + api.get(url) + .set('Accept', 'application/json') + .expect(200) + .end((err, resp) => { + if (err) { + return done(err); + } + let result = resp.body; + // expect(result).to.be.object; + // expect(result.custRef).to.equal('ICICI-BLR-XXXX'); + apiResponse = result; + done(); + }); + }); + + it('t44 should apply a fieldMask on the custRef field which is of type string', () => { + expect(apiResponse).to.be.object; + expect(apiResponse.custRef).to.equal('ICICI-BLR-XXXX'); + }); + + it('t45 should apply a fieldMask to the dob field and display only month and year', () =>{ + expect(apiResponse.dob).to.equal("Apr/1989"); + }); + }); }); From d3443624b23ebe0583d44d919d3e20fa3f7465b5 Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 18 May 2020 16:30:32 +0530 Subject: [PATCH 41/80] advanced fieldMask config - numberMask --- lib/service-personalizer.js | 4 ++-- test/test.js | 20 +++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index b3692d3..f77be12 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -593,7 +593,7 @@ const p13nFunctions = { modifyField(oldValue, innerProp, rule); } else if(isNumber(oldValue) && 'numberMask' in rule) { - record[key] = utils.getMaskedString(rule.numberMask, String(oldValue)); + record[`$${key}`] = utils.getMaskedString(rule.numberMask, String(oldValue)); } else if(isString(oldValue)) { record[key] = utils.getMaskedString(rule, oldValue); @@ -998,7 +998,7 @@ function init(app) { let isStringMask = 'stringMask' in maskDefAtRoot; let isNumberMask = 'numberMask' in maskDefAtRoot; let isDateMask = 'dateMask' in maskDefAtRoot; - let isAMask = isStringMask || isDateMask || isStringMask; + let isAMask = isStringMask || isDateMask || isNumberMask; if (isStringMask && isConstructor && fieldType !== String) { carrier.hasInvalidFieldMaskDefinition = true; carrier.error = fmError(`${fieldName} is of type ${fieldType.name}, expected ${String.name}`); diff --git a/test/test.js b/test/test.js index 780f49e..a346fe6 100755 --- a/test/test.js +++ b/test/test.js @@ -2189,7 +2189,8 @@ describe(chalk.blue('service personalization test started...'), function () { salutation: "string", dob: "date", kycInfo: ['Kyc'], - custRef: "string" + custRef: "string", + aadhar: "number" }, relations: { all_accounts: { @@ -2234,7 +2235,8 @@ describe(chalk.blue('service personalization test started...'), function () { } ], dob: new Date(1987, 3, 12), - custRef: "HDFC-VCHRY-12354" + custRef: "HDFC-VCHRY-12354", + aadhar: 12345678 }, { id: 2, @@ -2256,7 +2258,8 @@ describe(chalk.blue('service personalization test started...'), function () { } ], dob: new Date(1989, 3, 12), - custRef: "ICICI-BLR-0056" + custRef: "ICICI-BLR-0056", + aadhar: 45248632 } ]; Customer.create(data, {}, function (err) { @@ -2351,6 +2354,14 @@ describe(chalk.blue('service personalization test started...'), function () { dateMask: { format: 'MMM/yyyy' } + }, + aadhar: { + numberMask: { + pattern: '(\\d{2})(\\d{2})(\\d{2})(\\d{2})', + format: '$1 $2 $3 $4', + mask: ['$3', '$4'], + maskCharacter: '*' + } } } } @@ -2387,6 +2398,9 @@ describe(chalk.blue('service personalization test started...'), function () { expect(apiResponse.dob).to.equal("Apr/1989"); }); + it('t46 should apply fieldMask on the aadhar field (numberMask)', () => { + expect(apiResponse.aadhar).to.equal('45 24 ** **'); + }); }); }); From b7573f7e5a20f1e33bbbd60de9805b1b9183c3e2 Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 18 May 2020 16:43:58 +0530 Subject: [PATCH 42/80] lint fixes --- .../mixins/service-personalization-mixin.js | 5 +- lib/service-personalizer.js | 125 ++++++++---------- lib/utils.js | 9 +- 3 files changed, 59 insertions(+), 80 deletions(-) diff --git a/common/mixins/service-personalization-mixin.js b/common/mixins/service-personalization-mixin.js index c255e54..c779030 100644 --- a/common/mixins/service-personalization-mixin.js +++ b/common/mixins/service-personalization-mixin.js @@ -33,11 +33,10 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { let next = args[args.length - 1]; // let callCtx = ctx.req.callContext; log.debug(ctx, `afterRemote: (enter) MethodString: ${ctx.methodString}`); - runPersonalizations(ctx, false, function(err){ + runPersonalizations(ctx, false, function (err) { log.debug(ctx, `afterRemote: (leave${err ? '- with error' : ''}) MethodString: ${ctx.methodString}`); next(err); }); - }); TargetModel.beforeRemote('**', function ServicePersonalizationBeforeRemoteHook() { @@ -48,7 +47,7 @@ module.exports = function ServicePersonalizationMixin(TargetModel) { log.debug(ctx, `beforeRemote: (enter) MethodString: ${ctx.methodString}`); // let ctxInfo = parseMethodString(ctx.methodString); - runPersonalizations(ctx, true, function(err){ + runPersonalizations(ctx, true, function (err) { log.debug(ctx, `beforeRemote: (leave${err ? '- with error' : ''}) MethodString: ${ctx.methodString}`); next(err); }); diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index f77be12..645f6dd 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -84,7 +84,7 @@ function getCustomFunction() { // } -//begin - task-utils - utility functions +// begin - task-utils - utility functions const utils = { /** @@ -292,20 +292,20 @@ const utils = { }, getMaskedString(stringRule, value) { - let { pattern, flags, format, mask, maskCharacter } = stringRule; let char = maskCharacter || 'X'; let rgx = flags ? new RegExp(pattern, flags) : new RegExp(pattern); let groups = value.match(rgx); let masking = mask || []; - let newVal = format || []; //format can be an array or string - if(Array.isArray(newVal)) { - for(let i = 1, len = groups.length; i < len; i++) { + // eslint-disable-next-line no-inline-comments + let newVal = format || []; // format can be an array or string + if (Array.isArray(newVal)) { + for (let i = 1, len = groups.length; i < len; i++) { newVal.push(`$${i}`); } newVal = newVal.join(''); } - masking.forEach(function(elem){ + masking.forEach(function (elem) { newVal = newVal.replace(elem, new Array(groups[elem.substr(1)].length + 1).join(char)); }); for (let i = 0; i < groups.length; i++) { @@ -319,7 +319,7 @@ const utils = { return formatDateTimeJoda(value, format, locale); } }; -//end - task-utils - utility functions +// end - task-utils - utility functions const p13nFunctions = { /** @@ -582,23 +582,18 @@ const p13nFunctions = { } let oldValue = record[key]; - - if(isString(oldValue) && 'stringMask' in rule) { + + if (isString(oldValue) && 'stringMask' in rule) { record[key] = utils.getMaskedString(rule.stringMask, oldValue); - } - else if(isDate(oldValue) && 'dateMask' in rule) { + } else if (isDate(oldValue) && 'dateMask' in rule) { record[`$${key}`] = utils.getMaskedDate(rule.dateMask, oldValue); - } - else if(isObject(oldValue)) { + } else if (isObject(oldValue)) { modifyField(oldValue, innerProp, rule); - } - else if(isNumber(oldValue) && 'numberMask' in rule) { + } else if (isNumber(oldValue) && 'numberMask' in rule) { record[`$${key}`] = utils.getMaskedString(rule.numberMask, String(oldValue)); - } - else if(isString(oldValue)) { + } else if (isString(oldValue)) { record[key] = utils.getMaskedString(rule, oldValue); } - } function applyRuleOnRecord(record, charMaskRules) { @@ -747,7 +742,7 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { log.debug(context, `${prefix}: (enter) processing relation: ${name}/${relationName}`); // let { context: { _personalizationCache : { records }} } = personalizationOptions; // return applyServicePersonalization(relModel, relData, records, ) - // return applyServicePersonalization(relModel, relData, personalizationOptions, callback); + // return applyServicePersonalization(relModel, relData, personalizationOptions, callback); return fetchPersonalizationRecords(context, relModel, function (err, relModelP13nRecords) { if (err) { return callback(err); @@ -798,8 +793,7 @@ function checkPropsAndRecurse(Model, data, options, cb) { } if (typeof type === 'function' && !ctorTest(type)) { addFlag = true; - } - else if (typeof type === 'object' && Array.isArray(type)) { + } else if (typeof type === 'object' && Array.isArray(type)) { addFlag = type.some(ctorFn => !ctorTest(ctorFn)); } if (addFlag) { @@ -815,9 +809,8 @@ function checkPropsAndRecurse(Model, data, options, cb) { if (data.__data) { return data.__data[key]; } - else { - return data[key] - } + + return data[key]; }; let mInfo = getModelCtorProps(Model); @@ -829,7 +822,7 @@ function checkPropsAndRecurse(Model, data, options, cb) { let done = err => { log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? '- with error' : ''}) personalizing field ${modelName}/${key}`); cb(err); - } + }; log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (enter) personalizing field ${modelName}/${key}`); // begin - task01 @@ -850,23 +843,23 @@ function checkPropsAndRecurse(Model, data, options, cb) { let unpersonalizedData = null; let modelCtorName = null; - //begin - task02 - extract data + // begin - task02 - extract data if (typeof type === 'function') { //! this is a plain model constructor modelCtorName = type.name; unpersonalizedData = extractData(key, data); - } - else { + } else { //! this is an array of model constructors //! Only one model constructor available(?) let modelCtor = type[0]; modelCtorName = modelCtor.name; unpersonalizedData = extractData(key, data); + // eslint-disable-next-line no-inline-comments }// end if-else block - if(typeof type === 'function') - //end - task02 - extract data + // end - task02 - extract data return fetchPersonalizationRecords(ctx, modelCtorName, function (err, records) { if (err) { @@ -874,8 +867,8 @@ function checkPropsAndRecurse(Model, data, options, cb) { } applyServicePersonalization(modelCtorName, unpersonalizedData, records, options, done); }); - - } //end if-block if(!isAnEmbedsOneRelationProp) + // eslint-disable-next-line no-inline-comments + } // end if-block if(!isAnEmbedsOneRelationProp) nextTick(done); }, function (err) { @@ -893,7 +886,7 @@ function applyServicePersonalization(modelName, data, records, options, cb) { let done = err => { log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? ' - with error' : ''}) applying personalization for model: ${modelName}`); cb(err); - } + }; doMethodGating(ctx, records, function (isAllowedMethod) { if (isAllowedMethod) { return doRoleGating(ctx, records, function (isAllowedRole) { @@ -905,8 +898,7 @@ function applyServicePersonalization(modelName, data, records, options, cb) { } checkPropsAndRecurse(Model, data, options, done); }); - } - else { + } else { let entry = records[0]; let personalizationRule = entry.personalizationRule; let ctx = options.context; @@ -923,8 +915,7 @@ function applyServicePersonalization(modelName, data, records, options, cb) { }); }); } - } - else { + } else { done(); } }); @@ -933,13 +924,6 @@ function applyServicePersonalization(modelName, data, records, options, cb) { }); } -function isValidStringMask(data, fieldMaskDefinition) { - let lbModel = loopback.findModel(data.modelName); - if ('stringMask' in fieldMaskDefinition) { - - } -} - let PersonalizationRule = null; /** * Initializes this module for service personalization @@ -984,6 +968,7 @@ function init(app) { } if (fieldMask) { + // eslint-disable-next-line no-inline-comments let fMaskPropNames = Object.keys(fieldMask); // all the fields/properties for the fieldMask let fmError = msg => createError(`Invalid fieldMask: ${msg}`); @@ -1002,16 +987,13 @@ function init(app) { if (isStringMask && isConstructor && fieldType !== String) { carrier.hasInvalidFieldMaskDefinition = true; carrier.error = fmError(`${fieldName} is of type ${fieldType.name}, expected ${String.name}`); - } - else if (isNumberMask && isConstructor && fieldType !== Number) { + } else if (isNumberMask && isConstructor && fieldType !== Number) { carrier.hasInvalidFieldMaskDefinition = true; carrier.error = fmError(`${fieldName} is of type ${fieldType.name}, expected ${Number.name}`); - } - else if (isDateMask && isConstructor && fieldType !== Date) { + } else if (isDateMask && isConstructor && fieldType !== Date) { carrier.hasInvalidFieldMaskDefinition = true; carrier.error = fmError(`${fieldName} is of type ${fieldType.name}, expected ${Date.name}`); - } - else if (!isAMask && isConstructor && fieldType !== String) { + } else if (!isAMask && isConstructor && fieldType !== String) { carrier.hasInvalidFieldMaskDefinition = true; carrier.error = fmError(`${fieldName} is of type ${fieldType.name}, expected ${String.name}`); } @@ -1034,7 +1016,7 @@ function doMethodGating(ctx, records, cb) { } function doRoleGating(ctx, records, cb) { - //Assume all allowed for now + // Assume all allowed for now //! we need to extract info from context cache nextTick(() => cb(true)); @@ -1075,23 +1057,21 @@ function getPersonalizationMeta(ctx, info, isBeforeRemote) { data = isBeforeRemote ? {} : ctx.result; break; default: - //!some static method we missed... + //! some static method we missed... let statInfo = getValidStaticMethod(info, isBeforeRemote); if (!statInfo.isValid) { applyPersonalizationFlag = false; - } - else { + } else { let httpMethod = ctx.req.method; if (httpMethod === 'POST' || httpMethod === 'PUT') { data = isBeforeRemote ? ctx.req.body : ctx.result; - } - else { + } else { data = isBeforeRemote ? {} : ctx.result; } } + // eslint-disable-next-line no-inline-comments } // end switch() - } - else { + } else { switch (methodName) { case 'patchAttributes': data = isBeforeRemote ? ctx.req.body : ctx.result; @@ -1100,20 +1080,18 @@ function getPersonalizationMeta(ctx, info, isBeforeRemote) { let instInfo = getValidInstanceMethod(info, isBeforeRemote); if (!instInfo.isValid) { applyPersonalizationFlag = false; - } - else { + } else { let httpMethod = ctx.req.method; if (httpMethod === 'POST' || httpMethod === 'PUT') { data = isBeforeRemote ? ctx.req.body : ctx.result; - } - else { + } else { data = isBeforeRemote ? {} : ctx.result; } } } } - return { applyPersonalizationFlag, model: theModel, data } + return { applyPersonalizationFlag, model: theModel, data }; } function runPersonalizationPipeline(ctx, isBeforeRemote, cb) { let { _personalizationCache: { info, records } } = ctx; @@ -1124,10 +1102,10 @@ function runPersonalizationPipeline(ctx, isBeforeRemote, cb) { let pOptions = { isBeforeRemote, context: ctx - } + }; return applyServicePersonalization(model, data, records, pOptions, cb); } - next(cb); + nextTick(cb); } // FIXME: We don't need this method. function performPersonalizations(ctx, isBeforeRemote, cb) { @@ -1143,15 +1121,17 @@ function fetchPersonalizationRoles(ctx, cb) { * fetches the personalization rules * and checks against methodName if * personalizations are applicable - * - * @param {string} model - * @param {function} cb + * + * @param {HttpContext} ctx - the http context object + * @param {boolean} isBeforeRemote - flag denoting phase of the request + * @param {function} cb - callback function to signal completion + * @returns {undefined} - nothing */ function runPersonalizations(ctx, isBeforeRemote, cb) { if (isBeforeRemote) { //! we have not fetched the personalization record(s) - //fetch the personalization record and store it in ctx + // fetch the personalization record and store it in ctx let info = parseMethodString(ctx.methodString); return fetchPersonalizationRecords(ctx, info.model, function (err, records) { if (err) { @@ -1163,16 +1143,15 @@ function runPersonalizations(ctx, isBeforeRemote, cb) { }; fetchPersonalizationRoles(ctx, function (err) { if (err) { - return cb(err) + return cb(err); } performPersonalizations(ctx, isBeforeRemote, cb); }); }); } - else { - performPersonalizations(ctx, isBeforeRemote, cb); - } -}; + + performPersonalizations(ctx, isBeforeRemote, cb); +} function performServicePersonalizations(modelName, data, options, cb) { let ctx = options.context; diff --git a/lib/utils.js b/lib/utils.js index 6ca276f..2761327 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -41,13 +41,14 @@ module.exports = { /** * joda time formatter helper - * @param {Date} date + * @param {Date} date - the javascript date object to format * @param {string} format - joda format string * @param {string} locale - as per js-joda/locale api + * @return {string} - the formatted date */ - formatDateTimeJoda(date, format, locale = "US") { + formatDateTimeJoda(date, format, locale = 'US') { let ldt = LocalDateTime.from(nativeJs(date)); - let pattern = DateTimeFormatter.ofPattern(format).withLocale(Locale[locale]) + let pattern = DateTimeFormatter.ofPattern(format).withLocale(Locale[locale]); return ldt.format(pattern); }, @@ -63,7 +64,7 @@ module.exports = { isObject(object) { return toString.call(object) === '[object Object]'; }, - + isNumber(object) { return toString.call(object) === '[object Number]'; } From d9d38648c8cc37be37ff7cab5725346d62b30091 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 19 May 2020 11:46:38 +0530 Subject: [PATCH 43/80] methodName impl - 1 --- common/models/personalization-rule.json | 5 +++ lib/service-personalizer.js | 26 ++++++++++++-- lib/utils.js | 46 +++++++++++++++++++++++-- 3 files changed, 72 insertions(+), 5 deletions(-) diff --git a/common/models/personalization-rule.json b/common/models/personalization-rule.json index 10ed3d0..e9c5fe5 100644 --- a/common/models/personalization-rule.json +++ b/common/models/personalization-rule.json @@ -28,6 +28,11 @@ "personalizationRule": { "type": "object", "required": true + }, + "methodName" : { + "type": "string", + "default": "*", + "description": "The model methodName this rule should apply to. Should be the methodName (static/instance) or wildcards you specify in a afterRemote()/beforeRemote(). Default '*'" } }, "validations": [], diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 645f6dd..e5a57d2 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -19,7 +19,20 @@ var mergeQuery = require('loopback-datasource-juggler/lib/utils').mergeQuery; var logger = require('oe-logger'); var log = logger('service-personalizer'); var customFunction; -const { nextTick, parseMethodString, createError, formatDateTimeJoda, isDate, isString, isObject, isNumber } = require('./utils'); + +//begin - task-import - import from utils +const { + nextTick, + parseMethodString, + createError, + formatDateTimeJoda, + isDate, + isString, + isObject, + isNumber, + validateMethodName +} = require('./utils'); +//end - task-import /** * This function returns the necessary sorting logic to @@ -950,7 +963,7 @@ function init(app) { next(error); }); } - let { personalizationRule: { postCustomFunction, fieldMask } } = data; + let { modelName, personalizationRule: { postCustomFunction, fieldMask }, methodName } = data; if (postCustomFunction) { let { functionName } = postCustomFunction; if (functionName) { @@ -1005,6 +1018,15 @@ function init(app) { return nextTick(() => next(fMaskDef.error)); } } + + if(methodName) { + let Model = loopback.findModel(modelName); + if(!validateMethodName(Model, methodName)) { + let e = createError(`methodName: ${methodName} for model ${modelName} is invalid`); + return nextTick(() => next(e)); + } + } + nextTick(next); }); } diff --git a/lib/utils.js b/lib/utils.js index 2761327..87895af 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,7 +1,14 @@ const _slice = [].slice; -let { DateTimeFormatter, LocalDateTime, nativeJs } = require('@js-joda/core'); -let { Locale } = require('@js-joda/locale'); -let { prototype: {toString} } = Object; +const { DateTimeFormatter, LocalDateTime, nativeJs } = require('@js-joda/core'); +const { Locale } = require('@js-joda/locale'); +const { prototype: {toString} } = Object; + +const STAR = '*'; +const PROTOTYPE = 'prototype'; +const DOT = '.'; +const DOUBLE_STAR = '**'; +const STAR_DOT_STAR = '*.*'; +const PROTOTYPE_DOT_STAR = 'prototype.*'; module.exports = { /** @@ -67,5 +74,38 @@ module.exports = { isNumber(object) { return toString.call(object) === '[object Number]'; + }, + + /** + * + * @param {ModelConstructor} Model - loopback model + * @param {string} methodName - method name + * @returns {boolean} - true/false - true if the methodName string is valid + */ + validateMethodName(Model, methodName) { + let hasAsterisk = methodName.includes(STAR); + let hasPrototype = methodName.includes(PROTOTYPE); + + if(hasAsterisk && methodName === STAR) { + return true; + } + else if(hasAsterisk && methodName === DOUBLE_STAR) { + return true; + } + else if(hasAsterisk && hasPrototype && (methodName === PROTOTYPE_DOT_STAR || methodName === STAR_DOT_STAR)) { + return true; + } + else if(!hasPrototype && !hasAsterisk) { + //! static method + return !!Model[methodName] + } + else if(hasPrototype && !hasAsterisk) { + //! prototype method + let protoMethod = methodName.substr(methodName.indexOf(DOT) + 1); + return !!Model.prototype[protoMethod]; + } + return false; } + + }; From 1386e91c6d014a89ec197aa0d11eabd5c0154fbb Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 19 May 2020 16:11:42 +0530 Subject: [PATCH 44/80] method gating impl --- lib/service-personalizer.js | 49 ++++++++++++++++++++++--- lib/utils.js | 10 +++++- test/test.js | 72 +++++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 5 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index e5a57d2..d87a796 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -30,7 +30,8 @@ const { isString, isObject, isNumber, - validateMethodName + validateMethodName, + REMOTES } = require('./utils'); //end - task-import @@ -1026,13 +1027,53 @@ function init(app) { return nextTick(() => next(e)); } } - + nextTick(next); }); } -function doMethodGating(ctx, records, cb) { - // let { _personalizationCache: { info, records }} = ctx; +function doMethodGating(ctx, currentPersonalizationRecords, cb) { + let { _personalizationCache: { info }} = ctx; + let firstRecord = currentPersonalizationRecords[0]; + + if(info && firstRecord && info.modelName === firstRecord.modelName) { + // ! this is probably called via a true remote pipeline + let { methodName } = firstRecord; + let {STAR, STAR_DOT_STAR, DOT, DOUBLE_STAR, PROTOTYPE_DOT_STAR, PROTOTYPE} = REMOTES; + let isPattern = methodName.includes(STAR); + let isPrototype = methodName.includes(PROTOTYPE); + let hasDot = methodName.includes(DOT); + + let allowFlag = false; + if(isPattern && !isPrototype && (methodName === STAR || methodName === DOUBLE_STAR)) { + //! when * or ** + // allow everything. + allowFlag = true; + } + else if(isPattern && !isPrototype && hasDot && methodName === STAR_DOT_STAR) { + //! all prototypes (*.*) + allowFlag = !info.isStatic; + } + else if(isPattern && isPrototype && methodName === PROTOTYPE_DOT_STAR) { + //! all prototypes (prototype.*) + allowFlag = !info.isStatic + } + else if(!isPattern && !isPrototype) { + //! single static method (E.g. find, findById) + allowFlag = info.isStatic && info.methodName === methodName; + } + else if(isPrototype && hasDot && !isPattern) { + // single prototype method - (Eg. prototype.__get__orders) + let protoMethod = methodName.substr(methodName.indexOf(DOT) + 1); + allowFlag = !info.isStatic && info.methodName === protoMethod; + } + + // TODO: consider adding an invert flag to invert the allowFlag, + // for e.g. disallow only findById, allow others, + // or disallow a custom remote method + + return nextTick(() => cb(allowFlag)); + } // Assume all allowed for now nextTick(() => cb(true)); } diff --git a/lib/utils.js b/lib/utils.js index 87895af..c20ed6e 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -105,7 +105,15 @@ module.exports = { return !!Model.prototype[protoMethod]; } return false; - } + }, + REMOTES : { + STAR, + PROTOTYPE, + DOT, + DOUBLE_STAR, + STAR_DOT_STAR, + PROTOTYPE_DOT_STAR + } }; diff --git a/test/test.js b/test/test.js index a346fe6..de0633f 100755 --- a/test/test.js +++ b/test/test.js @@ -2402,6 +2402,78 @@ describe(chalk.blue('service personalization test started...'), function () { expect(apiResponse.aadhar).to.equal('45 24 ** **'); }); }); + + /** + * these tests describe the gating of personalization rules + * based on method of the model. + */ + + describe('Method based service personalization', () => { + before('creating personalization rules', done => { + let data = { + modelName: 'XCustomer', + methodName: 'find', + personalizationRule: { + fieldMask: { + custRef: { + stringMask: { + 'pattern': '(\\w+)\\-(\\w+)\\-(\\d+)', + 'maskCharacter': 'X', + 'format': '$1-$2-$3', + 'mask': ['$3'] + } + } + } + } + }; + + PersonalizationRule.create(data, {}, function(err){ + done(err); + }); + }); + + let apiCall1Response; + before('api call 1 - doing find() remote', done => { + let url = '/api/XCustomers'; + api.get(url) + .set('Accept', 'application/json') + .expect(200) + .end((err, resp) => { + if (err) { + return done(err); + } + let result = resp.body; + apiCall1Response = result; + done(); + }); + }); + + let apiCall2Response; + before('api call 2 - doing findById() remote', done => { + let url = '/api/XCustomers/2'; + api.get(url) + .set('Accept', 'application/json') + .expect(200) + .end((err, resp) => { + if (err) { + return done(err); + } + let result = resp.body; + apiCall2Response = result; + done(); + }); + }); + + it('t47 should assert that only find() call (i.e, api call 1) is personalized', () => { + expect(apiCall1Response).to.be.array; + apiCall1Response.forEach(rec => { + expect(rec.custRef).to.include('X'); + }); + expect(apiCall2Response).to.be.object; + expect(apiCall2Response.custRef).to.not.include('X'); + }); + }); + }); From 09e5523d2bcc8ed03d591f13bc438d589a991004 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 19 May 2020 16:13:38 +0530 Subject: [PATCH 45/80] lint fixes --- lib/service-personalizer.js | 48 +++++++++++++++++-------------------- lib/utils.js | 22 +++++++---------- 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index d87a796..005e03d 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -20,20 +20,20 @@ var logger = require('oe-logger'); var log = logger('service-personalizer'); var customFunction; -//begin - task-import - import from utils -const { - nextTick, - parseMethodString, - createError, - formatDateTimeJoda, - isDate, - isString, - isObject, +// begin - task-import - import from utils +const { + nextTick, + parseMethodString, + createError, + formatDateTimeJoda, + isDate, + isString, + isObject, isNumber, validateMethodName, - REMOTES + REMOTES } = require('./utils'); -//end - task-import +// end - task-import /** * This function returns the necessary sorting logic to @@ -1020,9 +1020,9 @@ function init(app) { } } - if(methodName) { + if (methodName) { let Model = loopback.findModel(modelName); - if(!validateMethodName(Model, methodName)) { + if (!validateMethodName(Model, methodName)) { let e = createError(`methodName: ${methodName} for model ${modelName} is invalid`); return nextTick(() => next(e)); } @@ -1036,39 +1036,35 @@ function doMethodGating(ctx, currentPersonalizationRecords, cb) { let { _personalizationCache: { info }} = ctx; let firstRecord = currentPersonalizationRecords[0]; - if(info && firstRecord && info.modelName === firstRecord.modelName) { + if (info && firstRecord && info.modelName === firstRecord.modelName) { // ! this is probably called via a true remote pipeline - let { methodName } = firstRecord; + let { methodName } = firstRecord; let {STAR, STAR_DOT_STAR, DOT, DOUBLE_STAR, PROTOTYPE_DOT_STAR, PROTOTYPE} = REMOTES; let isPattern = methodName.includes(STAR); let isPrototype = methodName.includes(PROTOTYPE); let hasDot = methodName.includes(DOT); let allowFlag = false; - if(isPattern && !isPrototype && (methodName === STAR || methodName === DOUBLE_STAR)) { + if (isPattern && !isPrototype && (methodName === STAR || methodName === DOUBLE_STAR)) { //! when * or ** // allow everything. allowFlag = true; - } - else if(isPattern && !isPrototype && hasDot && methodName === STAR_DOT_STAR) { + } else if (isPattern && !isPrototype && hasDot && methodName === STAR_DOT_STAR) { //! all prototypes (*.*) allowFlag = !info.isStatic; - } - else if(isPattern && isPrototype && methodName === PROTOTYPE_DOT_STAR) { + } else if (isPattern && isPrototype && methodName === PROTOTYPE_DOT_STAR) { //! all prototypes (prototype.*) - allowFlag = !info.isStatic - } - else if(!isPattern && !isPrototype) { + allowFlag = !info.isStatic; + } else if (!isPattern && !isPrototype) { //! single static method (E.g. find, findById) allowFlag = info.isStatic && info.methodName === methodName; - } - else if(isPrototype && hasDot && !isPattern) { + } else if (isPrototype && hasDot && !isPattern) { // single prototype method - (Eg. prototype.__get__orders) let protoMethod = methodName.substr(methodName.indexOf(DOT) + 1); allowFlag = !info.isStatic && info.methodName === protoMethod; } - // TODO: consider adding an invert flag to invert the allowFlag, + // TODO: consider adding an invert flag to invert the allowFlag, // for e.g. disallow only findById, allow others, // or disallow a custom remote method diff --git a/lib/utils.js b/lib/utils.js index c20ed6e..105693b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -77,7 +77,7 @@ module.exports = { }, /** - * + * * @param {ModelConstructor} Model - loopback model * @param {string} methodName - method name * @returns {boolean} - true/false - true if the methodName string is valid @@ -86,20 +86,16 @@ module.exports = { let hasAsterisk = methodName.includes(STAR); let hasPrototype = methodName.includes(PROTOTYPE); - if(hasAsterisk && methodName === STAR) { + if (hasAsterisk && methodName === STAR) { return true; - } - else if(hasAsterisk && methodName === DOUBLE_STAR) { + } else if (hasAsterisk && methodName === DOUBLE_STAR) { return true; - } - else if(hasAsterisk && hasPrototype && (methodName === PROTOTYPE_DOT_STAR || methodName === STAR_DOT_STAR)) { + } else if (hasAsterisk && hasPrototype && (methodName === PROTOTYPE_DOT_STAR || methodName === STAR_DOT_STAR)) { return true; - } - else if(!hasPrototype && !hasAsterisk) { + } else if (!hasPrototype && !hasAsterisk) { //! static method - return !!Model[methodName] - } - else if(hasPrototype && !hasAsterisk) { + return !!Model[methodName]; + } else if (hasPrototype && !hasAsterisk) { //! prototype method let protoMethod = methodName.substr(methodName.indexOf(DOT) + 1); return !!Model.prototype[protoMethod]; @@ -107,7 +103,7 @@ module.exports = { return false; }, - REMOTES : { + REMOTES: { STAR, PROTOTYPE, DOT, @@ -115,5 +111,5 @@ module.exports = { STAR_DOT_STAR, PROTOTYPE_DOT_STAR } - + }; From ae62e11c8fb340ac04a328a83421c2b7f82cecc1 Mon Sep 17 00:00:00 2001 From: deostroll Date: Fri, 22 May 2020 16:11:04 +0530 Subject: [PATCH 46/80] test setup for role-based personalization --- common/models/personalization-rule.json | 5 +- lib/service-personalizer.js | 13 +- test/test.js | 167 ++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 5 deletions(-) diff --git a/common/models/personalization-rule.json b/common/models/personalization-rule.json index e9c5fe5..cbe2e38 100644 --- a/common/models/personalization-rule.json +++ b/common/models/personalization-rule.json @@ -38,5 +38,8 @@ "validations": [], "relations": {}, "acls": [], - "methods": {} + "methods": {}, + "mixins": { + "DataPersonalizationMixin": true + } } \ No newline at end of file diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 005e03d..2b71eda 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -1045,10 +1045,10 @@ function doMethodGating(ctx, currentPersonalizationRecords, cb) { let hasDot = methodName.includes(DOT); let allowFlag = false; - if (isPattern && !isPrototype && (methodName === STAR || methodName === DOUBLE_STAR)) { - //! when * or ** - // allow everything. - allowFlag = true; + if (isPattern && !isPrototype && methodName === STAR) { + //! when * + // allow only static calls + allowFlag = info.isStatic; } else if (isPattern && !isPrototype && hasDot && methodName === STAR_DOT_STAR) { //! all prototypes (*.*) allowFlag = !info.isStatic; @@ -1063,6 +1063,10 @@ function doMethodGating(ctx, currentPersonalizationRecords, cb) { let protoMethod = methodName.substr(methodName.indexOf(DOT) + 1); allowFlag = !info.isStatic && info.methodName === protoMethod; } + else if(isPattern && !isPrototype && methodName === DOUBLE_STAR) { + //! ** - allow everthing + allowFlag = true; + } // TODO: consider adding an invert flag to invert the allowFlag, // for e.g. disallow only findById, allow others, @@ -1104,6 +1108,7 @@ function getPersonalizationMeta(ctx, info, isBeforeRemote) { let data = null; let applyPersonalizationFlag = true; let theModel = modelName; + // TODO: Assign data based on http method only - no need of switch statement if (isStatic) { switch (methodName) { case 'create': diff --git a/test/test.js b/test/test.js index de0633f..7039d51 100755 --- a/test/test.js +++ b/test/test.js @@ -2474,6 +2474,173 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); + + /** + * These tests describe the role based gating + * of service personalization + */ + + describe('Role-based service personalization', () => { + let allUsers; + before('setting up users and roles', done => { + let User = loopback.findModel('User'); + let Role = loopback.findModel('Role'); + let RoleMapping = loopback.findModel('RoleMapping'); + expect(typeof User !== 'undefined').to.be.ok; + expect(typeof Role !== 'undefined').to.be.ok; + expect(typeof RoleMapping !== 'undefined').to.be.ok; + + User.create([ + { username: 'John', email: 'John@ev.com', password: 'password1' }, + { username: 'Jane', email: 'Jane@ev.com', password: 'password1' }, + { username: 'Bob', email: 'Bob@ev.com', password: 'password1' }, + { username: 'Martha', email: 'Martha@ev.com', password: 'password1' } + ], function(err, users){ + if(err){ + return done(err); + } + allUsers = users; + + Role.create([ + { name: 'admin'}, + { name: 'manager'}, + { name: 'teller' }, + { name: 'agent' }, + ], function(err, roles){ + if(err){ + return done(err); + } + + let assignUserRole = (user, role) => cb => role.principals.create({ + principalType: RoleMapping.USER, + principleId: user.id + }, function(err){ + cb(err); + }); + + ['John', 'Jane', "Bob", 'Martha'].forEach((name, idx) => { + expect(users[idx].username).to.equal(name); + }); + + async.eachSeries([ + assignUserRole(users[0], roles[0]), + assignUserRole(users[1], roles[1]), + assignUserRole(users[2], roles[2]), + assignUserRole(users[3], roles[3]), + ], (fn, done) => fn(done), err => { + done(err); + }); + }); + }); + }); + + before('setup personalization rules', done => { + let rules = [ + { + ruleName: 'for tellers', + modelName: 'XCustomers', + personalizationRule: { + fieldMask : { + aadhar: { + numberMask: { + pattern: '(\\d{2})(\\d{2})(\\d{2})(\\d{2})', + format: '$1-$2-$3-$4', + mask: ['$1', '$2', '$3'] + } + } + } + }, + scope: { + roles: ['teller'] + } + }, + { + ruleName: 'for agents', + modelName: 'XCustomers', + personalizationRule: { + fieldMask : { + custRef: { + stringMask: { + pattern: '(\\w+)\\-(\\w+)\\-(\\d+)', + format: '$1-$2-$3', + mask: ['$1', '$3'] + } + } + } + }, + scope: { + roles: ['agent'] + } + } + ]; + + PersonalizationRule.create(rules, function(err){ + done(err); + }); + }); + + let accessTokens; + before('create access tokens', done => { + let url = '/api/Users/login'; + async.map(allUsers, function(user, cb){ + let { username } = user; + api.post(url) + .set('Accept', 'application/json') + .send({ username, password: 'password1'}) + .expect(200) + .end((err, resp) => { + if(err) { + return cb(err); + } + cb(null, { username, token: resp.body.id }); + }); + }, function(err, results) { + if(err) { + return done(err); + } + accessTokens = results.reduce((carrier, obj) => Object.assign(carrier, {[obj.username]: obj.token}), {}); + done(); + }); + }); + + let tellerResponse; + before('access teller data via remote', done => { + let accessToken = accessTokens['Bob']; + let url = `/api/XCustomer/2?accessToken=${accessToken}`; + api.get(url) + .set("Accept", 'application/json') + .expect(200) + .end((err, resp) => { + if(err) { + return done(err); + } + tellerResponse = resp.body; + done(); + }); + }); + + let agentResponse; + before('access teller data via remote', done => { + let accessToken = accessTokens['Martha']; + let url = `/api/XCustomer/2?accessToken=${accessToken}`; + api.get(url) + .set("Accept", 'application/json') + .expect(200) + .end((err, resp) => { + if(err) { + return done(err); + } + agentResponse = resp.body; + done(); + }); + }); + + it('t48 should assert that agentResponse and tellerResponse is not identital', () => { + expect(tellerResponse).to.not.deep.equal(agentResponse); + }); + + }); + }); From ca15bba2f0fc7438238e8e5ff894abb4a967bb74 Mon Sep 17 00:00:00 2001 From: deostroll Date: Fri, 22 May 2020 16:51:24 +0530 Subject: [PATCH 47/80] reverted partially to state 09e552 - disabled the datapersonalizationmixin - corrected the role-based tests --- common/models/personalization-rule.json | 6 ++---- lib/service-personalizer.js | 8 ++++---- test/test.js | 10 +++++----- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/common/models/personalization-rule.json b/common/models/personalization-rule.json index cbe2e38..47e55c0 100644 --- a/common/models/personalization-rule.json +++ b/common/models/personalization-rule.json @@ -31,7 +31,7 @@ }, "methodName" : { "type": "string", - "default": "*", + "default": "**", "description": "The model methodName this rule should apply to. Should be the methodName (static/instance) or wildcards you specify in a afterRemote()/beforeRemote(). Default '*'" } }, @@ -39,7 +39,5 @@ "relations": {}, "acls": [], "methods": {}, - "mixins": { - "DataPersonalizationMixin": true - } + "mixins": {} } \ No newline at end of file diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 2b71eda..3c05f52 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -1049,6 +1049,9 @@ function doMethodGating(ctx, currentPersonalizationRecords, cb) { //! when * // allow only static calls allowFlag = info.isStatic; + } else if(isPattern && !isPrototype && methodName === DOUBLE_STAR) { + //! ** - allow everthing + allowFlag = true; } else if (isPattern && !isPrototype && hasDot && methodName === STAR_DOT_STAR) { //! all prototypes (*.*) allowFlag = !info.isStatic; @@ -1063,10 +1066,7 @@ function doMethodGating(ctx, currentPersonalizationRecords, cb) { let protoMethod = methodName.substr(methodName.indexOf(DOT) + 1); allowFlag = !info.isStatic && info.methodName === protoMethod; } - else if(isPattern && !isPrototype && methodName === DOUBLE_STAR) { - //! ** - allow everthing - allowFlag = true; - } + // TODO: consider adding an invert flag to invert the allowFlag, // for e.g. disallow only findById, allow others, diff --git a/test/test.js b/test/test.js index 7039d51..5bcb716 100755 --- a/test/test.js +++ b/test/test.js @@ -2538,7 +2538,7 @@ describe(chalk.blue('service personalization test started...'), function () { let rules = [ { ruleName: 'for tellers', - modelName: 'XCustomers', + modelName: 'XCustomer', personalizationRule: { fieldMask : { aadhar: { @@ -2556,7 +2556,7 @@ describe(chalk.blue('service personalization test started...'), function () { }, { ruleName: 'for agents', - modelName: 'XCustomers', + modelName: 'XCustomer', personalizationRule: { fieldMask : { custRef: { @@ -2606,7 +2606,7 @@ describe(chalk.blue('service personalization test started...'), function () { let tellerResponse; before('access teller data via remote', done => { let accessToken = accessTokens['Bob']; - let url = `/api/XCustomer/2?accessToken=${accessToken}`; + let url = `/api/XCustomers/2?accessToken=${accessToken}`; api.get(url) .set("Accept", 'application/json') .expect(200) @@ -2620,9 +2620,9 @@ describe(chalk.blue('service personalization test started...'), function () { }); let agentResponse; - before('access teller data via remote', done => { + before('access agent data via remote', done => { let accessToken = accessTokens['Martha']; - let url = `/api/XCustomer/2?accessToken=${accessToken}`; + let url = `/api/XCustomers/2?accessToken=${accessToken}`; api.get(url) .set("Accept", 'application/json') .expect(200) From 51476ed2ef000ed12421a90859674fd8f331ef02 Mon Sep 17 00:00:00 2001 From: deostroll Date: Fri, 22 May 2020 17:10:28 +0530 Subject: [PATCH 48/80] added data personalization mixin --- common/models/personalization-rule.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/common/models/personalization-rule.json b/common/models/personalization-rule.json index 47e55c0..0e10ab4 100644 --- a/common/models/personalization-rule.json +++ b/common/models/personalization-rule.json @@ -39,5 +39,7 @@ "relations": {}, "acls": [], "methods": {}, - "mixins": {} + "mixins": { + "DataPersonalizationMixin": true + } } \ No newline at end of file From 274ec41627e561138614727ddd0b9d854dea7c68 Mon Sep 17 00:00:00 2001 From: deostroll Date: Fri, 22 May 2020 20:04:04 +0530 Subject: [PATCH 49/80] interim commit --- server/boot/service-personalization.js | 3 ++- test/middleware.json | 1 + test/middleware/foo.js | 6 ++++++ test/server.js | 1 + test/test.js | 4 ++-- 5 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 test/middleware/foo.js diff --git a/server/boot/service-personalization.js b/server/boot/service-personalization.js index e74df87..a5ed82e 100644 --- a/server/boot/service-personalization.js +++ b/server/boot/service-personalization.js @@ -15,7 +15,8 @@ // var messaging = require('../../lib/common/global-messaging'); var servicePersonalizer = require('../../lib/service-personalizer'); -module.exports = function ServicePersonalization(app) { +module.exports = function ServicePersonalization(app, cb) { servicePersonalizer.init(app); + process.nextTick(cb); }; diff --git a/test/middleware.json b/test/middleware.json index f781bc3..14b84ce 100644 --- a/test/middleware.json +++ b/test/middleware.json @@ -19,6 +19,7 @@ "parse": { }, "routes:before": { + "./middleware/foo": {}, "loopback#rest": { "paths": ["${restApiRoot}"] } diff --git a/test/middleware/foo.js b/test/middleware/foo.js new file mode 100644 index 0000000..27da11c --- /dev/null +++ b/test/middleware/foo.js @@ -0,0 +1,6 @@ +module.exports = function () { + return function(req, res, next) { + console.log(req.accessToken); + next(); + } +} \ No newline at end of file diff --git a/test/server.js b/test/server.js index 821e3ca..8b86cfb 100644 --- a/test/server.js +++ b/test/server.js @@ -1,5 +1,6 @@ var oecloud = require('oe-cloud'); oecloud.boot(__dirname, function (err) { + oecloud.enableAuth(); oecloud.start(); oecloud.emit('test-start'); }); diff --git a/test/test.js b/test/test.js index 5bcb716..125b6b7 100755 --- a/test/test.js +++ b/test/test.js @@ -2606,7 +2606,7 @@ describe(chalk.blue('service personalization test started...'), function () { let tellerResponse; before('access teller data via remote', done => { let accessToken = accessTokens['Bob']; - let url = `/api/XCustomers/2?accessToken=${accessToken}`; + let url = `/api/XCustomers/2?access_token=${accessToken}`; api.get(url) .set("Accept", 'application/json') .expect(200) @@ -2622,7 +2622,7 @@ describe(chalk.blue('service personalization test started...'), function () { let agentResponse; before('access agent data via remote', done => { let accessToken = accessTokens['Martha']; - let url = `/api/XCustomers/2?accessToken=${accessToken}`; + let url = `/api/XCustomers/2?access_token=${accessToken}`; api.get(url) .set("Accept", 'application/json') .expect(200) From f1cf1f82bb47dcd869da3e9eda46c112155352fe Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 25 May 2020 11:05:29 +0530 Subject: [PATCH 50/80] role-based tests --- test/test.js | 45 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 43 insertions(+), 2 deletions(-) diff --git a/test/test.js b/test/test.js index 125b6b7..3d335c3 100755 --- a/test/test.js +++ b/test/test.js @@ -2149,6 +2149,7 @@ describe(chalk.blue('service personalization test started...'), function () { * level personalizations work * */ + let CustomerRecords; describe('property level personalizations', () => { let ModelDefinition = null; @@ -2262,6 +2263,7 @@ describe(chalk.blue('service personalization test started...'), function () { aadhar: 45248632 } ]; + CustomerRecords = data; Customer.create(data, {}, function (err) { done(err); }); @@ -2513,7 +2515,7 @@ describe(chalk.blue('service personalization test started...'), function () { let assignUserRole = (user, role) => cb => role.principals.create({ principalType: RoleMapping.USER, - principleId: user.id + principalId: user.id }, function(err){ cb(err); }); @@ -2635,8 +2637,47 @@ describe(chalk.blue('service personalization test started...'), function () { }); }); - it('t48 should assert that agentResponse and tellerResponse is not identital', () => { + let managerResponse; + before('access manager data via remote', done => { + let accessToken = accessTokens['Jane']; + let url = `/api/XCustomers/2?access_token=${accessToken}`; + api.get(url) + .set("Accept", 'application/json') + .expect(200) + .end((err, resp) => { + if(err) { + return done(err); + } + managerResponse = resp.body; + done(); + }); + }); + + let adminResponse; + before('access admin data via remote', done => { + let accessToken = accessTokens['John']; + let url = `/api/XCustomers/2?access_token=${accessToken}`; + api.get(url) + .set("Accept", 'application/json') + .expect(200) + .end((err, resp) => { + if(err) { + return done(err); + } + adminResponse = resp.body; + done(); + }); + }); + + it('t48 should assert that agent and teller results are not identital', () => { expect(tellerResponse).to.not.deep.equal(agentResponse); + let originalRecord = CustomerRecords[1]; + expect(tellerResponse.aadhar).to.equal('XX-XX-XX-' + originalRecord.aadhar.toString().substr(-2)); + expect(agentResponse.custRef).to.equal('XXXXX-BLR-XXXX'); + }); + + it('t49 should assert that manager and admin results are identical since there is no personalization rules applied', () => { + expect(managerResponse).to.deep.equal(adminResponse); }); }); From 02551a62e57945a3e0540c7b3f76e1f93e0c3930 Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 25 May 2020 11:59:05 +0530 Subject: [PATCH 51/80] Revert "added data personalization mixin" This reverts commit 51476ed2ef000ed12421a90859674fd8f331ef02. --- common/models/personalization-rule.json | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/common/models/personalization-rule.json b/common/models/personalization-rule.json index 0e10ab4..47e55c0 100644 --- a/common/models/personalization-rule.json +++ b/common/models/personalization-rule.json @@ -39,7 +39,5 @@ "relations": {}, "acls": [], "methods": {}, - "mixins": { - "DataPersonalizationMixin": true - } + "mixins": {} } \ No newline at end of file From 7e1f2ef83d62966f745ed72f56a6129ab49a0d17 Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 25 May 2020 12:31:35 +0530 Subject: [PATCH 52/80] reverted middleware.json --- test/middleware.json | 1 - 1 file changed, 1 deletion(-) diff --git a/test/middleware.json b/test/middleware.json index 14b84ce..f781bc3 100644 --- a/test/middleware.json +++ b/test/middleware.json @@ -19,7 +19,6 @@ "parse": { }, "routes:before": { - "./middleware/foo": {}, "loopback#rest": { "paths": ["${restApiRoot}"] } From 3ed2c73cd0f066098a55d95b1d8713429ec936eb Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 25 May 2020 12:35:37 +0530 Subject: [PATCH 53/80] removed middleware --- test/middleware/foo.js | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 test/middleware/foo.js diff --git a/test/middleware/foo.js b/test/middleware/foo.js deleted file mode 100644 index 27da11c..0000000 --- a/test/middleware/foo.js +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = function () { - return function(req, res, next) { - console.log(req.accessToken); - next(); - } -} \ No newline at end of file From aae20064f3c85a275142036741e355735e02ba9b Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 25 May 2020 12:39:48 +0530 Subject: [PATCH 54/80] change in model property desc --- common/models/personalization-rule.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/models/personalization-rule.json b/common/models/personalization-rule.json index 47e55c0..caa9507 100644 --- a/common/models/personalization-rule.json +++ b/common/models/personalization-rule.json @@ -32,7 +32,7 @@ "methodName" : { "type": "string", "default": "**", - "description": "The model methodName this rule should apply to. Should be the methodName (static/instance) or wildcards you specify in a afterRemote()/beforeRemote(). Default '*'" + "description": "The model methodName this rule should apply to. Should be the methodName (static/instance) or wildcards you specify in a afterRemote()/beforeRemote(). Default '**'" } }, "validations": [], From 84a65ca96121cb5d112644432840fdbdac50477d Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 25 May 2020 15:36:14 +0530 Subject: [PATCH 55/80] fixed the postgres/oracle test failure --- test/test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test.js b/test/test.js index 3d335c3..50027d3 100755 --- a/test/test.js +++ b/test/test.js @@ -2066,7 +2066,8 @@ describe(chalk.blue('service personalization test started...'), function () { } let result = resp.body; // console.log(resp.body); - expect(result[0].modelNo).to.equal('123456XXXX'); + let idx = result.findIndex(r => r.id === 'watch3'); + expect(result[idx].modelNo).to.equal('123456XXXX'); done(); }); }); From 102d35e75a8acd19edc0e8cc320a9db94a496cd7 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 26 May 2020 19:46:25 +0530 Subject: [PATCH 56/80] updated docs --- README.md | 288 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 223 insertions(+), 65 deletions(-) diff --git a/README.md b/README.md index 491f919..486ba6f 100644 --- a/README.md +++ b/README.md @@ -44,6 +44,15 @@ $ # Run test cases along with code coverage - code coverage report will be avail $ npm run grunt-cover ``` +## Main features + +- Customizing remote responses, or data +(i.e. queried via loopback model api), + to appear in a certain manner + - Based on user role + - Custom scope - for e.g. for android, or, ios clients +- Limiting personalization to apply to a particular remote method + ## How to use 1. Install the module to your application @@ -123,6 +132,29 @@ we have provided `scope` for this rule to take effect only when it is specified in the http headers of the request; it is always a simple key/value pair. +To have this personalization apply only to a user of a specified +role (say to either `tellers` or `agents`), it must be defined as in the below example: + +Example: +```json +{ + "disabled" : false, + "modelName" : "ProductCatalog", + "personalizationRule" : { + "fieldValueReplace" : { + "keywords" : { + "Alpha" : "A", + "Bravo" : "B" + } + } + }, + "scope" : { + "roles" : ["teller", "agent"] + } +} +``` + + 5. _(Optional)_ If there are some custom function based operations, add the path in the application's `config.json` file. Alternatively, set the environment variable: @@ -143,19 +175,32 @@ $ export custom_function_path="/project/customFuncDir" ## Working Principle -During application startup all the models which have the -above mixin applied will behave differently; we attach -`beforeRemote` and `afterRemote` hooks which determine if -there are personalization rules for the model, -and, then finally performs -the defined operations on the request/response (as per the -case). It also recursively does this in the case relational data -is included. The net effect of all the operations is the -"personalized" data. +All models with the `ServicePersonalizationMixin` +enabled will have functions attached +to its `afterRemote()` and `beforeRemote()` which will +do the personalization. + +The personalization records, stored in `PersonalizationRule`, +are queried according to scope, and, model participating, in +the remote call. The information required for this +is obtained from the `HttpContext` which is an argument in +the callback of the aforementioned methods. + +After this, these steps are done: + +1. personalization is applied at the root model, i.e. the +one that participates in the remote call. +2. personalization is then applied to any relations +3. personalization is applied to all properties +of the root model, which are model constructors -These steps personalize data as long as its accessed via a -remote endpoint (aka _remotes_). To do this in code, please -see the programmatic api. +Personalization can happen through code also via `performServicePersonalization()` +api call. They follow the same process mentioned above, +however, there are a few limitations. Notably, those +operations which are meant to apply `post-fetch` will +be honoured, and, thus personalized. + +More details on `pre-fetch` and `post-fetch` in sections below. (Significance of pre-fetch & post-fetch) ## Supported operations @@ -175,45 +220,158 @@ corresponding tests: | Operation | Description | Aspect | Tests | |--------------------|---------------------------------------------------------------------------------------------------------------|----------------------|---------------------------------------| -| lbFilter | This applies a loopback filter to the request; it can contain an _include_ clause or a _where_ clause. | Pre-apply | t21 | -| filter | Same as above, but only adds the where clause to the request i.e. a query-based filter | Pre-apply | t9 | -| sort | Performs a sort at the datasource level | Pre-apply | t4, t5, t6, t7, t8, t10, t11 | -| fieldReplace | Replaces the property name in the data with another text. (Not its value) | Pre-apply/Post-apply | t1, t15, t17 | -| fieldValueReplace | Replaces the property value in the data | Pre-apply/Post-apply | t22, t20, t19, t18, t17, t16, t3, t23 | -| fieldMask | Masks value in the field according to a regex pattern | Post-apply | t24, t25, t26, t27, t28, t29 | -| mask | Hides a field in the response | Pre-apply | t13 | -| hide | Same as _mask_ | Pre-apply | t13 | -| postCustomFunction | Adds a custom function which can add desired customization to response. Please see step #5 in how to use. | Post-apply | t35, t36 | -| preCustomFunction | Adds a custom function which can add desired customization to the request. Please see step #5 in how to use. | Pre-apply | t35, t36 | +| lbFilter | This applies a loopback filter to the request; it can contain an _include_ clause or a _where_ clause. | pre-fetch | t21 | +| filter | Same as above, but only adds the where clause to the request i.e. a query-based filter | pre-fetch | t9 | +| sort | Performs a sort at the datasource level | pre-fetch | t4, t5, t6, t7, t8, t10, t11 | +| fieldReplace | Replaces the property name in the data with another text. (Not its value) | pre-fetch/post-fetch | t1, t15, t17 | +| fieldValueReplace | Replaces the property value in the data | pre-fetch/post-fetch | t22, t20, t19, t18, t17, t16, t3, t23 | +| fieldMask | Masks value in the field according to a regex pattern. More details in the section of fieldMask | post-fetch | t24, t25, t26, t27, t28, t29 | +| mask | Hides a field in the response | pre-fetch | t13 | +| postCustomFunction | Adds a custom function which can add desired customization to response. Please see step #5 in how to use. | post-fetch | t35, t36 | +| preCustomFunction | Adds a custom function which can add desired customization to the request. Please see step #5 in how to use. | pre-fetch | t35, t36 | -## Programmatic API -To do personalization in a custom remote method, or, in unit -tests you need the following api. +## **fieldMask** options -```JavaScript +Prior to version 2.4.0, a field mask definition looks like this: + +```json +{ + "modelName": "ProductCatalog", + "personalizationRule": { + "fieldMask": { + "modelNo": { + "pattern": "([0-9]{3})([0-9]{3})([0-9]{4})", + "maskCharacter": "X", + "format": "($1) $2-$3", + "mask": [ + "$3" + ] + } + } + }, + "scope": { + "region": "us" + } +} +``` + +This is still supported. The framework assumes +`modelNo` in this example to be of type `String` +and performs validation before insert into +`PersonalizationRule` model. -const { applyServicePersonalization } = require('oe-service-personalization/lib/service-personalizer'); +The **fieldMask** operations can be applied to the following data types: +- String +- Number +- Date -// ... -var options = { - isBeforeRemote: false, // required - context: ctx //the http context -}; +Validation will happen for the same at the time of creating +the PersonalzationRule record. -applyServicePersonalization(modelName, data, options, function(err){ - // nothing to access here since - // data gets mutated internally -}) +Formal way to specify masking data of type `String` is as follows: + +```json +{ + "modelName": "ProductCatalog", + "personalizationRule": { + "fieldMask": { + "modelNo": { + "stringMask" : { + "pattern": "([0-9]{3})([0-9]{3})([0-9]{4})", + "maskCharacter": "X", + "format": "($1) $2-$3", + "mask": [ + "$3" + ] + } + + } + } + }, + "scope": { + "region": "us" + } +} +``` + +Formal way to specify masking of numbers is as follows: +```json +{ + "modelName": "ProductCatalog", + "personalizationRule": { + "fieldMask": { + "modelNo": { + "numberMask" : { + "pattern": "([0-9]{3})([0-9]{3})([0-9]{4})", + "maskCharacter": "X", + "format": "($1) $2-$3", + "mask": [ + "$3" + ] + } + + } + } + }, + "scope": { + "region": "us" + } +} ``` -Example directly from our tests (test case `t41`): `./test/common/models/product-owner.js` +> Note: the options are similar to that of `stringMask`. +Validation is done to determine if modelNo is of type `Number` -```javascript -const { applyServicePersonalization } = require('./../../../lib/service-personalizer'); // or require('oe-service-personalization/lib/service-personalizer'); +Formal way to specify masking of dates are as follows: +```json +{ + "modelName": "XCustomer", + "personalizationRule": { + "fieldMask": { + "dob": { + "dateMask": { + "format": "MMM/yyyy" + } + } + } + } +} +``` +The `format` in a `dateMask` field accepts any valid joda-time string. It is also +assumed to be of the `en_us` locale by default. Characters +intended for masking can be embedded in the format string itself, +however, they are a limited set, as, certain commonly used +characters like `x` or `X` have special meaning in the joda +standard. + +A `locale` option can be passed +alternatively specifying a different locale. Acceptable values (`String`) are: +- ENGLISH +- US (_default_) +- UK +- CANADA +- FRENCH +- FRANCE +- GERMAN +- GERMANY + +For more info about joda-time format visit: https://js-joda.github.io/js-joda/manual/formatting.html#format-patterns + +## Programmatic API -module.exports = function(ProductOwner) { - ProductOwner.remoteMethod('demandchain', { +To do personalization in a custom remote method, or, in unit +tests you need the `performServicePersonalizations()` api. + +Example + +```JavaScript + +const { performServicePersonalizations } = require('./../../../lib/service-personalizer'); // or require('oe-service-personalization/lib/service-personalizer'); +const loopback = require('loopback'); + +module.exports = function(PseudoProductOwner) { + PseudoProductOwner.remoteMethod('demandchain', { description: 'Gets the stores, store addresses, and, contacts of a product owner', accepts: [ { @@ -238,12 +396,12 @@ module.exports = function(ProductOwner) { http: { path: '/:id/demandchain', verb: 'get' } }); - ProductOwner.demandchain = function(ownerId, options, done) { + PseudoProductOwner.demandchain = function(ownerId, options, done) { if(typeof done === 'undefined' && typeof options === 'function') { done = options; options = {}; }; - + let ProductOwner = loopback.findModel('ProductOwner'); let filter = { "include": [ { @@ -261,36 +419,36 @@ module.exports = function(ProductOwner) { }; ProductOwner.findOne(filter, options, function(err, result) { if(err) { - done(err) + return done(err); } - else { - let persOpts = { - isBeforeRemote: false, context: options - }; - applyServicePersonalization('ProductOwner', result, persOpts, function(err){ - done(err, result); - }); + let persOptions = { + isBeforeRemote: false, + context: options } - }) + performServicePersonalizations(ProductOwner.definition.name, result, persOptions, function(err){ + done(err, result); + }) + }); }; } ``` -## Pre-apply/Post-apply & Relations +## Significance of pre-fetch/post-fetch operations + +It impacts how personalizations is done for relations, and, nested +data. + +Operations are individual actions you can perform on data, such as +**fieldMask**, or, **fieldValueReplace**, etc. -All operations have two aspects. Some operations -modify the context of the http request (for e.g. -`lbFilter`, `sort`, etc). Some operations modify the response (e.g. `fieldMask`) -we can access in an `afterRemote` phase of a request/response -pipeline. Other operations (for e.g. `fieldReplace`) have to -take effect in both the stages. +A `pre-fetch` operation is applied before data is fetched from +a loopback datasource. For e.g. **lbFilter**, **filter**, etc -Pre-apply/Post-apply is the vocabulary adopted to distinguish -these aspects of an operation - namely how and when it is -applied in the request/response pipeline. +A `post-fetch` operation is carried out after data is fetched +from a loopback datasource. For e.g. **fieldMask** Due to the way loopback relations are -implemented only operations that post-apply are honoured. +implemented only operations that `post-fetch` are honoured. This is also the case when using the programmatic api for service personalization (regardless of whether relations are accessed or not). @@ -300,7 +458,7 @@ are accessed or not). 1. Datasource support. Datasources can be service-oriented. (Such as a web service). Hence support for sorting, filtering, etc may be limited. -Therefore operations which pre-apply may not give expected +Therefore operations which pre-fetch may not give expected results. 2. Using custom functions (`postCustomFunction` or `preCustomFunction`). @@ -309,7 +467,7 @@ operation. No point in trying to modify `ctx.result` in a `preCustomFunction`. Also ensure path to the directory where the custom functions are stored is configured correctly. -3. Pre-apply/post-apply and relations +3. Understand how pre-fetch/post-fetch applies to relations and nested data. ## Test Synopsis The following entity structure and relationships assumed for most of the tests. From 2e2315846152583f272a7a071fad467a4804cedb Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 26 May 2020 19:55:45 +0530 Subject: [PATCH 57/80] updated docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 486ba6f..0e35fe3 100644 --- a/README.md +++ b/README.md @@ -448,7 +448,7 @@ A `post-fetch` operation is carried out after data is fetched from a loopback datasource. For e.g. **fieldMask** Due to the way loopback relations are -implemented only operations that `post-fetch` are honoured. +implemented, only operations that `post-fetch` are honoured. This is also the case when using the programmatic api for service personalization (regardless of whether relations are accessed or not). From 58eacf17ec36fb3d1ba27ffb372a816ee3280cb9 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 26 May 2020 22:19:27 +0530 Subject: [PATCH 58/80] refactor --- lib/service-personalizer.js | 208 ++++++++++++++---------------------- 1 file changed, 80 insertions(+), 128 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 3c05f52..7cc0b79 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -113,16 +113,16 @@ const utils = { var pos = replacement.indexOf('\uFF0E'); var key; var elsePart; - if (pos !== null && pos !== 'undefined' && pos !== -1) { + if (pos !== -1) { key = replacement.substr(0, pos); elsePart = replacement.substr(pos + 1); } else { key = replacement; } - if (record[key] !== 'undefined' && typeof record[key] === 'object') { + if (typeof record[key] !== 'undefined' && typeof record[key] === 'object') { utils.replaceField(record[key], elsePart, value); - } else if (record[key] !== 'undefined' && typeof record[key] !== 'object') { + } else if (typeof record[key] !== 'undefined' && typeof record[key] !== 'object') { if (record[key]) { if (typeof record.__data !== 'undefined') { record.__data[value] = record[key]; @@ -350,7 +350,7 @@ const p13nFunctions = { fieldReplace(replacements, isBeforeRemote = false) { // Tests: t1, t15, t17 let replaceRecord = utils.replaceRecordFactory(utils.replaceField); - let process = function (replacements, data, cb) { + let execute = function (replacements, data, cb) { if (Array.isArray(data)) { let updatedResult = data.map(record => { return replaceRecord(record, replacements); @@ -380,13 +380,13 @@ const p13nFunctions = { } } // fieldReplacementFn(ctx, revInputJson, cb); - process(revInputJson, data, callback); + execute(revInputJson, data, callback); }; } // ! for afterRemote case return function (data, callback) { - process(replacements, data, callback); + execute(replacements, data, callback); }; }, @@ -901,41 +901,38 @@ function applyServicePersonalization(modelName, data, records, options, cb) { log.debug(ctx, `${isBeforeRemote ? 'beforeRemote' : 'afterRemote'}: (leave${err ? ' - with error' : ''}) applying personalization for model: ${modelName}`); cb(err); }; - doMethodGating(ctx, records, function (isAllowedMethod) { - if (isAllowedMethod) { - return doRoleGating(ctx, records, function (isAllowedRole) { - if (isAllowedRole) { - if (records.length === 0) { - checkRelationAndRecurse(Model, data, options, function (err) { - if (err) { - return done(err); - } - checkPropsAndRecurse(Model, data, options, done); - }); - } else { - let entry = records[0]; - let personalizationRule = entry.personalizationRule; - let ctx = options.context; - let isBeforeRemote = options.isBeforeRemote; - personalize(ctx, isBeforeRemote, personalizationRule, data, function (err) { - if (err) { - return done(err); - } - checkRelationAndRecurse(Model, data, options, function (err) { - if (err) { - return done(err); - } - checkPropsAndRecurse(Model, data, options, done); - }); - }); - } - } else { - done(); + + let execute = function ServicePersonalizationExecute(records) { + if (records.length === 0) { + checkRelationAndRecurse(Model, data, options, function (err) { + if (err) { + return done(err); } + checkPropsAndRecurse(Model, data, options, done); + }); + } else { + let entry = records[0]; + let personalizationRule = entry.personalizationRule; + let ctx = options.context; + let isBeforeRemote = options.isBeforeRemote; + personalize(ctx, isBeforeRemote, personalizationRule, data, function (err) { + if (err) { + return done(err); + } + checkRelationAndRecurse(Model, data, options, function (err) { + if (err) { + return done(err); + } + checkPropsAndRecurse(Model, data, options, done); + }); }); } - done(); - }); + }; + + if (isRemoteMethodAllowed(ctx, records)) { + return execute(records); + } + nextTick(done); } let PersonalizationRule = null; @@ -1032,7 +1029,7 @@ function init(app) { }); } -function doMethodGating(ctx, currentPersonalizationRecords, cb) { +function isRemoteMethodAllowed(ctx, currentPersonalizationRecords) { let { _personalizationCache: { info }} = ctx; let firstRecord = currentPersonalizationRecords[0]; @@ -1049,7 +1046,7 @@ function doMethodGating(ctx, currentPersonalizationRecords, cb) { //! when * // allow only static calls allowFlag = info.isStatic; - } else if(isPattern && !isPrototype && methodName === DOUBLE_STAR) { + } else if (isPattern && !isPrototype && methodName === DOUBLE_STAR) { //! ** - allow everthing allowFlag = true; } else if (isPattern && !isPrototype && hasDot && methodName === STAR_DOT_STAR) { @@ -1071,93 +1068,47 @@ function doMethodGating(ctx, currentPersonalizationRecords, cb) { // TODO: consider adding an invert flag to invert the allowFlag, // for e.g. disallow only findById, allow others, // or disallow a custom remote method - - return nextTick(() => cb(allowFlag)); + return allowFlag; } - // Assume all allowed for now - nextTick(() => cb(true)); + return true; } -function doRoleGating(ctx, records, cb) { - // Assume all allowed for now - - //! we need to extract info from context cache - nextTick(() => cb(true)); -} - -function getValidStaticMethod(info, isBeforeRemote) { - let { modelName, methodName } = info; - let Model = loopback.findModel(modelName); - if (Model[methodName]) { - return { isValid: true }; - } - return { isValid: false }; -} +// function getValidStaticMethod(info, isBeforeRemote) { +// let { modelName, methodName } = info; +// let Model = loopback.findModel(modelName); +// if (Model[methodName]) { +// return { isValid: true }; +// } +// return { isValid: false }; +// } -function getValidInstanceMethod(info, isBeforeRemote) { - let { modelName, methodName } = info; - let Model = loopback.findModel(modelName); - if (Model.prototype[methodName]) { - return { isValid: true }; - } - return { isValid: false }; -} +// function getValidInstanceMethod(info, isBeforeRemote) { +// let { modelName, methodName } = info; +// let Model = loopback.findModel(modelName); +// if (Model.prototype[methodName]) { +// return { isValid: true }; +// } +// return { isValid: false }; +// } function getPersonalizationMeta(ctx, info, isBeforeRemote) { - let { methodName, isStatic, modelName } = info; + let { modelName } = info; let data = null; let applyPersonalizationFlag = true; let theModel = modelName; - // TODO: Assign data based on http method only - no need of switch statement - if (isStatic) { - switch (methodName) { - case 'create': - case 'patchOrCreate': - data = isBeforeRemote ? ctx.req.body : ctx.result; - break; - case 'find': - case 'findById': - case 'findOne': - data = isBeforeRemote ? {} : ctx.result; - break; - default: - //! some static method we missed... - let statInfo = getValidStaticMethod(info, isBeforeRemote); - if (!statInfo.isValid) { - applyPersonalizationFlag = false; - } else { - let httpMethod = ctx.req.method; - if (httpMethod === 'POST' || httpMethod === 'PUT') { - data = isBeforeRemote ? ctx.req.body : ctx.result; - } else { - data = isBeforeRemote ? {} : ctx.result; - } - } - // eslint-disable-next-line no-inline-comments - } // end switch() + let { req } = ctx; + let httpMethod = req.method; + + // TODO: exclude methods such as count, exists from being personalized. + if (httpMethod === 'PUT' || httpMethod === 'POST' || httpMethod === 'PATCH') { + data = isBeforeRemote ? ctx.req.body : ctx.result; } else { - switch (methodName) { - case 'patchAttributes': - data = isBeforeRemote ? ctx.req.body : ctx.result; - break; - default: - let instInfo = getValidInstanceMethod(info, isBeforeRemote); - if (!instInfo.isValid) { - applyPersonalizationFlag = false; - } else { - let httpMethod = ctx.req.method; - if (httpMethod === 'POST' || httpMethod === 'PUT') { - data = isBeforeRemote ? ctx.req.body : ctx.result; - } else { - data = isBeforeRemote ? {} : ctx.result; - } - } - } + data = isBeforeRemote ? {} : ctx.result; } - return { applyPersonalizationFlag, model: theModel, data }; } -function runPersonalizationPipeline(ctx, isBeforeRemote, cb) { + +function performPersonalizations(ctx, isBeforeRemote, cb) { let { _personalizationCache: { info, records } } = ctx; // let { methodName, model, isStatic } = info; let pMeta = getPersonalizationMeta(ctx, info, isBeforeRemote); @@ -1171,15 +1122,15 @@ function runPersonalizationPipeline(ctx, isBeforeRemote, cb) { } nextTick(cb); } -// FIXME: We don't need this method. -function performPersonalizations(ctx, isBeforeRemote, cb) { - runPersonalizationPipeline(ctx, isBeforeRemote, cb); -} -function fetchPersonalizationRoles(ctx, cb) { - //! doing nothing - nextTick(cb); -} +// function performPersonalizations(ctx, isBeforeRemote, cb) { +// runPersonalizationPipeline(ctx, isBeforeRemote, cb); +// } + +// function fetchPersonalizationRoles(ctx, cb) { +// //! doing nothing +// nextTick(cb); +// } /** * fetches the personalization rules @@ -1205,12 +1156,13 @@ function runPersonalizations(ctx, isBeforeRemote, cb) { records, info }; - fetchPersonalizationRoles(ctx, function (err) { - if (err) { - return cb(err); - } - performPersonalizations(ctx, isBeforeRemote, cb); - }); + performPersonalizations(ctx, isBeforeRemote, cb); + // fetchPersonalizationRoles(ctx, function (err) { + // if (err) { + // return cb(err); + // } + // performPersonalizations(ctx, isBeforeRemote, cb); + // }); }); } From 64755c3a120c5519cbfaa989a91ef0213a63ee7b Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 26 May 2020 22:29:34 +0530 Subject: [PATCH 59/80] v2.4.0 - methodName based gating - tests to demonstrate role-based gating - fieldMask support extended to numbers and dates - support for personalizing properties which are model constructors --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e382d1..28b365d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oe-service-personalization", - "version": "2.3.0", + "version": "2.4.0", "description": "oe-cloud modularization project", "engines": { "node": ">=6" From d04856ab739b52688c021c4c4cda5b2e2b3fc65a Mon Sep 17 00:00:00 2001 From: deostroll Date: Wed, 27 May 2020 17:56:24 +0530 Subject: [PATCH 60/80] updated docs, refactored code --- README.md | 160 ++++++++++++++++++++++++++- lib/service-personalizer.js | 212 +++++++++++++++++++++++------------- 2 files changed, 293 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 0e35fe3..ac61d31 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,83 @@ $ npm run grunt-cover - Custom scope - for e.g. for android, or, ios clients - Limiting personalization to apply to a particular remote method +## `PersonalizationRule` model + +This is the framework model is used to store personalization rules. + +```json +{ + "name": "PersonalizationRule", + "base": "BaseEntity", + "plural": "PersonalizationRules", + "description": "Service Personalization metadata", + "idInjection": false, + "strict": true, + "options": { + "validateUpsert": true, + "isFrameworkModel": true + }, + "properties": { + "ruleName": { + "type": "string" + }, + "disabled": { + "type": "boolean", + "default": false + }, + "modelName": { + "type": "string", + "required": true, + "unique": true, + "notin": [ + "PersonalizationRule" + ] + }, + "personalizationRule": { + "type": "object", + "required": true + }, + "methodName" : { + "type": "string", + "default": "**", + "description": "The model methodName this rule should apply to. Should be the methodName (static/instance) or wildcards you specify in a afterRemote()/beforeRemote(). Default '**'" + } + }, + "validations": [], + "relations": {}, + "acls": [], + "methods": {}, + "mixins": {} +} +``` + +### Important properties +1. `modelName` - the target model for which the personalization should apply +2. `personalizationRule` - the json object which stores operations to apply. More in the `How to Use` and `Supported operations` sections +3. `disabled` - boolean flag which instructs framework to apply personalization or not - default _false_ (i.e. apply the personalizations) +4. `methodName` - the method for which personalization has to apply - default `**` - i.e. apply to all static and instance methods. See below for acceptable values. +5. `ruleName` - (_optional_) name for the personalization rule. Used for debugging. +6. `scope` - (_optional_) used to control personalization based on roles +or through http headers (by the api consumers). For e.g. +it can have a value `{ "roles" : ['admin'] }`... +it means, personalization will apply for a +logged-in `admin` user only. + +### Accepted values for `methodName` + +It can accept the following patterns (wildcards and names) +- `**` (_default_) - all static and instance methods +- `*` - only static methods +- `*.*` or `prototype.*` - only instance methods +- **Valid static method name**. It can be standard, +or, a custom static remote method. +- **Valid instance method name**. It can be standard, +or, a custom instance remote method. +E.g. `prototype.foo`, where `foo` is a method +defined on the prototype of the model's constructor. + +See section `Notes on loopback relations` for more information. + ## How to use 1. Install the module to your application @@ -130,7 +207,8 @@ The above example adds a `fieldValueReplace` operation to the `keywords` property of `ProductCatalog` model. Additionally we have provided `scope` for this rule to take effect only when it is specified in the http headers of the request; it -is always a simple key/value pair. +is always a simple key/value pair. See section `Supported operations` +for info about more operations. To have this personalization apply only to a user of a specified role (say to either `tellers` or `agents`), it must be defined as in the below example: @@ -269,6 +347,8 @@ The **fieldMask** operations can be applied to the following data types: Validation will happen for the same at the time of creating the PersonalzationRule record. +### fieldMask for strings + Formal way to specify masking data of type `String` is as follows: ```json @@ -295,6 +375,8 @@ Formal way to specify masking data of type `String` is as follows: } ``` +### fieldMask for numbers + Formal way to specify masking of numbers is as follows: ```json { @@ -323,6 +405,8 @@ Formal way to specify masking of numbers is as follows: > Note: the options are similar to that of `stringMask`. Validation is done to determine if modelNo is of type `Number` +### fieldMask for date + Formal way to specify masking of dates are as follows: ```json { @@ -358,6 +442,30 @@ alternatively specifying a different locale. Acceptable values (`String`) are: For more info about joda-time format visit: https://js-joda.github.io/js-joda/manual/formatting.html#format-patterns +## Operations on objects + +Operations such as `fieldMask`, `fieldValueReplace`, etc can be +applied on properties of type `Object`. + +The path to the nested property can be specified by using +the unicode character `\uFF0E` as seperator. + +Example (test `t31`): + +```json +{ + "modelName": "Customer", + "personalizationRule": { + "fieldReplace": { + "billingAddress\uFF0Estreet": "lane" + } + }, + "scope": { + "device": "android" + } +} +``` + ## Programmatic API To do personalization in a custom remote method, or, in unit @@ -468,6 +576,7 @@ operation. No point in trying to modify `ctx.result` in a the custom functions are stored is configured correctly. 3. Understand how pre-fetch/post-fetch applies to relations and nested data. +4. See section `Note on loopback relations` ## Test Synopsis The following entity structure and relationships assumed for most of the tests. @@ -536,3 +645,52 @@ $ node test/server.js It is also recommended to attach an explorer component (such as loopback-component-explorer) when running as a standalone application. + +## Note on loopback relations + +The standard names for instance methods +commonly refer to a loopback relation. The names +are governed by the loopback framework. The pattern +goes something like this: + +E.g. consider a simple Customer/Order relationship. +Assume the following description of a `Customer` +model: + +* `Customer` + - relations + - `orders` + - type: `hasMany` + - model: `Order` + +Assumes a client invokes the following api (GET): +``` +http://localhost:3000/api/Customers/2/orders +``` +The loopback framework creates a `methodString` on the `HttpContext` +object as follows: +``` +Customer.prototype.__get__orders +``` + +If the requirement is such that, only _this_ api call +should be personalized, do the following: + +1. Create an _empty_ personalization record on `Customer` +```json +{ + "modelName": "Customer", + "personalizationRule": {}, + "methodName": "prototype.__get__orders" +} +``` +2. Create a personalization record for `Order` + +This should ensure the required result in the desired remote call. + +> Note: both models should have `ServicePersonalizationMixin` enabled + +Therefore, when writing custom methods that operate over +remote, it is better not to collude to loopback's +internal naming standards. + diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 7cc0b79..7aa17be 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -334,13 +334,16 @@ const utils = { } }; // end - task-utils - utility functions + +// begin task-p13nFunctions + const p13nFunctions = { /** * Does field replace. * - * PreApplication: Yes - * PostApplication: Yes + * Pre-Fetch: Yes + * Post-Fetch: Yes * * For pre-appilication a reverse rule is applied. * @param {object} replacements @@ -393,8 +396,8 @@ const p13nFunctions = { /** * does a field value replace. * - * PreApplication: no - * PostApplication: yes + * Pre-Fetch: no + * Post-Fetch: yes * * @param {object} replacements - replacement rule */ @@ -425,8 +428,8 @@ const p13nFunctions = { * where the actual sorting is applied. (Provided the * underlying datasource supports it) * - * PreApplication: Yes - * PostApplication: No + * Pre-Fetch: Yes + * Post-Fetch: No * * @param {HttpContext} ctx - the context object * @param {object} instruction - the personalization sort rule @@ -487,8 +490,8 @@ const p13nFunctions = { * @param {CallContext} ctx - the request context * @param {object} instructions - personalization rule object * - * PreApplication: No - * PostApplication: Yes + * Pre-Fetch: No + * Post-Fetch: Yes * * Example Rule - Masks a "category field" * @@ -543,8 +546,8 @@ const p13nFunctions = { /** * add a filter to Model.find() * - * PreApplication: yes - * PostApplication: no + * Pre-Fetch: yes + * Post-Fetch: no * * @param {HttpContext} ctx - http context * @param {object} instruction - personalization filter rule @@ -637,8 +640,8 @@ const p13nFunctions = { * adds the post custom function added via * config.json to the after remote * - * PreApplication: no - * PostApplication: yes + * Pre-Fetch: no + * Post-Fetch: yes * @param {context} ctx * @param {object} instruction * @returns {function} function that applies custom function (async iterator function) @@ -652,12 +655,14 @@ const p13nFunctions = { } }; +// end task-p13nFunctions + /** * apply the personalization on the given data * @param {object} ctx - http context object * @param {bool} isBeforeRemote - flag indicating if this is invoked in a beforeRemote stage * @param {object} instructions - object containing personalization instructions - * @param {instance} data - data from the context. Could either be a List or a single model instance + * @param {*} data - data from the context. Could either be a List or a single model instance * @param {function} done - the callback which receives the new data. First argument of function is an error object * @returns {void} nothing */ @@ -715,7 +720,16 @@ function personalize(ctx, isBeforeRemote, instructions, data, done) { }); } - +/** + * Checks if the model has relations, and, + * also determines if data consists of + * relational data, then recursively personalizes + * @param {function} Model - loopback model constructor + * @param {*} data - data to personalize - object or array + * @param {object} personalizationOptions - standard options passed to applyServicePersonalization() + * @param {callback} done - to signal completion of task + * @returns {undefined} - nothing + */ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { let { settings: { relations }, definition: { name } } = Model; let { isBeforeRemote, context } = personalizationOptions; @@ -782,6 +796,17 @@ function fetchPersonalizationRecords(ctx, forModel, cb) { PersonalizationRule.find(filter, callContext, cb); } +/** + * Iterates properties of the lb model, if it + * is a model constructor, applies personalization + * recursively + * + * @param {function} Model - the loopback model + * @param {*} data - the data to personalize - object/array + * @param {object} options - standard options passed to applyServicePersonalization() + * @param {callback} cb - signal completion of activity + * @returns {undefined} - nothing + */ function checkPropsAndRecurse(Model, data, options, cb) { let ctx = options.context; let { isBeforeRemote } = options; @@ -892,6 +917,16 @@ function checkPropsAndRecurse(Model, data, options, cb) { nextTick(done); } +/** + * Personalization helper function. + * + * @param {string} modelName - name of the model + * @param {*} data - the data to personalize - object/array + * @param {[object]} records - the personalization records + * @param {object} options - the personalization options + * @param {callback} cb - to signal completion of task + * @return {undefined} - nothing + */ function applyServicePersonalization(modelName, data, records, options, cb) { let Model = loopback.findModel(modelName); let ctx = options.context; @@ -1073,69 +1108,92 @@ function isRemoteMethodAllowed(ctx, currentPersonalizationRecords) { return true; } -// function getValidStaticMethod(info, isBeforeRemote) { -// let { modelName, methodName } = info; -// let Model = loopback.findModel(modelName); -// if (Model[methodName]) { -// return { isValid: true }; -// } -// return { isValid: false }; -// } - -// function getValidInstanceMethod(info, isBeforeRemote) { -// let { modelName, methodName } = info; -// let Model = loopback.findModel(modelName); -// if (Model.prototype[methodName]) { -// return { isValid: true }; -// } -// return { isValid: false }; -// } - +/** + * Grabs data either from ctx.req.body + * or ctx.result appropriately + * + * @param {HttpContext} ctx - the http context + * @param {object} info - parsed context method string + * @param {boolean} isBeforeRemote - flag denoting the stage - beforeRemote/afterRemote + * @returns {object} - the model name and data are contained here + */ function getPersonalizationMeta(ctx, info, isBeforeRemote) { let { modelName } = info; let data = null; - let applyPersonalizationFlag = true; + let theModel = modelName; let { req } = ctx; let httpMethod = req.method; - // TODO: exclude methods such as count, exists from being personalized. if (httpMethod === 'PUT' || httpMethod === 'POST' || httpMethod === 'PATCH') { data = isBeforeRemote ? ctx.req.body : ctx.result; } else { data = isBeforeRemote ? {} : ctx.result; } - return { applyPersonalizationFlag, model: theModel, data }; + return { model: theModel, data }; } +/** + * Main entry point for personalizations + * via remote method call + * + * @param {httpContext} ctx - context + * @param {boolean} isBeforeRemote - flag denoting the stage - beforeRemote/afterRemote + * @param {callback} cb - to signal the personalizations are complete. + */ function performPersonalizations(ctx, isBeforeRemote, cb) { - let { _personalizationCache: { info, records } } = ctx; - // let { methodName, model, isStatic } = info; + let { _personalizationCache: { records, info } } = ctx; + let pMeta = getPersonalizationMeta(ctx, info, isBeforeRemote); - if (pMeta.applyPersonalizationFlag) { - let { data, model } = pMeta; - let pOptions = { - isBeforeRemote, - context: ctx - }; - return applyServicePersonalization(model, data, records, pOptions, cb); - } - nextTick(cb); + + let options = { isBeforeRemote, context: ctx }; + + applyServicePersonalization(pMeta.model, pMeta.data, records, options, cb); } -// function performPersonalizations(ctx, isBeforeRemote, cb) { -// runPersonalizationPipeline(ctx, isBeforeRemote, cb); -// } +const ALLOWED_INSTANCE_METHOD_NAMES = ['get', 'create', 'findById', 'updateById']; -// function fetchPersonalizationRoles(ctx, cb) { -// //! doing nothing -// nextTick(cb); -// } +/** + * This methods detects some of loopback's + * standard methods like exists, count and + * exempts such requests to be personalized + * + * It also exempts requests which are + * DELETE or HEAD + * + * @param {HttpContext} ctx - the http context object + * @param {object} info - the parsed represetation of the context's methodString + * @returns {boolean} - flag indicating if we can apply personalization + */ +function getCanApplyFlag(ctx, info) { + let { methodName, isStatic } = info; + let { req } = ctx; + let httpMethod = req.method; + + let isAllowedPrototype = name => { + let startIdx = 2; + let endIdx = name.indexOf('_', startIdx); + let extractedMethod = name.substr(startIdx, endIdx - startIdx); + return ALLOWED_INSTANCE_METHOD_NAMES.some(value => value === extractedMethod); + }; + + if (httpMethod === 'DELETE' || httpMethod === 'HEAD') { + return false; + } + + if (isStatic && methodName === 'exists') { + return false; + } + + if (!isStatic && methodName.startsWith('__') && !isAllowedPrototype(methodName)) { + return false; + } + + return true; +} /** - * fetches the personalization rules - * and checks against methodName if - * personalizations are applicable + * Initializes the pipeline for personalization * * @param {HttpContext} ctx - the http context object * @param {boolean} isBeforeRemote - flag denoting phase of the request @@ -1143,30 +1201,28 @@ function performPersonalizations(ctx, isBeforeRemote, cb) { * @returns {undefined} - nothing */ function runPersonalizations(ctx, isBeforeRemote, cb) { - if (isBeforeRemote) { - //! we have not fetched the personalization record(s) + let {methodString} = ctx; + let info = parseMethodString(methodString); + let canApply = null; - // fetch the personalization record and store it in ctx - let info = parseMethodString(ctx.methodString); - return fetchPersonalizationRecords(ctx, info.model, function (err, records) { - if (err) { - return cb(err); - } - ctx._personalizationCache = { - records, - info - }; - performPersonalizations(ctx, isBeforeRemote, cb); - // fetchPersonalizationRoles(ctx, function (err) { - // if (err) { - // return cb(err); - // } - // performPersonalizations(ctx, isBeforeRemote, cb); - // }); - }); + if (isBeforeRemote) { + canApply = getCanApplyFlag(ctx, info); + ctx._personalizationCache = { canApply }; + if (canApply) { + return fetchPersonalizationRecords(ctx, info.model, function (err, records) { + if (err) { + return cb(err); + } + ctx._personalizationCache = Object.assign(ctx._personalizationCache, { info, records }); + performPersonalizations(ctx, isBeforeRemote, cb); + }); + } + } else if (ctx._personalizationCache.canApply) { + return performPersonalizations(ctx, isBeforeRemote, cb); } - - performPersonalizations(ctx, isBeforeRemote, cb); + let stage = isBeforeRemote ? 'beforeRemote' : 'afterRemote'; + log.debug(ctx, `${stage}: Avoided personalization -> ${methodString}`); + nextTick(cb); } function performServicePersonalizations(modelName, data, options, cb) { From 8d30eff985283012495661ae5e5f025488571f5a Mon Sep 17 00:00:00 2001 From: deostroll Date: Wed, 27 May 2020 17:56:51 +0530 Subject: [PATCH 61/80] v2.4.1 - exempting loopback standard methods from being personalized - code cleanup and comments --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 28b365d..8af4595 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oe-service-personalization", - "version": "2.4.0", + "version": "2.4.1", "description": "oe-cloud modularization project", "engines": { "node": ">=6" From 27119a4c47bf7a7cab3486a65f7ee579f6b0c3e1 Mon Sep 17 00:00:00 2001 From: deostroll Date: Mon, 6 Jul 2020 17:06:52 +0530 Subject: [PATCH 62/80] fixed bug with implicit relation http calls --- lib/service-personalizer.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 7aa17be..5f3625f 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -734,6 +734,8 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { let { settings: { relations }, definition: { name } } = Model; let { isBeforeRemote, context } = personalizationOptions; let prefix = isBeforeRemote ? 'beforeRemote' : 'afterRemote'; + let personalizationCache = context._personalizationCache; + let rootInfo = personalizationCache && personalizationCache.info; if (relations) { // Object.entries(Model.setti) @@ -743,7 +745,19 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { let relData; let relModel = relation.model; let applyFlag = false; - if (Array.isArray(data)) { + let methodName = rootInfo && rootInfo.methodName; + + // begin - implicitRelationCheck - checking for implicit relation call + if(rootInfo && !rootInfo.isStatic && methodName.startsWith('__') && methodName.includes(relationName)) { + //! this is an implicit relation http call + // E.g GET /api/Customers/2/Orders + // Here data will be that of Order + // model. + relData = data; + applyFlag = true; + } + // end - implicitRelationCheck + else if (Array.isArray(data)) { relData = data.reduce((carrier, record) => { if (record.__data && typeof record.__data[relationName] !== 'undefined') { carrier.push(record.__data[relationName]); @@ -781,6 +795,7 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { log.debug(context, `${prefix}: (leave) processing relation "${relationName}"/"${name}" - skipped`); nextTick(done); }; + return async.eachSeries(relationItems, relationsIterator, done); } @@ -845,6 +860,9 @@ function checkPropsAndRecurse(Model, data, options, cb) { }; let extractData = (key, data) => { + if(typeof data === 'object' && Array.isArray(data)) { + return data.map(item => extractData(key, item)) + } if (data.__data) { return data.__data[key]; } @@ -1209,7 +1227,7 @@ function runPersonalizations(ctx, isBeforeRemote, cb) { canApply = getCanApplyFlag(ctx, info); ctx._personalizationCache = { canApply }; if (canApply) { - return fetchPersonalizationRecords(ctx, info.model, function (err, records) { + return fetchPersonalizationRecords(ctx, info.modelName, function (err, records) { if (err) { return cb(err); } From c17d25c8bf66157207c98c7e2cd47721f83f6ec8 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 7 Jul 2020 19:58:17 +0530 Subject: [PATCH 63/80] - added namespace for exposing api - lint fixes --- lib/api.js | 25 +++++++++++++++++++++++++ lib/service-personalizer.js | 33 ++++++++++++++++++++++++--------- 2 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 lib/api.js diff --git a/lib/api.js b/lib/api.js new file mode 100644 index 0000000..d463cb0 --- /dev/null +++ b/lib/api.js @@ -0,0 +1,25 @@ +const { performServicePersonalizations, applyServicePersonalization } = require('./service-personalizer'); +module.exports = { + /** + * Standard api for personalization + */ + performServicePersonalizations, + + /** + * Api for personalization. Rules can + * be manually passed as arguments to + * this function. + * + * @param {string} modelName - the model name. + * @param {*} data - object or array + * @param {array} personalizationRecords - the personalization rule as an array. + * @param {object} options - personalization options + * @param {function} done - callback to signal completion. Takes only one argument - error. + * @returns {undefined} - nothing + */ + applyServicePersonalization: function applyServicePersonalizationWrapper(modelName, data, personalizationRecords, options, done) { + let { context } = options; + context._personalizationCache = {}; + applyServicePersonalization(modelName, data, personalizationRecords, options, done); + } +}; diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 5f3625f..2e6cb98 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -748,16 +748,15 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { let methodName = rootInfo && rootInfo.methodName; // begin - implicitRelationCheck - checking for implicit relation call - if(rootInfo && !rootInfo.isStatic && methodName.startsWith('__') && methodName.includes(relationName)) { + if (rootInfo && !rootInfo.isStatic && methodName.startsWith('__') && methodName.includes(relationName)) { //! this is an implicit relation http call // E.g GET /api/Customers/2/Orders // Here data will be that of Order // model. - relData = data; - applyFlag = true; - } - // end - implicitRelationCheck - else if (Array.isArray(data)) { + relData = data; + applyFlag = true; + // end - implicitRelationCheck + } else if (Array.isArray(data)) { relData = data.reduce((carrier, record) => { if (record.__data && typeof record.__data[relationName] !== 'undefined') { carrier.push(record.__data[relationName]); @@ -795,7 +794,7 @@ function checkRelationAndRecurse(Model, data, personalizationOptions, done) { log.debug(context, `${prefix}: (leave) processing relation "${relationName}"/"${name}" - skipped`); nextTick(done); }; - + return async.eachSeries(relationItems, relationsIterator, done); } @@ -860,8 +859,8 @@ function checkPropsAndRecurse(Model, data, options, cb) { }; let extractData = (key, data) => { - if(typeof data === 'object' && Array.isArray(data)) { - return data.map(item => extractData(key, item)) + if (typeof data === 'object' && Array.isArray(data)) { + return data.map(item => extractData(key, item)); } if (data.__data) { return data.__data[key]; @@ -1243,6 +1242,22 @@ function runPersonalizations(ctx, isBeforeRemote, cb) { nextTick(cb); } +/** + * Personalizes data by mutating it. + * + * The personalization rules are queried + * using the modelName. + * + * @param {string} modelName - name of model + * @param {*} data - Array or Object + * @param {ServicePersonalizationOptions} options - service personalization options + * Two properties: + * - isBeforeRemote - always false + * - context - the HttpContext object + * @param {function} cb - the function to signal completion + * Has only one arguments - error + * @returns {undefined} - nothing + */ function performServicePersonalizations(modelName, data, options, cb) { let ctx = options.context; log.debug(ctx, `performServicePersonalizations: (enter) model -> ${modelName}`); From 74b9214db5ec32587b15493ac91205d20afc2735 Mon Sep 17 00:00:00 2001 From: deostroll Date: Tue, 7 Jul 2020 20:35:44 +0530 Subject: [PATCH 64/80] fixed bug in checkPropsRecurse --- lib/service-personalizer.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index 2e6cb98..cf03164 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -860,7 +860,7 @@ function checkPropsAndRecurse(Model, data, options, cb) { let extractData = (key, data) => { if (typeof data === 'object' && Array.isArray(data)) { - return data.map(item => extractData(key, item)); + return _.flatten(data.map(item => extractData(key, item))); } if (data.__data) { return data.__data[key]; From d4e059b3ddb928cd88c38909d7a40000fe591b1b Mon Sep 17 00:00:00 2001 From: deostroll Date: Wed, 8 Jul 2020 11:07:09 +0530 Subject: [PATCH 65/80] updated docs and api usage --- README.md | 122 ++++++++++++++++----- test/common/models/pseudo-product-owner.js | 2 +- 2 files changed, 96 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index ac61d31..1270f57 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ it can have a value `{ "roles" : ['admin'] }`... it means, personalization will apply for a logged-in `admin` user only. -### Accepted values for `methodName` +### Acceptable values for `methodName` It can accept the following patterns (wildcards and names) - `**` (_default_) - all static and instance methods @@ -132,21 +132,34 @@ See section `Notes on loopback relations` for more information. ## How to use -1. Install the module to your application +#### 1. Install the module to your application ``` npm install oe-service-personalization ``` -2. In your project's `app-list.json` file add the following config: +#### 2. Add config to your project's `app-list.json`: ``` { "path": "oe-service-personalization", + "enabled": true + } +``` + +If you require role-based service personalization, add `oe-personalization` depedency prior to `oe-service-personalization` +``` + { + "path": "oe-personalization", "enabled": true, "autoEnableMixins": true + }, + { + "path": "oe-service-personalization", + "enabled": true } ``` -3. Add `ServicePersonalizationMixin` mixin to the model declaration. + +#### 3. Add `ServicePersonalizationMixin` mixin to the model declaration. Example: ```json @@ -245,7 +258,7 @@ Example: (`config.json` snippet): "customFunctionPath": "D:\\Repos\\oecloud.io\\oe-service-personalization_master\\test\\customFunction" } ``` -Example: (via environment variable): +Example: (via environment variable) (bash prompt): ```bash $ export custom_function_path="/project/customFuncDir" ``` @@ -259,7 +272,7 @@ to its `afterRemote()` and `beforeRemote()` which will do the personalization. The personalization records, stored in `PersonalizationRule`, -are queried according to scope, and, model participating, in +are queried according to scope, and, model participating in the remote call. The information required for this is obtained from the `HttpContext` which is an argument in the callback of the aforementioned methods. @@ -273,12 +286,13 @@ one that participates in the remote call. of the root model, which are model constructors Personalization can happen through code also via `performServicePersonalization()` -api call. They follow the same process mentioned above, +or, `applyServicePersonalization()` +api calls. They follow the same process mentioned above, however, there are a few limitations. Notably, those operations which are meant to apply `post-fetch` will be honoured, and, thus personalized. -More details on `pre-fetch` and `post-fetch` in sections below. (Significance of pre-fetch & post-fetch) +More details on `pre-fetch` and `post-fetch` in sections below. (_Significance of pre-fetch & post-fetch_) ## Supported operations @@ -345,7 +359,9 @@ The **fieldMask** operations can be applied to the following data types: - Date Validation will happen for the same at the time of creating -the PersonalzationRule record. +the PersonalzationRule record, i.e., type validation on the +field will take place against the same field in the target +model. ### fieldMask for strings @@ -468,14 +484,45 @@ Example (test `t31`): ## Programmatic API -To do personalization in a custom remote method, or, in unit -tests you need the `performServicePersonalizations()` api. +There are two flavours of api wrt personalization. Both are available in the following namespace: + +``` +oe-service-personalization/lib/api +``` + +### 1. Using model name, and, model data + +Use `performServicePersonalizations()` api. + +The signature is as follows: + +```js +/** + * Personalizes data by mutating it. + * + * The personalization rules are queried + * using the modelName. + * + * @param {string} modelName - name of model + * @param {*} data - Array or Object + * @param {ServicePersonalizationOptions} options - service personalization options + * Two properties: + * - isBeforeRemote - always false + * - context - the HttpContext object + * @param {function} cb - the function to signal completion + * Has only one arguments - error + * @returns {undefined} - nothing + */ +function performServicePersonalizations(modelName, data, options, cb) { + // ... +} +``` Example ```JavaScript -const { performServicePersonalizations } = require('./../../../lib/service-personalizer'); // or require('oe-service-personalization/lib/service-personalizer'); +const { performServicePersonalizations } = require('./../../../lib/api'); // or require('oe-service-personalization/lib/api'); const loopback = require('loopback'); module.exports = function(PseudoProductOwner) { @@ -541,6 +588,32 @@ module.exports = function(PseudoProductOwner) { } ``` +> Note: the `options` in the remote method function +definition, in the example above is, `HttpContext` + +#### 2. Using model name, data, and, personalization rules + +Use the `applyServicePersonalization()` api + +Signature: +```js +/** + * Api for personalization. Rules can + * be manually passed as arguments to + * this function. + * + * @param {string} modelName - the model name. + * @param {*} data - object or array + * @param {array} personalizationRecords - the personalization rule as an array. + * @param {object} options - personalization options + * @param {function} done - callback to signal completion. Takes only one argument - error. + * @returns {undefined} - nothing + */ + function applyServicePersonalization(modelName, data, personalizationRecords, options, done) { + // ... + } +``` + ## Significance of pre-fetch/post-fetch operations It impacts how personalizations is done for relations, and, nested @@ -575,8 +648,10 @@ operation. No point in trying to modify `ctx.result` in a `preCustomFunction`. Also ensure path to the directory where the custom functions are stored is configured correctly. -3. Understand how pre-fetch/post-fetch applies to relations and nested data. -4. See section `Note on loopback relations` +3. Understand how pre-fetch/post-fetch applies to relations and nested data. See section `Significance of pre-fetch/post-fetch operations` + +4. See section `Notes on loopback relations` + ## Test Synopsis The following entity structure and relationships assumed for most of the tests. @@ -674,23 +749,16 @@ Customer.prototype.__get__orders ``` If the requirement is such that, only _this_ api call -should be personalized, do the following: - -1. Create an _empty_ personalization record on `Customer` -```json -{ - "modelName": "Customer", - "personalizationRule": {}, - "methodName": "prototype.__get__orders" -} -``` -2. Create a personalization record for `Order` +should be personalized, _create a personalization record_ for `Order` This should ensure the required result in the desired remote call. > Note: both models should have `ServicePersonalizationMixin` enabled -Therefore, when writing custom methods that operate over -remote, it is better not to collude to loopback's +A developer always has the freedom to define a non-static +instance method with the same name, and, still have the +relation defined. One must always refrain from doing this. + +Do not to collude with loopback's internal naming standards. diff --git a/test/common/models/pseudo-product-owner.js b/test/common/models/pseudo-product-owner.js index 67657f4..7c7dd6d 100644 --- a/test/common/models/pseudo-product-owner.js +++ b/test/common/models/pseudo-product-owner.js @@ -1,4 +1,4 @@ -const { performServicePersonalizations } = require('./../../../lib/service-personalizer'); // or require('oe-service-personalization/lib/service-personalizer'); +const { performServicePersonalizations } = require('./../../../lib/api'); // or require('oe-service-personalization/lib/api'); const loopback = require('loopback'); module.exports = function(PseudoProductOwner) { From ec4f3073d7c0ce9d187514f9cbe2d309327998c0 Mon Sep 17 00:00:00 2001 From: deostroll Date: Wed, 8 Jul 2020 11:13:34 +0530 Subject: [PATCH 66/80] updated docs --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1270f57..bf119dd 100644 --- a/README.md +++ b/README.md @@ -195,7 +195,7 @@ Example: } ``` -4. Insert rules into the `PersonalizationRule` model. +#### 4. Insert rules into the `PersonalizationRule` model. Example: ```json @@ -246,7 +246,7 @@ Example: ``` -5. _(Optional)_ If there are some custom function based +##### 5. _(Optional)_ If there are some custom function based operations, add the path in the application's `config.json` file. Alternatively, set the environment variable: `custom_function_path` From 9c52e97d24c422e26fb3057708e20a6452f2d8f9 Mon Sep 17 00:00:00 2001 From: deostroll Date: Wed, 8 Jul 2020 11:14:19 +0530 Subject: [PATCH 67/80] updated docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bf119dd..baded72 100644 --- a/README.md +++ b/README.md @@ -246,7 +246,7 @@ Example: ``` -##### 5. _(Optional)_ If there are some custom function based +#### 5. _(Optional)_ If there are some custom function based operations, add the path in the application's `config.json` file. Alternatively, set the environment variable: `custom_function_path` From 7a5274b2dd2fb629396ced3f7e67d748d32d284e Mon Sep 17 00:00:00 2001 From: deostroll Date: Wed, 8 Jul 2020 15:29:20 +0530 Subject: [PATCH 68/80] updated docs --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index baded72..8149ed3 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,12 @@ See section `Notes on loopback relations` for more information. ## How to use +This documents a general usage pattern - achieving +personalization via call to the http endpoint +of the model. + +The same can be done through code. Refer to `Programmatic Api` section for the same. + #### 1. Install the module to your application ``` @@ -246,8 +252,9 @@ Example: ``` -#### 5. _(Optional)_ If there are some custom function based -operations, add the path in the application's `config.json` +#### 5. _(Optional)_ Configure custom functions path + +If there are custom function operations, add the path in the application's `config.json` file. Alternatively, set the environment variable: `custom_function_path` From 1d5f153862fd394a7d6a52b3b9fa22115c5643a2 Mon Sep 17 00:00:00 2001 From: deostroll Date: Wed, 8 Jul 2020 19:59:30 +0530 Subject: [PATCH 69/80] updated docs and bumped version --- README.md | 32 +++++++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 30 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 8149ed3..36550b6 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,33 @@ is frequently accessed. Such information can be used to improve the user experience on that platform. However, this is not in scope of this document. -## dependency +## Table Of Contents +- [oe-service-personalization](#oe-service-personalization) + * [Table Of Contents](#table-of-contents) + * [Dependency](#dependency) + * [Install and test](#install-and-test) + * [Main features](#main-features) + * [`PersonalizationRule` model](#-personalizationrule--model) + + [Important properties](#important-properties) + + [Acceptable values for `methodName`](#acceptable-values-for--methodname-) + * [How to use](#how-to-use) + * [Working Principle](#working-principle) + * [Supported operations](#supported-operations) + * [**fieldMask** options](#--fieldmask---options) + + [fieldMask for strings](#fieldmask-for-strings) + + [fieldMask for numbers](#fieldmask-for-numbers) + + [fieldMask for date](#fieldmask-for-date) + * [Operations on objects](#operations-on-objects) + * [Programmatic API](#programmatic-api) + + [1. Using model name, and, model data](#1-using-model-name--and--model-data) + + [2. Using model name, data, and, personalization rules](#2-using-model-name--data--and--personalization-rules) + * [Significance of pre-fetch/post-fetch operations](#significance-of-pre-fetch-post-fetch-operations) + * [Points to consider](#points-to-consider) + * [Test Synopsis](#test-synopsis) + * [Note on loopback relations](#note-on-loopback-relations) + +## Dependency + * oe-cloud * oe-logger * oe-expression @@ -598,7 +624,7 @@ module.exports = function(PseudoProductOwner) { > Note: the `options` in the remote method function definition, in the example above is, `HttpContext` -#### 2. Using model name, data, and, personalization rules +### 2. Using model name, data, and, personalization rules Use the `applyServicePersonalization()` api @@ -766,6 +792,6 @@ A developer always has the freedom to define a non-static instance method with the same name, and, still have the relation defined. One must always refrain from doing this. -Do not to collude with loopback's +Do not collude with loopback's internal naming standards. diff --git a/package.json b/package.json index 8af4595..8025d70 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oe-service-personalization", - "version": "2.4.1", + "version": "2.5.0", "description": "oe-cloud modularization project", "engines": { "node": ">=6" From 0800874d7e7dde5a8eace401ccfd066205fada7c Mon Sep 17 00:00:00 2001 From: deostroll Date: Thu, 9 Jul 2020 11:16:15 +0530 Subject: [PATCH 70/80] checkPropsAndRecurse: disabling non-model constructors from personalization --- lib/service-personalizer.js | 38 ++++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/lib/service-personalizer.js b/lib/service-personalizer.js index cf03164..c6bcdfa 100755 --- a/lib/service-personalizer.js +++ b/lib/service-personalizer.js @@ -898,34 +898,42 @@ function checkPropsAndRecurse(Model, data, options, cb) { if (!isAnEmbedsOneRelationProp) { let unpersonalizedData = null; let modelCtorName = null; + let modelCtor = null; + let applyFlag = true; - // begin - task02 - extract data + // begin - task02 - extract data and set + // applyFlag if we encounter a model + // constructor if (typeof type === 'function') { - //! this is a plain model constructor + //! this could be a plain model constructor modelCtorName = type.name; + modelCtor = type; unpersonalizedData = extractData(key, data); } else { - //! this is an array of model constructors + //! this could be an array of model constructors //! Only one model constructor available(?) - let modelCtor = type[0]; + modelCtor = type[0]; modelCtorName = modelCtor.name; unpersonalizedData = extractData(key, data); - // eslint-disable-next-line no-inline-comments - }// end if-else block - if(typeof type === 'function') + } + // end if-else block - if(typeof type === 'function') - // end - task02 - extract data + applyFlag = typeof modelCtor.modelName === 'string'; - return fetchPersonalizationRecords(ctx, modelCtorName, function (err, records) { - if (err) { - return done(err); - } - applyServicePersonalization(modelCtorName, unpersonalizedData, records, options, done); - }); - // eslint-disable-next-line no-inline-comments - } // end if-block if(!isAnEmbedsOneRelationProp) + // end - task02 + if (applyFlag) { + return fetchPersonalizationRecords(ctx, modelCtorName, function (err, records) { + if (err) { + return done(err); + } + applyServicePersonalization(modelCtorName, unpersonalizedData, records, options, done); + }); + } + } + // end if-block if(!isAnEmbedsOneRelationProp) nextTick(done); }, function (err) { done(err); From 16b87938b7297ec787d342e4222691b6fd6db839 Mon Sep 17 00:00:00 2001 From: deostroll Date: Thu, 9 Jul 2020 11:24:18 +0530 Subject: [PATCH 71/80] v2.5.1 - fixed bug in checkPropsAndRecurse --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8025d70..4ddf1f8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oe-service-personalization", - "version": "2.5.0", + "version": "2.5.1", "description": "oe-cloud modularization project", "engines": { "node": ">=6" From 970a4213b28564f3eee6f465fa623b385dcc9a8c Mon Sep 17 00:00:00 2001 From: deostroll Date: Thu, 9 Jul 2020 11:42:18 +0530 Subject: [PATCH 72/80] toc --- README.md | 25 +------------------------ 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/README.md b/README.md index 36550b6..92b6c8c 100644 --- a/README.md +++ b/README.md @@ -26,30 +26,7 @@ is frequently accessed. Such information can be used to improve the user experience on that platform. However, this is not in scope of this document. -## Table Of Contents -- [oe-service-personalization](#oe-service-personalization) - * [Table Of Contents](#table-of-contents) - * [Dependency](#dependency) - * [Install and test](#install-and-test) - * [Main features](#main-features) - * [`PersonalizationRule` model](#-personalizationrule--model) - + [Important properties](#important-properties) - + [Acceptable values for `methodName`](#acceptable-values-for--methodname-) - * [How to use](#how-to-use) - * [Working Principle](#working-principle) - * [Supported operations](#supported-operations) - * [**fieldMask** options](#--fieldmask---options) - + [fieldMask for strings](#fieldmask-for-strings) - + [fieldMask for numbers](#fieldmask-for-numbers) - + [fieldMask for date](#fieldmask-for-date) - * [Operations on objects](#operations-on-objects) - * [Programmatic API](#programmatic-api) - + [1. Using model name, and, model data](#1-using-model-name--and--model-data) - + [2. Using model name, data, and, personalization rules](#2-using-model-name--data--and--personalization-rules) - * [Significance of pre-fetch/post-fetch operations](#significance-of-pre-fetch-post-fetch-operations) - * [Points to consider](#points-to-consider) - * [Test Synopsis](#test-synopsis) - * [Note on loopback relations](#note-on-loopback-relations) +[[_TOC_]] ## Dependency From e1cd538a82594a35e5369159570b139d309e63f3 Mon Sep 17 00:00:00 2001 From: deostroll Date: Thu, 9 Jul 2020 11:53:06 +0530 Subject: [PATCH 73/80] updated docs --- README.md | 44 ++++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 36550b6..b2aa1be 100644 --- a/README.md +++ b/README.md @@ -28,28 +28,28 @@ scope of this document. ## Table Of Contents - [oe-service-personalization](#oe-service-personalization) - * [Table Of Contents](#table-of-contents) - * [Dependency](#dependency) - * [Install and test](#install-and-test) - * [Main features](#main-features) - * [`PersonalizationRule` model](#-personalizationrule--model) - + [Important properties](#important-properties) - + [Acceptable values for `methodName`](#acceptable-values-for--methodname-) - * [How to use](#how-to-use) - * [Working Principle](#working-principle) - * [Supported operations](#supported-operations) - * [**fieldMask** options](#--fieldmask---options) - + [fieldMask for strings](#fieldmask-for-strings) - + [fieldMask for numbers](#fieldmask-for-numbers) - + [fieldMask for date](#fieldmask-for-date) - * [Operations on objects](#operations-on-objects) - * [Programmatic API](#programmatic-api) - + [1. Using model name, and, model data](#1-using-model-name--and--model-data) - + [2. Using model name, data, and, personalization rules](#2-using-model-name--data--and--personalization-rules) - * [Significance of pre-fetch/post-fetch operations](#significance-of-pre-fetch-post-fetch-operations) - * [Points to consider](#points-to-consider) - * [Test Synopsis](#test-synopsis) - * [Note on loopback relations](#note-on-loopback-relations) + - [Table Of Contents](#table-of-contents) + - [Dependency](#dependency) + - [Install and test](#install-and-test) + - [Main features](#main-features) + - [`PersonalizationRule` model](#-personalizationrule--model) + - [Important properties](#important-properties) + - [Acceptable values for `methodName`](#acceptable-values-for--methodname-) + - [How to use](#how-to-use) + - [Working Principle](#working-principle) + - [Supported operations](#supported-operations) + - [**fieldMask** options](#--fieldmask---options) + - [fieldMask for strings](#fieldmask-for-strings) + - [fieldMask for numbers](#fieldmask-for-numbers) + - [fieldMask for date](#fieldmask-for-date) + - [Operations on objects](#operations-on-objects) + - [Programmatic API](#programmatic-api) + - [1. Using model name, and, model data](#1-using-model-name--and--model-data) + - [2. Using model name, data, and, personalization rules](#2-using-model-name--data--and--personalization-rules) + - [Significance of pre-fetch/post-fetch operations](#significance-of-pre-fetch-post-fetch-operations) + - [Points to consider](#points-to-consider) + - [Test Synopsis](#test-synopsis) + - [Note on loopback relations](#note-on-loopback-relations) ## Dependency From b70fd1e4510f98e5178af47fe07108e5c32c2724 Mon Sep 17 00:00:00 2001 From: deostroll Date: Thu, 30 Jul 2020 10:51:16 +0530 Subject: [PATCH 74/80] Limited locale support, and, dependencies size reduction - removed unwanted deps - removed cldr-data, and, cldrjs deps - added support for en-us locale only - updated docs --- README.md | 122 ++++++++++++++++++++++++++++++++++----------------- lib/utils.js | 2 +- package.json | 5 +-- 3 files changed, 84 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index b2aa1be..be0db58 100644 --- a/README.md +++ b/README.md @@ -26,37 +26,42 @@ is frequently accessed. Such information can be used to improve the user experience on that platform. However, this is not in scope of this document. +> This documentation is best viewed in a github based +markdown viewer. E.g. Visual Code editor's inbuilt markdown +reader + ## Table Of Contents + - [oe-service-personalization](#oe-service-personalization) - - [Table Of Contents](#table-of-contents) - - [Dependency](#dependency) - - [Install and test](#install-and-test) - - [Main features](#main-features) - - [`PersonalizationRule` model](#-personalizationrule--model) - - [Important properties](#important-properties) - - [Acceptable values for `methodName`](#acceptable-values-for--methodname-) - - [How to use](#how-to-use) - - [Working Principle](#working-principle) - - [Supported operations](#supported-operations) - - [**fieldMask** options](#--fieldmask---options) - - [fieldMask for strings](#fieldmask-for-strings) - - [fieldMask for numbers](#fieldmask-for-numbers) - - [fieldMask for date](#fieldmask-for-date) - - [Operations on objects](#operations-on-objects) - - [Programmatic API](#programmatic-api) - - [1. Using model name, and, model data](#1-using-model-name--and--model-data) - - [2. Using model name, data, and, personalization rules](#2-using-model-name--data--and--personalization-rules) - - [Significance of pre-fetch/post-fetch operations](#significance-of-pre-fetch-post-fetch-operations) - - [Points to consider](#points-to-consider) - - [Test Synopsis](#test-synopsis) - - [Note on loopback relations](#note-on-loopback-relations) - -## Dependency - -* oe-cloud -* oe-logger -* oe-expression -* oe-personalization + * [Table Of Contents](#table-of-contents) + * [Install and test](#install-and-test) + * [Main features](#main-features) + * [`PersonalizationRule` model](#-personalizationrule--model) + + [Important properties](#important-properties) + + [Acceptable values for `methodName`](#acceptable-values-for--methodname-) + * [How to use](#how-to-use) + - [1. Install the module to your application](#1-install-the-module-to-your-application) + - [2. Add config to your project's `app-list.json`:](#2-add-config-to-your-project-s--app-listjson--) + - [3. Add `ServicePersonalizationMixin` mixin to the model declaration.](#3-add--servicepersonalizationmixin--mixin-to-the-model-declaration) + - [4. Insert rules into the `PersonalizationRule` model.](#4-insert-rules-into-the--personalizationrule--model) + - [5. _(Optional)_ Configure custom functions path](#5---optional---configure-custom-functions-path) + * [Working Principle](#working-principle) + * [Supported operations](#supported-operations) + * [**fieldMask** options](#--fieldmask---options) + + [fieldMask for strings](#fieldmask-for-strings) + + [fieldMask for numbers](#fieldmask-for-numbers) + + [fieldMask for date](#fieldmask-for-date) + * [Operations on objects](#operations-on-objects) + * [Programmatic API](#programmatic-api) + + [1. Using model name, and, model data](#1-using-model-name--and--model-data) + + [2. Using model name, data, and, personalization rules](#2-using-model-name--data--and--personalization-rules) + * [Significance of pre-fetch/post-fetch operations](#significance-of-pre-fetch-post-fetch-operations) + * [Points to consider](#points-to-consider) + * [Test Synopsis](#test-synopsis) + * [Note on loopback relations](#note-on-loopback-relations) + * [Additional Locale support for dates](#additional-locale-support-for-dates) + + [Dynamic locale support](#dynamic-locale-support) + + [Dynamic locale support (with limited locales)](#dynamic-locale-support--with-limited-locales-) ## Install and test @@ -478,19 +483,11 @@ however, they are a limited set, as, certain commonly used characters like `x` or `X` have special meaning in the joda standard. -A `locale` option can be passed -alternatively specifying a different locale. Acceptable values (`String`) are: -- ENGLISH -- US (_default_) -- UK -- CANADA -- FRENCH -- FRANCE -- GERMAN -- GERMANY - For more info about joda-time format visit: https://js-joda.github.io/js-joda/manual/formatting.html#format-patterns +See note on supporting other locales. +( `Additional Locale support for dates` ) + ## Operations on objects Operations such as `fieldMask`, `fieldValueReplace`, etc can be @@ -795,3 +792,48 @@ relation defined. One must always refrain from doing this. Do not collude with loopback's internal naming standards. +## Additional Locale support for dates + +To enable additional locale support, we must modify this module dependencies. + +### Dynamic locale support + +For multi-locale support, we must do the following: + +1. Uninstall `@js-joda/locale_en-us` +2. Install the following: `@js-joda/locale`, `cldr-data`, `cldrjs` modules +3. In the `./lib/utils.js` file, edit the `Locale` import line; +import from the generic module like follows: + +``` +const { Locale } = require('@js-joda/locale'); +``` +4. In the `dateMask` configuration, specify `locale` property. + +Acceptable values (`String`) for the `locale` property are: +- ENGLISH +- US (_default_) +- UK +- CANADA +- FRENCH +- FRANCE +- GERMAN +- GERMANY + +> Warning: These steps could increase the size of `node_modules`. + +### Dynamic locale support (with limited locales) + +For supporting only a few locales in a `dateMask` operation, +we need to only install those locales and the generic `@js-joda/locale` module. The general pattern +of the locale module will be: + +``` +@js-joda/locale_ +``` + +Also perform steps `3` and `4` in the previous section. + +Refer to this documentation for acceptable `` values: + +https://github.com/js-joda/js-joda-locale#use-prebuilt-locale-packages diff --git a/lib/utils.js b/lib/utils.js index 105693b..962cd79 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,6 @@ const _slice = [].slice; const { DateTimeFormatter, LocalDateTime, nativeJs } = require('@js-joda/core'); -const { Locale } = require('@js-joda/locale'); +const { Locale } = require('@js-joda/locale_en-us'); const { prototype: {toString} } = Object; const STAR = '*'; diff --git a/package.json b/package.json index 4ddf1f8..a592e8d 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,8 @@ }, "dependencies": { "@js-joda/core": "^2.0.0", - "@js-joda/locale": "^3.1.1", - "assertion-error": "1.1.0", + "@js-joda/locale_en-us": "^3.1.1", "async": "2.6.1", - "cldr-data": "^36.0.0", - "cldrjs": "^0.5.1", "lodash": "4.17.14", "oe-cloud": "git+http://evgit/oecloud.io/oe-cloud.git#master", "oe-expression": "git+http://evgit/oecloud.io/oe-expression.git#master", From e9b88442b19e17c1c6171d91ccaf32a6483aeced Mon Sep 17 00:00:00 2001 From: deostroll Date: Thu, 30 Jul 2020 10:56:23 +0530 Subject: [PATCH 75/80] v2.5.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a592e8d..ca691f1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oe-service-personalization", - "version": "2.5.1", + "version": "2.5.2", "description": "oe-cloud modularization project", "engines": { "node": ">=6" From 2440aa17139829ebd4b6de2e98f369e7c7ad0759 Mon Sep 17 00:00:00 2001 From: vamsee Date: Mon, 3 Aug 2020 00:18:24 +0530 Subject: [PATCH 76/80] commit for version 2.3.0 --- package.json | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index ca691f1..86204b7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oe-service-personalization", - "version": "2.5.2", + "version": "2.3.0", "description": "oe-cloud modularization project", "engines": { "node": ">=6" @@ -18,9 +18,9 @@ "@js-joda/locale_en-us": "^3.1.1", "async": "2.6.1", "lodash": "4.17.14", - "oe-cloud": "git+http://evgit/oecloud.io/oe-cloud.git#master", - "oe-expression": "git+http://evgit/oecloud.io/oe-expression.git#master", - "oe-personalization": "git+http://evgit/oecloud.io/oe-personalization.git#master" + "oe-cloud": "git+http://evgit/oecloud.io/oe-cloud.git#2.3.0", + "oe-expression": "git+http://evgit/oecloud.io/oe-expression.git#2.2.0", + "oe-personalization": "git+http://evgit/oecloud.io/oe-personalization.git#2.2.0" }, "devDependencies": { "babel-eslint": "7.2.3", @@ -35,9 +35,9 @@ "istanbul": "0.4.5", "md5": "^2.2.1", "mocha": "5.2.0", - "oe-connector-mongodb": "git+http://evgit/oecloud.io/oe-connector-mongodb.git#master", - "oe-connector-oracle": "git+http://evgit/oecloud.io/oe-connector-oracle.git#master", - "oe-connector-postgresql": "git+http://evgit/oecloud.io/oe-connector-postgresql.git#master", + "oe-connector-mongodb": "git+http://evgit/oecloud.io/oe-connector-mongodb.git#2.2.0", + "oe-connector-oracle": "git+http://evgit/oecloud.io/oe-connector-oracle.git#2.3.0", + "oe-connector-postgresql": "git+http://evgit/oecloud.io/oe-connector-postgresql.git#2.3.0", "superagent-defaults": "0.1.14", "supertest": "3.4.2" }, From 19565108bd5a5ea0e740733af4e634da164f3939 Mon Sep 17 00:00:00 2001 From: vamsee Date: Mon, 3 Aug 2020 00:18:24 +0530 Subject: [PATCH 77/80] commit for version 2.3.0 --- package.json | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index ca691f1..4f29500 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oe-service-personalization", - "version": "2.5.2", + "version": "2.3.0", "description": "oe-cloud modularization project", "engines": { "node": ">=6" @@ -17,10 +17,10 @@ "@js-joda/core": "^2.0.0", "@js-joda/locale_en-us": "^3.1.1", "async": "2.6.1", - "lodash": "4.17.14", - "oe-cloud": "git+http://evgit/oecloud.io/oe-cloud.git#master", - "oe-expression": "git+http://evgit/oecloud.io/oe-expression.git#master", - "oe-personalization": "git+http://evgit/oecloud.io/oe-personalization.git#master" + "lodash": "4.17.19", + "oe-cloud": "git+http://evgit/oecloud.io/oe-cloud.git#2.3.0", + "oe-expression": "git+http://evgit/oecloud.io/oe-expression.git#2.2.0", + "oe-personalization": "git+http://evgit/oecloud.io/oe-personalization.git#2.2.0" }, "devDependencies": { "babel-eslint": "7.2.3", @@ -35,9 +35,9 @@ "istanbul": "0.4.5", "md5": "^2.2.1", "mocha": "5.2.0", - "oe-connector-mongodb": "git+http://evgit/oecloud.io/oe-connector-mongodb.git#master", - "oe-connector-oracle": "git+http://evgit/oecloud.io/oe-connector-oracle.git#master", - "oe-connector-postgresql": "git+http://evgit/oecloud.io/oe-connector-postgresql.git#master", + "oe-connector-mongodb": "git+http://evgit/oecloud.io/oe-connector-mongodb.git#2.2.0", + "oe-connector-oracle": "git+http://evgit/oecloud.io/oe-connector-oracle.git#2.3.0", + "oe-connector-postgresql": "git+http://evgit/oecloud.io/oe-connector-postgresql.git#2.3.0", "superagent-defaults": "0.1.14", "supertest": "3.4.2" }, From 471efdd253b0b91a5726f12ff53628f4e25f3304 Mon Sep 17 00:00:00 2001 From: vamsee Date: Tue, 18 Aug 2020 03:07:29 +0530 Subject: [PATCH 78/80] update dep url for oe-personalization --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4f29500..a206448 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "lodash": "4.17.19", "oe-cloud": "git+http://evgit/oecloud.io/oe-cloud.git#2.3.0", "oe-expression": "git+http://evgit/oecloud.io/oe-expression.git#2.2.0", - "oe-personalization": "git+http://evgit/oecloud.io/oe-personalization.git#2.2.0" + "oe-personalization": "git+http://evgit/oecloud.io/oe-personalization.git#2.3.0" }, "devDependencies": { "babel-eslint": "7.2.3", From 42a54d4a53c48187f05edd0bfce041d2fc8624e1 Mon Sep 17 00:00:00 2001 From: vamsee Date: Tue, 18 Aug 2020 03:54:55 +0530 Subject: [PATCH 79/80] pin dep versions --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a206448..d653fa8 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "grunt-cover": "grunt test-with-coverage" }, "dependencies": { - "@js-joda/core": "^2.0.0", - "@js-joda/locale_en-us": "^3.1.1", + "@js-joda/core": "2.0.0", + "@js-joda/locale_en-us": "3.1.1", "async": "2.6.1", "lodash": "4.17.19", "oe-cloud": "git+http://evgit/oecloud.io/oe-cloud.git#2.3.0", @@ -33,7 +33,7 @@ "grunt-contrib-clean": "2.0.0", "grunt-mocha-istanbul": "5.0.2", "istanbul": "0.4.5", - "md5": "^2.2.1", + "md5": "2.3.0", "mocha": "5.2.0", "oe-connector-mongodb": "git+http://evgit/oecloud.io/oe-connector-mongodb.git#2.2.0", "oe-connector-oracle": "git+http://evgit/oecloud.io/oe-connector-oracle.git#2.3.0", From 9283700cda27b7f007bb917d98f8b013a321b9ed Mon Sep 17 00:00:00 2001 From: vamsee Date: Thu, 20 Aug 2020 16:32:24 +0530 Subject: [PATCH 80/80] Update lodash version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d653fa8..27b5544 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@js-joda/core": "2.0.0", "@js-joda/locale_en-us": "3.1.1", "async": "2.6.1", - "lodash": "4.17.19", + "lodash": "4.17.20", "oe-cloud": "git+http://evgit/oecloud.io/oe-cloud.git#2.3.0", "oe-expression": "git+http://evgit/oecloud.io/oe-expression.git#2.2.0", "oe-personalization": "git+http://evgit/oecloud.io/oe-personalization.git#2.3.0"