-
Notifications
You must be signed in to change notification settings - Fork 150
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(idempotency): deep sort payload during hashing (#2570)
Co-authored-by: Andrea Amorosi <[email protected]>
- Loading branch information
1 parent
f958d52
commit 6765f35
Showing
3 changed files
with
206 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { getType } from '@aws-lambda-powertools/commons'; | ||
import { | ||
JSONArray, | ||
JSONObject, | ||
JSONValue, | ||
} from '@aws-lambda-powertools/commons/types'; | ||
|
||
/** | ||
* 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. | ||
* | ||
* 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 object's 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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
/** | ||
* Test deepSort Function | ||
* | ||
* @group unit/idempotency/deepSort | ||
*/ | ||
import { deepSort } from '../../src/deepSort'; | ||
|
||
describe('Function: deepSort', () => { | ||
test('can sort string correctly', () => { | ||
expect(deepSort('test')).toEqual('test'); | ||
}); | ||
|
||
test('can sort number correctly', () => { | ||
expect(deepSort(5)).toEqual(5); | ||
}); | ||
|
||
test('can sort boolean correctly', () => { | ||
expect(deepSort(true)).toEqual(true); | ||
expect(deepSort(false)).toEqual(false); | ||
}); | ||
|
||
test('can sort null correctly', () => { | ||
expect(deepSort(null)).toEqual(null); | ||
}); | ||
|
||
test('can sort undefined correctly', () => { | ||
expect(deepSort(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 = deepSort(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 = deepSort(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 = deepSort(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(deepSort({})).toEqual({}); | ||
expect(deepSort([])).toEqual([]); | ||
}); | ||
}); |