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/Readme.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Lambder - Serverless NodeJS Web Framework (
|
|
1
|
+
# Lambder - Serverless NodeJS Web Framework (v3)
|
|
2
2
|
|
|
3
3
|
Lambder is a highly opinionated dynamic serverless framework designed to facilitate the management and implementation of routes and APIs within AWS Lambda functions, specifically tailored for TypeScript projects. It provides a streamlined approach to handling HTTP requests, managing sessions, and defining API routes, making serverless application development more intuitive and structured.
|
|
4
4
|
|
|
5
|
-
**New in
|
|
5
|
+
**New in v3:** Public file serving with `servePublicFiles()` + `serveIndexHtml()`, unified `addAction()` for non-HTTP triggers, automatic gzip + ETag, thrown responses with a real `die`, the comment-based `LambderTemplatingEngine`, type-safe `html`/`xml` tagged templates, and API Gateway HTTP API (payload v2) / Lambda Function URL support.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -12,7 +12,7 @@ Lambder is a highly opinionated dynamic serverless framework designed to facilit
|
|
|
12
12
|
- **Session Management**: Built-in session management to secure and personalize user experiences.
|
|
13
13
|
- **Flexible Hooks System**: Employ hooks to execute code at different stages of the request lifecycle.
|
|
14
14
|
- **Error Handling**: Comprehensive error handling capabilities, including global error handlers and route-specific fallbacks.
|
|
15
|
-
- **Seamless Integration**:
|
|
15
|
+
- **Seamless Integration**: Works with API Gateway REST APIs (payload v1), HTTP APIs (payload v2) and Lambda Function URLs; the payload format is detected per event.
|
|
16
16
|
|
|
17
17
|
## Installation
|
|
18
18
|
|
|
@@ -43,6 +43,7 @@ lambder
|
|
|
43
43
|
tableRegion: "us-east-1",
|
|
44
44
|
sessionSalt: "CHANGE-THIS-TO-A-SECURE-RANDOM-STRING"
|
|
45
45
|
})
|
|
46
|
+
// true allows any origin; or configure: { origins: ["https://app.example.com"], credentials: true }
|
|
46
47
|
.enableCors(true);
|
|
47
48
|
|
|
48
49
|
// Define type-safe APIs with Zod schemas
|
|
@@ -97,11 +98,17 @@ lambder
|
|
|
97
98
|
.addRoute((ctx)=>ctx.path === '/hello-fn-route', (ctx, res) => {
|
|
98
99
|
return res.html("Hello from a function route");
|
|
99
100
|
})
|
|
100
|
-
// Match
|
|
101
|
-
.addRoute("/
|
|
102
|
-
return res.
|
|
101
|
+
// Match on method/host with a structured matcher
|
|
102
|
+
.addRoute({ path: "/stripe-webhook", method: "POST" }, (ctx, res) => {
|
|
103
|
+
return res.json({ received: true });
|
|
103
104
|
})
|
|
104
|
-
//
|
|
105
|
+
// Serve real files from publicPath. This is a terminal fallback slot, NOT
|
|
106
|
+
// a catch-all route, so it can never shadow routes registered after it.
|
|
107
|
+
.servePublicFiles()
|
|
108
|
+
// Serve the app shell for GET/HEAD page requests nothing else handled
|
|
109
|
+
// (see "Hosting a frontend build" below).
|
|
110
|
+
.serveIndexHtml()
|
|
111
|
+
// Set a fallback handler for whatever remains
|
|
105
112
|
.setRouteFallbackHandler((ctx, res) => {
|
|
106
113
|
return res.status404("Not Found");
|
|
107
114
|
})
|
|
@@ -111,7 +118,7 @@ lambder
|
|
|
111
118
|
})
|
|
112
119
|
// Handle Zod validation errors for API inputs
|
|
113
120
|
.setApiInputValidationErrorHandler((ctx, res, zodError) => {
|
|
114
|
-
return res.api(null, { errorMessage: zodError.
|
|
121
|
+
return res.api(null, { errorMessage: zodError.issues });
|
|
115
122
|
})
|
|
116
123
|
// Global error handler
|
|
117
124
|
.setGlobalErrorHandler((err, ctx, res) => {
|
|
@@ -172,6 +179,40 @@ export type ApiContractType = typeof lambder.ApiContract;
|
|
|
172
179
|
```
|
|
173
180
|
|
|
174
181
|
|
|
182
|
+
### Actions (addAction)
|
|
183
|
+
|
|
184
|
+
The same Lambda often also receives non-HTTP invocations: EventBridge/CloudWatch schedules, custom events, SQS batches. `addAction(filter, action)` registers a handler whose filter sees the **raw Lambda event** (always) and the **HTTP context** (`ctx`, or `null` for non-HTTP invocations). `getHandler()` dispatches everything.
|
|
185
|
+
|
|
186
|
+
```typescript
|
|
187
|
+
lambder
|
|
188
|
+
// Non-HTTP trigger: filter on the raw event (one plain function, no DSL)
|
|
189
|
+
.addAction(
|
|
190
|
+
(event) => (event as { source?: string })?.source === "app.reconciliation",
|
|
191
|
+
async (event, { lambdaContext }) => {
|
|
192
|
+
await reconcileEverything();
|
|
193
|
+
return { reconciled: true };
|
|
194
|
+
},
|
|
195
|
+
)
|
|
196
|
+
// Type-guard filters give a typed event in the handler
|
|
197
|
+
.addAction(
|
|
198
|
+
(event): event is ScheduledEvent => isScheduledEvent(event),
|
|
199
|
+
async (event) => runMaintenance(),
|
|
200
|
+
)
|
|
201
|
+
// HTTP interception: ctx is present, and the action must return a response via tools.res
|
|
202
|
+
.addAction(
|
|
203
|
+
(event, ctx) => ctx !== null && ctx.host.endsWith("dev.example.com") && ctx.cookie.dev !== "atlas",
|
|
204
|
+
async (event, { res }) => res!.status404("Not found"),
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
export const handler = lambder.getHandler();
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Semantics:
|
|
211
|
+
- The handler's second argument is `{ ctx, res, lambdaContext }`, discriminated on `ctx`: both `ctx` and `res` are non-null for HTTP invocations and `null` otherwise, so `if (tools.ctx)` narrows both
|
|
212
|
+
- **HTTP invocations**: actions join the same first-match chain as routes/APIs (registration order) and must return a response built with `tools.res`
|
|
213
|
+
- **Non-HTTP invocations**: actions are the only handlers; return values pass through to Lambda untouched (e.g. `{ batchItemFailures }` for SQS) and errors **rethrow** (never routed to `setGlobalErrorHandler`), preserving Lambda-native retry/DLQ semantics
|
|
214
|
+
- A trailing `.addAction(() => true, handler)` acts as the fallback for unmatched non-HTTP events; with no match at all, a descriptive error is thrown
|
|
215
|
+
|
|
175
216
|
### Hooks
|
|
176
217
|
|
|
177
218
|
Lambder provides hooks to execute code at different stages of the request lifecycle.
|
|
@@ -182,7 +223,7 @@ lambder
|
|
|
182
223
|
.addHook("beforeRender", async (ctx, res) => {
|
|
183
224
|
// Perform actions before rendering
|
|
184
225
|
console.log("Request received:", ctx.path);
|
|
185
|
-
return ctx; // Return modified
|
|
226
|
+
return ctx; // Return the (modified) ctx to continue, a response to short-circuit, or throw an Error
|
|
186
227
|
})
|
|
187
228
|
// After render hook
|
|
188
229
|
.addHook("afterRender", async (ctx, res, response) => {
|
|
@@ -235,30 +276,104 @@ Access the session controller with `lambder.getSessionController(ctx)`:
|
|
|
235
276
|
| `endSessionAll()` | End all sessions for this sessionKey (all devices) |
|
|
236
277
|
| `regenerateSession()` | Regenerate token (use after password change) |
|
|
237
278
|
|
|
238
|
-
###
|
|
279
|
+
### Type-Safe Templating (html / xml)
|
|
280
|
+
|
|
281
|
+
Lambder ships zero-dependency tagged template literals instead of a template engine. Interpolated values are HTML-escaped automatically, and everything is plain TypeScript, so templates are fully type-checked and refactorable.
|
|
282
|
+
|
|
283
|
+
```typescript
|
|
284
|
+
import { html, xml, raw } from "lambder";
|
|
285
|
+
|
|
286
|
+
// Values are escaped by default (XSS-safe):
|
|
287
|
+
const page = html`<h1>Hello ${user.name}</h1>`;
|
|
288
|
+
|
|
289
|
+
// Arrays flatten; nested fragments are not double-escaped:
|
|
290
|
+
const list = html`<ul>${items.map((item) => html`<li>${item.label}</li>`)}</ul>`;
|
|
291
|
+
|
|
292
|
+
// Conditionals: null/undefined/false render as empty string:
|
|
293
|
+
const nav = html`${isLoggedIn && html`<a href="/logout">Log out</a>`}`;
|
|
294
|
+
|
|
295
|
+
// raw() inserts trusted markup verbatim (never pass user input):
|
|
296
|
+
const head = html`${raw('<meta charset="utf-8">')}`;
|
|
297
|
+
|
|
298
|
+
// Works for XML too (xml is an alias of html):
|
|
299
|
+
return res.xml(xml`<?xml version="1.0" encoding="UTF-8"?>
|
|
300
|
+
<urlset>${urls.map((loc) => xml`<url><loc>${loc}</loc></url>`)}</urlset>`);
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Templating with LambderTemplatingEngine
|
|
239
304
|
|
|
240
|
-
|
|
305
|
+
`LambderTemplatingEngine` is a standalone, comment-only HTML template engine. Every construct is an HTML comment, so templates survive HTML build pipelines (e.g. Vite) untouched, and during frontend development the browser simply renders the default content because the markers are invisible. It can template anything: SPA shells, emails, error pages.
|
|
241
306
|
|
|
242
|
-
|
|
243
|
-
- **Partial**: Included from a template with `<%- await include('partial/header.html.ejs', partialData) -%>`. Has both `page` and `partial` variables.
|
|
307
|
+
**Syntax** (everything is an HTML comment):
|
|
244
308
|
|
|
245
|
-
Example template:
|
|
246
309
|
```html
|
|
247
|
-
<
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
</div>
|
|
310
|
+
<title><!--slot:title-->Default Title<!--/slot:title--></title> <!-- replaceable region -->
|
|
311
|
+
<!--slot:head/--> <!-- insert-only point -->
|
|
312
|
+
<!--if:isRtl--><body dir="rtl"><!--else--><body><!--/if:isRtl--> <!-- conditional -->
|
|
313
|
+
<!--if:!minimal--><nav>...</nav><!--/if:!minimal--> <!-- negated conditional -->
|
|
252
314
|
```
|
|
253
315
|
|
|
254
|
-
|
|
316
|
+
**Usage** (standalone, importable directly from `lambder`):
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { LambderTemplatingEngine, html, jsonScript } from "lambder";
|
|
320
|
+
|
|
321
|
+
// Compile once (throws early on unclosed/mismatched blocks) ...
|
|
322
|
+
const template = await LambderTemplatingEngine.fromFile("./templates/page.html");
|
|
323
|
+
// ... render many times, per request:
|
|
324
|
+
const output = template.render({
|
|
325
|
+
title: userInput, // plain values are escaped (XSS-safe)
|
|
326
|
+
head: html`<link rel="canonical" href="${canonicalUrl}" />
|
|
327
|
+
${jsonScript("app-data", preloadedState)}`, // html`...`/raw()/jsonScript() inserted verbatim
|
|
328
|
+
isRtl: lang === "ar", // condition names use truthiness
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
// Runtime introspection (dynamically typed by design):
|
|
332
|
+
template.slotNames; // e.g. ["title", "head"]
|
|
333
|
+
template.conditionNames; // e.g. ["isRtl", "minimal"]
|
|
334
|
+
template.has("title"); // true
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Rules:
|
|
338
|
+
- Slot values: strings/numbers escaped; `html`/`raw()`/`jsonScript()` verbatim; arrays flattened; `null`/`undefined`/`false` keep the slot's default content
|
|
339
|
+
- Unknown data keys are ignored, so one data object can serve several templates with different slots
|
|
340
|
+
- Blocks nest freely; there are intentionally no loops or inline expressions: build dynamic lists server-side with `html` and pass them into a slot
|
|
341
|
+
- Attribute-position values (e.g. `<html lang="...">`) are handled with if/else around whole-tag variants
|
|
342
|
+
|
|
343
|
+
### Hosting a frontend build (servePublicFiles + templateFile)
|
|
344
|
+
|
|
345
|
+
Lambder has no SPA-specific machinery; hosting a frontend build is a recipe built from three generic primitives:
|
|
346
|
+
|
|
347
|
+
1. **`servePublicFiles(options?)`**: a terminal slot that serves real files under `publicPath`. It runs only when no route or API matched, so unlike a `"/(.*)"` catch-all route it can never shadow routes registered after it. Traversal-safe, mime-typed, memory-cached for warm invocations, immutable Cache-Control for content-hashed assets (`app-4f8a1b2c.js`), automatic ETag/gzip. When the file does not exist, the request **falls through**.
|
|
348
|
+
2. **`serveIndexHtml(handler?, options?)`**: the next slot in the fallback chain, gated by a built-in filter: only `GET`/`HEAD` (option `methods`) and, by default, only paths that do not look like files (`skipFilePaths: true`, so a missing `/logo.png` is a 404, not a soft-404 HTML shell). Optional `redirectTrailingSlash` (default false) 301s `/about/` to `/about`. Gated-out requests fall through to `setRouteFallbackHandler`. Without a handler it serves `publicPath/index.html` (option `indexFile`) via `res.templateFile` with `no-cache`, so plain hosting is zero-config and templating is opt-in.
|
|
349
|
+
3. **`res.templateFile(path, data?, options?)`**: render any HTML file under `publicPath` through `LambderTemplatingEngine` (compiled once, cached across warm invocations) and return it as an HTML response.
|
|
350
|
+
|
|
255
351
|
```html
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
</div>
|
|
352
|
+
<!-- frontend index.html (markers survive the Vite build; defaults show in vite dev) -->
|
|
353
|
+
<title><!--slot:title-->My App<!--/slot:title--></title>
|
|
354
|
+
<!--slot:head/-->
|
|
260
355
|
```
|
|
261
356
|
|
|
357
|
+
```typescript
|
|
358
|
+
lambder
|
|
359
|
+
// Multi-tenant roots are just app logic in the path mapper:
|
|
360
|
+
.servePublicFiles({ path: (ctx) => `${getBrandFromHost(ctx.host)}${ctx.path}` })
|
|
361
|
+
// Only GET/HEAD page requests reach this handler:
|
|
362
|
+
.serveIndexHtml(async (ctx, res) => {
|
|
363
|
+
return res.templateFile(`${getBrandFromHost(ctx.host)}/index.html`, {
|
|
364
|
+
title: pageTitle(ctx), // escaped automatically
|
|
365
|
+
head: html`<link rel="canonical" href="${canonicalUrl(ctx)}" />
|
|
366
|
+
${jsonScript("app-data", preloadedState(ctx))}`,
|
|
367
|
+
isRtl: activeLang(ctx) === "ar",
|
|
368
|
+
}, { cacheControl: "no-cache" });
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
// Or, zero-config for a single-tenant app without templating:
|
|
372
|
+
lambder.servePublicFiles().serveIndexHtml();
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Files without template markers can opt into virtual slots (`title` = the `<title>` element, `head` = before `</head>`) with `res.templateFile(path, data, { htmlVirtualSlots: true })`. File cache policies (`cacheControl`, `immutablePattern`, `memoryCache`) are configurable via `LambderPublicFilesOptions`, and both slots take an explicit compression policy: `servePublicFiles({ compress: (ctx) => /\.(css|js|svg)$/.test(ctx.path) })` and `serveIndexHtml(handler, { compress: true })` (default "auto").
|
|
376
|
+
|
|
262
377
|
### Render Context (ctx) Variables
|
|
263
378
|
|
|
264
379
|
The `ctx` object provides access to request data:
|
|
@@ -271,9 +386,12 @@ The `ctx` object provides access to request data:
|
|
|
271
386
|
| `method` | HTTP method | `"GET"`, `"POST"` |
|
|
272
387
|
| `get` | Query parameters | `{ page: "1" }` |
|
|
273
388
|
| `post` | POST body (parsed) | `{ name: "John" }` |
|
|
389
|
+
| `rawBody` | Decoded request body as received (webhook signatures) | `'{"a":1}'` |
|
|
390
|
+
| `ip` | Client IP (CF-Connecting-IP / X-Forwarded-For / source IP) | `"1.2.3.4"` |
|
|
391
|
+
| `header(name)` | Case-insensitive request header lookup | `ctx.header("accept-language")` |
|
|
274
392
|
| `cookie` | Cookies | `{ rememberMe: "true" }` |
|
|
275
393
|
| `headers` | Request headers | `{ "Content-Type": "..." }` |
|
|
276
|
-
| `event` | Raw APIGatewayProxyEvent | - |
|
|
394
|
+
| `event` | Raw Lambda event (APIGatewayProxyEvent or APIGatewayProxyEventV2) | - |
|
|
277
395
|
| `lambdaContext` | AWS Lambda Context | - |
|
|
278
396
|
| `apiName` | API name (for API calls) | `"getUser"` |
|
|
279
397
|
| `apiPayload` | Validated input | `{ userId: "123" }` |
|
|
@@ -286,27 +404,29 @@ The `ctx` object provides access to request data:
|
|
|
286
404
|
- `res.setHeader(key, value)` - Sets a header (replaces existing values)
|
|
287
405
|
- `res.logToApiResponse(data)` - Adds data to logList in API responses (debugging)
|
|
288
406
|
|
|
289
|
-
**Response Methods
|
|
407
|
+
**Response Methods** (all accept an options object: `{ statusCode?, headers?, cacheControl?, compress?, etag? }`):
|
|
290
408
|
|
|
291
409
|
| Method | Description |
|
|
292
410
|
|--------|-------------|
|
|
293
|
-
| `res.raw(
|
|
294
|
-
| `res.json(data,
|
|
295
|
-
| `res.
|
|
296
|
-
| `res.
|
|
297
|
-
| `res.
|
|
298
|
-
| `res.
|
|
299
|
-
| `res.
|
|
300
|
-
| `res.
|
|
301
|
-
| `res.
|
|
302
|
-
| `await res.
|
|
303
|
-
| `await res.
|
|
304
|
-
| `res.api(payload, config?,
|
|
305
|
-
| `res.apiBinary(payload, config?,
|
|
411
|
+
| `res.raw(init)` | Custom HTTP response |
|
|
412
|
+
| `res.json(data, options?)` | JSON response |
|
|
413
|
+
| `res.text(data, options?)` | Plain text response |
|
|
414
|
+
| `res.xml(data, options?)` | XML response (accepts xml\`...\` templates) |
|
|
415
|
+
| `res.html(data, options?)` | HTML response (accepts html\`...\` templates) |
|
|
416
|
+
| `res.status(code, body?, options?)` | Response with any status code |
|
|
417
|
+
| `res.redirect(url, statusCode?, options?)` | Redirect (default: 302) |
|
|
418
|
+
| `res.status404(data, options?)` | 404 Not Found response |
|
|
419
|
+
| `res.fileBase64(base64, mimeType, options?)` | File from base64 content |
|
|
420
|
+
| `await res.file(path, options? & { fallback? })` | Serve file from public directory (404 when missing) |
|
|
421
|
+
| `await res.templateFile(path, data?, options?)` | Render an HTML file via LambderTemplatingEngine (cached; throws when missing) |
|
|
422
|
+
| `res.api(payload, config?, options?)` | Standardized API response |
|
|
423
|
+
| `res.apiBinary(payload, config?, options?)` | API response with forced gzip |
|
|
424
|
+
|
|
425
|
+
Responses are finalized once at the end of the request: automatic gzip (when the client accepts it, the body is compressible and large enough), automatic ETag + `If-None-Match` 304 handling on GET/HEAD, and a clear error if the body would exceed Lambda's ~6MB cap. Override per response with `compress: true | false` and `etag: false`.
|
|
306
426
|
|
|
307
427
|
**API Config Options**: `{ notAuthorized, message, errorMessage, versionExpired, sessionExpired, logList }`
|
|
308
428
|
|
|
309
|
-
**Die Methods**: `res.die.*` -
|
|
429
|
+
**Die Methods**: `res.die.*` - Builds the response and throws it, immediately halting the request at any call depth (handlers, hooks, nested helper functions). Plain `throw res.html(...)` works the same way.
|
|
310
430
|
|
|
311
431
|
## Frontend Usage with LambderCaller
|
|
312
432
|
|
|
@@ -362,6 +482,7 @@ import type { ApiContractType } from './backend/handler';
|
|
|
362
482
|
|
|
363
483
|
const lambderMSW = new LambderMSW<ApiContractType>({
|
|
364
484
|
apiPath: '/api',
|
|
485
|
+
msw: await import('msw'),
|
|
365
486
|
});
|
|
366
487
|
|
|
367
488
|
const handlers = [
|
|
@@ -398,4 +519,4 @@ Contributions are welcome! Especially for documentation. If you have an idea for
|
|
|
398
519
|
|
|
399
520
|
## License
|
|
400
521
|
|
|
401
|
-
This project is licensed under the [MIT License](
|
|
522
|
+
This project is licensed under the [MIT License](License.md).
|
package/dist/Lambder.d.ts
CHANGED
|
@@ -1,23 +1,75 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type {
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
import type { Context } from "aws-lambda";
|
|
3
3
|
import LambderResolver from "./LambderResolver.js";
|
|
4
|
-
import LambderResponseBuilder
|
|
5
|
-
import
|
|
6
|
-
import
|
|
4
|
+
import LambderResponseBuilder from "./LambderResponseBuilder.js";
|
|
5
|
+
import { LambderResponse, type LambderHttpResponse } from "./LambderResponse.js";
|
|
6
|
+
import { type ConditionFunction, type LambderRouteMatcher, type PathParamsOf } from "./LambderRouting.js";
|
|
7
|
+
import { type LambderCorsConfig } from "./LambderCors.js";
|
|
8
|
+
import LambderSessionController, { type LambderSessionCookieOptions } from "./LambderSessionController.js";
|
|
9
|
+
import { type LambderPublicFilesOptions } from "./LambderPublicFiles.js";
|
|
7
10
|
import type { MergeContract } from "./LambderApiContract.js";
|
|
8
|
-
import { type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
|
|
11
|
+
import { type LambderHttpEvent, type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
|
|
12
|
+
export type { PathParamsOf, RouteCondition, ConditionFunction, LambderRouteMatcher } from "./LambderRouting.js";
|
|
13
|
+
export type { LambderCorsConfig } from "./LambderCors.js";
|
|
14
|
+
type MaybePromise<T> = T | Promise<T>;
|
|
9
15
|
type Path = `/${string}`;
|
|
10
|
-
type
|
|
11
|
-
type
|
|
12
|
-
type
|
|
13
|
-
|
|
14
|
-
type HookBeforeRenderFunction = (ctx: LambderRenderContext
|
|
15
|
-
type HookAfterRenderFunction = (ctx: LambderRenderContext
|
|
16
|
-
type HookFallbackFunction = (ctx: LambderRenderContext
|
|
17
|
-
type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext
|
|
18
|
-
type
|
|
19
|
-
type
|
|
20
|
-
type
|
|
16
|
+
type ActionFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => MaybePromise<LambderResponse>;
|
|
17
|
+
type SessionActionFunction<SessionData = any> = (ctx: LambderSessionRenderContext<any, SessionData>, resolver: LambderResolver) => MaybePromise<LambderResponse>;
|
|
18
|
+
type HookCreatedFunction = (lambderInstance: Lambder<any, any>) => void | Promise<void>;
|
|
19
|
+
/** Return the (possibly replaced) ctx to continue, a LambderResponse to short-circuit, or an Error to fail. */
|
|
20
|
+
type HookBeforeRenderFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => MaybePromise<LambderRenderContext | LambderResponse | Error>;
|
|
21
|
+
type HookAfterRenderFunction = (ctx: LambderRenderContext, resolver: LambderResolver, response: LambderResponse) => MaybePromise<LambderResponse | Error>;
|
|
22
|
+
type HookFallbackFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => void | Promise<void>;
|
|
23
|
+
type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext | null, response: LambderResponseBuilder, logListToApiResponse?: any[]) => MaybePromise<LambderResponse>;
|
|
24
|
+
type FallbackHandlerFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => MaybePromise<LambderResponse>;
|
|
25
|
+
type ApiInputValidationErrorHandlerFunction = (ctx: LambderRenderContext, resolver: LambderResolver, zodError: z.ZodError) => MaybePromise<LambderResponse>;
|
|
26
|
+
export type LambderIndexHtmlOptions = {
|
|
27
|
+
/** Methods that reach the index handler. Default: ["GET", "HEAD"]. */
|
|
28
|
+
methods?: string[];
|
|
29
|
+
/**
|
|
30
|
+
* Skip paths whose last segment has an extension (missing assets by this
|
|
31
|
+
* point; a 200 HTML shell would be a soft-404). Default: true.
|
|
32
|
+
*/
|
|
33
|
+
skipFilePaths?: boolean;
|
|
34
|
+
/** 301-redirect trailing-slash paths to the canonical no-slash URL. Default: false. */
|
|
35
|
+
redirectTrailingSlash?: boolean;
|
|
36
|
+
/** Shell served by the default handler. Default: "index.html". */
|
|
37
|
+
indexFile?: string | ((ctx: LambderRenderContext) => string);
|
|
38
|
+
/** Compression override, like servePublicFiles: "auto" (default), true/false, or (ctx) => boolean | "auto". */
|
|
39
|
+
compress?: boolean | "auto" | ((ctx: LambderRenderContext) => boolean | "auto");
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Second argument of an addAction handler. Discriminated on `ctx`: HTTP
|
|
43
|
+
* invocations get the full context and a resolver, non-HTTP invocations get
|
|
44
|
+
* null for both.
|
|
45
|
+
*/
|
|
46
|
+
export type LambderActionTools = {
|
|
47
|
+
ctx: LambderRenderContext;
|
|
48
|
+
res: LambderResolver;
|
|
49
|
+
lambdaContext: Context;
|
|
50
|
+
} | {
|
|
51
|
+
ctx: null;
|
|
52
|
+
res: null;
|
|
53
|
+
lambdaContext: Context;
|
|
54
|
+
};
|
|
55
|
+
/** Overloaded handler type returned by getHandler(): HTTP events get a typed response, others dispatch to actions. */
|
|
56
|
+
export type LambderHandler = {
|
|
57
|
+
(event: LambderHttpEvent, context: Context): Promise<LambderHttpResponse>;
|
|
58
|
+
(event: unknown, context: Context): Promise<unknown>;
|
|
59
|
+
};
|
|
60
|
+
export type LambderConstructorOptions = {
|
|
61
|
+
publicPath?: string;
|
|
62
|
+
apiPath?: string;
|
|
63
|
+
apiVersion?: string;
|
|
64
|
+
/** Automatic gzip for compressible responses. Default: { minBytes: 860 }. Set false to disable. */
|
|
65
|
+
compression?: false | {
|
|
66
|
+
minBytes?: number;
|
|
67
|
+
};
|
|
68
|
+
/** Automatic ETag + If-None-Match 304 on GET/HEAD 200 responses. Default: true. */
|
|
69
|
+
etag?: boolean;
|
|
70
|
+
/** Guard threshold for Lambda's ~6MB response cap. Default: 5,500,000. */
|
|
71
|
+
maxResponseBytes?: number;
|
|
72
|
+
};
|
|
21
73
|
/**
|
|
22
74
|
* Main Lambder class for building type-safe serverless APIs
|
|
23
75
|
*
|
|
@@ -36,9 +88,7 @@ type ApiInputValidationErrorHandlerFunction = (ctx: LambderRenderContext<any>, r
|
|
|
36
88
|
export default class Lambder<TSessionData = any, _TContract extends Record<string, any> = {}> {
|
|
37
89
|
apiPath: string;
|
|
38
90
|
apiVersion: null | string;
|
|
39
|
-
isCorsEnabled: boolean;
|
|
40
91
|
publicPath: string;
|
|
41
|
-
ejsPath: string;
|
|
42
92
|
/**
|
|
43
93
|
* Type property for extracting the API contract
|
|
44
94
|
* Use this to export your API types to the frontend
|
|
@@ -52,57 +102,115 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
52
102
|
readonly ApiContract: _TContract;
|
|
53
103
|
private actionList;
|
|
54
104
|
private hookList;
|
|
105
|
+
private createdHooks;
|
|
106
|
+
private initPromise;
|
|
55
107
|
private globalErrorHandler;
|
|
56
108
|
private routeFallbackHandler;
|
|
57
109
|
private apiFallbackHandler;
|
|
58
110
|
private apiInputValidationErrorHandler;
|
|
59
|
-
|
|
111
|
+
private sessionExpiredRouteHandler;
|
|
112
|
+
private publicFilesHandler;
|
|
113
|
+
private indexHtmlConfig;
|
|
114
|
+
private eventActionList;
|
|
115
|
+
private corsConfig;
|
|
116
|
+
private finalizeOptions;
|
|
60
117
|
private lambderSessionManager?;
|
|
118
|
+
private sessionCookieOptions;
|
|
61
119
|
private sessionTokenCookieKey;
|
|
62
120
|
private sessionCsrfCookieKey;
|
|
63
|
-
constructor(
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
ejsPath?: string;
|
|
67
|
-
apiVersion?: string;
|
|
68
|
-
});
|
|
69
|
-
enableCors(isCorsEnabled: boolean): this;
|
|
70
|
-
enableDdbSession({ tableName, tableRegion, sessionSalt, enableSlidingExpiration }: {
|
|
121
|
+
constructor(options?: LambderConstructorOptions);
|
|
122
|
+
enableCors(config: boolean | LambderCorsConfig): this;
|
|
123
|
+
enableDdbSession({ tableName, tableRegion, sessionSalt, enableSlidingExpiration, slidingWriteIntervalSeconds, cookie, partitionKey, sortKey, }: {
|
|
71
124
|
tableName: string;
|
|
72
125
|
tableRegion: string;
|
|
73
126
|
sessionSalt: string;
|
|
74
127
|
enableSlidingExpiration?: boolean;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
128
|
+
/** Min seconds between sliding-expiration writes. Default: max(60, 5% of TTL). */
|
|
129
|
+
slidingWriteIntervalSeconds?: number;
|
|
130
|
+
/** Session cookie attributes, e.g. { domain: ".example.com" } for cross-subdomain sessions. */
|
|
131
|
+
cookie?: LambderSessionCookieOptions;
|
|
132
|
+
partitionKey?: string;
|
|
133
|
+
sortKey?: string;
|
|
78
134
|
}): this;
|
|
79
135
|
setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): this;
|
|
80
|
-
setRouteFallbackHandler(routeFallbackHandler:
|
|
81
|
-
setApiFallbackHandler(apiFallbackHandler:
|
|
136
|
+
setRouteFallbackHandler(routeFallbackHandler: FallbackHandlerFunction): this;
|
|
137
|
+
setApiFallbackHandler(apiFallbackHandler: FallbackHandlerFunction): this;
|
|
82
138
|
setApiInputValidationErrorHandler(apiInputValidationErrorHandler: ApiInputValidationErrorHandlerFunction): this;
|
|
83
139
|
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction): this;
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
140
|
+
/** Response for session routes when the session is missing/expired (non-API). Default: 401. */
|
|
141
|
+
setSessionExpiredRouteHandler(handler: FallbackHandlerFunction): this;
|
|
142
|
+
/**
|
|
143
|
+
* Terminal public-file layer. Runs only when no route matched, so it can
|
|
144
|
+
* never shadow routes registered after it. Serves real files under
|
|
145
|
+
* publicPath (traversal-safe, mime-typed, memory-cached, immutable-cache
|
|
146
|
+
* heuristic for content-hashed assets); when the file does not exist the
|
|
147
|
+
* request falls through to setRouteFallbackHandler, where the app decides
|
|
148
|
+
* what remains (e.g. render an app shell with res.templateFile).
|
|
149
|
+
*/
|
|
150
|
+
servePublicFiles(options?: LambderPublicFilesOptions): this;
|
|
151
|
+
/**
|
|
152
|
+
* Serve the app shell for page requests that nothing else handled. Runs
|
|
153
|
+
* after servePublicFiles in the fallback chain, gated by a built-in
|
|
154
|
+
* filter: only configured methods (default GET/HEAD) and, by default, only
|
|
155
|
+
* paths that do not look like files. Gated-out requests fall through to
|
|
156
|
+
* setRouteFallbackHandler. Without a handler, publicPath/index.html is
|
|
157
|
+
* served via res.templateFile (markers optional) with no-cache.
|
|
158
|
+
*/
|
|
159
|
+
serveIndexHtml(handler?: FallbackHandlerFunction, options?: LambderIndexHtmlOptions): this;
|
|
160
|
+
/** Apply the serveIndexHtml gates; null means fall through. */
|
|
161
|
+
private tryServeIndexHtml;
|
|
162
|
+
addRoute<TPath extends Path>(condition: TPath, actionFn: (ctx: LambderRenderContext<any, PathParamsOf<TPath>>, resolver: LambderResolver) => MaybePromise<LambderResponse>): this;
|
|
163
|
+
addRoute(condition: RegExp | ConditionFunction | LambderRouteMatcher, actionFn: ActionFunction): this;
|
|
164
|
+
addSessionRoute<TPath extends Path>(condition: TPath, actionFn: (ctx: LambderSessionRenderContext<any, TSessionData, PathParamsOf<TPath>>, resolver: LambderResolver) => MaybePromise<LambderResponse>): this;
|
|
165
|
+
addSessionRoute(condition: RegExp | ConditionFunction | LambderRouteMatcher, actionFn: SessionActionFunction<TSessionData>): this;
|
|
89
166
|
use<_TNewContract extends Record<string, any>>(plugin: (lambder: Lambder<TSessionData, _TContract>) => Lambder<TSessionData, _TNewContract>): Lambder<TSessionData, _TNewContract extends _TContract ? _TNewContract : (_TContract & _TNewContract)>;
|
|
90
167
|
addApi<TName extends string, TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny>(name: TName, schema: {
|
|
91
168
|
input: TInput;
|
|
92
169
|
output: TOutput;
|
|
93
|
-
}, handler: (ctx: LambderRenderContext<z.infer<TInput>>, resolver: LambderResolver<z.infer<TOutput>>) =>
|
|
170
|
+
}, handler: (ctx: LambderRenderContext<z.infer<TInput>>, resolver: LambderResolver<z.infer<TOutput>>) => MaybePromise<LambderResponse>): Lambder<TSessionData, MergeContract<_TContract, TName, z.infer<TInput>, z.infer<TOutput>>>;
|
|
94
171
|
addSessionApi<TName extends string, TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny>(name: TName, schema: {
|
|
95
172
|
input: TInput;
|
|
96
173
|
output: TOutput;
|
|
97
|
-
}, handler: (ctx: LambderSessionRenderContext<z.infer<TInput>, TSessionData>, resolver: LambderResolver<z.infer<TOutput>>) =>
|
|
98
|
-
|
|
174
|
+
}, handler: (ctx: LambderSessionRenderContext<z.infer<TInput>, TSessionData>, resolver: LambderResolver<z.infer<TOutput>>) => MaybePromise<LambderResponse>): Lambder<TSessionData, MergeContract<_TContract, TName, z.infer<TInput>, z.infer<TOutput>>>;
|
|
175
|
+
/**
|
|
176
|
+
* Fetch the session or short-circuit the request: API calls get the
|
|
177
|
+
* protocol's { sessionExpired: true } response (handled by LambderCaller),
|
|
178
|
+
* routes get the sessionExpiredRouteHandler response (default 401).
|
|
179
|
+
*/
|
|
180
|
+
private requireSession;
|
|
181
|
+
addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): this;
|
|
99
182
|
addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): this;
|
|
100
183
|
addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): this;
|
|
101
184
|
addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): this;
|
|
102
|
-
getSessionController(ctx: LambderRenderContext
|
|
103
|
-
getResponseBuilder(): LambderResponseBuilder<any>;
|
|
185
|
+
getSessionController(ctx: LambderRenderContext | LambderSessionRenderContext<any, TSessionData>): LambderSessionController<TSessionData>;
|
|
186
|
+
getResponseBuilder(ctx?: LambderRenderContext): LambderResponseBuilder<any>;
|
|
104
187
|
private getResolver;
|
|
105
|
-
getHandler():
|
|
106
|
-
|
|
188
|
+
getHandler(): LambderHandler;
|
|
189
|
+
/** True when the Lambda event is an API Gateway HTTP event (REST API v1 or HTTP API / Function URL v2). */
|
|
190
|
+
static isHttpEvent(event: unknown): event is LambderHttpEvent;
|
|
191
|
+
/**
|
|
192
|
+
* Register an action that filters on the raw Lambda event and, for HTTP
|
|
193
|
+
* invocations, the context (ctx is null otherwise).
|
|
194
|
+
*
|
|
195
|
+
* - Non-HTTP invocations (EventBridge/CloudWatch schedules, SQS, ...):
|
|
196
|
+
* actions are the only handlers. Return values pass through to Lambda
|
|
197
|
+
* untouched and errors rethrow, so retry/DLQ semantics keep working.
|
|
198
|
+
* A trailing `.addAction(() => true, handler)` acts as the fallback;
|
|
199
|
+
* with no match, a descriptive error is thrown.
|
|
200
|
+
* - HTTP invocations: the action joins the same first-match chain as
|
|
201
|
+
* routes/APIs (registration order) and must return a response built
|
|
202
|
+
* with tools.res.
|
|
203
|
+
*
|
|
204
|
+
* Use a type-guard filter to get a typed event:
|
|
205
|
+
* `(event): event is ScheduledEvent => ...`
|
|
206
|
+
*/
|
|
207
|
+
addAction<TEvent>(filter: (event: unknown, ctx: LambderRenderContext | null) => event is TEvent, actionFn: (event: TEvent, tools: LambderActionTools) => MaybePromise<unknown>): this;
|
|
208
|
+
addAction(filter: (event: unknown, ctx: LambderRenderContext | null) => boolean, actionFn: (event: unknown, tools: LambderActionTools) => MaybePromise<unknown>): this;
|
|
209
|
+
/** Dispatch a non-HTTP Lambda event to the registered actions. */
|
|
210
|
+
renderEvent(event: unknown, lambdaContext: Context): Promise<unknown>;
|
|
211
|
+
private ensureInitialized;
|
|
212
|
+
private applyCors;
|
|
213
|
+
private handleNoMatchedAction;
|
|
214
|
+
private resolveRequest;
|
|
215
|
+
render(event: LambderHttpEvent, lambdaContext: Context): Promise<LambderHttpResponse>;
|
|
107
216
|
}
|
|
108
|
-
export {};
|