lambder 1.0.131 → 1.0.133

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/src/LambderMSW.ts CHANGED
@@ -1,295 +1,186 @@
1
- import type { ApiContractShape } from './LambderApiContract.js';
2
-
3
- /**
4
- * Creates a mock handler for a Lambder API endpoint
5
- *
6
- * @example
7
- * ```typescript
8
- * mockLambderApi('public.getInitialPageData', () => ({
9
- * userLocationData: { ... },
10
- * sessionUser: null
11
- * }))
12
- * ```
13
- *
14
- * @example with dynamic response based on input
15
- * ```typescript
16
- * mockLambderApi('user.updateProfile', (input) => ({
17
- * success: true,
18
- * userId: input.userId
19
- * }))
20
- * ```
21
- */
22
- export function mockLambderApi<
23
- TContract extends ApiContractShape,
24
- TApiName extends keyof TContract & string
25
- >(
26
- apiName: TApiName,
27
- responseFactory: (
28
- input: TContract[TApiName]['input']
29
- ) => TContract[TApiName]['output'] | Promise<TContract[TApiName]['output']>,
30
- options?: {
31
- /** Custom API path, defaults to '/api' */
32
- apiPath?: string
33
- /** Delay in ms before responding (for testing loading states) */
34
- delay?: number
35
- /** Custom apiVersion to include in response */
36
- apiVersion?: string | null
37
- }
38
- ) {
39
- const apiPath = options?.apiPath ?? '/api';
40
- const delay = options?.delay ?? 0;
41
- const apiVersion = options?.apiVersion ?? null;
42
-
43
- // @ts-ignore - MSW is an optional peer dependency
44
- const { http, HttpResponse } = require('msw');
45
-
46
- return http.post(apiPath, async ({ request }: any) => {
47
- const body = (await request.json()) as {
48
- apiName: string;
49
- payload?: TContract[TApiName]['input'];
50
- version?: string;
51
- token?: string;
52
- siteHost?: string;
53
- };
54
-
55
- // Only handle this specific API
56
- if (body.apiName !== apiName) {
57
- return;
58
- }
59
-
60
- console.log(`[MSW Lambder] Mocking ${apiName}`, body.payload);
61
-
62
- // Apply delay if specified
63
- if (delay > 0) {
64
- await new Promise((resolve) => setTimeout(resolve, delay));
1
+ import type { ApiContractShape } from './LambderApiContract';
2
+ import type { LambderApiResponse } from './LambderResponseBuilder';
3
+
4
+ // MSW types - these will be resolved at runtime when msw is installed
5
+ type RequestHandler = any;
6
+
7
+ type MockApiOptions = {
8
+ versionExpired?: boolean;
9
+ sessionExpired?: boolean;
10
+ notAuthorized?: boolean;
11
+ message?: any;
12
+ errorMessage?: any;
13
+ logList?: any[];
14
+ delay?: number; // Add artificial delay to simulate network latency
15
+ };
16
+
17
+ // Extended response type that includes apiVersion
18
+ type MockApiResponse<T> = {
19
+ apiVersion?: string;
20
+ } & LambderApiResponse<T>;
21
+
22
+ export default class LambderMSW<TContract extends ApiContractShape = any> {
23
+ private apiPath: string;
24
+ private apiVersion?: string;
25
+ private http: any;
26
+ private HttpResponse: any;
27
+
28
+ constructor({
29
+ apiPath,
30
+ apiVersion,
31
+ }: {
32
+ apiPath: string;
33
+ apiVersion?: string;
34
+ }) {
35
+ this.apiPath = apiPath;
36
+ this.apiVersion = apiVersion;
37
+
38
+ // Dynamically import MSW - it needs to be installed by the user
39
+ try {
40
+ const msw = require('msw');
41
+ this.http = msw.http;
42
+ this.HttpResponse = msw.HttpResponse;
43
+ } catch (err) {
44
+ throw new Error('MSW (Mock Service Worker) is required. Install it with: npm install msw --save-dev');
65
45
  }
66
-
67
- // Generate the response
68
- const payload = await responseFactory(body.payload as TContract[TApiName]['input']);
69
-
70
- // Return in Lambder API response format
71
- return HttpResponse.json({
72
- ...(apiVersion !== null ? { apiVersion } : {}),
73
- payload,
74
- });
75
- });
76
- }
77
-
78
- /**
79
- * Creates a mock handler that returns an error for a Lambder API endpoint
80
- *
81
- * @example
82
- * ```typescript
83
- * mockLambderApiError('user.deleteAccount', 'Account deletion failed')
84
- * ```
85
- */
86
- export function mockLambderApiError<
87
- TContract extends ApiContractShape,
88
- TApiName extends keyof TContract & string
89
- >(
90
- apiName: TApiName,
91
- errorMessage: string,
92
- options?: {
93
- /** Custom API path, defaults to '/api' */
94
- apiPath?: string;
95
- /** Delay in ms before responding */
96
- delay?: number;
97
- /** Custom apiVersion to include in response */
98
- apiVersion?: string | null;
99
46
  }
100
- ) {
101
- const apiPath = options?.apiPath ?? '/api';
102
- const delay = options?.delay ?? 0;
103
- const apiVersion = options?.apiVersion ?? null;
104
-
105
- // @ts-ignore - MSW is an optional peer dependency
106
- const { http, HttpResponse } = require('msw');
107
-
108
- return http.post(apiPath, async ({ request }: any) => {
109
- const body = (await request.json()) as {
110
- apiName: string;
111
- payload?: any;
112
- };
113
-
114
- if (body.apiName !== apiName) {
115
- return;
116
- }
117
-
118
- console.log(`[MSW Lambder] Mocking error for ${apiName}:`, errorMessage);
119
47
 
120
- if (delay > 0) {
121
- await new Promise((resolve) => setTimeout(resolve, delay));
122
- }
123
-
124
- return HttpResponse.json({
125
- ...(apiVersion !== null ? { apiVersion } : {}),
126
- payload: null,
127
- errorMessage,
48
+ /**
49
+ * Mock an API endpoint with MSW
50
+ * @param apiName - The name of the API to mock
51
+ * @param handler - Function that returns the mock payload
52
+ * @param options - Additional response options (session expired, version expired, etc.)
53
+ */
54
+ mockApi<TApiName extends keyof TContract & string>(
55
+ apiName: TApiName,
56
+ handler: (payload?: TContract[TApiName]['input']) => Promise<TContract[TApiName]['output']> | TContract[TApiName]['output'],
57
+ options?: MockApiOptions
58
+ ): RequestHandler {
59
+ return this.http.post(this.apiPath, async ({ request }: any) => {
60
+ let body: any;
61
+
62
+ try {
63
+ body = await request.json();
64
+ } catch (parseErr) {
65
+ // If JSON parsing fails, return undefined to let other handlers try
66
+ console.warn("LambderMSW: Failed to parse request body as JSON");
67
+ return;
68
+ }
69
+
70
+ // Check if body is valid and has apiName
71
+ if (!body || typeof body.apiName !== 'string') {
72
+ // Invalid request format, let other handlers try
73
+ return;
74
+ }
75
+
76
+ console.log("LambderMSW called for:", body.apiName, "matching against:", apiName);
77
+
78
+ // Check if this is the API we're mocking
79
+ if (body.apiName !== apiName) {
80
+ // If this handler doesn't match, return undefined to let MSW try other handlers
81
+ return;
82
+ }
83
+
84
+ try {
85
+ // Add artificial delay if specified
86
+ if (options?.delay) {
87
+ await new Promise(resolve => setTimeout(resolve, options.delay));
88
+ }
89
+
90
+ // Call the handler with the payload from the request
91
+ const payload = await handler(body.payload as TContract[TApiName]['input']);
92
+
93
+ console.log("Matched! Returning payload for:", apiName);
94
+
95
+ const response: MockApiResponse<TContract[TApiName]['output']> = {
96
+ apiVersion: this.apiVersion,
97
+ payload,
98
+ ...(options?.versionExpired ? { versionExpired: options.versionExpired } : {}),
99
+ ...(options?.sessionExpired ? { sessionExpired: options.sessionExpired } : {}),
100
+ ...(options?.notAuthorized ? { notAuthorized: options.notAuthorized } : {}),
101
+ ...(options?.message ? { message: options.message } : {}),
102
+ ...(options?.errorMessage ? { errorMessage: options.errorMessage } : {}),
103
+ ...(options?.logList?.length ? { logList: options.logList } : {}),
104
+ };
105
+
106
+ return this.HttpResponse.json(response);
107
+ } catch (err: any) {
108
+ // Only handle errors that occur during handler execution for matched APIs
109
+ console.error("Error in LambderMSW handler for", apiName, ":", err);
110
+
111
+ const errorResponse: MockApiResponse<null> = {
112
+ apiVersion: this.apiVersion,
113
+ payload: null,
114
+ errorMessage: err.message || "Unknown error",
115
+ };
116
+
117
+ return this.HttpResponse.json(errorResponse, { status: 500 });
118
+ }
128
119
  });
129
- });
130
- }
131
-
132
- /**
133
- * Creates a mock handler that simulates a session expired error
134
- *
135
- * @example
136
- * ```typescript
137
- * mockLambderSessionExpired('user.updateProfile')
138
- * ```
139
- */
140
- export function mockLambderSessionExpired<
141
- TContract extends ApiContractShape,
142
- TApiName extends keyof TContract & string
143
- >(
144
- apiName: TApiName,
145
- options?: {
146
- /** Custom API path, defaults to '/api' */
147
- apiPath?: string;
148
- /** Custom apiVersion to include in response */
149
- apiVersion?: string | null;
150
120
  }
151
- ) {
152
- const apiPath = options?.apiPath ?? '/api';
153
- const apiVersion = options?.apiVersion ?? null;
154
-
155
- // @ts-ignore - MSW is an optional peer dependency
156
- const { http, HttpResponse } = require('msw');
157
121
 
158
- return http.post(apiPath, async ({ request }: any) => {
159
- const body = (await request.json()) as {
160
- apiName: string;
161
- payload?: any;
162
- };
163
-
164
- if (body.apiName !== apiName) {
165
- return;
166
- }
167
-
168
- console.log(`[MSW Lambder] Mocking session expired for ${apiName}`);
169
-
170
- return HttpResponse.json({
171
- ...(apiVersion !== null ? { apiVersion } : {}),
172
- payload: null,
173
- sessionExpired: true,
174
- errorMessage: 'Session expired',
175
- });
176
- });
177
- }
178
-
179
- /**
180
- * Creates a mock handler that simulates a "not authorized" error
181
- *
182
- * @example
183
- * ```typescript
184
- * mockLambderNotAuthorized('admin.deleteUser')
185
- * ```
186
- */
187
- export function mockLambderNotAuthorized<
188
- TContract extends ApiContractShape,
189
- TApiName extends keyof TContract & string
190
- >(
191
- apiName: TApiName,
192
- options?: {
193
- /** Custom API path, defaults to '/api' */
194
- apiPath?: string;
195
- /** Custom apiVersion to include in response */
196
- apiVersion?: string | null;
122
+ /**
123
+ * Mock an API endpoint that returns a session expired error
124
+ */
125
+ mockSessionExpired<TApiName extends keyof TContract & string>(
126
+ apiName: TApiName
127
+ ): RequestHandler {
128
+ return this.mockApi(
129
+ apiName,
130
+ async () => null as any,
131
+ { sessionExpired: true }
132
+ );
197
133
  }
198
- ) {
199
- const apiPath = options?.apiPath ?? '/api';
200
- const apiVersion = options?.apiVersion ?? null;
201
-
202
- // @ts-ignore - MSW is an optional peer dependency
203
- const { http, HttpResponse } = require('msw');
204
-
205
- return http.post(apiPath, async ({ request }: any) => {
206
- const body = (await request.json()) as {
207
- apiName: string;
208
- payload?: any;
209
- };
210
-
211
- if (body.apiName !== apiName) {
212
- return;
213
- }
214
-
215
- console.log(`[MSW Lambder] Mocking not authorized for ${apiName}`);
216
134
 
217
- return HttpResponse.json({
218
- ...(apiVersion !== null ? { apiVersion } : {}),
219
- payload: null,
220
- notAuthorized: true,
221
- errorMessage: 'Not authorized',
222
- });
223
- });
224
- }
225
-
226
- /**
227
- * Creates a mock handler that simulates a version expired error
228
- *
229
- * @example
230
- * ```typescript
231
- * mockLambderVersionExpired('user.updateProfile')
232
- * ```
233
- */
234
- export function mockLambderVersionExpired<
235
- TContract extends ApiContractShape,
236
- TApiName extends keyof TContract & string
237
- >(
238
- apiName: TApiName,
239
- options?: {
240
- /** Custom API path, defaults to '/api' */
241
- apiPath?: string;
242
- /** Custom apiVersion to include in response */
243
- apiVersion?: string | null;
135
+ /**
136
+ * Mock an API endpoint that returns a version expired error
137
+ */
138
+ mockVersionExpired<TApiName extends keyof TContract & string>(
139
+ apiName: TApiName
140
+ ): RequestHandler {
141
+ return this.mockApi(
142
+ apiName,
143
+ async () => null as any,
144
+ { versionExpired: true }
145
+ );
244
146
  }
245
- ) {
246
- const apiPath = options?.apiPath ?? '/api';
247
- const apiVersion = options?.apiVersion ?? null;
248
147
 
249
- // @ts-ignore - MSW is an optional peer dependency
250
- const { http, HttpResponse } = require('msw');
251
-
252
- return http.post(apiPath, async ({ request }: any) => {
253
- const body = (await request.json()) as {
254
- apiName: string;
255
- payload?: any;
256
- };
257
-
258
- if (body.apiName !== apiName) {
259
- return;
260
- }
148
+ /**
149
+ * Mock an API endpoint that returns a not authorized error
150
+ */
151
+ mockNotAuthorized<TApiName extends keyof TContract & string>(
152
+ apiName: TApiName
153
+ ): RequestHandler {
154
+ return this.mockApi(
155
+ apiName,
156
+ async () => null as any,
157
+ { notAuthorized: true }
158
+ );
159
+ }
261
160
 
262
- console.log(`[MSW Lambder] Mocking version expired for ${apiName}`);
161
+ /**
162
+ * Mock an API endpoint that returns an error message
163
+ */
164
+ mockError<TApiName extends keyof TContract & string>(
165
+ apiName: TApiName,
166
+ errorMessage: string
167
+ ): RequestHandler {
168
+ return this.mockApi(
169
+ apiName,
170
+ async () => null as any,
171
+ { errorMessage }
172
+ );
173
+ }
263
174
 
264
- return HttpResponse.json({
265
- ...(apiVersion !== null ? { apiVersion } : {}),
266
- payload: null,
267
- versionExpired: true,
268
- errorMessage: 'Version expired',
269
- });
270
- });
175
+ /**
176
+ * Mock an API endpoint with a custom message
177
+ */
178
+ mockWithMessage<TApiName extends keyof TContract & string>(
179
+ apiName: TApiName,
180
+ handler: (payload?: TContract[TApiName]['input']) => Promise<TContract[TApiName]['output']> | TContract[TApiName]['output'],
181
+ message: any
182
+ ): RequestHandler {
183
+ return this.mockApi(apiName, handler, { message });
184
+ }
271
185
  }
272
186
 
273
- /**
274
- * Helper to combine multiple Lambder mock handlers
275
- * Since each handler checks for its specific apiName, you can just spread them into setupWorker/setupServer
276
- *
277
- * @deprecated This helper is not needed - just spread handlers directly into setupWorker/setupServer
278
- * @example
279
- * ```typescript
280
- * import { setupServer } from 'msw/node';
281
- *
282
- * const server = setupServer(
283
- * mockLambderApi('user.getProfile', () => ({ name: 'Test User' })),
284
- * mockLambderApi('user.updateProfile', (input) => ({ success: true }))
285
- * );
286
- * ```
287
- */
288
- export function createLambderApiHandler(
289
- apiPath: string,
290
- handlers: any[]
291
- ) {
292
- // This is deprecated - users should just spread handlers directly
293
- // Kept for backward compatibility
294
- return handlers;
295
- }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ export { default as LambderCaller } from "./LambderCaller.js";
5
5
  export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
6
6
  export { default as LambderResolver } from "./LambderResolver.js";
7
7
  export { default as LambderSessionManager } from "./LambderSessionManager.js";
8
+ export { default as LambderMSW } from "./LambderMSW.js";
8
9
 
9
10
  // Type-safe API contract utilities
10
11
  export {
@@ -13,13 +14,3 @@ export {
13
14
  type ApiInput,
14
15
  type ApiOutput,
15
16
  } from "./LambderApiContract.js";
16
-
17
- // MSW mocking utilities (optional - requires msw to be installed)
18
- export {
19
- mockLambderApi,
20
- mockLambderApiError,
21
- mockLambderSessionExpired,
22
- mockLambderNotAuthorized,
23
- mockLambderVersionExpired,
24
- createLambderApiHandler,
25
- } from "./LambderMSW.js";
@@ -1,191 +0,0 @@
1
- # MSW Integration - Complete Implementation Summary
2
-
3
- ## What Was Done
4
-
5
- Added Mock Service Worker (MSW) integration to Lambder as an optional peer dependency, allowing developers to easily mock Lambder API endpoints for testing and development.
6
-
7
- ## Files Created/Modified
8
-
9
- ### 1. Core Implementation
10
- - **`src/LambderMSW.ts`** - New file with MSW mocking utilities
11
- - `mockLambderApi()` - Mock successful API responses
12
- - `mockLambderApiError()` - Mock error responses
13
- - `mockLambderSessionExpired()` - Mock session expiry
14
- - `mockLambderNotAuthorized()` - Mock authorization errors
15
- - `mockLambderVersionExpired()` - Mock version expiry
16
- - All functions use `require('msw')` to dynamically load MSW
17
- - Each function calls `http.post()` and returns a proper MSW `RequestHandler`
18
-
19
- ### 2. Package Configuration
20
- - **`package.json`** - Added MSW as optional peer dependency
21
- ```json
22
- "peerDependencies": {
23
- "msw": "^2.0.0"
24
- },
25
- "peerDependenciesMeta": {
26
- "msw": {
27
- "optional": true
28
- }
29
- }
30
- ```
31
-
32
- ### 3. Exports
33
- - **`src/index.ts`** - Exported all MSW utilities from main entry point
34
-
35
- ### 4. Documentation
36
- - **`docs/MSW_MOCKING.md`** - Comprehensive guide (591 lines)
37
- - Installation instructions
38
- - Quick start with examples
39
- - API reference for all functions
40
- - Testing examples (Vitest, Jest, React Testing Library)
41
- - Storybook integration
42
- - Advanced usage patterns
43
- - Best practices and troubleshooting
44
-
45
- - **`docs/MSW_QUICK_FIX.md`** - Quick reference for the fix
46
- - Explains the solution to type compatibility
47
- - Shows correct usage patterns
48
- - Provides working examples
49
-
50
- - **`docs/MSW_INTEGRATION_SUMMARY.md`** - High-level overview
51
- - Lists all changes made
52
- - Explains benefits
53
- - Shows usage examples
54
-
55
- ### 5. Examples
56
- - **`examples/msw-mocking-example.ts`** - Detailed example with comments
57
- - API contract definition
58
- - Various mock handlers
59
- - Setup for Node.js tests
60
- - Setup for browser development
61
-
62
- - **`examples/msw-correct-usage.ts`** - Shows the correct way to use handlers
63
- - Demonstrates proper MSW integration
64
- - TypeScript type verification
65
-
66
- ### 6. Tests
67
- - **`tests/msw-integration.test.ts`** - Basic test to verify exports
68
- - Tests run without MSW installed
69
- - Includes commented examples for when MSW is installed
70
-
71
- ### 7. README Update
72
- - **`Readme.md`** - Added MSW to features and created new section
73
- - Added MSW to features list
74
- - New "Testing & Mocking with MSW" section
75
- - Links to detailed documentation
76
-
77
- ## How It Works
78
-
79
- ### The Fix for Type Compatibility
80
-
81
- **Problem**: Custom `HttpHandler` type didn't match MSW's `RequestHandler`
82
-
83
- **Solution**: Each Lambder MSW function now:
84
-
85
- 1. Dynamically requires MSW: `const { http, HttpResponse } = require('msw')`
86
- 2. Calls MSW's `http.post()` with the handler logic
87
- 3. Returns the `RequestHandler` that `http.post()` creates
88
- 4. Uses `@ts-ignore` for the require statement since MSW is optional
89
-
90
- This means the return type is automatically compatible with MSW's `setupWorker` and `setupServer`.
91
-
92
- ### Example Flow
93
-
94
- ```typescript
95
- // User code
96
- mockLambderApi('user.getProfile', () => ({ name: 'Test' }))
97
-
98
- // Internally:
99
- // 1. require('msw') → gets http and HttpResponse
100
- // 2. http.post('/api', async ({ request }) => { ... })
101
- // 3. Returns: RequestHandler from MSW
102
-
103
- // Result: Perfect type compatibility!
104
- ```
105
-
106
- ## Usage Example
107
-
108
- ```typescript
109
- // 1. Define API contract
110
- const ApiContract = {
111
- 'user.getProfile': {
112
- input: { userId: string },
113
- output: { name: string, email: string }
114
- }
115
- } satisfies ApiContractShape
116
-
117
- type MyApiContract = typeof ApiContract
118
-
119
- // 2. Create handlers
120
- import { mockLambderApi } from 'lambder'
121
-
122
- const handlers = [
123
- mockLambderApi<MyApiContract, 'user.getProfile'>(
124
- 'user.getProfile',
125
- () => ({
126
- name: 'Test User',
127
- email: 'test@example.com'
128
- })
129
- )
130
- ]
131
-
132
- // 3. Setup MSW
133
- import { setupWorker } from 'msw/browser'
134
- export const worker = setupWorker(...handlers) // ✅ Works!
135
- ```
136
-
137
- ## Key Features
138
-
139
- ✅ **Type-Safe**: Full TypeScript support with API contracts
140
- ✅ **Optional**: No hard dependency - only needed for testing
141
- ✅ **Easy**: Simple functions matching Lambder's API pattern
142
- ✅ **Comprehensive**: Covers all error scenarios
143
- ✅ **Compatible**: Returns proper MSW `RequestHandler` types
144
-
145
- ## Benefits
146
-
147
- 1. **No Breaking Changes**: Existing code continues to work
148
- 2. **Opt-In**: Only developers who want mocking need to install MSW
149
- 3. **Type Safety**: Full IntelliSense and type checking
150
- 4. **Developer Experience**: Works with popular testing frameworks
151
- 5. **Production Ready**: Well-documented with examples
152
-
153
- ## Testing
154
-
155
- - All tests pass without MSW installed
156
- - Build compiles successfully
157
- - No TypeScript errors
158
- - Example files demonstrate correct usage
159
-
160
- ## Installation for Users
161
-
162
- ```bash
163
- # Install lambder (already done)
164
- npm install lambder
165
-
166
- # Install MSW (only if you want mocking)
167
- npm install --save-dev msw
168
- ```
169
-
170
- ## Next Steps for Users
171
-
172
- 1. Read [MSW_MOCKING.md](./MSW_MOCKING.md) for detailed guide
173
- 2. Check [msw-correct-usage.ts](../examples/msw-correct-usage.ts) for example
174
- 3. Install MSW: `npm install --save-dev msw`
175
- 4. Create handlers using `mockLambderApi`
176
- 5. Set up MSW worker/server
177
- 6. Start mocking!
178
-
179
- ## Technical Details
180
-
181
- - Uses CommonJS `require()` for dynamic import
182
- - `@ts-ignore` suppresses optional dependency errors
183
- - Each handler checks `body.apiName` for routing
184
- - Supports all Lambder API response formats
185
- - Compatible with MSW v2.0.0+
186
-
187
- ---
188
-
189
- **Status**: ✅ Complete and tested
190
- **Compatibility**: MSW v2.0.0+, TypeScript 5.9+
191
- **Breaking Changes**: None