From 78902135b5c760f735e98975840383e76079e231 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 2 Oct 2024 18:23:01 +0600 Subject: [PATCH 1/5] feat: `jmesPathOptions` in `IdempotencyConfigOptions` --- packages/idempotency/src/types/IdempotencyOptions.ts | 5 +++++ packages/jmespath/src/PowertoolsFunctions.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/idempotency/src/types/IdempotencyOptions.ts b/packages/idempotency/src/types/IdempotencyOptions.ts index b9ca069fc9..01c409901c 100644 --- a/packages/idempotency/src/types/IdempotencyOptions.ts +++ b/packages/idempotency/src/types/IdempotencyOptions.ts @@ -1,4 +1,5 @@ import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import type { Functions } from '@aws-lambda-powertools/jmespath/functions'; import type { Context, Handler } from 'aws-lambda'; import type { IdempotencyConfig } from '../IdempotencyConfig.js'; import type { BasePersistenceLayer } from '../persistence/BasePersistenceLayer.js'; @@ -196,6 +197,10 @@ type IdempotencyConfigOptions = { * A hook that runs when an idempotent request is made */ responseHook?: ResponseHook; + /** + * Custom JMESPath functions to use when extracting data from the event + */ + jmesPathOptions?: Functions; }; export type { diff --git a/packages/jmespath/src/PowertoolsFunctions.ts b/packages/jmespath/src/PowertoolsFunctions.ts index fcdc86b668..fc353aa8b0 100644 --- a/packages/jmespath/src/PowertoolsFunctions.ts +++ b/packages/jmespath/src/PowertoolsFunctions.ts @@ -58,4 +58,4 @@ class PowertoolsFunctions extends Functions { } } -export { PowertoolsFunctions }; +export { PowertoolsFunctions, Functions }; From 87e76460f6ee53e37a5612389850ac15393ec787 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 2 Oct 2024 18:24:23 +0600 Subject: [PATCH 2/5] feat: set the `jmesPathOptions` from config if present --- packages/idempotency/src/IdempotencyConfig.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/idempotency/src/IdempotencyConfig.ts b/packages/idempotency/src/IdempotencyConfig.ts index 84e943d8ce..b93a14e5be 100644 --- a/packages/idempotency/src/IdempotencyConfig.ts +++ b/packages/idempotency/src/IdempotencyConfig.ts @@ -70,7 +70,9 @@ class IdempotencyConfig { public constructor(config: IdempotencyConfigOptions) { this.eventKeyJmesPath = config.eventKeyJmesPath ?? ''; this.payloadValidationJmesPath = config.payloadValidationJmesPath; - this.jmesPathOptions = { customFunctions: new PowertoolsFunctions() }; + this.jmesPathOptions = { + customFunctions: config.jmesPathOptions ?? new PowertoolsFunctions(), + }; this.throwOnNoIdempotencyKey = config.throwOnNoIdempotencyKey ?? false; this.expiresAfterSeconds = config.expiresAfterSeconds ?? 3600; // 1 hour default this.useLocalCache = config.useLocalCache ?? false; From c5a2211c8b0d74cd5874bf299678e79ffacd748c Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 2 Oct 2024 19:38:48 +0600 Subject: [PATCH 3/5] test: `jmesPathOptions` set from config --- .../tests/unit/IdempotencyConfig.test.ts | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/packages/idempotency/tests/unit/IdempotencyConfig.test.ts b/packages/idempotency/tests/unit/IdempotencyConfig.test.ts index fef85a96d5..8174c7727d 100644 --- a/packages/idempotency/tests/unit/IdempotencyConfig.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyConfig.test.ts @@ -1,3 +1,8 @@ +import type { JSONValue } from '@aws-lambda-powertools/commons/types'; +import { + Functions, + PowertoolsFunctions, +} from '@aws-lambda-powertools/jmespath/functions'; import context from '@aws-lambda-powertools/testing-utils/context'; import { afterAll, beforeEach, describe, expect, it } from 'vitest'; import { IdempotencyConfig } from '../../src/index.js'; @@ -32,12 +37,23 @@ describe('Class: IdempotencyConfig', () => { useLocalCache: false, hashFunction: 'md5', lambdaContext: undefined, + jmesPathOptions: expect.objectContaining({ + customFunctions: expect.any(PowertoolsFunctions), + }), }) ); }); it('initializes the config with the provided configs', () => { // Prepare + class MyFancyFunctions extends Functions { + @Functions.signature({ + argumentsSpecs: [['string']], + }) + public funcMyFancyFunction(value: string): JSONValue { + return JSON.parse(value); + } + } const configOptions: IdempotencyConfigOptions = { eventKeyJmesPath: 'eventKeyJmesPath', payloadValidationJmesPath: 'payloadValidationJmesPath', @@ -46,6 +62,7 @@ describe('Class: IdempotencyConfig', () => { useLocalCache: true, hashFunction: 'hashFunction', lambdaContext: context, + jmesPathOptions: new MyFancyFunctions(), }; // Act @@ -61,6 +78,9 @@ describe('Class: IdempotencyConfig', () => { useLocalCache: true, hashFunction: 'hashFunction', lambdaContext: context, + jmesPathOptions: expect.objectContaining({ + customFunctions: expect.any(MyFancyFunctions), + }), }) ); }); From 0ead1e8039acfe063797f752720195a09180ba20 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 2 Oct 2024 20:23:05 +0600 Subject: [PATCH 4/5] style: `jmesPathOptions` at the top --- packages/idempotency/src/types/IdempotencyOptions.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/idempotency/src/types/IdempotencyOptions.ts b/packages/idempotency/src/types/IdempotencyOptions.ts index 01c409901c..b366abb526 100644 --- a/packages/idempotency/src/types/IdempotencyOptions.ts +++ b/packages/idempotency/src/types/IdempotencyOptions.ts @@ -169,6 +169,10 @@ type IdempotencyConfigOptions = { * An optional JMESPath expression to extract the payload to be validated from the event record */ payloadValidationJmesPath?: string; + /** + * Custom JMESPath functions to use when parsing the JMESPath expressions + */ + jmesPathOptions?: Functions; /** * Throw an error if no idempotency key was found in the request, defaults to `false` */ @@ -197,10 +201,6 @@ type IdempotencyConfigOptions = { * A hook that runs when an idempotent request is made */ responseHook?: ResponseHook; - /** - * Custom JMESPath functions to use when extracting data from the event - */ - jmesPathOptions?: Functions; }; export type { From 7a0f4cdf68bca207166e2c0bad248c7f25ec9708 Mon Sep 17 00:00:00 2001 From: arnabrahman Date: Wed, 2 Oct 2024 20:26:51 +0600 Subject: [PATCH 5/5] doc: `jmesPathOptions` description --- docs/utilities/idempotency.md | 11 +++++++ .../workingWithCustomJmesPathFunctions.ts | 31 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 examples/snippets/idempotency/workingWithCustomJmesPathFunctions.ts diff --git a/docs/utilities/idempotency.md b/docs/utilities/idempotency.md index 75a9618f00..28b2f3a085 100644 --- a/docs/utilities/idempotency.md +++ b/docs/utilities/idempotency.md @@ -573,6 +573,7 @@ Idempotent decorator can be further configured with **`IdempotencyConfig`** as s | ----------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **eventKeyJmespath** | `''` | JMESPath expression to extract the idempotency key from the event record using [built-in functions](./jmespath.md#built-in-jmespath-functions){target="_blank"} | | **payloadValidationJmespath** | `''` | JMESPath expression to validate that the specified fields haven't changed across requests for the same idempotency key _e.g., payload tampering._ | +| **jmesPathOptions** | `undefined` | Custom JMESPath functions to use when parsing the JMESPath expressions. See [Custom JMESPath Functions](idempotency.md#custom-jmespath-functions) | | **throwOnNoIdempotencyKey** | `false` | Throw an error if no idempotency key was found in the request | | **expiresAfterSeconds** | 3600 | The number of seconds to wait before a record is expired, allowing a new transaction with the same idempotency key | | **useLocalCache** | `false` | Whether to cache idempotency results in-memory to save on persistence storage latency and costs | @@ -657,6 +658,16 @@ Without payload validation, we would have returned the same result as we did for By using **`payloadValidationJmesPath="amount"`**, we prevent this potentially confusing behavior and instead throw an error. +### Custom JMESPath Functions + +You can provide custom JMESPath functions for evaluating JMESPath expressions by passing them through the **`jmesPathOptions`** parameter. In this example, we use a custom function, `my_fancy_function`, to parse the payload as a JSON object instead of a string. + +=== "Custom JMESPath functions" + + ```typescript hl_lines="16 20 28-29" + --8<-- "examples/snippets/idempotency/workingWithCustomJmesPathFunctions.ts" + ``` + ### Making idempotency key required If you want to enforce that an idempotency key is required, you can set **`throwOnNoIdempotencyKey`** to `true`. diff --git a/examples/snippets/idempotency/workingWithCustomJmesPathFunctions.ts b/examples/snippets/idempotency/workingWithCustomJmesPathFunctions.ts new file mode 100644 index 0000000000..2d3380c011 --- /dev/null +++ b/examples/snippets/idempotency/workingWithCustomJmesPathFunctions.ts @@ -0,0 +1,31 @@ +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 { + Functions, + PowertoolsFunctions, +} from '@aws-lambda-powertools/jmespath/functions'; + +const persistenceStore = new DynamoDBPersistenceLayer({ + tableName: 'idempotencyTableName', +}); + +class MyFancyFunctions extends PowertoolsFunctions { + @Functions.signature({ + argumentsSpecs: [['string']], + }) + public funcMyFancyFunction(value: string): JSONValue { + return JSON.parse(value); + } +} + +export const handler = makeIdempotent(async () => true, { + persistenceStore, + config: new IdempotencyConfig({ + eventKeyJmesPath: 'my_fancy_function(body).["user", "productId"]', + jmesPathOptions: new MyFancyFunctions(), + }), +});