lambder 1.0.148 → 2.0.2

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.
Files changed (39) hide show
  1. package/Readme.md +355 -410
  2. package/dist/Lambder.d.ts +47 -21
  3. package/dist/Lambder.js +79 -24
  4. package/dist/LambderApiContract.d.ts +10 -42
  5. package/dist/LambderApiContract.js +2 -22
  6. package/dist/LambderCaller.js +2 -2
  7. package/dist/LambderMSW.js +0 -2
  8. package/dist/LambderResolver.d.ts +16 -17
  9. package/dist/LambderResponseBuilder.d.ts +2 -3
  10. package/dist/LambderResponseBuilder.js +1 -3
  11. package/dist/LambderUtils.js +1 -3
  12. package/dist/index.d.ts +1 -1
  13. package/docs/LAMBDER_MSW.md +6 -6
  14. package/docs/TYPE_SAFE_QUICK_START.md +54 -177
  15. package/examples/msw-testing-example.ts +36 -33
  16. package/examples/secure-session-example.ts +50 -34
  17. package/examples/zod-chained-api-example.ts +63 -0
  18. package/package.json +3 -2
  19. package/src/Lambder.ts +124 -83
  20. package/src/LambderApiContract.ts +7 -50
  21. package/src/LambderCaller.ts +2 -2
  22. package/src/LambderMSW.ts +0 -4
  23. package/src/LambderResolver.ts +21 -24
  24. package/src/LambderResponseBuilder.ts +4 -7
  25. package/src/LambderUtils.ts +1 -3
  26. package/src/index.ts +0 -3
  27. package/tests/UNTESTED_FEATURES.md +263 -0
  28. package/tests/error-handling.test.ts +585 -0
  29. package/tests/hooks.test.ts +561 -0
  30. package/tests/output-type-runtime.test.ts +80 -64
  31. package/tests/routes.test.ts +542 -0
  32. package/tests/session.test.ts +38 -24
  33. package/tests/type-safety.test.ts +147 -97
  34. package/tests/use-plugin.test.ts +437 -0
  35. package/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +0 -90
  36. package/examples/output-type-enforcement-example.ts +0 -218
  37. package/examples/simplified-typed-api-example.ts +0 -365
  38. package/examples/test-output-type-enforcement.ts +0 -101
  39. package/test-type-enforcement.ts +0 -111
