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.
Files changed (67) hide show
  1. package/Readme.md +162 -41
  2. package/dist/Lambder.d.ts +154 -46
  3. package/dist/Lambder.js +312 -166
  4. package/dist/LambderCaller.js +6 -3
  5. package/dist/LambderContext.d.ts +20 -9
  6. package/dist/LambderContext.js +57 -17
  7. package/dist/LambderCors.d.ts +12 -0
  8. package/dist/LambderCors.js +30 -0
  9. package/dist/LambderHtml.d.ts +33 -0
  10. package/dist/LambderHtml.js +62 -0
  11. package/dist/LambderMSW.d.ts +16 -1
  12. package/dist/LambderMSW.js +5 -9
  13. package/dist/LambderPublicFiles.d.ts +47 -0
  14. package/dist/LambderPublicFiles.js +108 -0
  15. package/dist/LambderResolver.d.ts +30 -31
  16. package/dist/LambderResolver.js +29 -43
  17. package/dist/LambderResponse.d.ts +71 -0
  18. package/dist/LambderResponse.js +196 -0
  19. package/dist/LambderResponseBuilder.d.ts +58 -33
  20. package/dist/LambderResponseBuilder.js +114 -167
  21. package/dist/LambderRouting.d.ts +23 -0
  22. package/dist/LambderRouting.js +67 -0
  23. package/dist/LambderSessionController.d.ts +13 -1
  24. package/dist/LambderSessionController.js +33 -10
  25. package/dist/LambderSessionManager.d.ts +3 -1
  26. package/dist/LambderSessionManager.js +15 -6
  27. package/dist/LambderTemplatingEngine.d.ts +87 -0
  28. package/dist/LambderTemplatingEngine.js +156 -0
  29. package/dist/index.d.ts +14 -2
  30. package/dist/index.js +10 -1
  31. package/dist/node-polyfills.d.ts +4 -2
  32. package/dist/node-polyfills.js +28 -0
  33. package/package.json +7 -5
  34. package/.eslintrc.cjs +0 -26
  35. package/.vscode/settings.json +0 -26
  36. package/deploy +0 -22
  37. package/dist/LambderUtils.d.ts +0 -10
  38. package/dist/LambderUtils.js +0 -70
  39. package/docs/DYNAMODB_SETUP.md +0 -96
  40. package/docs/LAMBDER_MSW.md +0 -409
  41. package/docs/TYPE_SAFE_QUICK_START.md +0 -77
  42. package/examples/msw-testing-example.ts +0 -280
  43. package/examples/secure-session-example.ts +0 -207
  44. package/examples/zod-chained-api-example.ts +0 -63
  45. package/src/Lambder.ts +0 -430
  46. package/src/LambderApiContract.ts +0 -20
  47. package/src/LambderCaller.ts +0 -238
  48. package/src/LambderContext.ts +0 -78
  49. package/src/LambderMSW.ts +0 -180
  50. package/src/LambderResolver.ts +0 -101
  51. package/src/LambderResponseBuilder.ts +0 -332
  52. package/src/LambderSessionController.ts +0 -114
  53. package/src/LambderSessionManager.ts +0 -217
  54. package/src/LambderUtils.ts +0 -75
  55. package/src/index.ts +0 -17
  56. package/src/node-polyfills.ts +0 -27
  57. package/tests/error-handling.test.ts +0 -585
  58. package/tests/file-serving.test.ts +0 -194
  59. package/tests/fixtures/public/index.html +0 -1
  60. package/tests/fixtures/public/main.css +0 -1
  61. package/tests/hooks.test.ts +0 -561
  62. package/tests/output-type-runtime.test.ts +0 -381
  63. package/tests/redirect.test.ts +0 -88
  64. package/tests/routes.test.ts +0 -543
  65. package/tests/session.test.ts +0 -1083
  66. package/tests/use-plugin.test.ts +0 -460
  67. package/tsconfig.json +0 -24
@@ -1,543 +0,0 @@
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 match all HTTP methods when no method is specified', async () => {
522
- const lambder = new Lambder({
523
- publicPath: './public',
524
- apiPath: '/api'
525
- })
526
- .addRoute('/resource', (ctx, res) => {
527
- return res.html(`${ctx.method} 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 also work (no method restriction)
538
- const postEvent = createMockEvent('/resource', 'POST');
539
- const postResult = await handler(postEvent, createMockContext());
540
- expect(postResult.statusCode).toBe(200);
541
- expect(Buffer.from(postResult.body || '', 'base64').toString()).toBe('POST Response');
542
- });
543
- });