From 869b6fced659ad820ffe79a0b905022061570974 Mon Sep 17 00:00:00 2001 From: Asifur Rahman Arnab Date: Fri, 4 Oct 2024 23:20:33 +0600 Subject: [PATCH] feat(idempotency): ability to specify JMESPath custom functions (#3150) Co-authored-by: Andrea Amorosi --- docs/utilities/idempotency.md | 11 +++++++ .../workingWithCustomJmesPathFunctions.ts | 31 +++++++++++++++++++ packages/idempotency/src/IdempotencyConfig.ts | 4 ++- .../src/types/IdempotencyOptions.ts | 5 +++ .../tests/unit/IdempotencyConfig.test.ts | 20 ++++++++++++ packages/jmespath/src/PowertoolsFunctions.ts | 2 +- 6 files changed, 71 insertions(+), 2 deletions(-) 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(), + }), +}); 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; diff --git a/packages/idempotency/src/types/IdempotencyOptions.ts b/packages/idempotency/src/types/IdempotencyOptions.ts index b9ca069fc9..b366abb526 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'; @@ -168,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` */ 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), + }), }) ); }); 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 };