Skip to content

feat(idempotency): add custom JMESPath functions #2364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/actions/cached-node-modules/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ runs:
# sequence, but still in the correct order.
run: |
npm run build -w packages/commons
npm run build -w packages/jmespath
npm run build -w packages/logger & \
npm run build -w packages/tracer & \
npm run build -w packages/metrics & \
npm run build -w packages/parameters & \
npm run build -w packages/idempotency & \
npm run build -w packages/batch & \
npm run build -w packages/testing & \
npm run build -w packages/jmespath
npm run build -w packages/testing
shell: bash
4 changes: 2 additions & 2 deletions docs/snippets/idempotency/makeIdempotentJmes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ const createSubscriptionPayment = async (
};
};

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

export const handler = makeIdempotent(
Expand Down
4 changes: 3 additions & 1 deletion docs/utilities/idempotency.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,11 @@ Imagine the function executes successfully, but the client never receives the re
???+ warning "Deserializing JSON strings in payloads for increased accuracy."
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.

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.

=== "index.ts"

```typescript hl_lines="4 26-28 49"
```typescript hl_lines="4 27 49"
--8<-- "docs/snippets/idempotency/makeIdempotentJmes.ts"
```

Expand Down
3 changes: 1 addition & 2 deletions layers/src/layer-publisher-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export class LayerPublisherStack extends Stack {
// the name is the same as the npm workspace name
const utilities = [
'commons',
'jmespath',
'logger',
'metrics',
'tracer',
Expand All @@ -87,8 +88,6 @@ export class LayerPublisherStack extends Stack {
'node_modules/async-hook-jl/test',
'node_modules/stack-chain/test',
'node_modules/shimmer/test',
'node_modules/jmespath/artifacts',
'node_modules/jmespath/bower.json',
'node_modules/obliterator/*.d.ts',
'node_modules/strnum/.vscode',
'node_modules/strnum/*.test.js',
Expand Down
10 changes: 2 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 29 additions & 8 deletions packages/idempotency/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ You can use the package in both TypeScript and JavaScript code bases.
- [Becoming a reference customer](#becoming-a-reference-customer)
- [Sharing your work](#sharing-your-work)
- [Using Lambda Layer](#using-lambda-layer)
- [Credits](#credits)
- [License](#license)

## Intro
Expand Down Expand Up @@ -158,7 +157,33 @@ export const handler = makeIdempotent(myHandler, {
config: new IdempotencyConfig({
eventKeyJmespath: 'requestContext.identity.user',
}),
});
});
```

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.

```ts
import { makeIdempotent, IdempotencyConfig } from '@aws-lambda-powertools/idempotency';
import { DynamoDBPersistenceLayer } from '@aws-lambda-powertools/idempotency/dynamodb';
import type { Context, APIGatewayProxyEvent } from 'aws-lambda';

const persistenceStore = new DynamoDBPersistenceLayer({
tableName: 'idempotencyTableName',
});

const myHandler = async (
event: APIGatewayProxyEvent,
_context: Context
): Promise<void> => {
// your code goes here here
};

export const handler = makeIdempotent(myHandler, {
persistenceStore,
config: new IdempotencyConfig({
eventKeyJmespath: 'powertools_json(body).["user", "productId"]',
}),
});
```

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

### Using Lambda Layer

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.

## Credits

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/).
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.

## License

