From fb3f2be6b6b837d950f64901563d29c3369855f2 Mon Sep 17 00:00:00 2001 From: lautaro-echeverria Date: Mon, 21 Jul 2025 09:10:51 +0200 Subject: [PATCH 1/5] add paths to exclude fields in logs --- README.md | 13 +- lib/helpers/log.js | 4 +- lib/utils.js | 100 ++++++- package.json | 3 +- tests/dispatcher-test.js | 557 ++++++++++++++++++++++++++++++++++++--- 5 files changed, 620 insertions(+), 57 deletions(-) diff --git a/README.md b/README.md index ce39aaa..7852ff5 100644 --- a/README.md +++ b/README.md @@ -55,10 +55,19 @@ Determines if the api response data should be logged or not. Determines if the api response body should be logged or not. - **excludeFieldsLogRequestData**. *string array*. -Returns the fields to exclude from the api request data. +Returns the fields to exclude from the api request data passing simple fields or specific paths to such fields. - **excludeFieldsLogResponseBody**. *string array*. -Returns the fields to exclude from the api response body. The fields will be omitted recursively. +Returns the fields to exclude from the api response data passing simple fields or specific paths to such fields. + +ℹ️ **Note**: +- The wildcard `*` in the field path of the `excludeFieldsLogRequestData` or `excludeFieldsLogResponseBody` static getter, is used to access properties inside arrays or when the root field path is unknown. +- The wildcard `**` in the field path of the `excludeFieldsLogRequestData` or `excludeFieldsLogResponseBody` static getter, can be used when the intermediate field path is unknown between the root and the field to exclude. +- The `excludeFieldsLogRequestData` or `excludeFieldsLogResponseBody` static getter can have both field names and field paths. + +⚠️ **Warning**: +- When using the wildcard `*` alone in the field path of the `excludeFieldsLogRequestData` or `excludeFieldsLogResponseBody` static getter, it will exclude all the fields in the log. +- In case the field path is incorrect, it will not exclude any field. ### Setters diff --git a/lib/helpers/log.js b/lib/helpers/log.js index 697b639..2d72f7d 100644 --- a/lib/helpers/log.js +++ b/lib/helpers/log.js @@ -31,13 +31,13 @@ module.exports = class LogHelper { api: { endpoint, httpMethod }, request: { ...api.shouldLogRequestHeaders && { headers: omitRecursive(headers, ['janis-api-key', 'janis-api-secret']) }, - ...api.shouldLogRequestData && { data: omitRecursive(pristineData, api.excludeFieldsLogRequestData) }, + ...api.shouldLogRequestData && { data: omitRecursive(pristineData, api.excludeFieldsLogRequestData || []) }, ...this.addServiceName(headers) }, response: { code: response.code, headers: response.headers, - ...api.shouldLogResponseBody && { body: omitRecursive(response.body, api.excludeFieldsLogResponseBody) } + ...api.shouldLogResponseBody && { body: omitRecursive(response.body, api.excludeFieldsLogResponseBody || []) } } }; diff --git a/lib/utils.js b/lib/utils.js index 5047ed5..9539879 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,6 +1,6 @@ 'use strict'; -const omit = require('lodash.omit'); +const cloneDeep = require('lodash.clonedeep'); /** * Determines if the passed value is an object. @@ -12,21 +12,103 @@ const isObject = value => { return !!value && typeof value === 'object' && !Array.isArray(value); }; +/** + * Checks if a given property path matches a specific pattern. + * + * @param {string[]} path - The current property path segments. + * @param {string[]} pattern - The pattern segments to match against. + * @param {number} i - Current index in the path array. + * @param {number} j - Current index in the pattern array. + * @returns {boolean} True if the path matches the pattern, false otherwise. + */ +const isPathMatch = (path, pattern, i = 0, j = 0) => { + + // Special case: if pattern has only one segment, check if it matches the last path segment or is a wildcard + if(pattern.length === 1) + return path[path.length - 1] === pattern[0] || pattern[0] === '*'; + + // If all pattern segments are consumed, check if all path segments are also consumed + if(j === pattern.length) + return i === path.length; + + // Handle double wildcard '**' which matches zero or more path segments + if(pattern[j] === '**') { + + // Try matching the rest of the pattern starting from each possible position in the path + for(let k = i; k <= path.length; k++) { + // Recursively check if the remaining pattern matches from position k + if(isPathMatch(path, pattern, k, j + 1)) + return true; + } + + // If no match found with any position, return false + return false; + } + + // If all path segments are consumed but still have pattern segments left, no match + if(i === path.length) + return false; + + // If current pattern segment is not a wildcard and doesn't match current path segment, no match + if(pattern[j] !== '*' && pattern[j] !== path[i]) + return false; + + // Move to next segments in both path and pattern + return isPathMatch(path, pattern, i + 1, j + 1); +}; + +/** + * Recursively iterates over the object/array structure to find and remove matching properties. + * + * @param {string[]} patterns - The patterns as path segments of the properties to exclude when matching. + * @param {*} current - The current element being processed (object, array, or primitive). + * @param {string[]} currentPath - The path segments leading to the current element. + */ +const recurse = (patterns, current, currentPath = []) => { + + // If current element is an array, recursively process each item with its index as path segment + if(Array.isArray(current)) + current.forEach((item, index) => recurse(patterns, item, [...currentPath, String(index)])); + // If current element is an object (but not null), process its properties + else if(current && typeof current === 'object') { + + // Iterate through all key-value pairs in the object + for(const [key, value] of Object.entries(current)) { + + // Build the new path by adding the current key to the existing path + const newPath = [...currentPath, key]; + + // Check if the current path matches any of the exclusion patterns + if(patterns.some(p => isPathMatch(newPath, p))) + // If it matches, delete this property from the object + delete current[key]; + else + // If it doesn't match, recursively process the value with the new path + recurse(patterns, value, newPath); + } + } +}; + /** * Returns a new object excluding one or more properties recursively * - * @param {Object} object - * @param {string|Array} exclude + * @param {object} object - The input object to process that will be cloned and modified. + * @param {string[]} pathPatterns - Property path(s) to exclude (e.g. ['a.b', '*.x.y', 'some-field']). + * @returns {object} A new object with specified properties omitted. */ -const omitRecursive = (object, exclude) => { +const omitRecursive = (object, pathPatterns) => { - object = { ...object }; // Avoid original object modification + // Convert dot-notation path patterns into arrays of path segments. + const patterns = pathPatterns.map(p => p.split('.')); - Object.entries(object).forEach(([key, value]) => { - object[key] = isObject(value) ? omitRecursive(value, exclude) : value; - }); + // Deep clone the original object to avoid modifying it directly. + const clonedObject = cloneDeep(object); + + // Recursive removal process from the root of the cloned object + recurse(patterns, clonedObject); - return omit(object, exclude); + // Return the modified clone with specified properties removed + return clonedObject; }; const trimObjectValues = value => { diff --git a/package.json b/package.json index d2b6f04..a06e98b 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,6 @@ "@janiscommerce/api-session": "^3.4.0", "@janiscommerce/log": "^5.0.6", "@janiscommerce/superstruct": "^1.2.1", - "lodash.clonedeep": "^4.5.0", - "lodash.omit": "4.5.0" + "lodash.clonedeep": "^4.5.0" } } diff --git a/tests/dispatcher-test.js b/tests/dispatcher-test.js index 070d0fc..5540e31 100644 --- a/tests/dispatcher-test.js +++ b/tests/dispatcher-test.js @@ -241,34 +241,34 @@ describe('Dispatcher', () => { ['foo', 'bar'] ]; - it('should reject when no request data given', () => { + it('Should reject when no request data given', () => { testConstructorReject(APIError.codes.INVALID_REQUEST_DATA); }); - it('should reject when no object request data received', () => { + it('Should reject when no object request data received', () => { noObjects.forEach(requestData => testConstructorReject(APIError.codes.INVALID_REQUEST_DATA, requestData)); }); - it('should reject when no endpoint given', () => { + it('Should reject when no endpoint given', () => { testConstructorReject(APIError.codes.INVALID_ENDPOINT, {}); }); - it('should reject when invalid method given', () => { + it('Should reject when invalid method given', () => { const endpoint = 'valid/endpoint'; noStrings.forEach(method => testConstructorReject(APIError.codes.INVALID_METHOD, { endpoint, method })); }); - it('should reject when invalid headers given', () => { + it('Should reject when invalid headers given', () => { const endpoint = 'valid/endpoint'; noObjects.forEach(headers => testConstructorReject(APIError.codes.INVALID_HEADERS, { endpoint, headers })); }); - it('should reject when invalid cookies given', () => { + it('Should reject when invalid cookies given', () => { const endpoint = 'valid/endpoint'; noObjects.forEach(cookies => testConstructorReject(APIError.codes.INVALID_COOKIES, { endpoint, cookies })); }); - it('should reject when invalid authentication data given', () => { + it('Should reject when invalid authentication data given', () => { const endpoint = 'valid/endpoint'; noObjects.forEach(authenticationData => testConstructorReject(APIError.codes.INVALID_AUTHENTICATION_DATA, { endpoint, authenticationData })); }); @@ -276,7 +276,7 @@ describe('Dispatcher', () => { context('5xx errors', () => { - it('should return code 500 when api file hasn\'t a class', async function() { + it('Should return code 500 when api file hasn\'t a class', async function() { await assert.rejects(() => test({ endpoint: 'api/invalid-api-class-endpoint' }), { @@ -284,7 +284,7 @@ describe('Dispatcher', () => { }); }); - it('should return code 500 when api does not have a process method', async function() { + it('Should return code 500 when api does not have a process method', async function() { await assert.rejects(() => test({ endpoint: 'api/no-process-method' }), { @@ -294,7 +294,7 @@ describe('Dispatcher', () => { }); }); - it('should return code 500 when api file found but api object has not a process method', async function() { + it('Should return code 500 when api file found but api object has not a process method', async function() { await assert.rejects(() => test({ endpoint: 'api/no-process-endpoint' }), { @@ -305,7 +305,7 @@ describe('Dispatcher', () => { }); }); - it('should return code 500 when api process method throw an internal server error', async function() { + it('Should return code 500 when api process method throw an internal server error', async function() { await assert.rejects(() => test({ endpoint: 'api/process-rejects-endpoint', method: 'post' @@ -317,7 +317,7 @@ describe('Dispatcher', () => { }); }); - it('should return code 500 when api process method throw an internal server error - default message', async function() { + it('Should return code 500 when api process method throw an internal server error - default message', async function() { await assert.rejects(() => test({ endpoint: 'api/process-rejects-default-message-endpoint', method: 'post' @@ -329,7 +329,7 @@ describe('Dispatcher', () => { }); }); - it('should return a custom HTTP Code and default message when code given', async function() { + it('Should return a custom HTTP Code and default message when code given', async function() { httpCode = 501; @@ -344,7 +344,7 @@ describe('Dispatcher', () => { }); }); - it('should use error.body as response body if it is defined', async function() { + it('Should use error.body as response body if it is defined', async function() { await assert.rejects(() => test({ endpoint: 'api/process-rejects-body-endpoint', method: 'post' @@ -356,7 +356,7 @@ describe('Dispatcher', () => { }); }); - it('should use error.message and error.messageVariables as response body if it is defined', async function() { + it('Should use error.message and error.messageVariables as response body if it is defined', async function() { await assert.rejects(() => test({ endpoint: 'api/process-rejects-with-variables-endpoint', method: 'post' @@ -374,7 +374,7 @@ describe('Dispatcher', () => { context('4xx errors', () => { - it('should return code 404 when api file not found', async function() { + it('Should return code 404 when api file not found', async function() { await assert.rejects(() => test({ endpoint: 'api/unknown-endpoint' }), { @@ -382,7 +382,7 @@ describe('Dispatcher', () => { }); }); - it('should return code 400 when api validate method throw a data invalid', async function() { + it('Should return code 400 when api validate method throw a data invalid', async function() { await assert.rejects(() => test({ endpoint: 'api/validate-rejects-endpoint', method: 'put' @@ -394,7 +394,7 @@ describe('Dispatcher', () => { }); }); - it('should return code 400 when api validate method throw a data invalid - default message', async function() { + it('Should return code 400 when api validate method throw a data invalid - default message', async function() { await assert.rejects(() => test({ endpoint: 'api/validate-rejects-default-message-endpoint', method: 'post' @@ -406,7 +406,7 @@ describe('Dispatcher', () => { }); }); - it('should response with custom HTTP Code and default message when validate fails and code given', async function() { + it('Should response with custom HTTP Code and default message when validate fails and code given', async function() { httpCode = 401; @@ -421,7 +421,7 @@ describe('Dispatcher', () => { }); }); - it('should return code 400 when api data is invalid against struct', async function() { + it('Should return code 400 when api data is invalid against struct', async function() { await assert.rejects(() => test({ endpoint: 'api/struct-endpoint' @@ -444,7 +444,7 @@ describe('Dispatcher', () => { }); - it('should return code 400 when api data is invalid against struct multiple', async function() { + it('Should return code 400 when api data is invalid against struct multiple', async function() { await assert.rejects(() => test({ endpoint: 'api/struct-multiple-endpoint', @@ -470,7 +470,7 @@ describe('Dispatcher', () => { context('2xx responses', () => { - it('should return code 200 when api validates correctly', async function() { + it('Should return code 200 when api validates correctly', async function() { await test({ endpoint: 'api/validate-correctly-endpoint' }, 200); @@ -485,7 +485,7 @@ describe('Dispatcher', () => { }); }; - it('should return code 200 when api validates correctly & apply trim to data', async function() { + it('Should return code 200 when api validates correctly & apply trim to data', async function() { extraProcess = api => { assert.deepStrictEqual(api.data, { @@ -507,20 +507,20 @@ describe('Dispatcher', () => { }, 200); }); - it('should return code 200 when api validates correctly the struct', async function() { + it('Should return code 200 when api validates correctly the struct', async function() { await test({ endpoint: 'api/struct-endpoint', data: { foo: 'bar' } }, 200); }); - it('should return code 200 when api has no validate method', async function() { + it('Should return code 200 when api has no validate method', async function() { await test({ endpoint: 'api/valid-endpoint' }, 200); }); - it('should return api requestData with getters', async function() { + it('Should return api requestData with getters', async function() { extraProcess = api => { @@ -543,7 +543,7 @@ describe('Dispatcher', () => { }, 200); }); - it('should response with a custom HTTP Code when given', async function() { + it('Should response with a custom HTTP Code when given', async function() { httpCode = 201; @@ -552,7 +552,7 @@ describe('Dispatcher', () => { }, 201); }); - it('should return code 200 when api response and set headers', async function() { + it('Should return code 200 when api response and set headers', async function() { responseHeaders = { 'valid-header': 123 }; @@ -561,7 +561,7 @@ describe('Dispatcher', () => { }, 200, responseHeaders); }); - it('should return code 200 when api response and set an individual header', async function() { + it('Should return code 200 when api response and set an individual header', async function() { responseHeader = { name: 'valid-header', value: 123 }; @@ -570,7 +570,7 @@ describe('Dispatcher', () => { }, 200, { 'valid-header': 123 }); }); - it('should return code 200 when api response and set cookies', async function() { + it('Should return code 200 when api response and set cookies', async function() { responseCookies = { 'valid-cookie': 123 }; @@ -579,7 +579,7 @@ describe('Dispatcher', () => { }, 200, {}, responseCookies); }); - it('should return code 200 when api response and set an individual cookie', async function() { + it('Should return code 200 when api response and set an individual cookie', async function() { responseCookie = { name: 'valid-cookie', value: 123 }; @@ -588,7 +588,7 @@ describe('Dispatcher', () => { }, 200, {}, { 'valid-cookie': 123 }); }); - it('should found api when using a prefix with ENV MS_PATH', async function() { + it('Should found api when using a prefix with ENV MS_PATH', async function() { process.env.MS_PATH = 'my-custom-prefix'; @@ -620,7 +620,9 @@ describe('Dispatcher', () => { log: { executionTime: sinon.match.number } }; - it('should log the api request with userCreated field', async () => { + it('Should log the api request with userCreated field', async () => { + + extraProcess = () => {}; responseBody = { message: 'ok' }; responseHeaders = { 'res-header': 'some-data' }; @@ -656,7 +658,7 @@ describe('Dispatcher', () => { }); }); - it('should log the request without request data, headers and response body', async function() { + it('Should log the request without request data, headers and response body', async function() { responseBody = { message: 'ok', id: '6663117329438a003c10247a' }; @@ -682,7 +684,7 @@ describe('Dispatcher', () => { }); }); - it('should log the request adding the janis-service-name when janis-api-key was service prefix', async function() { + it('Should log the request adding the janis-service-name when janis-api-key was service prefix', async function() { responseBody = { message: 'ok' }; @@ -712,7 +714,7 @@ describe('Dispatcher', () => { }); }); - it('should log the request excluding the specified fields of request data and response body', async function() { + it('Should log the request excluding the specified fields of request data and response body', async function() { extraProcess = api => { @@ -759,9 +761,7 @@ describe('Dispatcher', () => { request: { data: { some: 'data', - password: sinon.match.undefined, location: { - address: sinon.match.undefined, country: 'AR' } } @@ -770,9 +770,7 @@ describe('Dispatcher', () => { code: 200, body: { message: 'ok', - password: sinon.match.undefined, authData: { - secretCode: sinon.match.undefined, publicCode: 2 } } @@ -781,7 +779,482 @@ describe('Dispatcher', () => { }); }); - it('should not log the api request with get method when api.shouldCreateLog is not set', async function() { + it('Should exclude one field from the log when excludeFieldsLogRequestData has array pattern', async function() { + + extraProcess = api => { + api.excludeFieldsLogRequestData = ['shipping.*.secondFactor']; + api.excludeFieldsLogResponseBody = ['items.*.secretKey']; + }; + + responseBody = { + message: 'ok', + items: [{ + id: '123', + secretKey: 'secret-value', + name: 'Product A' + }] + }; + + await test({ + ...defaultApi, + data: { + orderId: 'order-123', + shipping: [{ + id: 'ship-1', + secondFactor: 'auth-token', + provider: 'fedex' + }] + } + }, 200); + + sinon.assert.calledWithMatch(Log.add, 'fizzmod', { + ...commonLog, + entity: 'logs-enabled', + log: { + api: { + endpoint: 'logs-enabled', + httpMethod: 'get' + }, + request: { + data: { + orderId: 'order-123', + shipping: [{ + id: 'ship-1', + provider: 'fedex' + }] + } + }, + response: { + code: 200, + body: { + message: 'ok', + items: [{ + id: '123', + name: 'Product A' + }] + } + } + } + }); + }); + + it('Should exclude two or more fields from the log when excludeFieldsLogRequestData has multiple patterns', async function() { + + extraProcess = api => { + api.excludeFieldsLogRequestData = [ + 'shipping.*.secondFactor', + 'shipping.*.addressCommerceId' + ]; + api.excludeFieldsLogResponseBody = [ + 'password', + 'userData.*.email' + ]; + }; + + responseBody = { + message: 'ok', + password: 'secret123', + userData: [{ + id: 'user1', + email: 'user@example.com', + name: 'John Doe' + }] + }; + + await test({ + ...defaultApi, + data: { + orderId: 'order-123', + shipping: [{ + id: 'ship-1', + secondFactor: 'auth-token', + addressCommerceId: 'addr-123', + provider: 'fedex' + }] + } + }, 200); + + sinon.assert.calledWithMatch(Log.add, 'fizzmod', { + ...commonLog, + entity: 'logs-enabled', + log: { + api: { + endpoint: 'logs-enabled', + httpMethod: 'get' + }, + request: { + data: { + orderId: 'order-123', + shipping: [{ + id: 'ship-1', + provider: 'fedex' + }] + } + }, + response: { + code: 200, + body: { + message: 'ok', + userData: [{ + id: 'user1', + name: 'John Doe' + }] + } + } + } + }); + }); + + it('Should exclude fields from nested arrays when pattern targets deep nested structure', async function() { + + extraProcess = api => { + api.excludeFieldsLogRequestData = ['shipping.*.items.*.quantity']; + api.excludeFieldsLogResponseBody = ['orders.*.products.*.price']; + }; + + responseBody = { + message: 'ok', + orders: [{ + id: 'order-1', + products: [{ + id: 'prod-1', + price: 100.50, + name: 'Product A' + }] + }] + }; + + await test({ + ...defaultApi, + data: { + orderId: 'order-123', + shipping: [{ + id: 'ship-1', + provider: 'fedex', + items: [{ + id: 'item-1', + quantity: 5, + weight: '2kg' + }] + }] + } + }, 200); + + sinon.assert.calledWithMatch(Log.add, 'fizzmod', { + ...commonLog, + entity: 'logs-enabled', + log: { + api: { + endpoint: 'logs-enabled', + httpMethod: 'get' + }, + request: { + data: { + orderId: 'order-123', + shipping: [{ + id: 'ship-1', + provider: 'fedex', + items: [{ + id: 'item-1', + weight: '2kg' + }] + }] + } + }, + response: { + code: 200, + body: { + message: 'ok', + orders: [{ + id: 'order-1', + products: [{ + id: 'prod-1', + name: 'Product A' + }] + }] + } + } + } + }); + }); + + it('Should exclude fields when using wildcard for unknown intermediate paths', async function() { + + extraProcess = api => { + api.excludeFieldsLogRequestData = ['item.**.quantity']; + api.excludeFieldsLogResponseBody = ['result.**.secretData']; + }; + + responseBody = { + message: 'ok', + result: { + data: { + nested: { + secretData: 'hidden-value' + }, + publicInfo: 'visible' + } + } + }; + + await test({ + ...defaultApi, + data: { + orderId: 'order-123', + item: { + details: { + specs: { + quantity: 10 + }, + name: 'Product A' + } + } + } + }, 200); + + sinon.assert.calledWithMatch(Log.add, 'fizzmod', { + ...commonLog, + entity: 'logs-enabled', + log: { + api: { + endpoint: 'logs-enabled', + httpMethod: 'get' + }, + request: { + data: { + orderId: 'order-123', + item: { + details: { + specs: {}, + name: 'Product A' + } + } + } + }, + response: { + code: 200, + body: { + message: 'ok', + result: { + data: { + nested: {}, + publicInfo: 'visible' + } + } + } + } + } + }); + }); + + it('Should exclude fields when one pattern exists and another does not', async function() { + + extraProcess = api => { + api.excludeFieldsLogRequestData = [ + 'shipping.*.secondFactor', + 'shipping.*.deliveryWindows.initialDate' + ]; + api.excludeFieldsLogResponseBody = [ + 'password', + 'nonExistentField.*.value' + ]; + }; + + responseBody = { + message: 'ok', + password: 'secret123', + otherData: 'visible' + }; + + await test({ + ...defaultApi, + data: { + orderId: 'order-123', + shipping: [{ + id: 'ship-1', + secondFactor: 'auth-token', + provider: 'fedex' + }] + } + }, 200); + + sinon.assert.calledWithMatch(Log.add, 'fizzmod', { + ...commonLog, + entity: 'logs-enabled', + log: { + api: { + endpoint: 'logs-enabled', + httpMethod: 'get' + }, + request: { + data: { + orderId: 'order-123', + shipping: [{ + id: 'ship-1', + provider: 'fedex' + }] + } + }, + response: { + code: 200, + body: { + message: 'ok', + otherData: 'visible' + } + } + } + }); + }); + + it('Should exclude all fields when wildcard pattern is used', async function() { + + extraProcess = api => { + api.excludeFieldsLogRequestData = ['*']; + api.excludeFieldsLogResponseBody = ['*']; + }; + + responseBody = { + message: 'ok', + data: { some: 'value' } + }; + + await test({ + ...defaultApi, + data: { + orderId: 'order-123', + customer: 'john@example.com' + } + }, 200); + + sinon.assert.calledWithMatch(Log.add, 'fizzmod', { + ...commonLog, + entity: 'logs-enabled', + log: { + api: { + endpoint: 'logs-enabled', + httpMethod: 'get' + }, + request: { + data: {} + }, + response: { + code: 200, + body: {} + } + } + }); + }); + + context('When fields from excludeFields arrays should not be excluded from the log', () => { + + it('Should not exclude fields when excludeFields patterns do not match existing fields', async function() { + + extraProcess = api => { + api.excludeFieldsLogRequestData = [ + '', + '*.shipping.*.deliveryWindows.initialDate', + '.secondFactor.value', + 'location.address.' + ]; + api.excludeFieldsLogResponseBody = [ + '*.nonExistent.*.field', + 'invalidPattern' + ]; + }; + + responseBody = { + message: 'ok', + data: { some: 'value' } + }; + + await test({ + ...defaultApi, + data: { + orderId: 'order-123', + shipping: [{ + id: 'ship-1', + secondFactor: 'auth-token', + provider: 'fedex' + }] + } + }, 200); + + sinon.assert.calledWithMatch(Log.add, 'fizzmod', { + ...commonLog, + entity: 'logs-enabled', + log: { + api: { + endpoint: 'logs-enabled', + httpMethod: 'get' + }, + request: { + data: { + orderId: 'order-123', + shipping: [{ + id: 'ship-1', + secondFactor: 'auth-token', + provider: 'fedex' + }] + } + }, + response: { + code: 200, + body: { + message: 'ok', + data: { some: 'value' } + } + } + } + }); + }); + + it('Should not exclude fields when excludeFields arrays are empty', async function() { + + extraProcess = api => { + api.excludeFieldsLogRequestData = []; + api.excludeFieldsLogResponseBody = []; + }; + + responseBody = { + message: 'ok', + password: 'secret123' + }; + + await test({ + ...defaultApi, + data: { + orderId: 'order-123', + password: 'user-secret' + } + }, 200); + + sinon.assert.calledWithMatch(Log.add, 'fizzmod', { + ...commonLog, + entity: 'logs-enabled', + log: { + api: { + endpoint: 'logs-enabled', + httpMethod: 'get' + }, + request: { + data: { + orderId: 'order-123', + password: 'user-secret' + } + }, + response: { + code: 200, + body: { + message: 'ok', + password: 'secret123' + } + } + } + }); + }); + }); + + it('Should not log the api request with get method when api.shouldCreateLog is not set', async function() { await test({ ...defaultApi, endpoint: 'api/valid-endpoint' @@ -789,7 +1262,7 @@ describe('Dispatcher', () => { sinon.assert.notCalled(Log.add); }); - it('should not log the api request when api.shouldCreateLog is false', async function() { + it('Should not log the api request when api.shouldCreateLog is false', async function() { await test({ ...defaultApi, From 70564fdf5563474a5e8751851f86398f4ee5b5e6 Mon Sep 17 00:00:00 2001 From: lautaro-echeverria Date: Mon, 21 Jul 2025 09:33:24 +0200 Subject: [PATCH 2/5] update node version in github workflows --- .github/workflows/build-status.yml | 2 +- .github/workflows/coverage-status.yml | 4 ++-- .github/workflows/npm-publish.yml | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-status.yml b/.github/workflows/build-status.yml index 412f806..371a2b9 100644 --- a/.github/workflows/build-status.yml +++ b/.github/workflows/build-status.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: - node-version: [14.x] + node-version: [18.x] steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/coverage-status.yml b/.github/workflows/coverage-status.yml index 592c264..643eec7 100644 --- a/.github/workflows/coverage-status.yml +++ b/.github/workflows/coverage-status.yml @@ -11,10 +11,10 @@ jobs: - uses: actions/checkout@v1 - - name: Use Node.js 14.x + - name: Use Node.js 18.x uses: actions/setup-node@v1 with: - node-version: 14.x + node-version: 18.x - name: npm install, make test-coverage run: | diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 043d5c3..8aeb6ab 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 14 + node-version: 18 - run: npm i - run: npm test @@ -22,7 +22,7 @@ jobs: - uses: actions/checkout@v2 - uses: actions/setup-node@v1 with: - node-version: 14 + node-version: 18 registry-url: https://registry.npmjs.org/ - run: npm i - run: npm run build-types From 5330f1fd75a86b5e8e82c5ad73540c39beaecf0d Mon Sep 17 00:00:00 2001 From: lautaro-echeverria Date: Mon, 21 Jul 2025 09:42:19 +0200 Subject: [PATCH 3/5] update actions checkout and setup-node to latest stable version --- .github/workflows/coverage-status.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/coverage-status.yml b/.github/workflows/coverage-status.yml index 643eec7..b5020ba 100644 --- a/.github/workflows/coverage-status.yml +++ b/.github/workflows/coverage-status.yml @@ -9,10 +9,10 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v4 - name: Use Node.js 18.x - uses: actions/setup-node@v1 + uses: actions/setup-node@v4 with: node-version: 18.x From 832fdefe70128631f17f176a15a2a5da6cd684c7 Mon Sep 17 00:00:00 2001 From: lautaro-echeverria Date: Mon, 21 Jul 2025 10:03:58 +0200 Subject: [PATCH 4/5] improve readme file --- README.md | 2 +- tests/dispatcher-test.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7852ff5..7287594 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ Returns the fields to exclude from the api request data passing simple fields or Returns the fields to exclude from the api response data passing simple fields or specific paths to such fields. ℹ️ **Note**: -- The wildcard `*` in the field path of the `excludeFieldsLogRequestData` or `excludeFieldsLogResponseBody` static getter, is used to access properties inside arrays or when the root field path is unknown. +- The wildcard `*` in the field path of the `excludeFieldsLogRequestData` or `excludeFieldsLogResponseBody` static getter, is used to access properties inside arrays of one level or nested arrays. - The wildcard `**` in the field path of the `excludeFieldsLogRequestData` or `excludeFieldsLogResponseBody` static getter, can be used when the intermediate field path is unknown between the root and the field to exclude. - The `excludeFieldsLogRequestData` or `excludeFieldsLogResponseBody` static getter can have both field names and field paths. diff --git a/tests/dispatcher-test.js b/tests/dispatcher-test.js index 5540e31..6f0eb31 100644 --- a/tests/dispatcher-test.js +++ b/tests/dispatcher-test.js @@ -779,7 +779,7 @@ describe('Dispatcher', () => { }); }); - it('Should exclude one field from the log when excludeFieldsLogRequestData has array pattern', async function() { + it('Should exclude one field from the log when excludeFieldsLogRequestData and excludeFieldsLogResponseBody have array patterns', async function() { extraProcess = api => { api.excludeFieldsLogRequestData = ['shipping.*.secondFactor']; @@ -838,7 +838,8 @@ describe('Dispatcher', () => { }); }); - it('Should exclude two or more fields from the log when excludeFieldsLogRequestData has multiple patterns', async function() { + // eslint-disable-next-line max-len + it('Should exclude two or more fields from the log when excludeFieldsLogRequestData and excludeFieldsLogResponseBody have multiple patterns', async function() { extraProcess = api => { api.excludeFieldsLogRequestData = [ From 0535ae7582bb759efb77f740529483f9e4f15c87 Mon Sep 17 00:00:00 2001 From: lautaro-echeverria Date: Tue, 29 Jul 2025 00:07:20 +0100 Subject: [PATCH 5/5] fixes PR (second reviewer) --- lib/utils.js | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/lib/utils.js b/lib/utils.js index 9539879..f3493c6 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -1,7 +1,5 @@ 'use strict'; -const cloneDeep = require('lodash.clonedeep'); - /** * Determines if the passed value is an object. * @@ -67,24 +65,33 @@ const isPathMatch = (path, pattern, i = 0, j = 0) => { const recurse = (patterns, current, currentPath = []) => { // If current element is an array, recursively process each item with its index as path segment - if(Array.isArray(current)) - current.forEach((item, index) => recurse(patterns, item, [...currentPath, String(index)])); - // If current element is an object (but not null), process its properties - else if(current && typeof current === 'object') { + if(Array.isArray(current)) { + + // Iterate through all items in the array + for(const [index, item] of current.entries()) { + currentPath.push(String(index)); + recurse(patterns, item, currentPath); + currentPath.pop(); + } + + } else if(current && typeof current === 'object') { - // Iterate through all key-value pairs in the object - for(const [key, value] of Object.entries(current)) { + // Iterate through all object keys + for(const key of Object.keys(current)) { - // Build the new path by adding the current key to the existing path - const newPath = [...currentPath, key]; + // Add the current key to the path + currentPath.push(key); // Check if the current path matches any of the exclusion patterns - if(patterns.some(p => isPathMatch(newPath, p))) + if(patterns.some(p => isPathMatch(currentPath, p))) // If it matches, delete this property from the object delete current[key]; else - // If it doesn't match, recursively process the value with the new path - recurse(patterns, value, newPath); + // If it doesn't match, recursively process the value + recurse(patterns, current[key], currentPath); + + // Remove the key from the path after processing + currentPath.pop(); } } }; @@ -99,10 +106,13 @@ const recurse = (patterns, current, currentPath = []) => { const omitRecursive = (object, pathPatterns) => { // Convert dot-notation path patterns into arrays of path segments. - const patterns = pathPatterns.map(p => p.split('.')); + const patterns = []; + + for(const p of pathPatterns) + patterns.push(p.split('.')); // Deep clone the original object to avoid modifying it directly. - const clonedObject = cloneDeep(object); + const clonedObject = structuredClone(object); // Recursive removal process from the root of the cloned object recurse(patterns, clonedObject); @@ -116,14 +126,14 @@ const trimObjectValues = value => { if(typeof value !== 'object' || !value) return value; - Object.keys(value).forEach(k => { + for(const k of Object.keys(value)) { if((typeof value[k] === 'string')) value[k] = value[k].trim(); if(typeof value[k] === 'object' && value[k] !== null) value[k] = trimObjectValues(value[k]); - }); + } return value; };