lambder 1.0.147 → 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 -4
  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 -7
  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,561 @@
1
+ /**
2
+ * Hooks System Tests
3
+ *
4
+ * Tests for the hooks system including:
5
+ * - beforeRender hook execution
6
+ * - afterRender hook execution
7
+ * - fallback hook execution
8
+ * - created hook execution
9
+ * - Hook priority ordering
10
+ * - Multiple hooks of same type
11
+ * - Hook error handling
12
+ * - Context modification in hooks
13
+ * - Response modification in hooks
14
+ */
15
+
16
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
17
+ import { z } from 'zod';
18
+ import Lambder from '../src/Lambder.js';
19
+ import type { APIGatewayProxyEvent, Context } from 'aws-lambda';
20
+
21
+ const createMockEvent = (path: string, method: string = 'GET', apiName?: string, payload?: any): APIGatewayProxyEvent => ({
22
+ body: apiName ? JSON.stringify({ apiName, payload }) : null,
23
+ headers: { Host: 'localhost' },
24
+ multiValueHeaders: {},
25
+ httpMethod: method,
26
+ isBase64Encoded: false,
27
+ path: apiName ? '/api' : path,
28
+ pathParameters: null,
29
+ queryStringParameters: null,
30
+ multiValueQueryStringParameters: null,
31
+ stageVariables: null,
32
+ requestContext: {} as any,
33
+ resource: '',
34
+ });
35
+
36
+ const createMockContext = (): Context => ({
37
+ callbackWaitsForEmptyEventLoop: false,
38
+ functionName: 'test',
39
+ functionVersion: '1',
40
+ invokedFunctionArn: 'arn',
41
+ memoryLimitInMB: '128',
42
+ awsRequestId: '123',
43
+ logGroupName: 'group',
44
+ logStreamName: 'stream',
45
+ getRemainingTimeInMillis: () => 1000,
46
+ done: () => {},
47
+ fail: () => {},
48
+ succeed: () => {},
49
+ });
50
+
51
+ describe('Hooks - beforeRender Hook', () => {
52
+ it('should execute beforeRender hook before route handler', async () => {
53
+ const executionOrder: string[] = [];
54
+
55
+ const lambder = new Lambder({
56
+ publicPath: './public',
57
+ apiPath: '/api'
58
+ });
59
+
60
+ await lambder.addHook('beforeRender', async (ctx, res) => {
61
+ executionOrder.push('beforeRender');
62
+ return ctx;
63
+ });
64
+
65
+ lambder.addRoute('/test', (ctx, res) => {
66
+ executionOrder.push('routeHandler');
67
+ return res.html('Test');
68
+ });
69
+
70
+ const handler = lambder.getHandler();
71
+ await handler(createMockEvent('/test'), createMockContext());
72
+
73
+ expect(executionOrder).toEqual(['beforeRender', 'routeHandler']);
74
+ });
75
+
76
+ it('should allow context modification in beforeRender', async () => {
77
+ const lambder = new Lambder({
78
+ publicPath: './public',
79
+ apiPath: '/api'
80
+ });
81
+
82
+ await lambder.addHook('beforeRender', async (ctx, res) => {
83
+ // Add custom property to context
84
+ (ctx as any).customData = 'modified';
85
+ return ctx;
86
+ });
87
+
88
+ lambder.addRoute('/test', (ctx, res) => {
89
+ return res.json({ data: (ctx as any).customData });
90
+ });
91
+
92
+ const handler = lambder.getHandler();
93
+ const result = await handler(createMockEvent('/test'), createMockContext());
94
+
95
+ const body = JSON.parse(result.body || '{}');
96
+ expect(body.data).toBe('modified');
97
+ });
98
+
99
+ it('should execute multiple beforeRender hooks', async () => {
100
+ const executionOrder: number[] = [];
101
+
102
+ const lambder = new Lambder({
103
+ publicPath: './public',
104
+ apiPath: '/api'
105
+ });
106
+
107
+ await lambder.addHook('beforeRender', async (ctx, res) => {
108
+ executionOrder.push(1);
109
+ return ctx;
110
+ });
111
+
112
+ await lambder.addHook('beforeRender', async (ctx, res) => {
113
+ executionOrder.push(2);
114
+ return ctx;
115
+ });
116
+
117
+ await lambder.addHook('beforeRender', async (ctx, res) => {
118
+ executionOrder.push(3);
119
+ return ctx;
120
+ });
121
+
122
+ lambder.addRoute('/test', (ctx, res) => res.html('Test'));
123
+
124
+ const handler = lambder.getHandler();
125
+ await handler(createMockEvent('/test'), createMockContext());
126
+
127
+ expect(executionOrder).toEqual([1, 2, 3]);
128
+ });
129
+
130
+ it('should stop execution if beforeRender returns Error', async () => {
131
+ let routeHandlerCalled = false;
132
+
133
+ const lambder = new Lambder({
134
+ publicPath: './public',
135
+ apiPath: '/api'
136
+ })
137
+ .setGlobalErrorHandler((err, ctx, res) => {
138
+ return res.raw({ statusCode: 403, body: err.message });
139
+ });
140
+
141
+ await lambder.addHook('beforeRender', async (ctx, res) => {
142
+ return new Error('Access Denied');
143
+ });
144
+
145
+ lambder.addRoute('/test', (ctx, res) => {
146
+ routeHandlerCalled = true;
147
+ return res.html('Test');
148
+ });
149
+
150
+ const handler = lambder.getHandler();
151
+ const result = await handler(createMockEvent('/test'), createMockContext());
152
+
153
+ expect(result.statusCode).toBe(403);
154
+ expect(result.body).toBe('Access Denied');
155
+ expect(routeHandlerCalled).toBe(false);
156
+ });
157
+ });
158
+
159
+ describe('Hooks - afterRender Hook', () => {
160
+ it('should execute afterRender hook after route handler', async () => {
161
+ const executionOrder: string[] = [];
162
+
163
+ const lambder = new Lambder({
164
+ publicPath: './public',
165
+ apiPath: '/api'
166
+ });
167
+
168
+ lambder.addRoute('/test', (ctx, res) => {
169
+ executionOrder.push('routeHandler');
170
+ return res.html('Test');
171
+ });
172
+
173
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
174
+ executionOrder.push('afterRender');
175
+ return response;
176
+ });
177
+
178
+ const handler = lambder.getHandler();
179
+ await handler(createMockEvent('/test'), createMockContext());
180
+
181
+ expect(executionOrder).toEqual(['routeHandler', 'afterRender']);
182
+ });
183
+
184
+ it('should allow response modification in afterRender', async () => {
185
+ const lambder = new Lambder({
186
+ publicPath: './public',
187
+ apiPath: '/api'
188
+ });
189
+
190
+ lambder.addRoute('/test', (ctx, res) => {
191
+ return res.json({ original: true });
192
+ });
193
+
194
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
195
+ // Modify response body
196
+ const body = JSON.parse(response.body || '{}');
197
+ body.modified = true;
198
+ response.body = JSON.stringify(body);
199
+ return response;
200
+ });
201
+
202
+ const handler = lambder.getHandler();
203
+ const result = await handler(createMockEvent('/test'), createMockContext());
204
+
205
+ const body = JSON.parse(result.body || '{}');
206
+ expect(body.original).toBe(true);
207
+ expect(body.modified).toBe(true);
208
+ });
209
+
210
+ it('should execute multiple afterRender hooks', async () => {
211
+ const executionOrder: number[] = [];
212
+
213
+ const lambder = new Lambder({
214
+ publicPath: './public',
215
+ apiPath: '/api'
216
+ });
217
+
218
+ lambder.addRoute('/test', (ctx, res) => res.html('Test'));
219
+
220
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
221
+ executionOrder.push(1);
222
+ return response;
223
+ });
224
+
225
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
226
+ executionOrder.push(2);
227
+ return response;
228
+ });
229
+
230
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
231
+ executionOrder.push(3);
232
+ return response;
233
+ });
234
+
235
+ const handler = lambder.getHandler();
236
+ await handler(createMockEvent('/test'), createMockContext());
237
+
238
+ expect(executionOrder).toEqual([1, 2, 3]);
239
+ });
240
+
241
+ it('should add custom headers in afterRender', async () => {
242
+ const lambder = new Lambder({
243
+ publicPath: './public',
244
+ apiPath: '/api'
245
+ });
246
+
247
+ lambder.addRoute('/test', (ctx, res) => res.json({ data: 'test' }));
248
+
249
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
250
+ response.multiValueHeaders = response.multiValueHeaders || {};
251
+ response.multiValueHeaders['X-Custom-Header'] = ['CustomValue'];
252
+ return response;
253
+ });
254
+
255
+ const handler = lambder.getHandler();
256
+ const result = await handler(createMockEvent('/test'), createMockContext());
257
+
258
+ expect(result.multiValueHeaders?.['X-Custom-Header']).toEqual(['CustomValue']);
259
+ });
260
+
261
+ it('should stop execution if afterRender returns Error', async () => {
262
+ const lambder = new Lambder({
263
+ publicPath: './public',
264
+ apiPath: '/api'
265
+ })
266
+ .setGlobalErrorHandler((err, ctx, res) => {
267
+ return res.raw({ statusCode: 500, body: `Error: ${err.message}` });
268
+ });
269
+
270
+ lambder.addRoute('/test', (ctx, res) => res.html('Test'));
271
+
272
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
273
+ return new Error('Post-processing failed');
274
+ });
275
+
276
+ const handler = lambder.getHandler();
277
+ const result = await handler(createMockEvent('/test'), createMockContext());
278
+
279
+ expect(result.statusCode).toBe(500);
280
+ expect(result.body).toContain('Post-processing failed');
281
+ });
282
+ });
283
+
284
+ describe('Hooks - fallback Hook', () => {
285
+ it('should execute fallback hook when no route matches', async () => {
286
+ let fallbackCalled = false;
287
+
288
+ const lambder = new Lambder({
289
+ publicPath: './public',
290
+ apiPath: '/api'
291
+ });
292
+
293
+ lambder.addRoute('/exists', (ctx, res) => res.html('Exists'));
294
+
295
+ await lambder.addHook('fallback', async (ctx, res) => {
296
+ fallbackCalled = true;
297
+ });
298
+
299
+ lambder.setRouteFallbackHandler((ctx, res) => {
300
+ return res.status404('Not Found');
301
+ });
302
+
303
+ const handler = lambder.getHandler();
304
+ await handler(createMockEvent('/nonexistent'), createMockContext());
305
+
306
+ expect(fallbackCalled).toBe(true);
307
+ });
308
+
309
+ it('should execute multiple fallback hooks', async () => {
310
+ const executionOrder: number[] = [];
311
+
312
+ const lambder = new Lambder({
313
+ publicPath: './public',
314
+ apiPath: '/api'
315
+ });
316
+
317
+ await lambder.addHook('fallback', async (ctx, res) => {
318
+ executionOrder.push(1);
319
+ });
320
+
321
+ await lambder.addHook('fallback', async (ctx, res) => {
322
+ executionOrder.push(2);
323
+ });
324
+
325
+ lambder.setRouteFallbackHandler((ctx, res) => res.status404('Not Found'));
326
+
327
+ const handler = lambder.getHandler();
328
+ await handler(createMockEvent('/nonexistent'), createMockContext());
329
+
330
+ expect(executionOrder).toEqual([1, 2]);
331
+ });
332
+
333
+ it('should not execute fallback hook when route matches', async () => {
334
+ let fallbackCalled = false;
335
+
336
+ const lambder = new Lambder({
337
+ publicPath: './public',
338
+ apiPath: '/api'
339
+ });
340
+
341
+ lambder.addRoute('/test', (ctx, res) => res.html('Test'));
342
+
343
+ await lambder.addHook('fallback', async (ctx, res) => {
344
+ fallbackCalled = true;
345
+ });
346
+
347
+ const handler = lambder.getHandler();
348
+ await handler(createMockEvent('/test'), createMockContext());
349
+
350
+ expect(fallbackCalled).toBe(false);
351
+ });
352
+ });
353
+
354
+ describe('Hooks - created Hook', () => {
355
+ it('should execute created hook immediately', async () => {
356
+ let createdCalled = false;
357
+ let lambderInstance: Lambder | null = null;
358
+
359
+ const lambder = new Lambder({
360
+ publicPath: './public',
361
+ apiPath: '/api'
362
+ });
363
+
364
+ await lambder.addHook('created', async (instance) => {
365
+ createdCalled = true;
366
+ lambderInstance = instance;
367
+ });
368
+
369
+ expect(createdCalled).toBe(true);
370
+ expect(lambderInstance).toBe(lambder);
371
+ });
372
+
373
+ it('should allow configuration in created hook', async () => {
374
+ const lambder = new Lambder({
375
+ publicPath: './public',
376
+ apiPath: '/api'
377
+ });
378
+
379
+ await lambder.addHook('created', async (instance) => {
380
+ instance.enableCors(true);
381
+ });
382
+
383
+ expect(lambder.isCorsEnabled).toBe(true);
384
+ });
385
+ });
386
+
387
+ describe('Hooks - Priority Ordering', () => {
388
+ it('should execute beforeRender hooks in priority order', async () => {
389
+ const executionOrder: number[] = [];
390
+
391
+ const lambder = new Lambder({
392
+ publicPath: './public',
393
+ apiPath: '/api'
394
+ });
395
+
396
+ await lambder.addHook('beforeRender', async (ctx, res) => {
397
+ executionOrder.push(20);
398
+ return ctx;
399
+ }, 20);
400
+
401
+ await lambder.addHook('beforeRender', async (ctx, res) => {
402
+ executionOrder.push(10);
403
+ return ctx;
404
+ }, 10);
405
+
406
+ await lambder.addHook('beforeRender', async (ctx, res) => {
407
+ executionOrder.push(30);
408
+ return ctx;
409
+ }, 30);
410
+
411
+ lambder.addRoute('/test', (ctx, res) => res.html('Test'));
412
+
413
+ const handler = lambder.getHandler();
414
+ await handler(createMockEvent('/test'), createMockContext());
415
+
416
+ // Should execute in priority order: 10, 20, 30
417
+ expect(executionOrder).toEqual([10, 20, 30]);
418
+ });
419
+
420
+ it('should execute afterRender hooks in priority order', async () => {
421
+ const executionOrder: number[] = [];
422
+
423
+ const lambder = new Lambder({
424
+ publicPath: './public',
425
+ apiPath: '/api'
426
+ });
427
+
428
+ lambder.addRoute('/test', (ctx, res) => res.html('Test'));
429
+
430
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
431
+ executionOrder.push(50);
432
+ return response;
433
+ }, 50);
434
+
435
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
436
+ executionOrder.push(5);
437
+ return response;
438
+ }, 5);
439
+
440
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
441
+ executionOrder.push(25);
442
+ return response;
443
+ }, 25);
444
+
445
+ const handler = lambder.getHandler();
446
+ await handler(createMockEvent('/test'), createMockContext());
447
+
448
+ expect(executionOrder).toEqual([5, 25, 50]);
449
+ });
450
+
451
+ it('should use priority 0 as default', async () => {
452
+ const executionOrder: string[] = [];
453
+
454
+ const lambder = new Lambder({
455
+ publicPath: './public',
456
+ apiPath: '/api'
457
+ });
458
+
459
+ await lambder.addHook('beforeRender', async (ctx, res) => {
460
+ executionOrder.push('no-priority');
461
+ return ctx;
462
+ }); // No priority specified, defaults to 0
463
+
464
+ await lambder.addHook('beforeRender', async (ctx, res) => {
465
+ executionOrder.push('negative');
466
+ return ctx;
467
+ }, -10);
468
+
469
+ await lambder.addHook('beforeRender', async (ctx, res) => {
470
+ executionOrder.push('positive');
471
+ return ctx;
472
+ }, 10);
473
+
474
+ lambder.addRoute('/test', (ctx, res) => res.html('Test'));
475
+
476
+ const handler = lambder.getHandler();
477
+ await handler(createMockEvent('/test'), createMockContext());
478
+
479
+ expect(executionOrder).toEqual(['negative', 'no-priority', 'positive']);
480
+ });
481
+ });
482
+
483
+ describe('Hooks - Combined Workflow', () => {
484
+ it('should execute hooks in correct order: beforeRender -> handler -> afterRender', async () => {
485
+ const executionOrder: string[] = [];
486
+
487
+ const lambder = new Lambder({
488
+ publicPath: './public',
489
+ apiPath: '/api'
490
+ });
491
+
492
+ await lambder.addHook('beforeRender', async (ctx, res) => {
493
+ executionOrder.push('before-1');
494
+ return ctx;
495
+ });
496
+
497
+ await lambder.addHook('beforeRender', async (ctx, res) => {
498
+ executionOrder.push('before-2');
499
+ return ctx;
500
+ });
501
+
502
+ lambder.addRoute('/test', (ctx, res) => {
503
+ executionOrder.push('handler');
504
+ return res.html('Test');
505
+ });
506
+
507
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
508
+ executionOrder.push('after-1');
509
+ return response;
510
+ });
511
+
512
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
513
+ executionOrder.push('after-2');
514
+ return response;
515
+ });
516
+
517
+ const handler = lambder.getHandler();
518
+ await handler(createMockEvent('/test'), createMockContext());
519
+
520
+ expect(executionOrder).toEqual([
521
+ 'before-1',
522
+ 'before-2',
523
+ 'handler',
524
+ 'after-1',
525
+ 'after-2'
526
+ ]);
527
+ });
528
+
529
+ it('should work with API handlers', async () => {
530
+ const executionOrder: string[] = [];
531
+
532
+ const lambder = new Lambder({
533
+ publicPath: './public',
534
+ apiPath: '/api'
535
+ });
536
+
537
+ await lambder.addHook('beforeRender', async (ctx, res) => {
538
+ executionOrder.push('beforeRender');
539
+ return ctx;
540
+ });
541
+
542
+ lambder.addApi('testApi', {
543
+ input: z.object({ value: z.string() }),
544
+ output: z.object({ result: z.string() })
545
+ }, async (ctx, res) => {
546
+ executionOrder.push('apiHandler');
547
+ return res.api({ result: ctx.apiPayload.value });
548
+ });
549
+
550
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
551
+ executionOrder.push('afterRender');
552
+ return response;
553
+ });
554
+
555
+ const handler = lambder.getHandler();
556
+ const event = createMockEvent('/api', 'POST', 'testApi', { value: 'test' });
557
+ await handler(event, createMockContext());
558
+
559
+ expect(executionOrder).toEqual(['beforeRender', 'apiHandler', 'afterRender']);
560
+ });
561
+ });