@jaypie/express 1.2.3 → 1.2.4-rc1
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/dist/cjs/adapter/LambdaRequest.d.ts +53 -0
- package/dist/cjs/adapter/LambdaResponseBuffered.d.ts +37 -0
- package/dist/cjs/adapter/LambdaResponseStreaming.d.ts +36 -0
- package/dist/cjs/adapter/__tests__/LambdaRequest.spec.d.ts +1 -0
- package/dist/cjs/adapter/__tests__/LambdaResponseBuffered.spec.d.ts +1 -0
- package/dist/cjs/adapter/__tests__/LambdaResponseStreaming.spec.d.ts +1 -0
- package/dist/cjs/adapter/__tests__/integration.spec.d.ts +1 -0
- package/dist/cjs/adapter/index.d.ts +50 -0
- package/dist/cjs/adapter/types.d.ts +107 -0
- package/dist/cjs/getCurrentInvokeUuid.adapter.d.ts +3 -3
- package/dist/cjs/index.cjs +684 -56
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.ts +4 -2
- package/dist/esm/adapter/LambdaRequest.d.ts +53 -0
- package/dist/esm/adapter/LambdaResponseBuffered.d.ts +37 -0
- package/dist/esm/adapter/LambdaResponseStreaming.d.ts +36 -0
- package/dist/esm/adapter/__tests__/LambdaRequest.spec.d.ts +1 -0
- package/dist/esm/adapter/__tests__/LambdaResponseBuffered.spec.d.ts +1 -0
- package/dist/esm/adapter/__tests__/LambdaResponseStreaming.spec.d.ts +1 -0
- package/dist/esm/adapter/__tests__/integration.spec.d.ts +1 -0
- package/dist/esm/adapter/index.d.ts +50 -0
- package/dist/esm/adapter/types.d.ts +107 -0
- package/dist/esm/getCurrentInvokeUuid.adapter.d.ts +3 -3
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +678 -56
- package/dist/esm/index.js.map +1 -1
- package/package.json +1 -2
package/dist/cjs/index.cjs
CHANGED
|
@@ -1,12 +1,620 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var node_stream = require('node:stream');
|
|
3
4
|
var errors = require('@jaypie/errors');
|
|
4
5
|
var kit = require('@jaypie/kit');
|
|
5
6
|
var expressCors = require('cors');
|
|
6
7
|
var logger$2 = require('@jaypie/logger');
|
|
7
8
|
var aws = require('@jaypie/aws');
|
|
8
9
|
var datadog = require('@jaypie/datadog');
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
//
|
|
12
|
+
//
|
|
13
|
+
// LambdaRequest Class
|
|
14
|
+
//
|
|
15
|
+
/**
|
|
16
|
+
* Mock IncomingMessage that extends Readable stream.
|
|
17
|
+
* Provides Express-compatible request interface from Lambda Function URL events.
|
|
18
|
+
*/
|
|
19
|
+
class LambdaRequest extends node_stream.Readable {
|
|
20
|
+
constructor(options) {
|
|
21
|
+
super();
|
|
22
|
+
this.httpVersion = "1.1";
|
|
23
|
+
this.httpVersionMajor = 1;
|
|
24
|
+
this.httpVersionMinor = 1;
|
|
25
|
+
this.complete = false;
|
|
26
|
+
this.baseUrl = "";
|
|
27
|
+
this.params = {};
|
|
28
|
+
this.query = {};
|
|
29
|
+
this.bodyPushed = false;
|
|
30
|
+
this.method = options.method;
|
|
31
|
+
this.url = options.url;
|
|
32
|
+
this.originalUrl = options.url;
|
|
33
|
+
this.path = options.url.split("?")[0];
|
|
34
|
+
this.headers = this.normalizeHeaders(options.headers);
|
|
35
|
+
this.bodyBuffer = options.body ?? null;
|
|
36
|
+
// Store Lambda context
|
|
37
|
+
this._lambdaContext = options.lambdaContext;
|
|
38
|
+
this._lambdaEvent = options.lambdaEvent;
|
|
39
|
+
// Create mock socket
|
|
40
|
+
this.socket = {
|
|
41
|
+
destroy: () => { },
|
|
42
|
+
encrypted: options.protocol === "https",
|
|
43
|
+
remoteAddress: options.remoteAddress,
|
|
44
|
+
};
|
|
45
|
+
this.connection = this.socket;
|
|
46
|
+
}
|
|
47
|
+
//
|
|
48
|
+
// Readable stream implementation
|
|
49
|
+
//
|
|
50
|
+
_read() {
|
|
51
|
+
if (!this.bodyPushed) {
|
|
52
|
+
if (this.bodyBuffer && this.bodyBuffer.length > 0) {
|
|
53
|
+
this.push(this.bodyBuffer);
|
|
54
|
+
}
|
|
55
|
+
this.push(null); // Signal end of stream
|
|
56
|
+
this.bodyPushed = true;
|
|
57
|
+
this.complete = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
//
|
|
61
|
+
// Express helper methods
|
|
62
|
+
//
|
|
63
|
+
get(headerName) {
|
|
64
|
+
const key = headerName.toLowerCase();
|
|
65
|
+
const value = this.headers[key];
|
|
66
|
+
return Array.isArray(value) ? value[0] : value;
|
|
67
|
+
}
|
|
68
|
+
header(headerName) {
|
|
69
|
+
return this.get(headerName);
|
|
70
|
+
}
|
|
71
|
+
//
|
|
72
|
+
// Private helpers
|
|
73
|
+
//
|
|
74
|
+
normalizeHeaders(headers) {
|
|
75
|
+
const normalized = {};
|
|
76
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
77
|
+
normalized[key.toLowerCase()] = value;
|
|
78
|
+
}
|
|
79
|
+
return normalized;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//
|
|
83
|
+
//
|
|
84
|
+
// Type Guards
|
|
85
|
+
//
|
|
86
|
+
/**
|
|
87
|
+
* Check if event is a Function URL / HTTP API v2 event.
|
|
88
|
+
*/
|
|
89
|
+
function isFunctionUrlEvent(event) {
|
|
90
|
+
return "requestContext" in event && "http" in event.requestContext;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Check if event is an API Gateway REST API v1 event.
|
|
94
|
+
*/
|
|
95
|
+
function isApiGatewayV1Event(event) {
|
|
96
|
+
return "httpMethod" in event;
|
|
97
|
+
}
|
|
98
|
+
//
|
|
99
|
+
//
|
|
100
|
+
// Factory Function
|
|
101
|
+
//
|
|
102
|
+
/**
|
|
103
|
+
* Create a LambdaRequest from a Lambda event (Function URL, HTTP API v2, or REST API v1).
|
|
104
|
+
*/
|
|
105
|
+
function createLambdaRequest(event, context) {
|
|
106
|
+
let url;
|
|
107
|
+
let method;
|
|
108
|
+
let protocol;
|
|
109
|
+
let remoteAddress;
|
|
110
|
+
const headers = { ...event.headers };
|
|
111
|
+
if (isFunctionUrlEvent(event)) {
|
|
112
|
+
// Function URL / HTTP API v2 format
|
|
113
|
+
url = event.rawQueryString
|
|
114
|
+
? `${event.rawPath}?${event.rawQueryString}`
|
|
115
|
+
: event.rawPath;
|
|
116
|
+
method = event.requestContext.http.method;
|
|
117
|
+
protocol = event.requestContext.http.protocol.split("/")[0].toLowerCase();
|
|
118
|
+
remoteAddress = event.requestContext.http.sourceIp;
|
|
119
|
+
// Normalize cookies into Cookie header if not already present
|
|
120
|
+
if (event.cookies && event.cookies.length > 0 && !headers.cookie) {
|
|
121
|
+
headers.cookie = event.cookies.join("; ");
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
else if (isApiGatewayV1Event(event)) {
|
|
125
|
+
// API Gateway REST API v1 format
|
|
126
|
+
const queryParams = event.queryStringParameters;
|
|
127
|
+
if (queryParams && Object.keys(queryParams).length > 0) {
|
|
128
|
+
const queryString = new URLSearchParams(queryParams).toString();
|
|
129
|
+
url = `${event.path}?${queryString}`;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
url = event.path;
|
|
133
|
+
}
|
|
134
|
+
method = event.httpMethod;
|
|
135
|
+
protocol = event.requestContext.protocol.split("/")[0].toLowerCase();
|
|
136
|
+
remoteAddress = event.requestContext.identity.sourceIp;
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
throw new Error("Unsupported Lambda event format. Expected Function URL, HTTP API v2, or REST API v1 event.");
|
|
140
|
+
}
|
|
141
|
+
// Decode body if present
|
|
142
|
+
let body = null;
|
|
143
|
+
if (event.body) {
|
|
144
|
+
body = event.isBase64Encoded
|
|
145
|
+
? Buffer.from(event.body, "base64")
|
|
146
|
+
: Buffer.from(event.body, "utf8");
|
|
147
|
+
}
|
|
148
|
+
return new LambdaRequest({
|
|
149
|
+
body,
|
|
150
|
+
headers,
|
|
151
|
+
lambdaContext: context,
|
|
152
|
+
lambdaEvent: event,
|
|
153
|
+
method,
|
|
154
|
+
protocol,
|
|
155
|
+
remoteAddress,
|
|
156
|
+
url,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
//
|
|
161
|
+
//
|
|
162
|
+
// Constants
|
|
163
|
+
//
|
|
164
|
+
const BINARY_CONTENT_TYPE_PATTERNS = [
|
|
165
|
+
/^application\/octet-stream$/,
|
|
166
|
+
/^application\/pdf$/,
|
|
167
|
+
/^application\/zip$/,
|
|
168
|
+
/^audio\//,
|
|
169
|
+
/^font\//,
|
|
170
|
+
/^image\//,
|
|
171
|
+
/^video\//,
|
|
172
|
+
];
|
|
173
|
+
//
|
|
174
|
+
//
|
|
175
|
+
// LambdaResponseBuffered Class
|
|
176
|
+
//
|
|
177
|
+
/**
|
|
178
|
+
* Mock ServerResponse that buffers the response.
|
|
179
|
+
* Collects status, headers, and body chunks, then returns a Lambda response.
|
|
180
|
+
*/
|
|
181
|
+
class LambdaResponseBuffered extends node_stream.Writable {
|
|
182
|
+
constructor() {
|
|
183
|
+
super();
|
|
184
|
+
this.statusCode = 200;
|
|
185
|
+
this.statusMessage = "OK";
|
|
186
|
+
// Mock socket to satisfy Express/finalhandler checks
|
|
187
|
+
this.socket = {
|
|
188
|
+
remoteAddress: "127.0.0.1",
|
|
189
|
+
};
|
|
190
|
+
this._chunks = [];
|
|
191
|
+
this._headers = new Map();
|
|
192
|
+
this._headersSent = false;
|
|
193
|
+
this._resolve = null;
|
|
194
|
+
}
|
|
195
|
+
//
|
|
196
|
+
// Promise-based API for getting final result
|
|
197
|
+
//
|
|
198
|
+
getResult() {
|
|
199
|
+
return new Promise((resolve) => {
|
|
200
|
+
if (this.writableEnded) {
|
|
201
|
+
resolve(this.buildResult());
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
this._resolve = resolve;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
//
|
|
209
|
+
// Header management
|
|
210
|
+
//
|
|
211
|
+
setHeader(name, value) {
|
|
212
|
+
if (this._headersSent) {
|
|
213
|
+
// In production, log warning but don't throw to match Express behavior
|
|
214
|
+
return this;
|
|
215
|
+
}
|
|
216
|
+
this._headers.set(name.toLowerCase(), String(value));
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
getHeader(name) {
|
|
220
|
+
return this._headers.get(name.toLowerCase());
|
|
221
|
+
}
|
|
222
|
+
removeHeader(name) {
|
|
223
|
+
this._headers.delete(name.toLowerCase());
|
|
224
|
+
}
|
|
225
|
+
getHeaders() {
|
|
226
|
+
const headers = {};
|
|
227
|
+
for (const [key, value] of this._headers) {
|
|
228
|
+
headers[key] = value;
|
|
229
|
+
}
|
|
230
|
+
return headers;
|
|
231
|
+
}
|
|
232
|
+
hasHeader(name) {
|
|
233
|
+
return this._headers.has(name.toLowerCase());
|
|
234
|
+
}
|
|
235
|
+
getHeaderNames() {
|
|
236
|
+
return Array.from(this._headers.keys());
|
|
237
|
+
}
|
|
238
|
+
writeHead(statusCode, statusMessageOrHeaders, headers) {
|
|
239
|
+
this.statusCode = statusCode;
|
|
240
|
+
let headersToSet;
|
|
241
|
+
if (typeof statusMessageOrHeaders === "string") {
|
|
242
|
+
this.statusMessage = statusMessageOrHeaders;
|
|
243
|
+
headersToSet = headers;
|
|
244
|
+
}
|
|
245
|
+
else if (statusMessageOrHeaders &&
|
|
246
|
+
typeof statusMessageOrHeaders === "object") {
|
|
247
|
+
headersToSet = statusMessageOrHeaders;
|
|
248
|
+
}
|
|
249
|
+
if (headersToSet) {
|
|
250
|
+
for (const [key, value] of Object.entries(headersToSet)) {
|
|
251
|
+
if (value !== undefined) {
|
|
252
|
+
this.setHeader(key, value);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
this._headersSent = true;
|
|
257
|
+
return this;
|
|
258
|
+
}
|
|
259
|
+
get headersSent() {
|
|
260
|
+
return this._headersSent;
|
|
261
|
+
}
|
|
262
|
+
//
|
|
263
|
+
// Express compatibility methods
|
|
264
|
+
//
|
|
265
|
+
status(code) {
|
|
266
|
+
this.statusCode = code;
|
|
267
|
+
return this;
|
|
268
|
+
}
|
|
269
|
+
json(data) {
|
|
270
|
+
this.setHeader("content-type", "application/json");
|
|
271
|
+
this.end(JSON.stringify(data));
|
|
272
|
+
return this;
|
|
273
|
+
}
|
|
274
|
+
send(body) {
|
|
275
|
+
if (typeof body === "object" && !Buffer.isBuffer(body)) {
|
|
276
|
+
return this.json(body);
|
|
277
|
+
}
|
|
278
|
+
this.end(body);
|
|
279
|
+
return this;
|
|
280
|
+
}
|
|
281
|
+
//
|
|
282
|
+
// Writable stream implementation
|
|
283
|
+
//
|
|
284
|
+
_write(chunk, encoding, // eslint-disable-line no-undef
|
|
285
|
+
callback) {
|
|
286
|
+
const buffer = Buffer.isBuffer(chunk)
|
|
287
|
+
? chunk
|
|
288
|
+
: Buffer.from(chunk, encoding);
|
|
289
|
+
this._chunks.push(buffer);
|
|
290
|
+
this._headersSent = true;
|
|
291
|
+
callback();
|
|
292
|
+
}
|
|
293
|
+
_final(callback) {
|
|
294
|
+
if (this._resolve) {
|
|
295
|
+
this._resolve(this.buildResult());
|
|
296
|
+
}
|
|
297
|
+
callback();
|
|
298
|
+
}
|
|
299
|
+
//
|
|
300
|
+
// Private helpers
|
|
301
|
+
//
|
|
302
|
+
buildResult() {
|
|
303
|
+
const body = Buffer.concat(this._chunks);
|
|
304
|
+
const contentType = this.getHeader("content-type") || "";
|
|
305
|
+
// Determine if response should be base64 encoded
|
|
306
|
+
const isBase64Encoded = this.isBinaryContentType(contentType);
|
|
307
|
+
// Build headers object
|
|
308
|
+
const headers = {};
|
|
309
|
+
const cookies = [];
|
|
310
|
+
for (const [key, value] of this._headers) {
|
|
311
|
+
if (key === "set-cookie") {
|
|
312
|
+
// Collect Set-Cookie headers for v2 response format
|
|
313
|
+
if (Array.isArray(value)) {
|
|
314
|
+
cookies.push(...value);
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
cookies.push(value);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
headers[key] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
const result = {
|
|
325
|
+
body: isBase64Encoded ? body.toString("base64") : body.toString("utf8"),
|
|
326
|
+
headers,
|
|
327
|
+
isBase64Encoded,
|
|
328
|
+
statusCode: this.statusCode,
|
|
329
|
+
};
|
|
330
|
+
// Only include cookies if present (v2 format)
|
|
331
|
+
if (cookies.length > 0) {
|
|
332
|
+
result.cookies = cookies;
|
|
333
|
+
}
|
|
334
|
+
return result;
|
|
335
|
+
}
|
|
336
|
+
isBinaryContentType(contentType) {
|
|
337
|
+
return BINARY_CONTENT_TYPE_PATTERNS.some((pattern) => pattern.test(contentType));
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
//
|
|
342
|
+
//
|
|
343
|
+
// LambdaResponseStreaming Class
|
|
344
|
+
//
|
|
345
|
+
/**
|
|
346
|
+
* Mock ServerResponse that streams directly to Lambda responseStream.
|
|
347
|
+
* Uses awslambda.HttpResponseStream.from() to set status and headers.
|
|
348
|
+
*/
|
|
349
|
+
class LambdaResponseStreaming extends node_stream.Writable {
|
|
350
|
+
constructor(responseStream) {
|
|
351
|
+
super();
|
|
352
|
+
this.statusCode = 200;
|
|
353
|
+
this.statusMessage = "OK";
|
|
354
|
+
// Mock socket to satisfy Express/finalhandler checks
|
|
355
|
+
this.socket = {
|
|
356
|
+
remoteAddress: "127.0.0.1",
|
|
357
|
+
};
|
|
358
|
+
this._headers = new Map();
|
|
359
|
+
this._headersSent = false;
|
|
360
|
+
this._pendingWrites = [];
|
|
361
|
+
this._wrappedStream = null;
|
|
362
|
+
this._responseStream = responseStream;
|
|
363
|
+
}
|
|
364
|
+
//
|
|
365
|
+
// Header management
|
|
366
|
+
//
|
|
367
|
+
setHeader(name, value) {
|
|
368
|
+
if (this._headersSent) {
|
|
369
|
+
// In streaming mode, log warning but don't throw
|
|
370
|
+
// Headers cannot be changed after body starts
|
|
371
|
+
return this;
|
|
372
|
+
}
|
|
373
|
+
this._headers.set(name.toLowerCase(), String(value));
|
|
374
|
+
return this;
|
|
375
|
+
}
|
|
376
|
+
getHeader(name) {
|
|
377
|
+
return this._headers.get(name.toLowerCase());
|
|
378
|
+
}
|
|
379
|
+
removeHeader(name) {
|
|
380
|
+
if (!this._headersSent) {
|
|
381
|
+
this._headers.delete(name.toLowerCase());
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
getHeaders() {
|
|
385
|
+
const headers = {};
|
|
386
|
+
for (const [key, value] of this._headers) {
|
|
387
|
+
headers[key] = value;
|
|
388
|
+
}
|
|
389
|
+
return headers;
|
|
390
|
+
}
|
|
391
|
+
hasHeader(name) {
|
|
392
|
+
return this._headers.has(name.toLowerCase());
|
|
393
|
+
}
|
|
394
|
+
getHeaderNames() {
|
|
395
|
+
return Array.from(this._headers.keys());
|
|
396
|
+
}
|
|
397
|
+
writeHead(statusCode, statusMessageOrHeaders, headers) {
|
|
398
|
+
if (this._headersSent) {
|
|
399
|
+
return this;
|
|
400
|
+
}
|
|
401
|
+
this.statusCode = statusCode;
|
|
402
|
+
let headersToSet;
|
|
403
|
+
if (typeof statusMessageOrHeaders === "string") {
|
|
404
|
+
this.statusMessage = statusMessageOrHeaders;
|
|
405
|
+
headersToSet = headers;
|
|
406
|
+
}
|
|
407
|
+
else if (statusMessageOrHeaders &&
|
|
408
|
+
typeof statusMessageOrHeaders === "object") {
|
|
409
|
+
headersToSet = statusMessageOrHeaders;
|
|
410
|
+
}
|
|
411
|
+
if (headersToSet) {
|
|
412
|
+
for (const [key, value] of Object.entries(headersToSet)) {
|
|
413
|
+
if (value !== undefined) {
|
|
414
|
+
this.setHeader(key, value);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
this.flushHeaders();
|
|
419
|
+
return this;
|
|
420
|
+
}
|
|
421
|
+
get headersSent() {
|
|
422
|
+
return this._headersSent;
|
|
423
|
+
}
|
|
424
|
+
flushHeaders() {
|
|
425
|
+
if (this._headersSent)
|
|
426
|
+
return;
|
|
427
|
+
const headers = {};
|
|
428
|
+
for (const [key, value] of this._headers) {
|
|
429
|
+
headers[key] = Array.isArray(value) ? value.join(", ") : String(value);
|
|
430
|
+
}
|
|
431
|
+
const metadata = {
|
|
432
|
+
headers,
|
|
433
|
+
statusCode: this.statusCode,
|
|
434
|
+
};
|
|
435
|
+
// Wrap the stream with metadata using awslambda global
|
|
436
|
+
this._wrappedStream = awslambda.HttpResponseStream.from(this._responseStream, metadata);
|
|
437
|
+
this._headersSent = true;
|
|
438
|
+
// Flush pending writes
|
|
439
|
+
for (const { callback, chunk } of this._pendingWrites) {
|
|
440
|
+
this._wrappedStream.write(chunk);
|
|
441
|
+
callback();
|
|
442
|
+
}
|
|
443
|
+
this._pendingWrites = [];
|
|
444
|
+
}
|
|
445
|
+
//
|
|
446
|
+
// Express compatibility methods
|
|
447
|
+
//
|
|
448
|
+
status(code) {
|
|
449
|
+
this.statusCode = code;
|
|
450
|
+
return this;
|
|
451
|
+
}
|
|
452
|
+
json(data) {
|
|
453
|
+
this.setHeader("content-type", "application/json");
|
|
454
|
+
this.end(JSON.stringify(data));
|
|
455
|
+
return this;
|
|
456
|
+
}
|
|
457
|
+
send(body) {
|
|
458
|
+
if (typeof body === "object" && !Buffer.isBuffer(body)) {
|
|
459
|
+
return this.json(body);
|
|
460
|
+
}
|
|
461
|
+
this.end(body);
|
|
462
|
+
return this;
|
|
463
|
+
}
|
|
464
|
+
//
|
|
465
|
+
// Writable stream implementation
|
|
466
|
+
//
|
|
467
|
+
_write(chunk, encoding, // eslint-disable-line no-undef
|
|
468
|
+
callback) {
|
|
469
|
+
const buffer = Buffer.isBuffer(chunk)
|
|
470
|
+
? chunk
|
|
471
|
+
: Buffer.from(chunk, encoding);
|
|
472
|
+
if (!this._headersSent) {
|
|
473
|
+
// Buffer writes until headers are sent
|
|
474
|
+
this._pendingWrites.push({ callback: () => callback(), chunk: buffer });
|
|
475
|
+
// Auto-flush headers on first write
|
|
476
|
+
this.flushHeaders();
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
this._wrappedStream.write(buffer);
|
|
480
|
+
callback();
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
_final(callback) {
|
|
484
|
+
if (!this._headersSent) {
|
|
485
|
+
this.flushHeaders();
|
|
486
|
+
}
|
|
487
|
+
this._wrappedStream?.end();
|
|
488
|
+
callback();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
//
|
|
493
|
+
//
|
|
494
|
+
// Current Invoke Context
|
|
495
|
+
//
|
|
496
|
+
let currentInvoke = null;
|
|
497
|
+
/**
|
|
498
|
+
* Get the current Lambda invoke context.
|
|
499
|
+
* Used by getCurrentInvokeUuid adapter to get the request ID.
|
|
500
|
+
*/
|
|
501
|
+
function getCurrentInvoke() {
|
|
502
|
+
return currentInvoke;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Set the current Lambda invoke context.
|
|
506
|
+
* Called at the start of each Lambda invocation.
|
|
507
|
+
*/
|
|
508
|
+
function setCurrentInvoke(event, context) {
|
|
509
|
+
currentInvoke = { context, event };
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Clear the current Lambda invoke context.
|
|
513
|
+
* Called at the end of each Lambda invocation.
|
|
514
|
+
*/
|
|
515
|
+
function clearCurrentInvoke() {
|
|
516
|
+
currentInvoke = null;
|
|
517
|
+
}
|
|
518
|
+
//
|
|
519
|
+
//
|
|
520
|
+
// Express App Runner
|
|
521
|
+
//
|
|
522
|
+
/**
|
|
523
|
+
* Run Express app with mock request/response.
|
|
524
|
+
* Returns a promise that resolves when the response is complete.
|
|
525
|
+
*/
|
|
526
|
+
function runExpressApp(app, req, res) {
|
|
527
|
+
return new Promise((resolve, reject) => {
|
|
528
|
+
// Listen for response completion
|
|
529
|
+
res.on("finish", resolve);
|
|
530
|
+
res.on("error", reject);
|
|
531
|
+
// Run the Express app
|
|
532
|
+
// Cast to Express types since our mocks implement the required interface
|
|
533
|
+
app(req, res);
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
//
|
|
537
|
+
//
|
|
538
|
+
// Buffered Handler Factory
|
|
539
|
+
//
|
|
540
|
+
/**
|
|
541
|
+
* Create a Lambda handler that buffers the Express response.
|
|
542
|
+
* Returns the complete response as a Lambda response object.
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* ```typescript
|
|
546
|
+
* import express from "express";
|
|
547
|
+
* import { createLambdaHandler } from "@jaypie/express";
|
|
548
|
+
*
|
|
549
|
+
* const app = express();
|
|
550
|
+
* app.get("/", (req, res) => res.json({ message: "Hello" }));
|
|
551
|
+
*
|
|
552
|
+
* export const handler = createLambdaHandler(app);
|
|
553
|
+
* ```
|
|
554
|
+
*/
|
|
555
|
+
function createLambdaHandler(app, _options) {
|
|
556
|
+
return async (event, context) => {
|
|
557
|
+
try {
|
|
558
|
+
// Set current invoke for getCurrentInvokeUuid
|
|
559
|
+
setCurrentInvoke(event, context);
|
|
560
|
+
// Create mock request from Lambda event
|
|
561
|
+
const req = createLambdaRequest(event, context);
|
|
562
|
+
// Create buffered response collector
|
|
563
|
+
const res = new LambdaResponseBuffered();
|
|
564
|
+
// Run Express app
|
|
565
|
+
await runExpressApp(app, req, res);
|
|
566
|
+
// Return Lambda response
|
|
567
|
+
return res.getResult();
|
|
568
|
+
}
|
|
569
|
+
finally {
|
|
570
|
+
// Clear current invoke context
|
|
571
|
+
clearCurrentInvoke();
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
//
|
|
576
|
+
//
|
|
577
|
+
// Streaming Handler Factory
|
|
578
|
+
//
|
|
579
|
+
/**
|
|
580
|
+
* Create a Lambda handler that streams the Express response.
|
|
581
|
+
* Uses awslambda.streamifyResponse() for Lambda response streaming.
|
|
582
|
+
*
|
|
583
|
+
* @example
|
|
584
|
+
* ```typescript
|
|
585
|
+
* import express from "express";
|
|
586
|
+
* import { createLambdaStreamHandler } from "@jaypie/express";
|
|
587
|
+
*
|
|
588
|
+
* const app = express();
|
|
589
|
+
* app.get("/stream", (req, res) => {
|
|
590
|
+
* res.setHeader("Content-Type", "text/event-stream");
|
|
591
|
+
* res.write("data: Hello\n\n");
|
|
592
|
+
* res.end();
|
|
593
|
+
* });
|
|
594
|
+
*
|
|
595
|
+
* export const handler = createLambdaStreamHandler(app);
|
|
596
|
+
* ```
|
|
597
|
+
*/
|
|
598
|
+
function createLambdaStreamHandler(app, _options) {
|
|
599
|
+
// Wrap with awslambda.streamifyResponse for Lambda streaming
|
|
600
|
+
// @ts-expect-error awslambda is a Lambda runtime global
|
|
601
|
+
return awslambda.streamifyResponse(async (event, responseStream, context) => {
|
|
602
|
+
try {
|
|
603
|
+
// Set current invoke for getCurrentInvokeUuid
|
|
604
|
+
setCurrentInvoke(event, context);
|
|
605
|
+
// Create mock request from Lambda event
|
|
606
|
+
const req = createLambdaRequest(event, context);
|
|
607
|
+
// Create streaming response that pipes to Lambda responseStream
|
|
608
|
+
const res = new LambdaResponseStreaming(responseStream);
|
|
609
|
+
// Run Express app
|
|
610
|
+
await runExpressApp(app, req, res);
|
|
611
|
+
}
|
|
612
|
+
finally {
|
|
613
|
+
// Clear current invoke context
|
|
614
|
+
clearCurrentInvoke();
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
10
618
|
|
|
11
619
|
//
|
|
12
620
|
//
|
|
@@ -258,40 +866,54 @@ function isWebAdapterMode(req) {
|
|
|
258
866
|
return false;
|
|
259
867
|
}
|
|
260
868
|
/**
|
|
261
|
-
*
|
|
869
|
+
* Get UUID from Jaypie Lambda adapter context.
|
|
870
|
+
* This is set by createLambdaHandler/createLambdaStreamHandler.
|
|
262
871
|
*/
|
|
263
|
-
function
|
|
264
|
-
const currentInvoke =
|
|
265
|
-
if (currentInvoke
|
|
266
|
-
currentInvoke.context &&
|
|
267
|
-
currentInvoke.context.awsRequestId) {
|
|
872
|
+
function getJaypieAdapterUuid() {
|
|
873
|
+
const currentInvoke = getCurrentInvoke();
|
|
874
|
+
if (currentInvoke?.context?.awsRequestId) {
|
|
268
875
|
return currentInvoke.context.awsRequestId;
|
|
269
876
|
}
|
|
270
877
|
return undefined;
|
|
271
878
|
}
|
|
879
|
+
/**
|
|
880
|
+
* Get UUID from request object if it has Lambda context attached.
|
|
881
|
+
* The Jaypie adapter attaches _lambdaContext to the request.
|
|
882
|
+
*/
|
|
883
|
+
function getRequestContextUuid(req) {
|
|
884
|
+
if (req && req._lambdaContext?.awsRequestId) {
|
|
885
|
+
return req._lambdaContext.awsRequestId;
|
|
886
|
+
}
|
|
887
|
+
return undefined;
|
|
888
|
+
}
|
|
272
889
|
//
|
|
273
890
|
//
|
|
274
891
|
// Main
|
|
275
892
|
//
|
|
276
893
|
/**
|
|
277
894
|
* Get the current invoke UUID from Lambda context.
|
|
278
|
-
* Works
|
|
895
|
+
* Works with Jaypie Lambda adapter and Lambda Web Adapter mode.
|
|
279
896
|
*
|
|
280
|
-
* @param req - Optional Express request object.
|
|
281
|
-
*
|
|
897
|
+
* @param req - Optional Express request object. Used to extract context
|
|
898
|
+
* from Web Adapter headers or Jaypie adapter's _lambdaContext.
|
|
282
899
|
* @returns The AWS request ID or undefined if not in Lambda context
|
|
283
900
|
*/
|
|
284
901
|
function getCurrentInvokeUuid(req) {
|
|
285
|
-
//
|
|
902
|
+
// Priority 1: Web Adapter mode (header-based)
|
|
286
903
|
if (isWebAdapterMode(req)) {
|
|
287
904
|
return getWebAdapterUuid(req);
|
|
288
905
|
}
|
|
289
|
-
//
|
|
290
|
-
const
|
|
291
|
-
if (
|
|
292
|
-
return
|
|
906
|
+
// Priority 2: Request has Lambda context attached (Jaypie adapter)
|
|
907
|
+
const requestContextUuid = getRequestContextUuid(req);
|
|
908
|
+
if (requestContextUuid) {
|
|
909
|
+
return requestContextUuid;
|
|
910
|
+
}
|
|
911
|
+
// Priority 3: Global context from Jaypie adapter
|
|
912
|
+
const jaypieAdapterUuid = getJaypieAdapterUuid();
|
|
913
|
+
if (jaypieAdapterUuid) {
|
|
914
|
+
return jaypieAdapterUuid;
|
|
293
915
|
}
|
|
294
|
-
//
|
|
916
|
+
// Fallback: Web Adapter env var
|
|
295
917
|
return getWebAdapterUuid();
|
|
296
918
|
}
|
|
297
919
|
|
|
@@ -709,6 +1331,46 @@ function expressHandler(handlerOrOptions, optionsOrHandler) {
|
|
|
709
1331
|
};
|
|
710
1332
|
}
|
|
711
1333
|
|
|
1334
|
+
//
|
|
1335
|
+
//
|
|
1336
|
+
// Main
|
|
1337
|
+
//
|
|
1338
|
+
const httpHandler = (statusCode = kit.HTTP.CODE.OK, context = {}) => {
|
|
1339
|
+
// Give a default name if there isn't one
|
|
1340
|
+
if (!context.name) {
|
|
1341
|
+
context.name = "_http";
|
|
1342
|
+
}
|
|
1343
|
+
// Return a function that will be used as an express route
|
|
1344
|
+
return expressHandler(async (req, res) => {
|
|
1345
|
+
// Map the most throwable status codes to errors and throw them!
|
|
1346
|
+
const error = {
|
|
1347
|
+
[kit.HTTP.CODE.BAD_REQUEST]: errors.BadRequestError,
|
|
1348
|
+
[kit.HTTP.CODE.UNAUTHORIZED]: errors.UnauthorizedError,
|
|
1349
|
+
[kit.HTTP.CODE.FORBIDDEN]: errors.ForbiddenError,
|
|
1350
|
+
[kit.HTTP.CODE.NOT_FOUND]: errors.NotFoundError,
|
|
1351
|
+
[kit.HTTP.CODE.METHOD_NOT_ALLOWED]: errors.MethodNotAllowedError,
|
|
1352
|
+
[kit.HTTP.CODE.GONE]: errors.GoneError,
|
|
1353
|
+
[kit.HTTP.CODE.TEAPOT]: errors.TeapotError,
|
|
1354
|
+
[kit.HTTP.CODE.INTERNAL_ERROR]: errors.InternalError,
|
|
1355
|
+
[kit.HTTP.CODE.BAD_GATEWAY]: errors.BadGatewayError,
|
|
1356
|
+
[kit.HTTP.CODE.UNAVAILABLE]: errors.UnavailableError,
|
|
1357
|
+
[kit.HTTP.CODE.GATEWAY_TIMEOUT]: errors.GatewayTimeoutError,
|
|
1358
|
+
};
|
|
1359
|
+
// If this maps to an error, throw it
|
|
1360
|
+
if (error[statusCode]) {
|
|
1361
|
+
logger$2.log.trace(`@knowdev/express: gracefully throwing ${statusCode} up to projectHandler`);
|
|
1362
|
+
throw new error[statusCode]();
|
|
1363
|
+
}
|
|
1364
|
+
// If this is an error and we didn't get thrown, log a warning
|
|
1365
|
+
if (statusCode >= 400) {
|
|
1366
|
+
logger$2.log.warn(`@knowdev/express: status code ${statusCode} not mapped as throwable`);
|
|
1367
|
+
}
|
|
1368
|
+
// Send the response
|
|
1369
|
+
res.status(statusCode);
|
|
1370
|
+
return statusCode === kit.HTTP.CODE.NO_CONTENT ? null : {};
|
|
1371
|
+
}, context);
|
|
1372
|
+
};
|
|
1373
|
+
|
|
712
1374
|
// Cast logger to extended interface for runtime features not in type definitions
|
|
713
1375
|
const logger = logger$2.log;
|
|
714
1376
|
//
|
|
@@ -939,46 +1601,6 @@ function expressStreamHandler(handlerOrOptions, optionsOrHandler) {
|
|
|
939
1601
|
};
|
|
940
1602
|
}
|
|
941
1603
|
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
// Main
|
|
945
|
-
//
|
|
946
|
-
const httpHandler = (statusCode = kit.HTTP.CODE.OK, context = {}) => {
|
|
947
|
-
// Give a default name if there isn't one
|
|
948
|
-
if (!context.name) {
|
|
949
|
-
context.name = "_http";
|
|
950
|
-
}
|
|
951
|
-
// Return a function that will be used as an express route
|
|
952
|
-
return expressHandler(async (req, res) => {
|
|
953
|
-
// Map the most throwable status codes to errors and throw them!
|
|
954
|
-
const error = {
|
|
955
|
-
[kit.HTTP.CODE.BAD_REQUEST]: errors.BadRequestError,
|
|
956
|
-
[kit.HTTP.CODE.UNAUTHORIZED]: errors.UnauthorizedError,
|
|
957
|
-
[kit.HTTP.CODE.FORBIDDEN]: errors.ForbiddenError,
|
|
958
|
-
[kit.HTTP.CODE.NOT_FOUND]: errors.NotFoundError,
|
|
959
|
-
[kit.HTTP.CODE.METHOD_NOT_ALLOWED]: errors.MethodNotAllowedError,
|
|
960
|
-
[kit.HTTP.CODE.GONE]: errors.GoneError,
|
|
961
|
-
[kit.HTTP.CODE.TEAPOT]: errors.TeapotError,
|
|
962
|
-
[kit.HTTP.CODE.INTERNAL_ERROR]: errors.InternalError,
|
|
963
|
-
[kit.HTTP.CODE.BAD_GATEWAY]: errors.BadGatewayError,
|
|
964
|
-
[kit.HTTP.CODE.UNAVAILABLE]: errors.UnavailableError,
|
|
965
|
-
[kit.HTTP.CODE.GATEWAY_TIMEOUT]: errors.GatewayTimeoutError,
|
|
966
|
-
};
|
|
967
|
-
// If this maps to an error, throw it
|
|
968
|
-
if (error[statusCode]) {
|
|
969
|
-
logger$2.log.trace(`@knowdev/express: gracefully throwing ${statusCode} up to projectHandler`);
|
|
970
|
-
throw new error[statusCode]();
|
|
971
|
-
}
|
|
972
|
-
// If this is an error and we didn't get thrown, log a warning
|
|
973
|
-
if (statusCode >= 400) {
|
|
974
|
-
logger$2.log.warn(`@knowdev/express: status code ${statusCode} not mapped as throwable`);
|
|
975
|
-
}
|
|
976
|
-
// Send the response
|
|
977
|
-
res.status(statusCode);
|
|
978
|
-
return statusCode === kit.HTTP.CODE.NO_CONTENT ? null : {};
|
|
979
|
-
}, context);
|
|
980
|
-
};
|
|
981
|
-
|
|
982
1604
|
//
|
|
983
1605
|
//
|
|
984
1606
|
// Main
|
|
@@ -1033,14 +1655,20 @@ const notFoundRoute = routes.notFoundRoute;
|
|
|
1033
1655
|
const notImplementedRoute = routes.notImplementedRoute;
|
|
1034
1656
|
|
|
1035
1657
|
exports.EXPRESS = EXPRESS;
|
|
1658
|
+
exports.LambdaRequest = LambdaRequest;
|
|
1659
|
+
exports.LambdaResponseBuffered = LambdaResponseBuffered;
|
|
1660
|
+
exports.LambdaResponseStreaming = LambdaResponseStreaming;
|
|
1036
1661
|
exports.badRequestRoute = badRequestRoute;
|
|
1037
1662
|
exports.cors = cors;
|
|
1663
|
+
exports.createLambdaHandler = createLambdaHandler;
|
|
1664
|
+
exports.createLambdaStreamHandler = createLambdaStreamHandler;
|
|
1038
1665
|
exports.createServer = createServer;
|
|
1039
1666
|
exports.echoRoute = echoRoute;
|
|
1040
1667
|
exports.expressHandler = expressHandler;
|
|
1041
1668
|
exports.expressHttpCodeHandler = httpHandler;
|
|
1042
1669
|
exports.expressStreamHandler = expressStreamHandler;
|
|
1043
1670
|
exports.forbiddenRoute = forbiddenRoute;
|
|
1671
|
+
exports.getCurrentInvoke = getCurrentInvoke;
|
|
1044
1672
|
exports.getCurrentInvokeUuid = getCurrentInvokeUuid;
|
|
1045
1673
|
exports.goneRoute = goneRoute;
|
|
1046
1674
|
exports.methodNotAllowedRoute = methodNotAllowedRoute;
|