lambder 1.0.128 → 1.0.130
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/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +90 -0
- package/Readme.md +49 -0
- package/dist/Lambder.d.ts +5 -4
- package/dist/Lambder.js +2 -2
- package/dist/LambderMSW.d.ts +104 -0
- package/dist/LambderMSW.js +203 -0
- package/dist/LambderResolver.d.ts +17 -15
- package/dist/LambderResolver.js +4 -0
- package/dist/LambderResponseBuilder.d.ts +2 -1
- package/dist/LambderSessionController.d.ts +1 -0
- package/dist/LambderSessionController.js +10 -0
- package/dist/LambderSessionManager.d.ts +6 -1
- package/dist/LambderSessionManager.js +43 -12
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/docs/DYNAMODB_SETUP.md +96 -0
- package/docs/MSW_MOCKING.md +590 -0
- package/docs/TYPE_SAFE_QUICK_START.md +48 -4
- package/examples/msw-mocking-example.ts +241 -0
- package/examples/output-type-enforcement-example.ts +218 -0
- package/examples/secure-session-example.ts +191 -0
- package/examples/test-output-type-enforcement.ts +101 -0
- package/package.json +9 -1
- package/src/Lambder.ts +4 -4
- package/src/LambderMSW.ts +330 -0
- package/src/LambderResolver.ts +32 -15
- package/src/LambderResponseBuilder.ts +2 -1
- package/src/LambderSessionController.ts +9 -0
- package/src/LambderSessionManager.ts +51 -10
- package/src/index.ts +11 -0
- package/tests/msw-integration.test.ts +183 -0
- package/tests/output-type-runtime.test.ts +365 -0
- package/tests/type-safety.test.ts +312 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output Type Enforcement Runtime Tests
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that the typed APIs work correctly at runtime
|
|
5
|
+
* while maintaining type safety at compile time.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { APIGatewayProxyEvent, Context } from 'aws-lambda';
|
|
10
|
+
import Lambder from '../src/Lambder.js';
|
|
11
|
+
import type { ApiContract } from '../src/index.js';
|
|
12
|
+
|
|
13
|
+
// Mock AWS Lambda event and context
|
|
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
|
+
// Test contract
|
|
45
|
+
type TestContract = ApiContract<{
|
|
46
|
+
echo: { input: { message: string }, output: { echo: string } },
|
|
47
|
+
add: { input: { a: number, b: number }, output: number },
|
|
48
|
+
getUser: { input: { userId: string }, output: { id: string, name: string, age: number } },
|
|
49
|
+
listUsers: { input: void, output: Array<{ id: string, name: string }> },
|
|
50
|
+
findUser: { input: { email: string }, output: { id: string, name: string } | null },
|
|
51
|
+
deleteUser: { input: { userId: string }, output: boolean },
|
|
52
|
+
}>;
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Runtime Tests
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
describe('Output Type Enforcement - Runtime', () => {
|
|
59
|
+
it('should return correct primitive types', async () => {
|
|
60
|
+
const lambder = new Lambder<TestContract>({
|
|
61
|
+
publicPath: './public',
|
|
62
|
+
apiPath: '/api',
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
lambder.addApi('add', async (ctx, resolver) => {
|
|
66
|
+
const { a, b } = ctx.apiPayload;
|
|
67
|
+
const sum = a + b;
|
|
68
|
+
return resolver.api(sum);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const event = createMockEvent('add', { a: 5, b: 3 });
|
|
72
|
+
const context = createMockContext();
|
|
73
|
+
const response = await lambder.render(event, context);
|
|
74
|
+
|
|
75
|
+
expect(response.statusCode).toBe(200);
|
|
76
|
+
const body = JSON.parse(response.body || '{}');
|
|
77
|
+
expect(body.payload).toBe(8);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should return correct object types', async () => {
|
|
81
|
+
const lambder = new Lambder<TestContract>({
|
|
82
|
+
publicPath: './public',
|
|
83
|
+
apiPath: '/api',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
87
|
+
const userId = ctx.apiPayload.userId;
|
|
88
|
+
return resolver.api({
|
|
89
|
+
id: userId,
|
|
90
|
+
name: 'John Doe',
|
|
91
|
+
age: 30
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const event = createMockEvent('getUser', { userId: '123' });
|
|
96
|
+
const context = createMockContext();
|
|
97
|
+
const response = await lambder.render(event, context);
|
|
98
|
+
|
|
99
|
+
expect(response.statusCode).toBe(200);
|
|
100
|
+
const body = JSON.parse(response.body || '{}');
|
|
101
|
+
expect(body.payload).toEqual({
|
|
102
|
+
id: '123',
|
|
103
|
+
name: 'John Doe',
|
|
104
|
+
age: 30
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should return correct array types', async () => {
|
|
109
|
+
const lambder = new Lambder<TestContract>({
|
|
110
|
+
publicPath: './public',
|
|
111
|
+
apiPath: '/api',
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
115
|
+
return resolver.api([
|
|
116
|
+
{ id: '1', name: 'Alice' },
|
|
117
|
+
{ id: '2', name: 'Bob' }
|
|
118
|
+
]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const event = createMockEvent('listUsers', undefined);
|
|
122
|
+
const context = createMockContext();
|
|
123
|
+
const response = await lambder.render(event, context);
|
|
124
|
+
|
|
125
|
+
expect(response.statusCode).toBe(200);
|
|
126
|
+
const body = JSON.parse(response.body || '{}');
|
|
127
|
+
expect(body.payload).toEqual([
|
|
128
|
+
{ id: '1', name: 'Alice' },
|
|
129
|
+
{ id: '2', name: 'Bob' }
|
|
130
|
+
]);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('should handle null returns correctly', async () => {
|
|
134
|
+
const lambder = new Lambder<TestContract>({
|
|
135
|
+
publicPath: './public',
|
|
136
|
+
apiPath: '/api',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
lambder.addApi('findUser', async (ctx, resolver) => {
|
|
140
|
+
const email = ctx.apiPayload.email;
|
|
141
|
+
if (email === 'notfound@example.com') {
|
|
142
|
+
return resolver.api(null);
|
|
143
|
+
}
|
|
144
|
+
return resolver.api({ id: '1', name: 'Found User' });
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Test null case
|
|
148
|
+
const event1 = createMockEvent('findUser', { email: 'notfound@example.com' });
|
|
149
|
+
const context1 = createMockContext();
|
|
150
|
+
const response1 = await lambder.render(event1, context1);
|
|
151
|
+
|
|
152
|
+
expect(response1.statusCode).toBe(200);
|
|
153
|
+
const body1 = JSON.parse(response1.body || '{}');
|
|
154
|
+
expect(body1.payload).toBeNull();
|
|
155
|
+
|
|
156
|
+
// Test found case
|
|
157
|
+
const event2 = createMockEvent('findUser', { email: 'found@example.com' });
|
|
158
|
+
const context2 = createMockContext();
|
|
159
|
+
const response2 = await lambder.render(event2, context2);
|
|
160
|
+
|
|
161
|
+
expect(response2.statusCode).toBe(200);
|
|
162
|
+
const body2 = JSON.parse(response2.body || '{}');
|
|
163
|
+
expect(body2.payload).toEqual({ id: '1', name: 'Found User' });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('should return boolean types correctly', async () => {
|
|
167
|
+
const lambder = new Lambder<TestContract>({
|
|
168
|
+
publicPath: './public',
|
|
169
|
+
apiPath: '/api',
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
lambder.addApi('deleteUser', async (ctx, resolver) => {
|
|
173
|
+
const userId = ctx.apiPayload.userId;
|
|
174
|
+
// Mock deletion
|
|
175
|
+
const success = userId !== '';
|
|
176
|
+
return resolver.api(success);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
const event = createMockEvent('deleteUser', { userId: '123' });
|
|
180
|
+
const context = createMockContext();
|
|
181
|
+
const response = await lambder.render(event, context);
|
|
182
|
+
|
|
183
|
+
expect(response.statusCode).toBe(200);
|
|
184
|
+
const body = JSON.parse(response.body || '{}');
|
|
185
|
+
expect(body.payload).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should work with die.api()', async () => {
|
|
189
|
+
const lambder = new Lambder<TestContract>({
|
|
190
|
+
publicPath: './public',
|
|
191
|
+
apiPath: '/api',
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
lambder.addApi('echo', async (ctx, resolver) => {
|
|
195
|
+
return resolver.die.api({ echo: ctx.apiPayload.message });
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const event = createMockEvent('echo', { message: 'Hello!' });
|
|
199
|
+
const context = createMockContext();
|
|
200
|
+
const response = await lambder.render(event, context);
|
|
201
|
+
|
|
202
|
+
expect(response.statusCode).toBe(200);
|
|
203
|
+
const body = JSON.parse(response.body || '{}');
|
|
204
|
+
expect(body.payload).toEqual({ echo: 'Hello!' });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should work with session APIs', async () => {
|
|
208
|
+
const lambder = new Lambder<TestContract>({
|
|
209
|
+
publicPath: './public',
|
|
210
|
+
apiPath: '/api',
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Note: Session will fail without DynamoDB setup, but we're testing type enforcement
|
|
214
|
+
lambder.addSessionApi('getUser', async (ctx, resolver) => {
|
|
215
|
+
return resolver.api({
|
|
216
|
+
id: ctx.apiPayload.userId,
|
|
217
|
+
name: 'Session User',
|
|
218
|
+
age: 25
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// This will return 500 error because session is not configured
|
|
223
|
+
const event = createMockEvent('getUser', { userId: '123' });
|
|
224
|
+
const context = createMockContext();
|
|
225
|
+
|
|
226
|
+
const response = await lambder.render(event, context);
|
|
227
|
+
|
|
228
|
+
// Should return 500 error when session is not configured
|
|
229
|
+
expect(response.statusCode).toBe(500);
|
|
230
|
+
expect(response.body).toBe('Internal Server Error.');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// ============================================================================
|
|
235
|
+
// Input Type Tests
|
|
236
|
+
// ============================================================================
|
|
237
|
+
|
|
238
|
+
describe('Input Type Enforcement - Runtime', () => {
|
|
239
|
+
it('should receive correctly typed input', async () => {
|
|
240
|
+
const lambder = new Lambder<TestContract>({
|
|
241
|
+
publicPath: './public',
|
|
242
|
+
apiPath: '/api',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
lambder.addApi('echo', async (ctx, resolver) => {
|
|
246
|
+
// Verify input is correctly typed at runtime
|
|
247
|
+
const message: string = ctx.apiPayload.message;
|
|
248
|
+
expect(typeof message).toBe('string');
|
|
249
|
+
return resolver.api({ echo: message });
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const event = createMockEvent('echo', { message: 'Test Message' });
|
|
253
|
+
const context = createMockContext();
|
|
254
|
+
await lambder.render(event, context);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('should handle void input correctly', async () => {
|
|
258
|
+
const lambder = new Lambder<TestContract>({
|
|
259
|
+
publicPath: './public',
|
|
260
|
+
apiPath: '/api',
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
264
|
+
// apiPayload should be undefined for void input
|
|
265
|
+
expect(ctx.apiPayload).toBeUndefined();
|
|
266
|
+
return resolver.api([]);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const event = createMockEvent('listUsers', undefined);
|
|
270
|
+
const context = createMockContext();
|
|
271
|
+
await lambder.render(event, context);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('should handle complex input objects', async () => {
|
|
275
|
+
const lambder = new Lambder<TestContract>({
|
|
276
|
+
publicPath: './public',
|
|
277
|
+
apiPath: '/api',
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
lambder.addApi('add', async (ctx, resolver) => {
|
|
281
|
+
const { a, b } = ctx.apiPayload;
|
|
282
|
+
expect(typeof a).toBe('number');
|
|
283
|
+
expect(typeof b).toBe('number');
|
|
284
|
+
return resolver.api(a + b);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
const event = createMockEvent('add', { a: 10, b: 20 });
|
|
288
|
+
const context = createMockContext();
|
|
289
|
+
const response = await lambder.render(event, context);
|
|
290
|
+
|
|
291
|
+
const body = JSON.parse(response.body || '{}');
|
|
292
|
+
expect(body.payload).toBe(30);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ============================================================================
|
|
297
|
+
// Edge Cases
|
|
298
|
+
// ============================================================================
|
|
299
|
+
|
|
300
|
+
describe('Edge Cases', () => {
|
|
301
|
+
it('should handle empty arrays', async () => {
|
|
302
|
+
const lambder = new Lambder<TestContract>({
|
|
303
|
+
publicPath: './public',
|
|
304
|
+
apiPath: '/api',
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
308
|
+
return resolver.api([]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const event = createMockEvent('listUsers', undefined);
|
|
312
|
+
const context = createMockContext();
|
|
313
|
+
const response = await lambder.render(event, context);
|
|
314
|
+
|
|
315
|
+
expect(response.statusCode).toBe(200);
|
|
316
|
+
const body = JSON.parse(response.body || '{}');
|
|
317
|
+
expect(body.payload).toEqual([]);
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should handle zero as a valid number', async () => {
|
|
321
|
+
const lambder = new Lambder<TestContract>({
|
|
322
|
+
publicPath: './public',
|
|
323
|
+
apiPath: '/api',
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
lambder.addApi('add', async (ctx, resolver) => {
|
|
327
|
+
return resolver.api(0);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
const event = createMockEvent('add', { a: 0, b: 0 });
|
|
331
|
+
const context = createMockContext();
|
|
332
|
+
const response = await lambder.render(event, context);
|
|
333
|
+
|
|
334
|
+
expect(response.statusCode).toBe(200);
|
|
335
|
+
const body = JSON.parse(response.body || '{}');
|
|
336
|
+
expect(body.payload).toBe(0);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('should handle empty strings in objects', async () => {
|
|
340
|
+
const lambder = new Lambder<TestContract>({
|
|
341
|
+
publicPath: './public',
|
|
342
|
+
apiPath: '/api',
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
346
|
+
return resolver.api({
|
|
347
|
+
id: '',
|
|
348
|
+
name: '',
|
|
349
|
+
age: 0
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
const event = createMockEvent('getUser', { userId: '' });
|
|
354
|
+
const context = createMockContext();
|
|
355
|
+
const response = await lambder.render(event, context);
|
|
356
|
+
|
|
357
|
+
expect(response.statusCode).toBe(200);
|
|
358
|
+
const body = JSON.parse(response.body || '{}');
|
|
359
|
+
expect(body.payload).toEqual({
|
|
360
|
+
id: '',
|
|
361
|
+
name: '',
|
|
362
|
+
age: 0
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
});
|
|
@@ -197,6 +197,318 @@ describe('Complex Types', () => {
|
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
199
|
|
|
200
|
+
// ============================================================================
|
|
201
|
+
// Test 6: Output Type Enforcement
|
|
202
|
+
// ============================================================================
|
|
203
|
+
|
|
204
|
+
describe('Output Type Enforcement', () => {
|
|
205
|
+
it('should enforce output types in resolver.api()', () => {
|
|
206
|
+
const lambder = new Lambder<TestApiContract>({
|
|
207
|
+
publicPath: './public',
|
|
208
|
+
apiPath: '/api'
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// ✅ CORRECT: Return correct output type
|
|
212
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
213
|
+
const userId = ctx.apiPayload.userId;
|
|
214
|
+
return resolver.api({ id: userId, name: 'John Doe' });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ✅ CORRECT: Return null is allowed
|
|
218
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
219
|
+
return resolver.api(null);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ✅ CORRECT: Boolean output
|
|
223
|
+
lambder.addApi('deleteUser', async (ctx, resolver) => {
|
|
224
|
+
return resolver.api(true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ✅ CORRECT: Array output
|
|
228
|
+
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
229
|
+
return resolver.api([
|
|
230
|
+
{ id: '1', name: 'User 1' },
|
|
231
|
+
{ id: '2', name: 'User 2' }
|
|
232
|
+
]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// ✅ CORRECT: Empty array
|
|
236
|
+
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
237
|
+
return resolver.api([]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
expect(lambder).toBeDefined();
|
|
241
|
+
|
|
242
|
+
// ❌ These would cause TypeScript errors (commented out):
|
|
243
|
+
|
|
244
|
+
// Wrong output type:
|
|
245
|
+
// lambder.addApi('getUser', async (ctx, resolver) => {
|
|
246
|
+
// return resolver.api("wrong"); // Error: string not assignable to { id: string, name: string }
|
|
247
|
+
// });
|
|
248
|
+
|
|
249
|
+
// Missing required property:
|
|
250
|
+
// lambder.addApi('getUser', async (ctx, resolver) => {
|
|
251
|
+
// return resolver.api({ id: '123' }); // Error: missing 'name' property
|
|
252
|
+
// });
|
|
253
|
+
|
|
254
|
+
// Wrong property type:
|
|
255
|
+
// lambder.addApi('getUser', async (ctx, resolver) => {
|
|
256
|
+
// return resolver.api({ id: 123, name: 'Test' }); // Error: id should be string
|
|
257
|
+
// });
|
|
258
|
+
|
|
259
|
+
// Wrong boolean output:
|
|
260
|
+
// lambder.addApi('deleteUser', async (ctx, resolver) => {
|
|
261
|
+
// return resolver.api("true"); // Error: string not assignable to boolean
|
|
262
|
+
// });
|
|
263
|
+
|
|
264
|
+
// Wrong array element type:
|
|
265
|
+
// lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
266
|
+
// return resolver.api([{ id: '1' }]); // Error: missing 'name' property
|
|
267
|
+
// });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('should enforce output types in resolver.die.api()', () => {
|
|
271
|
+
const lambder = new Lambder<TestApiContract>({
|
|
272
|
+
publicPath: './public',
|
|
273
|
+
apiPath: '/api'
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// ✅ CORRECT: die.api also enforces types
|
|
277
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
278
|
+
return resolver.die.api({ id: ctx.apiPayload.userId, name: 'Test' });
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ✅ CORRECT: die.api with null
|
|
282
|
+
lambder.addApi('deleteUser', async (ctx, resolver) => {
|
|
283
|
+
return resolver.die.api(true);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
expect(lambder).toBeDefined();
|
|
287
|
+
|
|
288
|
+
// ❌ These would cause TypeScript errors (commented out):
|
|
289
|
+
// lambder.addApi('getUser', async (ctx, resolver) => {
|
|
290
|
+
// return resolver.die.api("wrong"); // Error: wrong type
|
|
291
|
+
// });
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
it('should enforce output types in addSessionApi()', () => {
|
|
295
|
+
const lambder = new Lambder<TestApiContract>({
|
|
296
|
+
publicPath: './public',
|
|
297
|
+
apiPath: '/api'
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
// ✅ CORRECT: Session APIs also enforce output types
|
|
301
|
+
lambder.addSessionApi('createUser', async (ctx, resolver) => {
|
|
302
|
+
const { name, email } = ctx.apiPayload;
|
|
303
|
+
return resolver.api({ id: '1', name, email });
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// ✅ CORRECT: Session API with null
|
|
307
|
+
lambder.addSessionApi('deleteUser', async (ctx, resolver) => {
|
|
308
|
+
return resolver.api(true);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(lambder).toBeDefined();
|
|
312
|
+
|
|
313
|
+
// ❌ These would cause TypeScript errors (commented out):
|
|
314
|
+
// lambder.addSessionApi('createUser', async (ctx, resolver) => {
|
|
315
|
+
// return resolver.api({ id: '1' }); // Error: missing name and email
|
|
316
|
+
// });
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// ============================================================================
|
|
321
|
+
// Test 7: Complex Output Types
|
|
322
|
+
// ============================================================================
|
|
323
|
+
|
|
324
|
+
type ComplexOutputContract = ApiContract<{
|
|
325
|
+
// Primitive outputs
|
|
326
|
+
getCount: { input: void, output: number },
|
|
327
|
+
getMessage: { input: void, output: string },
|
|
328
|
+
isActive: { input: void, output: boolean },
|
|
329
|
+
|
|
330
|
+
// Nested objects
|
|
331
|
+
getStats: {
|
|
332
|
+
input: void,
|
|
333
|
+
output: {
|
|
334
|
+
users: { total: number, active: number },
|
|
335
|
+
products: { total: number, inStock: number }
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
// Union types
|
|
340
|
+
findUser: {
|
|
341
|
+
input: { email: string },
|
|
342
|
+
output: { id: string, name: string } | null
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
// Array of complex objects
|
|
346
|
+
getOrders: {
|
|
347
|
+
input: { userId: string },
|
|
348
|
+
output: Array<{
|
|
349
|
+
id: string,
|
|
350
|
+
items: Array<{ name: string, price: number }>,
|
|
351
|
+
total: number
|
|
352
|
+
}>
|
|
353
|
+
}
|
|
354
|
+
}>;
|
|
355
|
+
|
|
356
|
+
describe('Complex Output Types', () => {
|
|
357
|
+
it('should enforce primitive output types', () => {
|
|
358
|
+
const lambder = new Lambder<ComplexOutputContract>({
|
|
359
|
+
publicPath: './public',
|
|
360
|
+
apiPath: '/api'
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// ✅ CORRECT: Primitive outputs
|
|
364
|
+
lambder.addApi('getCount', async (ctx, resolver) => {
|
|
365
|
+
return resolver.api(42);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
lambder.addApi('getMessage', async (ctx, resolver) => {
|
|
369
|
+
return resolver.api("Hello World");
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
lambder.addApi('isActive', async (ctx, resolver) => {
|
|
373
|
+
return resolver.api(true);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
expect(lambder).toBeDefined();
|
|
377
|
+
|
|
378
|
+
// ❌ These would cause TypeScript errors (commented out):
|
|
379
|
+
// lambder.addApi('getCount', async (ctx, resolver) => {
|
|
380
|
+
// return resolver.api("42"); // Error: string not assignable to number
|
|
381
|
+
// });
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should enforce nested object types', () => {
|
|
385
|
+
const lambder = new Lambder<ComplexOutputContract>({
|
|
386
|
+
publicPath: './public',
|
|
387
|
+
apiPath: '/api'
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
// ✅ CORRECT: Nested structure
|
|
391
|
+
lambder.addApi('getStats', async (ctx, resolver) => {
|
|
392
|
+
return resolver.api({
|
|
393
|
+
users: { total: 100, active: 75 },
|
|
394
|
+
products: { total: 50, inStock: 40 }
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
expect(lambder).toBeDefined();
|
|
399
|
+
|
|
400
|
+
// ❌ These would cause TypeScript errors (commented out):
|
|
401
|
+
// lambder.addApi('getStats', async (ctx, resolver) => {
|
|
402
|
+
// return resolver.api({
|
|
403
|
+
// users: { total: 100 } // Error: missing 'active'
|
|
404
|
+
// });
|
|
405
|
+
// });
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should enforce union types correctly', () => {
|
|
409
|
+
const lambder = new Lambder<ComplexOutputContract>({
|
|
410
|
+
publicPath: './public',
|
|
411
|
+
apiPath: '/api'
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// ✅ CORRECT: Return object
|
|
415
|
+
lambder.addApi('findUser', async (ctx, resolver) => {
|
|
416
|
+
return resolver.api({ id: '123', name: 'John' });
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// ✅ CORRECT: Return null (part of union)
|
|
420
|
+
lambder.addApi('findUser', async (ctx, resolver) => {
|
|
421
|
+
return resolver.api(null);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
expect(lambder).toBeDefined();
|
|
425
|
+
|
|
426
|
+
// ❌ These would cause TypeScript errors (commented out):
|
|
427
|
+
// lambder.addApi('findUser', async (ctx, resolver) => {
|
|
428
|
+
// return resolver.api(undefined); // Error: undefined not in union
|
|
429
|
+
// });
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
it('should enforce complex nested array types', () => {
|
|
433
|
+
const lambder = new Lambder<ComplexOutputContract>({
|
|
434
|
+
publicPath: './public',
|
|
435
|
+
apiPath: '/api'
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
// ✅ CORRECT: Complex nested array
|
|
439
|
+
lambder.addApi('getOrders', async (ctx, resolver) => {
|
|
440
|
+
return resolver.api([
|
|
441
|
+
{
|
|
442
|
+
id: '1',
|
|
443
|
+
items: [
|
|
444
|
+
{ name: 'Item 1', price: 10 },
|
|
445
|
+
{ name: 'Item 2', price: 20 }
|
|
446
|
+
],
|
|
447
|
+
total: 30
|
|
448
|
+
}
|
|
449
|
+
]);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
expect(lambder).toBeDefined();
|
|
453
|
+
|
|
454
|
+
// ❌ These would cause TypeScript errors (commented out):
|
|
455
|
+
// lambder.addApi('getOrders', async (ctx, resolver) => {
|
|
456
|
+
// return resolver.api([
|
|
457
|
+
// {
|
|
458
|
+
// id: '1',
|
|
459
|
+
// items: [{ name: 'Item 1' }], // Error: missing 'price'
|
|
460
|
+
// total: 30
|
|
461
|
+
// }
|
|
462
|
+
// ]);
|
|
463
|
+
// });
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
// ============================================================================
|
|
468
|
+
// Test 8: Type Inference
|
|
469
|
+
// ============================================================================
|
|
470
|
+
|
|
471
|
+
describe('Type Inference', () => {
|
|
472
|
+
it('should infer types correctly from contract', () => {
|
|
473
|
+
const lambder = new Lambder<TestApiContract>({
|
|
474
|
+
publicPath: './public',
|
|
475
|
+
apiPath: '/api'
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// Test that TypeScript correctly infers the types
|
|
479
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
480
|
+
// ctx.apiPayload type should be inferred as { userId: string }
|
|
481
|
+
const userId: string = ctx.apiPayload.userId;
|
|
482
|
+
|
|
483
|
+
// Variable with explicit type matching contract
|
|
484
|
+
const user: { id: string, name: string } = {
|
|
485
|
+
id: userId,
|
|
486
|
+
name: 'Test User'
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
// Should accept the correctly typed variable
|
|
490
|
+
return resolver.api(user);
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
lambder.addApi('createUser', async (ctx, resolver) => {
|
|
494
|
+
// Input type inference
|
|
495
|
+
const name: string = ctx.apiPayload.name;
|
|
496
|
+
const email: string = ctx.apiPayload.email;
|
|
497
|
+
|
|
498
|
+
// Output type inference
|
|
499
|
+
const result: { id: string, name: string, email: string } = {
|
|
500
|
+
id: '123',
|
|
501
|
+
name,
|
|
502
|
+
email
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
return resolver.api(result);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
expect(lambder).toBeDefined();
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
200
512
|
// ============================================================================
|
|
201
513
|
// If this file compiles without errors, the type system is working! ✅
|
|
202
514
|
// ============================================================================
|