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,437 @@
1
+ /**
2
+ * Plugin System (.use) Tests
3
+ *
4
+ * This file tests the plugin system that allows modular API composition
5
+ */
6
+
7
+ import { describe, it, expect } from 'vitest';
8
+ import { z } from 'zod';
9
+ import Lambder from '../src/Lambder.js';
10
+ import LambderCaller from '../src/LambderCaller.js';
11
+ import { APIGatewayProxyEvent, Context } from 'aws-lambda';
12
+
13
+ // Mock AWS Lambda event and context helpers
14
+ const createMockEvent = (apiName: string, payload: any): APIGatewayProxyEvent => ({
15
+ body: JSON.stringify({ apiName, payload }),
16
+ headers: { Host: 'localhost' },
17
+ multiValueHeaders: {},
18
+ httpMethod: 'POST',
19
+ isBase64Encoded: false,
20
+ path: '/api',
21
+ pathParameters: null,
22
+ queryStringParameters: null,
23
+ multiValueQueryStringParameters: null,
24
+ stageVariables: null,
25
+ requestContext: {} as any,
26
+ resource: '',
27
+ });
28
+
29
+ const createMockContext = (): Context => ({
30
+ callbackWaitsForEmptyEventLoop: false,
31
+ functionName: 'test',
32
+ functionVersion: '1',
33
+ invokedFunctionArn: 'arn',
34
+ memoryLimitInMB: '128',
35
+ awsRequestId: '123',
36
+ logGroupName: 'group',
37
+ logStreamName: 'stream',
38
+ getRemainingTimeInMillis: () => 1000,
39
+ done: () => {},
40
+ fail: () => {},
41
+ succeed: () => {},
42
+ });
43
+
44
+ // ============================================================================
45
+ // Test 1: Basic Plugin Usage
46
+ // ============================================================================
47
+
48
+ describe('Plugin System - Basic Usage', () => {
49
+ it('should allow adding APIs via plugin', async () => {
50
+ // Define a simple plugin
51
+ const userPlugin = <T>(lambder: Lambder<T>) => {
52
+ return lambder
53
+ .addApi('getUser', {
54
+ input: z.object({ userId: z.string() }),
55
+ output: z.object({ id: z.string(), name: z.string() })
56
+ }, async (ctx, res) => {
57
+ return res.api({ id: ctx.apiPayload.userId, name: 'John Doe' });
58
+ });
59
+ };
60
+
61
+ // Use the plugin
62
+ const lambder = new Lambder({
63
+ publicPath: './public',
64
+ apiPath: '/api'
65
+ }).use(userPlugin);
66
+
67
+ // Test runtime execution
68
+ const handler = lambder.getHandler();
69
+ const event = createMockEvent('getUser', { userId: '123' });
70
+ const result = await handler(event, createMockContext());
71
+
72
+ expect(result.statusCode).toBe(200);
73
+ const body = JSON.parse(result.body);
74
+ expect(body.payload).toEqual({ id: '123', name: 'John Doe' });
75
+ });
76
+
77
+ it('should preserve type contract after using plugin', () => {
78
+ const userPlugin = <T>(lambder: Lambder<T>) => {
79
+ return lambder
80
+ .addApi('getUser', {
81
+ input: z.object({ userId: z.string() }),
82
+ output: z.object({ id: z.string(), name: z.string() })
83
+ }, async (ctx, res) => {
84
+ return res.api({ id: ctx.apiPayload.userId, name: 'Test' });
85
+ });
86
+ };
87
+
88
+ const lambder = new Lambder({
89
+ publicPath: './public',
90
+ apiPath: '/api'
91
+ }).use(userPlugin);
92
+
93
+ type AppContract = typeof lambder.ApiContractType;
94
+
95
+ const caller = new LambderCaller<AppContract>({
96
+ apiPath: '/api',
97
+ isCorsEnabled: false
98
+ });
99
+
100
+ // Type check - if this compiles, types are correct
101
+ type GetUserInput = Parameters<typeof caller.api<'getUser'>>[1];
102
+
103
+ expect(caller).toBeDefined();
104
+ });
105
+ });
106
+
107
+ // ============================================================================
108
+ // Test 2: Multiple Plugins
109
+ // ============================================================================
110
+
111
+ describe('Plugin System - Multiple Plugins', () => {
112
+ it('should allow chaining multiple plugins', async () => {
113
+ const userPlugin = <T>(lambder: Lambder<T>) => {
114
+ return lambder
115
+ .addApi('getUser', {
116
+ input: z.object({ userId: z.string() }),
117
+ output: z.object({ id: z.string(), name: z.string() })
118
+ }, async (ctx, res) => {
119
+ return res.api({ id: ctx.apiPayload.userId, name: 'John' });
120
+ });
121
+ };
122
+
123
+ const productPlugin = <T>(lambder: Lambder<T>) => {
124
+ return lambder
125
+ .addApi('getProduct', {
126
+ input: z.object({ productId: z.string() }),
127
+ output: z.object({ id: z.string(), title: z.string(), price: z.number() })
128
+ }, async (ctx, res) => {
129
+ return res.api({
130
+ id: ctx.apiPayload.productId,
131
+ title: 'Test Product',
132
+ price: 99.99
133
+ });
134
+ });
135
+ };
136
+
137
+ const orderPlugin = <T>(lambder: Lambder<T>) => {
138
+ return lambder
139
+ .addApi('createOrder', {
140
+ input: z.object({ userId: z.string(), productId: z.string() }),
141
+ output: z.object({ orderId: z.string(), status: z.string() })
142
+ }, async (ctx, res) => {
143
+ return res.api({
144
+ orderId: 'order-123',
145
+ status: 'pending'
146
+ });
147
+ });
148
+ };
149
+
150
+ // Chain multiple plugins
151
+ const lambder = new Lambder({
152
+ publicPath: './public',
153
+ apiPath: '/api'
154
+ })
155
+ .use(userPlugin)
156
+ .use(productPlugin)
157
+ .use(orderPlugin);
158
+
159
+ // Test each API works
160
+ const handler = lambder.getHandler();
161
+
162
+ // Test user API
163
+ const userEvent = createMockEvent('getUser', { userId: '123' });
164
+ const userResult = await handler(userEvent, createMockContext());
165
+ expect(userResult.statusCode).toBe(200);
166
+ expect(JSON.parse(userResult.body).payload.name).toBe('John');
167
+
168
+ // Test product API
169
+ const productEvent = createMockEvent('getProduct', { productId: 'prod-456' });
170
+ const productResult = await handler(productEvent, createMockContext());
171
+ expect(productResult.statusCode).toBe(200);
172
+ expect(JSON.parse(productResult.body).payload.title).toBe('Test Product');
173
+
174
+ // Test order API
175
+ const orderEvent = createMockEvent('createOrder', { userId: '123', productId: 'prod-456' });
176
+ const orderResult = await handler(orderEvent, createMockContext());
177
+ expect(orderResult.statusCode).toBe(200);
178
+ expect(JSON.parse(orderResult.body).payload.orderId).toBe('order-123');
179
+ });
180
+
181
+ it('should accumulate types from multiple plugins', () => {
182
+ const userPlugin = <T>(lambder: Lambder<T>) => {
183
+ return lambder
184
+ .addApi('getUser', {
185
+ input: z.object({ userId: z.string() }),
186
+ output: z.object({ id: z.string(), name: z.string() })
187
+ }, async (ctx, res) => {
188
+ return res.api({ id: ctx.apiPayload.userId, name: 'Test' });
189
+ });
190
+ };
191
+
192
+ const productPlugin = <T>(lambder: Lambder<T>) => {
193
+ return lambder
194
+ .addApi('getProduct', {
195
+ input: z.object({ productId: z.string() }),
196
+ output: z.object({ id: z.string(), title: z.string() })
197
+ }, async (ctx, res) => {
198
+ return res.api({ id: ctx.apiPayload.productId, title: 'Test' });
199
+ });
200
+ };
201
+
202
+ const lambder = new Lambder({
203
+ publicPath: './public',
204
+ apiPath: '/api'
205
+ })
206
+ .use(userPlugin)
207
+ .use(productPlugin);
208
+
209
+ type AppContract = typeof lambder.ApiContractType;
210
+
211
+ const caller = new LambderCaller<AppContract>({
212
+ apiPath: '/api',
213
+ isCorsEnabled: false
214
+ });
215
+
216
+ // Type check - both APIs should be available
217
+ type GetUserInput = Parameters<typeof caller.api<'getUser'>>[1];
218
+ type GetProductInput = Parameters<typeof caller.api<'getProduct'>>[1];
219
+
220
+ expect(caller).toBeDefined();
221
+ });
222
+ });
223
+
224
+ // ============================================================================
225
+ // Test 3: Plugin with Additional APIs
226
+ // ============================================================================
227
+
228
+ describe('Plugin System - Mixed Usage', () => {
229
+ it('should allow mixing direct API addition and plugins', async () => {
230
+ const userPlugin = <T>(lambder: Lambder<T>) => {
231
+ return lambder
232
+ .addApi('getUser', {
233
+ input: z.object({ userId: z.string() }),
234
+ output: z.object({ id: z.string(), name: z.string() })
235
+ }, async (ctx, res) => {
236
+ return res.api({ id: ctx.apiPayload.userId, name: 'John' });
237
+ });
238
+ };
239
+
240
+ const lambder = new Lambder({
241
+ publicPath: './public',
242
+ apiPath: '/api'
243
+ })
244
+ // Direct API
245
+ .addApi('healthCheck', {
246
+ input: z.void(),
247
+ output: z.object({ status: z.string() })
248
+ }, async (ctx, res) => {
249
+ return res.api({ status: 'ok' });
250
+ })
251
+ // Plugin
252
+ .use(userPlugin)
253
+ // Another direct API
254
+ .addApi('getVersion', {
255
+ input: z.void(),
256
+ output: z.object({ version: z.string() })
257
+ }, async (ctx, res) => {
258
+ return res.api({ version: '2.0' });
259
+ });
260
+
261
+ const handler = lambder.getHandler();
262
+
263
+ // Test direct API before plugin
264
+ const healthEvent = createMockEvent('healthCheck', undefined);
265
+ const healthResult = await handler(healthEvent, createMockContext());
266
+ expect(JSON.parse(healthResult.body).payload.status).toBe('ok');
267
+
268
+ // Test plugin API
269
+ const userEvent = createMockEvent('getUser', { userId: '123' });
270
+ const userResult = await handler(userEvent, createMockContext());
271
+ expect(JSON.parse(userResult.body).payload.name).toBe('John');
272
+
273
+ // Test direct API after plugin
274
+ const versionEvent = createMockEvent('getVersion', undefined);
275
+ const versionResult = await handler(versionEvent, createMockContext());
276
+ expect(JSON.parse(versionResult.body).payload.version).toBe('2.0');
277
+ });
278
+ });
279
+
280
+ // ============================================================================
281
+ // Test 4: Plugin with Routes
282
+ // ============================================================================
283
+
284
+ describe('Plugin System - Routes', () => {
285
+ it('should allow plugins to add routes', async () => {
286
+ const healthPlugin = <T>(lambder: Lambder<T>) => {
287
+ // Now addRoute is chainable!
288
+ return lambder
289
+ .addRoute('/health', (ctx, res) => {
290
+ return res.json({ status: 'healthy' });
291
+ })
292
+ .addRoute('/version', (ctx, res) => {
293
+ return res.json({ version: '2.0' });
294
+ });
295
+ };
296
+
297
+ const lambder = new Lambder({
298
+ publicPath: './public',
299
+ apiPath: '/api'
300
+ }).use(healthPlugin);
301
+
302
+ const handler = lambder.getHandler();
303
+
304
+ // Create GET request event
305
+ const healthEvent: APIGatewayProxyEvent = {
306
+ ...createMockEvent('', {}),
307
+ httpMethod: 'GET',
308
+ path: '/health'
309
+ };
310
+
311
+ const result = await handler(healthEvent, createMockContext());
312
+ expect(result.statusCode).toBe(200);
313
+ expect(JSON.parse(result.body || '{}').status).toBe('healthy');
314
+ });
315
+ });
316
+
317
+ // ============================================================================
318
+ // Test 5: Complex Plugin Composition
319
+ // ============================================================================
320
+
321
+ describe('Plugin System - Complex Composition', () => {
322
+ it('should support nested plugins (plugin that uses another plugin)', async () => {
323
+ const basePlugin = <T>(lambder: Lambder<T>) => {
324
+ return lambder
325
+ .addApi('base', {
326
+ input: z.void(),
327
+ output: z.object({ value: z.string() })
328
+ }, async (ctx, res) => {
329
+ return res.api({ value: 'base' });
330
+ });
331
+ };
332
+
333
+ const extendedPlugin = <T>(lambder: Lambder<T>) => {
334
+ return lambder
335
+ .use(basePlugin)
336
+ .addApi('extended', {
337
+ input: z.void(),
338
+ output: z.object({ value: z.string() })
339
+ }, async (ctx, res) => {
340
+ return res.api({ value: 'extended' });
341
+ });
342
+ };
343
+
344
+ const lambder = new Lambder({
345
+ publicPath: './public',
346
+ apiPath: '/api'
347
+ }).use(extendedPlugin);
348
+
349
+ const handler = lambder.getHandler();
350
+
351
+ // Both base and extended APIs should work
352
+ const baseEvent = createMockEvent('base', undefined);
353
+ const baseResult = await handler(baseEvent, createMockContext());
354
+ expect(JSON.parse(baseResult.body || '{}').payload.value).toBe('base');
355
+
356
+ const extendedEvent = createMockEvent('extended', undefined);
357
+ const extendedResult = await handler(extendedEvent, createMockContext());
358
+ expect(JSON.parse(extendedResult.body || '{}').payload.value).toBe('extended');
359
+ });
360
+
361
+ it('should allow plugins to be reusable across different lambder instances', async () => {
362
+ const sharedPlugin = <T>(lambder: Lambder<T>) => {
363
+ return lambder
364
+ .addApi('shared', {
365
+ input: z.object({ id: z.string() }),
366
+ output: z.object({ id: z.string(), source: z.string() })
367
+ }, async (ctx, res) => {
368
+ return res.api({ id: ctx.apiPayload.id, source: 'shared-plugin' });
369
+ });
370
+ };
371
+
372
+ // Use same plugin in two different instances
373
+ const lambder1 = new Lambder({
374
+ publicPath: './public',
375
+ apiPath: '/api'
376
+ }).use(sharedPlugin);
377
+
378
+ const lambder2 = new Lambder({
379
+ publicPath: './public',
380
+ apiPath: '/api'
381
+ }).use(sharedPlugin);
382
+
383
+ // Both should work independently
384
+ const event = createMockEvent('shared', { id: 'test-123' });
385
+
386
+ const result1 = await lambder1.getHandler()(event, createMockContext());
387
+ const result2 = await lambder2.getHandler()(event, createMockContext());
388
+
389
+ expect(JSON.parse(result1.body || '{}').payload.source).toBe('shared-plugin');
390
+ expect(JSON.parse(result2.body || '{}').payload.source).toBe('shared-plugin');
391
+ });
392
+ });
393
+
394
+ // ============================================================================
395
+ // Test 6: Plugin Type Safety Edge Cases
396
+ // ============================================================================
397
+
398
+ describe('Plugin System - Type Safety', () => {
399
+ it('should maintain type safety through plugin chain', () => {
400
+ const plugin1 = <T>(lambder: Lambder<T>) => {
401
+ return lambder
402
+ .addApi('api1', {
403
+ input: z.object({ value: z.string() }),
404
+ output: z.object({ result: z.string() })
405
+ }, async (ctx, res) => {
406
+ return res.api({ result: ctx.apiPayload.value });
407
+ });
408
+ };
409
+
410
+ const plugin2 = <T>(lambder: Lambder<T>) => {
411
+ return lambder
412
+ .addApi('api2', {
413
+ input: z.object({ count: z.number() }),
414
+ output: z.object({ total: z.number() })
415
+ }, async (ctx, res) => {
416
+ return res.api({ total: ctx.apiPayload.count * 2 });
417
+ });
418
+ };
419
+
420
+ const lambder = new Lambder({
421
+ publicPath: './public',
422
+ apiPath: '/api'
423
+ })
424
+ .use(plugin1)
425
+ .use(plugin2);
426
+
427
+ type Contract = typeof lambder.ApiContractType;
428
+
429
+ // Type assertions - both api1 and api2 should be in the contract
430
+ // We test this at runtime by creating a caller
431
+ const caller = new LambderCaller<Contract>({
432
+ apiPath: '/api',
433
+ isCorsEnabled: false
434
+ });
435
+ expect(caller).toBeDefined();
436
+ });
437
+ });
@@ -1,90 +0,0 @@
1
- # Output Type Enforcement - Implementation Summary
2
-
3
- ## Problem
4
- The type system was only enforcing input types for `ctx.apiPayload`, but not enforcing that the output returned by `resolver.api()` matched the contract's output type.
5
-
6
- ## Solution
7
- Made the following components generic to propagate type information:
8
-
9
- ### 1. LambderResponseBuilder
10
- - Added generic parameter: `LambderResponseBuilder<TContract extends ApiContractShape = any>`
11
- - The `api()` method now uses the generic type for type checking
12
-
13
- ### 2. LambderResolver
14
- - Added generic parameters: `LambderResolver<TContract extends ApiContractShape = any, TApiName extends keyof TContract & string = any>`
15
- - Overrides the `api()` method to enforce output type: `api(payload: ApiOutput<TContract, TApiName> | null, ...)`
16
- - The `die.api()` method also enforces the output type through the interface
17
-
18
- ### 3. Lambder addApi/addSessionApi
19
- - Updated type signatures to pass `LambderResolver<TContract, TApiName>` to the action function
20
- - Now both methods enforce:
21
- - ✅ Input type via `ctx.apiPayload: TContract[TApiName]['input']`
22
- - ✅ Output type via `resolver.api(payload: TContract[TApiName]['output'] | null)`
23
-
24
- ## Type Flow
25
- ```
26
- Contract Definition
27
-
28
- Lambder<TContract>
29
-
30
- addApi<TApiName>(apiName, actionFn)
31
-
32
- actionFn(ctx: LambderRenderContext<Input>, resolver: LambderResolver<TContract, TApiName>)
33
-
34
- resolver.api(payload: Output | null)
35
-
36
- Type validation at compile time!
37
- ```
38
-
39
- ## What's Enforced Now
40
-
41
- ### Input Types (already working)
42
- ```typescript
43
- lambder.addApi('getUserById', async (ctx, resolver) => {
44
- ctx.apiPayload.userId; // ✅ TypeScript knows this is string
45
- });
46
- ```
47
-
48
- ### Output Types (NEW!)
49
- ```typescript
50
- lambder.addApi('getUserById', async (ctx, resolver) => {
51
- const user = { id: "123", name: "John", age: 30 };
52
- return resolver.api(user); // ✅ TypeScript validates user matches User type
53
-
54
- // return resolver.api("wrong"); // ❌ TypeScript error!
55
- });
56
- ```
57
-
58
- ## Features
59
- - ✅ Works with `resolver.api()`
60
- - ✅ Works with `resolver.die.api()`
61
- - ✅ Works with `addApi()`
62
- - ✅ Works with `addSessionApi()`
63
- - ✅ Supports nullable outputs (`output: User | null`)
64
- - ✅ Supports complex nested types
65
- - ✅ Supports arrays
66
- - ✅ Supports primitive types
67
- - ✅ Zero runtime overhead - all validation is at compile time
68
- - ✅ Backward compatible - untyped APIs still work
69
-
70
- ## Files Changed
71
- 1. `src/LambderResponseBuilder.ts` - Made generic, imported ApiContractShape
72
- 2. `src/LambderResolver.ts` - Made generic, added typed `api()` override
73
- 3. `src/Lambder.ts` - Updated addApi/addSessionApi type signatures
74
- 4. `docs/TYPE_SAFE_QUICK_START.md` - Updated documentation with output type examples
75
-
76
- ## Examples Created
77
- 1. `examples/test-output-type-enforcement.ts` - Unit test for type enforcement
78
- 2. `examples/output-type-enforcement-example.ts` - Comprehensive demonstration
79
-
80
- ## Testing
81
- All existing examples and tests compile without errors:
82
- - ✅ `examples/simplified-typed-api-example.ts`
83
- - ✅ `tests/type-safety.test.ts`
84
- - ✅ No TypeScript compilation errors in project
85
-
86
- ## Backward Compatibility
87
- The changes are fully backward compatible:
88
- - Untyped usage still works: `new Lambder({ ... })` without generic
89
- - Typed usage is opt-in: `new Lambder<MyContract>({ ... })`
90
- - Existing code continues to work unchanged