lambder 1.0.132 → 1.0.134

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 CHANGED
@@ -544,6 +544,42 @@ const user = await caller.api('getUserById', { userId: '123' });
544
544
 
545
545
  📖 **[Read the Quick Start Guide](docs/TYPE_SAFE_QUICK_START.md)** for more details and examples!
546
546
 
547
+ ## Testing with LambderMSW
548
+
549
+ LambderMSW provides seamless integration with [MSW (Mock Service Worker)](https://mswjs.io/) for testing your APIs. It works perfectly with type-safe API contracts!
550
+
551
+ ```typescript
552
+ import { LambderMSW } from 'lambder';
553
+ import { setupServer } from 'msw/node';
554
+ import type { MyApiContract } from './shared/apiContract';
555
+
556
+ const lambderMSW = new LambderMSW<MyApiContract>({
557
+ apiPath: '/api',
558
+ });
559
+
560
+ const handlers = [
561
+ // Mock API with full type safety! ✨
562
+ lambderMSW.mockApi('getUserById', async (payload) => {
563
+ return {
564
+ id: payload.userId,
565
+ name: 'John Doe',
566
+ email: 'john@example.com'
567
+ };
568
+ }),
569
+
570
+ // Mock errors, delays, and more
571
+ lambderMSW.mockApi('createUser', async (payload) => {
572
+ return { id: '123', ...payload };
573
+ }, { delay: 500 }),
574
+
575
+ lambderMSW.mockSessionExpired('protectedApi'),
576
+ ];
577
+
578
+ const server = setupServer(...handlers);
579
+ ```
580
+
581
+ 📖 **[Read the LambderMSW Guide](docs/LAMBDER_MSW.md)** for complete testing documentation!
582
+
547
583
  ## Contributing
548
584
 
549
585
  Contributions are welcome! Especially for documentation. If you have an idea for an improvement or have found a bug, please open an issue or submit a pull request.
@@ -23,27 +23,32 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
23
23
  * @param apiName - The name of the API to mock
24
24
  * @param handler - Function that returns the mock payload
25
25
  * @param options - Additional response options (session expired, version expired, etc.)
26
+ *
27
+ * Note: TypeScript will check that the return type is assignable to the output type,
28
+ * but due to structural typing, extra properties are allowed. Enable strict checks
29
+ * in your tsconfig.json with "noUncheckedIndexedAccess" and "exactOptionalPropertyTypes"
30
+ * for better type safety.
26
31
  */
27
- mockApi<TApiName extends keyof TContract & string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any, TInput = TApiName extends keyof TContract ? TContract[TApiName]['input'] : any>(apiName: TApiName, handler: (payload?: TInput) => Promise<TOutput> | TOutput, options?: MockApiOptions): RequestHandler;
32
+ mockApi<TApiName extends keyof TContract>(apiName: TApiName, handler: (payload?: TContract[TApiName]['input']) => Promise<TContract[TApiName]['output']> | TContract[TApiName]['output'], options?: MockApiOptions): RequestHandler;
28
33
  /**
29
34
  * Mock an API endpoint that returns a session expired error
30
35
  */
31
- mockSessionExpired<TApiName extends keyof TContract & string>(apiName: TApiName): RequestHandler;
36
+ mockSessionExpired<TApiName extends keyof TContract>(apiName: TApiName): RequestHandler;
32
37
  /**
33
38
  * Mock an API endpoint that returns a version expired error
34
39
  */
35
- mockVersionExpired<TApiName extends keyof TContract & string>(apiName: TApiName): RequestHandler;
40
+ mockVersionExpired<TApiName extends keyof TContract>(apiName: TApiName): RequestHandler;
36
41
  /**
37
42
  * Mock an API endpoint that returns a not authorized error
38
43
  */
39
- mockNotAuthorized<TApiName extends keyof TContract & string>(apiName: TApiName): RequestHandler;
44
+ mockNotAuthorized<TApiName extends keyof TContract>(apiName: TApiName): RequestHandler;
40
45
  /**
41
46
  * Mock an API endpoint that returns an error message
42
47
  */
43
- mockError<TApiName extends keyof TContract & string>(apiName: TApiName, errorMessage: string): RequestHandler;
48
+ mockError<TApiName extends keyof TContract>(apiName: TApiName, errorMessage: string): RequestHandler;
44
49
  /**
45
50
  * Mock an API endpoint with a custom message
46
51
  */
47
- mockWithMessage<TApiName extends keyof TContract & string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any, TInput = TApiName extends keyof TContract ? TContract[TApiName]['input'] : any>(apiName: TApiName, handler: (payload?: TInput) => Promise<TOutput> | TOutput, message: any): RequestHandler;
52
+ mockWithMessage<TApiName extends keyof TContract>(apiName: TApiName, handler: (payload?: TContract[TApiName]['input']) => Promise<TContract[TApiName]['output']> | TContract[TApiName]['output'], message: any): RequestHandler;
48
53
  }
49
54
  export {};
