Repository: aws-powertools/powertools-lambda-typescript
Pull request #3931: feat(parser): add support for tumbling windows in Kinesis and DynamoDB events
Author: login: kiitosu
Labels: name: size/L
description: PRs between 100-499 LOC
name: feature
description: PRs that introduce new features or minor changes
name: tests
description: PRs that add or change tests
name: parser
description: This item relates to the Parser Utility
Created: 2025-05-15T11:37:10Z | Merged: 2025-05-19T08:52:26Z

## Summary
 Enable TumblingWindow support for KinesisDataStreamRecord and DynamoDB.

### Changes

Added properties to support tumbling windows in KinesisStreamEvent and DynamoDBStream Event:

- window
- state
- shard_id
- event_source_arn
- is_final_invoke_for_window
- is_window_terminated_early


**Issue number:** #3915

<!-------
Before creating the pull request, please make sure you do the following:

- Read the Contributing Guidelines at https://github.com/aws-powertools/powertools-lambda-typescript/blob/main/CONTRIBUTING.md#sending-a-pull-request
- Check that there isn't already a PR that addresses the same issue. If you find a duplicate, please leave a comment under the existing PR so we can discuss how to move forward
- Check that the change meets the project's tenets https://docs.powertools.aws.dev/lambda/typescript/latest/#tenets
- Add a PR title that follows the conventional commit semantics - https://github.com/aws-powertools/powertools-lambda-typescript/blob/main/.github/semantic.yml#L2
- If relevant, add tests that prove that the change is effective and works
- Whenever relevant, make sure to comment functions/methods/types and make appropriate changes to the documentation
------->

---

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

**Disclaimer**: We value your time and bandwidth. As such, any pull requests created on non-triaged issues might not be successful.



