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/Lambder.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { match } from "path-to-regexp";
|
|
2
1
|
import LambderResolver from "./LambderResolver.js";
|
|
3
2
|
import LambderResponseBuilder from "./LambderResponseBuilder.js";
|
|
4
|
-
import
|
|
3
|
+
import { LambderResponse, finalizeResponse, DEFAULT_FINALIZE_OPTIONS, } from "./LambderResponse.js";
|
|
4
|
+
import { compileRouteMatcher } from "./LambderRouting.js";
|
|
5
|
+
import { applyCorsHeaders } from "./LambderCors.js";
|
|
5
6
|
import LambderSessionManager from "./LambderSessionManager.js";
|
|
6
7
|
import LambderSessionController from "./LambderSessionController.js";
|
|
7
|
-
import {
|
|
8
|
+
import { LambderPublicFilesHandler } from "./LambderPublicFiles.js";
|
|
9
|
+
import { createContext, isV2HttpEvent } from "./LambderContext.js";
|
|
8
10
|
/**
|
|
9
11
|
* Main Lambder class for building type-safe serverless APIs
|
|
10
12
|
*
|
|
@@ -23,9 +25,7 @@ import { createContext } from "./LambderContext.js";
|
|
|
23
25
|
export default class Lambder {
|
|
24
26
|
apiPath;
|
|
25
27
|
apiVersion;
|
|
26
|
-
isCorsEnabled = false;
|
|
27
28
|
publicPath;
|
|
28
|
-
ejsPath;
|
|
29
29
|
/**
|
|
30
30
|
* Type property for extracting the API contract
|
|
31
31
|
* Use this to export your API types to the frontend
|
|
@@ -37,37 +37,48 @@ export default class Lambder {
|
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
39
|
ApiContract;
|
|
40
|
-
actionList;
|
|
41
|
-
hookList;
|
|
40
|
+
actionList = [];
|
|
41
|
+
hookList = { "beforeRender": [], "afterRender": [], "fallback": [] };
|
|
42
|
+
createdHooks = [];
|
|
43
|
+
initPromise = null;
|
|
42
44
|
globalErrorHandler = null;
|
|
43
45
|
routeFallbackHandler = null;
|
|
44
46
|
apiFallbackHandler = null;
|
|
45
47
|
apiInputValidationErrorHandler = null;
|
|
46
|
-
|
|
48
|
+
sessionExpiredRouteHandler = null;
|
|
49
|
+
publicFilesHandler = null;
|
|
50
|
+
indexHtmlConfig = null;
|
|
51
|
+
eventActionList = [];
|
|
52
|
+
corsConfig = null;
|
|
53
|
+
finalizeOptions;
|
|
47
54
|
lambderSessionManager;
|
|
55
|
+
sessionCookieOptions = {};
|
|
48
56
|
sessionTokenCookieKey = "LMDRSESSIONTKID";
|
|
49
57
|
sessionCsrfCookieKey = "LMDRSESSIONCSTK";
|
|
50
|
-
constructor(
|
|
51
|
-
this.publicPath = publicPath || "/incorrect-path-not-found";
|
|
52
|
-
this.
|
|
53
|
-
this.
|
|
54
|
-
this.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
constructor(options = {}) {
|
|
59
|
+
this.publicPath = options.publicPath || "/incorrect-path-not-found";
|
|
60
|
+
this.apiPath = options.apiPath ?? "/api";
|
|
61
|
+
this.apiVersion = options.apiVersion ?? null;
|
|
62
|
+
this.finalizeOptions = {
|
|
63
|
+
compression: options.compression === false
|
|
64
|
+
? false
|
|
65
|
+
: { minBytes: options.compression?.minBytes ?? DEFAULT_FINALIZE_OPTIONS.compression.minBytes },
|
|
66
|
+
etag: options.etag ?? DEFAULT_FINALIZE_OPTIONS.etag,
|
|
67
|
+
maxResponseBytes: options.maxResponseBytes ?? DEFAULT_FINALIZE_OPTIONS.maxResponseBytes,
|
|
60
68
|
};
|
|
61
|
-
this.utils = new LambderUtils({ ejsPath });
|
|
62
69
|
}
|
|
63
|
-
enableCors(
|
|
64
|
-
this.
|
|
70
|
+
enableCors(config) {
|
|
71
|
+
this.corsConfig = config === true ? {} : (config === false ? null : config);
|
|
65
72
|
return this;
|
|
66
73
|
}
|
|
67
|
-
enableDdbSession({ tableName, tableRegion, sessionSalt, enableSlidingExpiration
|
|
74
|
+
enableDdbSession({ tableName, tableRegion, sessionSalt, enableSlidingExpiration, slidingWriteIntervalSeconds, cookie, partitionKey, sortKey, }) {
|
|
68
75
|
this.lambderSessionManager = new LambderSessionManager({
|
|
69
|
-
tableName, tableRegion,
|
|
76
|
+
tableName, tableRegion,
|
|
77
|
+
partitionKey: partitionKey ?? "pk",
|
|
78
|
+
sortKey: sortKey ?? "sk",
|
|
79
|
+
sessionSalt, enableSlidingExpiration, slidingWriteIntervalSeconds,
|
|
70
80
|
});
|
|
81
|
+
this.sessionCookieOptions = cookie ?? {};
|
|
71
82
|
return this;
|
|
72
83
|
}
|
|
73
84
|
setSessionCookieKey(sessionTokenCookieKey, sessionCsrfCookieKey) {
|
|
@@ -91,71 +102,71 @@ export default class Lambder {
|
|
|
91
102
|
this.globalErrorHandler = globalErrorHandler;
|
|
92
103
|
return this;
|
|
93
104
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
return result?.params || {};
|
|
105
|
+
/** Response for session routes when the session is missing/expired (non-API). Default: 401. */
|
|
106
|
+
setSessionExpiredRouteHandler(handler) {
|
|
107
|
+
this.sessionExpiredRouteHandler = handler;
|
|
108
|
+
return this;
|
|
99
109
|
}
|
|
100
|
-
|
|
101
|
-
|
|
110
|
+
/**
|
|
111
|
+
* Terminal public-file layer. Runs only when no route matched, so it can
|
|
112
|
+
* never shadow routes registered after it. Serves real files under
|
|
113
|
+
* publicPath (traversal-safe, mime-typed, memory-cached, immutable-cache
|
|
114
|
+
* heuristic for content-hashed assets); when the file does not exist the
|
|
115
|
+
* request falls through to setRouteFallbackHandler, where the app decides
|
|
116
|
+
* what remains (e.g. render an app shell with res.templateFile).
|
|
117
|
+
*/
|
|
118
|
+
servePublicFiles(options = {}) {
|
|
119
|
+
this.publicFilesHandler = new LambderPublicFilesHandler(this.publicPath, options);
|
|
120
|
+
return this;
|
|
102
121
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
122
|
+
/**
|
|
123
|
+
* Serve the app shell for page requests that nothing else handled. Runs
|
|
124
|
+
* after servePublicFiles in the fallback chain, gated by a built-in
|
|
125
|
+
* filter: only configured methods (default GET/HEAD) and, by default, only
|
|
126
|
+
* paths that do not look like files. Gated-out requests fall through to
|
|
127
|
+
* setRouteFallbackHandler. Without a handler, publicPath/index.html is
|
|
128
|
+
* served via res.templateFile (markers optional) with no-cache.
|
|
129
|
+
*/
|
|
130
|
+
serveIndexHtml(handler, options = {}) {
|
|
131
|
+
this.indexHtmlConfig = { handler: handler ?? null, options };
|
|
132
|
+
return this;
|
|
133
|
+
}
|
|
134
|
+
/** Apply the serveIndexHtml gates; null means fall through. */
|
|
135
|
+
async tryServeIndexHtml(ctx, resolver) {
|
|
136
|
+
if (!this.indexHtmlConfig)
|
|
137
|
+
return null;
|
|
138
|
+
const { handler, options } = this.indexHtmlConfig;
|
|
139
|
+
const methods = (options.methods ?? ["GET", "HEAD"]).map((m) => m.toUpperCase());
|
|
140
|
+
if (!methods.includes(ctx.method.toUpperCase()))
|
|
141
|
+
return null;
|
|
142
|
+
if ((options.skipFilePaths ?? true) && (ctx.path.split("/").pop() ?? "").includes("."))
|
|
143
|
+
return null;
|
|
144
|
+
if (options.redirectTrailingSlash && ctx.path.length > 1 && ctx.path.endsWith("/")) {
|
|
145
|
+
const target = ctx.path.replace(/\/+$/, "") || "/";
|
|
146
|
+
return resolver.redirect(target + buildQueryString(ctx), 301);
|
|
116
147
|
}
|
|
117
|
-
|
|
118
|
-
|
|
148
|
+
const response = handler
|
|
149
|
+
? await handler(ctx, resolver)
|
|
150
|
+
: await resolver.templateFile(typeof options.indexFile === "function" ? options.indexFile(ctx) : (options.indexFile ?? "index.html"), {}, { cacheControl: "no-cache" });
|
|
151
|
+
if (options.compress !== undefined) {
|
|
152
|
+
response.compress = typeof options.compress === "function" ? options.compress(ctx) : options.compress;
|
|
119
153
|
}
|
|
154
|
+
return response;
|
|
120
155
|
}
|
|
121
156
|
addRoute(condition, actionFn) {
|
|
122
157
|
this.actionList.push({
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
(condition?.constructor == RegExp && condition.test(ctx.path)))),
|
|
126
|
-
actionFn: async (ctx, resolver) => {
|
|
127
|
-
if (typeof condition === "string") {
|
|
128
|
-
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
129
|
-
}
|
|
130
|
-
else if (condition?.constructor == RegExp) {
|
|
131
|
-
const match = ctx.path.match(condition);
|
|
132
|
-
ctx.pathParams = match ? (match.groups || match) : {};
|
|
133
|
-
}
|
|
134
|
-
return await actionFn(ctx, resolver);
|
|
135
|
-
}
|
|
158
|
+
match: compileRouteMatcher(condition),
|
|
159
|
+
actionFn: (ctx, resolver) => actionFn(ctx, resolver),
|
|
136
160
|
});
|
|
137
161
|
return this;
|
|
138
162
|
}
|
|
139
163
|
addSessionRoute(condition, actionFn) {
|
|
140
164
|
this.actionList.push({
|
|
141
|
-
|
|
142
|
-
(typeof condition === "function" && condition(ctx)) ||
|
|
143
|
-
(condition?.constructor == RegExp && condition.test(ctx.path)))),
|
|
165
|
+
match: compileRouteMatcher(condition),
|
|
144
166
|
actionFn: async (ctx, resolver) => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
else if (condition?.constructor == RegExp) {
|
|
149
|
-
const match = ctx.path.match(condition);
|
|
150
|
-
ctx.pathParams = match ? (match.groups || match) : {};
|
|
151
|
-
}
|
|
152
|
-
const sessionCtx = ctx;
|
|
153
|
-
await this.getSessionController(ctx).fetchSession();
|
|
154
|
-
if (!sessionCtx.session) {
|
|
155
|
-
throw new Error("Session not found.");
|
|
156
|
-
}
|
|
157
|
-
return await actionFn(sessionCtx, resolver);
|
|
158
|
-
}
|
|
167
|
+
await this.requireSession(ctx, resolver);
|
|
168
|
+
return await actionFn(ctx, resolver);
|
|
169
|
+
},
|
|
159
170
|
});
|
|
160
171
|
return this;
|
|
161
172
|
}
|
|
@@ -166,21 +177,15 @@ export default class Lambder {
|
|
|
166
177
|
// Typed API with Zod
|
|
167
178
|
addApi(name, schema, handler) {
|
|
168
179
|
this.actionList.push({
|
|
169
|
-
|
|
180
|
+
match: (ctx) => ctx.apiName === name ? {} : false,
|
|
170
181
|
actionFn: async (ctx, resolver) => {
|
|
171
|
-
// Validate Input
|
|
172
182
|
const inputResult = schema.input.safeParse(ctx.apiPayload);
|
|
173
183
|
if (!inputResult.success) {
|
|
174
184
|
if (this.apiInputValidationErrorHandler) {
|
|
175
185
|
return await this.apiInputValidationErrorHandler(ctx, resolver, inputResult.error);
|
|
176
186
|
}
|
|
177
|
-
return resolver.
|
|
178
|
-
statusCode: 422,
|
|
179
|
-
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
180
|
-
multiValueHeaders: { "Content-Type": ["application/json"] }
|
|
181
|
-
});
|
|
187
|
+
return resolver.json({ error: "Input validation failed", zodError: inputResult.error }, { statusCode: 422 });
|
|
182
188
|
}
|
|
183
|
-
// Run Handler with validated data
|
|
184
189
|
ctx.apiPayload = inputResult.data;
|
|
185
190
|
return await handler(ctx, resolver);
|
|
186
191
|
},
|
|
@@ -190,41 +195,49 @@ export default class Lambder {
|
|
|
190
195
|
// Typed Session API with Zod
|
|
191
196
|
addSessionApi(name, schema, handler) {
|
|
192
197
|
this.actionList.push({
|
|
193
|
-
|
|
198
|
+
match: (ctx) => ctx.apiName === name ? {} : false,
|
|
194
199
|
actionFn: async (ctx, resolver) => {
|
|
195
|
-
|
|
196
|
-
await this.getSessionController(ctx).fetchSession();
|
|
197
|
-
if (!sessionCtx.session) {
|
|
198
|
-
throw new Error("Session not found.");
|
|
199
|
-
}
|
|
200
|
-
// Validate Input
|
|
200
|
+
await this.requireSession(ctx, resolver);
|
|
201
201
|
const inputResult = schema.input.safeParse(ctx.apiPayload);
|
|
202
202
|
if (!inputResult.success) {
|
|
203
203
|
if (this.apiInputValidationErrorHandler) {
|
|
204
204
|
return await this.apiInputValidationErrorHandler(ctx, resolver, inputResult.error);
|
|
205
205
|
}
|
|
206
|
-
return resolver.
|
|
207
|
-
statusCode: 400,
|
|
208
|
-
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
209
|
-
multiValueHeaders: { "Content-Type": ["application/json"] }
|
|
210
|
-
});
|
|
206
|
+
return resolver.json({ error: "Input validation failed", zodError: inputResult.error }, { statusCode: 422 });
|
|
211
207
|
}
|
|
212
|
-
// Run Handler with validated data
|
|
213
208
|
ctx.apiPayload = inputResult.data;
|
|
214
|
-
return await handler(
|
|
209
|
+
return await handler(ctx, resolver);
|
|
215
210
|
}
|
|
216
211
|
});
|
|
217
212
|
return this;
|
|
218
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Fetch the session or short-circuit the request: API calls get the
|
|
216
|
+
* protocol's { sessionExpired: true } response (handled by LambderCaller),
|
|
217
|
+
* routes get the sessionExpiredRouteHandler response (default 401).
|
|
218
|
+
*/
|
|
219
|
+
async requireSession(ctx, resolver) {
|
|
220
|
+
const session = await this.getSessionController(ctx).fetchSessionIfExists();
|
|
221
|
+
if (!session) {
|
|
222
|
+
if (ctx._otherInternal.isApiCall) {
|
|
223
|
+
throw resolver.api(null, { sessionExpired: true });
|
|
224
|
+
}
|
|
225
|
+
if (this.sessionExpiredRouteHandler) {
|
|
226
|
+
throw await this.sessionExpiredRouteHandler(ctx, resolver);
|
|
227
|
+
}
|
|
228
|
+
throw resolver.status(401, "Session required.");
|
|
229
|
+
}
|
|
230
|
+
}
|
|
219
231
|
addHook(hookEvent, hookFn, priority = 0) {
|
|
220
232
|
if (hookEvent === "created") {
|
|
221
|
-
|
|
233
|
+
// Runs once, lazily, at the first render() call.
|
|
234
|
+
this.createdHooks.push(hookFn);
|
|
222
235
|
}
|
|
223
236
|
else {
|
|
224
237
|
this.hookList[hookEvent].push({ priority, hookFn });
|
|
225
238
|
this.hookList[hookEvent].sort((a, b) => a.priority - b.priority);
|
|
226
|
-
return this;
|
|
227
239
|
}
|
|
240
|
+
return this;
|
|
228
241
|
}
|
|
229
242
|
getSessionController(ctx) {
|
|
230
243
|
if (!this.lambderSessionManager)
|
|
@@ -233,97 +246,230 @@ export default class Lambder {
|
|
|
233
246
|
lambderSessionManager: this.lambderSessionManager,
|
|
234
247
|
sessionTokenCookieKey: this.sessionTokenCookieKey,
|
|
235
248
|
sessionCsrfCookieKey: this.sessionCsrfCookieKey,
|
|
249
|
+
cookieOptions: this.sessionCookieOptions,
|
|
236
250
|
ctx,
|
|
237
251
|
});
|
|
238
252
|
}
|
|
239
|
-
getResponseBuilder() {
|
|
253
|
+
getResponseBuilder(ctx) {
|
|
240
254
|
return new LambderResponseBuilder({
|
|
241
|
-
isCorsEnabled: this.isCorsEnabled,
|
|
242
255
|
publicPath: this.publicPath,
|
|
243
256
|
apiVersion: this.apiVersion,
|
|
244
|
-
|
|
257
|
+
ctx,
|
|
245
258
|
});
|
|
246
259
|
}
|
|
247
260
|
;
|
|
248
|
-
getResolver(ctx
|
|
261
|
+
getResolver(ctx) {
|
|
249
262
|
return new LambderResolver({
|
|
250
|
-
isCorsEnabled: this.isCorsEnabled,
|
|
251
263
|
publicPath: this.publicPath,
|
|
252
264
|
apiVersion: this.apiVersion,
|
|
253
|
-
|
|
254
|
-
ctx, resolve, reject
|
|
265
|
+
ctx,
|
|
255
266
|
});
|
|
256
267
|
}
|
|
257
268
|
;
|
|
258
269
|
getHandler() {
|
|
259
|
-
return (event, context) =>
|
|
270
|
+
return ((event, context) => Lambder.isHttpEvent(event)
|
|
271
|
+
? this.render(event, context)
|
|
272
|
+
: this.renderEvent(event, context));
|
|
273
|
+
}
|
|
274
|
+
// ---------------------------------------------------------------------
|
|
275
|
+
// Actions (raw-event or context filtering; the only handler for non-HTTP)
|
|
276
|
+
// ---------------------------------------------------------------------
|
|
277
|
+
/** True when the Lambda event is an API Gateway HTTP event (REST API v1 or HTTP API / Function URL v2). */
|
|
278
|
+
static isHttpEvent(event) {
|
|
279
|
+
if (!event || typeof event !== "object")
|
|
280
|
+
return false;
|
|
281
|
+
if ("httpMethod" in event && "path" in event)
|
|
282
|
+
return true;
|
|
283
|
+
return isV2HttpEvent(event);
|
|
284
|
+
}
|
|
285
|
+
addAction(filter, actionFn) {
|
|
286
|
+
// HTTP side: joins the route/API chain in registration order.
|
|
287
|
+
this.actionList.push({
|
|
288
|
+
match: (ctx) => filter(ctx.event, ctx) ? {} : false,
|
|
289
|
+
actionFn: async (ctx, resolver) => {
|
|
290
|
+
const result = await actionFn(ctx.event, { ctx, res: resolver, lambdaContext: ctx.lambdaContext });
|
|
291
|
+
if (!(result instanceof LambderResponse)) {
|
|
292
|
+
throw new Error("Lambder: an addAction matched an HTTP request but did not return a response. Build one with tools.res.");
|
|
293
|
+
}
|
|
294
|
+
return result;
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
// Non-HTTP side.
|
|
298
|
+
this.eventActionList.push({
|
|
299
|
+
match: (event) => filter(event, null),
|
|
300
|
+
actionFn: (event, lambdaContext) => actionFn(event, { ctx: null, res: null, lambdaContext }),
|
|
301
|
+
});
|
|
302
|
+
return this;
|
|
303
|
+
}
|
|
304
|
+
/** Dispatch a non-HTTP Lambda event to the registered actions. */
|
|
305
|
+
async renderEvent(event, lambdaContext) {
|
|
306
|
+
await this.ensureInitialized();
|
|
307
|
+
for (const action of this.eventActionList) {
|
|
308
|
+
if (action.match(event)) {
|
|
309
|
+
return await action.actionFn(event, lambdaContext);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const summary = event && typeof event === "object"
|
|
313
|
+
? ` (source: ${String(event.source ?? "?")}, detail-type: ${String(event["detail-type"] ?? "?")})`
|
|
314
|
+
: "";
|
|
315
|
+
throw new Error(`Lambder: no action matched non-HTTP event${summary}. Register one with addAction(); a trailing addAction(() => true, ...) acts as a fallback.`);
|
|
316
|
+
}
|
|
317
|
+
// ---------------------------------------------------------------------
|
|
318
|
+
// Render pipeline
|
|
319
|
+
// ---------------------------------------------------------------------
|
|
320
|
+
ensureInitialized() {
|
|
321
|
+
if (!this.initPromise) {
|
|
322
|
+
this.initPromise = (async () => {
|
|
323
|
+
for (const hookFn of this.createdHooks) {
|
|
324
|
+
await hookFn(this);
|
|
325
|
+
}
|
|
326
|
+
})();
|
|
327
|
+
}
|
|
328
|
+
return this.initPromise;
|
|
329
|
+
}
|
|
330
|
+
applyCors(ctx, response, isPreflight) {
|
|
331
|
+
applyCorsHeaders(this.corsConfig, ctx, response, isPreflight);
|
|
332
|
+
}
|
|
333
|
+
async handleNoMatchedAction(ctx, resolver) {
|
|
334
|
+
for (const hook of this.hookList["fallback"]) {
|
|
335
|
+
await hook.hookFn(ctx, resolver);
|
|
336
|
+
}
|
|
337
|
+
const isAPI = ctx._otherInternal.isApiCall || ctx.path === this.apiPath;
|
|
338
|
+
if (isAPI) {
|
|
339
|
+
if (this.apiFallbackHandler)
|
|
340
|
+
return await this.apiFallbackHandler(ctx, resolver);
|
|
341
|
+
return resolver.api(null, { errorMessage: "API not found." });
|
|
342
|
+
}
|
|
343
|
+
if (this.publicFilesHandler) {
|
|
344
|
+
const fileResponse = await this.publicFilesHandler.handle(ctx);
|
|
345
|
+
if (fileResponse)
|
|
346
|
+
return fileResponse;
|
|
347
|
+
}
|
|
348
|
+
const indexResponse = await this.tryServeIndexHtml(ctx, resolver);
|
|
349
|
+
if (indexResponse)
|
|
350
|
+
return indexResponse;
|
|
351
|
+
if (this.routeFallbackHandler)
|
|
352
|
+
return await this.routeFallbackHandler(ctx, resolver);
|
|
353
|
+
return resolver.text("Not found.", { statusCode: 404 });
|
|
354
|
+
}
|
|
355
|
+
async resolveRequest(ctx, resolver) {
|
|
356
|
+
if (ctx.method === "OPTIONS" && this.corsConfig) {
|
|
357
|
+
const preflight = new LambderResponse({ statusCode: 204, body: null });
|
|
358
|
+
this.applyCors(ctx, preflight, true);
|
|
359
|
+
return preflight;
|
|
360
|
+
}
|
|
361
|
+
// Version check if provided by both the client and the server
|
|
362
|
+
if (this.apiVersion && ctx._otherInternal.requestVersion && ctx._otherInternal.requestVersion !== this.apiVersion) {
|
|
363
|
+
return resolver.versionExpired();
|
|
364
|
+
}
|
|
365
|
+
let matched = null;
|
|
366
|
+
for (const action of this.actionList) {
|
|
367
|
+
const params = action.match(ctx);
|
|
368
|
+
if (params !== false) {
|
|
369
|
+
matched = { action, params };
|
|
370
|
+
break;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (!matched)
|
|
374
|
+
return await this.handleNoMatchedAction(ctx, resolver);
|
|
375
|
+
ctx.pathParams = matched.params;
|
|
376
|
+
let currentCtx = ctx;
|
|
377
|
+
for (const hook of this.hookList["beforeRender"]) {
|
|
378
|
+
const hookResult = await hook.hookFn(currentCtx, resolver);
|
|
379
|
+
if (hookResult instanceof Error)
|
|
380
|
+
throw hookResult;
|
|
381
|
+
if (hookResult instanceof LambderResponse)
|
|
382
|
+
return hookResult;
|
|
383
|
+
currentCtx = hookResult;
|
|
384
|
+
}
|
|
385
|
+
return await matched.action.actionFn(currentCtx, resolver);
|
|
260
386
|
}
|
|
261
387
|
async render(event, lambdaContext) {
|
|
262
|
-
let
|
|
388
|
+
let ctx = null;
|
|
263
389
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
;
|
|
281
|
-
// Run beforeRender hooks
|
|
282
|
-
for (const hook of this.hookList["beforeRender"]) {
|
|
283
|
-
const hookCtx = await hook.hookFn(ctx, resolver);
|
|
284
|
-
if (hookCtx instanceof Error) {
|
|
285
|
-
throw hookCtx;
|
|
286
|
-
}
|
|
287
|
-
ctx = hookCtx;
|
|
288
|
-
}
|
|
289
|
-
// Run matched action
|
|
290
|
-
let response = await firstMatchedAction.actionFn(ctx, resolver);
|
|
291
|
-
// Run afterRender hooks
|
|
292
|
-
for (const hook of this.hookList["afterRender"]) {
|
|
293
|
-
const hookResponse = await hook.hookFn(ctx, resolver, response);
|
|
294
|
-
if (hookResponse instanceof Error) {
|
|
295
|
-
throw hookResponse;
|
|
296
|
-
}
|
|
297
|
-
response = hookResponse;
|
|
298
|
-
}
|
|
299
|
-
// Apply setHeader, addHeader values.
|
|
300
|
-
response.multiValueHeaders = response.multiValueHeaders || {};
|
|
301
|
-
for (const header of ctx._otherInternal.setHeaderFnAccumulator) {
|
|
302
|
-
response.multiValueHeaders[header.key] = Array.isArray(header.value) ? header.value : [header.value];
|
|
303
|
-
}
|
|
304
|
-
for (const header of ctx._otherInternal.addHeaderFnAccumulator) {
|
|
305
|
-
response.multiValueHeaders[header.key] = response.multiValueHeaders[header.key] || [];
|
|
306
|
-
response.multiValueHeaders[header.key].push(header.value);
|
|
307
|
-
}
|
|
308
|
-
resolve(response);
|
|
309
|
-
}
|
|
310
|
-
else {
|
|
311
|
-
return this.handleNoMatchedAction(ctx, resolver);
|
|
312
|
-
}
|
|
390
|
+
await this.ensureInitialized();
|
|
391
|
+
ctx = createContext(event, lambdaContext, this.apiPath);
|
|
392
|
+
const resolver = this.getResolver(ctx);
|
|
393
|
+
let response;
|
|
394
|
+
try {
|
|
395
|
+
response = await this.resolveRequest(ctx, resolver);
|
|
396
|
+
}
|
|
397
|
+
catch (err) {
|
|
398
|
+
// A thrown LambderResponse IS the response (res.die.*, throw res.html(...)).
|
|
399
|
+
if (err instanceof LambderResponse) {
|
|
400
|
+
response = err;
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
throw err;
|
|
313
404
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
405
|
+
}
|
|
406
|
+
try {
|
|
407
|
+
for (const hook of this.hookList["afterRender"]) {
|
|
408
|
+
const hookResponse = await hook.hookFn(ctx, resolver, response);
|
|
409
|
+
if (hookResponse instanceof Error)
|
|
410
|
+
throw hookResponse;
|
|
411
|
+
response = hookResponse;
|
|
317
412
|
}
|
|
318
|
-
}
|
|
413
|
+
}
|
|
414
|
+
catch (err) {
|
|
415
|
+
if (err instanceof LambderResponse) {
|
|
416
|
+
response = err;
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
throw err;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Apply setHeader, addHeader values.
|
|
423
|
+
for (const header of ctx._otherInternal.setHeaderFnAccumulator) {
|
|
424
|
+
response.setHeader(header.key, header.value);
|
|
425
|
+
}
|
|
426
|
+
for (const header of ctx._otherInternal.addHeaderFnAccumulator) {
|
|
427
|
+
response.addHeader(header.key, header.value);
|
|
428
|
+
}
|
|
429
|
+
this.applyCors(ctx, response, false);
|
|
430
|
+
return await finalizeResponse(ctx, response, this.finalizeOptions, ctx._otherInternal.eventFormat);
|
|
319
431
|
}
|
|
320
432
|
catch (err) {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
433
|
+
const wrappedError = err instanceof Error ? err : new Error("Error: " + String(err));
|
|
434
|
+
try {
|
|
435
|
+
if (this.globalErrorHandler) {
|
|
436
|
+
const responseBuilder = this.getResponseBuilder(ctx ?? undefined);
|
|
437
|
+
const errorResponse = await this.globalErrorHandler(wrappedError, ctx, responseBuilder, ctx?._otherInternal.logToApiResponseAccumulator);
|
|
438
|
+
return await finalizeResponse(ctx, errorResponse, this.finalizeOptions, ctx?._otherInternal.eventFormat ?? "v1");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
catch (handlerErr) {
|
|
442
|
+
if (handlerErr instanceof LambderResponse) {
|
|
443
|
+
try {
|
|
444
|
+
return await finalizeResponse(ctx, handlerErr, this.finalizeOptions, ctx?._otherInternal.eventFormat ?? "v1");
|
|
445
|
+
}
|
|
446
|
+
catch { /* fall through */ }
|
|
447
|
+
}
|
|
325
448
|
}
|
|
326
|
-
return { statusCode: 500, body: "Internal Server Error.", };
|
|
449
|
+
return { statusCode: 500, multiValueHeaders: {}, body: "Internal Server Error.", isBase64Encoded: false };
|
|
327
450
|
}
|
|
328
451
|
}
|
|
329
452
|
}
|
|
453
|
+
/** Rebuild the query string from the API Gateway event for redirects. */
|
|
454
|
+
const buildQueryString = (ctx) => {
|
|
455
|
+
if (isV2HttpEvent(ctx.event)) {
|
|
456
|
+
return ctx.event.rawQueryString ? `?${ctx.event.rawQueryString}` : "";
|
|
457
|
+
}
|
|
458
|
+
const multi = ctx.event.multiValueQueryStringParameters;
|
|
459
|
+
const single = ctx.event.queryStringParameters;
|
|
460
|
+
const params = new URLSearchParams();
|
|
461
|
+
if (multi) {
|
|
462
|
+
for (const [key, values] of Object.entries(multi)) {
|
|
463
|
+
for (const value of values ?? [])
|
|
464
|
+
params.append(key, value);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
else if (single) {
|
|
468
|
+
for (const [key, value] of Object.entries(single)) {
|
|
469
|
+
if (value !== undefined)
|
|
470
|
+
params.append(key, value);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const queryString = params.toString();
|
|
474
|
+
return queryString ? `?${queryString}` : "";
|
|
475
|
+
};
|
package/dist/LambderCaller.js
CHANGED
|
@@ -49,8 +49,11 @@ export default class LambderCaller {
|
|
|
49
49
|
const token = Cookies.get(this.sessionCsrfCookieKey) || "";
|
|
50
50
|
const siteHost = window.location.hostname;
|
|
51
51
|
let data = await fetch(this.apiPath, {
|
|
52
|
-
method: 'POST',
|
|
53
|
-
|
|
52
|
+
method: 'POST', cache: 'no-cache',
|
|
53
|
+
// Cross-origin API hosts need CORS mode and included credentials.
|
|
54
|
+
mode: this.isCorsEnabled ? 'cors' : 'same-origin',
|
|
55
|
+
credentials: this.isCorsEnabled ? 'include' : 'same-origin',
|
|
56
|
+
redirect: 'follow', referrerPolicy: 'origin',
|
|
54
57
|
headers: { 'Content-Type': 'application/json', ...(headers || {}) },
|
|
55
58
|
body: JSON.stringify({ apiName, version, token, siteHost, payload, }),
|
|
56
59
|
}).then(async (res) => {
|
|
@@ -105,7 +108,7 @@ export default class LambderCaller {
|
|
|
105
108
|
await this.sessionExpiredHandler();
|
|
106
109
|
}
|
|
107
110
|
else if (this.errorHandler) {
|
|
108
|
-
await this.errorHandler(new Error("
|
|
111
|
+
await this.errorHandler(new Error("Session Expired; Please log in again;"));
|
|
109
112
|
}
|
|
110
113
|
return null;
|
|
111
114
|
}
|