@@ -21,38 +21,57 @@ export default class LambderMSW {
21
21
  * @param apiName - The name of the API to mock
22
22
  * @param handler - Function that returns the mock payload
23
23
  * @param options - Additional response options (session expired, version expired, etc.)
24
+ *
25
+ * Note: TypeScript will check that the return type is assignable to the output type,
26
+ * but due to structural typing, extra properties are allowed. Enable strict checks
27
+ * in your tsconfig.json with "noUncheckedIndexedAccess" and "exactOptionalPropertyTypes"
28
+ * for better type safety.
24
29
  */
25
30
  mockApi(apiName, handler, options) {
26
31
  return this.http.post(this.apiPath, async ({ request }) => {
32
+ let body;
27
33
  try {
28
- const body = await request.json();
29
- console.log("LambderMSW called for:", body?.apiName, "matching against:", apiName);
30
- // Check if this is the API we're mocking
31
- if (body?.apiName === apiName) {
32
- // Add artificial delay if specified
33
- if (options?.delay) {
34
- await new Promise(resolve => setTimeout(resolve, options.delay));
35
- }
36
- // Call the handler with the payload from the request
37
- const payload = await handler(body?.payload);
38
- console.log("Matched! Returning payload for:", apiName);
39
- const response = {
40
- apiVersion: this.apiVersion,
41
- payload,
42
- ...(options?.versionExpired ? { versionExpired: options.versionExpired } : {}),
43
- ...(options?.sessionExpired ? { sessionExpired: options.sessionExpired } : {}),
44
- ...(options?.notAuthorized ? { notAuthorized: options.notAuthorized } : {}),
45
- ...(options?.message ? { message: options.message } : {}),
46
- ...(options?.errorMessage ? { errorMessage: options.errorMessage } : {}),
47
- ...(options?.logList?.length ? { logList: options.logList } : {}),
48
- };
49
- return this.HttpResponse.json(response);
50
- }
34
+ body = await request.json();
35
+ }
36
+ catch (parseErr) {
37
+ // If JSON parsing fails, return undefined to let other handlers try
38
+ console.warn("LambderMSW: Failed to parse request body as JSON");
39
+ return;
40
+ }
41
+ // Check if body is valid and has apiName
42
+ if (!body || typeof body.apiName !== 'string') {
43
+ // Invalid request format, let other handlers try
44
+ return;
45
+ }
46
+ console.log("LambderMSW called for:", body.apiName, "matching against:", apiName);
47
+ // Check if this is the API we're mocking
48
+ if (body.apiName !== apiName) {
51
49
  // If this handler doesn't match, return undefined to let MSW try other handlers
52
50
  return;
53
51
  }
52
+ try {
53
+ // Add artificial delay if specified
54
+ if (options?.delay) {
55
+ await new Promise(resolve => setTimeout(resolve, options.delay));
56
+ }
57
+ // Call the handler with the payload from the request
58
+ const payload = await handler(body.payload);
59
+ console.log("Matched! Returning payload for:", apiName);
60
+ const response = {
61
+ apiVersion: this.apiVersion,
62
+ payload,
63
+ ...(options?.versionExpired ? { versionExpired: options.versionExpired } : {}),
64
+ ...(options?.sessionExpired ? { sessionExpired: options.sessionExpired } : {}),
65
+ ...(options?.notAuthorized ? { notAuthorized: options.notAuthorized } : {}),
66
+ ...(options?.message ? { message: options.message } : {}),
67
+ ...(options?.errorMessage ? { errorMessage: options.errorMessage } : {}),
68
+ ...(options?.logList?.length ? { logList: options.logList } : {}),
69
+ };
70
+ return this.HttpResponse.json(response);
71
+ }
54
72
  catch (err) {
55
- console.error("Error in LambderMSW:", err);
73
+ // Only handle errors that occur during handler execution for matched APIs
74
+ console.error("Error in LambderMSW handler for", apiName, ":", err);
56
75
  const errorResponse = {
57
76
  apiVersion: this.apiVersion,
58
77
  payload: null,
@@ -0,0 +1,430 @@
1
+ # LambderMSW - Mock Service Worker Integration
2
+
3
+ LambderMSW provides seamless integration with [MSW (Mock Service Worker)](https://mswjs.io/) for testing your Lambder APIs. It allows you to mock API endpoints with full type safety when using TypeScript API contracts.
4
+
5
+ ## Installation
6
+
7
+ First, install MSW as a dev dependency:
8
+
9
+ ```bash
10
+ npm install msw --save-dev
11
+ # or
12
+ yarn add msw --dev
13
+ ```
14
+
15
+ ## Basic Setup
16
+
17
+ ```typescript
18
+ import { LambderMSW } from 'lambder';
19
+ import { setupServer } from 'msw/node';
20
+
21
+ // Create an MSW instance
22
+ const lambderMSW = new LambderMSW({
23
+ apiPath: '/secure', // Must match your Lambder backend apiPath
24
+ });
25
+
26
+ // Create mock handlers
27
+ const handlers = [
28
+ lambderMSW.mockApi('getUserById', async (payload) => {
29
+ return {
30
+ id: payload.userId,
31
+ name: 'John Doe',
32
+ email: 'john@example.com'
33
+ };
34
+ }),
35
+
36
+ lambderMSW.mockApi('createUser', async (payload) => {
37
+ return {
38
+ id: '123',
39
+ name: payload.name,
40
+ email: payload.email
41
+ };
42
+ })
43
+ ];
44
+
45
+ // Setup MSW server
46
+ const server = setupServer(...handlers);
47
+
48
+ // Start server before tests
49
+ beforeAll(() => server.listen());
50
+ afterEach(() => server.resetHandlers());
51
+ afterAll(() => server.close());
52
+ ```
53
+
54
+ ## Type-Safe Mocking
55
+
56
+ When using TypeScript API contracts, LambderMSW provides full type safety:
57
+
58
+ ```typescript
59
+ // shared/apiContract.ts
60
+ import type { ApiContract } from 'lambder';
61
+
62
+ export type MyApiContract = ApiContract<{
63
+ getUserById: {
64
+ input: { userId: string },
65
+ output: { id: string, name: string, email: string }
66
+ },
67
+ createUser: {
68
+ input: { name: string, email: string },
69
+ output: { id: string, name: string, email: string }
70
+ }
71
+ }>;
72
+
73
+ // test/setup.ts
74
+ import { LambderMSW } from 'lambder';
75
+ import type { MyApiContract } from '../shared/apiContract';
76
+
77
+ const lambderMSW = new LambderMSW<MyApiContract>({
78
+ apiPath: '/secure',
79
+ apiVersion: '1.0.0'
80
+ });
81
+
82
+ // Now mockApi is fully typed! ✨
83
+ const handler = lambderMSW.mockApi('getUserById', async (payload) => {
84
+ // payload is automatically typed as { userId: string }
85
+ // Return value is type-checked against output type
86
+ return {
87
+ id: payload.userId,
88
+ name: 'John Doe',
89
+ email: 'john@example.com'
90
+ };
91
+ });
92
+ ```
93
+
94
+ ## API Reference
95
+
96
+ ### `new LambderMSW(options)`
97
+
98
+ Creates a new LambderMSW instance.
99
+
100
+ **Parameters:**
101
+ - `apiPath` (string): The API endpoint path (must match your Lambder backend)
102
+ - `apiVersion` (string, optional): API version to include in responses
103
+
104
+ ### `mockApi(apiName, handler, options?)`
105
+
106
+ Mock an API endpoint with a custom handler.
107
+
108
+ **Parameters:**
109
+ - `apiName` (string): Name of the API to mock
110
+ - `handler` (function): Async or sync function that returns the mock payload
111
+ - Input: API payload from the request
112
+ - Output: The mocked response payload
113
+ - `options` (object, optional):
114
+ - `versionExpired` (boolean): Simulate version expired error
115
+ - `sessionExpired` (boolean): Simulate session expired error
116
+ - `notAuthorized` (boolean): Simulate not authorized error
117
+ - `message` (any): Custom message to include in response
118
+ - `errorMessage` (any): Error message to include in response
119
+ - `logList` (array): Array of log entries
120
+ - `delay` (number): Artificial delay in milliseconds to simulate network latency
121
+
122
+ **Returns:** MSW RequestHandler
123
+
124
+ **Example:**
125
+ ```typescript
126
+ lambderMSW.mockApi('getCompanyPage', async (payload) => {
127
+ return {
128
+ companyName: payload.companyName,
129
+ description: 'Mock company description',
130
+ employees: 100
131
+ };
132
+ }, {
133
+ delay: 500, // Simulate 500ms network delay
134
+ message: 'Data fetched successfully'
135
+ });
136
+ ```
137
+
138
+ ### `mockSessionExpired(apiName)`
139
+
140
+ Mock an API that returns a session expired error.
141
+
142
+ **Parameters:**
143
+ - `apiName` (string): Name of the API to mock
144
+
145
+ **Example:**
146
+ ```typescript
147
+ lambderMSW.mockSessionExpired('getProtectedData');
148
+ ```
149
+
150
+ ### `mockVersionExpired(apiName)`
151
+
152
+ Mock an API that returns a version expired error.
153
+
154
+ **Parameters:**
155
+ - `apiName` (string): Name of the API to mock
156
+
157
+ **Example:**
158
+ ```typescript
159
+ lambderMSW.mockVersionExpired('getUserProfile');
160
+ ```
161
+
162
+ ### `mockNotAuthorized(apiName)`
163
+
164
+ Mock an API that returns a not authorized error.
165
+
166
+ **Parameters:**
167
+ - `apiName` (string): Name of the API to mock
168
+
169
+ **Example:**
170
+ ```typescript
171
+ lambderMSW.mockNotAuthorized('deleteUser');
172
+ ```
173
+
174
+ ### `mockError(apiName, errorMessage)`
175
+
176
+ Mock an API that returns a custom error message.
177
+
178
+ **Parameters:**
179
+ - `apiName` (string): Name of the API to mock
180
+ - `errorMessage` (string): The error message to return
181
+
182
+ **Example:**
183
+ ```typescript
184
+ lambderMSW.mockError('submitOrder', 'Payment processing failed');
185
+ ```
186
+
187
+ ### `mockWithMessage(apiName, handler, message)`
188
+
189
+ Mock an API with a custom success message.
190
+
191
+ **Parameters:**
192
+ - `apiName` (string): Name of the API to mock
193
+ - `handler` (function): Handler function that returns the mock payload
194
+ - `message` (any): Custom message to include in response
195
+
196
+ **Example:**
197
+ ```typescript
198
+ lambderMSW.mockWithMessage('updateProfile', async (payload) => {
199
+ return { userId: payload.userId, updated: true };
200
+ }, 'Profile updated successfully');
201
+ ```
202
+
203
+ ## Complete Testing Example
204
+
205
+ ```typescript
206
+ // test/api.test.ts
207
+ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
208
+ import { setupServer } from 'msw/node';
209
+ import { LambderMSW } from 'lambder';
210
+ import { LambderCaller } from 'lambder';
211
+ import type { MyApiContract } from '../shared/apiContract';
212
+
213
+ // Setup MSW
214
+ const lambderMSW = new LambderMSW<MyApiContract>({
215
+ apiPath: '/secure',
216
+ apiVersion: '1.0.0'
217
+ });
218
+
219
+ const handlers = [
220
+ lambderMSW.mockApi('getUserById', async (payload) => {
221
+ if (payload.userId === '123') {
222
+ return {
223
+ id: '123',
224
+ name: 'John Doe',
225
+ email: 'john@example.com'
226
+ };
227
+ }
228
+ return null;
229
+ }),
230
+
231
+ lambderMSW.mockApi('createUser', async (payload) => {
232
+ return {
233
+ id: Math.random().toString(),
234
+ name: payload.name,
235
+ email: payload.email
236
+ };
237
+ }, { delay: 100 }), // Simulate 100ms delay
238
+
239
+ lambderMSW.mockSessionExpired('getProtectedData'),
240
+
241
+ lambderMSW.mockError('failingApi', 'Something went wrong')
242
+ ];
243
+
244
+ const server = setupServer(...handlers);
245
+
246
+ beforeAll(() => server.listen());
247
+ afterEach(() => server.resetHandlers());
248
+ afterAll(() => server.close());
249
+
250
+ // Setup LambderCaller
251
+ const caller = new LambderCaller<MyApiContract>({
252
+ apiPath: '/secure',
253
+ isCorsEnabled: false
254
+ });
255
+
256
+ describe('User APIs', () => {
257
+ it('should fetch user by id', async () => {
258
+ const result = await caller.api('getUserById', { userId: '123' });
259
+
260
+ expect(result).toEqual({
261
+ id: '123',
262
+ name: 'John Doe',
263
+ email: 'john@example.com'
264
+ });
265
+ });
266
+
267
+ it('should create a new user', async () => {
268
+ const result = await caller.api('createUser', {
269
+ name: 'Jane Smith',
270
+ email: 'jane@example.com'
271
+ });
272
+
273
+ expect(result?.name).toBe('Jane Smith');
274
+ expect(result?.email).toBe('jane@example.com');
275
+ expect(result?.id).toBeDefined();
276
+ });
277
+
278
+ it('should handle session expired', async () => {
279
+ try {
280
+ await caller.api('getProtectedData', {});
281
+ // Should not reach here
282
+ expect(true).toBe(false);
283
+ } catch (error: any) {
284
+ expect(error.sessionExpired).toBe(true);
285
+ }
286
+ });
287
+
288
+ it('should handle errors', async () => {
289
+ try {
290
+ await caller.api('failingApi', {});
291
+ // Should not reach here
292
+ expect(true).toBe(false);
293
+ } catch (error: any) {
294
+ expect(error.errorMessage).toBe('Something went wrong');
295
+ }
296
+ });
297
+ });
298
+ ```
299
+
300
+ ## Browser Testing
301
+
302
+ LambderMSW also works in browser environments with MSW's browser integration:
303
+
304
+ ```typescript
305
+ // test/browser-setup.ts
306
+ import { setupWorker } from 'msw/browser';
307
+ import { LambderMSW } from 'lambder';
308
+ import type { MyApiContract } from '../shared/apiContract';
309
+
310
+ const lambderMSW = new LambderMSW<MyApiContract>({
311
+ apiPath: '/secure'
312
+ });
313
+
314
+ const handlers = [
315
+ lambderMSW.mockApi('getUserById', async (payload) => {
316
+ return {
317
+ id: payload.userId,
318
+ name: 'John Doe',
319
+ email: 'john@example.com'
320
+ };
321
+ })
322
+ ];
323
+
324
+ const worker = setupWorker(...handlers);
325
+
326
+ // Start the worker
327
+ worker.start();
328
+ ```
329
+
330
+ ## Advanced Usage
331
+
332
+ ### Dynamic Mock Responses
333
+
334
+ ```typescript
335
+ lambderMSW.mockApi('searchUsers', async (payload) => {
336
+ const { query, limit } = payload;
337
+
338
+ // Return different responses based on input
339
+ if (query === 'admin') {
340
+ return [{
341
+ id: '1',
342
+ name: 'Admin User',
343
+ role: 'admin'
344
+ }];
345
+ }
346
+
347
+ return Array.from({ length: limit }, (_, i) => ({
348
+ id: `${i}`,
349
+ name: `User ${i}`,
350
+ role: 'user'
351
+ }));
352
+ });
353
+ ```
354
+
355
+ ### Simulating Network Conditions
356
+
357
+ ```typescript
358
+ // Slow network
359
+ lambderMSW.mockApi('slowApi', async () => {
360
+ return { data: 'slow response' };
361
+ }, { delay: 3000 }); // 3 second delay
362
+
363
+ // Intermittent failures
364
+ let callCount = 0;
365
+ lambderMSW.mockApi('flakeyApi', async () => {
366
+ callCount++;
367
+ if (callCount % 3 === 0) {
368
+ throw new Error('Random failure');
369
+ }
370
+ return { success: true };
371
+ });
372
+ ```
373
+
374
+ ### Override Handlers Per Test
375
+
376
+ ```typescript
377
+ it('should handle specific user', async () => {
378
+ // Override the default handler for this test
379
+ server.use(
380
+ lambderMSW.mockApi('getUserById', async (payload) => {
381
+ return {
382
+ id: payload.userId,
383
+ name: 'Special User',
384
+ email: 'special@example.com'
385
+ };
386
+ })
387
+ );
388
+
389
+ const result = await caller.api('getUserById', { userId: '999' });
390
+ expect(result?.name).toBe('Special User');
391
+ });
392
+ ```
393
+
394
+ ## Benefits
395
+
396
+ ✅ **Type Safety** - Full TypeScript support with API contracts
397
+ ✅ **Simple API** - Intuitive methods matching Lambder's API structure
398
+ ✅ **Flexible** - Mock success, errors, delays, and custom responses
399
+ ✅ **Isolated** - Tests run without real backend dependencies
400
+ ✅ **Fast** - No network calls, instant test execution
401
+ ✅ **Realistic** - Simulate real-world scenarios (delays, errors, etc.)
402
+
403
+ ## Troubleshooting
404
+
405
+ ### MSW Not Found Error
406
+
407
+ If you see "MSW (Mock Service Worker) is required", make sure MSW is installed:
408
+
409
+ ```bash
410
+ npm install msw --save-dev
411
+ ```
412
+
413
+ ### Handler Not Matching
414
+
415
+ LambderMSW matches handlers based on the `apiName` in the request body. Make sure:
416
+ 1. Your `apiPath` matches between backend, frontend, and mocks
417
+ 2. The API name string matches exactly
418
+ 3. Handlers are registered before making requests
419
+
420
+ ### Console Warnings
421
+
422
+ LambderMSW logs matching information to console for debugging. To see these logs:
423
+ - Check browser console for "LambderMSW called for:" messages
424
+ - Verify "Matched!" appears when handler should execute
425
+
426
+ ## See Also
427
+
428
+ - [MSW Documentation](https://mswjs.io/)
429
+ - [Type-Safe Quick Start](./TYPE_SAFE_QUICK_START.md)
430
+ - [Lambder Main Documentation](../Readme.md)
@@ -156,6 +156,38 @@ lambder.addApi('anyApi', async (ctx, resolver) => {
156
156
 
157
157
  See [simplified-typed-api-example.ts](../examples/simplified-typed-api-example.ts) for a complete working example.
158
158
 
159
+ ## Testing Your Type-Safe APIs
160
+
161
+ LambderMSW provides full type safety for testing your APIs with MSW (Mock Service Worker):
162
+
163
+ ```typescript
164
+ import { LambderMSW } from 'lambder';
165
+ import { setupServer } from 'msw/node';
166
+ import type { MyApiContract } from './shared/apiContract';
167
+
168
+ // Create type-safe MSW instance
169
+ const lambderMSW = new LambderMSW<MyApiContract>({
170
+ apiPath: '/api'
171
+ });
172
+
173
+ // Mock with full type safety! ✨
174
+ const handlers = [
175
+ lambderMSW.mockApi('getUserById', async (payload) => {
176
+ // payload is typed as { userId: string }
177
+ // Return value is type-checked against output
178
+ return {
179
+ id: payload.userId,
180
+ name: 'John Doe',
181
+ email: 'john@example.com'
182
+ };
183
+ })
184
+ ];
185
+
186
+ const server = setupServer(...handlers);
187
+ ```
188
+
189
+ 📖 See [LAMBDER_MSW.md](./LAMBDER_MSW.md) for complete testing documentation.
190
+
159
191
  ## Key Points
160
192
 
161
193
  - **Contract is just a TypeScript type** - No runtime code!
@@ -165,3 +197,4 @@ See [simplified-typed-api-example.ts](../examples/simplified-typed-api-example.t
165
197
  - **Opt-in** - Use types when you want them
166
198
  - **Simple** - Just pass type to constructor
167
199
  - **Autocomplete** - IDE shows available APIs as you type
200
+ - **Testing support** - LambderMSW provides type-safe mocking
@@ -0,0 +1,277 @@
1
+ /**
2
+ * LambderMSW Testing Example
3
+ *
4
+ * This example shows how to use LambderMSW with MSW (Mock Service Worker)
5
+ * to test your Lambder APIs with full type safety.
6
+ *
7
+ * To run this example:
8
+ * 1. Install MSW: npm install msw --save-dev
9
+ * 2. Install a test runner: npm install vitest --save-dev
10
+ * 3. Create this file in your test directory
11
+ *
12
+ * NOTE: This is an example file. Type errors are expected since dependencies
13
+ * may not be installed in the examples directory. Copy this to your project's
14
+ * test directory to use it.
15
+ */
16
+
17
+ // @ts-nocheck - Example file, types may not be available
18
+ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
19
+ import { setupServer } from 'msw/node';
20
+ import { LambderMSW, LambderCaller, type ApiContract } from '../src/index.ts';
21
+
22
+ // Define your API contract
23
+ type TestApiContract = ApiContract<{
24
+ getUserById: {
25
+ input: { userId: string };
26
+ output: { id: string; name: string; email: string } | null;
27
+ };
28
+ createUser: {
29
+ input: { name: string; email: string };
30
+ output: { id: string; name: string; email: string };
31
+ };
32
+ getUsers: {
33
+ input: { limit?: number };
34
+ output: Array<{ id: string; name: string; email: string }>;
35
+ };
36
+ deleteUser: {
37
+ input: { userId: string };
38
+ output: { success: boolean };
39
+ };
40
+ }>;
41
+
42
+ // Setup LambderMSW with type safety
43
+ const lambderMSW = new LambderMSW<TestApiContract>({
44
+ apiPath: '/secure',
45
+ apiVersion: '1.0.0',
46
+ });
47
+
48
+ // Create mock handlers
49
+ const handlers = [
50
+ // Mock getUserById - returns user or null
51
+ lambderMSW.mockApi('getUserById', async (payload) => {
52
+ const users: Record<string, { id: string; name: string; email: string }> = {
53
+ '1': { id: '1', name: 'John Doe', email: 'john@example.com' },
54
+ '2': { id: '2', name: 'Jane Smith', email: 'jane@example.com' },
55
+ };
56
+
57
+ return users[payload?.userId || ''] || null;
58
+ }),
59
+
60
+ // Mock createUser - simulates creating a user
61
+ lambderMSW.mockApi('createUser', async (payload) => {
62
+ return {
63
+ id: Math.random().toString(36).substring(7),
64
+ name: payload?.name || '',
65
+ email: payload?.email || '',
66
+ };
67
+ }, {
68
+ delay: 100, // Simulate 100ms network delay
69
+ message: 'User created successfully'
70
+ }),
71
+
72
+ // Mock getUsers - returns a list of users
73
+ lambderMSW.mockApi('getUsers', async (payload) => {
74
+ const allUsers = [
75
+ { id: '1', name: 'John Doe', email: 'john@example.com' },
76
+ { id: '2', name: 'Jane Smith', email: 'jane@example.com' },
77
+ { id: '3', name: 'Bob Johnson', email: 'bob@example.com' },
78
+ ];
79
+
80
+ const limit = payload?.limit || 10;
81
+ return allUsers.slice(0, limit);
82
+ }),
83
+
84
+ // Mock deleteUser - simulate authorization error
85
+ lambderMSW.mockNotAuthorized('deleteUser'),
86
+ ];
87
+
88
+ // Setup MSW server
89
+ const server = setupServer(...handlers);
90
+
91
+ // Setup LambderCaller
92
+ const caller = new LambderCaller<TestApiContract>({
93
+ apiPath: '/secure',
94
+ isCorsEnabled: false,
95
+ });
96
+
97
+ // Test suite
98
+ describe('LambderMSW Testing Example', () => {
99
+ beforeAll(() => {
100
+ server.listen({ onUnhandledRequest: 'error' });
101
+ });
102
+
103
+ afterEach(() => {
104
+ server.resetHandlers();
105
+ });
106
+
107
+ afterAll(() => {
108
+ server.close();
109
+ });
110
+
111
+ it('should fetch user by id', async () => {
112
+ const user = await caller.api('getUserById', { userId: '1' });
113
+
114
+ expect(user).toEqual({
115
+ id: '1',
116
+ name: 'John Doe',
117
+ email: 'john@example.com',
118
+ });
119
+ });
120
+
121
+ it('should return null for non-existent user', async () => {
122
+ const user = await caller.api('getUserById', { userId: '999' });
123
+
124
+ expect(user).toBeNull();
125
+ });
126
+
127
+ it('should create a new user with delay', async () => {
128
+ const startTime = Date.now();
129
+
130
+ const newUser = await caller.api('createUser', {
131
+ name: 'Alice Wonder',
132
+ email: 'alice@example.com',
133
+ });
134
+
135
+ const endTime = Date.now();
136
+
137
+ expect(newUser?.name).toBe('Alice Wonder');
138
+ expect(newUser?.email).toBe('alice@example.com');
139
+ expect(newUser?.id).toBeDefined();
140
+
141
+ // Check that delay was applied (at least 100ms)
142
+ expect(endTime - startTime).toBeGreaterThanOrEqual(100);
143
+ });
144
+
145
+ it('should fetch list of users', async () => {
146
+ const users = await caller.api('getUsers', { limit: 2 });
147
+
148
+ expect(users).toHaveLength(2);
149
+ expect(users?.[0]?.name).toBe('John Doe');
150
+ expect(users?.[1]?.name).toBe('Jane Smith');
151
+ });
152
+
153
+ it('should handle authorization error', async () => {
154
+ let errorCaught = false;
155
+
156
+ try {
157
+ await caller.api('deleteUser', { userId: '1' });
158
+ } catch (error: any) {
159
+ errorCaught = true;
160
+ expect(error.notAuthorized).toBe(true);
161
+ }
162
+
163
+ expect(errorCaught).toBe(true);
164
+ });
165
+
166
+ it('should override handler for specific test', async () => {
167
+ // Override the getUserById handler for this test only
168
+ server.use(
169
+ lambderMSW.mockApi('getUserById', async (payload) => {
170
+ return {
171
+ id: payload?.userId || '',
172
+ name: 'Override User',
173
+ email: 'override@example.com',
174
+ };
175
+ })
176
+ );
177
+
178
+ const user = await caller.api('getUserById', { userId: '999' });
179
+
180
+ expect(user?.name).toBe('Override User');
181
+ });
182
+
183
+ it('should simulate session expired', async () => {
184
+ // Add a handler that simulates session expiration
185
+ server.use(
186
+ lambderMSW.mockSessionExpired('getUserById')
187
+ );
188
+
189
+ let errorCaught = false;
190
+
191
+ try {
192
+ await caller.api('getUserById', { userId: '1' });
193
+ } catch (error: any) {
194
+ errorCaught = true;
195
+ expect(error.sessionExpired).toBe(true);
196
+ }
197
+
198
+ expect(errorCaught).toBe(true);
199
+ });
200
+
201
+ it('should simulate custom error', async () => {
202
+ server.use(
203
+ lambderMSW.mockError('createUser', 'Email already exists')
204
+ );
205
+
206
+ let errorCaught = false;
207
+
208
+ try {
209
+ await caller.api('createUser', {
210
+ name: 'Duplicate',
211
+ email: 'john@example.com',
212
+ });
213
+ } catch (error: any) {
214
+ errorCaught = true;
215
+ expect(error.errorMessage).toBe('Email already exists');
216
+ }
217
+
218
+ expect(errorCaught).toBe(true);
219
+ });
220
+ });
221
+
222
+ // Example: Testing with dynamic responses
223
+ describe('Dynamic Response Testing', () => {
224
+ beforeAll(() => server.listen());
225
+ afterEach(() => server.resetHandlers());
226
+ afterAll(() => server.close());
227
+
228
+ it('should handle different query parameters', async () => {
229
+ server.use(
230
+ lambderMSW.mockApi('getUsers', async (payload) => {
231
+ const limit = payload?.limit || 10;
232
+
233
+ // Generate mock users based on limit
234
+ return Array.from({ length: limit }, (_, i) => ({
235
+ id: `${i + 1}`,
236
+ name: `User ${i + 1}`,
237
+ email: `user${i + 1}@example.com`,
238
+ }));
239
+ })
240
+ );
241
+
242
+ const users3 = await caller.api('getUsers', { limit: 3 });
243
+ expect(users3).toHaveLength(3);
244
+
245
+ const users5 = await caller.api('getUsers', { limit: 5 });
246
+ expect(users5).toHaveLength(5);
247
+ });
248
+ });
249
+
250
+ // Example: Testing error conditions
251
+ describe('Error Handling', () => {
252
+ beforeAll(() => server.listen());
253
+ afterEach(() => server.resetHandlers());
254
+ afterAll(() => server.close());
255
+
256
+ it('should handle handler throwing error', async () => {
257
+ server.use(
258
+ lambderMSW.mockApi('getUserById', async () => {
259
+ throw new Error('Database connection failed');
260
+ })
261
+ );
262
+
263
+ let errorCaught = false;
264
+
265
+ try {
266
+ await caller.api('getUserById', { userId: '1' });
267
+ } catch (error: any) {
268
+ errorCaught = true;
269
+ expect(error.errorMessage).toBe('Database connection failed');
270
+ }
271
+
272
+ expect(errorCaught).toBe(true);
273
+ });
274
+ });
275
+
276
+ console.log('✅ LambderMSW example tests configured!');
277
+ console.log('📖 See docs/LAMBDER_MSW.md for more information');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "1.0.132",
3
+ "version": "1.0.134",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/LambderMSW.ts CHANGED
@@ -50,52 +50,68 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
50
50
  * @param apiName - The name of the API to mock
51
51
  * @param handler - Function that returns the mock payload
52
52
  * @param options - Additional response options (session expired, version expired, etc.)
53
+ *
54
+ * Note: TypeScript will check that the return type is assignable to the output type,
55
+ * but due to structural typing, extra properties are allowed. Enable strict checks
56
+ * in your tsconfig.json with "noUncheckedIndexedAccess" and "exactOptionalPropertyTypes"
57
+ * for better type safety.
53
58
  */
54
- mockApi<
55
- TApiName extends keyof TContract & string,
56
- TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any,
57
- TInput = TApiName extends keyof TContract ? TContract[TApiName]['input'] : any
58
- >(
59
+ mockApi<TApiName extends keyof TContract>(
59
60
  apiName: TApiName,
60
- handler: (payload?: TInput) => Promise<TOutput> | TOutput,
61
+ handler: (payload?: TContract[TApiName]['input']) => Promise<TContract[TApiName]['output']> | TContract[TApiName]['output'],
61
62
  options?: MockApiOptions
62
63
  ): RequestHandler {
63
64
  return this.http.post(this.apiPath, async ({ request }: any) => {
65
+ let body: any;
66
+
64
67
  try {
65
- const body: any = await request.json();
66
-
67
- console.log("LambderMSW called for:", body?.apiName, "matching against:", apiName);
68
-
69
- // Check if this is the API we're mocking
70
- if (body?.apiName === apiName) {
71
- // Add artificial delay if specified
72
- if (options?.delay) {
73
- await new Promise(resolve => setTimeout(resolve, options.delay));
74
- }
75
-
76
- // Call the handler with the payload from the request
77
- const payload = await handler(body?.payload as TInput);
78
-
79
- console.log("Matched! Returning payload for:", apiName);
80
-
81
- const response: MockApiResponse<TOutput> = {
82
- apiVersion: this.apiVersion,
83
- payload,
84
- ...(options?.versionExpired ? { versionExpired: options.versionExpired } : {}),
85
- ...(options?.sessionExpired ? { sessionExpired: options.sessionExpired } : {}),
86
- ...(options?.notAuthorized ? { notAuthorized: options.notAuthorized } : {}),
87
- ...(options?.message ? { message: options.message } : {}),
88
- ...(options?.errorMessage ? { errorMessage: options.errorMessage } : {}),
89
- ...(options?.logList?.length ? { logList: options.logList } : {}),
90
- };
91
-
92
- return this.HttpResponse.json(response);
93
- }
94
-
68
+ body = await request.json();
69
+ } catch (parseErr) {
70
+ // If JSON parsing fails, return undefined to let other handlers try
71
+ console.warn("LambderMSW: Failed to parse request body as JSON");
72
+ return;
73
+ }
74
+
75
+ // Check if body is valid and has apiName
76
+ if (!body || typeof body.apiName !== 'string') {
77
+ // Invalid request format, let other handlers try
78
+ return;
79
+ }
80
+
81
+ console.log("LambderMSW called for:", body.apiName, "matching against:", apiName);
82
+
83
+ // Check if this is the API we're mocking
84
+ if (body.apiName !== apiName) {
95
85
  // If this handler doesn't match, return undefined to let MSW try other handlers
96
86
  return;
87
+ }
88
+
89
+ try {
90
+ // Add artificial delay if specified
91
+ if (options?.delay) {
92
+ await new Promise(resolve => setTimeout(resolve, options.delay));
93
+ }
94
+
95
+ // Call the handler with the payload from the request
96
+ const payload = await handler(body.payload as TContract[TApiName]['input']);
97
+
98
+ console.log("Matched! Returning payload for:", apiName);
99
+
100
+ const response: MockApiResponse<TContract[TApiName]['output']> = {
101
+ apiVersion: this.apiVersion,
102
+ payload,
103
+ ...(options?.versionExpired ? { versionExpired: options.versionExpired } : {}),
104
+ ...(options?.sessionExpired ? { sessionExpired: options.sessionExpired } : {}),
105
+ ...(options?.notAuthorized ? { notAuthorized: options.notAuthorized } : {}),
106
+ ...(options?.message ? { message: options.message } : {}),
107
+ ...(options?.errorMessage ? { errorMessage: options.errorMessage } : {}),
108
+ ...(options?.logList?.length ? { logList: options.logList } : {}),
109
+ };
110
+
111
+ return this.HttpResponse.json(response);
97
112
  } catch (err: any) {
98
- console.error("Error in LambderMSW:", err);
113
+ // Only handle errors that occur during handler execution for matched APIs
114
+ console.error("Error in LambderMSW handler for", apiName, ":", err);
99
115
 
100
116
  const errorResponse: MockApiResponse<null> = {
101
117
  apiVersion: this.apiVersion,
@@ -111,7 +127,7 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
111
127
  /**
112
128
  * Mock an API endpoint that returns a session expired error
113
129
  */
114
- mockSessionExpired<TApiName extends keyof TContract & string>(
130
+ mockSessionExpired<TApiName extends keyof TContract>(
115
131
  apiName: TApiName
116
132
  ): RequestHandler {
117
133
  return this.mockApi(
@@ -124,7 +140,7 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
124
140
  /**
125
141
  * Mock an API endpoint that returns a version expired error
126
142
  */
127
- mockVersionExpired<TApiName extends keyof TContract & string>(
143
+ mockVersionExpired<TApiName extends keyof TContract>(
128
144
  apiName: TApiName
129
145
  ): RequestHandler {
130
146
  return this.mockApi(
@@ -137,7 +153,7 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
137
153
  /**
138
154
  * Mock an API endpoint that returns a not authorized error
139
155
  */
140
- mockNotAuthorized<TApiName extends keyof TContract & string>(
156
+ mockNotAuthorized<TApiName extends keyof TContract>(
141
157
  apiName: TApiName
142
158
  ): RequestHandler {
143
159
  return this.mockApi(
@@ -150,7 +166,7 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
150
166
  /**
151
167
  * Mock an API endpoint that returns an error message
152
168
  */
153
- mockError<TApiName extends keyof TContract & string>(
169
+ mockError<TApiName extends keyof TContract>(
154
170
  apiName: TApiName,
155
171
  errorMessage: string
156
172
  ): RequestHandler {
@@ -164,13 +180,9 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
164
180
  /**
165
181
  * Mock an API endpoint with a custom message
166
182
  */
167
- mockWithMessage<
168
- TApiName extends keyof TContract & string,
169
- TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any,
170
- TInput = TApiName extends keyof TContract ? TContract[TApiName]['input'] : any
171
- >(
183
+ mockWithMessage<TApiName extends keyof TContract>(
172
184
  apiName: TApiName,
173
- handler: (payload?: TInput) => Promise<TOutput> | TOutput,
185
+ handler: (payload?: TContract[TApiName]['input']) => Promise<TContract[TApiName]['output']> | TContract[TApiName]['output'],
174
186
  message: any
175
187
  ): RequestHandler {
176
188
  return this.mockApi(apiName, handler, { message });
@@ -0,0 +1,111 @@
1
+ // Test file to verify LambderMSW type enforcement
2
+ import LambderMSW from './src/LambderMSW';
3
+ import type { ApiContract } from './src/LambderApiContract';
4
+
5
+ // Define a test API contract
6
+ type TestContract = ApiContract<{
7
+ 'public.getInitialPageData': {
8
+ input: void;
9
+ output: {
10
+ userLocationData: {
11
+ country: string;
12
+ region: string;
13
+ regionCode: string;
14
+ city: string;
15
+ zipCode: string;
16
+ lat: number;
17
+ lng: number;
18
+ };
19
+ sessionUser: {
20
+ id: string;
21
+ username: string;
22
+ email: string;
23
+ name: string;
24
+ bio: string;
25
+ // NOTE: 's' property should NOT be here
26
+ };
27
+ };
28
+ };
29
+ }>;
30
+
31
+ const apiMocker = new LambderMSW<TestContract>({
32
+ apiPath: '/secure',
33
+ });
34
+
35
+ // TEST 1: Extra properties - TypeScript allows this due to structural typing
36
+ const handler1 = apiMocker.mockApi(
37
+ 'public.getInitialPageData',
38
+ async () => {
39
+ return {
40
+ userLocationData: {
41
+ country: 'United States',
42
+ region: 'California',
43
+ regionCode: 'CA',
44
+ city: 'San Francisco',
45
+ zipCode: '94102',
46
+ lat: 37.7749,
47
+ lng: -122.4194,
48
+ },
49
+ sessionUser: {
50
+ id: 'mock-user-123',
51
+ username: 'mockuser',
52
+ email: 'mock@example.com',
53
+ name: 'Mock User',
54
+ bio: 'This is a mock user for development',
55
+ s: 43 // ⚠️ Extra property - TypeScript structural typing allows this
56
+ },
57
+ };
58
+ }
59
+ );
60
+
61
+ // TEST 2: Missing required property - THIS WILL CAUSE AN ERROR!
62
+ const handler2 = apiMocker.mockApi(
63
+ 'public.getInitialPageData',
64
+ async () => {
65
+ return {
66
+ userLocationData: {
67
+ country: 'United States',
68
+ region: 'California',
69
+ regionCode: 'CA',
70
+ city: 'San Francisco',
71
+ zipCode: '94102',
72
+ lat: 37.7749,
73
+ lng: -122.4194,
74
+ },
75
+ sessionUser: {
76
+ id: 'mock-user-123',
77
+ username: 'mockuser',
78
+ email: 'mock@example.com',
79
+ name: 'Mock User',
80
+ // bio: 'This is a mock user for development', // ❌ Missing required property - ERROR!
81
+ },
82
+ };
83
+ }
84
+ );
85
+
86
+ // TEST 3: Wrong type - THIS WILL CAUSE AN ERROR!
87
+ const handler3 = apiMocker.mockApi(
88
+ 'public.getInitialPageData',
89
+ async () => {
90
+ return {
91
+ userLocationData: {
92
+ country: 'United States',
93
+ region: 'California',
94
+ regionCode: 'CA',
95
+ city: 'San Francisco',
96
+ zipCode: '94102',
97
+ lat: 37.7749,
98
+ lng: -122.4194,
99
+ },
100
+ sessionUser: {
101
+ id: 123, // ❌ Wrong type - should be string - ERROR!
102
+ username: 'mockuser',
103
+ email: 'mock@example.com',
104
+ name: 'Mock User',
105
+ bio: 'This is a mock user for development',
106
+ },
107
+ };
108
+ }
109
+ );
110
+
111
+ console.log('Check for TypeScript errors above!');