@@ -0,0 +1,542 @@
1
+ /**
2
+ * Routes Tests
3
+ *
4
+ * Tests for addRoute and addSessionRoute functionality including:
5
+ * - String path matching
6
+ * - Path parameter extraction
7
+ * - RegExp route matching
8
+ * - Function-based conditional routing
9
+ * - Session-protected routes
10
+ * - Route priority/ordering
11
+ * - Wildcard routes
12
+ */
13
+
14
+ import { describe, it, expect, beforeEach } from 'vitest';
15
+ import { mockClient } from 'aws-sdk-client-mock';
16
+ import { DynamoDBDocumentClient, GetCommand, PutCommand } from '@aws-sdk/lib-dynamodb';
17
+ import Lambder from '../src/Lambder.js';
18
+ import type { APIGatewayProxyEvent, Context } from 'aws-lambda';
19
+
20
+ // Mock DynamoDB
21
+ const ddbMock = mockClient(DynamoDBDocumentClient);
22
+
23
+ const createMockEvent = (path: string, method: string = 'GET', sessionToken?: string): APIGatewayProxyEvent => ({
24
+ body: null,
25
+ headers: {
26
+ Host: 'localhost',
27
+ Cookie: sessionToken ? `LMDRSESSIONTKID=${sessionToken}` : ''
28
+ },
29
+ multiValueHeaders: {},
30
+ httpMethod: method,
31
+ isBase64Encoded: false,
32
+ path,
33
+ pathParameters: null,
34
+ queryStringParameters: null,
35
+ multiValueQueryStringParameters: null,
36
+ stageVariables: null,
37
+ requestContext: {} as any,
38
+ resource: '',
39
+ });
40
+
41
+ const createMockContext = (): Context => ({
42
+ callbackWaitsForEmptyEventLoop: false,
43
+ functionName: 'test',
44
+ functionVersion: '1',
45
+ invokedFunctionArn: 'arn',
46
+ memoryLimitInMB: '128',
47
+ awsRequestId: '123',
48
+ logGroupName: 'group',
49
+ logStreamName: 'stream',
50
+ getRemainingTimeInMillis: () => 1000,
51
+ done: () => {},
52
+ fail: () => {},
53
+ succeed: () => {},
54
+ });
55
+
56
+ describe('Routes - Basic Path Matching', () => {
57
+ it('should match simple string paths', async () => {
58
+ const lambder = new Lambder({
59
+ publicPath: './public',
60
+ apiPath: '/api'
61
+ })
62
+ .addRoute('/hello', (ctx, res) => {
63
+ return res.html('Hello World');
64
+ });
65
+
66
+ const handler = lambder.getHandler();
67
+ const event = createMockEvent('/hello');
68
+ const result = await handler(event, createMockContext());
69
+
70
+ expect(result.statusCode).toBe(200);
71
+ const body = Buffer.from(result.body || '', 'base64').toString();
72
+ expect(body).toBe('Hello World');
73
+ });
74
+
75
+ it('should not match wrong paths', async () => {
76
+ const lambder = new Lambder({
77
+ publicPath: './public',
78
+ apiPath: '/api'
79
+ })
80
+ .addRoute('/hello', (ctx, res) => {
81
+ return res.html('Hello');
82
+ })
83
+ .setRouteFallbackHandler((ctx, res) => {
84
+ return res.status404('Not Found');
85
+ });
86
+
87
+ const handler = lambder.getHandler();
88
+ const event = createMockEvent('/goodbye');
89
+ const result = await handler(event, createMockContext());
90
+
91
+ expect(result.statusCode).toBe(404);
92
+ });
93
+
94
+ it('should match multiple routes', async () => {
95
+ const lambder = new Lambder({
96
+ publicPath: './public',
97
+ apiPath: '/api'
98
+ })
99
+ .addRoute('/home', (ctx, res) => {
100
+ return res.html('Home Page');
101
+ })
102
+ .addRoute('/about', (ctx, res) => {
103
+ return res.html('About Page');
104
+ })
105
+ .addRoute('/contact', (ctx, res) => {
106
+ return res.html('Contact Page');
107
+ });
108
+
109
+ const handler = lambder.getHandler();
110
+
111
+ const homeResult = await handler(createMockEvent('/home'), createMockContext());
112
+ expect(Buffer.from(homeResult.body || '', 'base64').toString()).toBe('Home Page');
113
+
114
+ const aboutResult = await handler(createMockEvent('/about'), createMockContext());
115
+ expect(Buffer.from(aboutResult.body || '', 'base64').toString()).toBe('About Page');
116
+
117
+ const contactResult = await handler(createMockEvent('/contact'), createMockContext());
118
+ expect(Buffer.from(contactResult.body || '', 'base64').toString()).toBe('Contact Page');
119
+ });
120
+ });
121
+
122
+ describe('Routes - Path Parameters', () => {
123
+ it('should extract path parameters from string patterns', async () => {
124
+ const lambder = new Lambder({
125
+ publicPath: './public',
126
+ apiPath: '/api'
127
+ })
128
+ .addRoute('/user/:userId', (ctx, res) => {
129
+ return res.json({ userId: ctx.pathParams?.userId });
130
+ });
131
+
132
+ const handler = lambder.getHandler();
133
+ const event = createMockEvent('/user/123');
134
+ const result = await handler(event, createMockContext());
135
+
136
+ expect(result.statusCode).toBe(200);
137
+ const body = JSON.parse(result.body || '{}');
138
+ expect(body.userId).toBe('123');
139
+ });
140
+
141
+ it('should extract multiple path parameters', async () => {
142
+ const lambder = new Lambder({
143
+ publicPath: './public',
144
+ apiPath: '/api'
145
+ })
146
+ .addRoute('/users/:userId/posts/:postId', (ctx, res) => {
147
+ return res.json({
148
+ userId: ctx.pathParams?.userId,
149
+ postId: ctx.pathParams?.postId
150
+ });
151
+ });
152
+
153
+ const handler = lambder.getHandler();
154
+ const event = createMockEvent('/users/456/posts/789');
155
+ const result = await handler(event, createMockContext());
156
+
157
+ const body = JSON.parse(result.body || '{}');
158
+ expect(body.userId).toBe('456');
159
+ expect(body.postId).toBe('789');
160
+ });
161
+
162
+ it('should handle optional parameters', async () => {
163
+ const lambder = new Lambder({
164
+ publicPath: './public',
165
+ apiPath: '/api'
166
+ })
167
+ .addRoute('/files/:path*', (ctx, res) => {
168
+ return res.json({ path: ctx.pathParams?.path });
169
+ });
170
+
171
+ const handler = lambder.getHandler();
172
+ const event = createMockEvent('/files/documents/report.pdf');
173
+ const result = await handler(event, createMockContext());
174
+
175
+ const body = JSON.parse(result.body || '{}');
176
+ expect(body.path).toBeTruthy();
177
+ });
178
+ });
179
+
180
+ describe('Routes - RegExp Matching', () => {
181
+ it('should match routes using RegExp', async () => {
182
+ const lambder = new Lambder({
183
+ publicPath: './public',
184
+ apiPath: '/api'
185
+ })
186
+ .addRoute(/^\/admin/, (ctx, res) => {
187
+ return res.html('Admin Area');
188
+ });
189
+
190
+ const handler = lambder.getHandler();
191
+
192
+ const adminResult = await handler(createMockEvent('/admin'), createMockContext());
193
+ expect(Buffer.from(adminResult.body || '', 'base64').toString()).toBe('Admin Area');
194
+
195
+ const adminDashResult = await handler(createMockEvent('/admin/dashboard'), createMockContext());
196
+ expect(Buffer.from(adminDashResult.body || '', 'base64').toString()).toBe('Admin Area');
197
+ });
198
+
199
+ it('should extract regex match groups', async () => {
200
+ const lambder = new Lambder({
201
+ publicPath: './public',
202
+ apiPath: '/api'
203
+ })
204
+ .addRoute(/^\/products\/(\d+)$/, (ctx, res) => {
205
+ const productId = ctx.pathParams?.[1];
206
+ return res.json({ productId });
207
+ });
208
+
209
+ const handler = lambder.getHandler();
210
+ const event = createMockEvent('/products/999');
211
+ const result = await handler(event, createMockContext());
212
+
213
+ const body = JSON.parse(result.body || '{}');
214
+ expect(body.productId).toBe('999');
215
+ });
216
+
217
+ it('should support complex regex patterns', async () => {
218
+ const lambder = new Lambder({
219
+ publicPath: './public',
220
+ apiPath: '/api'
221
+ })
222
+ .addRoute(/^\/api\/v\d+/, (ctx, res) => {
223
+ return res.json({ matched: true });
224
+ });
225
+
226
+ const handler = lambder.getHandler();
227
+
228
+ const v1Result = await handler(createMockEvent('/api/v1'), createMockContext());
229
+ expect(JSON.parse(v1Result.body || '{}').matched).toBe(true);
230
+
231
+ const v2Result = await handler(createMockEvent('/api/v2'), createMockContext());
232
+ expect(JSON.parse(v2Result.body || '{}').matched).toBe(true);
233
+ });
234
+ });
235
+
236
+ describe('Routes - Function-based Conditional Routing', () => {
237
+ it('should match routes using custom functions', async () => {
238
+ const lambder = new Lambder({
239
+ publicPath: './public',
240
+ apiPath: '/api'
241
+ })
242
+ .addRoute((ctx) => ctx.path.startsWith('/custom'), (ctx, res) => {
243
+ return res.html('Custom Route');
244
+ });
245
+
246
+ const handler = lambder.getHandler();
247
+ const event = createMockEvent('/custom/anything');
248
+ const result = await handler(event, createMockContext());
249
+
250
+ expect(Buffer.from(result.body || '', 'base64').toString()).toBe('Custom Route');
251
+ });
252
+
253
+ it('should support complex conditional logic', async () => {
254
+ const lambder = new Lambder({
255
+ publicPath: './public',
256
+ apiPath: '/api'
257
+ })
258
+ .addRoute(
259
+ (ctx) => ctx.path === '/special' && ctx.get.key === 'secret',
260
+ (ctx, res) => {
261
+ return res.html('Special Access');
262
+ }
263
+ );
264
+
265
+ const handler = lambder.getHandler();
266
+
267
+ // Without query param
268
+ const event1 = createMockEvent('/special');
269
+ const result1 = await handler(event1, createMockContext());
270
+ expect(result1.statusCode).toBe(204); // Fallback
271
+
272
+ // With query param
273
+ const event2: APIGatewayProxyEvent = {
274
+ ...createMockEvent('/special'),
275
+ queryStringParameters: { key: 'secret' }
276
+ };
277
+ const result2 = await handler(event2, createMockContext());
278
+ expect(Buffer.from(result2.body || '', 'base64').toString()).toBe('Special Access');
279
+ });
280
+
281
+ it('should access context variables in condition', async () => {
282
+ const lambder = new Lambder({
283
+ publicPath: './public',
284
+ apiPath: '/api'
285
+ })
286
+ .addRoute(
287
+ (ctx) => ctx.host === 'admin.example.com' && ctx.path === '/dashboard',
288
+ (ctx, res) => {
289
+ return res.html('Admin Dashboard');
290
+ }
291
+ );
292
+
293
+ const handler = lambder.getHandler();
294
+
295
+ const event: APIGatewayProxyEvent = {
296
+ ...createMockEvent('/dashboard'),
297
+ headers: { Host: 'admin.example.com' }
298
+ };
299
+ const result = await handler(event, createMockContext());
300
+
301
+ expect(Buffer.from(result.body || '', 'base64').toString()).toBe('Admin Dashboard');
302
+ });
303
+ });
304
+
305
+ describe('Routes - Session Protected Routes', () => {
306
+ beforeEach(() => {
307
+ ddbMock.reset();
308
+ });
309
+
310
+ it('should protect routes with addSessionRoute', async () => {
311
+ const mockSession = {
312
+ pk: 'hash',
313
+ sk: 'sortkey',
314
+ sessionToken: 'hash:sortkey',
315
+ csrfToken: 'csrf-token',
316
+ sessionKey: 'user-123',
317
+ data: { userId: '123', role: 'user' },
318
+ createdAt: Math.floor(Date.now() / 1000),
319
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
320
+ lastAccessedAt: Math.floor(Date.now() / 1000),
321
+ ttlInSeconds: 3600,
322
+ };
323
+
324
+ ddbMock.on(GetCommand).resolves({ Item: mockSession });
325
+ ddbMock.on(PutCommand).resolves({});
326
+
327
+ const lambder = new Lambder({
328
+ publicPath: './public',
329
+ apiPath: '/api'
330
+ })
331
+ .enableDdbSession(
332
+ {
333
+ tableName: 'test-sessions',
334
+ tableRegion: 'us-east-1',
335
+ sessionSalt: 'test-salt',
336
+ },
337
+ { partitionKey: 'pk', sortKey: 'sk' }
338
+ )
339
+ .setGlobalErrorHandler((err, ctx, res) => {
340
+ return res.html(`<h1>Error: ${err.message}</h1>`);
341
+ })
342
+ .addSessionRoute('/protected', (ctx, res) => {
343
+ return res.html(`Welcome ${ctx.session.data.userId}`);
344
+ });
345
+
346
+ const handler = lambder.getHandler();
347
+ const event = createMockEvent('/protected', 'GET', 'hash:sortkey');
348
+ const result = await handler(event, createMockContext());
349
+
350
+ expect(result.statusCode).toBe(200);
351
+ expect(Buffer.from(result.body || '', 'base64').toString()).toContain('Welcome 123');
352
+ });
353
+
354
+ it('should reject access without valid session', async () => {
355
+ ddbMock.on(GetCommand).resolves({}); // No session found
356
+
357
+ const lambder = new Lambder({
358
+ publicPath: './public',
359
+ apiPath: '/api'
360
+ })
361
+ .enableDdbSession(
362
+ {
363
+ tableName: 'test-sessions',
364
+ tableRegion: 'us-east-1',
365
+ sessionSalt: 'test-salt',
366
+ },
367
+ { partitionKey: 'pk', sortKey: 'sk' }
368
+ )
369
+ .addSessionRoute('/protected', (ctx, res) => {
370
+ return res.html('Protected');
371
+ })
372
+ .setGlobalErrorHandler((err, ctx, res) => {
373
+ return res.raw({ statusCode: 401, body: 'Unauthorized' });
374
+ });
375
+
376
+ const handler = lambder.getHandler();
377
+ const event = createMockEvent('/protected', 'GET', 'invalid-token');
378
+ const result = await handler(event, createMockContext());
379
+
380
+ expect(result.statusCode).toBe(401);
381
+ });
382
+
383
+ it('should access session data in session routes', async () => {
384
+ const mockSession = {
385
+ pk: 'hash',
386
+ sk: 'sortkey',
387
+ sessionToken: 'hash:sortkey',
388
+ csrfToken: 'csrf-token',
389
+ sessionKey: 'user-456',
390
+ data: { userId: '456', username: 'testuser', role: 'admin' },
391
+ createdAt: Math.floor(Date.now() / 1000),
392
+ expiresAt: Math.floor(Date.now() / 1000) + 3600,
393
+ lastAccessedAt: Math.floor(Date.now() / 1000),
394
+ ttlInSeconds: 3600,
395
+ };
396
+
397
+ ddbMock.on(GetCommand).resolves({ Item: mockSession });
398
+ ddbMock.on(PutCommand).resolves({});
399
+
400
+ const lambder = new Lambder({
401
+ publicPath: './public',
402
+ apiPath: '/api'
403
+ })
404
+ .enableDdbSession(
405
+ {
406
+ tableName: 'test-sessions',
407
+ tableRegion: 'us-east-1',
408
+ sessionSalt: 'test-salt',
409
+ },
410
+ { partitionKey: 'pk', sortKey: 'sk' }
411
+ )
412
+ .setGlobalErrorHandler((err, ctx, res) => {
413
+ return res.json({ error: err.message });
414
+ })
415
+ .addSessionRoute('/profile', (ctx, res) => {
416
+ return res.json({
417
+ sessionKey: ctx.session.sessionKey,
418
+ username: ctx.session.data.username,
419
+ role: ctx.session.data.role
420
+ });
421
+ });
422
+
423
+ const handler = lambder.getHandler();
424
+ const event = createMockEvent('/profile', 'GET', 'hash:sortkey');
425
+ const result = await handler(event, createMockContext());
426
+
427
+ const body = JSON.parse(result.body || '{}');
428
+ expect(body.sessionKey).toBe('user-456');
429
+ expect(body.username).toBe('testuser');
430
+ expect(body.role).toBe('admin');
431
+ });
432
+ });
433
+
434
+ describe('Routes - Priority and Ordering', () => {
435
+ it('should match first defined route when multiple routes match', async () => {
436
+ const lambder = new Lambder({
437
+ publicPath: './public',
438
+ apiPath: '/api'
439
+ })
440
+ .addRoute('/item', (ctx, res) => {
441
+ return res.html('Exact Match');
442
+ })
443
+ .addRoute(/^\/item/, (ctx, res) => {
444
+ return res.html('Regex Match');
445
+ });
446
+
447
+ const handler = lambder.getHandler();
448
+ const event = createMockEvent('/item');
449
+ const result = await handler(event, createMockContext());
450
+
451
+ // First route should win
452
+ expect(Buffer.from(result.body || '', 'base64').toString()).toBe('Exact Match');
453
+ });
454
+
455
+ it('should respect route definition order', async () => {
456
+ const lambder = new Lambder({
457
+ publicPath: './public',
458
+ apiPath: '/api'
459
+ })
460
+ .addRoute('/users/admin', (ctx, res) => {
461
+ return res.html('Admin User');
462
+ })
463
+ .addRoute('/users/:userId', (ctx, res) => {
464
+ return res.html(`User ${ctx.pathParams?.userId}`);
465
+ });
466
+
467
+ const handler = lambder.getHandler();
468
+
469
+ // Should match specific route first
470
+ const adminResult = await handler(createMockEvent('/users/admin'), createMockContext());
471
+ expect(Buffer.from(adminResult.body || '', 'base64').toString()).toBe('Admin User');
472
+
473
+ // Should match parameterized route
474
+ const userResult = await handler(createMockEvent('/users/123'), createMockContext());
475
+ expect(Buffer.from(userResult.body || '', 'base64').toString()).toContain('User 123');
476
+ });
477
+ });
478
+
479
+ describe('Routes - Wildcard and Catch-all Routes', () => {
480
+ it('should support wildcard routes', async () => {
481
+ const lambder = new Lambder({
482
+ publicPath: './public',
483
+ apiPath: '/api'
484
+ })
485
+ .addRoute('/(.*)', (ctx, res) => {
486
+ return res.html('Catch All');
487
+ });
488
+
489
+ const handler = lambder.getHandler();
490
+
491
+ const result1 = await handler(createMockEvent('/anything'), createMockContext());
492
+ expect(Buffer.from(result1.body || '', 'base64').toString()).toBe('Catch All');
493
+
494
+ const result2 = await handler(createMockEvent('/deeply/nested/path'), createMockContext());
495
+ expect(Buffer.from(result2.body || '', 'base64').toString()).toBe('Catch All');
496
+ });
497
+
498
+ it('should use wildcard as final fallback', async () => {
499
+ const lambder = new Lambder({
500
+ publicPath: './public',
501
+ apiPath: '/api'
502
+ })
503
+ .addRoute('/specific', (ctx, res) => {
504
+ return res.html('Specific');
505
+ })
506
+ .addRoute('/(.*)', (ctx, res) => {
507
+ return res.html('Fallback');
508
+ });
509
+
510
+ const handler = lambder.getHandler();
511
+
512
+ const specificResult = await handler(createMockEvent('/specific'), createMockContext());
513
+ expect(Buffer.from(specificResult.body || '', 'base64').toString()).toBe('Specific');
514
+
515
+ const fallbackResult = await handler(createMockEvent('/anything-else'), createMockContext());
516
+ expect(Buffer.from(fallbackResult.body || '', 'base64').toString()).toBe('Fallback');
517
+ });
518
+ });
519
+
520
+ describe('Routes - Method Filtering', () => {
521
+ it('should only match GET requests', async () => {
522
+ const lambder = new Lambder({
523
+ publicPath: './public',
524
+ apiPath: '/api'
525
+ })
526
+ .addRoute('/resource', (ctx, res) => {
527
+ return res.html('GET Response');
528
+ });
529
+
530
+ const handler = lambder.getHandler();
531
+
532
+ // GET should work
533
+ const getEvent = createMockEvent('/resource', 'GET');
534
+ const getResult = await handler(getEvent, createMockContext());
535
+ expect(Buffer.from(getResult.body || '', 'base64').toString()).toBe('GET Response');
536
+
537
+ // POST should not match routes (should hit fallback)
538
+ const postEvent = createMockEvent('/resource', 'POST');
539
+ const postResult = await handler(postEvent, createMockContext());
540
+ expect(postResult.statusCode).toBe(204); // Default fallback
541
+ });
542
+ });
@@ -11,6 +11,7 @@
11
11
  import { describe, it, expect, beforeEach, vi } from 'vitest';
