Skip to content

Commit e83c865

Browse files
committed
docs(jmespath): documentation & tests
1 parent 035c4bb commit e83c865

12 files changed

+439
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"Records": [
3+
{
4+
"messageId": "19dd0b57-b21e-4ac1-bd88-01bbb068cb78",
5+
"receiptHandle": "MessageReceiptHandle",
6+
"body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\",\"booking\":{\"id\":\"5b2c4803-330b-42b7-811a-c68689425de1\",\"reference\":\"ySz7oA\",\"outboundFlightId\":\"20c0d2f2-56a3-4068-bf20-ff7703db552d\"},\"payment\":{\"receipt\":\"https://pay.stripe.com/receipts/acct_1Dvn7pF4aIiftV70/ch_3JTC14F4aIiftV700iFq2CHB/rcpt_K7QsrFln9FgFnzUuBIiNdkkRYGxUL0X\",\"amount\":100}}",
7+
"attributes": {
8+
"ApproximateReceiveCount": "1",
9+
"SentTimestamp": "1523232000000",
10+
"SenderId": "123456789012",
11+
"ApproximateFirstReceiveTimestamp": "1523232000001"
12+
},
13+
"messageAttributes": {},
14+
"md5OfBody": "7b270e59b47ff90a553787216d55d91d",
15+
"eventSource": "aws:sqs",
16+
"eventSourceARN": "arn:aws:sqs:us-east-1:123456789012:MyQueue",
17+
"awsRegion": "us-east-1"
18+
}
19+
]
20+
}
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {
2+
extractDataFromEnvelope,
3+
SQS,
4+
} from '@aws-lambda-powertools/jmespath/envelopes';
5+
import { Logger } from '@aws-lambda-powertools/logger';
6+
import type { SQSEvent } from 'aws-lambda';
7+
8+
const logger = new Logger();
9+
10+
type MessageBody = {
11+
customerId: string;
12+
};
13+
14+
export const handler = async (event: SQSEvent): Promise<void> => {
15+
const records = extractDataFromEnvelope<Array<MessageBody>>(event, SQS);
16+
for (const record of records) {
17+
// records is now a list containing the deserialized body of each message
18+
const { customerId } = record;
19+
logger.appendKeys({ customerId });
20+
}
21+
};

Diff for: docs/snippets/jmespath/extractDataFromEnvelope.json

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"body": "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}",
3+
"deeplyNested": [
4+
{
5+
"someData": [1, 2, 3]
6+
}
7+
]
8+
}

Diff for: docs/snippets/jmespath/extractDataFromEnvelope.ts

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { extractDataFromEnvelope } from '@aws-lambda-powertools/jmespath/envelopes';
2+
3+
type MyEvent = {
4+
body: string; // "{\"customerId\":\"dd4649e6-2484-4993-acb8-0f9123103394\"}"
5+
deeplyNested: Array<{ someData: number[] }>;
6+
};
7+
8+
type MessageBody = {
9+
customerId: string;
10+
};
11+
12+
export const handler = async (event: MyEvent): Promise<unknown> => {
13+
const payload = extractDataFromEnvelope<MessageBody>(
14+
event,
15+
'powertools_json(body)'
16+
);
17+
const { customerId } = payload; // now deserialized
18+
19+
// also works for fetching and flattening deeply nested data
20+
const someData = extractDataFromEnvelope<number[]>(
21+
event,
22+
'deeplyNested[*].someData[]'
23+
);
24+
25+
return {
26+
customerId,
27+
message: 'success',
28+
context: someData,
29+
statusCode: 200,
30+
};
31+
};

Diff for: docs/snippets/tsconfig.json

+5-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,11 @@
2727
"@aws-lambda-powertools/idempotency/middleware": [
2828
"../../packages/idempotency/lib/middleware"
2929
],
30-
"@aws-lambda-powertools/batch": ["../../packages/batch/lib"]
30+
"@aws-lambda-powertools/batch": ["../../packages/batch/lib"],
31+
"@aws-lambda-powertools/jmespath": ["../../packages/jmespath/lib"],
32+
"@aws-lambda-powertools/jmespath/envelopes": [
33+
"../../packages/jmespath/lib/envelopes"
34+
]
3135
}
3236
}
3337
}

