From 80609af67cf57932f9d7e8058fde1661818ad75b Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Thu, 12 Sep 2024 21:38:09 +0600 Subject: [PATCH 01/13] feat: `responseHook` in `IdempotencyConfig` --- packages/idempotency/src/IdempotencyConfig.ts | 9 ++++++++- packages/idempotency/src/types/IdempotencyOptions.ts | 10 ++++++++++ packages/idempotency/src/types/index.ts | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/idempotency/src/IdempotencyConfig.ts b/packages/idempotency/src/IdempotencyConfig.ts index 043fa828f8..87a26aa878 100644 --- a/packages/idempotency/src/IdempotencyConfig.ts +++ b/packages/idempotency/src/IdempotencyConfig.ts @@ -2,7 +2,10 @@ import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions'; import type { JMESPathParsingOptions } from '@aws-lambda-powertools/jmespath/types'; import type { Context } from 'aws-lambda'; import { EnvironmentVariablesService } from './config/EnvironmentVariablesService.js'; -import type { IdempotencyConfigOptions } from './types/IdempotencyOptions.js'; +import type { + IdempotencyConfigOptions, + ResponseHook, +} from './types/IdempotencyOptions.js'; /** * Configuration for the idempotency feature. @@ -52,6 +55,10 @@ class IdempotencyConfig { * @default false */ public throwOnNoIdempotencyKey: boolean; + /** + * A hook that runs when an idempotent request is made. + */ + public responseHook?: ResponseHook; /** * Use the local cache to store idempotency keys. diff --git a/packages/idempotency/src/types/IdempotencyOptions.ts b/packages/idempotency/src/types/IdempotencyOptions.ts index 99736c6e22..6252081a09 100644 --- a/packages/idempotency/src/types/IdempotencyOptions.ts +++ b/packages/idempotency/src/types/IdempotencyOptions.ts @@ -2,6 +2,7 @@ import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import type { Context, Handler } from 'aws-lambda'; import type { IdempotencyConfig } from '../IdempotencyConfig.js'; import type { BasePersistenceLayer } from '../persistence/BasePersistenceLayer.js'; +import type { IdempotencyRecord } from '../persistence/IdempotencyRecord.js'; /** * Configuration options for the idempotency utility. @@ -185,10 +186,19 @@ type IdempotencyConfigOptions = { lambdaContext?: Context; }; +/** + * A hook that runs when an idempotent request is made. + */ +type ResponseHook = ( + response: JSONValue, + record: IdempotencyRecord +) => JSONValue; + export type { AnyFunction, IdempotencyConfigOptions, ItempotentFunctionOptions, IdempotencyLambdaHandlerOptions, IdempotencyHandlerOptions, + ResponseHook, }; diff --git a/packages/idempotency/src/types/index.ts b/packages/idempotency/src/types/index.ts index c390a7d185..0158d7855b 100644 --- a/packages/idempotency/src/types/index.ts +++ b/packages/idempotency/src/types/index.ts @@ -12,6 +12,7 @@ export type { IdempotencyHandlerOptions, ItempotentFunctionOptions, AnyFunction, + ResponseHook, } from './IdempotencyOptions.js'; export type { DynamoDBPersistenceOptions, From 81c98d6fd759b0fa33eea4d10853e265d4c93a59 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 13 Sep 2024 22:16:13 +0600 Subject: [PATCH 02/13] feat: `ResponseHook` type --- .../src/types/IdempotencyOptions.ts | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/idempotency/src/types/IdempotencyOptions.ts b/packages/idempotency/src/types/IdempotencyOptions.ts index 6252081a09..b9ca069fc9 100644 --- a/packages/idempotency/src/types/IdempotencyOptions.ts +++ b/packages/idempotency/src/types/IdempotencyOptions.ts @@ -148,6 +148,14 @@ type IdempotencyHandlerOptions = { thisArg?: Handler; }; +/** + * A hook that runs when an idempotent request is made. + */ +type ResponseHook = ( + response: JSONValue, + record: IdempotencyRecord +) => JSONValue; + /** * Idempotency configuration options */ @@ -184,16 +192,12 @@ type IdempotencyConfigOptions = { * AWS Lambda Context object containing information about the current invocation, function, and execution environment */ lambdaContext?: Context; + /** + * A hook that runs when an idempotent request is made + */ + responseHook?: ResponseHook; }; -/** - * A hook that runs when an idempotent request is made. - */ -type ResponseHook = ( - response: JSONValue, - record: IdempotencyRecord -) => JSONValue; - export type { AnyFunction, IdempotencyConfigOptions, From dc0ac3e1336d29213eaf179b9046355db5d03016 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 13 Sep 2024 22:16:44 +0600 Subject: [PATCH 03/13] fix: set `responseHook` property from config --- packages/idempotency/src/IdempotencyConfig.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/idempotency/src/IdempotencyConfig.ts b/packages/idempotency/src/IdempotencyConfig.ts index 87a26aa878..84e943d8ce 100644 --- a/packages/idempotency/src/IdempotencyConfig.ts +++ b/packages/idempotency/src/IdempotencyConfig.ts @@ -77,6 +77,7 @@ class IdempotencyConfig { this.maxLocalCacheSize = config.maxLocalCacheSize ?? 1000; this.hashFunction = config.hashFunction ?? 'md5'; this.lambdaContext = config.lambdaContext; + this.responseHook = config.responseHook; this.#envVarsService = new EnvironmentVariablesService(); this.#enabled = this.#envVarsService.getIdempotencyEnabled(); } From 526cdd23ee94fed1092d31ae3d123400f8d7eb3e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Fri, 13 Sep 2024 22:18:39 +0600 Subject: [PATCH 04/13] feat: call response hook when idempotency is hit --- packages/idempotency/src/IdempotencyHandler.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/idempotency/src/IdempotencyHandler.ts b/packages/idempotency/src/IdempotencyHandler.ts index 4dcfa03def..df1cf08077 100644 --- a/packages/idempotency/src/IdempotencyHandler.ts +++ b/packages/idempotency/src/IdempotencyHandler.ts @@ -92,7 +92,7 @@ export class IdempotencyHandler { * @param idempotencyRecord The idempotency record stored in the persistence layer * @returns The result of the function if the idempotency record is in a terminal state */ - public static determineResultFromIdempotencyRecord( + public determineResultFromIdempotencyRecord( idempotencyRecord: IdempotencyRecord ): JSONValue { if (idempotencyRecord.getStatus() === IdempotencyRecordStatus.EXPIRED) { @@ -115,7 +115,14 @@ export class IdempotencyHandler { ); } - return idempotencyRecord.getResponse(); + const response = idempotencyRecord.getResponse(); + + // If a response hook is provided, call it to allow the user to modify the response + if (this.#idempotencyConfig.responseHook) { + return this.#idempotencyConfig.responseHook(response, idempotencyRecord); + } + + return response; } /** @@ -381,9 +388,7 @@ export class IdempotencyHandler { returnValue.isIdempotent = true; returnValue.result = - IdempotencyHandler.determineResultFromIdempotencyRecord( - idempotencyRecord - ); + this.determineResultFromIdempotencyRecord(idempotencyRecord); return returnValue; } From 2c9f4a8c148b93e0643e028c16c6e9efd9e8876d Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sat, 14 Sep 2024 23:03:26 +0600 Subject: [PATCH 05/13] test: response hook trigger for idempotency --- .../tests/unit/IdempotencyHandler.test.ts | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts index 89e0f91d10..33df419306 100644 --- a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts @@ -64,7 +64,7 @@ describe('Class IdempotencyHandler', () => { expect(stubRecord.isExpired()).toBe(false); expect(stubRecord.getStatus()).toBe(IdempotencyRecordStatus.INPROGRESS); expect(() => - IdempotencyHandler.determineResultFromIdempotencyRecord(stubRecord) + idempotentHandler.determineResultFromIdempotencyRecord(stubRecord) ).toThrow(IdempotencyAlreadyInProgressError); }); @@ -83,7 +83,7 @@ describe('Class IdempotencyHandler', () => { expect(stubRecord.isExpired()).toBe(false); expect(stubRecord.getStatus()).toBe(IdempotencyRecordStatus.INPROGRESS); expect(() => - IdempotencyHandler.determineResultFromIdempotencyRecord(stubRecord) + idempotentHandler.determineResultFromIdempotencyRecord(stubRecord) ).toThrow(IdempotencyInconsistentStateError); }); @@ -102,9 +102,37 @@ describe('Class IdempotencyHandler', () => { expect(stubRecord.isExpired()).toBe(true); expect(stubRecord.getStatus()).toBe(IdempotencyRecordStatus.EXPIRED); expect(() => - IdempotencyHandler.determineResultFromIdempotencyRecord(stubRecord) + idempotentHandler.determineResultFromIdempotencyRecord(stubRecord) ).toThrow(IdempotencyInconsistentStateError); }); + + test('when response hook is provided, it should should call responseHook during an idempotent request', () => { + // Prepare + const stubRecord = new IdempotencyRecord({ + idempotencyKey: 'idempotencyKey', + responseData: { responseData: 'responseData' }, + payloadHash: 'payloadHash', + status: IdempotencyRecordStatus.COMPLETED, + }); + + const mockResponseHook = jest.fn(); + + const idempotentHandler = new IdempotencyHandler({ + functionToMakeIdempotent: mockFunctionToMakeIdempotent, + functionPayloadToBeHashed: mockFunctionPayloadToBeHashed, + persistenceStore: mockIdempotencyOptions.persistenceStore, + functionArguments: [], + idempotencyConfig: new IdempotencyConfig({ + responseHook: mockResponseHook, + }), + }); + + // Act + idempotentHandler.determineResultFromIdempotencyRecord(stubRecord); + + // Assess + expect(mockResponseHook).toHaveBeenCalled(); + }); }); describe('Method: handle', () => { From 8d2dbb5774352fc7f4819303f0e6407ec426da12 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sat, 14 Sep 2024 23:08:22 +0600 Subject: [PATCH 06/13] test: response hook should not be called in non-idempotent request --- .../tests/unit/IdempotencyHandler.test.ts | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts index 33df419306..080a9b0a37 100644 --- a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts @@ -16,12 +16,17 @@ import { IdempotencyRecord } from '../../src/persistence/index.js'; import { PersistenceLayerTestClass } from '../helpers/idempotencyUtils.js'; const mockFunctionToMakeIdempotent = jest.fn(); +const mockResponseHook = jest + .fn() + .mockImplementation((response, record) => response); const mockFunctionPayloadToBeHashed = {}; const persistenceStore = new PersistenceLayerTestClass(); const mockIdempotencyOptions = { persistenceStore, dataKeywordArgument: 'testKeywordArgument', - config: new IdempotencyConfig({}), + config: new IdempotencyConfig({ + responseHook: mockResponseHook, + }), }; const idempotentHandler = new IdempotencyHandler({ @@ -66,6 +71,7 @@ describe('Class IdempotencyHandler', () => { expect(() => idempotentHandler.determineResultFromIdempotencyRecord(stubRecord) ).toThrow(IdempotencyAlreadyInProgressError); + expect(mockResponseHook).not.toHaveBeenCalled(); }); test('when record is in progress and outside expiry window, it rejects with IdempotencyInconsistentStateError', async () => { @@ -85,6 +91,7 @@ describe('Class IdempotencyHandler', () => { expect(() => idempotentHandler.determineResultFromIdempotencyRecord(stubRecord) ).toThrow(IdempotencyInconsistentStateError); + expect(mockResponseHook).not.toHaveBeenCalled(); }); test('when record is expired, it rejects with IdempotencyInconsistentStateError', async () => { @@ -104,6 +111,7 @@ describe('Class IdempotencyHandler', () => { expect(() => idempotentHandler.determineResultFromIdempotencyRecord(stubRecord) ).toThrow(IdempotencyInconsistentStateError); + expect(mockResponseHook).not.toHaveBeenCalled(); }); test('when response hook is provided, it should should call responseHook during an idempotent request', () => { @@ -115,18 +123,6 @@ describe('Class IdempotencyHandler', () => { status: IdempotencyRecordStatus.COMPLETED, }); - const mockResponseHook = jest.fn(); - - const idempotentHandler = new IdempotencyHandler({ - functionToMakeIdempotent: mockFunctionToMakeIdempotent, - functionPayloadToBeHashed: mockFunctionPayloadToBeHashed, - persistenceStore: mockIdempotencyOptions.persistenceStore, - functionArguments: [], - idempotencyConfig: new IdempotencyConfig({ - responseHook: mockResponseHook, - }), - }); - // Act idempotentHandler.determineResultFromIdempotencyRecord(stubRecord); From 56791e6916709910221735d5ad440413ae2aab77 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sat, 14 Sep 2024 23:12:39 +0600 Subject: [PATCH 07/13] test: response hook can manipulate response when it's hit --- .../tests/unit/IdempotencyHandler.test.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts index 080a9b0a37..fc52df5569 100644 --- a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts @@ -1,3 +1,4 @@ +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; import { IdempotencyHandler } from '../../src/IdempotencyHandler.js'; import { IdempotencyRecordStatus, MAX_RETRIES } from '../../src/constants.js'; import { @@ -129,6 +130,68 @@ describe('Class IdempotencyHandler', () => { // Assess expect(mockResponseHook).toHaveBeenCalled(); }); + + test('when response hook is provided, it can manipulate response during an idempotent request', () => { + // Prepare + interface HandlerResponse { + message: string; + statusCode: number; + headers?: Record; + } + + const responseHook = jest + .fn() + .mockImplementation( + (response: JSONValue, record: IdempotencyRecord) => { + const handlerResponse = response as unknown as HandlerResponse; + handlerResponse.headers = { + 'x-idempotency-key': record.idempotencyKey, + }; + return handlerResponse as unknown as JSONValue; + } + ); + + const idempotentHandler = new IdempotencyHandler({ + functionToMakeIdempotent: mockFunctionToMakeIdempotent, + functionPayloadToBeHashed: mockFunctionPayloadToBeHashed, + persistenceStore: mockIdempotencyOptions.persistenceStore, + functionArguments: [], + idempotencyConfig: new IdempotencyConfig({ + responseHook, + }), + }); + + const mockResponse: HandlerResponse = { + message: 'Original message', + statusCode: 200, + }; + + const idempotencyRecord = { + getStatus: jest.fn().mockReturnValue(IdempotencyRecordStatus.COMPLETED), + getResponse: jest.fn().mockReturnValue(mockResponse), + idempotencyKey: 'test-key', + isExpired: jest.fn().mockReturnValue(false), + } as unknown as IdempotencyRecord; + + // Act + const result = + idempotentHandler.determineResultFromIdempotencyRecord( + idempotencyRecord + ); + + // Assess + expect(responseHook).toHaveBeenCalledWith( + mockResponse, + idempotencyRecord + ); + expect(result).toEqual({ + message: 'Original message', + statusCode: 200, + headers: { + 'x-idempotency-key': 'test-key', + }, + }); + }); }); describe('Method: handle', () => { From 9a4afe661139040db83630d9a3a81499f6ee1da0 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 15 Sep 2024 08:57:23 +0600 Subject: [PATCH 08/13] fix: make `stubRecord` similar to other tests --- .../tests/unit/IdempotencyHandler.test.ts | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts index fc52df5569..7c5f218a1a 100644 --- a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts @@ -161,29 +161,24 @@ describe('Class IdempotencyHandler', () => { }), }); - const mockResponse: HandlerResponse = { + const responseData = { message: 'Original message', statusCode: 200, }; - const idempotencyRecord = { - getStatus: jest.fn().mockReturnValue(IdempotencyRecordStatus.COMPLETED), - getResponse: jest.fn().mockReturnValue(mockResponse), + const stubRecord = new IdempotencyRecord({ idempotencyKey: 'test-key', - isExpired: jest.fn().mockReturnValue(false), - } as unknown as IdempotencyRecord; + responseData, + payloadHash: 'payloadHash', + status: IdempotencyRecordStatus.COMPLETED, + }); // Act const result = - idempotentHandler.determineResultFromIdempotencyRecord( - idempotencyRecord - ); + idempotentHandler.determineResultFromIdempotencyRecord(stubRecord); // Assess - expect(responseHook).toHaveBeenCalledWith( - mockResponse, - idempotencyRecord - ); + expect(responseHook).toHaveBeenCalledWith(responseData, stubRecord); expect(result).toEqual({ message: 'Original message', statusCode: 200, From a64bb9f212ab8d5a7e6facd22725d02d5ab15c11 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 15 Sep 2024 15:05:35 +0600 Subject: [PATCH 09/13] doc: `responseHook` related docs similar to python --- docs/utilities/idempotency.md | 65 +++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index 552fb4bcec..20d98ec316 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -371,6 +371,40 @@ sequenceDiagram Idempotent successful request cached +#### Successful request with responseHook configured + +
+```mermaid +sequenceDiagram + participant Client + participant Lambda + participant Response hook + participant Persistence Layer + alt initial request + Client->>Lambda: Invoke (event) + Lambda->>Persistence Layer: Get or set idempotency_key=hash(payload) + activate Persistence Layer + Note over Lambda,Persistence Layer: Set record status to INPROGRESS.
Prevents concurrent invocations
with the same payload + Lambda-->>Lambda: Call your function + Lambda->>Persistence Layer: Update record with result + deactivate Persistence Layer + Persistence Layer-->>Persistence Layer: Update record + Note over Lambda,Persistence Layer: Set record status to COMPLETE.
New invocations with the same payload
now return the same result + Lambda-->>Client: Response sent to client + else retried request + Client->>Lambda: Invoke (event) + Lambda->>Persistence Layer: Get or set idempotency_key=hash(payload) + activate Persistence Layer + Persistence Layer-->>Response hook: Already exists in persistence layer. + deactivate Persistence Layer + Note over Response hook,Persistence Layer: Record status is COMPLETE and not expired + Response hook->>Lambda: Response hook invoked + Lambda-->>Client: Manipulated idempotent response sent to client + end +``` +Successful idempotent request with a response hook +
+ #### Expired idempotency records
@@ -542,6 +576,7 @@ Idempotent decorator can be further configured with **`IdempotencyConfig`** as s | **useLocalCache** | `false` | Whether to locally cache idempotency results | | **localCacheMaxItems** | 256 | Max number of items to store in local cache | | **hashFunction** | `md5` | Function to use for calculating hashes, as provided by the [crypto](https://nodejs.org/api/crypto.html#cryptocreatehashalgorithm-options){target="_blank"} module in the standard library. | +| **responseHook** | `undefined` | Function to use for processing the stored Idempotent response. This function hook is called when an existing idempotent response is found. See [Manipulating The Idempotent Response](idempotency.md#manipulating-the-idempotent-response) | ### Handling concurrent executions with the same payload @@ -742,6 +777,36 @@ Below an example implementation of a custom persistence layer backed by a generi For example, the `_putRecord()` method needs to throw an error if a non-expired record already exists in the data store with a matching key. +### Manipulating the Idempotent Response + +You can set up a `responseHook` in the `IdempotentConfig` class to manipulate the returned data when an operation is idempotent. The hook function will be called with the current deserialized response object and the Idempotency record. + +=== "Using an Idempotent Response Hook" + + ```typescript hl_lines="20 22 28 36" + --8<-- "examples/snippets/idempotency/workingWithResponseHook.ts" + ``` + +=== "Sample event" + + ```json + --8<-- "examples/snippets/idempotency/samples/workingWithResponseHook.json" + ``` + +???+ info "Info: Using custom de-serialization?" + + The responseHook is called after the custom de-serialization so the payload you process will be the de-serialized version. + +#### Being a good citizen + +When using response hooks to manipulate returned data from idempotent operations, it's important to follow best practices to avoid introducing complexity or issues. Keep these guidelines in mind: + +1. **Response hook works exclusively when operations are idempotent.** The hook will not be called when an operation is not idempotent, or when the idempotent logic fails. + +2. **Catch and Handle Exceptions.** Your response hook code should catch and handle any exceptions that may arise from your logic. Unhandled exceptions will cause the Lambda function to fail unexpectedly. + +3. **Keep Hook Logic Simple** Response hooks should consist of minimal and straightforward logic for manipulating response data. Avoid complex conditional branching and aim for hooks that are easy to reason about. + ## Testing your code The idempotency utility provides several routes to test your code. From d290d00b4d078a10146d88de431236b4d0d4019d Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 15 Sep 2024 15:47:22 +0600 Subject: [PATCH 10/13] doc: idempotency with `responseHook` example --- docs/utilities/idempotency.md | 2 +- .../samples/workingWithResponseHook.json | 4 ++ .../idempotency/workingWithResponseHook.ts | 58 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 examples/snippets/idempotency/samples/workingWithResponseHook.json create mode 100644 examples/snippets/idempotency/workingWithResponseHook.ts diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index 20d98ec316..e09e23a4a6 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -783,7 +783,7 @@ You can set up a `responseHook` in the `IdempotentConfig` class to manipulate th === "Using an Idempotent Response Hook" - ```typescript hl_lines="20 22 28 36" + ```typescript hl_lines="15 18 26 55" --8<-- "examples/snippets/idempotency/workingWithResponseHook.ts" ``` diff --git a/examples/snippets/idempotency/samples/workingWithResponseHook.json b/examples/snippets/idempotency/samples/workingWithResponseHook.json new file mode 100644 index 0000000000..40a46dcbf4 --- /dev/null +++ b/examples/snippets/idempotency/samples/workingWithResponseHook.json @@ -0,0 +1,4 @@ +{ + "user": "John Doe", + "productId": "123456" +} \ No newline at end of file diff --git a/examples/snippets/idempotency/workingWithResponseHook.ts b/examples/snippets/idempotency/workingWithResponseHook.ts new file mode 100644 index 0000000000..e24509ba06 --- /dev/null +++ b/examples/snippets/idempotency/workingWithResponseHook.ts @@ -0,0 +1,58 @@ +import { randomUUID } from 'node:crypto'; +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import { + IdempotencyConfig, + makeIdempotent, +} from '@aws-lambda-powertools/idempotency'; +import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb'; +import type { IdempotencyRecord } from '@aws-lambda-powertools/idempotency/persistence'; +import type { Context } from 'aws-lambda'; +import type { Request, Response, SubscriptionResult } from './types.js'; + +const persistenceStore = new DynamoDBPersistenceLayer({ + tableName: 'idempotencyTableName', +}); + +const responseHook = (response: JSONValue, record: IdempotencyRecord) => { + // Return inserted Header data into the Idempotent Response + (response as Response).headers = { + 'x-idempotency-key': record.idempotencyKey, + }; + + // Must return the response here + return response as JSONValue; +}; + +const config = new IdempotencyConfig({ + responseHook, +}); + +const createSubscriptionPayment = async ( + event: Request +): Promise => { + // ... create payment + return { + id: randomUUID(), + productId: event.productId, + }; +}; + +export const handler = makeIdempotent( + async (event: Request, _context: Context): Promise => { + try { + const payment = await createSubscriptionPayment(event); + + return { + paymentId: payment.id, + message: 'success', + statusCode: 200, + }; + } catch (error) { + throw new Error('Error creating payment'); + } + }, + { + persistenceStore, + config, + } +); From 10a339b757dfa2364d622be0c96638ad33fd587e Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 15 Sep 2024 16:14:01 +0600 Subject: [PATCH 11/13] doc: update `determineResultFromIdempotencyRecord` doc --- packages/idempotency/src/IdempotencyHandler.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/idempotency/src/IdempotencyHandler.ts b/packages/idempotency/src/IdempotencyHandler.ts index df1cf08077..b81dde00b8 100644 --- a/packages/idempotency/src/IdempotencyHandler.ts +++ b/packages/idempotency/src/IdempotencyHandler.ts @@ -87,6 +87,8 @@ export class IdempotencyHandler { /** * Takes an idempotency key and returns the idempotency record from the persistence layer. * + * If a response hook is provided in the idempotency configuration, it will be called to before returning the response. + * * If the idempotency record is not COMPLETE, then it will throw an error based on the status of the record. * * @param idempotencyRecord The idempotency record stored in the persistence layer From a8d5610990731e30f6fcdb91eb91d760303fbccd Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 15 Sep 2024 16:37:00 +0600 Subject: [PATCH 12/13] doc: sample idempotent response --- docs/utilities/idempotency.md | 10 ++++++++-- .../workingWithResponseHookIdempotentResponse.json | 8 ++++++++ ...ok.json => workingWithResponseHookSampleEvent.json} | 0 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 examples/snippets/idempotency/samples/workingWithResponseHookIdempotentResponse.json rename examples/snippets/idempotency/samples/{workingWithResponseHook.json => workingWithResponseHookSampleEvent.json} (100%) diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index e09e23a4a6..d316d7a071 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -783,14 +783,20 @@ You can set up a `responseHook` in the `IdempotentConfig` class to manipulate th === "Using an Idempotent Response Hook" - ```typescript hl_lines="15 18 26 55" + ```typescript hl_lines="16 19 27 56" --8<-- "examples/snippets/idempotency/workingWithResponseHook.ts" ``` === "Sample event" ```json - --8<-- "examples/snippets/idempotency/samples/workingWithResponseHook.json" + --8<-- "examples/snippets/idempotency/samples/workingWithResponseHookSampleEvent.json" + ``` + +=== "Sample Idempotent response" + + ```json hl_lines="6" + --8<-- "examples/snippets/idempotency/samples/workingWithResponseHookIdempotentResponse.json" ``` ???+ info "Info: Using custom de-serialization?" diff --git a/examples/snippets/idempotency/samples/workingWithResponseHookIdempotentResponse.json b/examples/snippets/idempotency/samples/workingWithResponseHookIdempotentResponse.json new file mode 100644 index 0000000000..0c7e1abdae --- /dev/null +++ b/examples/snippets/idempotency/samples/workingWithResponseHookIdempotentResponse.json @@ -0,0 +1,8 @@ +{ + "message": "success", + "paymentId": "31a964eb-7477-4fe1-99fe-7f8a6a351a7e", + "statusCode": 200, + "headers": { + "x-idempotency-key": "function-name#mHfGv2vJ8h+ZvLIr/qGBbQ==" + } + } \ No newline at end of file diff --git a/examples/snippets/idempotency/samples/workingWithResponseHook.json b/examples/snippets/idempotency/samples/workingWithResponseHookSampleEvent.json similarity index 100% rename from examples/snippets/idempotency/samples/workingWithResponseHook.json rename to examples/snippets/idempotency/samples/workingWithResponseHookSampleEvent.json From b547693b46beb81508594acb0021cfc2735cdcb0 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Sun, 15 Sep 2024 16:43:26 +0600 Subject: [PATCH 13/13] style: fix typo --- packages/idempotency/src/IdempotencyHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/idempotency/src/IdempotencyHandler.ts b/packages/idempotency/src/IdempotencyHandler.ts index b81dde00b8..3c64a92a6b 100644 --- a/packages/idempotency/src/IdempotencyHandler.ts +++ b/packages/idempotency/src/IdempotencyHandler.ts @@ -87,7 +87,7 @@ export class IdempotencyHandler { /** * Takes an idempotency key and returns the idempotency record from the persistence layer. * - * If a response hook is provided in the idempotency configuration, it will be called to before returning the response. + * If a response hook is provided in the idempotency configuration, it will be called before returning the response. * * If the idempotency record is not COMPLETE, then it will throw an error based on the status of the record. *