From c8d2bd393da7bac66cc4902ead870d8da0e70d39 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 21 May 2024 20:29:12 +0600 Subject: [PATCH 1/8] feat: sort function this function can sort a JSON with nested keys --- packages/idempotency/src/sort.ts | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 packages/idempotency/src/sort.ts diff --git a/packages/idempotency/src/sort.ts b/packages/idempotency/src/sort.ts new file mode 100644 index 0000000000..84fb59f09c --- /dev/null +++ b/packages/idempotency/src/sort.ts @@ -0,0 +1,29 @@ +import { getType } from '@aws-lambda-powertools/commons'; +import { + JSONArray, + JSONObject, + JSONValue, +} from '@aws-lambda-powertools/commons/types'; + +const sortObject = (object: JSONObject): JSONObject => { + return Object.keys(object) + .sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : 1)) + .reduce((acc, key) => { + acc[key] = sort(object[key]); + + return acc; + }, {} as JSONObject); +}; + +const sort = (data: JSONValue): JSONValue => { + const type = getType(data); + if (type === 'object') { + return sortObject(data as JSONObject); + } else if (type === 'array') { + return (data as JSONArray).map(sort); + } + + return data; +}; + +export { sort }; From 64b2e69d07d2bddb131a93ddcdc74e7ee20dcad4 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 21 May 2024 20:34:45 +0600 Subject: [PATCH 2/8] test: sort function for primitive values --- packages/idempotency/tests/unit/sort.test.ts | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 packages/idempotency/tests/unit/sort.test.ts diff --git a/packages/idempotency/tests/unit/sort.test.ts b/packages/idempotency/tests/unit/sort.test.ts new file mode 100644 index 0000000000..7b834cae6c --- /dev/null +++ b/packages/idempotency/tests/unit/sort.test.ts @@ -0,0 +1,35 @@ +/** + * Test sort function + * + * @group unit/commons/sort + */ + +import { sort } from '../../src/sort'; + +describe('Function: sort', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + test('can sort string correctly', () => { + expect(sort('test')).toEqual('test'); + }); + + test('can sort number correctly', () => { + expect(sort(5)).toEqual(5); + }); + + test('can sort boolean correctly', () => { + expect(sort(true)).toEqual(true); + expect(sort(false)).toEqual(false); + }); + + test('can sort null correctly', () => { + expect(sort(null)).toEqual(null); + }); + + test('can sort undefined correctly', () => { + expect(sort(undefined)).toEqual(undefined); + }); +}); From d7d78795265a64faf783324895d9ac7199448bbc Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Tue, 21 May 2024 20:53:10 +0600 Subject: [PATCH 3/8] test: sort function for nested JSON array/object --- packages/idempotency/tests/unit/sort.test.ts | 128 +++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/packages/idempotency/tests/unit/sort.test.ts b/packages/idempotency/tests/unit/sort.test.ts index 7b834cae6c..e4fe4e6ee6 100644 --- a/packages/idempotency/tests/unit/sort.test.ts +++ b/packages/idempotency/tests/unit/sort.test.ts @@ -32,4 +32,132 @@ describe('Function: sort', () => { test('can sort undefined correctly', () => { expect(sort(undefined)).toEqual(undefined); }); + + test('can sort object with nested keys correctly', () => { + // Prepare + const input = { + name: 'John', + age: 30, + city: 'New York', + address: { + street: '5th Avenue', + number: 123, + }, + }; + + // Act + const result = sort(input); + + // Assess + expect(JSON.stringify(result)).toEqual( + JSON.stringify({ + address: { + number: 123, + street: '5th Avenue', + }, + age: 30, + city: 'New York', + name: 'John', + }) + ); + }); + + test('can sort deeply nested structures', () => { + // Prepare + const input = { + z: [{ b: { d: 4, c: 3 }, a: { f: 6, e: 5 } }], + a: { c: 3, b: 2, a: 1 }, + }; + + // Act + const result = sort(input); + + //Assess + expect(JSON.stringify(result)).toEqual( + JSON.stringify({ + a: { a: 1, b: 2, c: 3 }, + z: [{ a: { e: 5, f: 6 }, b: { c: 3, d: 4 } }], + }) + ); + }); + + test('can sort JSON array with objects containing words as keys and nested objects/arrays correctly', () => { + // Prepare + const input = [ + { + transactions: [ + 50, + 40, + { field: 'a', category: 'x', purpose: 's' }, + [ + { + zone: 'c', + warehouse: 'd', + attributes: { region: 'a', quality: 'x', batch: 's' }, + }, + ], + ], + totalAmount: 30, + customerName: 'John', + location: 'New York', + transactionType: 'a', + }, + { + customerName: 'John', + location: 'New York', + transactionDetails: [ + { field: 'a', category: 'x', purpose: 's' }, + null, + 50, + [{ zone: 'c', warehouse: 'd', attributes: 't' }], + 40, + ], + amount: 30, + }, + ]; + + // Act + const result = sort(input); + + // Assess + expect(JSON.stringify(result)).toEqual( + JSON.stringify([ + { + customerName: 'John', + location: 'New York', + totalAmount: 30, + transactions: [ + 50, + 40, + { category: 'x', field: 'a', purpose: 's' }, + [ + { + attributes: { batch: 's', quality: 'x', region: 'a' }, + warehouse: 'd', + zone: 'c', + }, + ], + ], + transactionType: 'a', + }, + { + amount: 30, + customerName: 'John', + location: 'New York', + transactionDetails: [ + { category: 'x', field: 'a', purpose: 's' }, + null, + 50, + [{ attributes: 't', warehouse: 'd', zone: 'c' }], + 40, + ], + }, + ]) + ); + }); + + test('handles empty objects and arrays correctly', () => { + expect(sort({})).toEqual({}); + expect(sort([])).toEqual([]); + }); }); From baf6e735337ef24836af950e22522017192f8464 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 22 May 2024 12:27:46 +0600 Subject: [PATCH 4/8] docs: deepSort function description --- packages/idempotency/src/deepSort.ts | 40 +++++++++++++++++++ packages/idempotency/src/sort.ts | 29 -------------- .../unit/{sort.test.ts => deepSort.test.ts} | 31 +++++++------- 3 files changed, 55 insertions(+), 45 deletions(-) create mode 100644 packages/idempotency/src/deepSort.ts delete mode 100644 packages/idempotency/src/sort.ts rename packages/idempotency/tests/unit/{sort.test.ts => deepSort.test.ts} (83%) diff --git a/packages/idempotency/src/deepSort.ts b/packages/idempotency/src/deepSort.ts new file mode 100644 index 0000000000..73523d0a9c --- /dev/null +++ b/packages/idempotency/src/deepSort.ts @@ -0,0 +1,40 @@ +import { getType } from '@aws-lambda-powertools/commons'; +import { + JSONArray, + JSONObject, + JSONValue, +} from '@aws-lambda-powertools/commons/types'; + +const sortObject = (object: JSONObject): JSONObject => { + return Object.keys(object) + .sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : 1)) + .reduce((acc, key) => { + acc[key] = deepSort(object[key]); + + return acc; + }, {} as JSONObject); +}; + +/** + * Recursively sorts the keys of an object or elements of an array. + * + * This function sorts the keys of any JSON object in a case-insensitive manner, + * and recursively applies the same sorting to nested objects and arrays. + * Primitives (strings, numbers, booleans, null) are returned unchanged. + * + * @param {JSONValue} data - The input data to be sorted, which can be an object, array, or primitive value. + * @returns {JSONValue} - The sorted data, with all objects' keys sorted alphabetically in a case-insensitive manner. + */ + +const deepSort = (data: JSONValue): JSONValue => { + const type = getType(data); + if (type === 'object') { + return sortObject(data as JSONObject); + } else if (type === 'array') { + return (data as JSONArray).map(deepSort); + } + + return data; +}; + +export { deepSort }; diff --git a/packages/idempotency/src/sort.ts b/packages/idempotency/src/sort.ts deleted file mode 100644 index 84fb59f09c..0000000000 --- a/packages/idempotency/src/sort.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { getType } from '@aws-lambda-powertools/commons'; -import { - JSONArray, - JSONObject, - JSONValue, -} from '@aws-lambda-powertools/commons/types'; - -const sortObject = (object: JSONObject): JSONObject => { - return Object.keys(object) - .sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : 1)) - .reduce((acc, key) => { - acc[key] = sort(object[key]); - - return acc; - }, {} as JSONObject); -}; - -const sort = (data: JSONValue): JSONValue => { - const type = getType(data); - if (type === 'object') { - return sortObject(data as JSONObject); - } else if (type === 'array') { - return (data as JSONArray).map(sort); - } - - return data; -}; - -export { sort }; diff --git a/packages/idempotency/tests/unit/sort.test.ts b/packages/idempotency/tests/unit/deepSort.test.ts similarity index 83% rename from packages/idempotency/tests/unit/sort.test.ts rename to packages/idempotency/tests/unit/deepSort.test.ts index e4fe4e6ee6..3688851875 100644 --- a/packages/idempotency/tests/unit/sort.test.ts +++ b/packages/idempotency/tests/unit/deepSort.test.ts @@ -1,36 +1,35 @@ /** - * Test sort function + * Test deepSort Function * - * @group unit/commons/sort + * @group unit/idempotency/deepSort */ +import { deepSort } from '../../src/deepSort'; -import { sort } from '../../src/sort'; - -describe('Function: sort', () => { +describe('Function: deepSort', () => { beforeEach(() => { jest.clearAllMocks(); jest.resetModules(); }); test('can sort string correctly', () => { - expect(sort('test')).toEqual('test'); + expect(deepSort('test')).toEqual('test'); }); test('can sort number correctly', () => { - expect(sort(5)).toEqual(5); + expect(deepSort(5)).toEqual(5); }); test('can sort boolean correctly', () => { - expect(sort(true)).toEqual(true); - expect(sort(false)).toEqual(false); + expect(deepSort(true)).toEqual(true); + expect(deepSort(false)).toEqual(false); }); test('can sort null correctly', () => { - expect(sort(null)).toEqual(null); + expect(deepSort(null)).toEqual(null); }); test('can sort undefined correctly', () => { - expect(sort(undefined)).toEqual(undefined); + expect(deepSort(undefined)).toEqual(undefined); }); test('can sort object with nested keys correctly', () => { @@ -46,7 +45,7 @@ describe('Function: sort', () => { }; // Act - const result = sort(input); + const result = deepSort(input); // Assess expect(JSON.stringify(result)).toEqual( @@ -70,7 +69,7 @@ describe('Function: sort', () => { }; // Act - const result = sort(input); + const result = deepSort(input); //Assess expect(JSON.stringify(result)).toEqual( @@ -117,7 +116,7 @@ describe('Function: sort', () => { ]; // Act - const result = sort(input); + const result = deepSort(input); // Assess expect(JSON.stringify(result)).toEqual( @@ -157,7 +156,7 @@ describe('Function: sort', () => { }); test('handles empty objects and arrays correctly', () => { - expect(sort({})).toEqual({}); - expect(sort([])).toEqual([]); + expect(deepSort({})).toEqual({}); + expect(deepSort([])).toEqual([]); }); }); From d6f52cf55adcca46b00deb9a5fc02594130816c5 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 22 May 2024 12:28:31 +0600 Subject: [PATCH 5/8] test: remove jest beforeEach function --- packages/idempotency/tests/unit/deepSort.test.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/idempotency/tests/unit/deepSort.test.ts b/packages/idempotency/tests/unit/deepSort.test.ts index 3688851875..59a8a2cfb1 100644 --- a/packages/idempotency/tests/unit/deepSort.test.ts +++ b/packages/idempotency/tests/unit/deepSort.test.ts @@ -6,11 +6,6 @@ import { deepSort } from '../../src/deepSort'; describe('Function: deepSort', () => { - beforeEach(() => { - jest.clearAllMocks(); - jest.resetModules(); - }); - test('can sort string correctly', () => { expect(deepSort('test')).toEqual('test'); }); From c55e2ede3f87615515b5e1b288ca6516d456edc5 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 22 May 2024 14:19:38 +0600 Subject: [PATCH 6/8] feat(idempotency): deep sort the data before generating hash --- packages/idempotency/src/persistence/BasePersistenceLayer.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/idempotency/src/persistence/BasePersistenceLayer.ts b/packages/idempotency/src/persistence/BasePersistenceLayer.ts index 1456d70f12..d229e140bb 100644 --- a/packages/idempotency/src/persistence/BasePersistenceLayer.ts +++ b/packages/idempotency/src/persistence/BasePersistenceLayer.ts @@ -15,6 +15,7 @@ import { } from '../errors.js'; import { LRUCache } from './LRUCache.js'; import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import { deepSort } from '../deepSort.js'; /** * Base class for all persistence layers. This class provides the basic functionality for @@ -301,7 +302,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { } return `${this.idempotencyKeyPrefix}#${this.generateHash( - JSON.stringify(data) + JSON.stringify(deepSort(data)) )}`; } @@ -318,7 +319,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { this.#jmesPathOptions ) as JSONValue; - return this.generateHash(JSON.stringify(data)); + return this.generateHash(JSON.stringify(deepSort(data))); } else { return ''; } From 7b41f6a7be432d609702947dcc4263e6ee1d16df Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 22 May 2024 14:22:45 +0600 Subject: [PATCH 7/8] doc: minor description change for deepSort function --- packages/idempotency/src/deepSort.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/idempotency/src/deepSort.ts b/packages/idempotency/src/deepSort.ts index 73523d0a9c..caa442ad01 100644 --- a/packages/idempotency/src/deepSort.ts +++ b/packages/idempotency/src/deepSort.ts @@ -18,12 +18,11 @@ const sortObject = (object: JSONObject): JSONObject => { /** * Recursively sorts the keys of an object or elements of an array. * - * This function sorts the keys of any JSON object in a case-insensitive manner, - * and recursively applies the same sorting to nested objects and arrays. - * Primitives (strings, numbers, booleans, null) are returned unchanged. + * This function sorts the keys of any JSON in a case-insensitive manner and recursively applies the same sorting to + * nested objects and arrays. Primitives (strings, numbers, booleans, null) are returned unchanged. * - * @param {JSONValue} data - The input data to be sorted, which can be an object, array, or primitive value. - * @returns {JSONValue} - The sorted data, with all objects' keys sorted alphabetically in a case-insensitive manner. + * @param {JSONValue} data - The input data to be sorted, which can be an object, array or primitive value. + * @returns {JSONValue} - The sorted data, with all object's keys sorted alphabetically in a case-insensitive manner. */ const deepSort = (data: JSONValue): JSONValue => { From fa88b55015ab4b50ab74082b62ed8f6ce1ff4a6c Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 23 May 2024 20:31:15 +0600 Subject: [PATCH 8/8] chore: comment about sortObject function --- packages/idempotency/src/deepSort.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/idempotency/src/deepSort.ts b/packages/idempotency/src/deepSort.ts index caa442ad01..29e052b1a4 100644 --- a/packages/idempotency/src/deepSort.ts +++ b/packages/idempotency/src/deepSort.ts @@ -5,15 +5,23 @@ import { JSONValue, } from '@aws-lambda-powertools/commons/types'; -const sortObject = (object: JSONObject): JSONObject => { - return Object.keys(object) +/** + * Sorts the keys of a provided object in a case-insensitive manner. + * + * This function takes an object as input, sorts its keys alphabetically without + * considering case sensitivity and recursively sorts any nested objects or arrays. + * + * @param {JSONObject} object - The JSON object to be sorted. + * @returns {JSONObject} - A new JSON object with all keys sorted alphabetically in a case-insensitive manner. + */ +const sortObject = (object: JSONObject): JSONObject => + Object.keys(object) .sort((a, b) => (a.toLowerCase() < b.toLowerCase() ? -1 : 1)) .reduce((acc, key) => { acc[key] = deepSort(object[key]); return acc; }, {} as JSONObject); -}; /** * Recursively sorts the keys of an object or elements of an array. @@ -24,7 +32,6 @@ const sortObject = (object: JSONObject): JSONObject => { * @param {JSONValue} data - The input data to be sorted, which can be an object, array or primitive value. * @returns {JSONValue} - The sorted data, with all object's keys sorted alphabetically in a case-insensitive manner. */ - const deepSort = (data: JSONValue): JSONValue => { const type = getType(data); if (type === 'object') {