Skip to content

Commit 9721e7c

Browse files
dreamorosiam29d
andauthored
feat(idempotency): add custom JMESPath functions (#2364)
* feat(idempotency): add custom jmespath functions * docs(idempotency): add custom functions to docs * chore(layers): add package to layer * chore: move jmespath pkg up into build process * feat: enable custom functions * chore: move jmespath pkg up into build process * chore: move jmespath pkg up into build process * chore: update layer setup * refactor: moved Powertools function initialization * chore: added pkg to pre-push hook --------- Co-authored-by: Alexander Schueren <sha@amazon.com>
1 parent fc2d709 commit 9721e7c

File tree

13 files changed

+74
-36
lines changed

13 files changed

+74
-36
lines changed

.github/actions/cached-node-modules/action.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,12 @@ runs:
3939
# sequence, but still in the correct order.
4040
run: |
4141
npm run build -w packages/commons
42+
npm run build -w packages/jmespath
4243
npm run build -w packages/logger & \
4344
npm run build -w packages/tracer & \
4445
npm run build -w packages/metrics & \
4546
npm run build -w packages/parameters & \
4647
npm run build -w packages/idempotency & \
4748
npm run build -w packages/batch & \
48-
npm run build -w packages/testing & \
49-
npm run build -w packages/jmespath
49+
npm run build -w packages/testing
5050
shell: bash

.husky/pre-push

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
npm t \
22
-w packages/commons \
3+
-w packages/jmespath \
34
-w packages/logger \
45
-w packages/metrics \
56
-w packages/tracer \

docs/snippets/idempotency/makeIdempotentJmes.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ const createSubscriptionPayment = async (
2222
};
2323
};
2424

25-
// Extract the idempotency key from the request headers
25+
// Deserialize JSON string under the "body" key, then extract the "user" and "productId" keys
2626
const config = new IdempotencyConfig({
27-
eventKeyJmesPath: 'body',
27+
eventKeyJmesPath: 'powertools_json(body).["user", "productId"]',
2828
});
2929

