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,585 @@
1
+ /**
2
+ * Error Handling Tests
3
+ *
4
+ * Tests for error handling including:
5
+ * - setGlobalErrorHandler functionality
6
+ * - Error handling with context available
7
+ * - Error handling with null context
8
+ * - Custom error responses
9
+ * - Error in API handlers
10
+ * - Error in route handlers
11
+ * - Error in hooks
12
+ * - Error log accumulation
13
+ */
14
+
15
+ import { describe, it, expect } from 'vitest';
16
+ import { z } from 'zod';
17
+ import Lambder from '../src/Lambder.js';
18
+ import type { APIGatewayProxyEvent, Context } from 'aws-lambda';
19
+
20
+ const createMockEvent = (path: string, method: string = 'GET', apiName?: string, payload?: any): APIGatewayProxyEvent => ({
21
+ body: apiName ? JSON.stringify({ apiName, payload }) : null,
22
+ headers: { Host: 'localhost' },
23
+ multiValueHeaders: {},
24
+ httpMethod: method,
25
+ isBase64Encoded: false,
26
+ path: apiName ? '/api' : path,
27
+ pathParameters: null,
28
+ queryStringParameters: null,
29
+ multiValueQueryStringParameters: null,
30
+ stageVariables: null,
31
+ requestContext: {} as any,
32
+ resource: '',
33
+ });
34
+
35
+ const createMockContext = (): Context => ({
36
+ callbackWaitsForEmptyEventLoop: false,
37
+ functionName: 'test',
38
+ functionVersion: '1',
39
+ invokedFunctionArn: 'arn',
40
+ memoryLimitInMB: '128',
41
+ awsRequestId: '123',
42
+ logGroupName: 'group',
43
+ logStreamName: 'stream',
44
+ getRemainingTimeInMillis: () => 1000,
45
+ done: () => {},
46
+ fail: () => {},
47
+ succeed: () => {},
48
+ });
49
+
50
+ describe('Error Handling - Global Error Handler', () => {
51
+ it('should catch errors in route handlers', async () => {
52
+ const lambder = new Lambder({
53
+ publicPath: './public',
54
+ apiPath: '/api'
55
+ })
56
+ .setGlobalErrorHandler((err, ctx, res) => {
57
+ return res.raw({
58
+ statusCode: 500,
59
+ body: `Error: ${err.message}`
60
+ });
61
+ })
62
+ .addRoute('/error', (ctx, res) => {
63
+ throw new Error('Route handler error');
64
+ });
65
+
66
+ const handler = lambder.getHandler();
67
+ const result = await handler(createMockEvent('/error'), createMockContext());
68
+
69
+ expect(result.statusCode).toBe(500);
70
+ expect(result.body).toBe('Error: Route handler error');
71
+ });
72
+
73
+ it('should catch errors in API handlers', async () => {
74
+ const lambder = new Lambder({
75
+ publicPath: './public',
76
+ apiPath: '/api'
77
+ })
78
+ .setGlobalErrorHandler((err, ctx, res) => {
79
+ if (ctx?._otherInternal.isApiCall) {
80
+ return res.api({ error: err.message });
81
+ }
82
+ return res.raw({ statusCode: 500, body: err.message });
83
+ })
84
+ .addApi('errorApi', {
85
+ input: z.object({ value: z.string() }),
86
+ output: z.object({ result: z.string() })
87
+ }, async (ctx, res) => {
88
+ throw new Error('API handler error');
89
+ });
90
+
91
+ const handler = lambder.getHandler();
92
+ const event = createMockEvent('/api', 'POST', 'errorApi', { value: 'test' });
93
+ const result = await handler(event, createMockContext());
94
+
95
+ expect(result.statusCode).toBe(200);
96
+ const body = JSON.parse(result.body || '{}');
97
+ expect(body.payload.error).toBe('API handler error');
98
+ });
99
+
100
+ it('should catch async errors', async () => {
101
+ const lambder = new Lambder({
102
+ publicPath: './public',
103
+ apiPath: '/api'
104
+ })
105
+ .setGlobalErrorHandler((err, ctx, res) => {
106
+ return res.raw({ statusCode: 500, body: err.message });
107
+ })
108
+ .addRoute('/async-error', async (ctx, res) => {
109
+ await Promise.resolve();
110
+ throw new Error('Async error');
111
+ });
112
+
113
+ const handler = lambder.getHandler();
114
+ const result = await handler(createMockEvent('/async-error'), createMockContext());
115
+
116
+ expect(result.statusCode).toBe(500);
117
+ expect(result.body).toBe('Async error');
118
+ });
119
+
120
+ it('should provide context in error handler', async () => {
121
+ let capturedContext: any = null;
122
+
123
+ const lambder = new Lambder({
124
+ publicPath: './public',
125
+ apiPath: '/api'
126
+ })
127
+ .setGlobalErrorHandler((err, ctx, res) => {
128
+ capturedContext = ctx;
129
+ return res.raw({ statusCode: 500, body: 'Error' });
130
+ })
131
+ .addRoute('/test', (ctx, res) => {
132
+ throw new Error('Test error');
133
+ });
134
+
135
+ const handler = lambder.getHandler();
136
+ await handler(createMockEvent('/test'), createMockContext());
137
+
138
+ expect(capturedContext).not.toBeNull();
139
+ expect(capturedContext.path).toBe('/test');
140
+ expect(capturedContext.method).toBe('GET');
141
+ });
142
+
143
+ it('should handle null context in error handler', async () => {
144
+ let capturedContext: any = undefined;
145
+
146
+ const lambder = new Lambder({
147
+ publicPath: './public',
148
+ apiPath: '/api'
149
+ })
150
+ .setGlobalErrorHandler((err, ctx, res) => {
151
+ capturedContext = ctx;
152
+ return res.raw({ statusCode: 500, body: 'Error occurred' });
153
+ });
154
+
155
+ const handler = lambder.getHandler();
156
+
157
+ // Create a malformed event that might cause early errors
158
+ const malformedEvent = {
159
+ ...createMockEvent('/test'),
160
+ headers: null as any
161
+ };
162
+
163
+ try {
164
+ await handler(malformedEvent, createMockContext());
165
+ } catch (e) {
166
+ // Expected to potentially fail
167
+ }
168
+
169
+ // Context might be null for early errors
170
+ expect(capturedContext === null || capturedContext !== undefined).toBe(true);
171
+ });
172
+ });
173
+
174
+ describe('Error Handling - Custom Error Responses', () => {
175
+ it('should return custom error format for APIs', async () => {
176
+ const lambder = new Lambder({
177
+ publicPath: './public',
178
+ apiPath: '/api'
179
+ })
180
+ .setGlobalErrorHandler((err, ctx, res) => {
181
+ if (ctx?._otherInternal.isApiCall) {
182
+ return res.api(
183
+ { success: false, error: err.message },
184
+ { errorMessage: err.message }
185
+ );
186
+ }
187
+ return res.raw({ statusCode: 500, body: err.message });
188
+ })
189
+ .addApi('testApi', {
190
+ input: z.void(),
191
+ output: z.object({ success: z.boolean(), error: z.string().optional() })
192
+ }, async (ctx, res) => {
193
+ throw new Error('Custom error message');
194
+ });
195
+
196
+ const handler = lambder.getHandler();
197
+ const event = createMockEvent('/api', 'POST', 'testApi', undefined);
198
+ const result = await handler(event, createMockContext());
199
+
200
+ const body = JSON.parse(result.body || '{}');
201
+ expect(body.payload.success).toBe(false);
202
+ expect(body.payload.error).toBe('Custom error message');
203
+ expect(body.errorMessage).toBe('Custom error message');
204
+ });
205
+
206
+ it('should return HTML error for routes', async () => {
207
+ const lambder = new Lambder({
208
+ publicPath: './public',
209
+ apiPath: '/api'
210
+ })
211
+ .setGlobalErrorHandler((err, ctx, res) => {
212
+ return res.html(`<h1>Error</h1><p>${err.message}</p>`);
213
+ })
214
+ .addRoute('/page', (ctx, res) => {
215
+ throw new Error('Page load failed');
216
+ });
217
+
218
+ const handler = lambder.getHandler();
219
+ const result = await handler(createMockEvent('/page'), createMockContext());
220
+
221
+ expect(result.statusCode).toBe(200);
222
+ const body = Buffer.from(result.body || '', 'base64').toString();
223
+ expect(body).toContain('<h1>Error</h1>');
224
+ expect(body).toContain('Page load failed');
225
+ });
226
+
227
+ it('should return JSON error for routes if desired', async () => {
228
+ const lambder = new Lambder({
229
+ publicPath: './public',
230
+ apiPath: '/api'
231
+ })
232
+ .setGlobalErrorHandler((err, ctx, res) => {
233
+ return res.json({
234
+ error: true,
235
+ message: err.message,
236
+ timestamp: Date.now()
237
+ });
238
+ })
239
+ .addRoute('/api-style-error', (ctx, res) => {
240
+ throw new Error('JSON error');
241
+ });
242
+
243
+ const handler = lambder.getHandler();
244
+ const result = await handler(createMockEvent('/api-style-error'), createMockContext());
245
+
246
+ const body = JSON.parse(result.body || '{}');
247
+ expect(body.error).toBe(true);
248
+ expect(body.message).toBe('JSON error');
249
+ expect(body.timestamp).toBeDefined();
250
+ });
251
+ });
252
+
253
+ describe('Error Handling - Different Error Types', () => {
254
+ it('should handle Error objects', async () => {
255
+ const lambder = new Lambder({
256
+ publicPath: './public',
257
+ apiPath: '/api'
258
+ })
259
+ .setGlobalErrorHandler((err, ctx, res) => {
260
+ return res.raw({ statusCode: 500, body: err.message });
261
+ })
262
+ .addRoute('/test', (ctx, res) => {
263
+ throw new Error('Standard Error');
264
+ });
265
+
266
+ const handler = lambder.getHandler();
267
+ const result = await handler(createMockEvent('/test'), createMockContext());
268
+
269
+ expect(result.body).toBe('Standard Error');
270
+ });
271
+
272
+ it('should handle TypeError', async () => {
273
+ const lambder = new Lambder({
274
+ publicPath: './public',
275
+ apiPath: '/api'
276
+ })
277
+ .setGlobalErrorHandler((err, ctx, res) => {
278
+ return res.raw({ statusCode: 500, body: `${err.name}: ${err.message}` });
279
+ })
280
+ .addRoute('/test', (ctx, res) => {
281
+ const obj: any = null;
282
+ obj.property.access; // Will throw TypeError
283
+ return res.html('Never reached');
284
+ });
285
+
286
+ const handler = lambder.getHandler();
287
+ const result = await handler(createMockEvent('/test'), createMockContext());
288
+
289
+ expect(result.statusCode).toBe(500);
290
+ expect(result.body).toContain('Error');
291
+ });
292
+
293
+ it('should handle string throws as errors', async () => {
294
+ const lambder = new Lambder({
295
+ publicPath: './public',
296
+ apiPath: '/api'
297
+ })
298
+ .setGlobalErrorHandler((err, ctx, res) => {
299
+ return res.raw({ statusCode: 500, body: err.message });
300
+ })
301
+ .addRoute('/test', (ctx, res) => {
302
+ throw 'String error'; // Non-standard but should be handled
303
+ });
304
+
305
+ const handler = lambder.getHandler();
306
+ const result = await handler(createMockEvent('/test'), createMockContext());
307
+
308
+ expect(result.statusCode).toBe(500);
309
+ expect(result.body).toContain('String error');
310
+ });
311
+ });
312
+
313
+ describe('Error Handling - Errors in Hooks', () => {
314
+ it('should catch errors in beforeRender hooks', async () => {
315
+ const lambder = new Lambder({
316
+ publicPath: './public',
317
+ apiPath: '/api'
318
+ })
319
+ .setGlobalErrorHandler((err, ctx, res) => {
320
+ return res.raw({ statusCode: 500, body: `Hook error: ${err.message}` });
321
+ });
322
+
323
+ await lambder.addHook('beforeRender', async (ctx, res) => {
324
+ throw new Error('Before render failed');
325
+ });
326
+
327
+ lambder.addRoute('/test', (ctx, res) => res.html('Test'));
328
+
329
+ const handler = lambder.getHandler();
330
+ const result = await handler(createMockEvent('/test'), createMockContext());
331
+
332
+ expect(result.statusCode).toBe(500);
333
+ expect(result.body).toBe('Hook error: Before render failed');
334
+ });
335
+
336
+ it('should catch errors in afterRender hooks', async () => {
337
+ const lambder = new Lambder({
338
+ publicPath: './public',
339
+ apiPath: '/api'
340
+ })
341
+ .setGlobalErrorHandler((err, ctx, res) => {
342
+ return res.raw({ statusCode: 500, body: `Hook error: ${err.message}` });
343
+ });
344
+
345
+ lambder.addRoute('/test', (ctx, res) => res.html('Test'));
346
+
347
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
348
+ throw new Error('After render failed');
349
+ });
350
+
351
+ const handler = lambder.getHandler();
352
+ const result = await handler(createMockEvent('/test'), createMockContext());
353
+
354
+ expect(result.statusCode).toBe(500);
355
+ expect(result.body).toBe('Hook error: After render failed');
356
+ });
357
+
358
+ it('should handle Error returned from beforeRender hook', async () => {
359
+ const lambder = new Lambder({
360
+ publicPath: './public',
361
+ apiPath: '/api'
362
+ })
363
+ .setGlobalErrorHandler((err, ctx, res) => {
364
+ return res.raw({ statusCode: 403, body: err.message });
365
+ });
366
+
367
+ await lambder.addHook('beforeRender', async (ctx, res) => {
368
+ return new Error('Access denied by hook');
369
+ });
370
+
371
+ lambder.addRoute('/test', (ctx, res) => res.html('Test'));
372
+
373
+ const handler = lambder.getHandler();
374
+ const result = await handler(createMockEvent('/test'), createMockContext());
375
+
376
+ expect(result.statusCode).toBe(403);
377
+ expect(result.body).toBe('Access denied by hook');
378
+ });
379
+
380
+ it('should handle Error returned from afterRender hook', async () => {
381
+ const lambder = new Lambder({
382
+ publicPath: './public',
383
+ apiPath: '/api'
384
+ })
385
+ .setGlobalErrorHandler((err, ctx, res) => {
386
+ return res.raw({ statusCode: 500, body: err.message });
387
+ });
388
+
389
+ lambder.addRoute('/test', (ctx, res) => res.html('Test'));
390
+
391
+ await lambder.addHook('afterRender', async (ctx, res, response) => {
392
+ return new Error('Response validation failed');
393
+ });
394
+
395
+ const handler = lambder.getHandler();
396
+ const result = await handler(createMockEvent('/test'), createMockContext());
397
+
398
+ expect(result.statusCode).toBe(500);
399
+ expect(result.body).toBe('Response validation failed');
400
+ });
401
+ });
402
+
403
+ describe('Error Handling - Default Error Behavior', () => {
404
+ it('should return 500 when no global error handler is set', async () => {
405
+ const lambder = new Lambder({
406
+ publicPath: './public',
407
+ apiPath: '/api'
408
+ })
409
+ .addRoute('/error', (ctx, res) => {
410
+ throw new Error('Unhandled error');
411
+ });
412
+
413
+ const handler = lambder.getHandler();
414
+ const result = await handler(createMockEvent('/error'), createMockContext());
415
+
416
+ expect(result.statusCode).toBe(500);
417
+ expect(result.body).toBe('Internal Server Error.');
418
+ });
419
+ });
420
+
421
+ describe('Error Handling - Input Validation Errors', () => {
422
+ it('should return 400 for invalid API input', async () => {
423
+ const lambder = new Lambder({
424
+ publicPath: './public',
425
+ apiPath: '/api'
426
+ })
427
+ .addApi('testApi', {
428
+ input: z.object({
429
+ email: z.string().email(),
430
+ age: z.number().positive()
431
+ }),
432
+ output: z.object({ success: z.boolean() })
433
+ }, async (ctx, res) => {
434
+ return res.api({ success: true });
435
+ });
436
+
437
+ const handler = lambder.getHandler();
438
+ const event = createMockEvent('/api', 'POST', 'testApi', {
439
+ email: 'invalid-email',
440
+ age: -5
441
+ });
442
+ const result = await handler(event, createMockContext());
443
+
444
+ expect(result.statusCode).toBe(400);
445
+ const body = JSON.parse(result.body || '{}');
446
+ expect(body.error).toBe('Input validation failed');
447
+ expect(body.zodError).toBeDefined();
448
+ });
449
+
450
+ it('should allow custom handling of validation errors', async () => {
451
+ const lambder = new Lambder({
452
+ publicPath: './public',
453
+ apiPath: '/api'
454
+ })
455
+ .setGlobalErrorHandler((err, ctx, res) => {
456
+ // This won't be called for validation errors since they're handled before the handler
457
+ return res.raw({ statusCode: 500, body: err.message });
458
+ })
459
+ .addApi('testApi', {
460
+ input: z.object({ value: z.string().min(5) }),
461
+ output: z.object({ result: z.string() })
462
+ }, async (ctx, res) => {
463
+ return res.api({ result: 'success' });
464
+ });
465
+
466
+ const handler = lambder.getHandler();
467
+ const event = createMockEvent('/api', 'POST', 'testApi', { value: 'abc' }); // Too short
468
+ const result = await handler(event, createMockContext());
469
+
470
+ expect(result.statusCode).toBe(400);
471
+ });
472
+ });
473
+
474
+ describe('Error Handling - Error with Additional Context', () => {
475
+ it('should access request context in error handler', async () => {
476
+ let errorContext: any = null;
477
+
478
+ const lambder = new Lambder({
479
+ publicPath: './public',
480
+ apiPath: '/api'
481
+ })
482
+ .setGlobalErrorHandler((err, ctx, res) => {
483
+ errorContext = {
484
+ path: ctx?.path,
485
+ method: ctx?.method,
486
+ host: ctx?.host
487
+ };
488
+ return res.raw({ statusCode: 500, body: 'Error' });
489
+ })
490
+ .addRoute('/test', (ctx, res) => {
491
+ throw new Error('Test');
492
+ });
493
+
494
+ const handler = lambder.getHandler();
495
+ await handler(createMockEvent('/test'), createMockContext());
496
+
497
+ expect(errorContext.path).toBe('/test');
498
+ expect(errorContext.method).toBe('GET');
499
+ expect(errorContext.host).toBe('localhost');
500
+ });
501
+
502
+ it('should provide response builder in error handler', async () => {
503
+ const lambder = new Lambder({
504
+ publicPath: './public',
505
+ apiPath: '/api'
506
+ })
507
+ .setGlobalErrorHandler((err, ctx, responseBuilder) => {
508
+ // responseBuilder should have all response methods
509
+ return responseBuilder.json({
510
+ error: err.message,
511
+ code: 'CUSTOM_ERROR'
512
+ });
513
+ })
514
+ .addRoute('/test', (ctx, res) => {
515
+ throw new Error('Test error');
516
+ });
517
+
518
+ const handler = lambder.getHandler();
519
+ const result = await handler(createMockEvent('/test'), createMockContext());
520
+
521
+ const body = JSON.parse(result.body || '{}');
522
+ expect(body.error).toBe('Test error');
523
+ expect(body.code).toBe('CUSTOM_ERROR');
524
+ });
525
+ });
526
+
527
+ describe('Error Handling - Complex Error Scenarios', () => {
528
+ it('should handle errors in chained operations', async () => {
529
+ const lambder = new Lambder({
530
+ publicPath: './public',
531
+ apiPath: '/api'
532
+ })
533
+ .setGlobalErrorHandler((err, ctx, res) => {
534
+ return res.json({ error: err.message });
535
+ })
536
+ .addRoute('/chain-error', async (ctx, res) => {
537
+ await Promise.resolve()
538
+ .then(() => Promise.resolve())
539
+ .then(() => {
540
+ throw new Error('Chain error');
541
+ });
542
+ return res.html('Never reached');
543
+ });
544
+
545
+ const handler = lambder.getHandler();
546
+ const result = await handler(createMockEvent('/chain-error'), createMockContext());
547
+
548
+ const body = JSON.parse(result.body || '{}');
549
+ expect(body.error).toBe('Chain error');
550
+ });
551
+
552
+ it('should distinguish between route errors and API errors', async () => {
553
+ const errorTypes: string[] = [];
554
+
555
+ const lambder = new Lambder({
556
+ publicPath: './public',
557
+ apiPath: '/api'
558
+ })
559
+ .setGlobalErrorHandler((err, ctx, res) => {
560
+ if (ctx?._otherInternal.isApiCall) {
561
+ errorTypes.push('api');
562
+ return res.api({ error: err.message });
563
+ } else {
564
+ errorTypes.push('route');
565
+ return res.html(`<h1>${err.message}</h1>`);
566
+ }
567
+ })
568
+ .addRoute('/route-error', (ctx, res) => {
569
+ throw new Error('Route error');
570
+ })
571
+ .addApi('errorApi', {
572
+ input: z.void(),
573
+ output: z.object({ error: z.string() })
574
+ }, async (ctx, res) => {
575
+ throw new Error('API error');
576
+ });
577
+
578
+ const handler = lambder.getHandler();
579
+
580
+ await handler(createMockEvent('/route-error'), createMockContext());
581
+ await handler(createMockEvent('/api', 'POST', 'errorApi', undefined), createMockContext());
582
+
583
+ expect(errorTypes).toEqual(['route', 'api']);
584
+ });
585
+ });