lambder 2.0.18 → 3.0.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 +162 -41
- package/dist/Lambder.d.ts +154 -46
- package/dist/Lambder.js +312 -166
- package/dist/LambderCaller.js +6 -3
- package/dist/LambderContext.d.ts +20 -9
- package/dist/LambderContext.js +57 -17
- package/dist/LambderCors.d.ts +12 -0
- package/dist/LambderCors.js +30 -0
- package/dist/LambderHtml.d.ts +33 -0
- package/dist/LambderHtml.js +62 -0
- package/dist/LambderMSW.d.ts +16 -1
- package/dist/LambderMSW.js +5 -9
- package/dist/LambderPublicFiles.d.ts +47 -0
- package/dist/LambderPublicFiles.js +108 -0
- package/dist/LambderResolver.d.ts +30 -31
- package/dist/LambderResolver.js +29 -43
- package/dist/LambderResponse.d.ts +71 -0
- package/dist/LambderResponse.js +196 -0
- package/dist/LambderResponseBuilder.d.ts +58 -33
- package/dist/LambderResponseBuilder.js +114 -167
- package/dist/LambderRouting.d.ts +23 -0
- package/dist/LambderRouting.js +67 -0
- package/dist/LambderSessionController.d.ts +13 -1
- package/dist/LambderSessionController.js +33 -10
- package/dist/LambderSessionManager.d.ts +3 -1
- package/dist/LambderSessionManager.js +15 -6
- package/dist/LambderTemplatingEngine.d.ts +87 -0
- package/dist/LambderTemplatingEngine.js +156 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.js +10 -1
- package/dist/node-polyfills.d.ts +4 -2
- package/dist/node-polyfills.js +28 -0
- package/package.json +7 -5
- package/.eslintrc.cjs +0 -26
- package/.vscode/settings.json +0 -26
- package/deploy +0 -22
- package/dist/LambderUtils.d.ts +0 -10
- package/dist/LambderUtils.js +0 -70
- package/docs/DYNAMODB_SETUP.md +0 -96
- package/docs/LAMBDER_MSW.md +0 -409
- package/docs/TYPE_SAFE_QUICK_START.md +0 -77
- package/examples/msw-testing-example.ts +0 -280
- package/examples/secure-session-example.ts +0 -207
- package/examples/zod-chained-api-example.ts +0 -63
- package/src/Lambder.ts +0 -430
- package/src/LambderApiContract.ts +0 -20
- package/src/LambderCaller.ts +0 -238
- package/src/LambderContext.ts +0 -78
- package/src/LambderMSW.ts +0 -180
- package/src/LambderResolver.ts +0 -101
- package/src/LambderResponseBuilder.ts +0 -332
- package/src/LambderSessionController.ts +0 -114
- package/src/LambderSessionManager.ts +0 -217
- package/src/LambderUtils.ts +0 -75
- package/src/index.ts +0 -17
- package/src/node-polyfills.ts +0 -27
- package/tests/error-handling.test.ts +0 -585
- package/tests/file-serving.test.ts +0 -194
- package/tests/fixtures/public/index.html +0 -1
- package/tests/fixtures/public/main.css +0 -1
- package/tests/hooks.test.ts +0 -561
- package/tests/output-type-runtime.test.ts +0 -381
- package/tests/redirect.test.ts +0 -88
- package/tests/routes.test.ts +0 -543
- package/tests/session.test.ts +0 -1083
- package/tests/use-plugin.test.ts +0 -460
- package/tsconfig.json +0 -24
package/dist/LambderResolver.js
CHANGED
|
@@ -1,52 +1,38 @@
|
|
|
1
1
|
import LambderResponseBuilder from "./LambderResponseBuilder.js";
|
|
2
|
+
/**
|
|
3
|
+
* Response builder passed to route/api handlers and hooks.
|
|
4
|
+
*
|
|
5
|
+
* `res.die.*` builds the response and THROWS it, immediately halting the
|
|
6
|
+
* request at any call depth (handlers, hooks, nested service functions).
|
|
7
|
+
* Lambder's render pipeline catches thrown LambderResponse instances and uses
|
|
8
|
+
* them as the response. Plain `throw res.html(...)` works the same way.
|
|
9
|
+
*/
|
|
2
10
|
export default class LambderResolver extends LambderResponseBuilder {
|
|
3
|
-
resolve;
|
|
4
|
-
reject;
|
|
5
11
|
die;
|
|
6
|
-
constructor(
|
|
7
|
-
super(
|
|
8
|
-
this.resolve = resolve;
|
|
9
|
-
this.reject = reject;
|
|
12
|
+
constructor(...args) {
|
|
13
|
+
super(...args);
|
|
10
14
|
this.die = {
|
|
11
|
-
raw:
|
|
12
|
-
json:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
raw: (...a) => { throw this.raw(...a); },
|
|
16
|
+
json: (...a) => { throw this.json(...a); },
|
|
17
|
+
text: (...a) => { throw this.text(...a); },
|
|
18
|
+
xml: (...a) => { throw this.xml(...a); },
|
|
19
|
+
html: (...a) => { throw this.html(...a); },
|
|
20
|
+
status: (...a) => { throw this.status(...a); },
|
|
21
|
+
status404: (...a) => { throw this.status404(...a); },
|
|
22
|
+
redirect: (...a) => { throw this.redirect(...a); },
|
|
23
|
+
versionExpired: (...a) => { throw this.versionExpired(...a); },
|
|
24
|
+
fileBase64: (...a) => { throw this.fileBase64(...a); },
|
|
25
|
+
api: (...a) => { throw this.api(...a); },
|
|
26
|
+
apiBinary: (...a) => { throw this.apiBinary(...a); },
|
|
27
|
+
file: async (...a) => { throw await this.file(...a); },
|
|
28
|
+
templateFile: async (...a) => { throw await this.templateFile(...a); },
|
|
23
29
|
};
|
|
24
30
|
}
|
|
25
|
-
// Override api method with proper typing
|
|
26
|
-
api(payload, config,
|
|
27
|
-
return super.api(payload, config,
|
|
31
|
+
// Override api method with proper output typing
|
|
32
|
+
api(payload, config, options) {
|
|
33
|
+
return super.api(payload, config, options);
|
|
28
34
|
}
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
const result = method.apply(this, args);
|
|
32
|
-
this.resolve(result);
|
|
33
|
-
return result;
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
autoResolvePromise(method) {
|
|
37
|
-
return (...args) => {
|
|
38
|
-
return new Promise((resolve, reject) => {
|
|
39
|
-
method.apply(this, args)
|
|
40
|
-
.then(result => {
|
|
41
|
-
this.resolve(result);
|
|
42
|
-
resolve(result);
|
|
43
|
-
})
|
|
44
|
-
.catch(err => {
|
|
45
|
-
this.reject(err);
|
|
46
|
-
reject(err);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
};
|
|
35
|
+
apiBinary(payload, config, options) {
|
|
36
|
+
return super.apiBinary(payload, config, options);
|
|
50
37
|
}
|
|
51
38
|
}
|
|
52
|
-
;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { LambderRenderContext } from "./LambderContext.js";
|
|
2
|
+
export type HttpStatusCode = 100 | 101 | 200 | 201 | 202 | 203 | 204 | 206 | 300 | 301 | 302 | 303 | 304 | 307 | 308 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 408 | 409 | 410 | 412 | 413 | 415 | 416 | 418 | 422 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504;
|
|
3
|
+
export type LambderHeadersInput = Record<string, string | string[]>;
|
|
4
|
+
/**
|
|
5
|
+
* Final Lambda response: v1 (REST API) uses multiValueHeaders, v2 (HTTP API /
|
|
6
|
+
* Function URLs) uses headers + cookies. Assignable to both official aws-lambda
|
|
7
|
+
* result types (compile-time asserted below), so exporting getHandler() as an
|
|
8
|
+
* APIGatewayProxyHandler / APIGatewayProxyHandlerV2 is type-safe.
|
|
9
|
+
*/
|
|
10
|
+
export type LambderHttpResponse = {
|
|
11
|
+
statusCode: number;
|
|
12
|
+
body: string;
|
|
13
|
+
isBase64Encoded: boolean;
|
|
14
|
+
/** API Gateway REST API (payload v1). */
|
|
15
|
+
multiValueHeaders?: Record<string, string[]>;
|
|
16
|
+
/** API Gateway HTTP API / Lambda Function URLs (payload v2). */
|
|
17
|
+
headers?: Record<string, string>;
|
|
18
|
+
cookies?: string[];
|
|
19
|
+
};
|
|
20
|
+
export type LambderHttpEventFormat = "v1" | "v2";
|
|
21
|
+
export declare const normalizeHeaders: (headers?: LambderHeadersInput) => Record<string, string[]>;
|
|
22
|
+
export type LambderResponseInit = {
|
|
23
|
+
statusCode: HttpStatusCode;
|
|
24
|
+
headers?: LambderHeadersInput;
|
|
25
|
+
body?: string | Buffer | null;
|
|
26
|
+
/** True when body is already a base64-encoded string (pre-encoded binary content). */
|
|
27
|
+
isBodyBase64?: boolean;
|
|
28
|
+
/** "auto": gzip when enabled + compressible + large enough. true: force (if client accepts). false: never. */
|
|
29
|
+
compress?: boolean | "auto";
|
|
30
|
+
/** "auto": ETag on GET/HEAD 200 when globally enabled. true: force. false: never. */
|
|
31
|
+
etag?: boolean | "auto";
|
|
32
|
+
};
|
|
33
|
+
/**
|
|
34
|
+
* Intermediate response object returned by all response builder methods and by
|
|
35
|
+
* route/api handlers. Bodies stay uncompressed and un-encoded so hooks can
|
|
36
|
+
* still transform them; a single finalize step at the end of render() applies
|
|
37
|
+
* compression, ETag/304 handling and base64 encoding.
|
|
38
|
+
*
|
|
39
|
+
* Throwing a LambderResponse anywhere inside a handler or hook short-circuits
|
|
40
|
+
* the request: the thrown response becomes the response.
|
|
41
|
+
*/
|
|
42
|
+
export declare class LambderResponse {
|
|
43
|
+
statusCode: HttpStatusCode;
|
|
44
|
+
headers: Record<string, string[]>;
|
|
45
|
+
body: string | Buffer | null;
|
|
46
|
+
isBodyBase64: boolean;
|
|
47
|
+
compress: boolean | "auto";
|
|
48
|
+
etag: boolean | "auto";
|
|
49
|
+
constructor(init: LambderResponseInit);
|
|
50
|
+
getHeader(key: string): string[] | undefined;
|
|
51
|
+
setHeader(key: string, value: string | string[]): this;
|
|
52
|
+
addHeader(key: string, value: string): this;
|
|
53
|
+
}
|
|
54
|
+
export declare const isCompressibleContentType: (contentType: string | undefined) => boolean;
|
|
55
|
+
export declare const acceptsEncoding: (acceptEncoding: string | undefined | null, encoding: string) => boolean;
|
|
56
|
+
export type LambderFinalizeOptions = {
|
|
57
|
+
compression: false | {
|
|
58
|
+
minBytes: number;
|
|
59
|
+
};
|
|
60
|
+
etag: boolean;
|
|
61
|
+
/** Guard against Lambda's ~6MB response cap with a clear error. */
|
|
62
|
+
maxResponseBytes: number;
|
|
63
|
+
};
|
|
64
|
+
export declare const DEFAULT_FINALIZE_OPTIONS: LambderFinalizeOptions;
|
|
65
|
+
/**
|
|
66
|
+
* Convert an intermediate LambderResponse into the final Lambda response:
|
|
67
|
+
* gzip negotiation (Accept-Encoding), ETag + If-None-Match 304, base64
|
|
68
|
+
* encoding, HEAD body stripping, and Lambda payload size guard. Emits the v1
|
|
69
|
+
* (REST API) or v2 (HTTP API / Function URL) response shape.
|
|
70
|
+
*/
|
|
71
|
+
export declare const finalizeResponse: (ctx: Pick<LambderRenderContext, "method" | "headers"> | null, response: LambderResponse, options: LambderFinalizeOptions, format?: LambderHttpEventFormat) => Promise<LambderHttpResponse>;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { getZlib, getCrypto } from "./node-polyfills.js";
|
|
2
|
+
export const normalizeHeaders = (headers) => Object.fromEntries(Object.entries(headers ?? {}).map(([k, v]) => [k, Array.isArray(v) ? [...v] : [v]]));
|
|
3
|
+
/**
|
|
4
|
+
* Intermediate response object returned by all response builder methods and by
|
|
5
|
+
* route/api handlers. Bodies stay uncompressed and un-encoded so hooks can
|
|
6
|
+
* still transform them; a single finalize step at the end of render() applies
|
|
7
|
+
* compression, ETag/304 handling and base64 encoding.
|
|
8
|
+
*
|
|
9
|
+
* Throwing a LambderResponse anywhere inside a handler or hook short-circuits
|
|
10
|
+
* the request: the thrown response becomes the response.
|
|
11
|
+
*/
|
|
12
|
+
export class LambderResponse {
|
|
13
|
+
statusCode;
|
|
14
|
+
headers;
|
|
15
|
+
body;
|
|
16
|
+
isBodyBase64;
|
|
17
|
+
compress;
|
|
18
|
+
etag;
|
|
19
|
+
constructor(init) {
|
|
20
|
+
this.statusCode = init.statusCode;
|
|
21
|
+
this.headers = normalizeHeaders(init.headers);
|
|
22
|
+
this.body = init.body ?? null;
|
|
23
|
+
this.isBodyBase64 = init.isBodyBase64 ?? false;
|
|
24
|
+
this.compress = init.compress ?? "auto";
|
|
25
|
+
this.etag = init.etag ?? "auto";
|
|
26
|
+
}
|
|
27
|
+
getHeader(key) {
|
|
28
|
+
const lower = key.toLowerCase();
|
|
29
|
+
for (const [k, v] of Object.entries(this.headers)) {
|
|
30
|
+
if (k.toLowerCase() === lower)
|
|
31
|
+
return v;
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
setHeader(key, value) {
|
|
36
|
+
const lower = key.toLowerCase();
|
|
37
|
+
for (const k of Object.keys(this.headers)) {
|
|
38
|
+
if (k.toLowerCase() === lower)
|
|
39
|
+
delete this.headers[k];
|
|
40
|
+
}
|
|
41
|
+
this.headers[key] = Array.isArray(value) ? [...value] : [value];
|
|
42
|
+
return this;
|
|
43
|
+
}
|
|
44
|
+
addHeader(key, value) {
|
|
45
|
+
const lower = key.toLowerCase();
|
|
46
|
+
const existingKey = Object.keys(this.headers).find((k) => k.toLowerCase() === lower);
|
|
47
|
+
if (existingKey) {
|
|
48
|
+
this.headers[existingKey].push(value);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
this.headers[key] = [value];
|
|
52
|
+
}
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export const isCompressibleContentType = (contentType) => {
|
|
57
|
+
if (!contentType)
|
|
58
|
+
return false;
|
|
59
|
+
const mime = (contentType.split(";")[0] ?? "").trim().toLowerCase();
|
|
60
|
+
if (mime.startsWith("text/"))
|
|
61
|
+
return true;
|
|
62
|
+
if (mime.endsWith("+json") || mime.endsWith("+xml"))
|
|
63
|
+
return true;
|
|
64
|
+
return [
|
|
65
|
+
"application/json",
|
|
66
|
+
"application/javascript",
|
|
67
|
+
"application/x-javascript",
|
|
68
|
+
"application/xml",
|
|
69
|
+
"application/wasm",
|
|
70
|
+
"image/svg+xml",
|
|
71
|
+
"application/lambder-json-stream",
|
|
72
|
+
].includes(mime);
|
|
73
|
+
};
|
|
74
|
+
export const acceptsEncoding = (acceptEncoding, encoding) => {
|
|
75
|
+
if (!acceptEncoding)
|
|
76
|
+
return false;
|
|
77
|
+
return acceptEncoding.split(",").some((part) => {
|
|
78
|
+
const [token, ...params] = part.trim().split(";");
|
|
79
|
+
const name = (token ?? "").trim().toLowerCase();
|
|
80
|
+
if (name !== encoding && name !== "*")
|
|
81
|
+
return false;
|
|
82
|
+
const q = params.map((p) => p.trim()).find((p) => p.startsWith("q="));
|
|
83
|
+
return !q || Number(q.slice(2)) > 0;
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
export const DEFAULT_FINALIZE_OPTIONS = {
|
|
87
|
+
compression: { minBytes: 860 },
|
|
88
|
+
etag: true,
|
|
89
|
+
maxResponseBytes: 5_500_000,
|
|
90
|
+
};
|
|
91
|
+
const getRequestHeader = (ctx, name) => {
|
|
92
|
+
if (!ctx?.headers)
|
|
93
|
+
return undefined;
|
|
94
|
+
const lower = name.toLowerCase();
|
|
95
|
+
for (const [k, v] of Object.entries(ctx.headers)) {
|
|
96
|
+
if (k.toLowerCase() === lower)
|
|
97
|
+
return v ?? undefined;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
};
|
|
101
|
+
/** Emit the format-specific Lambda response shape. */
|
|
102
|
+
const emitResponse = (format, statusCode, headers, body, isBase64Encoded) => {
|
|
103
|
+
if (format === "v2") {
|
|
104
|
+
// Payload v2 has no multiValueHeaders: multi-values are comma-joined,
|
|
105
|
+
// except Set-Cookie which uses the dedicated cookies array.
|
|
106
|
+
const singleHeaders = {};
|
|
107
|
+
const cookies = [];
|
|
108
|
+
for (const [key, values] of Object.entries(headers)) {
|
|
109
|
+
if (key.toLowerCase() === "set-cookie")
|
|
110
|
+
cookies.push(...values);
|
|
111
|
+
else
|
|
112
|
+
singleHeaders[key] = values.join(", ");
|
|
113
|
+
}
|
|
114
|
+
return { statusCode, headers: singleHeaders, cookies, body, isBase64Encoded };
|
|
115
|
+
}
|
|
116
|
+
return { statusCode, multiValueHeaders: headers, body, isBase64Encoded };
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Convert an intermediate LambderResponse into the final Lambda response:
|
|
120
|
+
* gzip negotiation (Accept-Encoding), ETag + If-None-Match 304, base64
|
|
121
|
+
* encoding, HEAD body stripping, and Lambda payload size guard. Emits the v1
|
|
122
|
+
* (REST API) or v2 (HTTP API / Function URL) response shape.
|
|
123
|
+
*/
|
|
124
|
+
export const finalizeResponse = async (ctx, response, options, format = "v1") => {
|
|
125
|
+
const method = (ctx?.method ?? "GET").toUpperCase();
|
|
126
|
+
if (response.body === null) {
|
|
127
|
+
return emitResponse(format, response.statusCode, response.headers, "", false);
|
|
128
|
+
}
|
|
129
|
+
let outBody;
|
|
130
|
+
let isBase64 = false;
|
|
131
|
+
if (response.isBodyBase64) {
|
|
132
|
+
// Pre-encoded binary content: passes through untouched (no compression).
|
|
133
|
+
outBody = String(response.body);
|
|
134
|
+
isBase64 = true;
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
let bodyBuffer = Buffer.isBuffer(response.body)
|
|
138
|
+
? response.body
|
|
139
|
+
: Buffer.from(String(response.body), "utf8");
|
|
140
|
+
const contentType = response.getHeader("Content-Type")?.[0];
|
|
141
|
+
const alreadyEncoded = !!response.getHeader("Content-Encoding");
|
|
142
|
+
const eligibleForCompression = !alreadyEncoded && (response.compress === true ||
|
|
143
|
+
(response.compress === "auto" &&
|
|
144
|
+
options.compression !== false &&
|
|
145
|
+
bodyBuffer.length >= options.compression.minBytes &&
|
|
146
|
+
isCompressibleContentType(contentType)));
|
|
147
|
+
if (eligibleForCompression) {
|
|
148
|
+
// Vary even when this client didn't accept an encoding, to keep caches correct.
|
|
149
|
+
response.addHeader("Vary", "Accept-Encoding");
|
|
150
|
+
if (acceptsEncoding(getRequestHeader(ctx, "accept-encoding"), "gzip")) {
|
|
151
|
+
const zlib = await getZlib();
|
|
152
|
+
if (zlib) {
|
|
153
|
+
bodyBuffer = zlib.gzipSync(bodyBuffer);
|
|
154
|
+
response.setHeader("Content-Encoding", "gzip");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (Buffer.isBuffer(response.body) || response.getHeader("Content-Encoding")) {
|
|
159
|
+
outBody = bodyBuffer.toString("base64");
|
|
160
|
+
isBase64 = true;
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
outBody = bodyBuffer.toString("utf8");
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const etagEnabled = response.etag === true || (response.etag === "auto" &&
|
|
167
|
+
options.etag &&
|
|
168
|
+
response.statusCode === 200 &&
|
|
169
|
+
(method === "GET" || method === "HEAD"));
|
|
170
|
+
if (etagEnabled) {
|
|
171
|
+
const crypto = await getCrypto();
|
|
172
|
+
if (crypto) {
|
|
173
|
+
const etagValue = `"${crypto.createHash("sha256").update(outBody).digest("hex").slice(0, 32)}"`;
|
|
174
|
+
response.setHeader("ETag", etagValue);
|
|
175
|
+
const ifNoneMatch = getRequestHeader(ctx, "if-none-match");
|
|
176
|
+
if (ifNoneMatch && ifNoneMatch.split(",").map((s) => s.trim()).includes(etagValue)) {
|
|
177
|
+
const preservedHeaders = {};
|
|
178
|
+
for (const key of ["ETag", "Cache-Control", "Vary", "Expires", "Last-Modified"]) {
|
|
179
|
+
const value = response.getHeader(key);
|
|
180
|
+
if (value)
|
|
181
|
+
preservedHeaders[key] = value;
|
|
182
|
+
}
|
|
183
|
+
return emitResponse(format, 304, preservedHeaders, "", false);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (method === "HEAD") {
|
|
188
|
+
return emitResponse(format, response.statusCode, response.headers, "", false);
|
|
189
|
+
}
|
|
190
|
+
if (outBody.length > options.maxResponseBytes) {
|
|
191
|
+
throw new Error(`Lambder: final response body is ${outBody.length} bytes which exceeds the configured ` +
|
|
192
|
+
`maxResponseBytes (${options.maxResponseBytes}). Lambda caps proxy responses at ~6MB. ` +
|
|
193
|
+
`Consider pagination or enabling compression.`);
|
|
194
|
+
}
|
|
195
|
+
return emitResponse(format, response.statusCode, response.headers, outBody, isBase64);
|
|
196
|
+
};
|
|
@@ -1,11 +1,16 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
1
|
+
import type { LambderRenderContext } from "./LambderContext.js";
|
|
2
|
+
import { LambderResponse, type HttpStatusCode, type LambderHeadersInput } from "./LambderResponse.js";
|
|
3
|
+
import { LambderSafeHtml } from "./LambderHtml.js";
|
|
4
|
+
import { type LambderTemplateData } from "./LambderTemplatingEngine.js";
|
|
5
|
+
export type LambderResponseOptions = {
|
|
6
|
+
statusCode?: HttpStatusCode;
|
|
7
|
+
headers?: LambderHeadersInput;
|
|
8
|
+
/** Shorthand for the Cache-Control header. */
|
|
9
|
+
cacheControl?: string;
|
|
10
|
+
/** "auto" (default): gzip when compressible/large enough. true: force. false: never. */
|
|
11
|
+
compress?: boolean | "auto";
|
|
12
|
+
/** "auto" (default): ETag on GET/HEAD 200 when globally enabled. true: force. false: never. */
|
|
13
|
+
etag?: boolean | "auto";
|
|
9
14
|
};
|
|
10
15
|
export type LambderApiResponseConfig = {
|
|
11
16
|
versionExpired?: boolean;
|
|
@@ -16,38 +21,58 @@ export type LambderApiResponseConfig = {
|
|
|
16
21
|
logList?: any[];
|
|
17
22
|
};
|
|
18
23
|
export type LambderApiResponse<T> = LambderApiResponseConfig & {
|
|
24
|
+
apiVersion?: string | null;
|
|
19
25
|
payload?: T | null;
|
|
20
26
|
};
|
|
27
|
+
export type LambderRawResponseInit = {
|
|
28
|
+
statusCode: HttpStatusCode;
|
|
29
|
+
headers?: LambderHeadersInput;
|
|
30
|
+
/** Legacy alias for headers (API Gateway naming). */
|
|
31
|
+
multiValueHeaders?: Record<string, string[]>;
|
|
32
|
+
body: string | Buffer | null;
|
|
33
|
+
/** True when body is already a base64-encoded string. */
|
|
34
|
+
isBase64Encoded?: boolean;
|
|
35
|
+
compress?: boolean | "auto";
|
|
36
|
+
etag?: boolean | "auto";
|
|
37
|
+
};
|
|
21
38
|
export default class LambderResponseBuilder<TResponse = any> {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
private ctx?;
|
|
27
|
-
constructor({ isCorsEnabled, publicPath, apiVersion, lambderUtils, ctx }: {
|
|
28
|
-
isCorsEnabled: boolean;
|
|
39
|
+
protected publicPath: string;
|
|
40
|
+
protected apiVersion: string | null;
|
|
41
|
+
protected ctx?: LambderRenderContext;
|
|
42
|
+
constructor({ publicPath, apiVersion, ctx }: {
|
|
29
43
|
publicPath: string;
|
|
30
44
|
apiVersion?: string | null;
|
|
31
|
-
|
|
32
|
-
ctx?: LambderRenderContext<any>;
|
|
45
|
+
ctx?: LambderRenderContext;
|
|
33
46
|
});
|
|
34
|
-
private
|
|
35
|
-
private
|
|
47
|
+
private buildResponse;
|
|
48
|
+
private resolvePublicFilePath;
|
|
36
49
|
addHeader(key: string, value: string): void;
|
|
37
50
|
setHeader(key: string, value: string | string[]): void;
|
|
38
51
|
logToApiResponse(input: any): void;
|
|
39
|
-
raw(
|
|
40
|
-
json(data: Record<string, any>,
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
raw(init: LambderRawResponseInit): LambderResponse;
|
|
53
|
+
json(data: Record<string, any>, options?: LambderResponseOptions): LambderResponse;
|
|
54
|
+
text(data: string, options?: LambderResponseOptions): LambderResponse;
|
|
55
|
+
xml(data: string | LambderSafeHtml, options?: LambderResponseOptions): LambderResponse;
|
|
56
|
+
html(data: string | LambderSafeHtml, options?: LambderResponseOptions): LambderResponse;
|
|
57
|
+
status(statusCode: HttpStatusCode, body?: string, options?: LambderResponseOptions): LambderResponse;
|
|
58
|
+
status404(data: string, options?: LambderResponseOptions): LambderResponse;
|
|
59
|
+
redirect(url: string, statusCode?: HttpStatusCode, options?: LambderResponseOptions): LambderResponse;
|
|
60
|
+
versionExpired(options?: LambderResponseOptions): LambderResponse;
|
|
61
|
+
fileBase64(fileBase64: string, mimeType: string, options?: LambderResponseOptions): LambderResponse;
|
|
62
|
+
file(filePath: string, options?: LambderResponseOptions & {
|
|
63
|
+
fallback?: string;
|
|
64
|
+
}): Promise<LambderResponse>;
|
|
65
|
+
/**
|
|
66
|
+
* Render an HTML file under publicPath through LambderTemplatingEngine
|
|
67
|
+
* (comment-based slots/conditionals) and return it as an HTML response.
|
|
68
|
+
* The compiled template is cached across warm invocations; a missing file
|
|
69
|
+
* throws (it is a server-side configuration error, not a client 404).
|
|
70
|
+
* Set htmlVirtualSlots to expose "title"/"head" slots on marker-less files.
|
|
71
|
+
*/
|
|
72
|
+
templateFile(filePath: string, data?: LambderTemplateData, options?: LambderResponseOptions & {
|
|
73
|
+
htmlVirtualSlots?: boolean;
|
|
74
|
+
}): Promise<LambderResponse>;
|
|
75
|
+
api(payload: TResponse | null, { versionExpired, sessionExpired, notAuthorized, message, errorMessage, logList, }?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
76
|
+
/** Same as api() but forces gzip compression of the response body. */
|
|
77
|
+
apiBinary(payload: TResponse | null, config?: LambderApiResponseConfig, options?: LambderResponseOptions): LambderResponse;
|
|
53
78
|
}
|