@middy/event-batch-response 7.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -0
- package/index.d.ts +25 -0
- package/index.js +219 -0
- package/package.json +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<h1>Middy `event-batch-response` middleware</h1>
|
|
3
|
+
<img alt="Middy logo" src="https://raw.githubusercontent.com/middyjs/middy/main/docs/img/middy-logo.svg"/>
|
|
4
|
+
<p><strong>Batch response middleware for the middy framework, the stylish Node.js middleware engine for AWS Lambda. Shapes the response for SQS, Kinesis, DynamoDB Streams, Kafka, S3 Batch Operations, and Kinesis Firehose transform.</strong></p>
|
|
5
|
+
<p>
|
|
6
|
+
<a href="https://github.com/middyjs/middy/actions/workflows/test-unit.yml"><img src="https://github.com/middyjs/middy/actions/workflows/test-unit.yml/badge.svg" alt="GitHub Actions unit test status"></a>
|
|
7
|
+
<a href="https://github.com/middyjs/middy/actions/workflows/test-dast.yml"><img src="https://github.com/middyjs/middy/actions/workflows/test-dast.yml/badge.svg" alt="GitHub Actions dast test status"></a>
|
|
8
|
+
<a href="https://github.com/middyjs/middy/actions/workflows/test-perf.yml"><img src="https://github.com/middyjs/middy/actions/workflows/test-perf.yml/badge.svg" alt="GitHub Actions perf test status"></a>
|
|
9
|
+
<a href="https://github.com/middyjs/middy/actions/workflows/test-sast.yml"><img src="https://github.com/middyjs/middy/actions/workflows/test-sast.yml/badge.svg" alt="GitHub Actions SAST test status"></a>
|
|
10
|
+
<a href="https://github.com/middyjs/middy/actions/workflows/test-lint.yml"><img src="https://github.com/middyjs/middy/actions/workflows/test-lint.yml/badge.svg" alt="GitHub Actions lint test status"></a>
|
|
11
|
+
<br/>
|
|
12
|
+
<a href="https://www.npmjs.com/package/@middy/event-batch-response"><img alt="npm version" src="https://img.shields.io/npm/v/@middy/event-batch-response.svg"></a>
|
|
13
|
+
<a href="https://packagephobia.com/result?p=@middy/event-batch-response"><img src="https://packagephobia.com/badge?p=@middy/event-batch-response" alt="npm install size"></a>
|
|
14
|
+
<a href="https://www.npmjs.com/package/@middy/event-batch-response">
|
|
15
|
+
<img alt="npm weekly downloads" src="https://img.shields.io/npm/dw/@middy/event-batch-response.svg"></a>
|
|
16
|
+
<a href="https://www.npmjs.com/package/@middy/event-batch-response#provenance">
|
|
17
|
+
<img alt="npm provenance" src="https://img.shields.io/badge/provenance-Yes-brightgreen"></a>
|
|
18
|
+
<br/>
|
|
19
|
+
<a href="https://scorecard.dev/viewer/?uri=github.com/middyjs/middy"><img src="https://api.scorecard.dev/projects/github.com/middyjs/middy/badge" alt="Open Source Security Foundation (OpenSSF) Scorecard"></a>
|
|
20
|
+
<a href="https://slsa.dev"><img src="https://slsa.dev/images/gh-badge-level3.svg" alt="SLSA 3"></a>
|
|
21
|
+
<a href="https://github.com/middyjs/middy/blob/main/docs/CODE_OF_CONDUCT.md"><img src="https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg"></a>
|
|
22
|
+
<a href="https://biomejs.dev"><img alt="Checked with Biome" src="https://img.shields.io/badge/Checked_with-Biome-60a5fa?style=flat&logo=biome"></a>
|
|
23
|
+
<a href="https://conventionalcommits.org"><img alt="Conventional Commits" src="https://img.shields.io/badge/Conventional%20Commits-1.0.0-%23FE5196?logo=conventionalcommits&logoColor=white"></a>
|
|
24
|
+
<a href="https://github.com/middyjs/middy/blob/main/package.json#L32">
|
|
25
|
+
<img alt="code coverage" src="https://img.shields.io/badge/code%20coverage-100%25-brightgreen"></a>
|
|
26
|
+
<br/>
|
|
27
|
+
</p>
|
|
28
|
+
<p>You can read the documentation at: <a href="https://middy.js.org/docs/middlewares/event-batch-response">https://middy.js.org/docs/middlewares/event-batch-response</a></p>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install --save @middy/event-batch-response
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Documentation and examples
|
|
39
|
+
|
|
40
|
+
For documentation and examples, refer to the main [Middy monorepo on GitHub](https://github.com/middyjs/middy) or [Middy official website](https://middy.js.org/docs/middlewares/event-batch-response).
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Contributing
|
|
44
|
+
|
|
45
|
+
Everyone is very welcome to contribute to this repository. Feel free to [raise issues](https://github.com/middyjs/middy/issues) or to [submit Pull Requests](https://github.com/middyjs/middy/pulls).
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
## License
|
|
49
|
+
|
|
50
|
+
Licensed under [MIT License](https://github.com/middyjs/middy/blob/main/LICENSE). Copyright (c) 2017-2026 [will Farrell](https://github.com/willfarrell), [Luciano Mammino](https://github.com/lmammino), and [Middy contributors](https://github.com/middyjs/middy/graphs/contributors).
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Copyright 2017 - 2026 will Farrell, Luciano Mammino, and Middy contributors.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import type middy from "@middy/core";
|
|
4
|
+
|
|
5
|
+
declare function eventBatchResponse(): middy.MiddlewareObj<
|
|
6
|
+
unknown,
|
|
7
|
+
unknown,
|
|
8
|
+
Error
|
|
9
|
+
>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Walk a Lambda batch event into a flat array of records, using the same
|
|
13
|
+
* detection precedence as the middleware. Shared with `@middy/event-batch-handler`
|
|
14
|
+
* so both packages iterate records in the same order.
|
|
15
|
+
*
|
|
16
|
+
* Supported sources: SQS, Kinesis Data Streams, DynamoDB Streams, MSK,
|
|
17
|
+
* self-managed Kafka, S3 Batch Operations, Kinesis Firehose transform.
|
|
18
|
+
*
|
|
19
|
+
* Returns `[]` for unrecognized event shapes or missing containers.
|
|
20
|
+
* Coerces non-array containers to `[]` rather than throwing — malformed
|
|
21
|
+
* events degrade silently.
|
|
22
|
+
*/
|
|
23
|
+
export declare function flattenBatchRecords(event: unknown): unknown[];
|
|
24
|
+
|
|
25
|
+
export default eventBatchResponse;
|
package/index.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// Copyright 2017 - 2026 will Farrell, Luciano Mammino, and Middy contributors.
|
|
2
|
+
// SPDX-License-Identifier: MIT
|
|
3
|
+
import { isExecutionModeDurable } from "@middy/util";
|
|
4
|
+
|
|
5
|
+
const name = "event-batch-response";
|
|
6
|
+
const pkg = `@middy/${name}`;
|
|
7
|
+
|
|
8
|
+
const buildBatchItemFailures = ({ items, settled }) => {
|
|
9
|
+
const batchItemFailures = [];
|
|
10
|
+
for (let idx = 0; idx < items.length; idx += 1) {
|
|
11
|
+
const entry = settled[idx] ?? {};
|
|
12
|
+
if (entry.status === "fulfilled") continue;
|
|
13
|
+
batchItemFailures.push({ itemIdentifier: items[idx].identifier });
|
|
14
|
+
}
|
|
15
|
+
return { batchItemFailures };
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const toS3BatchResult = (taskId, value, defaultCode, reason) => {
|
|
19
|
+
if (value && typeof value === "object" && "resultCode" in value) {
|
|
20
|
+
return {
|
|
21
|
+
taskId,
|
|
22
|
+
resultCode: value.resultCode,
|
|
23
|
+
resultString: value.resultString ?? "",
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (typeof value === "string") {
|
|
27
|
+
return { taskId, resultCode: defaultCode, resultString: value };
|
|
28
|
+
}
|
|
29
|
+
const resultString =
|
|
30
|
+
reason instanceof Error ? reason.message : (reason ?? "");
|
|
31
|
+
return {
|
|
32
|
+
taskId,
|
|
33
|
+
resultCode: defaultCode,
|
|
34
|
+
resultString: String(resultString),
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const buildS3BatchResponse = ({ items, settled, request }) => {
|
|
39
|
+
const results = items.map((item, idx) => {
|
|
40
|
+
const entry = settled[idx] ?? {};
|
|
41
|
+
if (entry.status === "fulfilled") {
|
|
42
|
+
return toS3BatchResult(item.identifier, entry.value, "Succeeded");
|
|
43
|
+
}
|
|
44
|
+
return toS3BatchResult(
|
|
45
|
+
item.identifier,
|
|
46
|
+
entry.value,
|
|
47
|
+
"TemporaryFailure",
|
|
48
|
+
entry.reason,
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
invocationSchemaVersion: request.event.invocationSchemaVersion,
|
|
53
|
+
treatMissingKeysAs: "PermanentFailure",
|
|
54
|
+
invocationId: request.event.invocationId,
|
|
55
|
+
results,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const encodeFirehoseData = (value, fallback) => {
|
|
60
|
+
if (value === undefined || value === null) return fallback;
|
|
61
|
+
if (typeof value === "string") return Buffer.from(value).toString("base64");
|
|
62
|
+
if (Buffer.isBuffer(value)) return value.toString("base64");
|
|
63
|
+
if (value instanceof Uint8Array) return Buffer.from(value).toString("base64");
|
|
64
|
+
return Buffer.from(JSON.stringify(value)).toString("base64");
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const toFirehoseRecord = (recordId, inputData, value, defaultResult) => {
|
|
68
|
+
if (value && typeof value === "object" && "result" in value) {
|
|
69
|
+
return {
|
|
70
|
+
recordId,
|
|
71
|
+
result: value.result,
|
|
72
|
+
data: encodeFirehoseData(value.data, inputData),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
recordId,
|
|
77
|
+
result: defaultResult,
|
|
78
|
+
data: encodeFirehoseData(value, inputData),
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const buildFirehoseResponse = ({ items, settled }) => {
|
|
83
|
+
const records = items.map((item, idx) => {
|
|
84
|
+
const entry = settled[idx] ?? {};
|
|
85
|
+
const inputData = item.record?.data;
|
|
86
|
+
if (entry.status === "fulfilled") {
|
|
87
|
+
return toFirehoseRecord(item.identifier, inputData, entry.value, "Ok");
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
recordId: item.identifier,
|
|
91
|
+
result: "ProcessingFailed",
|
|
92
|
+
data: inputData,
|
|
93
|
+
};
|
|
94
|
+
});
|
|
95
|
+
return { records };
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const asArray = (value) => (Array.isArray(value) ? value : []);
|
|
99
|
+
const sqsLikeRecords = (event) => asArray(event.Records);
|
|
100
|
+
const kafkaRecords = (event) => {
|
|
101
|
+
const records = event.records;
|
|
102
|
+
if (!records || typeof records !== "object") return [];
|
|
103
|
+
const out = [];
|
|
104
|
+
for (const messages of Object.values(records)) {
|
|
105
|
+
if (!Array.isArray(messages)) continue;
|
|
106
|
+
for (const message of messages) out.push(message);
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const sources = {
|
|
112
|
+
"aws:sqs": {
|
|
113
|
+
getRecords: sqsLikeRecords,
|
|
114
|
+
identify: (record) => record?.messageId,
|
|
115
|
+
buildResponse: buildBatchItemFailures,
|
|
116
|
+
},
|
|
117
|
+
"aws:kinesis": {
|
|
118
|
+
getRecords: sqsLikeRecords,
|
|
119
|
+
identify: (record) => record?.kinesis?.sequenceNumber,
|
|
120
|
+
buildResponse: buildBatchItemFailures,
|
|
121
|
+
},
|
|
122
|
+
"aws:dynamodb": {
|
|
123
|
+
getRecords: sqsLikeRecords,
|
|
124
|
+
identify: (record) => record?.dynamodb?.SequenceNumber,
|
|
125
|
+
buildResponse: buildBatchItemFailures,
|
|
126
|
+
},
|
|
127
|
+
"aws:kafka": {
|
|
128
|
+
getRecords: kafkaRecords,
|
|
129
|
+
identify: (message) =>
|
|
130
|
+
message && `${message.topic}-${message.partition}-${message.offset}`,
|
|
131
|
+
buildResponse: buildBatchItemFailures,
|
|
132
|
+
},
|
|
133
|
+
"aws:s3:batch": {
|
|
134
|
+
getRecords: (event) => asArray(event.tasks),
|
|
135
|
+
identify: (task) => task?.taskId,
|
|
136
|
+
buildResponse: buildS3BatchResponse,
|
|
137
|
+
},
|
|
138
|
+
"aws:lambda:events": {
|
|
139
|
+
getRecords: (event) => asArray(event.records),
|
|
140
|
+
identify: (record) => record?.recordId,
|
|
141
|
+
buildResponse: buildFirehoseResponse,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
sources.SelfManagedKafka = sources["aws:kafka"];
|
|
145
|
+
|
|
146
|
+
const detectEventSource = (event) => {
|
|
147
|
+
if (!event || typeof event !== "object") return undefined;
|
|
148
|
+
if (event.eventSource) return event.eventSource;
|
|
149
|
+
// Firehose transform: identified by deliveryStreamArn.
|
|
150
|
+
if (event.deliveryStreamArn) return "aws:lambda:events";
|
|
151
|
+
// S3 Batch Operations: no eventSource on event or task; identified by
|
|
152
|
+
// invocationSchemaVersion + tasks array.
|
|
153
|
+
if (event.invocationSchemaVersion && Array.isArray(event.tasks)) {
|
|
154
|
+
return "aws:s3:batch";
|
|
155
|
+
}
|
|
156
|
+
const records = event.Records ?? event.records ?? event.events ?? event.tasks;
|
|
157
|
+
if (Array.isArray(records) && records[0]) {
|
|
158
|
+
return records[0].eventSource ?? records[0].EventSource;
|
|
159
|
+
}
|
|
160
|
+
return undefined;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export const flattenBatchRecords = (event) => {
|
|
164
|
+
const source = sources[detectEventSource(event)];
|
|
165
|
+
if (!source) return [];
|
|
166
|
+
return source.getRecords(event);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const buildItems = (source, event) =>
|
|
170
|
+
source.getRecords(event).map((record) => ({
|
|
171
|
+
record,
|
|
172
|
+
identifier: source.identify(record),
|
|
173
|
+
}));
|
|
174
|
+
|
|
175
|
+
const eventBatchResponseMiddleware = () => {
|
|
176
|
+
const eventBatchResponseMiddlewareBefore = (request) => {
|
|
177
|
+
const source = sources[detectEventSource(request.event)];
|
|
178
|
+
if (!source) return;
|
|
179
|
+
request.internal[pkg] = {
|
|
180
|
+
source,
|
|
181
|
+
items: buildItems(source, request.event),
|
|
182
|
+
};
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const eventBatchResponseMiddlewareAfter = (request) => {
|
|
186
|
+
const cached = request.internal[pkg];
|
|
187
|
+
if (!cached) return;
|
|
188
|
+
if (!Array.isArray(request.response)) return;
|
|
189
|
+
|
|
190
|
+
request.response = cached.source.buildResponse({
|
|
191
|
+
items: cached.items,
|
|
192
|
+
settled: request.response,
|
|
193
|
+
request,
|
|
194
|
+
});
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const eventBatchResponseMiddlewareOnError = (request) => {
|
|
198
|
+
// TODO remove in v8: core already skips the onError stack in durable mode.
|
|
199
|
+
if (isExecutionModeDurable(request.context)) throw request.error;
|
|
200
|
+
if (typeof request.response !== "undefined") return;
|
|
201
|
+
const cached = request.internal[pkg];
|
|
202
|
+
if (!cached) return;
|
|
203
|
+
|
|
204
|
+
request.response = Array.from({ length: cached.items.length }, () => ({
|
|
205
|
+
status: "rejected",
|
|
206
|
+
reason: request.error,
|
|
207
|
+
}));
|
|
208
|
+
|
|
209
|
+
eventBatchResponseMiddlewareAfter(request);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
before: eventBatchResponseMiddlewareBefore,
|
|
214
|
+
after: eventBatchResponseMiddlewareAfter,
|
|
215
|
+
onError: eventBatchResponseMiddlewareOnError,
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export default eventBatchResponseMiddleware;
|
package/package.json
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@middy/event-batch-response",
|
|
3
|
+
"version": "7.5.0",
|
|
4
|
+
"description": "Batch response middleware for the middy framework (SQS, Kinesis, DynamoDB Streams, Kafka, S3 Batch, Firehose)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22"
|
|
8
|
+
},
|
|
9
|
+
"engineStrict": true,
|
|
10
|
+
"publishConfig": {
|
|
11
|
+
"access": "public"
|
|
12
|
+
},
|
|
13
|
+
"module": "./index.js",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": {
|
|
18
|
+
"types": "./index.d.ts",
|
|
19
|
+
"default": "./index.js"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"types": "index.d.ts",
|
|
24
|
+
"files": [
|
|
25
|
+
"index.js",
|
|
26
|
+
"index.d.ts"
|
|
27
|
+
],
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "npm run test:unit && npm run test:fuzz",
|
|
30
|
+
"test:unit": "node --test",
|
|
31
|
+
"test:fuzz": "node --test index.fuzz.js",
|
|
32
|
+
"test:perf": "node --test index.perf.js"
|
|
33
|
+
},
|
|
34
|
+
"license": "MIT",
|
|
35
|
+
"keywords": [
|
|
36
|
+
"Lambda",
|
|
37
|
+
"Middleware",
|
|
38
|
+
"Serverless",
|
|
39
|
+
"Framework",
|
|
40
|
+
"AWS",
|
|
41
|
+
"AWS Lambda",
|
|
42
|
+
"Middy",
|
|
43
|
+
"SQS",
|
|
44
|
+
"Kinesis",
|
|
45
|
+
"DynamoDB",
|
|
46
|
+
"Kafka",
|
|
47
|
+
"S3 Batch",
|
|
48
|
+
"Firehose",
|
|
49
|
+
"Batch"
|
|
50
|
+
],
|
|
51
|
+
"author": {
|
|
52
|
+
"name": "Middy contributors",
|
|
53
|
+
"url": "https://github.com/middyjs/middy/graphs/contributors"
|
|
54
|
+
},
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/middyjs/middy.git",
|
|
58
|
+
"directory": "packages/event-batch-response"
|
|
59
|
+
},
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/middyjs/middy/issues"
|
|
62
|
+
},
|
|
63
|
+
"homepage": "https://middy.js.org",
|
|
64
|
+
"funding": {
|
|
65
|
+
"type": "github",
|
|
66
|
+
"url": "https://github.com/sponsors/willfarrell"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@middy/util": "7.5.0"
|
|
70
|
+
},
|
|
71
|
+
"devDependencies": {
|
|
72
|
+
"@middy/core": "7.5.0",
|
|
73
|
+
"@serverless/event-mocks": "^1.0.0",
|
|
74
|
+
"@types/aws-lambda": "^8.0.0",
|
|
75
|
+
"@types/node": "^22.0.0"
|
|
76
|
+
}
|
|
77
|
+
}
|