lambder 1.0.129 → 1.0.131
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 +49 -0
- package/dist/LambderMSW.d.ts +102 -0
- package/dist/LambderMSW.js +176 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/docs/MSW_IMPLEMENTATION_COMPLETE.md +191 -0
- package/docs/MSW_MOCKING.md +606 -0
- package/examples/msw-correct-usage.ts +136 -0
- package/examples/msw-mocking-example.ts +241 -0
- package/package.json +9 -1
- package/src/LambderMSW.ts +295 -0
- package/src/index.ts +10 -0
- package/tests/msw-integration.test.ts +45 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CORRECT USAGE: Lambder MSW Integration Example
|
|
3
|
+
*
|
|
4
|
+
* This demonstrates the proper way to use MSW with Lambder.
|
|
5
|
+
* Each mockLambderApi call returns an MSW RequestHandler that can be used directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Step 1: Define your API contract
|
|
9
|
+
import type { ApiContractShape } from '../src/index.js';
|
|
10
|
+
|
|
11
|
+
const ApiContract = {
|
|
12
|
+
'public.getInitialPageData': {
|
|
13
|
+
input: null,
|
|
14
|
+
output: {
|
|
15
|
+
userLocationData: {
|
|
16
|
+
country: '' as string,
|
|
17
|
+
region: '' as string,
|
|
18
|
+
regionCode: '' as string,
|
|
19
|
+
city: '' as string,
|
|
20
|
+
zipCode: '' as string,
|
|
21
|
+
lat: 0 as number,
|
|
22
|
+
lng: 0 as number,
|
|
23
|
+
},
|
|
24
|
+
sessionUser: null as null | {
|
|
25
|
+
id: string;
|
|
26
|
+
username: string;
|
|
27
|
+
email: string;
|
|
28
|
+
name: string;
|
|
29
|
+
bio: string;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
'user.updateProfile': {
|
|
34
|
+
input: {
|
|
35
|
+
name: '' as string,
|
|
36
|
+
bio: '' as string
|
|
37
|
+
},
|
|
38
|
+
output: {
|
|
39
|
+
success: true as boolean
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} satisfies ApiContractShape;
|
|
43
|
+
|
|
44
|
+
type MyApiContract = typeof ApiContract;
|
|
45
|
+
|
|
46
|
+
// Step 2: Create mock handlers using Lambder's utility functions
|
|
47
|
+
import { mockLambderApi } from '../src/index.js';
|
|
48
|
+
|
|
49
|
+
export const handlers = [
|
|
50
|
+
// Each mockLambderApi call returns a proper MSW RequestHandler
|
|
51
|
+
// You can use them directly in setupWorker/setupServer
|
|
52
|
+
mockLambderApi<MyApiContract, 'public.getInitialPageData'>(
|
|
53
|
+
'public.getInitialPageData',
|
|
54
|
+
() => ({
|
|
55
|
+
userLocationData: {
|
|
56
|
+
country: 'United States',
|
|
57
|
+
region: 'California',
|
|
58
|
+
regionCode: 'CA',
|
|
59
|
+
city: 'San Francisco',
|
|
60
|
+
zipCode: '94102',
|
|
61
|
+
lat: 37.7749,
|
|
62
|
+
lng: -122.4194,
|
|
63
|
+
},
|
|
64
|
+
sessionUser: {
|
|
65
|
+
id: 'mock-user-123',
|
|
66
|
+
username: 'mockuser',
|
|
67
|
+
email: 'mock@example.com',
|
|
68
|
+
name: 'Mock User',
|
|
69
|
+
bio: 'This is a mock user for development',
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
),
|
|
73
|
+
|
|
74
|
+
mockLambderApi<MyApiContract, 'user.updateProfile'>(
|
|
75
|
+
'user.updateProfile',
|
|
76
|
+
(input) => {
|
|
77
|
+
console.log('Updating profile:', input);
|
|
78
|
+
return {
|
|
79
|
+
success: true
|
|
80
|
+
};
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
delay: 500 // Optional: simulate network delay
|
|
84
|
+
}
|
|
85
|
+
),
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
// Step 3: Set up MSW
|
|
89
|
+
// For Browser (development):
|
|
90
|
+
/**
|
|
91
|
+
* ```typescript
|
|
92
|
+
* // src/mocks/browser.ts
|
|
93
|
+
* import { setupWorker } from 'msw/browser';
|
|
94
|
+
* import { handlers } from './handlers';
|
|
95
|
+
*
|
|
96
|
+
* export const worker = setupWorker(...handlers);
|
|
97
|
+
*
|
|
98
|
+
* // src/main.tsx
|
|
99
|
+
* if (import.meta.env.DEV) {
|
|
100
|
+
* const { worker } = await import('./mocks/browser');
|
|
101
|
+
* worker.start();
|
|
102
|
+
* }
|
|
103
|
+
* ```
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
// For Node.js (testing):
|
|
107
|
+
/**
|
|
108
|
+
* ```typescript
|
|
109
|
+
* // src/mocks/server.ts
|
|
110
|
+
* import { setupServer } from 'msw/node';
|
|
111
|
+
* import { handlers } from './handlers';
|
|
112
|
+
*
|
|
113
|
+
* export const server = setupServer(...handlers);
|
|
114
|
+
*
|
|
115
|
+
* // vitest.setup.ts
|
|
116
|
+
* import { server } from './mocks/server';
|
|
117
|
+
*
|
|
118
|
+
* beforeAll(() => server.listen());
|
|
119
|
+
* afterEach(() => server.resetHandlers());
|
|
120
|
+
* afterAll(() => server.close());
|
|
121
|
+
* ```
|
|
122
|
+
*/
|
|
123
|
+
|
|
124
|
+
// Type verification: handlers is an array of MSW RequestHandler
|
|
125
|
+
// TypeScript should infer this correctly without needing explicit types
|
|
126
|
+
export type HandlersType = typeof handlers; // RequestHandler[]
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* KEY POINTS:
|
|
130
|
+
*
|
|
131
|
+
* 1. mockLambderApi returns a proper MSW RequestHandler (from http.post)
|
|
132
|
+
* 2. You can spread handlers directly into setupWorker/setupServer
|
|
133
|
+
* 3. Each handler checks body.apiName and only responds to its specific API
|
|
134
|
+
* 4. Full type safety with your API contract
|
|
135
|
+
* 5. No need to import HttpHandler type - MSW provides RequestHandler
|
|
136
|
+
*/
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example: Using MSW to mock Lambder API endpoints
|
|
3
|
+
*
|
|
4
|
+
* This example demonstrates how to use Mock Service Worker (MSW) with Lambder
|
|
5
|
+
* to create type-safe mocks for your API endpoints.
|
|
6
|
+
*
|
|
7
|
+
* Prerequisites:
|
|
8
|
+
* - Install MSW: npm install --save-dev msw
|
|
9
|
+
* - Define your API contract
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import {
|
|
13
|
+
mockLambderApi,
|
|
14
|
+
mockLambderApiError,
|
|
15
|
+
mockLambderSessionExpired,
|
|
16
|
+
mockLambderNotAuthorized,
|
|
17
|
+
mockLambderVersionExpired,
|
|
18
|
+
type ApiContractShape
|
|
19
|
+
} from '../src/index.js';
|
|
20
|
+
|
|
21
|
+
// 1. Define your API contract
|
|
22
|
+
const myApiContract = {
|
|
23
|
+
'user.getProfile': {
|
|
24
|
+
input: { userId: '' as string },
|
|
25
|
+
output: { name: '' as string, email: '' as string, role: '' as string }
|
|
26
|
+
},
|
|
27
|
+
'user.updateProfile': {
|
|
28
|
+
input: { userId: '' as string, name: '' as string, email: '' as string },
|
|
29
|
+
output: { success: true as boolean, message: '' as string }
|
|
30
|
+
},
|
|
31
|
+
'user.deleteAccount': {
|
|
32
|
+
input: { userId: '' as string },
|
|
33
|
+
output: { success: true as boolean }
|
|
34
|
+
},
|
|
35
|
+
'admin.listUsers': {
|
|
36
|
+
input: { page: 0 as number, limit: 0 as number },
|
|
37
|
+
output: { users: [] as Array<{ id: string; name: string }>, total: 0 as number }
|
|
38
|
+
}
|
|
39
|
+
} satisfies ApiContractShape;
|
|
40
|
+
|
|
41
|
+
type MyApiContract = typeof myApiContract;
|
|
42
|
+
|
|
43
|
+
// 2. Create mock handlers
|
|
44
|
+
|
|
45
|
+
// Basic success mock
|
|
46
|
+
export const getUserProfileHandler = mockLambderApi<MyApiContract, 'user.getProfile'>(
|
|
47
|
+
'user.getProfile',
|
|
48
|
+
(input) => {
|
|
49
|
+
console.log('Mocking user.getProfile for userId:', input.userId);
|
|
50
|
+
|
|
51
|
+
// Return mock data with full type safety
|
|
52
|
+
return {
|
|
53
|
+
name: 'John Doe',
|
|
54
|
+
email: 'john@example.com',
|
|
55
|
+
role: 'user'
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
// Mock with dynamic response based on input
|
|
61
|
+
export const updateProfileHandler = mockLambderApi<MyApiContract, 'user.updateProfile'>(
|
|
62
|
+
'user.updateProfile',
|
|
63
|
+
(input) => {
|
|
64
|
+
console.log('Mocking user.updateProfile:', input);
|
|
65
|
+
|
|
66
|
+
// Validate input
|
|
67
|
+
if (!input.name || input.name.length < 2) {
|
|
68
|
+
throw new Error('Name must be at least 2 characters');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
success: true,
|
|
73
|
+
message: `Profile updated for ${input.name}`
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
delay: 500 // Simulate network delay
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// Mock with error response
|
|
82
|
+
export const updateProfileErrorHandler = mockLambderApiError<MyApiContract, 'user.updateProfile'>(
|
|
83
|
+
'user.updateProfile',
|
|
84
|
+
'Failed to update profile due to server error',
|
|
85
|
+
{
|
|
86
|
+
delay: 300
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Mock session expired
|
|
91
|
+
export const deleteAccountSessionExpiredHandler = mockLambderSessionExpired<MyApiContract, 'user.deleteAccount'>(
|
|
92
|
+
'user.deleteAccount'
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Mock not authorized
|
|
96
|
+
export const listUsersNotAuthorizedHandler = mockLambderNotAuthorized<MyApiContract, 'admin.listUsers'>(
|
|
97
|
+
'admin.listUsers'
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Mock version expired
|
|
101
|
+
export const getProfileVersionExpiredHandler = mockLambderVersionExpired<MyApiContract, 'user.getProfile'>(
|
|
102
|
+
'user.getProfile'
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Mock with pagination
|
|
106
|
+
export const listUsersHandler = mockLambderApi<MyApiContract, 'admin.listUsers'>(
|
|
107
|
+
'admin.listUsers',
|
|
108
|
+
(input) => {
|
|
109
|
+
// Generate mock paginated data
|
|
110
|
+
const allUsers = Array.from({ length: 50 }, (_, i) => ({
|
|
111
|
+
id: `user-${i + 1}`,
|
|
112
|
+
name: `User ${i + 1}`
|
|
113
|
+
}));
|
|
114
|
+
|
|
115
|
+
const start = input.page * input.limit;
|
|
116
|
+
const end = start + input.limit;
|
|
117
|
+
const users = allUsers.slice(start, end);
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
users,
|
|
121
|
+
total: allUsers.length
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
// 3. Set up MSW handlers
|
|
127
|
+
|
|
128
|
+
// For Node.js testing (Vitest, Jest, etc.)
|
|
129
|
+
/**
|
|
130
|
+
* Setup example for Node.js tests:
|
|
131
|
+
*
|
|
132
|
+
* ```typescript
|
|
133
|
+
* // test-setup.ts
|
|
134
|
+
* import { setupServer } from 'msw/node';
|
|
135
|
+
* import { getUserProfileHandler, updateProfileHandler } from './msw-example';
|
|
136
|
+
*
|
|
137
|
+
* export const server = setupServer(
|
|
138
|
+
* getUserProfileHandler,
|
|
139
|
+
* updateProfileHandler,
|
|
140
|
+
* listUsersHandler
|
|
141
|
+
* );
|
|
142
|
+
*
|
|
143
|
+
* // vitest.setup.ts
|
|
144
|
+
* beforeAll(() => server.listen());
|
|
145
|
+
* afterEach(() => server.resetHandlers());
|
|
146
|
+
* afterAll(() => server.close());
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
// For browser development/Storybook
|
|
151
|
+
/**
|
|
152
|
+
* Setup example for browser:
|
|
153
|
+
*
|
|
154
|
+
* ```typescript
|
|
155
|
+
* // mocks/browser.ts
|
|
156
|
+
* import { setupWorker } from 'msw/browser';
|
|
157
|
+
* import { getUserProfileHandler, updateProfileHandler } from './msw-example';
|
|
158
|
+
*
|
|
159
|
+
* export const worker = setupWorker(
|
|
160
|
+
* getUserProfileHandler,
|
|
161
|
+
* updateProfileHandler,
|
|
162
|
+
* listUsersHandler
|
|
163
|
+
* );
|
|
164
|
+
*
|
|
165
|
+
* // main.tsx
|
|
166
|
+
* if (import.meta.env.DEV) {
|
|
167
|
+
* const { worker } = await import('./mocks/browser');
|
|
168
|
+
* worker.start();
|
|
169
|
+
* }
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
|
|
173
|
+
// 4. Using in tests
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Test example:
|
|
177
|
+
*
|
|
178
|
+
* ```typescript
|
|
179
|
+
* import { describe, it, expect } from 'vitest';
|
|
180
|
+
* import { server } from './test-setup';
|
|
181
|
+
* import { caller } from './api-client';
|
|
182
|
+
*
|
|
183
|
+
* describe('User Profile', () => {
|
|
184
|
+
* it('should get user profile', async () => {
|
|
185
|
+
* const profile = await caller.api('user.getProfile', { userId: '123' });
|
|
186
|
+
*
|
|
187
|
+
* expect(profile).toEqual({
|
|
188
|
+
* name: 'John Doe',
|
|
189
|
+
* email: 'john@example.com',
|
|
190
|
+
* role: 'user'
|
|
191
|
+
* });
|
|
192
|
+
* });
|
|
193
|
+
*
|
|
194
|
+
* it('should handle update errors', async () => {
|
|
195
|
+
* // Override handler for this test
|
|
196
|
+
* server.use(updateProfileErrorHandler);
|
|
197
|
+
*
|
|
198
|
+
* const result = await caller.api('user.updateProfile', {
|
|
199
|
+
* userId: '123',
|
|
200
|
+
* name: 'New Name',
|
|
201
|
+
* email: 'new@example.com'
|
|
202
|
+
* });
|
|
203
|
+
*
|
|
204
|
+
* expect(result).toBeNull(); // Error handler returns null
|
|
205
|
+
* });
|
|
206
|
+
*
|
|
207
|
+
* it('should handle session expiry', async () => {
|
|
208
|
+
* server.use(deleteAccountSessionExpiredHandler);
|
|
209
|
+
*
|
|
210
|
+
* const result = await caller.api('user.deleteAccount', { userId: '123' });
|
|
211
|
+
*
|
|
212
|
+
* expect(result).toBeNull();
|
|
213
|
+
* // Verify sessionExpiredHandler was called if configured
|
|
214
|
+
* });
|
|
215
|
+
*
|
|
216
|
+
* it('should handle not authorized', async () => {
|
|
217
|
+
* server.use(listUsersNotAuthorizedHandler);
|
|
218
|
+
*
|
|
219
|
+
* const result = await caller.api('admin.listUsers', { page: 0, limit: 10 });
|
|
220
|
+
*
|
|
221
|
+
* expect(result).toBeNull();
|
|
222
|
+
* // Verify notAuthorizedHandler was called if configured
|
|
223
|
+
* });
|
|
224
|
+
* });
|
|
225
|
+
* ```
|
|
226
|
+
*/
|
|
227
|
+
|
|
228
|
+
// Export all handlers for easy setup
|
|
229
|
+
export const handlers = [
|
|
230
|
+
getUserProfileHandler,
|
|
231
|
+
updateProfileHandler,
|
|
232
|
+
listUsersHandler,
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
// Export error handlers separately
|
|
236
|
+
export const errorHandlers = [
|
|
237
|
+
updateProfileErrorHandler,
|
|
238
|
+
deleteAccountSessionExpiredHandler,
|
|
239
|
+
listUsersNotAuthorizedHandler,
|
|
240
|
+
getProfileVersionExpiredHandler,
|
|
241
|
+
];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.131",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -27,6 +27,14 @@
|
|
|
27
27
|
"path-to-regexp": "^6.2.1",
|
|
28
28
|
"querystring": "^0.2.1"
|
|
29
29
|
},
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"msw": "^2.0.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependenciesMeta": {
|
|
34
|
+
"msw": {
|
|
35
|
+
"optional": true
|
|
36
|
+
}
|
|
37
|
+
},
|
|
30
38
|
"devDependencies": {
|
|
31
39
|
"@types/aws-lambda": "^8.10.136",
|
|
32
40
|
"@types/cookie": "^0.6.0",
|
|
@@ -0,0 +1,295 @@
|
|
|
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));
|
|
65
|
+
}
|
|
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
|
+
}
|
|
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
|
+
|
|
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,
|
|
128
|
+
});
|
|
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
|
+
}
|
|
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
|
+
|
|
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;
|
|
197
|
+
}
|
|
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
|
+
|
|
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;
|
|
244
|
+
}
|
|
245
|
+
) {
|
|
246
|
+
const apiPath = options?.apiPath ?? '/api';
|
|
247
|
+
const apiVersion = options?.apiVersion ?? null;
|
|
248
|
+
|
|
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
|
+
}
|
|
261
|
+
|
|
262
|
+
console.log(`[MSW Lambder] Mocking version expired for ${apiName}`);
|
|
263
|
+
|
|
264
|
+
return HttpResponse.json({
|
|
265
|
+
...(apiVersion !== null ? { apiVersion } : {}),
|
|
266
|
+
payload: null,
|
|
267
|
+
versionExpired: true,
|
|
268
|
+
errorMessage: 'Version expired',
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
|
|
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
|
@@ -13,3 +13,13 @@ export {
|
|
|
13
13
|
type ApiInput,
|
|
14
14
|
type ApiOutput,
|
|
15
15
|
} 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";
|