3030
export const handler = makeIdempotent(

docs/utilities/idempotency.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,11 @@ Imagine the function executes successfully, but the client never receives the re
220220
???+ warning "Deserializing JSON strings in payloads for increased accuracy."
221221
The payload extracted by the `eventKeyJmesPath` is treated as a string by default. This means there could be differences in whitespace even when the JSON payload itself is identical.
222222

223+
To alter this behaviour, we can use the [JMESPath built-in function `powertools_json()`](jmespath.md#powertools_json-function) to treat the payload as a JSON object rather than a string.
224+
223225
=== "index.ts"
224226

225-
```typescript hl_lines="4 26-28 49"
227+
```typescript hl_lines="4 27 49"
226228
--8<-- "docs/snippets/idempotency/makeIdempotentJmes.ts"
227229
```
228230

layers/src/layer-publisher-stack.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export class LayerPublisherStack extends Stack {
6262
// the name is the same as the npm workspace name
6363
const utilities = [
6464
'commons',
65+
'jmespath',
6566
'logger',
6667
'metrics',
6768
'tracer',
@@ -87,8 +88,6 @@ export class LayerPublisherStack extends Stack {
8788
'node_modules/async-hook-jl/test',
8889
'node_modules/stack-chain/test',
8990
'node_modules/shimmer/test',
90-
'node_modules/jmespath/artifacts',
91-
'node_modules/jmespath/bower.json',
9291
'node_modules/obliterator/*.d.ts',
9392
'node_modules/strnum/.vscode',
9493
'node_modules/strnum/*.test.js',

package-lock.json

Lines changed: 2 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/idempotency/README.md

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ You can use the package in both TypeScript and JavaScript code bases.
1818
- [Becoming a reference customer](#becoming-a-reference-customer)
1919
- [Sharing your work](#sharing-your-work)
2020
- [Using Lambda Layer](#using-lambda-layer)
21-
- [Credits](#credits)
2221
- [License](#license)
2322

2423
## Intro
@@ -158,7 +157,33 @@ export const handler = makeIdempotent(myHandler, {
158157
config: new IdempotencyConfig({
159158
eventKeyJmespath: 'requestContext.identity.user',
160159
}),
161-
});
160+
});
161+
```
162+
163+
Additionally, you can also use one of the [JMESPath built-in functions](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/jmespath/#built-in-jmespath-functions) like `powertools_json()` to decode keys and use parts of the payload as the idempotency key.
164+
165+
```ts
166+
import { makeIdempotent, IdempotencyConfig } from '@aws-lambda-powertools/idempotency';
167+
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
168+
import type { Context, APIGatewayProxyEvent } from 'aws-lambda';
169+
170+
const persistenceStore = new DynamoDBPersistenceLayer({
171+
tableName: 'idempotencyTableName',
172+
});
173+
174+
const myHandler = async (
175+
event: APIGatewayProxyEvent,
176+
_context: Context
177+
): Promise<void> => {
178+
// your code goes here here
179+
};
180+
181+
export const handler = makeIdempotent(myHandler, {
182+
persistenceStore,
183+
config: new IdempotencyConfig({
184+
eventKeyJmespath: 'powertools_json(body).["user", "productId"]',
185+
}),
186+
});
162187
```
163188

164189
Check the [docs](https://docs.powertools.aws.dev/lambda/typescript/latest/utilities/idempotency/) for more examples.
@@ -311,12 +336,8 @@ Share what you did with Powertools for AWS Lambda (TypeScript) 💞💞. Blog po
311336

312337
### Using Lambda Layer
313338

314-
This helps us understand who uses Powertools for AWS Lambda (TypeScript) in a non-intrusive way, and helps us gain future investments for other Powertools for AWS Lambda languages. When [using Layers](#lambda-layers), you can add Powertools as a dev dependency (or as part of your virtual env) to not impact the development process.
315-
316-
## Credits
317-
318-
Credits for the Lambda Powertools for AWS Lambda (TypeScript) idea go to [DAZN](https://github.com/getndazn) and their [DAZN Lambda Powertools](https://github.com/getndazn/dazn-lambda-powertools/).
339+
This helps us understand who uses Powertools for AWS Lambda (TypeScript) in a non-intrusive way, and helps us gain future investments for other Powertools for AWS Lambda languages. When [using Layers](https://docs.powertools.aws.dev/lambda/typescript/latest/#lambda-layer), you can add Powertools as a dev dependency to not impact the development process.
319340

320341
## License
321342

322-
This library is licensed under the MIT-0 License. See the LICENSE file.
343+
This library is licensed under the MIT-0 License. See the LICENSE file.

packages/idempotency/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@
101101
},
102102
"dependencies": {
103103
"@aws-lambda-powertools/commons": "^2.0.4",
104-
"jmespath": "^0.16.0"
104+
"@aws-lambda-powertools/jmespath": "^2.0.4"
105105
},
106106
"peerDependencies": {
107107
"@aws-sdk/client-dynamodb": ">=3.x",
@@ -131,7 +131,6 @@
131131
"@aws-lambda-powertools/testing-utils": "file:../testing",
132132
"@aws-sdk/client-dynamodb": "^3.554.0",
133133
"@aws-sdk/lib-dynamodb": "^3.554.0",
134-
"@types/jmespath": "^0.15.0",
135134
"aws-sdk-client-mock": "^4.0.0",
136135
"aws-sdk-client-mock-jest": "^4.0.0"
137136
}

packages/idempotency/src/IdempotencyConfig.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { EnvironmentVariablesService } from './config/EnvironmentVariablesService.js';
22
import type { Context } from 'aws-lambda';
33
import type { IdempotencyConfigOptions } from './types/IdempotencyOptions.js';
4+
import type { ParsingOptions } from '@aws-lambda-powertools/jmespath/types';
5+
import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions';
46

57
/**
68
* Configuration for the idempotency feature.
@@ -22,6 +24,10 @@ class IdempotencyConfig {
2224
* @default 'md5'
2325
*/
2426
public hashFunction: string;
27+
/**
28+
*
29+
*/
30+
public jmesPathOptions: ParsingOptions;
2531
/**
2632
* The lambda context object.
2733
*/
@@ -53,6 +59,7 @@ class IdempotencyConfig {
5359
public constructor(config: IdempotencyConfigOptions) {
5460
this.eventKeyJmesPath = config.eventKeyJmesPath ?? '';
5561
this.payloadValidationJmesPath = config.payloadValidationJmesPath;
62+
this.jmesPathOptions = { customFunctions: new PowertoolsFunctions() };
5663
this.throwOnNoIdempotencyKey = config.throwOnNoIdempotencyKey ?? false;
5764
this.expiresAfterSeconds = config.expiresAfterSeconds ?? 3600; // 1 hour default
5865
this.useLocalCache = config.useLocalCache ?? false;

packages/idempotency/src/IdempotencyHandler.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { BasePersistenceLayer } from './persistence/BasePersistenceLayer.js';
1616
import { IdempotencyRecord } from './persistence/IdempotencyRecord.js';
1717
import { IdempotencyConfig } from './IdempotencyConfig.js';
1818
import { MAX_RETRIES, IdempotencyRecordStatus } from './constants.js';
19-
import { search } from 'jmespath';
19+
import { search } from '@aws-lambda-powertools/jmespath';
2020

2121
/**
2222
* @internal
@@ -275,8 +275,9 @@ export class IdempotencyHandler<Func extends AnyFunction> {
275275
!this.#idempotencyConfig.throwOnNoIdempotencyKey
276276
) {
277277
const selection = search(
278+
this.#idempotencyConfig.eventKeyJmesPath,
278279
this.#functionPayloadToBeHashed,
279-
this.#idempotencyConfig.eventKeyJmesPath
280+
this.#idempotencyConfig.jmesPathOptions
280281
);
281282

282283
return selection === undefined || selection === null;

packages/idempotency/src/persistence/BasePersistenceLayer.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createHash, Hash } from 'node:crypto';
2-
import { search } from 'jmespath';
2+
import { search } from '@aws-lambda-powertools/jmespath';
3+
import type { ParsingOptions } from '@aws-lambda-powertools/jmespath/types';
34
import type {
45
BasePersistenceLayerOptions,
56
BasePersistenceLayerInterface,
@@ -36,6 +37,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
3637
private throwOnNoIdempotencyKey = false;
3738
private useLocalCache = false;
3839
private validationKeyJmesPath?: string;
40+
#jmesPathOptions?: ParsingOptions;
3941

4042
public constructor() {
4143
this.envVarsService = new EnvironmentVariablesService();
@@ -63,6 +65,7 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
6365

6466
this.eventKeyJmesPath = idempotencyConfig?.eventKeyJmesPath;
6567
this.validationKeyJmesPath = idempotencyConfig?.payloadValidationJmesPath;
68+
this.#jmesPathOptions = idempotencyConfig.jmesPathOptions;
6669
this.payloadValidationEnabled =
6770
this.validationKeyJmesPath !== undefined || false;
6871
this.throwOnNoIdempotencyKey =
@@ -279,7 +282,11 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
279282
*/
280283
private getHashedIdempotencyKey(data: JSONValue): string {
281284
if (this.eventKeyJmesPath) {
282-
data = search(data, this.eventKeyJmesPath);
285+
data = search(
286+
this.eventKeyJmesPath,
287+
data,
288+
this.#jmesPathOptions
289+
) as JSONValue;
283290
}
284291

285292
if (BasePersistenceLayer.isMissingIdempotencyKey(data)) {
@@ -305,7 +312,11 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
305312
*/
306313
private getHashedPayload(data: JSONValue): string {
307314
if (this.isPayloadValidationEnabled() && this.validationKeyJmesPath) {
308-
data = search(data, this.validationKeyJmesPath);
315+
data = search(
316+
this.validationKeyJmesPath,
317+
data,
318+
this.#jmesPathOptions
319+
) as JSONValue;
309320

310321
return this.generateHash(JSON.stringify(data));
311322
} else {

packages/idempotency/tests/e2e/makeIdempotent.test.FunctionCode.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,16 +90,17 @@ export const handlerCustomized = async (
9090
* Test idempotent Lambda handler with JMESPath expression to extract event key.
9191
*/
9292
export const handlerLambda = makeIdempotent(
93-
async (event: { foo: string }, context: Context) => {
93+
async (event: { body: string }, context: Context) => {
9494
logger.addContext(context);
95-
logger.info(`foo`, { details: event.foo });
95+
const body = JSON.parse(event.body);
96+
logger.info('foo', { details: body.foo });
9697

97-
return event.foo;
98+
return body.foo;
9899
},
99100
{
100101
persistenceStore: dynamoDBPersistenceLayer,
101102
config: new IdempotencyConfig({
102-
eventKeyJmesPath: 'foo',
103+
eventKeyJmesPath: 'powertools_json(body).foo',
103104
useLocalCache: true,
104105
}),
105106
}

packages/idempotency/tests/e2e/makeIdempotent.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -268,10 +268,12 @@ describe(`Idempotency E2E tests, wrapper function usage`, () => {
268268
async () => {
269269
// Prepare
270270
const payload = {
271-
foo: 'bar',
271+
body: JSON.stringify({
272+
foo: 'bar',
273+
}),
272274
};
273275
const payloadHash = createHash('md5')
274-
.update(JSON.stringify(payload.foo))
276+
.update(JSON.stringify('bar'))
275277
.digest('base64');
276278

277279
// Act

0 commit comments

Comments
 (0)