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.
- 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 -4
- 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 -7
- 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
|
@@ -6,17 +6,9 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, it, expect } from 'vitest';
|
|
9
|
+
import { z } from 'zod';
|
|
9
10
|
import Lambder from '../src/Lambder.js';
|
|
10
11
|
import LambderCaller from '../src/LambderCaller.js';
|
|
11
|
-
import type { ApiContract } from '../src/index.js';
|
|
12
|
-
|
|
13
|
-
// Test contract
|
|
14
|
-
type TestApiContract = ApiContract<{
|
|
15
|
-
getUser: { input: { userId: string }, output: { id: string, name: string } },
|
|
16
|
-
createUser: { input: { name: string, email: string }, output: { id: string, name: string, email: string } },
|
|
17
|
-
listUsers: { input: void, output: Array<{ id: string, name: string }> },
|
|
18
|
-
deleteUser: { input: { userId: string }, output: boolean }
|
|
19
|
-
}>;
|
|
20
12
|
|
|
21
13
|
// ============================================================================
|
|
22
14
|
// Test 1: LambderCaller Type Safety
|
|
@@ -24,6 +16,26 @@ type TestApiContract = ApiContract<{
|
|
|
24
16
|
|
|
25
17
|
describe('LambderCaller Type Safety', () => {
|
|
26
18
|
it('should have correct types', () => {
|
|
19
|
+
// Define contract via chaining
|
|
20
|
+
const lambder = new Lambder({
|
|
21
|
+
publicPath: './public',
|
|
22
|
+
apiPath: '/api'
|
|
23
|
+
})
|
|
24
|
+
.addApi('getUser', {
|
|
25
|
+
input: z.object({ userId: z.string() }),
|
|
26
|
+
output: z.object({ id: z.string(), name: z.string() })
|
|
27
|
+
}, async (ctx, resolver) => {
|
|
28
|
+
return resolver.api({ id: ctx.apiPayload.userId, name: 'Test' });
|
|
29
|
+
})
|
|
30
|
+
.addApi('createUser', {
|
|
31
|
+
input: z.object({ name: z.string(), email: z.string() }),
|
|
32
|
+
output: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
33
|
+
}, async (ctx, resolver) => {
|
|
34
|
+
return resolver.api({ id: '1', ...ctx.apiPayload });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
type TestApiContract = typeof lambder.ApiContractType;
|
|
38
|
+
|
|
27
39
|
const caller = new LambderCaller<TestApiContract>({
|
|
28
40
|
apiPath: '/api',
|
|
29
41
|
isCorsEnabled: false
|
|
@@ -55,13 +67,16 @@ describe('LambderCaller Type Safety', () => {
|
|
|
55
67
|
|
|
56
68
|
describe('Lambder Type Safety', () => {
|
|
57
69
|
it('should have correct types', () => {
|
|
58
|
-
const lambder = new Lambder
|
|
70
|
+
const lambder = new Lambder({
|
|
59
71
|
publicPath: './public',
|
|
60
72
|
apiPath: '/api'
|
|
61
73
|
});
|
|
62
74
|
|
|
63
75
|
// ✅ Should work: Typed API
|
|
64
|
-
lambder.addApi('getUser',
|
|
76
|
+
lambder.addApi('getUser', {
|
|
77
|
+
input: z.object({ userId: z.string() }),
|
|
78
|
+
output: z.object({ id: z.string(), name: z.string() })
|
|
79
|
+
}, async (ctx, resolver) => {
|
|
65
80
|
// ctx.apiPayload should be typed as { userId: string }
|
|
66
81
|
const userId: string = ctx.apiPayload.userId; // Should work
|
|
67
82
|
|
|
@@ -70,7 +85,10 @@ describe('Lambder Type Safety', () => {
|
|
|
70
85
|
});
|
|
71
86
|
|
|
72
87
|
// ✅ Should work: Typed session API
|
|
73
|
-
lambder.addSessionApi('createUser',
|
|
88
|
+
lambder.addSessionApi('createUser', {
|
|
89
|
+
input: z.object({ name: z.string(), email: z.string() }),
|
|
90
|
+
output: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
91
|
+
}, async (ctx, resolver) => {
|
|
74
92
|
// ctx.apiPayload should be typed as { name: string, email: string }
|
|
75
93
|
const name: string = ctx.apiPayload.name;
|
|
76
94
|
const email: string = ctx.apiPayload.email;
|
|
@@ -79,7 +97,10 @@ describe('Lambder Type Safety', () => {
|
|
|
79
97
|
});
|
|
80
98
|
|
|
81
99
|
// ✅ Should work: Void input API
|
|
82
|
-
lambder.addApi('listUsers',
|
|
100
|
+
lambder.addApi('listUsers', {
|
|
101
|
+
input: z.void(),
|
|
102
|
+
output: z.array(z.object({ id: z.string(), name: z.string() }))
|
|
103
|
+
}, async (ctx, resolver) => {
|
|
83
104
|
// ctx.apiPayload should be void/undefined
|
|
84
105
|
return resolver.api([
|
|
85
106
|
{ id: '1', name: 'User 1' },
|
|
@@ -88,7 +109,10 @@ describe('Lambder Type Safety', () => {
|
|
|
88
109
|
});
|
|
89
110
|
|
|
90
111
|
// ✅ Should work: RegExp (untyped)
|
|
91
|
-
lambder.addApi(
|
|
112
|
+
lambder.addApi('admin.anyAction', {
|
|
113
|
+
input: z.any(),
|
|
114
|
+
output: z.any()
|
|
115
|
+
}, async (ctx, resolver) => {
|
|
92
116
|
// ctx.apiPayload is any (untyped)
|
|
93
117
|
return resolver.api({});
|
|
94
118
|
});
|
|
@@ -126,7 +150,10 @@ describe('Backward Compatibility (No Contract)', () => {
|
|
|
126
150
|
});
|
|
127
151
|
|
|
128
152
|
// Should work - untyped
|
|
129
|
-
lambder.addApi('anyApi',
|
|
153
|
+
lambder.addApi('anyApi', {
|
|
154
|
+
input: z.any(),
|
|
155
|
+
output: z.any()
|
|
156
|
+
}, async (ctx, resolver) => {
|
|
130
157
|
// ctx.apiPayload is any
|
|
131
158
|
return resolver.api({ anything: ctx.apiPayload });
|
|
132
159
|
});
|
|
@@ -143,6 +170,19 @@ describe('Backward Compatibility (No Contract)', () => {
|
|
|
143
170
|
|
|
144
171
|
describe('apiRaw Method Type Safety', () => {
|
|
145
172
|
it('should have correct types for apiRaw', () => {
|
|
173
|
+
const lambder = new Lambder({
|
|
174
|
+
publicPath: './public',
|
|
175
|
+
apiPath: '/api'
|
|
176
|
+
})
|
|
177
|
+
.addApi('getUser', {
|
|
178
|
+
input: z.object({ userId: z.string() }),
|
|
179
|
+
output: z.object({ id: z.string(), name: z.string() })
|
|
180
|
+
}, async (ctx, resolver) => {
|
|
181
|
+
return resolver.api({ id: ctx.apiPayload.userId, name: 'Test' });
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
type TestApiContract = typeof lambder.ApiContractType;
|
|
185
|
+
|
|
146
186
|
const caller = new LambderCaller<TestApiContract>({
|
|
147
187
|
apiPath: '/api',
|
|
148
188
|
isCorsEnabled: false
|
|
@@ -203,40 +243,33 @@ describe('Complex Types', () => {
|
|
|
203
243
|
|
|
204
244
|
describe('Output Type Enforcement', () => {
|
|
205
245
|
it('should enforce output types in resolver.api()', () => {
|
|
206
|
-
const lambder = new Lambder
|
|
246
|
+
const lambder = new Lambder({
|
|
207
247
|
publicPath: './public',
|
|
208
248
|
apiPath: '/api'
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
249
|
+
})
|
|
250
|
+
.addApi('getUser', {
|
|
251
|
+
input: z.object({ userId: z.string() }),
|
|
252
|
+
output: z.object({ id: z.string(), name: z.string() }).nullable()
|
|
253
|
+
}, async (ctx, resolver) => {
|
|
213
254
|
const userId = ctx.apiPayload.userId;
|
|
214
255
|
return resolver.api({ id: userId, name: 'John Doe' });
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
// ✅ CORRECT: Boolean output
|
|
223
|
-
lambder.addApi('deleteUser', async (ctx, resolver) => {
|
|
256
|
+
})
|
|
257
|
+
.addApi('deleteUser', {
|
|
258
|
+
input: z.object({ userId: z.string() }),
|
|
259
|
+
output: z.boolean()
|
|
260
|
+
}, async (ctx, resolver) => {
|
|
224
261
|
return resolver.api(true);
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
262
|
+
})
|
|
263
|
+
.addApi('listUsers', {
|
|
264
|
+
input: z.void(),
|
|
265
|
+
output: z.array(z.object({ id: z.string(), name: z.string() }))
|
|
266
|
+
}, async (ctx, resolver) => {
|
|
229
267
|
return resolver.api([
|
|
230
268
|
{ id: '1', name: 'User 1' },
|
|
231
269
|
{ id: '2', name: 'User 2' }
|
|
232
270
|
]);
|
|
233
271
|
});
|
|
234
272
|
|
|
235
|
-
// ✅ CORRECT: Empty array
|
|
236
|
-
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
237
|
-
return resolver.api([]);
|
|
238
|
-
});
|
|
239
|
-
|
|
240
273
|
expect(lambder).toBeDefined();
|
|
241
274
|
|
|
242
275
|
// ❌ These would cause TypeScript errors (commented out):
|
|
@@ -268,18 +301,20 @@ describe('Output Type Enforcement', () => {
|
|
|
268
301
|
});
|
|
269
302
|
|
|
270
303
|
it('should enforce output types in resolver.die.api()', () => {
|
|
271
|
-
const lambder = new Lambder
|
|
304
|
+
const lambder = new Lambder({
|
|
272
305
|
publicPath: './public',
|
|
273
306
|
apiPath: '/api'
|
|
274
|
-
})
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
307
|
+
})
|
|
308
|
+
.addApi('getUser', {
|
|
309
|
+
input: z.object({ userId: z.string() }),
|
|
310
|
+
output: z.object({ id: z.string(), name: z.string() })
|
|
311
|
+
}, async (ctx, resolver) => {
|
|
278
312
|
return resolver.die.api({ id: ctx.apiPayload.userId, name: 'Test' });
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
313
|
+
})
|
|
314
|
+
.addApi('deleteUser', {
|
|
315
|
+
input: z.object({ userId: z.string() }),
|
|
316
|
+
output: z.boolean()
|
|
317
|
+
}, async (ctx, resolver) => {
|
|
283
318
|
return resolver.die.api(true);
|
|
284
319
|
});
|
|
285
320
|
|
|
@@ -292,19 +327,21 @@ describe('Output Type Enforcement', () => {
|
|
|
292
327
|
});
|
|
293
328
|
|
|
294
329
|
it('should enforce output types in addSessionApi()', () => {
|
|
295
|
-
const lambder = new Lambder
|
|
330
|
+
const lambder = new Lambder({
|
|
296
331
|
publicPath: './public',
|
|
297
332
|
apiPath: '/api'
|
|
298
|
-
})
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
333
|
+
})
|
|
334
|
+
.addSessionApi('createUser', {
|
|
335
|
+
input: z.object({ name: z.string(), email: z.string() }),
|
|
336
|
+
output: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
337
|
+
}, async (ctx, resolver) => {
|
|
302
338
|
const { name, email } = ctx.apiPayload;
|
|
303
339
|
return resolver.api({ id: '1', name, email });
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
340
|
+
})
|
|
341
|
+
.addSessionApi('deleteUser', {
|
|
342
|
+
input: z.object({ userId: z.string() }),
|
|
343
|
+
output: z.boolean()
|
|
344
|
+
}, async (ctx, resolver) => {
|
|
308
345
|
return resolver.api(true);
|
|
309
346
|
});
|
|
310
347
|
|
|
@@ -321,7 +358,7 @@ describe('Output Type Enforcement', () => {
|
|
|
321
358
|
// Test 7: Complex Output Types
|
|
322
359
|
// ============================================================================
|
|
323
360
|
|
|
324
|
-
type ComplexOutputContract =
|
|
361
|
+
type ComplexOutputContract = {
|
|
325
362
|
// Primitive outputs
|
|
326
363
|
getCount: { input: void, output: number },
|
|
327
364
|
getMessage: { input: void, output: string },
|
|
@@ -351,25 +388,30 @@ type ComplexOutputContract = ApiContract<{
|
|
|
351
388
|
total: number
|
|
352
389
|
}>
|
|
353
390
|
}
|
|
354
|
-
}
|
|
391
|
+
};
|
|
355
392
|
|
|
356
393
|
describe('Complex Output Types', () => {
|
|
357
394
|
it('should enforce primitive output types', () => {
|
|
358
|
-
const lambder = new Lambder
|
|
395
|
+
const lambder = new Lambder({
|
|
359
396
|
publicPath: './public',
|
|
360
397
|
apiPath: '/api'
|
|
361
|
-
})
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
398
|
+
})
|
|
399
|
+
.addApi('getCount', {
|
|
400
|
+
input: z.void(),
|
|
401
|
+
output: z.number()
|
|
402
|
+
}, async (ctx, resolver) => {
|
|
365
403
|
return resolver.api(42);
|
|
366
|
-
})
|
|
367
|
-
|
|
368
|
-
|
|
404
|
+
})
|
|
405
|
+
.addApi('getMessage', {
|
|
406
|
+
input: z.void(),
|
|
407
|
+
output: z.string()
|
|
408
|
+
}, async (ctx, resolver) => {
|
|
369
409
|
return resolver.api("Hello World");
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
|
|
410
|
+
})
|
|
411
|
+
.addApi('isActive', {
|
|
412
|
+
input: z.void(),
|
|
413
|
+
output: z.boolean()
|
|
414
|
+
}, async (ctx, resolver) => {
|
|
373
415
|
return resolver.api(true);
|
|
374
416
|
});
|
|
375
417
|
|
|
@@ -382,13 +424,17 @@ describe('Complex Output Types', () => {
|
|
|
382
424
|
});
|
|
383
425
|
|
|
384
426
|
it('should enforce nested object types', () => {
|
|
385
|
-
const lambder = new Lambder
|
|
427
|
+
const lambder = new Lambder({
|
|
386
428
|
publicPath: './public',
|
|
387
429
|
apiPath: '/api'
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
430
|
+
})
|
|
431
|
+
.addApi('getStats', {
|
|
432
|
+
input: z.void(),
|
|
433
|
+
output: z.object({
|
|
434
|
+
users: z.object({ total: z.number(), active: z.number() }),
|
|
435
|
+
products: z.object({ total: z.number(), inStock: z.number() })
|
|
436
|
+
})
|
|
437
|
+
}, async (ctx, resolver) => {
|
|
392
438
|
return resolver.api({
|
|
393
439
|
users: { total: 100, active: 75 },
|
|
394
440
|
products: { total: 50, inStock: 40 }
|
|
@@ -406,21 +452,17 @@ describe('Complex Output Types', () => {
|
|
|
406
452
|
});
|
|
407
453
|
|
|
408
454
|
it('should enforce union types correctly', () => {
|
|
409
|
-
const lambder = new Lambder
|
|
455
|
+
const lambder = new Lambder({
|
|
410
456
|
publicPath: './public',
|
|
411
457
|
apiPath: '/api'
|
|
412
|
-
})
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
458
|
+
})
|
|
459
|
+
.addApi('findUser', {
|
|
460
|
+
input: z.object({ email: z.string() }),
|
|
461
|
+
output: z.object({ id: z.string(), name: z.string() }).nullable()
|
|
462
|
+
}, async (ctx, resolver) => {
|
|
416
463
|
return resolver.api({ id: '123', name: 'John' });
|
|
417
464
|
});
|
|
418
465
|
|
|
419
|
-
// ✅ CORRECT: Return null (part of union)
|
|
420
|
-
lambder.addApi('findUser', async (ctx, resolver) => {
|
|
421
|
-
return resolver.api(null);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
466
|
expect(lambder).toBeDefined();
|
|
425
467
|
|
|
426
468
|
// ❌ These would cause TypeScript errors (commented out):
|
|
@@ -430,13 +472,18 @@ describe('Complex Output Types', () => {
|
|
|
430
472
|
});
|
|
431
473
|
|
|
432
474
|
it('should enforce complex nested array types', () => {
|
|
433
|
-
const lambder = new Lambder
|
|
475
|
+
const lambder = new Lambder({
|
|
434
476
|
publicPath: './public',
|
|
435
477
|
apiPath: '/api'
|
|
436
|
-
})
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
478
|
+
})
|
|
479
|
+
.addApi('getOrders', {
|
|
480
|
+
input: z.object({ userId: z.string() }),
|
|
481
|
+
output: z.array(z.object({
|
|
482
|
+
id: z.string(),
|
|
483
|
+
items: z.array(z.object({ name: z.string(), price: z.number() })),
|
|
484
|
+
total: z.number()
|
|
485
|
+
}))
|
|
486
|
+
}, async (ctx, resolver) => {
|
|
440
487
|
return resolver.api([
|
|
441
488
|
{
|
|
442
489
|
id: '1',
|
|
@@ -470,13 +517,14 @@ describe('Complex Output Types', () => {
|
|
|
470
517
|
|
|
471
518
|
describe('Type Inference', () => {
|
|
472
519
|
it('should infer types correctly from contract', () => {
|
|
473
|
-
const lambder = new Lambder
|
|
520
|
+
const lambder = new Lambder({
|
|
474
521
|
publicPath: './public',
|
|
475
522
|
apiPath: '/api'
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
523
|
+
})
|
|
524
|
+
.addApi('getUser', {
|
|
525
|
+
input: z.object({ userId: z.string() }),
|
|
526
|
+
output: z.object({ id: z.string(), name: z.string() })
|
|
527
|
+
}, async (ctx, resolver) => {
|
|
480
528
|
// ctx.apiPayload type should be inferred as { userId: string }
|
|
481
529
|
const userId: string = ctx.apiPayload.userId;
|
|
482
530
|
|
|
@@ -488,9 +536,11 @@ describe('Type Inference', () => {
|
|
|
488
536
|
|
|
489
537
|
// Should accept the correctly typed variable
|
|
490
538
|
return resolver.api(user);
|
|
491
|
-
})
|
|
492
|
-
|
|
493
|
-
|
|
539
|
+
})
|
|
540
|
+
.addApi('createUser', {
|
|
541
|
+
input: z.object({ name: z.string(), email: z.string() }),
|
|
542
|
+
output: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
543
|
+
}, async (ctx, resolver) => {
|
|
494
544
|
// Input type inference
|
|
495
545
|
const name: string = ctx.apiPayload.name;
|
|
496
546
|
const email: string = ctx.apiPayload.email;
|