lambder 2.0.3 → 2.0.5
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 +3 -3
- package/dist/Lambder.d.ts +3 -3
- package/dist/Lambder.js +2 -2
- package/dist/LambderApiContract.d.ts +1 -1
- package/dist/LambderApiContract.js +1 -1
- package/docs/LAMBDER_MSW.md +25 -16
- package/docs/TYPE_SAFE_QUICK_START.md +1 -1
- package/examples/msw-testing-example.ts +1 -1
- package/examples/zod-chained-api-example.ts +2 -2
- package/package.json +1 -1
- package/src/Lambder.ts +4 -4
- package/src/LambderApiContract.ts +1 -1
- package/tests/type-safety.test.ts +2 -2
- package/tests/use-plugin.test.ts +34 -11
package/Readme.md
CHANGED
|
@@ -70,7 +70,7 @@ const app = lambder
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
// Export the inferred contract for the frontend
|
|
73
|
-
export type AppContract = typeof lambder.
|
|
73
|
+
export type AppContract = typeof lambder.ApiContract;
|
|
74
74
|
|
|
75
75
|
// Export the handler
|
|
76
76
|
export const handler = lambder.getHandler();
|
|
@@ -196,7 +196,7 @@ import { userApi } from "./user-api";
|
|
|
196
196
|
const lambder = new Lambder()
|
|
197
197
|
.use(userApi);
|
|
198
198
|
|
|
199
|
-
export type AppContract = typeof lambder.
|
|
199
|
+
export type AppContract = typeof lambder.ApiContract;
|
|
200
200
|
```
|
|
201
201
|
|
|
202
202
|
|
|
@@ -457,7 +457,7 @@ const lambder = new Lambder({ apiPath: '/api' })
|
|
|
457
457
|
return res.api({ id: ctx.apiPayload.userId, name: "John" });
|
|
458
458
|
});
|
|
459
459
|
|
|
460
|
-
export type AppContract = typeof lambder.
|
|
460
|
+
export type AppContract = typeof lambder.ApiContract;
|
|
461
461
|
export const handler = lambder.getHandler();
|
|
462
462
|
```
|
|
463
463
|
|
package/dist/Lambder.d.ts
CHANGED
|
@@ -77,10 +77,10 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
77
77
|
* @example
|
|
78
78
|
* ```typescript
|
|
79
79
|
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
80
|
-
* export type AppContract = typeof lambder.
|
|
80
|
+
* export type AppContract = typeof lambder.ApiContract;
|
|
81
81
|
* ```
|
|
82
82
|
*/
|
|
83
|
-
readonly
|
|
83
|
+
readonly ApiContract: _TContract;
|
|
84
84
|
private actionList;
|
|
85
85
|
private hookList;
|
|
86
86
|
private globalErrorHandler;
|
|
@@ -115,7 +115,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
115
115
|
private handleNoMatchedAction;
|
|
116
116
|
addRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): this;
|
|
117
117
|
addSessionRoute(condition: Path | ConditionFunction | RegExp, actionFn: SessionActionFunction<TSessionData>): this;
|
|
118
|
-
use<_TNewContract extends Record<string, any>>(plugin: (lambder: Lambder<TSessionData, _TContract>) => Lambder<TSessionData, _TNewContract>): Lambder<TSessionData, _TNewContract>;
|
|
118
|
+
use<_TNewContract extends Record<string, any>>(plugin: (lambder: Lambder<TSessionData, _TContract>) => Lambder<TSessionData, _TNewContract>): Lambder<TSessionData, _TNewContract extends _TContract ? _TNewContract : (_TContract & _TNewContract)>;
|
|
119
119
|
addApi<TName extends string, TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny>(name: TName, schema: {
|
|
120
120
|
input: TInput;
|
|
121
121
|
output: TOutput;
|
package/dist/Lambder.js
CHANGED
|
@@ -76,10 +76,10 @@ export default class Lambder {
|
|
|
76
76
|
* @example
|
|
77
77
|
* ```typescript
|
|
78
78
|
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
79
|
-
* export type AppContract = typeof lambder.
|
|
79
|
+
* export type AppContract = typeof lambder.ApiContract;
|
|
80
80
|
* ```
|
|
81
81
|
*/
|
|
82
|
-
|
|
82
|
+
ApiContract;
|
|
83
83
|
actionList;
|
|
84
84
|
hookList;
|
|
85
85
|
globalErrorHandler = null;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lambder API Contract System
|
|
3
3
|
*
|
|
4
|
-
* Contracts are built via method chaining and inferred using typeof lambder.
|
|
4
|
+
* Contracts are built via method chaining and inferred using typeof lambder.ApiContract
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* Base shape for API contracts - used by LambderCaller and LambderMSW
|
package/docs/LAMBDER_MSW.md
CHANGED
|
@@ -56,23 +56,32 @@ afterAll(() => server.close());
|
|
|
56
56
|
When using TypeScript API contracts, LambderMSW provides full type safety:
|
|
57
57
|
|
|
58
58
|
```typescript
|
|
59
|
-
//
|
|
60
|
-
import
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
59
|
+
// backend/index.ts (define your APIs with Lambder)
|
|
60
|
+
import { z } from 'zod';
|
|
61
|
+
import Lambder from 'lambder';
|
|
62
|
+
|
|
63
|
+
const lambder = new Lambder({ apiPath: '/secure' })
|
|
64
|
+
.addApi('getUserById', {
|
|
65
|
+
input: z.object({ userId: z.string() }),
|
|
66
|
+
output: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
67
|
+
}, async (ctx, resolver) => {
|
|
68
|
+
// Implementation...
|
|
69
|
+
return resolver.api({ id: ctx.apiPayload.userId, name: 'John', email: 'john@example.com' });
|
|
70
|
+
})
|
|
71
|
+
.addApi('createUser', {
|
|
72
|
+
input: z.object({ name: z.string(), email: z.string() }),
|
|
73
|
+
output: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
74
|
+
}, async (ctx, resolver) => {
|
|
75
|
+
// Implementation...
|
|
76
|
+
return resolver.api({ id: '123', ...ctx.apiPayload });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Export the inferred contract type
|
|
80
|
+
export type MyApiContract = typeof lambder.ApiContract;
|
|
72
81
|
|
|
73
82
|
// test/setup.ts
|
|
74
83
|
import { LambderMSW } from 'lambder';
|
|
75
|
-
import type { MyApiContract } from '../
|
|
84
|
+
import type { MyApiContract } from '../backend';
|
|
76
85
|
|
|
77
86
|
const lambderMSW = new LambderMSW<MyApiContract>({
|
|
78
87
|
apiPath: '/secure',
|
|
@@ -208,7 +217,7 @@ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
|
208
217
|
import { setupServer } from 'msw/node';
|
|
209
218
|
import { LambderMSW } from 'lambder';
|
|
210
219
|
import { LambderCaller } from 'lambder';
|
|
211
|
-
import type { MyApiContract } from '../
|
|
220
|
+
import type { MyApiContract } from '../backend'; // Type-only import from your backend
|
|
212
221
|
|
|
213
222
|
// Setup MSW
|
|
214
223
|
const lambderMSW = new LambderMSW<MyApiContract>({
|
|
@@ -305,7 +314,7 @@ LambderMSW also works in browser environments with MSW's browser integration:
|
|
|
305
314
|
// test/browser-setup.ts
|
|
306
315
|
import { setupWorker } from 'msw/browser';
|
|
307
316
|
import { LambderMSW } from 'lambder';
|
|
308
|
-
import type { MyApiContract } from '../
|
|
317
|
+
import type { MyApiContract } from '../backend'; // Type-only import from your backend
|
|
309
318
|
|
|
310
319
|
const lambderMSW = new LambderMSW<MyApiContract>({
|
|
311
320
|
apiPath: '/secure'
|
|
@@ -40,7 +40,7 @@ const lambder = new Lambder()
|
|
|
40
40
|
output: z.object({ success: z.boolean() })
|
|
41
41
|
}, async () => ({} as any));
|
|
42
42
|
|
|
43
|
-
type TestApiContract = typeof lambder.
|
|
43
|
+
type TestApiContract = typeof lambder.ApiContract;
|
|
44
44
|
|
|
45
45
|
// Setup LambderMSW with type safety
|
|
46
46
|
const lambderMSW = new LambderMSW<TestApiContract>({
|
|
@@ -46,7 +46,7 @@ const lambder = new Lambder({
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// 3. Export the inferred contract type for Frontend
|
|
49
|
-
export type AppContract = typeof lambder.
|
|
49
|
+
export type AppContract = typeof lambder.ApiContract;
|
|
50
50
|
|
|
51
51
|
// 4. Modular example using .use()
|
|
52
52
|
const authApi = <T>(l: Lambder<T>) => {
|
|
@@ -60,4 +60,4 @@ const authApi = <T>(l: Lambder<T>) => {
|
|
|
60
60
|
|
|
61
61
|
const lambderWithAuth = lambder.use(authApi);
|
|
62
62
|
|
|
63
|
-
export type AuthContract = typeof lambderWithAuth.
|
|
63
|
+
export type AuthContract = typeof lambderWithAuth.ApiContract;
|
package/package.json
CHANGED
package/src/Lambder.ts
CHANGED
|
@@ -133,10 +133,10 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
133
133
|
* @example
|
|
134
134
|
* ```typescript
|
|
135
135
|
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
136
|
-
* export type AppContract = typeof lambder.
|
|
136
|
+
* export type AppContract = typeof lambder.ApiContract;
|
|
137
137
|
* ```
|
|
138
138
|
*/
|
|
139
|
-
public readonly
|
|
139
|
+
public readonly ApiContract!: _TContract;
|
|
140
140
|
|
|
141
141
|
private actionList: ActionObject[];
|
|
142
142
|
private hookList: {
|
|
@@ -285,8 +285,8 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
285
285
|
// Plugin system
|
|
286
286
|
public use<_TNewContract extends Record<string, any>>(
|
|
287
287
|
plugin: (lambder: Lambder<TSessionData, _TContract>) => Lambder<TSessionData, _TNewContract>
|
|
288
|
-
): Lambder<TSessionData, _TNewContract> {
|
|
289
|
-
return plugin(this);
|
|
288
|
+
): Lambder<TSessionData, _TNewContract extends _TContract ? _TNewContract : (_TContract & _TNewContract)> {
|
|
289
|
+
return plugin(this) as any;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
292
|
// Typed API with Zod
|
|
@@ -34,7 +34,7 @@ describe('LambderCaller Type Safety', () => {
|
|
|
34
34
|
return resolver.api({ id: '1', ...ctx.apiPayload });
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
type TestApiContract = typeof lambder.
|
|
37
|
+
type TestApiContract = typeof lambder.ApiContract;
|
|
38
38
|
|
|
39
39
|
const caller = new LambderCaller<TestApiContract>({
|
|
40
40
|
apiPath: '/api',
|
|
@@ -181,7 +181,7 @@ describe('apiRaw Method Type Safety', () => {
|
|
|
181
181
|
return resolver.api({ id: ctx.apiPayload.userId, name: 'Test' });
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
type TestApiContract = typeof lambder.
|
|
184
|
+
type TestApiContract = typeof lambder.ApiContract;
|
|
185
185
|
|
|
186
186
|
const caller = new LambderCaller<TestApiContract>({
|
|
187
187
|
apiPath: '/api',
|
package/tests/use-plugin.test.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This file tests the plugin system that allows modular API composition
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { describe, it, expect } from 'vitest';
|
|
7
|
+
import { describe, it, expect, expectTypeOf } from 'vitest';
|
|
8
8
|
import { z } from 'zod';
|
|
9
9
|
import Lambder from '../src/Lambder.js';
|
|
10
10
|
import LambderCaller from '../src/LambderCaller.js';
|
|
@@ -70,7 +70,7 @@ describe('Plugin System - Basic Usage', () => {
|
|
|
70
70
|
const result = await handler(event, createMockContext());
|
|
71
71
|
|
|
72
72
|
expect(result.statusCode).toBe(200);
|
|
73
|
-
const body = JSON.parse(result.body);
|
|
73
|
+
const body = JSON.parse(result.body || '{}');
|
|
74
74
|
expect(body.payload).toEqual({ id: '123', name: 'John Doe' });
|
|
75
75
|
});
|
|
76
76
|
|
|
@@ -90,7 +90,7 @@ describe('Plugin System - Basic Usage', () => {
|
|
|
90
90
|
apiPath: '/api'
|
|
91
91
|
}).use(userPlugin);
|
|
92
92
|
|
|
93
|
-
type AppContract = typeof lambder.
|
|
93
|
+
type AppContract = typeof lambder.ApiContract;
|
|
94
94
|
|
|
95
95
|
const caller = new LambderCaller<AppContract>({
|
|
96
96
|
apiPath: '/api',
|
|
@@ -163,19 +163,19 @@ describe('Plugin System - Multiple Plugins', () => {
|
|
|
163
163
|
const userEvent = createMockEvent('getUser', { userId: '123' });
|
|
164
164
|
const userResult = await handler(userEvent, createMockContext());
|
|
165
165
|
expect(userResult.statusCode).toBe(200);
|
|
166
|
-
expect(JSON.parse(userResult.body).payload.name).toBe('John');
|
|
166
|
+
expect(JSON.parse(userResult.body || '{}').payload.name).toBe('John');
|
|
167
167
|
|
|
168
168
|
// Test product API
|
|
169
169
|
const productEvent = createMockEvent('getProduct', { productId: 'prod-456' });
|
|
170
170
|
const productResult = await handler(productEvent, createMockContext());
|
|
171
171
|
expect(productResult.statusCode).toBe(200);
|
|
172
|
-
expect(JSON.parse(productResult.body).payload.title).toBe('Test Product');
|
|
172
|
+
expect(JSON.parse(productResult.body || '{}').payload.title).toBe('Test Product');
|
|
173
173
|
|
|
174
174
|
// Test order API
|
|
175
175
|
const orderEvent = createMockEvent('createOrder', { userId: '123', productId: 'prod-456' });
|
|
176
176
|
const orderResult = await handler(orderEvent, createMockContext());
|
|
177
177
|
expect(orderResult.statusCode).toBe(200);
|
|
178
|
-
expect(JSON.parse(orderResult.body).payload.orderId).toBe('order-123');
|
|
178
|
+
expect(JSON.parse(orderResult.body || '{}').payload.orderId).toBe('order-123');
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
it('should accumulate types from multiple plugins', () => {
|
|
@@ -206,7 +206,7 @@ describe('Plugin System - Multiple Plugins', () => {
|
|
|
206
206
|
.use(userPlugin)
|
|
207
207
|
.use(productPlugin);
|
|
208
208
|
|
|
209
|
-
type AppContract = typeof lambder.
|
|
209
|
+
type AppContract = typeof lambder.ApiContract;
|
|
210
210
|
|
|
211
211
|
const caller = new LambderCaller<AppContract>({
|
|
212
212
|
apiPath: '/api',
|
|
@@ -263,17 +263,17 @@ describe('Plugin System - Mixed Usage', () => {
|
|
|
263
263
|
// Test direct API before plugin
|
|
264
264
|
const healthEvent = createMockEvent('healthCheck', undefined);
|
|
265
265
|
const healthResult = await handler(healthEvent, createMockContext());
|
|
266
|
-
expect(JSON.parse(healthResult.body).payload.status).toBe('ok');
|
|
266
|
+
expect(JSON.parse(healthResult.body || '{}').payload.status).toBe('ok');
|
|
267
267
|
|
|
268
268
|
// Test plugin API
|
|
269
269
|
const userEvent = createMockEvent('getUser', { userId: '123' });
|
|
270
270
|
const userResult = await handler(userEvent, createMockContext());
|
|
271
|
-
expect(JSON.parse(userResult.body).payload.name).toBe('John');
|
|
271
|
+
expect(JSON.parse(userResult.body || '{}').payload.name).toBe('John');
|
|
272
272
|
|
|
273
273
|
// Test direct API after plugin
|
|
274
274
|
const versionEvent = createMockEvent('getVersion', undefined);
|
|
275
275
|
const versionResult = await handler(versionEvent, createMockContext());
|
|
276
|
-
expect(JSON.parse(versionResult.body).payload.version).toBe('2.0');
|
|
276
|
+
expect(JSON.parse(versionResult.body || '{}').payload.version).toBe('2.0');
|
|
277
277
|
});
|
|
278
278
|
});
|
|
279
279
|
|
|
@@ -424,7 +424,7 @@ describe('Plugin System - Type Safety', () => {
|
|
|
424
424
|
.use(plugin1)
|
|
425
425
|
.use(plugin2);
|
|
426
426
|
|
|
427
|
-
type Contract = typeof lambder.
|
|
427
|
+
type Contract = typeof lambder.ApiContract;
|
|
428
428
|
|
|
429
429
|
// Type assertions - both api1 and api2 should be in the contract
|
|
430
430
|
// We test this at runtime by creating a caller
|
|
@@ -435,3 +435,26 @@ describe('Plugin System - Type Safety', () => {
|
|
|
435
435
|
expect(caller).toBeDefined();
|
|
436
436
|
});
|
|
437
437
|
});
|
|
438
|
+
|
|
439
|
+
// ============================================================================
|
|
440
|
+
// Test 7: Non-Generic Plugin Type Accumulation
|
|
441
|
+
// ============================================================================
|
|
442
|
+
|
|
443
|
+
describe('Plugin System - Non-Generic Plugins', () => {
|
|
444
|
+
it('should accumulate types correctly when using non-generic plugins', () => {
|
|
445
|
+
const plugin1 = (l: Lambder) => l.addApi('api1', { input: z.void(), output: z.void() }, async (ctx, res) => res.raw({ statusCode: 200, body: '' }));
|
|
446
|
+
const plugin2 = (l: Lambder) => l.addApi('api2', { input: z.void(), output: z.void() }, async (ctx, res) => res.raw({ statusCode: 200, body: '' }));
|
|
447
|
+
|
|
448
|
+
const lambder = new Lambder({ publicPath: '', apiPath: '' })
|
|
449
|
+
.addApi('initialApi', { input: z.void(), output: z.void() }, async (ctx, res) => res.raw({ statusCode: 200, body: '' }))
|
|
450
|
+
.use(plugin1)
|
|
451
|
+
.use(plugin2);
|
|
452
|
+
|
|
453
|
+
type Contract = typeof lambder.ApiContract;
|
|
454
|
+
|
|
455
|
+
// Check if both api1 and api2 exist in Contract
|
|
456
|
+
expectTypeOf<Contract>().toHaveProperty('initialApi');
|
|
457
|
+
expectTypeOf<Contract>().toHaveProperty('api1');
|
|
458
|
+
expectTypeOf<Contract>().toHaveProperty('api2');
|
|
459
|
+
});
|
|
460
|
+
});
|