Skip to content

Commit 869b6fc

Browse files
feat(idempotency): ability to specify JMESPath custom functions (#3150)
Co-authored-by: Andrea Amorosi <[email protected]>
1 parent 686e524 commit 869b6fc

File tree

6 files changed

+71
-2
lines changed

6 files changed

+71
-2
lines changed

Diff for: docs/utilities/idempotency.md

+11
Original file line numberDiff line numberDiff line change
@@ -573,6 +573,7 @@ Idempotent decorator can be further configured with **`IdempotencyConfig`** as s
573573
| ----------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
574574
| **eventKeyJmespath** | `''` | JMESPath expression to extract the idempotency key from the event record using [built-in functions](./jmespath.md#built-in-jmespath-functions){target="_blank"} |
575575
| **payloadValidationJmespath** | `''` | JMESPath expression to validate that the specified fields haven't changed across requests for the same idempotency key _e.g., payload tampering._ |
576+
| **jmesPathOptions** | `undefined` | Custom JMESPath functions to use when parsing the JMESPath expressions. See [Custom JMESPath Functions](idempotency.md#custom-jmespath-functions) |
576577
| **throwOnNoIdempotencyKey** | `false` | Throw an error if no idempotency key was found in the request |
577578
| **expiresAfterSeconds** | 3600 | The number of seconds to wait before a record is expired, allowing a new transaction with the same idempotency key |
578579
| **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
657658

658659
By using **`payloadValidationJmesPath="amount"`**, we prevent this potentially confusing behavior and instead throw an error.
659660

661+
### Custom JMESPath Functions
662+
663+
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.
664+
665+
=== "Custom JMESPath functions"
666+
667+
```typescript hl_lines="16 20 28-29"
668+
--8<-- "examples/snippets/idempotency/workingWithCustomJmesPathFunctions.ts"
669+
```
670+
660671
### Making idempotency key required
661672

662673
If you want to enforce that an idempotency key is required, you can set **`throwOnNoIdempotencyKey`** to `true`.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { JSONValue } from '@aws-lambda-powertools/commons/types';
2+
import {
3+
IdempotencyConfig,
4+
makeIdempotent,
5+
} from '@aws-lambda-powertools/idempotency';
6+
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
7+
import {
8+
Functions,
9+
PowertoolsFunctions,
10+
} from '@aws-lambda-powertools/jmespath/functions';
11+
12+
const persistenceStore = new DynamoDBPersistenceLayer({
13+
tableName: 'idempotencyTableName',
14+
});
15+
16+
class MyFancyFunctions extends PowertoolsFunctions {
17+
@Functions.signature({
18+
argumentsSpecs: [['string']],
19+
})
20+
public funcMyFancyFunction(value: string): JSONValue {
21+
return JSON.parse(value);
22+
}
23+
}
24+
25+
export const handler = makeIdempotent(async () => true, {
26+
persistenceStore,
27+
config: new IdempotencyConfig({
28+
eventKeyJmesPath: 'my_fancy_function(body).["user", "productId"]',
29+
jmesPathOptions: new MyFancyFunctions(),
30+
}),
31+
});

Diff for: packages/idempotency/src/IdempotencyConfig.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ class IdempotencyConfig {
7070
public constructor(config: IdempotencyConfigOptions) {
7171
this.eventKeyJmesPath = config.eventKeyJmesPath ?? '';
7272
this.payloadValidationJmesPath = config.payloadValidationJmesPath;
73-
this.jmesPathOptions = { customFunctions: new PowertoolsFunctions() };
73+
this.jmesPathOptions = {
74+
customFunctions: config.jmesPathOptions ?? new PowertoolsFunctions(),
75+
};
7476
this.throwOnNoIdempotencyKey = config.throwOnNoIdempotencyKey ?? false;
7577
this.expiresAfterSeconds = config.expiresAfterSeconds ?? 3600; // 1 hour default
7678
this.useLocalCache = config.useLocalCache ?? false;

Diff for: packages/idempotency/src/types/IdempotencyOptions.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { JSONValue } from '@aws-lambda-powertools/commons/types';
2+
import type { Functions } from '@aws-lambda-powertools/jmespath/functions';
23
import type { Context, Handler } from 'aws-lambda';
34
import type { IdempotencyConfig } from '../IdempotencyConfig.js';
45
import type { BasePersistenceLayer } from '../persistence/BasePersistenceLayer.js';
@@ -168,6 +169,10 @@ type IdempotencyConfigOptions = {
168169
* An optional JMESPath expression to extract the payload to be validated from the event record
169170
*/
170171
payloadValidationJmesPath?: string;
172+
/**
173+
* Custom JMESPath functions to use when parsing the JMESPath expressions
174+
*/
175+
jmesPathOptions?: Functions;
171176
/**
172177
* Throw an error if no idempotency key was found in the request, defaults to `false`
173178
*/

Diff for: packages/idempotency/tests/unit/IdempotencyConfig.test.ts

+20
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import type { JSONValue } from '@aws-lambda-powertools/commons/types';
2+
import {
3+
Functions,
4+
PowertoolsFunctions,
5+
} from '@aws-lambda-powertools/jmespath/functions';
16
import context from '@aws-lambda-powertools/testing-utils/context';
27
import { afterAll, beforeEach, describe, expect, it } from 'vitest';
38
import { IdempotencyConfig } from '../../src/index.js';
@@ -32,12 +37,23 @@ describe('Class: IdempotencyConfig', () => {
3237
useLocalCache: false,
3338
hashFunction: 'md5',
3439
lambdaContext: undefined,
40+
jmesPathOptions: expect.objectContaining({
41+
customFunctions: expect.any(PowertoolsFunctions),
42+
}),
3543
})
3644
);
3745
});
3846

3947
it('initializes the config with the provided configs', () => {
4048
// Prepare
49+
class MyFancyFunctions extends Functions {
50+
@Functions.signature({
51+
argumentsSpecs: [['string']],
52+
})
53+
public funcMyFancyFunction(value: string): JSONValue {
54+
return JSON.parse(value);
55+
}
56+
}
4157
const configOptions: IdempotencyConfigOptions = {
4258
eventKeyJmesPath: 'eventKeyJmesPath',
4359
payloadValidationJmesPath: 'payloadValidationJmesPath',
@@ -46,6 +62,7 @@ describe('Class: IdempotencyConfig', () => {
4662
useLocalCache: true,
4763
hashFunction: 'hashFunction',
4864
lambdaContext: context,
65+
jmesPathOptions: new MyFancyFunctions(),
4966
};
5067

5168
// Act
@@ -61,6 +78,9 @@ describe('Class: IdempotencyConfig', () => {
6178
useLocalCache: true,
6279
hashFunction: 'hashFunction',
6380
lambdaContext: context,
81+
jmesPathOptions: expect.objectContaining({
82+
customFunctions: expect.any(MyFancyFunctions),
83+
}),
6484
})
6585
);
6686
});

Diff for: packages/jmespath/src/PowertoolsFunctions.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,4 @@ class PowertoolsFunctions extends Functions {
5858
}
5959
}
6060

61-
export { PowertoolsFunctions };
61+
export { PowertoolsFunctions, Functions };

0 commit comments

Comments
 (0)