diff --git a/package-lock.json b/package-lock.json index fd42692109..5ec11f04d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,6 +43,7 @@ "js-sha1": "^0.6.0", "json-diff": "^1.0.3", "json-size": "^1.0.0", + "jsonpath": "^1.1.1", "jsontoxml": "^1.0.1", "koa": "^2.14.1", "koa-bodyparser": "^4.4.0", @@ -78,6 +79,7 @@ "@digitalroute/cz-conventional-changelog-for-jira": "^8.0.1", "@types/fast-json-stable-stringify": "^2.1.0", "@types/jest": "^29.5.1", + "@types/jsonpath": "^0.2.4", "@types/koa": "^2.13.6", "@types/koa-bodyparser": "^4.3.10", "@types/lodash": "^4.14.197", @@ -5407,6 +5409,12 @@ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==" }, + "node_modules/@types/jsonpath": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@types/jsonpath/-/jsonpath-0.2.4.tgz", + "integrity": "sha512-K3hxB8Blw0qgW6ExKgMbXQv2UPZBoE2GqLpVY+yr7nMD2Pq86lsuIzyAaiQ7eMqFL5B6di6pxSkogLJEyEHoGA==", + "dev": true + }, "node_modules/@types/keygrip": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", @@ -10016,7 +10024,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, "bin": { "esparse": "bin/esparse.js", "esvalidate": "bin/esvalidate.js" @@ -14514,6 +14521,28 @@ "node >= 0.2.0" ] }, + "node_modules/jsonpath": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz", + "integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==", + "dependencies": { + "esprima": "1.2.2", + "static-eval": "2.0.2", + "underscore": "1.12.1" + } + }, + "node_modules/jsonpath/node_modules/esprima": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz", + "integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/JSONStream": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", @@ -19480,6 +19509,91 @@ "node": ">=10" } }, + "node_modules/static-eval": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz", + "integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==", + "dependencies": { + "escodegen": "^1.8.1" + } + }, + "node_modules/static-eval/node_modules/escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=4.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/static-eval/node_modules/levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/static-eval/node_modules/optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/static-eval/node_modules/prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/static-eval/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/static-eval/node_modules/type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/stats-accumulator": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/stats-accumulator/-/stats-accumulator-1.1.3.tgz", @@ -20420,6 +20534,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", + "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -20810,7 +20929,6 @@ "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, "engines": { "node": ">=0.10.0" } diff --git a/package.json b/package.json index c839bc8acc..c93d8f15ed 100644 --- a/package.json +++ b/package.json @@ -88,6 +88,7 @@ "js-sha1": "^0.6.0", "json-diff": "^1.0.3", "json-size": "^1.0.0", + "jsonpath": "^1.1.1", "jsontoxml": "^1.0.1", "koa": "^2.14.1", "koa-bodyparser": "^4.4.0", @@ -123,6 +124,7 @@ "@digitalroute/cz-conventional-changelog-for-jira": "^8.0.1", "@types/fast-json-stable-stringify": "^2.1.0", "@types/jest": "^29.5.1", + "@types/jsonpath": "^0.2.4", "@types/koa": "^2.13.6", "@types/koa-bodyparser": "^4.3.10", "@types/lodash": "^4.14.197", diff --git a/src/v0/destinations/ga4/customMappingsHandler.js b/src/v0/destinations/ga4/customMappingsHandler.js index 1f673c617f..38c6198a07 100644 --- a/src/v0/destinations/ga4/customMappingsHandler.js +++ b/src/v0/destinations/ga4/customMappingsHandler.js @@ -1,32 +1,6 @@ +var jsonpath = require('jsonpath'); const { validateEventName } = require('./utils'); - -const { - get, - set, - InstrumentationError, - isDefinedAndNotNull, -} = require('@rudderstack/integrations-lib'); - -function removeLeadingTrailingDots(str) { - return str.replace(/^\.+|\.+$/g, ''); -} - -const splitOnInstance = (str, char, instance) => { - // Find the index of the specified instance of the character - let index = -1; - for (let i = 0; i < instance; i++) { - index = str.indexOf(char, index + 1); - if (index === -1) { - return [str]; // Return the original string if instance not found - } - } - - // Split the string into two parts based on the found index - let firstPart = removeLeadingTrailingDots(str.substring(0, index)); - let secondPart = removeLeadingTrailingDots(str.slice(index + 1)); - - return [firstPart, secondPart]; -}; +const { get, InstrumentationError } = require('@rudderstack/integrations-lib'); const findGA4Event = (eventsMapping, event) => { // Find the event using destructuring and early return @@ -54,143 +28,55 @@ const handleCustomMappings = (message, Config) => { const eventPropertiesMappings = ga4Event.eventProperties || {}; validateEventName(eventName); // validation for ga4 event name - const ga4Payload = { - events: [ - { - name: eventName, - }, - ], - }; + const ga4Payload = {}; for (let propertyMapping of eventPropertiesMappings) { - // { from: properties.products.product_id, to: events[0].params.items.item_id } - const { from, to } = propertyMapping; - let updatedTopath = to; - let isEventProp = false; - // if it starts with events.$ then - if (to.startsWith('events.$')) { - updatedTopath = removeLeadingTrailingDots(to.slice('events.$'.length)); - isEventProp = true; - } - const fromArr = splitOnInstance(from, '$', 1); - const toArr = splitOnInstance(updatedTopath, '$', 1); - if (fromArr.length === 1 && toArr.length === 1) { - isEventProp - ? set(ga4Payload.events[0], updatedTopath, get(message, from)) - : set(ga4Payload, to, get(message, from)); - } else if (fromArr.length === 2 && toArr.length === 1) { - // { from: properties.products.$.sku, to: events[0].params.sku } - - const paths = [].concat(getNormalisedPathArray(toArr[0])); - - const localValues = get(message, fromArr[0]); - // localvalue is array - if (Array.isArray(localValues)) { - isEventProp - ? setValueGeneric( - ga4Payload.events[0], - paths, - localValues.map((localValue) => { - return get(localValue, fromArr[1]); - }), - ) - : setValueGeneric( - ga4Payload, - paths, - localValues.map((localValue) => { - return get(localValue, fromArr[1]); - }), - ); - } - } else if (fromArr.length === 1 && toArr.length === 2) { - // { from: properties.categroy, to: events[0].params.items.$.category } - const localValue = get(message, fromArr[0]); - - const existingValue = isEventProp - ? get(ga4Payload.events[0], toArr[0]) - : get(ga4Payload, toArr[0]); - - if (Array.isArray(existingValue)) { - for (let i = 0; i < existingValue.length; i++) { - const paths = [] - .concat(getNormalisedPathArray(toArr[0])) - .concat({ - key: i, - type: 'index', - }) - .concat(getNormalisedPathArray(toArr[1])); - - isEventProp - ? setValueGeneric(ga4Payload.events[0], paths, localValue) - : setValueGeneric(ga4Payload, paths, localValue); - } - } - } else { - // { from: properties.products.$.product_id, to: events[0].params.items.$.item_id } - const localValues = get(message, fromArr[0]); - // localvalue is array - if (Array.isArray(localValues)) { - for (let i = 0; i < localValues.length; i++) { - // normalised paths - const paths = [] - .concat(getNormalisedPathArray(toArr[0])) - .concat({ - key: i, - type: 'index', - }) - .concat(getNormalisedPathArray(toArr[1])); - - const valueToSet = get(localValues[i], fromArr[1]); - isEventProp - ? setValueGeneric(ga4Payload.events[0], paths, valueToSet) - : setValueGeneric(ga4Payload, paths, valueToSet); - } - } - } + mapWithJsonPath(message, ga4Payload, propertyMapping.from, propertyMapping.to); } - return ga4Payload; -}; -const getNormalisedPathArray = (path) => { - let pathArr = path.split('.'); - pathArr = pathArr.map((path) => { - return removeLeadingTrailingDots(path); - }); - return pathArr.map((path) => { - return { key: path, type: 'path' }; - }); + return ga4Payload; }; -const setValueGeneric = (data, pathArr, value) => { - /* - pathArr = [ - { key: params , type: path} - { key: items, type: path} - { key: i, type: index } - { key: item_id, type: path} +function mapWithJsonPath(message, targetObject, sourcePath, targetPath) { + const values = jsonpath.query(message, sourcePath); + if (/\[\*\]/.test(sourcePath) && /\[\*\]/.test(targetPath)) { + // both paths are arrays - ] - */ - - const checkAndInstantiate = (key, pathArr, index) => { - // Check if the key exists and is an object, otherwise create object or array depending on the next path type - - if (!isDefinedAndNotNull(currentObject[key])) { - if (pathArr[index + 1]?.type === 'path') { - currentObject[key] = {}; - } else if (pathArr[index + 1]?.type === 'index') { - currentObject[key] = []; + for (let i = 0; i < values.length; i++) { + const targetPathWithIndex = targetPath.replace(/\[\*\]/g, `[${i}]`); + jsonpath.value(targetObject, targetPathWithIndex, values[i]); + } + } else if (!/\[\*\]/.test(sourcePath) && /\[\*\]/.test(targetPath)) { + // source path is not array and target path is + const targetPathArr = targetPath.split('.'); + const holdingArr = []; + for (let i = 0; i < targetPathArr.length; i++) { + if (/\[\*\]/.test(targetPathArr[i])) { + holdingArr.push(targetPathArr[i]); + break; + } else { + holdingArr.push(targetPathArr[i]); + } + } + const parentTargetPath = holdingArr.join('.'); + const exisitngTargetValues = jsonpath.query(targetObject, parentTargetPath); + if (exisitngTargetValues.length > 0) { + for (let i = 0; i < exisitngTargetValues.length; i++) { + const targetPathWithIndex = targetPath.replace(/\[\*\]/g, `[${i}]`); + jsonpath.value(targetObject, targetPathWithIndex, values[0]); } + } else { + const targetPathWithIndex = targetPath.replace(/\[\*\]/g, '[0]'); + jsonpath.value(targetObject, targetPathWithIndex, values[0]); } - }; - let currentObject = data; - for (let i = 0; i < pathArr.length - 1; i++) { - const { key, type } = pathArr[i]; - checkAndInstantiate(key, pathArr, i); - currentObject = currentObject[key]; + } else if (/\[\*\]/.test(sourcePath)) { + // source path is an array but target path is not + jsonpath.value(targetObject, targetPath, values); + } else { + // both paths are not arrays + jsonpath.value(targetObject, targetPath, values[0]); } - currentObject[pathArr[pathArr.length - 1].key] = value; -}; +} module.exports = { handleCustomMappings,