diff --git a/package-lock.json b/package-lock.json index 64c02ad3..539555ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@croct-tech/time": "^0.8.0", "@croct/json": "^2.0.0", "@croct/logging": "^0.2.2", - "object-hash": "^3.0.0" + "node-object-hash": "^3.0.0" }, "devDependencies": { "@croct/eslint-plugin": "^0.6.0", @@ -4949,6 +4949,15 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node_modules/node-object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-object-hash/-/node-object-hash-3.0.0.tgz", + "integrity": "sha512-jLF6tlyletktvSAawuPmH1SReP0YfZQ+tBrDiTCK+Ai7eXPMS9odi5xW/iKC7ZhrWJJ0Z5xYcW/x+1fVMn1Qvw==", + "engines": { + "node": ">=16", + "pnpm": ">=8" + } + }, "node_modules/node-releases": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", @@ -4985,14 +4994,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", @@ -10020,6 +10021,11 @@ "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", "dev": true }, + "node-object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/node-object-hash/-/node-object-hash-3.0.0.tgz", + "integrity": "sha512-jLF6tlyletktvSAawuPmH1SReP0YfZQ+tBrDiTCK+Ai7eXPMS9odi5xW/iKC7ZhrWJJ0Z5xYcW/x+1fVMn1Qvw==" + }, "node-releases": { "version": "2.0.12", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", @@ -10047,11 +10053,6 @@ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", "dev": true }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" - }, "object-inspect": { "version": "1.12.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", diff --git a/package.json b/package.json index 811d8313..df98d620 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "@croct-tech/time": "^0.8.0", "@croct/json": "^2.0.0", "@croct/logging": "^0.2.2", - "object-hash": "^3.0.0" + "node-object-hash": "^3.0.0" }, "devDependencies": { "@croct/eslint-plugin": "^0.6.0", diff --git a/src/adapted.ts b/src/adapted.ts index 71ca045b..8b3d648f 100644 --- a/src/adapted.ts +++ b/src/adapted.ts @@ -1,11 +1,30 @@ import {JsonCompatible, JsonValue} from '@croct/json'; -import * as hash from 'object-hash'; +import {hasher, HasherOptions} from 'node-object-hash'; import {CacheLoader, CacheProvider} from './cacheProvider'; export type Transformer = (value: D) => S; export type HashAlgorithm = 'passthrough' | 'md5' | 'sha1'; +const HASHER = hasher({ + // Support classes and instances like `ResourceId` + coerce: true, + + // Do not differentiate Objects, Sets and Maps constructed in different order + // `new Set([1,2,3])` should match `new Set([3,2,1])` + sort: { + array: false, + typedArray: false, + object: true, + set: true, + map: true, + bigint: true, + }, + + // Do not trim values, "foo " and "foo" should not match as the same key + trim: false, +}); + type Configuration = { cache: CacheProvider, keyTransformer: Transformer, @@ -82,12 +101,17 @@ export class AdaptedCache implements CacheProvider { } public static createHashSerializer(algorithm?: HashAlgorithm): Transformer { - const options: hash.Options = { - encoding: 'base64', - algorithm: algorithm, + if (algorithm === 'passthrough') { + // Do not hash when algorithm is set to `passthrough` + return HASHER.sort.bind(HASHER); + } + + const options: HasherOptions = { + enc: 'base64', + alg: algorithm, }; - return (value: any): string => hash(value, options); + return (value: any): string => HASHER.hash(value, options); } /** diff --git a/test/adapted.test.ts b/test/adapted.test.ts index f1dfb44d..b301e536 100644 --- a/test/adapted.test.ts +++ b/test/adapted.test.ts @@ -114,6 +114,33 @@ describe('A cache adapter that can transform keys and values', () => { expect(mockCache.set).toHaveBeenCalledWith('key', 'transformed'); }); + it('should transform a value into a hash-able string', () => { + const transformer = AdaptedCache.createHashSerializer('passthrough'); + + const value = { + some: { + deeply: { + nested: [ + 'value', + ], + }, + }, + with: 1, + multiple: true, + keys: null, + andTypes: [ + 'string', + 1, + true, + null, + ], + }; + + const result = transformer(value); + + expect(result).toBe('{andTypes:[string,1,1,],keys:,multiple:1,some:{deeply:{nested:[value]}},with:1}'); + }); + it('should transform a value into a hash', () => { const transformer = AdaptedCache.createHashSerializer('md5'); @@ -138,7 +165,7 @@ describe('A cache adapter that can transform keys and values', () => { const result = transformer(value); - expect(result).toBe('DB8pVafUNdepTqZc8eE5qw=='); + expect(result).toBe('oDA+C/1fqcOT90c6vwhaWg=='); }); it('should transform a value into its JSON representation', () => {