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.
- package/Readme.md +355 -410
- package/dist/Lambder.d.ts +47 -21
- package/dist/Lambder.js +79 -24
- package/dist/LambderApiContract.d.ts +10 -42
- package/dist/LambderApiContract.js +2 -22
- package/dist/LambderCaller.js +2 -2
- package/dist/LambderMSW.js +0 -2
- package/dist/LambderResolver.d.ts +16 -17
- package/dist/LambderResponseBuilder.d.ts +2 -3
- package/dist/LambderResponseBuilder.js +1 -3
- package/dist/LambderUtils.js +1 -3
- package/dist/index.d.ts +1 -1
- package/docs/LAMBDER_MSW.md +6 -6
- package/docs/TYPE_SAFE_QUICK_START.md +54 -177
- package/examples/msw-testing-example.ts +36 -33
- package/examples/secure-session-example.ts +50 -34
- package/examples/zod-chained-api-example.ts +63 -0
- package/package.json +3 -2
- package/src/Lambder.ts +124 -83
- package/src/LambderApiContract.ts +7 -50
- package/src/LambderCaller.ts +2 -2
- package/src/LambderMSW.ts +0 -4
- package/src/LambderResolver.ts +21 -24
- package/src/LambderResponseBuilder.ts +4 -7
- package/src/LambderUtils.ts +1 -3
- package/src/index.ts +0 -3
- package/tests/UNTESTED_FEATURES.md +263 -0
- package/tests/error-handling.test.ts +585 -0
- package/tests/hooks.test.ts +561 -0
- package/tests/output-type-runtime.test.ts +80 -64
- package/tests/routes.test.ts +542 -0
- package/tests/session.test.ts +38 -24
- package/tests/type-safety.test.ts +147 -97
- package/tests/use-plugin.test.ts +437 -0
- package/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +0 -90
- package/examples/output-type-enforcement-example.ts +0 -218
- package/examples/simplified-typed-api-example.ts +0 -365
- package/examples/test-output-type-enforcement.ts +0 -101
- 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
|