lambder 2.0.13 → 2.0.15
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 +91 -224
- package/dist/Lambder.d.ts +1 -1
- package/dist/Lambder.js +7 -9
- package/docs/LAMBDER_MSW.md +14 -44
- package/docs/TYPE_SAFE_QUICK_START.md +4 -4
- package/examples/msw-testing-example.ts +4 -4
- package/examples/zod-chained-api-example.ts +2 -2
- package/package.json +1 -1
- package/src/Lambder.ts +1 -3
- package/tests/routes.test.ts +5 -4
- package/tests/UNTESTED_FEATURES.md +0 -263
package/Readme.md
CHANGED
|
@@ -27,17 +27,16 @@ yarn add lambder zod
|
|
|
27
27
|
### Basic Setup
|
|
28
28
|
|
|
29
29
|
```typescript
|
|
30
|
-
import Lambder
|
|
30
|
+
import Lambder from 'lambder';
|
|
31
31
|
import { z } from 'zod';
|
|
32
32
|
import * as path from 'path';
|
|
33
33
|
|
|
34
34
|
const lambder = new Lambder({
|
|
35
35
|
apiPath: "/api",
|
|
36
36
|
publicPath: path.resolve(`./public`),
|
|
37
|
-
// ejsPath: path.resolve(`./ejs-templates`), // Optional
|
|
38
37
|
});
|
|
39
38
|
|
|
40
|
-
// Enable session and CORS
|
|
39
|
+
// Enable session and CORS
|
|
41
40
|
lambder
|
|
42
41
|
.enableDdbSession({
|
|
43
42
|
tableName: "website-session",
|
|
@@ -47,7 +46,7 @@ lambder
|
|
|
47
46
|
.enableCors(true);
|
|
48
47
|
|
|
49
48
|
// Define type-safe APIs with Zod schemas
|
|
50
|
-
|
|
49
|
+
lambder
|
|
51
50
|
.addApi("getCompanyPage", {
|
|
52
51
|
input: z.object({ companyName: z.string() }),
|
|
53
52
|
output: z.object({ id: z.string(), name: z.string(), description: z.string() })
|
|
@@ -70,7 +69,7 @@ const app = lambder
|
|
|
70
69
|
});
|
|
71
70
|
|
|
72
71
|
// Export the inferred contract for the frontend
|
|
73
|
-
export type
|
|
72
|
+
export type ApiContractType = typeof lambder.ApiContract;
|
|
74
73
|
|
|
75
74
|
// Export the handler
|
|
76
75
|
export const handler = lambder.getHandler();
|
|
@@ -78,8 +77,6 @@ export const handler = lambder.getHandler();
|
|
|
78
77
|
|
|
79
78
|
### Adding Routes
|
|
80
79
|
|
|
81
|
-
Routes are fully chainable for a fluent interface.
|
|
82
|
-
|
|
83
80
|
```typescript
|
|
84
81
|
lambder
|
|
85
82
|
// Define a simple route
|
|
@@ -100,25 +97,6 @@ lambder
|
|
|
100
97
|
.addRoute((ctx)=>ctx.path === '/hello-fn-route', (ctx, res) => {
|
|
101
98
|
return res.html("Hello from a function route");
|
|
102
99
|
})
|
|
103
|
-
// Define a simple route that serves an EJS template file
|
|
104
|
-
.addRoute("/product/:productId", async (ctx, res) => {
|
|
105
|
-
const product = await getProduct(ctx.pathParams.productId);
|
|
106
|
-
// Serve the file from ejsPath defined above.
|
|
107
|
-
return await res.ejsFile("productPage.html.ejs", { product });
|
|
108
|
-
})
|
|
109
|
-
// Serve sitemap using an ejs template
|
|
110
|
-
.addRoute("/sitemap", async (ctx, res) => {
|
|
111
|
-
const templateString = `
|
|
112
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
113
|
-
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
114
|
-
<%~
|
|
115
|
-
page.urlList.map(url => \`<url><loc>\${url}</loc></url>\`).join("")
|
|
116
|
-
%>
|
|
117
|
-
</urlset>
|
|
118
|
-
`.trim();
|
|
119
|
-
const urlList = await getUrlList();
|
|
120
|
-
return await res.ejsTemplate(templateString, { urlList }, { "Content-Type": ["application/xml; charset=utf-8"]});
|
|
121
|
-
})
|
|
122
100
|
// Match all other paths and serve static files from publicPath
|
|
123
101
|
.addRoute("/(.*)", (ctx, res)=>{
|
|
124
102
|
return res.file(ctx.path, {}, "index.html");
|
|
@@ -127,6 +105,14 @@ lambder
|
|
|
127
105
|
.setRouteFallbackHandler((ctx, res) => {
|
|
128
106
|
return res.status404("Not Found");
|
|
129
107
|
})
|
|
108
|
+
// Set a fallback handler for unmatched APIs
|
|
109
|
+
.setApiFallbackHandler((ctx, res) => {
|
|
110
|
+
return res.api(null, { errorMessage: "API not found" });
|
|
111
|
+
})
|
|
112
|
+
// Handle Zod validation errors for API inputs
|
|
113
|
+
.setApiInputValidationErrorHandler((ctx, res, zodError) => {
|
|
114
|
+
return res.api(null, { errorMessage: zodError.errors });
|
|
115
|
+
})
|
|
130
116
|
// Global error handler
|
|
131
117
|
.setGlobalErrorHandler((err, ctx, res) => {
|
|
132
118
|
console.error("Error:", err);
|
|
@@ -134,35 +120,21 @@ lambder
|
|
|
134
120
|
});
|
|
135
121
|
```
|
|
136
122
|
|
|
137
|
-
###
|
|
123
|
+
### Session-Protected APIs
|
|
138
124
|
|
|
139
|
-
|
|
125
|
+
Use `addSessionApi` for endpoints that require authentication:
|
|
140
126
|
|
|
141
127
|
```typescript
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
// ctx.apiPayload is typed as { userId: string }
|
|
151
|
-
const user = await db.getUser(ctx.apiPayload.userId);
|
|
152
|
-
return res.api(user); // Type-checked against output schema
|
|
153
|
-
})
|
|
154
|
-
// Session-protected API
|
|
155
|
-
.addSessionApi("getProfile", {
|
|
156
|
-
input: z.void(),
|
|
157
|
-
output: z.object({ userId: z.string(), username: z.string() })
|
|
158
|
-
}, async (ctx, res) => {
|
|
159
|
-
// Session is automatically fetched and validated
|
|
160
|
-
// ctx.session.data contains your session data
|
|
161
|
-
return res.api({
|
|
162
|
-
userId: ctx.session.data.userId,
|
|
163
|
-
username: ctx.session.data.username
|
|
164
|
-
});
|
|
128
|
+
lambder.addSessionApi("getProfile", {
|
|
129
|
+
input: z.void(),
|
|
130
|
+
output: z.object({ userId: z.string(), username: z.string() })
|
|
131
|
+
}, async (ctx, res) => {
|
|
132
|
+
// Session is automatically fetched and validated
|
|
133
|
+
return res.api({
|
|
134
|
+
userId: ctx.session.data.userId,
|
|
135
|
+
username: ctx.session.data.username
|
|
165
136
|
});
|
|
137
|
+
});
|
|
166
138
|
```
|
|
167
139
|
|
|
168
140
|
### Modular APIs with .use()
|
|
@@ -193,10 +165,10 @@ export const userApi = <T>(l: Lambder<T>) => {
|
|
|
193
165
|
// index.ts
|
|
194
166
|
import { userApi } from "./user-api";
|
|
195
167
|
|
|
196
|
-
const lambder = new Lambder()
|
|
168
|
+
const lambder = new Lambder({ publicPath: './public' })
|
|
197
169
|
.use(userApi);
|
|
198
170
|
|
|
199
|
-
export type
|
|
171
|
+
export type ApiContractType = typeof lambder.ApiContract;
|
|
200
172
|
```
|
|
201
173
|
|
|
202
174
|
|
|
@@ -217,24 +189,28 @@ lambder
|
|
|
217
189
|
// Modify response before sending
|
|
218
190
|
console.log("Response status:", response.statusCode);
|
|
219
191
|
return response;
|
|
192
|
+
})
|
|
193
|
+
// Fallback hook - runs when no route/API matches
|
|
194
|
+
.addHook("fallback", async (ctx, res) => {
|
|
195
|
+
// Perform cleanup or logging for unmatched requests
|
|
196
|
+
console.log("No handler matched for:", ctx.path);
|
|
220
197
|
});
|
|
221
198
|
```
|
|
222
199
|
|
|
223
200
|
### Session Management
|
|
224
201
|
|
|
225
|
-
Enable DynamoDB-based
|
|
202
|
+
Enable DynamoDB-based sessions with `enableDdbSession()`. Optional configuration:
|
|
226
203
|
|
|
227
204
|
```typescript
|
|
228
|
-
|
|
229
|
-
const lambder = new Lambder({ apiPath: '/api', publicPath: './public' })
|
|
205
|
+
lambder
|
|
230
206
|
.enableDdbSession({
|
|
231
207
|
tableName: "website-session",
|
|
232
208
|
tableRegion: "us-east-1",
|
|
233
209
|
sessionSalt: "CHANGE-THIS-TO-A-SECURE-RANDOM-STRING",
|
|
234
210
|
enableSlidingExpiration: true // Optional: extend session on each access
|
|
235
211
|
})
|
|
236
|
-
|
|
237
|
-
.
|
|
212
|
+
// Optionally customize session cookie names (defaults: LMDRSESSIONTKID, LMDRSESSIONCSTK)
|
|
213
|
+
.setSessionCookieKey("MY_SESSION_TOKEN", "MY_CSRF_TOKEN");
|
|
238
214
|
```
|
|
239
215
|
|
|
240
216
|
#### DynamoDB Session Table Structure
|
|
@@ -247,71 +223,17 @@ See [docs/DYNAMODB_SETUP.md](docs/DYNAMODB_SETUP.md) for detailed setup instruct
|
|
|
247
223
|
|
|
248
224
|
#### Session Controller
|
|
249
225
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
```typescript
|
|
253
|
-
const sessionController = lambder.getSessionController(ctx);
|
|
254
|
-
|
|
255
|
-
// Available methods:
|
|
256
|
-
await sessionController.createSession(sessionKey, data, ttlInSeconds);
|
|
257
|
-
// Starts a new session and persists the session data to DDB.
|
|
226
|
+
Access the session controller with `lambder.getSessionController(ctx)`:
|
|
258
227
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
// Updates the active session's data and persists it to DDB
|
|
269
|
-
|
|
270
|
-
await sessionController.endSession();
|
|
271
|
-
// End session and delete from DDB
|
|
272
|
-
|
|
273
|
-
await sessionController.endSessionAll();
|
|
274
|
-
// Ends and deletes all sessions for this sessionKey across all devices
|
|
275
|
-
|
|
276
|
-
await sessionController.regenerateSession();
|
|
277
|
-
// Regenerates session token (use after password change, etc.)
|
|
278
|
-
```
|
|
279
|
-
|
|
280
|
-
#### Session Examples
|
|
281
|
-
|
|
282
|
-
```typescript
|
|
283
|
-
lambder
|
|
284
|
-
.addApi("createSession", {
|
|
285
|
-
input: z.object({ userId: z.string() }),
|
|
286
|
-
output: z.object({ success: z.boolean() })
|
|
287
|
-
}, async (ctx, res) => {
|
|
288
|
-
// Create a new session
|
|
289
|
-
const userId = ctx.apiPayload.userId;
|
|
290
|
-
await lambder.getSessionController(ctx)
|
|
291
|
-
.createSession(userId, { business: "Session data goes here" });
|
|
292
|
-
|
|
293
|
-
console.log(ctx.session?.sessionKey); // userId
|
|
294
|
-
console.log(ctx.session?.data?.business); // "Session data goes here"
|
|
295
|
-
|
|
296
|
-
return res.api({ success: true });
|
|
297
|
-
})
|
|
298
|
-
.addSessionApi("updateSession", {
|
|
299
|
-
input: z.object({ newData: z.string() }),
|
|
300
|
-
output: z.object({ success: z.boolean() })
|
|
301
|
-
}, async (ctx, res) => {
|
|
302
|
-
// Session is automatically fetched
|
|
303
|
-
console.log(ctx.session.sessionKey); // userId
|
|
304
|
-
|
|
305
|
-
// Update session data
|
|
306
|
-
await lambder.getSessionController(ctx)
|
|
307
|
-
.updateSessionData({ business2: ctx.apiPayload.newData });
|
|
308
|
-
|
|
309
|
-
console.log(ctx.session.data.business); // undefined
|
|
310
|
-
console.log(ctx.session.data.business2); // newData value
|
|
311
|
-
|
|
312
|
-
return res.api({ success: true });
|
|
313
|
-
});
|
|
314
|
-
```
|
|
228
|
+
| Method | Description |
|
|
229
|
+
|--------|-------------|
|
|
230
|
+
| `createSession(sessionKey, data?, ttlInSeconds?)` | Start new session, persist to DDB |
|
|
231
|
+
| `fetchSession()` | Fetch & validate existing session (throws if not found) |
|
|
232
|
+
| `fetchSessionIfExists()` | Returns session or null |
|
|
233
|
+
| `updateSessionData(newData)` | Update session data in DDB |
|
|
234
|
+
| `endSession()` | End session, delete from DDB |
|
|
235
|
+
| `endSessionAll()` | End all sessions for this sessionKey (all devices) |
|
|
236
|
+
| `regenerateSession()` | Regenerate token (use after password change) |
|
|
315
237
|
|
|
316
238
|
### EJS Templates
|
|
317
239
|
|
|
@@ -339,75 +261,52 @@ Example partial:
|
|
|
339
261
|
|
|
340
262
|
### Render Context (ctx) Variables
|
|
341
263
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
return res.api({ result: ctx.apiPayload.value });
|
|
361
|
-
});
|
|
362
|
-
```
|
|
264
|
+
The `ctx` object provides access to request data:
|
|
265
|
+
|
|
266
|
+
| Property | Description | Example |
|
|
267
|
+
|----------|-------------|----------|
|
|
268
|
+
| `host` | Request host | `"www.example.com"` |
|
|
269
|
+
| `path` | Request path | `"/api"` |
|
|
270
|
+
| `pathParams` | Path parameters (routes) | `{ userId: "123" }` |
|
|
271
|
+
| `method` | HTTP method | `"GET"`, `"POST"` |
|
|
272
|
+
| `get` | Query parameters | `{ page: "1" }` |
|
|
273
|
+
| `post` | POST body (parsed) | `{ name: "John" }` |
|
|
274
|
+
| `cookie` | Cookies | `{ rememberMe: "true" }` |
|
|
275
|
+
| `headers` | Request headers | `{ "Content-Type": "..." }` |
|
|
276
|
+
| `event` | Raw APIGatewayProxyEvent | - |
|
|
277
|
+
| `lambdaContext` | AWS Lambda Context | - |
|
|
278
|
+
| `apiName` | API name (for API calls) | `"getUser"` |
|
|
279
|
+
| `apiPayload` | Validated input | `{ userId: "123" }` |
|
|
280
|
+
| `session` | Session data | Available in `addSessionApi` |
|
|
363
281
|
|
|
364
282
|
### Resolver Methods
|
|
365
283
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
return res.file(filePath, headers, fallbackFilePath);
|
|
394
|
-
// Serves a file from the public directory.
|
|
395
|
-
|
|
396
|
-
return await res.ejsFile(filePath, pageData, headers);
|
|
397
|
-
// Renders and serves an EJS file.
|
|
398
|
-
|
|
399
|
-
return await res.ejsTemplate(template, pageData, headers);
|
|
400
|
-
// Renders and serves an EJS template string.
|
|
401
|
-
|
|
402
|
-
return res.api(payload, config, headers);
|
|
403
|
-
// Sends a standardized API response for use with LambderCaller.
|
|
404
|
-
// Config: { notAuthorized, message, errorMessage, versionExpired, sessionExpired }
|
|
405
|
-
|
|
406
|
-
// res.die.* - Same as res.* but immediately returns and skips afterRender hooks
|
|
407
|
-
return res.die.json(data, headers);
|
|
408
|
-
return res.die.api(payload, config, headers);
|
|
409
|
-
// ... etc
|
|
410
|
-
```
|
|
284
|
+
**Header Manipulation** (call before returning response):
|
|
285
|
+
- `res.addHeader(key, value)` - Adds a header value (can be called multiple times for same key)
|
|
286
|
+
- `res.setHeader(key, value)` - Sets a header (replaces existing values)
|
|
287
|
+
- `res.logToApiResponse(data)` - Adds data to logList in API responses (debugging)
|
|
288
|
+
|
|
289
|
+
**Response Methods**:
|
|
290
|
+
|
|
291
|
+
| Method | Description |
|
|
292
|
+
|--------|-------------|
|
|
293
|
+
| `res.raw(param)` | Custom HTTP response |
|
|
294
|
+
| `res.json(data, headers?)` | JSON response |
|
|
295
|
+
| `res.xml(data)` | XML response (base64 encoded) |
|
|
296
|
+
| `res.html(data, headers?)` | HTML response (base64 encoded) |
|
|
297
|
+
| `res.redirect(url, statusCode?, headers?)` | Redirect (default: 302) |
|
|
298
|
+
| `res.status404(data, headers?)` | 404 Not Found response |
|
|
299
|
+
| `res.cors()` | 200 OK with CORS headers (preflight) |
|
|
300
|
+
| `res.fileBase64(base64, mimeType, headers?)` | File from base64 content |
|
|
301
|
+
| `res.file(path, headers?, fallbackPath?)` | Serve file from public directory |
|
|
302
|
+
| `await res.ejsFile(path, pageData, headers?)` | Render EJS file |
|
|
303
|
+
| `await res.ejsTemplate(template, pageData, headers?)` | Render EJS template string |
|
|
304
|
+
| `res.api(payload, config?, headers?)` | Standardized API response |
|
|
305
|
+
| `res.apiBinary(payload, config?, headers?)` | Gzip-compressed API response |
|
|
306
|
+
|
|
307
|
+
**API Config Options**: `{ notAuthorized, message, errorMessage, versionExpired, sessionExpired, logList }`
|
|
308
|
+
|
|
309
|
+
**Die Methods**: `res.die.*` - Same as above but immediately returns, skipping `afterRender` hooks.
|
|
411
310
|
|
|
412
311
|
## Frontend Usage with LambderCaller
|
|
413
312
|
|
|
@@ -417,9 +316,9 @@ LambderCaller is a frontend companion library for Lambder (only 2kb compressed)
|
|
|
417
316
|
|
|
418
317
|
```typescript
|
|
419
318
|
import { LambderCaller } from "lambder";
|
|
420
|
-
import type {
|
|
319
|
+
import type { ApiContractType } from "./backend/handler"; // Import the inferred contract type
|
|
421
320
|
|
|
422
|
-
const lambderCaller = new LambderCaller<
|
|
321
|
+
const lambderCaller = new LambderCaller<ApiContractType>({
|
|
423
322
|
apiPath: "/api",
|
|
424
323
|
isCorsEnabled: false,
|
|
425
324
|
fetchStartedHandler: ({ fetchParams, activeFetchList }) => {
|
|
@@ -441,38 +340,6 @@ const user = await lambderCaller.api("getCompanyPage", { companyName: "Acme" });
|
|
|
441
340
|
// - Expected output type
|
|
442
341
|
```
|
|
443
342
|
|
|
444
|
-
### How Type Safety Works
|
|
445
|
-
|
|
446
|
-
1. **Backend**: Chain your APIs and export the inferred contract
|
|
447
|
-
```typescript
|
|
448
|
-
// backend/handler.ts
|
|
449
|
-
import Lambder from 'lambder';
|
|
450
|
-
import { z } from 'zod';
|
|
451
|
-
|
|
452
|
-
const lambder = new Lambder({ apiPath: '/api' })
|
|
453
|
-
.addApi('getUser', {
|
|
454
|
-
input: z.object({ userId: z.string() }),
|
|
455
|
-
output: z.object({ id: z.string(), name: z.string() })
|
|
456
|
-
}, async (ctx, res) => {
|
|
457
|
-
return res.api({ id: ctx.apiPayload.userId, name: "John" });
|
|
458
|
-
});
|
|
459
|
-
|
|
460
|
-
export type AppContract = typeof lambder.ApiContract;
|
|
461
|
-
export const handler = lambder.getHandler();
|
|
462
|
-
```
|
|
463
|
-
|
|
464
|
-
2. **Frontend**: Import the **type** (not the code) and use it
|
|
465
|
-
```typescript
|
|
466
|
-
// frontend/api.ts
|
|
467
|
-
import { LambderCaller } from 'lambder';
|
|
468
|
-
import type { AppContract } from '../backend/handler'; // Type-only import
|
|
469
|
-
|
|
470
|
-
const lambderCaller = new LambderCaller<AppContract>({ apiPath: '/api' });
|
|
471
|
-
|
|
472
|
-
// ✅ Fully typed - TypeScript knows all APIs and their input/output types
|
|
473
|
-
const user = await lambderCaller.api('getUser', { userId: '123' });
|
|
474
|
-
```
|
|
475
|
-
|
|
476
343
|
### Benefits
|
|
477
344
|
|
|
478
345
|
✅ **No Manual Type Definitions** - Types are inferred from your Zod schemas
|
|
@@ -491,9 +358,9 @@ LambderMSW provides seamless integration with [MSW (Mock Service Worker)](https:
|
|
|
491
358
|
```typescript
|
|
492
359
|
import { LambderMSW } from 'lambder';
|
|
493
360
|
import { setupServer } from 'msw/node';
|
|
494
|
-
import type {
|
|
361
|
+
import type { ApiContractType } from './backend/handler';
|
|
495
362
|
|
|
496
|
-
const lambderMSW = new LambderMSW<
|
|
363
|
+
const lambderMSW = new LambderMSW<ApiContractType>({
|
|
497
364
|
apiPath: '/api',
|
|
498
365
|
});
|
|
499
366
|
|
package/dist/Lambder.d.ts
CHANGED
|
@@ -46,7 +46,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
46
46
|
* @example
|
|
47
47
|
* ```typescript
|
|
48
48
|
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
49
|
-
* export type
|
|
49
|
+
* export type ApiContractType = typeof lambder.ApiContract;
|
|
50
50
|
* ```
|
|
51
51
|
*/
|
|
52
52
|
readonly ApiContract: _TContract;
|
package/dist/Lambder.js
CHANGED
|
@@ -33,7 +33,7 @@ export default class Lambder {
|
|
|
33
33
|
* @example
|
|
34
34
|
* ```typescript
|
|
35
35
|
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
36
|
-
* export type
|
|
36
|
+
* export type ApiContractType = typeof lambder.ApiContract;
|
|
37
37
|
* ```
|
|
38
38
|
*/
|
|
39
39
|
ApiContract;
|
|
@@ -120,10 +120,9 @@ export default class Lambder {
|
|
|
120
120
|
}
|
|
121
121
|
addRoute(condition, actionFn) {
|
|
122
122
|
this.actionList.push({
|
|
123
|
-
conditionFn: (ctx) => (
|
|
124
|
-
(
|
|
125
|
-
|
|
126
|
-
(condition?.constructor == RegExp && condition.test(ctx.path)))),
|
|
123
|
+
conditionFn: (ctx) => (((typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
|
|
124
|
+
(typeof condition === "function" && condition(ctx)) ||
|
|
125
|
+
(condition?.constructor == RegExp && condition.test(ctx.path)))),
|
|
127
126
|
actionFn: async (ctx, resolver) => {
|
|
128
127
|
if (typeof condition === "string") {
|
|
129
128
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
@@ -139,10 +138,9 @@ export default class Lambder {
|
|
|
139
138
|
}
|
|
140
139
|
addSessionRoute(condition, actionFn) {
|
|
141
140
|
this.actionList.push({
|
|
142
|
-
conditionFn: (ctx) => (
|
|
143
|
-
(
|
|
144
|
-
|
|
145
|
-
(condition?.constructor == RegExp && condition.test(ctx.path)))),
|
|
141
|
+
conditionFn: (ctx) => (((typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
|
|
142
|
+
(typeof condition === "function" && condition(ctx)) ||
|
|
143
|
+
(condition?.constructor == RegExp && condition.test(ctx.path)))),
|
|
146
144
|
actionFn: async (ctx, resolver) => {
|
|
147
145
|
if (typeof condition === "string") {
|
|
148
146
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
package/docs/LAMBDER_MSW.md
CHANGED
|
@@ -31,14 +31,6 @@ const handlers = [
|
|
|
31
31
|
name: 'John Doe',
|
|
32
32
|
email: 'john@example.com'
|
|
33
33
|
};
|
|
34
|
-
}),
|
|
35
|
-
|
|
36
|
-
lambderMSW.mockApi('createUser', async (payload) => {
|
|
37
|
-
return {
|
|
38
|
-
id: '123',
|
|
39
|
-
name: payload.name,
|
|
40
|
-
email: payload.email
|
|
41
|
-
};
|
|
42
34
|
})
|
|
43
35
|
];
|
|
44
36
|
|
|
@@ -60,7 +52,7 @@ When using TypeScript API contracts, LambderMSW provides full type safety:
|
|
|
60
52
|
import { z } from 'zod';
|
|
61
53
|
import Lambder from 'lambder';
|
|
62
54
|
|
|
63
|
-
const lambder = new Lambder({ apiPath: '/secure' })
|
|
55
|
+
const lambder = new Lambder({ apiPath: '/secure', publicPath: './public' })
|
|
64
56
|
.addApi('getUserById', {
|
|
65
57
|
input: z.object({ userId: z.string() }),
|
|
66
58
|
output: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
@@ -77,13 +69,13 @@ const lambder = new Lambder({ apiPath: '/secure' })
|
|
|
77
69
|
});
|
|
78
70
|
|
|
79
71
|
// Export the inferred contract type
|
|
80
|
-
export type
|
|
72
|
+
export type ApiContractType = typeof lambder.ApiContract;
|
|
81
73
|
|
|
82
74
|
// test/setup.ts
|
|
83
75
|
import { LambderMSW } from 'lambder';
|
|
84
|
-
import type {
|
|
76
|
+
import type { ApiContractType } from '../backend';
|
|
85
77
|
|
|
86
|
-
const lambderMSW = new LambderMSW<
|
|
78
|
+
const lambderMSW = new LambderMSW<ApiContractType>({
|
|
87
79
|
apiPath: '/secure',
|
|
88
80
|
apiVersion: '1.0.0'
|
|
89
81
|
});
|
|
@@ -215,12 +207,11 @@ lambderMSW.mockWithMessage('updateProfile', async (payload) => {
|
|
|
215
207
|
// test/api.test.ts
|
|
216
208
|
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
217
209
|
import { setupServer } from 'msw/node';
|
|
218
|
-
import { LambderMSW } from 'lambder';
|
|
219
|
-
import {
|
|
220
|
-
import type { MyApiContract } from '../backend'; // Type-only import from your backend
|
|
210
|
+
import { LambderMSW, LambderCaller } from 'lambder';
|
|
211
|
+
import type { ApiContractType } from '../backend'; // Type-only import from your backend
|
|
221
212
|
|
|
222
213
|
// Setup MSW
|
|
223
|
-
const lambderMSW = new LambderMSW<
|
|
214
|
+
const lambderMSW = new LambderMSW<ApiContractType>({
|
|
224
215
|
apiPath: '/secure',
|
|
225
216
|
apiVersion: '1.0.0'
|
|
226
217
|
});
|
|
@@ -257,7 +248,7 @@ afterEach(() => server.resetHandlers());
|
|
|
257
248
|
afterAll(() => server.close());
|
|
258
249
|
|
|
259
250
|
// Setup LambderCaller
|
|
260
|
-
const lambderCaller = new LambderCaller<
|
|
251
|
+
const lambderCaller = new LambderCaller<ApiContractType>({
|
|
261
252
|
apiPath: '/secure',
|
|
262
253
|
isCorsEnabled: false
|
|
263
254
|
});
|
|
@@ -314,9 +305,9 @@ LambderMSW also works in browser environments with MSW's browser integration:
|
|
|
314
305
|
// test/browser-setup.ts
|
|
315
306
|
import { setupWorker } from 'msw/browser';
|
|
316
307
|
import { LambderMSW } from 'lambder';
|
|
317
|
-
import type {
|
|
308
|
+
import type { ApiContractType } from '../backend'; // Type-only import from your backend
|
|
318
309
|
|
|
319
|
-
const lambderMSW = new LambderMSW<
|
|
310
|
+
const lambderMSW = new LambderMSW<ApiContractType>({
|
|
320
311
|
apiPath: '/secure'
|
|
321
312
|
});
|
|
322
313
|
|
|
@@ -361,25 +352,6 @@ lambderMSW.mockApi('searchUsers', async (payload) => {
|
|
|
361
352
|
});
|
|
362
353
|
```
|
|
363
354
|
|
|
364
|
-
### Simulating Network Conditions
|
|
365
|
-
|
|
366
|
-
```typescript
|
|
367
|
-
// Slow network
|
|
368
|
-
lambderMSW.mockApi('slowApi', async () => {
|
|
369
|
-
return { data: 'slow response' };
|
|
370
|
-
}, { delay: 3000 }); // 3 second delay
|
|
371
|
-
|
|
372
|
-
// Intermittent failures
|
|
373
|
-
let callCount = 0;
|
|
374
|
-
lambderMSW.mockApi('flakeyApi', async () => {
|
|
375
|
-
callCount++;
|
|
376
|
-
if (callCount % 3 === 0) {
|
|
377
|
-
throw new Error('Random failure');
|
|
378
|
-
}
|
|
379
|
-
return { success: true };
|
|
380
|
-
});
|
|
381
|
-
```
|
|
382
|
-
|
|
383
355
|
### Override Handlers Per Test
|
|
384
356
|
|
|
385
357
|
```typescript
|
|
@@ -402,12 +374,10 @@ it('should handle specific user', async () => {
|
|
|
402
374
|
|
|
403
375
|
## Benefits
|
|
404
376
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
✅ **Fast** - No network calls, instant test execution
|
|
410
|
-
✅ **Realistic** - Simulate real-world scenarios (delays, errors, etc.)
|
|
377
|
+
- Full TypeScript support with API contracts
|
|
378
|
+
- Intuitive methods matching Lambder's API structure
|
|
379
|
+
- Test isolation without real backend dependencies
|
|
380
|
+
- Simulate real-world scenarios (delays, errors, etc.)
|
|
411
381
|
|
|
412
382
|
## Troubleshooting
|
|
413
383
|
|
|
@@ -29,7 +29,7 @@ const lambder = new Lambder({
|
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
// Export the inferred contract type
|
|
32
|
-
export type
|
|
32
|
+
export type ApiContractType = typeof lambder.ApiContract;
|
|
33
33
|
|
|
34
34
|
export const handler = lambder.getHandler();
|
|
35
35
|
```
|
|
@@ -40,9 +40,9 @@ Import the type (not the code) and use `LambderCaller`.
|
|
|
40
40
|
|
|
41
41
|
```typescript
|
|
42
42
|
import { LambderCaller } from "lambder";
|
|
43
|
-
import type {
|
|
43
|
+
import type { ApiContractType } from "./backend"; // Type-only import
|
|
44
44
|
|
|
45
|
-
const lambderCaller = new LambderCaller<
|
|
45
|
+
const lambderCaller = new LambderCaller<ApiContractType>({
|
|
46
46
|
apiPath: "/api"
|
|
47
47
|
});
|
|
48
48
|
|
|
@@ -72,6 +72,6 @@ export const userApi = <T>(l: Lambder<T>) => {
|
|
|
72
72
|
// index.ts
|
|
73
73
|
import { userApi } from "./api.user";
|
|
74
74
|
|
|
75
|
-
const lambder = new Lambder()
|
|
75
|
+
const lambder = new Lambder({ publicPath: './public' })
|
|
76
76
|
.use(userApi); // Types are preserved!
|
|
77
77
|
```
|
|
@@ -22,7 +22,7 @@ import { LambderMSW, LambderCaller } from '../src/index.ts';
|
|
|
22
22
|
import Lambder from '../src/Lambder.js';
|
|
23
23
|
|
|
24
24
|
// Define your API contract using Lambder chaining
|
|
25
|
-
const lambder = new Lambder()
|
|
25
|
+
const lambder = new Lambder({ publicPath: './public' })
|
|
26
26
|
.addApi('getUserById', {
|
|
27
27
|
input: z.object({ userId: z.string() }),
|
|
28
28
|
output: z.object({ id: z.string(), name: z.string(), email: z.string() }).nullable()
|
|
@@ -40,10 +40,10 @@ const lambder = new Lambder()
|
|
|
40
40
|
output: z.object({ success: z.boolean() })
|
|
41
41
|
}, async () => ({} as any));
|
|
42
42
|
|
|
43
|
-
type
|
|
43
|
+
type ApiContractType = typeof lambder.ApiContract;
|
|
44
44
|
|
|
45
45
|
// Setup LambderMSW with type safety
|
|
46
|
-
const lambderMSW = new LambderMSW<
|
|
46
|
+
const lambderMSW = new LambderMSW<ApiContractType>({
|
|
47
47
|
apiPath: '/secure',
|
|
48
48
|
apiVersion: '1.0.0',
|
|
49
49
|
});
|
|
@@ -92,7 +92,7 @@ const handlers = [
|
|
|
92
92
|
const server = setupServer(...handlers);
|
|
93
93
|
|
|
94
94
|
// Setup LambderCaller
|
|
95
|
-
const lambderCaller = new LambderCaller<
|
|
95
|
+
const lambderCaller = new LambderCaller<ApiContractType>({
|
|
96
96
|
apiPath: '/secure',
|
|
97
97
|
isCorsEnabled: false,
|
|
98
98
|
});
|
|
@@ -45,8 +45,8 @@ const lambder = new Lambder({
|
|
|
45
45
|
});
|
|
46
46
|
});
|
|
47
47
|
|
|
48
|
-
// 3. Export the inferred contract type for
|
|
49
|
-
export type
|
|
48
|
+
// 3. Export the inferred contract type for frontend use
|
|
49
|
+
export type ApiContractType = typeof lambder.ApiContract;
|
|
50
50
|
|
|
51
51
|
// 4. Modular example using .use()
|
|
52
52
|
const authApi = <T>(l: Lambder<T>) => {
|
package/package.json
CHANGED
package/src/Lambder.ts
CHANGED
|
@@ -58,7 +58,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
58
58
|
* @example
|
|
59
59
|
* ```typescript
|
|
60
60
|
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
61
|
-
* export type
|
|
61
|
+
* export type ApiContractType = typeof lambder.ApiContract;
|
|
62
62
|
* ```
|
|
63
63
|
*/
|
|
64
64
|
public readonly ApiContract!: _TContract;
|
|
@@ -163,7 +163,6 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
163
163
|
addRoute(condition: Path|ConditionFunction|RegExp, actionFn: ActionFunction): this {
|
|
164
164
|
this.actionList.push({
|
|
165
165
|
conditionFn: (ctx:LambderRenderContext<any>) => (
|
|
166
|
-
ctx.method === "GET" &&
|
|
167
166
|
(
|
|
168
167
|
(typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
|
|
169
168
|
(typeof condition === "function" && condition(ctx)) ||
|
|
@@ -187,7 +186,6 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
187
186
|
addSessionRoute(condition: Path|ConditionFunction|RegExp, actionFn: SessionActionFunction<TSessionData>): this {
|
|
188
187
|
this.actionList.push({
|
|
189
188
|
conditionFn: (ctx:LambderRenderContext<any>) => (
|
|
190
|
-
ctx.method === "GET" &&
|
|
191
189
|
(
|
|
192
190
|
(typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
|
|
193
191
|
(typeof condition === "function" && condition(ctx)) ||
|
package/tests/routes.test.ts
CHANGED
|
@@ -518,13 +518,13 @@ describe('Routes - Wildcard and Catch-all Routes', () => {
|
|
|
518
518
|
});
|
|
519
519
|
|
|
520
520
|
describe('Routes - Method Filtering', () => {
|
|
521
|
-
it('should
|
|
521
|
+
it('should match all HTTP methods when no method is specified', async () => {
|
|
522
522
|
const lambder = new Lambder({
|
|
523
523
|
publicPath: './public',
|
|
524
524
|
apiPath: '/api'
|
|
525
525
|
})
|
|
526
526
|
.addRoute('/resource', (ctx, res) => {
|
|
527
|
-
return res.html(
|
|
527
|
+
return res.html(`${ctx.method} Response`);
|
|
528
528
|
});
|
|
529
529
|
|
|
530
530
|
const handler = lambder.getHandler();
|
|
@@ -534,9 +534,10 @@ describe('Routes - Method Filtering', () => {
|
|
|
534
534
|
const getResult = await handler(getEvent, createMockContext());
|
|
535
535
|
expect(Buffer.from(getResult.body || '', 'base64').toString()).toBe('GET Response');
|
|
536
536
|
|
|
537
|
-
// POST should
|
|
537
|
+
// POST should also work (no method restriction)
|
|
538
538
|
const postEvent = createMockEvent('/resource', 'POST');
|
|
539
539
|
const postResult = await handler(postEvent, createMockContext());
|
|
540
|
-
expect(postResult.statusCode).toBe(
|
|
540
|
+
expect(postResult.statusCode).toBe(200);
|
|
541
|
+
expect(Buffer.from(postResult.body || '', 'base64').toString()).toBe('POST Response');
|
|
541
542
|
});
|
|
542
543
|
});
|
|
@@ -1,263 +0,0 @@
|
|
|
1
|
-
# Untested Features in Lambder
|
|
2
|
-
|
|
3
|
-
This document lists features that currently lack test coverage.
|
|
4
|
-
|
|
5
|
-
## ✅ Currently Tested Features
|
|
6
|
-
|
|
7
|
-
Based on existing test files:
|
|
8
|
-
|
|
9
|
-
1. **Type Safety** (`type-safety.test.ts`)
|
|
10
|
-
- LambderCaller type inference
|
|
11
|
-
- Lambder API type safety
|
|
12
|
-
- Input/output type enforcement
|
|
13
|
-
- Complex type patterns
|
|
14
|
-
- Type inference from contracts
|
|
15
|
-
|
|
16
|
-
2. **Session Management** (`session.test.ts`)
|
|
17
|
-
- Session creation, fetching, updating, deletion
|
|
18
|
-
- Session regeneration
|
|
19
|
-
- Session security (CSRF, validation)
|
|
20
|
-
- Session type safety
|
|
21
|
-
- Sliding expiration
|
|
22
|
-
|
|
23
|
-
3. **Output Type Runtime** (`output-type-runtime.test.ts`)
|
|
24
|
-
- Runtime validation of API outputs
|
|
25
|
-
- Zod schema enforcement at runtime
|
|
26
|
-
- API response format validation
|
|
27
|
-
|
|
28
|
-
4. **Plugin System** (`use-plugin.test.ts`)
|
|
29
|
-
- Basic plugin usage
|
|
30
|
-
- Multiple plugin chaining
|
|
31
|
-
- Plugin type accumulation
|
|
32
|
-
- Nested plugins
|
|
33
|
-
- Plugin reusability
|
|
34
|
-
|
|
35
|
-
## ❌ Untested Features (Needs Test Coverage)
|
|
36
|
-
|
|
37
|
-
### 1. Routes (`addRoute`, `addSessionRoute`)
|
|
38
|
-
**Priority: HIGH**
|
|
39
|
-
|
|
40
|
-
- [x] Basic route matching (string paths)
|
|
41
|
-
- [ ] Path parameter extraction (e.g., `/user/:userId`)
|
|
42
|
-
- [ ] RegExp route matching
|
|
43
|
-
- [ ] Function-based conditional routing
|
|
44
|
-
- [ ] Session-protected routes (`addSessionRoute`)
|
|
45
|
-
- [ ] Route priority/ordering
|
|
46
|
-
- [ ] Wildcard routes
|
|
47
|
-
|
|
48
|
-
**Suggested test file:** `tests/routes.test.ts`
|
|
49
|
-
|
|
50
|
-
### 2. Hooks System
|
|
51
|
-
**Priority: HIGH**
|
|
52
|
-
|
|
53
|
-
- [ ] `beforeRender` hook execution
|
|
54
|
-
- [ ] `afterRender` hook execution
|
|
55
|
-
- [ ] `fallback` hook execution
|
|
56
|
-
- [ ] `created` hook execution
|
|
57
|
-
- [ ] Hook priority ordering
|
|
58
|
-
- [ ] Multiple hooks of same type
|
|
59
|
-
- [ ] Hook error handling
|
|
60
|
-
- [ ] Context modification in hooks
|
|
61
|
-
- [ ] Response modification in hooks
|
|
62
|
-
|
|
63
|
-
**Suggested test file:** `tests/hooks.test.ts`
|
|
64
|
-
|
|
65
|
-
### 3. Error Handling
|
|
66
|
-
**Priority: HIGH**
|
|
67
|
-
|
|
68
|
-
- [ ] `setGlobalErrorHandler` functionality
|
|
69
|
-
- [ ] Error handling with context available
|
|
70
|
-
- [ ] Error handling with null context
|
|
71
|
-
- [ ] Custom error responses
|
|
72
|
-
- [ ] Error in API handlers
|
|
73
|
-
- [ ] Error in route handlers
|
|
74
|
-
- [ ] Error in hooks
|
|
75
|
-
- [ ] Error log accumulation
|
|
76
|
-
|
|
77
|
-
**Suggested test file:** `tests/error-handling.test.ts`
|
|
78
|
-
|
|
79
|
-
### 4. Fallback Handlers
|
|
80
|
-
**Priority: MEDIUM**
|
|
81
|
-
|
|
82
|
-
- [ ] `setRouteFallbackHandler` for unmatched routes
|
|
83
|
-
- [ ] `setApiFallbackHandler` for unmatched APIs
|
|
84
|
-
- [ ] Default fallback behavior (no handler set)
|
|
85
|
-
- [ ] Fallback handler with custom responses
|
|
86
|
-
|
|
87
|
-
**Suggested test file:** `tests/fallback-handlers.test.ts`
|
|
88
|
-
|
|
89
|
-
### 5. Response Methods (LambderResolver/LambderResponseBuilder)
|
|
90
|
-
**Priority: MEDIUM**
|
|
91
|
-
|
|
92
|
-
- [x] `res.api()` - tested in output-type-runtime
|
|
93
|
-
- [ ] `res.json()`
|
|
94
|
-
- [ ] `res.html()`
|
|
95
|
-
- [ ] `res.xml()`
|
|
96
|
-
- [ ] `res.raw()`
|
|
97
|
-
- [ ] `res.file()` with fallback
|
|
98
|
-
- [ ] `res.fileBase64()`
|
|
99
|
-
- [ ] `res.ejsFile()`
|
|
100
|
-
- [ ] `res.ejsTemplate()`
|
|
101
|
-
- [ ] `res.redirect()` (redirects)
|
|
102
|
-
- [ ] `res.status404()`
|
|
103
|
-
- [ ] `res.cors()`
|
|
104
|
-
- [ ] `res.die.*` methods (skip afterRender hooks)
|
|
105
|
-
- [ ] Custom headers in responses
|
|
106
|
-
- [ ] `setHeader()` and `addHeader()` in context
|
|
107
|
-
|
|
108
|
-
**Suggested test file:** `tests/response-methods.test.ts`
|
|
109
|
-
|
|
110
|
-
### 6. CORS Support
|
|
111
|
-
**Priority: MEDIUM**
|
|
112
|
-
|
|
113
|
-
- [ ] `enableCors(true)` functionality
|
|
114
|
-
- [ ] CORS headers in responses
|
|
115
|
-
- [ ] OPTIONS request handling (preflight)
|
|
116
|
-
- [ ] CORS disabled behavior
|
|
117
|
-
|
|
118
|
-
**Suggested test file:** `tests/cors.test.ts`
|
|
119
|
-
|
|
120
|
-
### 7. API Version Management
|
|
121
|
-
**Priority: MEDIUM**
|
|
122
|
-
|
|
123
|
-
- [ ] Setting `apiVersion` in constructor
|
|
124
|
-
- [ ] Version mismatch detection
|
|
125
|
-
- [ ] Version expired response
|
|
126
|
-
- [ ] Client version validation
|
|
127
|
-
- [ ] Version header handling
|
|
128
|
-
|
|
129
|
-
**Suggested test file:** `tests/api-versioning.test.ts`
|
|
130
|
-
|
|
131
|
-
### 8. Context Variables
|
|
132
|
-
**Priority: LOW**
|
|
133
|
-
|
|
134
|
-
- [ ] `ctx.host` extraction
|
|
135
|
-
- [ ] `ctx.path` extraction
|
|
136
|
-
- [ ] `ctx.pathParams` for route parameters
|
|
137
|
-
- [ ] `ctx.get` (query parameters)
|
|
138
|
-
- [ ] `ctx.post` (POST body parsing)
|
|
139
|
-
- [ ] `ctx.cookie` parsing
|
|
140
|
-
- [ ] `ctx.headers` access
|
|
141
|
-
- [ ] `ctx.event` (raw Lambda event)
|
|
142
|
-
- [ ] `ctx.lambdaContext` (raw Lambda context)
|
|
143
|
-
- [ ] Base64 encoded body handling
|
|
144
|
-
- [ ] URL-encoded form data parsing
|
|
145
|
-
|
|
146
|
-
**Suggested test file:** `tests/context-extraction.test.ts`
|
|
147
|
-
|
|
148
|
-
### 9. LambderCaller Features
|
|
149
|
-
**Priority: MEDIUM**
|
|
150
|
-
|
|
151
|
-
- [x] Basic API calls - tested in type-safety
|
|
152
|
-
- [ ] `apiRaw()` method
|
|
153
|
-
- [ ] `fetchStartedHandler` callback
|
|
154
|
-
- [ ] `fetchEndedHandler` callback
|
|
155
|
-
- [ ] `errorMessageHandler` callback
|
|
156
|
-
- [ ] `activeFetchList` tracking
|
|
157
|
-
- [ ] Error response handling
|
|
158
|
-
- [ ] Session expired handling
|
|
159
|
-
- [ ] Version expired handling
|
|
160
|
-
- [ ] Not authorized handling
|
|
161
|
-
- [ ] Custom headers in requests
|
|
162
|
-
- [ ] CORS mode handling
|
|
163
|
-
|
|
164
|
-
**Suggested test file:** `tests/lambder-caller.test.ts`
|
|
165
|
-
|
|
166
|
-
### 10. LambderMSW Features
|
|
167
|
-
**Priority: MEDIUM**
|
|
168
|
-
|
|
169
|
-
- [ ] `mockApi()` basic functionality
|
|
170
|
-
- [ ] `mockSessionExpired()` helper
|
|
171
|
-
- [ ] `mockNotAuthorized()` helper
|
|
172
|
-
- [ ] `mockVersionExpired()` helper
|
|
173
|
-
- [ ] Custom mock options (delay, message, errorMessage)
|
|
174
|
-
- [ ] Type safety in mocks
|
|
175
|
-
- [ ] Multiple API mocks
|
|
176
|
-
- [ ] Mock priority/ordering
|
|
177
|
-
|
|
178
|
-
**Suggested test file:** `tests/lambder-msw.test.ts`
|
|
179
|
-
|
|
180
|
-
### 11. EJS Template Rendering
|
|
181
|
-
**Priority: LOW**
|
|
182
|
-
|
|
183
|
-
- [ ] `res.ejsFile()` rendering
|
|
184
|
-
- [ ] `res.ejsTemplate()` rendering
|
|
185
|
-
- [ ] Template `page` variable
|
|
186
|
-
- [ ] Partial `partial` variable
|
|
187
|
-
- [ ] `include()` function in templates
|
|
188
|
-
- [ ] Template error handling
|
|
189
|
-
- [ ] Custom headers with EJS
|
|
190
|
-
|
|
191
|
-
**Suggested test file:** `tests/ejs-templates.test.ts`
|
|
192
|
-
|
|
193
|
-
### 12. File Serving
|
|
194
|
-
**Priority: LOW**
|
|
195
|
-
|
|
196
|
-
- [ ] Static file serving with `res.file()`
|
|
197
|
-
- [ ] Fallback file (e.g., index.html)
|
|
198
|
-
- [ ] MIME type detection
|
|
199
|
-
- [ ] Base64 file responses
|
|
200
|
-
- [ ] File not found handling
|
|
201
|
-
- [ ] Public path configuration
|
|
202
|
-
|
|
203
|
-
**Suggested test file:** `tests/file-serving.test.ts`
|
|
204
|
-
|
|
205
|
-
### 13. Utility Methods
|
|
206
|
-
**Priority: LOW**
|
|
207
|
-
|
|
208
|
-
- [ ] `lambder.utils.*` methods
|
|
209
|
-
- [ ] `getSessionController()` helper
|
|
210
|
-
- [ ] `getResponseBuilder()` helper
|
|
211
|
-
- [ ] `getHandler()` export
|
|
212
|
-
|
|
213
|
-
**Suggested test file:** `tests/utilities.test.ts`
|
|
214
|
-
|
|
215
|
-
### 14. Edge Cases & Integration
|
|
216
|
-
**Priority: LOW**
|
|
217
|
-
|
|
218
|
-
- [ ] Multiple simultaneous requests
|
|
219
|
-
- [ ] Race conditions in session management
|
|
220
|
-
- [ ] Large payload handling
|
|
221
|
-
- [ ] Invalid JSON in request body
|
|
222
|
-
- [ ] Missing required fields
|
|
223
|
-
- [ ] Empty/null payloads
|
|
224
|
-
- [ ] Very long session data
|
|
225
|
-
- [ ] Malformed Lambda events
|
|
226
|
-
|
|
227
|
-
**Suggested test file:** `tests/edge-cases.test.ts`
|
|
228
|
-
|
|
229
|
-
## Test Coverage Priorities
|
|
230
|
-
|
|
231
|
-
### Must Have (Before v2.0 stable)
|
|
232
|
-
1. Routes and path parameters
|
|
233
|
-
2. Hooks system
|
|
234
|
-
3. Error handling
|
|
235
|
-
4. Response methods (at least common ones)
|
|
236
|
-
|
|
237
|
-
### Should Have (Before v2.1)
|
|
238
|
-
1. CORS support
|
|
239
|
-
2. API versioning
|
|
240
|
-
3. LambderCaller edge cases
|
|
241
|
-
4. Fallback handlers
|
|
242
|
-
|
|
243
|
-
### Nice to Have (Future releases)
|
|
244
|
-
1. EJS templates
|
|
245
|
-
2. File serving
|
|
246
|
-
3. Edge cases and stress testing
|
|
247
|
-
4. Integration tests
|
|
248
|
-
|
|
249
|
-
## How to Run Tests
|
|
250
|
-
|
|
251
|
-
```bash
|
|
252
|
-
# Run all tests
|
|
253
|
-
npm run test
|
|
254
|
-
|
|
255
|
-
# Run specific test file
|
|
256
|
-
npm run test -- tests/routes.test.ts
|
|
257
|
-
|
|
258
|
-
# Run tests in watch mode
|
|
259
|
-
npm run test -- --watch
|
|
260
|
-
|
|
261
|
-
# Run tests with coverage
|
|
262
|
-
npm run test -- --coverage
|
|
263
|
-
```
|