Diff for: docs/utilities/jmespath.md

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
---
2+
title: JMESPath Functions
3+
description: Utility
4+
---
5+
6+
???+ tip
7+
JMESPath is a query language for JSON used by tools like the AWS CLI and Powertools for AWS Lambda (TypeScript).
8+
9+
Built-in [JMESPath](https://jmespath.org/){target="_blank" rel="nofollow"} Functions to easily deserialize common encoded JSON payloads in Lambda functions.
10+
11+
## Key features
12+
13+
* Deserialize JSON from JSON strings, base64, and compressed data
14+
* Use JMESPath to extract and combine data recursively
15+
* Provides commonly used JMESPath expression with popular event sources
16+
17+
## Getting started
18+
19+
You might have events that contains encoded JSON payloads as string, base64, or even in compressed format. It is a common use case to decode and extract them partially or fully as part of your Lambda function invocation.
20+
21+
Powertools for AWS Lambda (TypeScript) also have utilities like [idempotency](idempotency.md){target="_blank"} where you might need to extract a portion of your data before using them.
22+
23+
???+ info "Terminology"
24+
**Envelope** is the terminology we use for the **JMESPath expression** to extract your JSON object from your data input. We might use those two terms interchangeably.
25+
26+
### Extracting data
27+
28+
You can use the `extractDataFromEnvelope` function with any [JMESPath expression](https://jmespath.org/tutorial.html){target="_blank" rel="nofollow"}.
29+
30+
???+ tip
31+
Another common use case is to fetch deeply nested data, filter, flatten, and more.
32+
33+
=== "extractDataFromBuiltinEnvelope.ts"
34+
```typescript hl_lines="1 13 20"
35+
--8<-- "docs/snippets/jmespath/extractDataFromEnvelope.ts"
36+
```
37+
38+
=== "extractDataFromEnvelope.json"
39+
40+
```json
41+
--8<-- "docs/snippets/jmespath/extractDataFromEnvelope.json"
42+
```
43+
44+
### Built-in envelopes
45+
46+
We provide built-in envelopes for popular AWS Lambda event sources to easily decode and/or deserialize JSON objects.
47+
48+
=== "extractDataFromBuiltinEnvelope.ts"
49+
```typescript hl_lines="2-3 15"
50+
--8<-- "docs/snippets/jmespath/extractDataFromBuiltinEnvelope.ts"
51+
```
52+
53+
=== "extractDataFromBuiltinEnvelope.json"
54+
55+
```json hl_lines="6 15"
56+
--8<-- "docs/snippets/jmespath/extractDataFromBuiltinEnvelope.json"
57+
```
58+
59+
These are all built-in envelopes you can use along with their expression as a reference:
60+
61+
| Envelope | JMESPath expression |
62+
| --------------------------------- | ----------------------------------------------------------------------------------------- |
63+
| **`API_GATEWAY_HTTP`** | `powertools_json(body)` |
64+
| **`API_GATEWAY_REST`** | `powertools_json(body)` |
65+
| **`CLOUDWATCH_EVENTS_SCHEDULED`** | `detail` |
66+
| **`CLOUDWATCH_LOGS`** | `awslogs.powertools_base64_gzip(data) | powertools_json(@).logEvents[*]` |
67+
| **`EVENTBRIDGE`** | `detail` |
68+
| **`KINESIS_DATA_STREAM`** | `Records[*].kinesis.powertools_json(powertools_base64(data))` |
69+
| **`S3_EVENTBRIDGE_SQS`** | `Records[*].powertools_json(body).detail` |
70+
| **`S3_KINESIS_FIREHOSE`** | `records[*].powertools_json(powertools_base64(data)).Records[0]` |
71+
| **`S3_SNS_KINESIS_FIREHOSE`** | `records[*].powertools_json(powertools_base64(data)).powertools_json(Message).Records[0]` |
72+
| **`S3_SNS_SQS`** | `Records[*].powertools_json(body).powertools_json(Message).Records[0]` |
73+
| **`S3_SQS`** | `Records[*].powertools_json(body).Records[0]` |
74+
| **`SNS`** | `Records[0].Sns.Message | powertools_json(@)` |
75+
| **`SQS`** | `Records[*].powertools_json(body)` |
76+
77+
???+ tip "Using SNS?"
78+
If you don't require SNS metadata, enable [raw message delivery](https://docs.aws.amazon.com/sns/latest/dg/sns-large-payload-raw-message-delivery.html). It will reduce multiple payload layers and size, when using SNS in combination with other services (_e.g., SQS, S3, etc_).
79+
80+
## Advanced
81+
82+
### Built-in JMESPath functions
83+
84+
You can use our built-in JMESPath functions within your envelope expression. They handle deserialization for common data formats found in AWS Lambda event sources such as JSON strings, base64, and uncompress gzip data.
85+
86+
#### powertools_json function
87+
88+
Use `powertools_json` function to decode any JSON string anywhere a JMESPath expression is allowed.
89+
90+
> **Idempotency scenario**
91+
92+
This sample will deserialize the JSON string within the `body` key before [Idempotency](./idempotency.md){target="_blank"} processes it.
93+
94+
=== "powertools_json_idempotency_jmespath.py"
95+
96+
```python hl_lines="16"
97+
--8<-- "examples/jmespath_functions/src/powertools_json_idempotency_jmespath.py"
98+
```
99+
100+
=== "powertools_json_idempotency_jmespath.json"
101+
102+
```json hl_lines="28"
103+
--8<-- "examples/jmespath_functions/src/powertools_json_idempotency_jmespath.json"
104+
```
105+
106+
#### powertools_base64 function
107+
108+
Use `powertools_base64` function to decode any base64 data.
109+
110+
This sample will decode the base64 value within the `data` key, and deserialize the JSON string before processing.
111+
112+
=== "powertools_base64_jmespath_function.py"
113+
114+
```python hl_lines="7 10 37 49 53 55 57"
115+
--8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_function.py"
116+
```
117+
118+
=== "powertools_base64_jmespath_schema.py"
119+
120+
```python hl_lines="7 8 10 12 17 19 24 26 31 33 38 40"
121+
--8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_schema.py"
122+
```
123+
124+
=== "powertools_base64_jmespath_payload.json"
125+
126+
```json
127+
--8<-- "examples/jmespath_functions/src/powertools_base64_jmespath_payload.json"
128+
```
129+
130+
#### powertools_base64_gzip function
131+
132+
Use `powertools_base64_gzip` function to decompress and decode base64 data.
133+
134+
This sample will decompress and decode base64 data from Cloudwatch Logs, then use JMESPath pipeline expression to pass the result for decoding its JSON string.
135+
136+
=== "powertools_base64_gzip_jmespath_function.py"
137+
138+
```python hl_lines="6 10 15 29 31 33 35"
139+
--8<-- "examples/jmespath_functions/src/powertools_base64_gzip_jmespath_function.py"
140+
```
141+
142+
=== "powertools_base64_gzip_jmespath_schema.py"
143+
144+
```python hl_lines="7-15 17 19 24 26 31 33 38 40"
145+
--8<-- "examples/jmespath_functions/src/powertools_base64_gzip_jmespath_schema.py"
146+
```
147+
148+
=== "powertools_base64_gzip_jmespath_payload.json"
149+
150+
```json
151+
--8<-- "examples/jmespath_functions/src/powertools_base64_gzip_jmespath_payload.json"
152+
```
153+
154+
### Bring your own JMESPath function
155+
156+
???+ warning
157+
This should only be used for advanced use cases where you have special formats not covered by the built-in functions.
158+
159+
For special binary formats that you want to decode before processing, you can bring your own [JMESPath function](https://github.com/jmespath/jmespath.py#custom-functions){target="_blank" rel="nofollow"} and any additional option via `jmespath_options` param. To keep Powertools for AWS Lambda (TypeScript) built-in functions, you can extend the `PowertoolsFunctions` class.
160+
161+
Here is an example of how to decompress messages using [zlib](https://docs.python.org/3/library/zlib.html){target="_blank" rel="nofollow"}:
162+
163+
=== "powertools_custom_jmespath_function.py"
164+
165+
```python hl_lines="9 14 17-18 23 34 39 41 43"
166+
--8<-- "examples/jmespath_functions/src/powertools_custom_jmespath_function.py"
167+
```
168+
169+
=== "powertools_custom_jmespath_function.json"
170+
171+
```json
172+
--8<-- "examples/jmespath_functions/src/powertools_custom_jmespath_function.json"
173+
```

Diff for: packages/commons/package.json

+11-3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@
4040
"default": "./lib/esm/index.js"
4141
}
4242
},
43+
"./utils/base64": {
44+
"import": "./lib/esm/fromBase64.js",
45+
"require": "./lib/cjs/fromBase64.js"
46+
},
4347
"./typeutils": {
4448
"import": "./lib/esm/typeUtils.js",
4549
"require": "./lib/cjs/typeUtils.js"
@@ -51,13 +55,17 @@
5155
},
5256
"typesVersions": {
5357
"*": {
54-
"types": [
55-
"lib/cjs/types/index.d.ts",
56-
"lib/esm/types/index.d.ts"
58+
"utils/base64": [
59+
"lib/cjs/fromBase64.d.ts",
60+
"lib/esm/fromBase64.d.ts"
5761
],
5862
"typeutils": [
5963
"lib/cjs/typeUtils.d.ts",
6064
"lib/esm/typeUtils.d.ts"
65+
],
66+
"types": [
67+
"lib/cjs/types/index.d.ts",
68+
"lib/esm/types/index.d.ts"
6169
]
6270
}
6371
},

Diff for: packages/commons/src/fromBase64.ts

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const BASE64_REGEX = /^[A-Za-z0-9+/]*={0,2}$/;
2+
3+
const fromBase64 = (input: string, encoding?: BufferEncoding): Uint8Array => {
4+
if ((input.length * 3) % 4 !== 0) {
5+
throw new TypeError(`Incorrect padding on base64 string.`);
6+
}
7+
if (!BASE64_REGEX.exec(input)) {
8+
throw new TypeError(`Invalid base64 string.`);
9+
}
10+
const buffer = encoding ? Buffer.from(input, encoding) : Buffer.from(input);
11+
12+
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
13+
};
14+
15+
export { fromBase64 };

Diff for: packages/jmespath/package.json

+8
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@
3939
"default": "./lib/esm/index.js"
4040
}
4141
},
42+
"./envelopes": {
43+
"import": "./lib/esm/envelopes.js",
44+
"require": "./lib/cjs/envelopes.js"
45+
},
4246
"./types": {
4347
"import": "./lib/esm/types.js",
4448
"require": "./lib/cjs/types.js"
@@ -49,6 +53,10 @@
4953
"types": [
5054
"lib/cjs/types.d.ts",
5155
"lib/esm/types.d.ts"
56+
],
57+
"envelopes": [
58+
"lib/cjs/envelopes.d.ts",
59+
"lib/esm/envelopes.d.ts"
5260
]
5361
}
5462
},

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

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import zlib from 'node:zlib';
2+
import type { JSONValue } from '@aws-lambda-powertools/commons/types';
3+
import { fromBase64 } from '@aws-lambda-powertools/commons/utils/base64';
4+
import { Functions } from './Functions.js';
5+
6+
const decoder = new TextDecoder('utf-8');
7+
8+
class PowertoolsFunctions extends Functions {
9+
@Functions.signature({
10+
argumentsSpecs: [['string']],
11+
})
12+
public funcPowertoolsBase64(value: string): string {
13+
return decoder.decode(fromBase64(value, 'base64'));
14+
}
15+
16+
@Functions.signature({
17+
argumentsSpecs: [['string']],
18+
})
19+
public funcPowertoolsBase64Gzip(value: string): string {
20+
const encoded = fromBase64(value, 'base64');
21+
const uncompressed = zlib.gunzipSync(encoded);
22+
23+
return uncompressed.toString();
24+
}
25+
26+
@Functions.signature({
27+
argumentsSpecs: [['string']],
28+
})
29+
public funcPowertoolsJson(value: string): JSONValue {
30+
return JSON.parse(value);
31+
}
32+
}
33+
34+
export { PowertoolsFunctions };

0 commit comments

Comments
 (0)