12
12
  import { mockClient } from 'aws-sdk-client-mock';
13
13
  import { DynamoDBDocumentClient, GetCommand, PutCommand, DeleteCommand, QueryCommand } from '@aws-sdk/lib-dynamodb';
14
+ import { z } from 'zod';
14
15
  import LambderSessionManager, { type LambderSessionContext } from '../src/LambderSessionManager.js';
15
16
  import LambderSessionController from '../src/LambderSessionController.js';
16
17
  import Lambder from '../src/Lambder.js';
@@ -738,7 +739,7 @@ describe('LambderSessionController', () => {
738
739
  });
739
740
 
740
741
  describe('Session Endpoint Protection', () => {
741
- let lambder: Lambder<any, UserSessionData>;
742
+ let lambder: Lambder<UserSessionData>;
742
743
 
743
744
  const createMockEvent = (path: string, method: string, sessionToken?: string, apiName?: string, payload?: any, csrfToken?: string): APIGatewayProxyEvent => {
744
745
  const cookieHeader = sessionToken ? `sessionToken=${sessionToken}` : '';
@@ -787,24 +788,22 @@ describe('Session Endpoint Protection', () => {
787
788
  publicPath: '/public',
788
789
  apiPath: '/api',
789
790
  ejsPath: '/views',
790
- });
791
-
792
- lambder.enableDdbSession(
793
- {
794
- tableName: 'test-sessions',
795
- tableRegion: 'us-east-1',
796
- sessionSalt: 'test-salt',
797
- },
798
- { partitionKey: 'pk', sortKey: 'sk' }
799
- );
800
-
801
- // Set up error handler to expose actual error messages for testing
802
- lambder.setGlobalErrorHandler((err, ctx, responseBuilder) => {
803
- if (ctx?._otherInternal.isApiCall) {
804
- return responseBuilder.api({ error: err.message });
805
- }
806
- return responseBuilder.html(`<h1>Error: ${err.message}</h1>`);
807
- });
791
+ })
792
+ .enableDdbSession(
793
+ {
794
+ tableName: 'test-sessions',
795
+ tableRegion: 'us-east-1',
796
+ sessionSalt: 'test-salt',
797
+ },
798
+ { partitionKey: 'pk', sortKey: 'sk' }
799
+ )
800
+ // Set up error handler to expose actual error messages for testing
801
+ .setGlobalErrorHandler((err, ctx, responseBuilder) => {
802
+ if (ctx?._otherInternal.isApiCall) {
803
+ return responseBuilder.api({ error: err.message });
804
+ }
805
+ return responseBuilder.html(`<h1>Error: ${err.message}</h1>`);
806
+ });
808
807
  });