This library is licensed under the MIT-0 License. See the LICENSE file.
This library is licensed under the MIT-0 License. See the LICENSE file.
3 changes: 1 addition & 2 deletions packages/idempotency/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
},
"dependencies": {
"@aws-lambda-powertools/commons": "^2.0.4",
"jmespath": "^0.16.0"
"@aws-lambda-powertools/jmespath": "^2.0.4"
},
"peerDependencies": {
"@aws-sdk/client-dynamodb": ">=3.x",
Expand Down Expand Up @@ -131,7 +131,6 @@
"@aws-lambda-powertools/testing-utils": "file:../testing",
"@aws-sdk/client-dynamodb": "^3.554.0",
"@aws-sdk/lib-dynamodb": "^3.554.0",
"@types/jmespath": "^0.15.0",
"aws-sdk-client-mock": "^4.0.0",
"aws-sdk-client-mock-jest": "^4.0.0"
}
Expand Down
6 changes: 4 additions & 2 deletions packages/idempotency/src/IdempotencyHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ import { BasePersistenceLayer } from './persistence/BasePersistenceLayer.js';
import { IdempotencyRecord } from './persistence/IdempotencyRecord.js';
import { IdempotencyConfig } from './IdempotencyConfig.js';
import { MAX_RETRIES, IdempotencyRecordStatus } from './constants.js';
import { search } from 'jmespath';
import { search } from '@aws-lambda-powertools/jmespath';
import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions';

/**
* @internal
Expand Down Expand Up @@ -275,8 +276,9 @@ export class IdempotencyHandler<Func extends AnyFunction> {
!this.#idempotencyConfig.throwOnNoIdempotencyKey
) {
const selection = search(
this.#idempotencyConfig.eventKeyJmesPath,
this.#functionPayloadToBeHashed,
this.#idempotencyConfig.eventKeyJmesPath
{ customFunctions: new PowertoolsFunctions() }
);

return selection === undefined || selection === null;
Expand Down
11 changes: 8 additions & 3 deletions packages/idempotency/src/persistence/BasePersistenceLayer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createHash, Hash } from 'node:crypto';
import { search } from 'jmespath';
import { search } from '@aws-lambda-powertools/jmespath';
import { PowertoolsFunctions } from '@aws-lambda-powertools/jmespath/functions';
import type {
BasePersistenceLayerOptions,
BasePersistenceLayerInterface,
Expand Down Expand Up @@ -279,7 +280,9 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
*/
private getHashedIdempotencyKey(data: JSONValue): string {
if (this.eventKeyJmesPath) {
data = search(data, this.eventKeyJmesPath);
data = search(this.eventKeyJmesPath, data, {
customFunctions: new PowertoolsFunctions(),
}) as JSONValue;
}

if (BasePersistenceLayer.isMissingIdempotencyKey(data)) {
Expand All @@ -305,7 +308,9 @@ abstract class BasePersistenceLayer implements BasePersistenceLayerInterface {
*/
private getHashedPayload(data: JSONValue): string {
if (this.isPayloadValidationEnabled() && this.validationKeyJmesPath) {
data = search(data, this.validationKeyJmesPath);
data = search(this.validationKeyJmesPath, data, {
customFunctions: new PowertoolsFunctions(),
}) as JSONValue;

return this.generateHash(JSON.stringify(data));
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,17 @@ export const handlerCustomized = async (
* Test idempotent Lambda handler with JMESPath expression to extract event key.
*/
export const handlerLambda = makeIdempotent(
async (event: { foo: string }, context: Context) => {
async (event: { body: string }, context: Context) => {
logger.addContext(context);
logger.info(`foo`, { details: event.foo });
const body = JSON.parse(event.body);
logger.info('foo', { details: body.foo });

return event.foo;
return body.foo;
},
{
persistenceStore: dynamoDBPersistenceLayer,
config: new IdempotencyConfig({
eventKeyJmesPath: 'foo',
eventKeyJmesPath: 'powertools_json(body).foo',
useLocalCache: true,
}),
}
Expand Down
6 changes: 4 additions & 2 deletions packages/idempotency/tests/e2e/makeIdempotent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,10 +268,12 @@ describe(`Idempotency E2E tests, wrapper function usage`, () => {
async () => {
// Prepare
const payload = {
foo: 'bar',
body: JSON.stringify({
foo: 'bar',
}),
};
const payloadHash = createHash('md5')
.update(JSON.stringify(payload.foo))
.update(JSON.stringify('bar'))
.digest('base64');

// Act
Expand Down
Loading