From ca58f7562b8728c5edf1a7612d0bb26111c676e6 Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Sun, 26 Jan 2025 13:03:14 +0400 Subject: [PATCH 1/7] test(idempotency): add tests --- .../src/types/BasePersistenceLayer.ts | 1 + .../persistence/BasePersistenceLayer.test.ts | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/packages/idempotency/src/types/BasePersistenceLayer.ts b/packages/idempotency/src/types/BasePersistenceLayer.ts index a37440873e..c3c7f19e55 100644 --- a/packages/idempotency/src/types/BasePersistenceLayer.ts +++ b/packages/idempotency/src/types/BasePersistenceLayer.ts @@ -4,6 +4,7 @@ import type { IdempotencyRecord } from '../persistence/IdempotencyRecord.js'; type BasePersistenceLayerOptions = { config: IdempotencyConfig; functionName?: string; + keyPrefix?: string; }; interface BasePersistenceLayerInterface { diff --git a/packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts b/packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts index 65cb40be11..924f44ff31 100644 --- a/packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts +++ b/packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts @@ -80,6 +80,34 @@ describe('Class: BasePersistenceLayer', () => { ); }); + it('should trim function name before appending as key prefix', () => { + // Prepare + const config = new IdempotencyConfig({}); + const persistenceLayer = new PersistenceLayerTestClass(); + + // Act + persistenceLayer.configure({ config, functionName: ' my-function ' }); + + // Assess + expect(persistenceLayer.idempotencyKeyPrefix).toBe( + 'my-lambda-function.my-function' + ); + }); + + it('appends custom prefix to the idempotence key prefix', () => { + // Prepare + const config = new IdempotencyConfig({}); + const persistenceLayer = new PersistenceLayerTestClass(); + + // Act + persistenceLayer.configure({ config, keyPrefix: 'my-custom-prefix' }); + + // Assess + expect(persistenceLayer.idempotencyKeyPrefix).toBe( + 'my-custom-prefix' + ); + }); + it('uses default config when no option is provided', () => { // Prepare const config = new IdempotencyConfig({}); From b58a8acd81db984147d27d8d9f2041a1a26a0972 Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Sun, 26 Jan 2025 13:34:07 +0400 Subject: [PATCH 2/7] refactor: change parameter name --- .../src/persistence/BasePersistenceLayer.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/idempotency/src/persistence/BasePersistenceLayer.ts b/packages/idempotency/src/persistence/BasePersistenceLayer.ts index 51e48f8b4f..3f3d78bfcc 100644 --- a/packages/idempotency/src/persistence/BasePersistenceLayer.ts +++ b/packages/idempotency/src/persistence/BasePersistenceLayer.ts @@ -46,15 +46,15 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { /** * Initialize the base persistence layer from the configuration settings * - * @param {BasePersistenceLayerConfigureOptions} config - configuration object for the persistence layer + * @param {BasePersistenceLayerConfigureOptions} options - configuration object for the persistence layer */ - public configure(config: BasePersistenceLayerOptions): void { - // Extracting the idempotency config from the config object for easier access - const { config: idempotencyConfig } = config; - - if (config?.functionName && config.functionName.trim() !== '') { - this.idempotencyKeyPrefix = `${this.idempotencyKeyPrefix}.${config.functionName}`; - } + public configure(options: BasePersistenceLayerOptions): void { + // Extracting the idempotency config from the config object for easier access + const { config: idempotencyConfig } = options; + + if (options?.functionName && options.functionName.trim() !== '') { + this.idempotencyKeyPrefix = `${this.idempotencyKeyPrefix}.${options.functionName}`; + } // Prevent reconfiguration if (this.configured) { From 4479fa6afd3e1e013a85498cf7ed1b1f0eb0fd07 Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Sat, 1 Feb 2025 15:01:07 +0400 Subject: [PATCH 3/7] test(idempotency): add keyPrefix test for decorator --- .../tests/unit/idempotencyDecorator.test.ts | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/packages/idempotency/tests/unit/idempotencyDecorator.test.ts b/packages/idempotency/tests/unit/idempotencyDecorator.test.ts index 8a64b19afe..b8b2347156 100644 --- a/packages/idempotency/tests/unit/idempotencyDecorator.test.ts +++ b/packages/idempotency/tests/unit/idempotencyDecorator.test.ts @@ -1,9 +1,10 @@ import type { LambdaInterface } from '@aws-lambda-powertools/commons/types'; import context from '@aws-lambda-powertools/testing-utils/context'; import type { Context } from 'aws-lambda'; -import { describe, expect, it } from 'vitest'; -import { idempotent } from '../../src/index.js'; +import { describe, expect, it, vi } from 'vitest'; +import { idempotent, IdempotencyConfig } from '../../src/index.js'; import { PersistenceLayerTestClass } from '../helpers/idempotencyUtils.js'; +import { BasePersistenceLayer } from '../../src/persistence/BasePersistenceLayer.js'; describe('Given a class with a function to decorate', () => { it('maintains the scope of the decorated function', async () => { @@ -35,4 +36,42 @@ describe('Given a class with a function to decorate', () => { // Assess expect(result).toBe('private foo'); }); + + it('configure persistenceStore idempotency key with custom keyPrefix', async () => { + // Prepare + const configureSpy = vi.spyOn(BasePersistenceLayer.prototype, 'configure'); + const idempotencyConfig = new IdempotencyConfig({}); + + class TestClass implements LambdaInterface { + @idempotent({ + persistenceStore: new PersistenceLayerTestClass(), + config: idempotencyConfig, + keyPrefix: 'my-custom-prefix', + }) + public async handler( + _event: unknown, + _context: Context + ): Promise { + return true; + } + } + + const handlerClass = new TestClass(); + const handler = handlerClass.handler.bind(handlerClass); + + // Act + const result = await handler({}, context); + + + // Assert + expect(result).toBeTruthy(); + + expect(configureSpy).toHaveBeenCalled(); + const configureCallArgs = configureSpy.mock.calls[0][0]; // Extract first call's arguments + expect(configureCallArgs.config).toBe(idempotencyConfig); + expect(configureCallArgs.keyPrefix).toBe('my-custom-prefix'); + + // Restore the spy + configureSpy.mockRestore(); + }); }); From 7f8a1f4d7da1fd4abf3b4640fa22d3e8e0ef658f Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Sat, 1 Feb 2025 15:05:09 +0400 Subject: [PATCH 4/7] feat(idempotency): add custom idempotency key support with keyPrefix option --- packages/idempotency/src/IdempotencyHandler.ts | 7 +++++++ packages/idempotency/src/makeIdempotent.ts | 3 ++- .../src/persistence/BasePersistenceLayer.ts | 16 +++++++++------- .../idempotency/src/types/IdempotencyOptions.ts | 5 +++++ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/idempotency/src/IdempotencyHandler.ts b/packages/idempotency/src/IdempotencyHandler.ts index 3c64a92a6b..90b7372317 100644 --- a/packages/idempotency/src/IdempotencyHandler.ts +++ b/packages/idempotency/src/IdempotencyHandler.ts @@ -51,6 +51,10 @@ export class IdempotencyHandler { * Idempotency configuration options. */ readonly #idempotencyConfig: IdempotencyConfig; + /** + * Custom prefix to be used when generating the idempotency key. + */ + readonly #keyPrefix: string | undefined; /** * Persistence layer used to store the idempotency records. */ @@ -69,11 +73,13 @@ export class IdempotencyHandler { idempotencyConfig, functionArguments, persistenceStore, + keyPrefix, thisArg, } = options; this.#functionToMakeIdempotent = functionToMakeIdempotent; this.#functionPayloadToBeHashed = functionPayloadToBeHashed; this.#idempotencyConfig = idempotencyConfig; + this.#keyPrefix = keyPrefix; this.#functionArguments = functionArguments; this.#thisArg = thisArg; @@ -81,6 +87,7 @@ export class IdempotencyHandler { this.#persistenceStore.configure({ config: this.#idempotencyConfig, + keyPrefix: this.#keyPrefix, }); } diff --git a/packages/idempotency/src/makeIdempotent.ts b/packages/idempotency/src/makeIdempotent.ts index 251a72a8f8..31718d80da 100644 --- a/packages/idempotency/src/makeIdempotent.ts +++ b/packages/idempotency/src/makeIdempotent.ts @@ -79,7 +79,7 @@ function makeIdempotent( fn: Func, options: ItempotentFunctionOptions> ): (...args: Parameters) => ReturnType { - const { persistenceStore, config } = options; + const { persistenceStore, config, keyPrefix } = options; const idempotencyConfig = config ? config : new IdempotencyConfig({}); if (!idempotencyConfig.isEnabled()) return fn; @@ -102,6 +102,7 @@ function makeIdempotent( functionToMakeIdempotent: fn, idempotencyConfig: idempotencyConfig, persistenceStore: persistenceStore, + keyPrefix: keyPrefix, functionArguments: args, functionPayloadToBeHashed, thisArg: this, diff --git a/packages/idempotency/src/persistence/BasePersistenceLayer.ts b/packages/idempotency/src/persistence/BasePersistenceLayer.ts index 3f3d78bfcc..6a2a95f4e7 100644 --- a/packages/idempotency/src/persistence/BasePersistenceLayer.ts +++ b/packages/idempotency/src/persistence/BasePersistenceLayer.ts @@ -48,13 +48,15 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { * * @param {BasePersistenceLayerConfigureOptions} options - configuration object for the persistence layer */ - public configure(options: BasePersistenceLayerOptions): void { - // Extracting the idempotency config from the config object for easier access - const { config: idempotencyConfig } = options; - - if (options?.functionName && options.functionName.trim() !== '') { - this.idempotencyKeyPrefix = `${this.idempotencyKeyPrefix}.${options.functionName}`; - } + public configure(options: BasePersistenceLayerOptions): void { + // Extracting the idempotency configuration from the options for easier access + const { config: idempotencyConfig, keyPrefix, functionName } = options; + + if (keyPrefix?.trim()) { + this.idempotencyKeyPrefix = keyPrefix.trim(); + } else if (functionName?.trim()) { + this.idempotencyKeyPrefix = `${this.idempotencyKeyPrefix}.${functionName.trim()}`; + } // Prevent reconfiguration if (this.configured) { diff --git a/packages/idempotency/src/types/IdempotencyOptions.ts b/packages/idempotency/src/types/IdempotencyOptions.ts index b366abb526..4c48e25579 100644 --- a/packages/idempotency/src/types/IdempotencyOptions.ts +++ b/packages/idempotency/src/types/IdempotencyOptions.ts @@ -19,6 +19,7 @@ import type { IdempotencyRecord } from '../persistence/IdempotencyRecord.js'; type IdempotencyLambdaHandlerOptions = { persistenceStore: BasePersistenceLayer; config?: IdempotencyConfig; + keyPrefix?: string; }; /** @@ -137,6 +138,10 @@ type IdempotencyHandlerOptions = { * Idempotency configuration options. */ idempotencyConfig: IdempotencyConfig; + /** + * The custom idempotency key prefix. + */ + keyPrefix?: string; /** * Persistence layer used to store the idempotency records. */ From 06bb1cca4ac512eb755445576585d3bf14fe6a87 Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Sat, 15 Feb 2025 11:22:41 +0400 Subject: [PATCH 5/7] test(idempotency): add keyPrefix test for middy and wrapper --- .../tests/unit/idempotencyDecorator.test.ts | 1 - .../tests/unit/makeIdempotent.test.ts | 39 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/packages/idempotency/tests/unit/idempotencyDecorator.test.ts b/packages/idempotency/tests/unit/idempotencyDecorator.test.ts index b8b2347156..26ff3bc091 100644 --- a/packages/idempotency/tests/unit/idempotencyDecorator.test.ts +++ b/packages/idempotency/tests/unit/idempotencyDecorator.test.ts @@ -61,7 +61,6 @@ describe('Given a class with a function to decorate', () => { // Act const result = await handler({}, context); - // Assert expect(result).toBeTruthy(); diff --git a/packages/idempotency/tests/unit/makeIdempotent.test.ts b/packages/idempotency/tests/unit/makeIdempotent.test.ts index 4fdc58cb65..1e1a1e2054 100644 --- a/packages/idempotency/tests/unit/makeIdempotent.test.ts +++ b/packages/idempotency/tests/unit/makeIdempotent.test.ts @@ -393,6 +393,45 @@ describe('Function: makeIdempotent', () => { expect(saveSuccessSpy).toHaveBeenCalledTimes(0); } ); + + it.each([ + { + type: 'wrapper', + }, + { type: 'middleware' }, + ])( + 'passes keyPrefix correctly in idempotency handler ($type)', + async ({ type }) => { + // Prepare + const keyPrefix = 'my-custom-prefix'; + const options = { + ...mockIdempotencyOptions, + keyPrefix, + config: new IdempotencyConfig({ + eventKeyJmesPath: 'idempotencyKey', + }), + }; + const handler = + type === 'wrapper' + ? makeIdempotent(fnSuccessfull, options) + : middy(fnSuccessfull).use(makeHandlerIdempotent(options)); + + const configureSpy = vi.spyOn( + mockIdempotencyOptions.persistenceStore, + 'configure' + ); + + // Act + const result = await handler(event, context); + + // Assess + expect(result).toBe(true); + expect(configureSpy).toHaveBeenCalledWith( + expect.objectContaining({ keyPrefix }) + ); + } + ); + it('uses the first argument when when wrapping an arbitrary function', async () => { // Prepare const config = new IdempotencyConfig({}); From b479a08e469da68bc6390512604cc86508644ae4 Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Sat, 15 Feb 2025 11:23:54 +0400 Subject: [PATCH 6/7] feat(idempotency): add custom idempotency key for middleware --- packages/idempotency/src/middleware/makeHandlerIdempotent.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/idempotency/src/middleware/makeHandlerIdempotent.ts b/packages/idempotency/src/middleware/makeHandlerIdempotent.ts index 11958b3e3b..3803062a01 100644 --- a/packages/idempotency/src/middleware/makeHandlerIdempotent.ts +++ b/packages/idempotency/src/middleware/makeHandlerIdempotent.ts @@ -117,8 +117,10 @@ const makeHandlerIdempotent = ( ? options.config : new IdempotencyConfig({}); const persistenceStore = options.persistenceStore; + const keyPrefix = options.keyPrefix; persistenceStore.configure({ config: idempotencyConfig, + keyPrefix: keyPrefix, }); const idempotencyHandler = new IdempotencyHandler({ @@ -126,6 +128,7 @@ const makeHandlerIdempotent = ( functionArguments: [], idempotencyConfig, persistenceStore, + keyPrefix, functionPayloadToBeHashed: undefined, }); setIdempotencyHandlerInRequestInternal(request, idempotencyHandler); From c167390e8e8f3bfc33085f7fa229d096e5fcae27 Mon Sep 17 00:00:00 2001 From: Sergei Cherniaev Date: Sat, 15 Feb 2025 19:12:25 +0300 Subject: [PATCH 7/7] apply suggestions from code review Co-authored-by: Andrea Amorosi --- packages/idempotency/src/persistence/BasePersistenceLayer.ts | 1 - packages/idempotency/tests/unit/idempotencyDecorator.test.ts | 4 ++-- .../tests/unit/persistence/BasePersistenceLayer.test.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/idempotency/src/persistence/BasePersistenceLayer.ts b/packages/idempotency/src/persistence/BasePersistenceLayer.ts index 9f589beda8..0c13a6d753 100644 --- a/packages/idempotency/src/persistence/BasePersistenceLayer.ts +++ b/packages/idempotency/src/persistence/BasePersistenceLayer.ts @@ -49,7 +49,6 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface { * @param {BasePersistenceLayerConfigureOptions} options - configuration object for the persistence layer */ public configure(options: BasePersistenceLayerOptions): void { - // Extracting the idempotency configuration from the options for easier access const { config: idempotencyConfig, keyPrefix, functionName } = options; if (keyPrefix?.trim()) { diff --git a/packages/idempotency/tests/unit/idempotencyDecorator.test.ts b/packages/idempotency/tests/unit/idempotencyDecorator.test.ts index 26ff3bc091..7f32a4e98c 100644 --- a/packages/idempotency/tests/unit/idempotencyDecorator.test.ts +++ b/packages/idempotency/tests/unit/idempotencyDecorator.test.ts @@ -37,7 +37,7 @@ describe('Given a class with a function to decorate', () => { expect(result).toBe('private foo'); }); - it('configure persistenceStore idempotency key with custom keyPrefix', async () => { + it('passes the custom keyPrefix to the persistenceStore', async () => { // Prepare const configureSpy = vi.spyOn(BasePersistenceLayer.prototype, 'configure'); const idempotencyConfig = new IdempotencyConfig({}); @@ -62,7 +62,7 @@ describe('Given a class with a function to decorate', () => { // Act const result = await handler({}, context); - // Assert + // Assess expect(result).toBeTruthy(); expect(configureSpy).toHaveBeenCalled(); diff --git a/packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts b/packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts index 924f44ff31..2a2d5e1a56 100644 --- a/packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts +++ b/packages/idempotency/tests/unit/persistence/BasePersistenceLayer.test.ts @@ -80,7 +80,7 @@ describe('Class: BasePersistenceLayer', () => { ); }); - it('should trim function name before appending as key prefix', () => { + it('trims the function name before appending as key prefix', () => { // Prepare const config = new IdempotencyConfig({}); const persistenceLayer = new PersistenceLayerTestClass();