809
808
 
810
809
  describe('addSessionRoute', () => {
@@ -904,7 +903,10 @@ describe('Session Endpoint Protection', () => {
904
903
  it('should throw error when no session exists', async () => {
905
904
  ddbMock.on(GetCommand).resolves({}); // No session found
906
905
 
907
- lambder.addSessionApi('user.profile', async (ctx, resolver) => {
906
+ lambder.addSessionApi('user.profile', {
907
+ input: z.any(),
908
+ output: z.any()
909
+ }, async (ctx, resolver) => {
908
910
  return resolver.api({ userId: ctx.session.data.userId });
909
911
  });
910
912
 
@@ -937,7 +939,10 @@ describe('Session Endpoint Protection', () => {
937
939
  ddbMock.on(GetCommand).resolves({ Item: mockSession });
938
940
  ddbMock.on(PutCommand).resolves({});
939
941
 
940
- lambder.addSessionApi('user.profile', async (ctx, resolver) => {
942
+ lambder.addSessionApi('user.profile', {
943
+ input: z.any(),
944
+ output: z.any()
945
+ }, async (ctx, resolver) => {
941
946
  return resolver.api({ userId: ctx.session.data.userId });
942
947
  });
943
948
 
@@ -973,7 +978,10 @@ describe('Session Endpoint Protection', () => {
973
978
 
974
979
  ddbMock.on(GetCommand).resolves({ Item: mockSession });
975
980
 
976
- lambder.addSessionApi('user.profile', async (ctx, resolver) => {
981
+ lambder.addSessionApi('user.profile', {
982
+ input: z.any(),
983
+ output: z.any()
984
+ }, async (ctx, resolver) => {
977
985
  return resolver.api({ userId: ctx.session.data.userId });
978
986
  });
979
987
 
@@ -1005,7 +1013,10 @@ describe('Session Endpoint Protection', () => {
1005
1013
 
1006
1014
  ddbMock.on(GetCommand).resolves({ Item: mockSession });
1007
1015
 
1008
- lambder.addSessionApi('user.profile', async (ctx, resolver) => {
1016
+ lambder.addSessionApi('user.profile', {
1017
+ input: z.any(),
1018
+ output: z.any()
1019
+ }, async (ctx, resolver) => {
1009
1020
  return resolver.api({ userId: ctx.session.data.userId });
1010
1021
  });
1011
1022
 
@@ -1038,7 +1049,10 @@ describe('Session Endpoint Protection', () => {
1038
1049
  ddbMock.on(GetCommand).resolves({ Item: mockSession });
1039
1050
  ddbMock.on(PutCommand).resolves({});
1040
1051
 
1041
- lambder.addSessionApi('user.profile', async (ctx, resolver) => {
1052
+ lambder.addSessionApi('user.profile', {
1053
+ input: z.any(),
1054
+ output: z.any()
1055
+ }, async (ctx, resolver) => {
1042
1056
  // Type test: ctx.session.data should have UserSessionData type
1043
1057
  const userId: string = ctx.session.data.userId;
1044
1058
  const username: string = ctx.session.data.username;