Diff:
diff --git a/packages/parser/src/schemas/dynamodb.ts b/packages/parser/src/schemas/dynamodb.ts
index 00170ac672..5d735ee5bf 100644
--- a/packages/parser/src/schemas/dynamodb.ts
+++ b/packages/parser/src/schemas/dynamodb.ts
@@ -151,69 +151,103 @@ const DynamoDBStreamToKinesisRecord = DynamoDBStreamRecord.extend({
  * @example
  * ```json
  * {
- *   "Records": [
- *     {
- *       "eventID": "1",
- *       "eventVersion": "1.0",
- *       "dynamodb": {
- *         "ApproximateCreationDateTime": 1693997155.0,
- *         "Keys": {
- *           "Id": {
- *             "N": "101"
- *           }
- *         },
- *         "NewImage": {
- *           "Message": {
- *             "S": "New item!"
- *           },
- *           "Id": {
- *             "N": "101"
- *           }
+ *   "Records":[{
+ *     "eventID":"1",
+ *     "eventName":"INSERT",
+ *     "eventVersion":"1.0",
+ *     "eventSource":"aws:dynamodb",
+ *     "awsRegion":"us-east-1",
+ *     "dynamodb":{
+ *       "Keys":{
+ *         "Id":{
+ *           "N":"101"
+ *         }
+ *       },
+ *       "NewImage":{
+ *         "Message":{
+ *           "S":"New item!"
  *         },
- *         "StreamViewType": "NEW_AND_OLD_IMAGES",
- *         "SequenceNumber": "111",
- *         "SizeBytes": 26
+ *         "Id":{
+ *           "N":"101"
+ *         }
  *       },
- *       "awsRegion": "us-west-2",
- *       "eventName": "INSERT",
- *       "eventSourceARN": "eventsource_arn",
- *       "eventSource": "aws:dynamodb"
+ *       "SequenceNumber":"111",
+ *       "SizeBytes":26,
+ *       "StreamViewType":"NEW_AND_OLD_IMAGES"
  *     },
- *     {
- *       "eventID": "2",
- *       "eventVersion": "1.0",
- *       "dynamodb": {
- *         "OldImage": {
- *           "Message": {
- *             "S": "New item!"
- *           },
- *           "Id": {
- *             "N": "101"
- *           }
+ *     "eventSourceARN":"stream-ARN"
+ *   },
+ *   {
+ *     "eventID":"2",
+ *     "eventName":"MODIFY",
+ *     "eventVersion":"1.0",
+ *     "eventSource":"aws:dynamodb",
+ *     "awsRegion":"us-east-1",
+ *     "dynamodb":{
+ *       "Keys":{
+ *         "Id":{
+ *           "N":"101"
+ *         }
+ *       },
+ *       "NewImage":{
+ *         "Message":{
+ *           "S":"This item has changed"
  *         },
- *         "SequenceNumber": "222",
- *         "Keys": {
- *           "Id": {
- *             "N": "101"
- *           }
+ *         "Id":{
+ *           "N":"101"
+ *         }
+ *       },
+ *       "OldImage":{
+ *         "Message":{
+ *           "S":"New item!"
  *         },
- *         "SizeBytes": 59,
- *         "NewImage": {
- *           "Message": {
- *             "S": "This item has changed"
- *           },
- *           "Id": {
- *             "N": "101"
- *           }
+ *         "Id":{
+ *           "N":"101"
+ *         }
+ *       },
+ *       "SequenceNumber":"222",
+ *       "SizeBytes":59,
+ *       "StreamViewType":"NEW_AND_OLD_IMAGES"
+ *     },
+ *     "eventSourceARN":"stream-ARN"
+ *   },
+ *   {
+ *     "eventID":"3",
+ *     "eventName":"REMOVE",
+ *     "eventVersion":"1.0",
+ *     "eventSource":"aws:dynamodb",
+ *     "awsRegion":"us-east-1",
+ *     "dynamodb":{
+ *       "Keys":{
+ *         "Id":{
+ *           "N":"101"
+ *         }
+ *       },
+ *       "OldImage":{
+ *         "Message":{
+ *           "S":"This item has changed"
  *         },
- *         "StreamViewType": "NEW_AND_OLD_IMAGES"
+ *         "Id":{
+ *           "N":"101"
+ *         }
  *       },
- *       "awsRegion": "us-west-2",
- *       "eventName": "MODIFY",
- *       "eventSourceARN": "source_arn",
- *       "eventSource": "aws:dynamodb"
- *     }
- *   ]
+ *       "SequenceNumber":"333",
+ *       "SizeBytes":38,
+ *       "StreamViewType":"NEW_AND_OLD_IMAGES"
+ *     },
+ *     "eventSourceARN":"stream-ARN"
+ *   }],
+ *   "window": {
+ *     "start": "2020-07-30T17:00:00Z",
+ *     "end": "2020-07-30T17:05:00Z"
+ *   },
+ *   "state": {
+ *     "1": "state1"
+ *   },
+ *   "shardId": "shard123456789",
+ *   "eventSourceARN": "stream-ARN",
+ *   "isFinalInvokeForWindow": false,
+ *   "isWindowTerminatedEarly": false
  * }
  * ```
  *
@@ -222,6 +256,17 @@ const DynamoDBStreamToKinesisRecord = DynamoDBStreamRecord.extend({
  */
 const DynamoDBStreamSchema = z.object({
   Records: z.array(DynamoDBStreamRecord).min(1),
+  window: z
+    .object({
+      start: z.string().datetime(),
+      end: z.string().datetime(),
+    })
+    .optional(),
+  state: z.record(z.string(), z.string()).optional(),
+  shardId: z.string().optional(),
+  eventSourceARN: z.string().optional(),
+  isFinalInvokeForWindow: z.boolean().optional(),
+  isWindowTerminatedEarly: z.boolean().optional(),
 });
 
 export {
diff --git a/packages/parser/src/schemas/kinesis.ts b/packages/parser/src/schemas/kinesis.ts
index 8e21724305..9e0b8ba8e2 100644
--- a/packages/parser/src/schemas/kinesis.ts
+++ b/packages/parser/src/schemas/kinesis.ts
@@ -70,41 +70,48 @@ const KinesisDynamoDBStreamSchema = z.object({
  *         "partitionKey": "1",
  *         "sequenceNumber": "49590338271490256608559692538361571095921575989136588898",
  *         "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0Lg==",
- *         "approximateArrivalTimestamp": 1545084650.987
+ *         "approximateArrivalTimestamp": 1607497475.000
  *       },
  *       "eventSource": "aws:kinesis",
  *       "eventVersion": "1.0",
  *       "eventID": "shardId-000000000006:49590338271490256608559692538361571095921575989136588898",
  *       "eventName": "aws:kinesis:record",
- *       "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-role",
- *       "awsRegion": "us-east-2",
- *       "eventSourceARN": "arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream"
- *     },
- *     {
- *       "kinesis": {
- *         "kinesisSchemaVersion": "1.0",
- *         "partitionKey": "1",
- *         "sequenceNumber": "49590338271490256608559692540925702759324208523137515618",
- *         "data": "VGhpcyBpcyBvbmx5IGEgdGVzdC4=",
- *         "approximateArrivalTimestamp": 1545084711.166
- *       },
- *       "eventSource": "aws:kinesis",
- *       "eventVersion": "1.0",
- *       "eventID": "shardId-000000000006:49590338271490256608559692540925702759324208523137515618",
- *       "eventName": "aws:kinesis:record",
- *       "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-role",
- *       "awsRegion": "us-east-2",
- *       "eventSourceARN": "arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream"
+ *       "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-kinesis-role",
+ *       "awsRegion": "us-east-1",
+ *       "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/lambda-stream"
  *     }
- *   ]
+ *   ],
+ *   "window": {
+ *     "start": "2020-12-09T07:04:00Z",
+ *     "end": "2020-12-09T07:06:00Z"
+ *   },
+ *   "state": {
+ *     "1": 282,
+ *     "2": 715
+ *   },
+ *   "shardId": "shardId-000000000006",
+ *   "eventSourceARN": "arn:aws:kinesis:us-east-1:123456789012:stream/lambda-stream",
+ *   "isFinalInvokeForWindow": false,
+ *   "isWindowTerminatedEarly": false
  * }
  *```
  * @see {@link types.KinesisDataStreamEvent | KinesisDataStreamEvent}
- * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/with-kinesis.html#services-kinesis-event-example}
+ * @see {@link https://docs.aws.amazon.com/lambda/latest/dg/services-kinesis-windows.html#streams-tumbling-processing}
  *
  */
 const KinesisDataStreamSchema = z.object({
   Records: z.array(KinesisDataStreamRecord).min(1),
+  window: z
+    .object({
+      start: z.string().datetime(),
+      end: z.string().datetime(),
+    })
+    .optional(),
+  state: z.record(z.string(), z.unknown()).optional(),
+  shardId: z.string().optional(),
+  eventSourceARN: z.string().optional(),
+  isFinalInvokeForWindow: z.boolean().optional(),
+  isWindowTerminatedEarly: z.boolean().optional(),
 });
 
 export {
diff --git a/packages/parser/tests/unit/schema/dynamodb.test.ts b/packages/parser/tests/unit/schema/dynamodb.test.ts
index 62b4ab8d06..d10940b289 100644
--- a/packages/parser/tests/unit/schema/dynamodb.test.ts
+++ b/packages/parser/tests/unit/schema/dynamodb.test.ts
@@ -112,4 +112,85 @@ describe('Schema: DynamoDB', () => {
     // Act & Assess
     expect(() => DynamoDBStreamSchema.parse(event)).toThrow();
   });
+
+  it('parses a DynamoDB Stream with tumbling window event', () => {
+    // Prepare
+    const event = structuredClone(baseEvent);
+    event.window = {
+      start: '2020-07-30T17:00:00Z',
+      end: '2020-07-30T17:05:00Z',
+    };
+    event.state = {
+      '1': 'state1',
+    };
+    event.shardId = 'shard123456789';
+    event.eventSourceARN = 'stream-ARN';
+    event.isFinalInvokeForWindow = false;
+    event.isWindowTerminatedEarly = false;
+
+    // Act
+    const result = DynamoDBStreamSchema.parse(event);
+
+    // Assess
+    expect(result).toStrictEqual({
+      Records: [
+        {
+          eventID: '1',
+          eventVersion: '1.0',
+          dynamodb: {
+            ApproximateCreationDateTime: 1693997155.0,
+            Keys: {
+              Id: 101,
+            },
+            NewImage: {
+              Message: 'New item!',
+              Id: 101,
+            },
+            StreamViewType: 'NEW_IMAGE',
+            SequenceNumber: '111',
+            SizeBytes: 26,
+          },
+          awsRegion: 'us-west-2',
+          eventName: 'INSERT',
+          eventSourceARN: 'eventsource_arn',
+          eventSource: 'aws:dynamodb',
+        },
+        {
+          eventID: '2',
+          eventVersion: '1.0',
+          dynamodb: {
+            OldImage: {
+              Message: 'New item!',
+              Id: 101,
+            },
+            SequenceNumber: '222',
+            Keys: {
+              Id: 101,
+            },
+            SizeBytes: 59,
+            NewImage: {
+              Message: 'This item has changed',
+              Id: 101,
+            },
+            StreamViewType: 'NEW_AND_OLD_IMAGES',
+          },
+          awsRegion: 'us-west-2',
+          eventName: 'MODIFY',
+          eventSourceARN: 'source_arn',
+          eventSource: 'aws:dynamodb',
+        },
+      ],
+      window: {
+        start: '2020-07-30T17:00:00Z',
+        end: '2020-07-30T17:05:00Z',
+      },
+      state: {
+        '1': 'state1',
+      },
+      shardId: 'shard123456789',
+      eventSourceARN: 'stream-ARN',
+      isFinalInvokeForWindow: false,
+      isWindowTerminatedEarly: false,
+    });
+  });
 });
diff --git a/packages/parser/tests/unit/schema/kinesis.test.ts b/packages/parser/tests/unit/schema/kinesis.test.ts
index 94f7bc944a..0f8817abf3 100644
--- a/packages/parser/tests/unit/schema/kinesis.test.ts
+++ b/packages/parser/tests/unit/schema/kinesis.test.ts
@@ -167,6 +167,41 @@ describe('Schema: Kinesis', () => {
     expect(parsed).toStrictEqual(transformedInput);
   });
 
+  it('parses Kinesis event with tumbling window', () => {
+    // Prepare
+    const testEvent = structuredClone(kinesisStreamEvent);
+    testEvent.window = {
+      start: '2020-07-30T17:00:00Z',
+      end: '2020-07-30T17:05:00Z',
+    };
+    testEvent.state = {
+      '1': 'state1',
+    };
+    testEvent.shardId = 'shard123456789';
+    testEvent.eventSourceARN = 'stream-ARN';
+    testEvent.isFinalInvokeForWindow = false;
+    testEvent.isWindowTerminatedEarly = false;
+
+    // Act
+    const parsed = KinesisDataStreamSchema.parse(testEvent);
+
+    const transformedInput = {
+      ...testEvent,
+      Records: testEvent.Records.map((record, index) => {
+        return {
+          ...record,
+          kinesis: {
+            ...record.kinesis,
+            data: Buffer.from(record.kinesis.data, 'base64').toString(),
+          },
+        };
+      }),
+    };
+
+    // Assess
+    expect(parsed).toStrictEqual(transformedInput);
+  });
+
   it('throws if cannot parse SQS record of KinesisFirehoseSqsRecord', () => {
     // Prepare
     const testEvent = getTestEvent<KinesisFireHoseSqsEvent>({


Reviews:
user: login: am29d
body: Just a minor question about example in the docs
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-16T14:57:46Z
user: login: dreamorosi
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-18T08:18:04Z
user: login: am29d
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T08:51:48Z

Review comments:
diff_hunk: @@ -70,41 +70,48 @@ const KinesisDynamoDBStreamSchema = z.object({
  *         "partitionKey": "1",
  *         "sequenceNumber": "49590338271490256608559692538361571095921575989136588898",
  *         "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0Lg==",
- *         "approximateArrivalTimestamp": 1545084650.987
+ *         "approximateArrivalTimestamp": 1607497475.000
  *       },
  *       "eventSource": "aws:kinesis",
  *       "eventVersion": "1.0",
  *       "eventID": "shardId-000000000006:49590338271490256608559692538361571095921575989136588898",
  *       "eventName": "aws:kinesis:record",
- *       "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-role",
- *       "awsRegion": "us-east-2",
- *       "eventSourceARN": "arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream"
- *     },
- *     {
- *       "kinesis": {
- *         "kinesisSchemaVersion": "1.0",
- *         "partitionKey": "1",
- *         "sequenceNumber": "49590338271490256608559692540925702759324208523137515618",
- *         "data": "VGhpcyBpcyBvbmx5IGEgdGVzdC4=",
- *         "approximateArrivalTimestamp": 1545084711.166
- *       },
- *       "eventSource": "aws:kinesis",
- *       "eventVersion": "1.0",
- *       "eventID": "shardId-000000000006:49590338271490256608559692540925702759324208523137515618",
- *       "eventName": "aws:kinesis:record",
- *       "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-role",
- *       "awsRegion": "us-east-2",
- *       "eventSourceARN": "arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream"
path: packages/parser/src/schemas/kinesis.ts
user: login: am29d
body: Why do we remove these fields from the example? Or was it unintentional?
created_at: 2025-05-16T14:57:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -70,41 +70,48 @@ const KinesisDynamoDBStreamSchema = z.object({
  *         "partitionKey": "1",
  *         "sequenceNumber": "49590338271490256608559692538361571095921575989136588898",
  *         "data": "SGVsbG8sIHRoaXMgaXMgYSB0ZXN0Lg==",
- *         "approximateArrivalTimestamp": 1545084650.987
+ *         "approximateArrivalTimestamp": 1607497475.000
  *       },
  *       "eventSource": "aws:kinesis",
  *       "eventVersion": "1.0",
  *       "eventID": "shardId-000000000006:49590338271490256608559692538361571095921575989136588898",
  *       "eventName": "aws:kinesis:record",
- *       "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-role",
- *       "awsRegion": "us-east-2",
- *       "eventSourceARN": "arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream"
- *     },
- *     {
- *       "kinesis": {
- *         "kinesisSchemaVersion": "1.0",
- *         "partitionKey": "1",
- *         "sequenceNumber": "49590338271490256608559692540925702759324208523137515618",
- *         "data": "VGhpcyBpcyBvbmx5IGEgdGVzdC4=",
- *         "approximateArrivalTimestamp": 1545084711.166
- *       },
- *       "eventSource": "aws:kinesis",
- *       "eventVersion": "1.0",
- *       "eventID": "shardId-000000000006:49590338271490256608559692540925702759324208523137515618",
- *       "eventName": "aws:kinesis:record",
- *       "invokeIdentityArn": "arn:aws:iam::123456789012:role/lambda-role",
- *       "awsRegion": "us-east-2",
- *       "eventSourceARN": "arn:aws:kinesis:us-east-2:123456789012:stream/lambda-stream"
path: packages/parser/src/schemas/kinesis.ts
user: login: dreamorosi
body: The example had two records in the array, I removed one for brevity.
created_at: 2025-05-18T08:18:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: boring-cyborg[bot]
created_at: 2025-05-15T11:37:15Z
body: Thanks a lot for your first contribution! Please check out our [contributing guidelines](https://github.com/aws-powertools/powertools-lambda-typescript/blob/main/CONTRIBUTING.md) and don't hesitate to ask whatever you need.
In the meantime, check out the #typescript channel on our Powertools for AWS Lambda Discord: [Invite link](https://discord.gg/B8zZKbbyET)

author_association: NONE
user: login: kiitosu
created_at: 2025-05-15T12:29:17Z
body: @leandrodamascena
I just opened a pull request—I’d appreciate it if you could take a look!

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-15T13:55:08Z
body: No related issues found. Please ensure there is an open issue related to this change to avoid significant delays or closure.
author_association: CONTRIBUTOR
user: login: dreamorosi
created_at: 2025-05-16T14:52:15Z
body: @kiitosu thank you for the PR, I made some small fixes in the Kinesis schema which was implemented as a type and fixed the indentation of one of the comments. I also adapted the unit tests to be in line with the others. 

As soon as another maintainer approves the PR we're good to merge it.

@am29d I had to make a commit to fix the things above, can you pls review and approve when you can? The schemas were tested by Leandro in the same PR but in the Python repo.
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-19T08:08:08Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=aws-powertools_powertools-lambda-typescript&pullRequest=3931) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=aws-powertools_powertools-lambda-typescript&pullRequest=3931&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=aws-powertools_powertools-lambda-typescript&pullRequest=3931&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=aws-powertools_powertools-lambda-typescript&pullRequest=3931&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=aws-powertools_powertools-lambda-typescript&pullRequest=3931&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [24.6% Duplication on New Code](https://sonarcloud.io/component_measures?id=aws-powertools_powertools-lambda-typescript&pullRequest=3931&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=aws-powertools_powertools-lambda-typescript&pullRequest=3931)


author_association: NONE
user: login: boring-cyborg[bot]
created_at: 2025-05-19T08:52:28Z
body: Awesome work, congrats on your first merged pull request and thank you for helping improve everyone's experience!

author_association: NONE