lambder 1.0.129 → 1.0.130

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.
@@ -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.129",
3
+ "version": "1.0.130",
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,330 @@
1
+ import type { ApiContractShape } from './LambderApiContract.js';
2
+
3
+ // Type for MSW HttpHandler (defined locally to avoid hard dependency on msw types)
4
+ export type HttpHandler = (info: any) => Promise<Response | undefined> | Response | undefined;
5
+
6
+ // Dynamic imports to avoid requiring MSW as a hard dependency
7
+ let http: any;
8
+ let HttpResponse: any;
9
+
10
+ /**
11
+ * Initialize MSW dependencies dynamically
12
+ * This allows the module to be imported even if MSW is not installed
13
+ */
14
+ async function ensureMSW() {
15
+ if (!http || !HttpResponse) {
16
+ try {
17
+ // @ts-ignore - MSW is an optional peer dependency
18
+ const msw = await import('msw');
19
+ http = msw.http;
20
+ HttpResponse = msw.HttpResponse;
21
+ } catch (err) {
22
+ throw new Error(
23
+ 'MSW is required to use Lambder mocking utilities. ' +
24
+ 'Install it with: npm install --save-dev msw'
25
+ );
26
+ }
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Creates a mock handler for a Lambder API endpoint
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * mockLambderApi('public.getInitialPageData', () => ({
36
+ * userLocationData: { ... },
37
+ * sessionUser: null
38
+ * }))
39
+ * ```
40
+ *
41
+ * @example with dynamic response based on input
42
+ * ```typescript
43
+ * mockLambderApi('user.updateProfile', (input) => ({
44
+ * success: true,
45
+ * userId: input.userId
46
+ * }))
47
+ * ```
48
+ */
49
+ export function mockLambderApi<
50
+ TContract extends ApiContractShape,
51
+ TApiName extends keyof TContract & string
52
+ >(
53
+ apiName: TApiName,
54
+ responseFactory: (
55
+ input: TContract[TApiName]['input']
56
+ ) => TContract[TApiName]['output'] | Promise<TContract[TApiName]['output']>,
57
+ options?: {
58
+ /** Custom API path, defaults to '/api' */
59
+ apiPath?: string
60
+ /** Delay in ms before responding (for testing loading states) */
61
+ delay?: number
62
+ /** Custom apiVersion to include in response */
63
+ apiVersion?: string | null
64
+ }
65
+ ): HttpHandler {
66
+ const apiPath = options?.apiPath ?? '/api';
67
+ const delay = options?.delay ?? 0;
68
+ const apiVersion = options?.apiVersion ?? null;
69
+
70
+ // Return a handler that will be initialized when MSW is available
71
+ return (async ({ request }: any) => {
72
+ await ensureMSW();
73
+
74
+ const body = (await request.json()) as {
75
+ apiName: string;
76
+ payload?: TContract[TApiName]['input'];
77
+ version?: string;
78
+ token?: string;
79
+ siteHost?: string;
80
+ };
81
+
82
+ // Only handle this specific API
83
+ if (body.apiName !== apiName) {
84
+ return;
85
+ }
86
+
87
+ console.log(`[MSW Lambder] Mocking ${apiName}`, body.payload);
88
+
89
+ // Apply delay if specified
90
+ if (delay > 0) {
91
+ await new Promise((resolve) => setTimeout(resolve, delay));
92
+ }
93
+
94
+ // Generate the response
95
+ const payload = await responseFactory(body.payload as TContract[TApiName]['input']);
96
+
97
+ // Return in Lambder API response format
98
+ return HttpResponse.json({
99
+ ...(apiVersion !== null ? { apiVersion } : {}),
100
+ payload,
101
+ });
102
+ }) as any;
103
+ }
104
+
105
+ /**
106
+ * Creates a mock handler that returns an error for a Lambder API endpoint
107
+ *
108
+ * @example
109
+ * ```typescript
110
+ * mockLambderApiError('user.deleteAccount', 'Account deletion failed')
111
+ * ```
112
+ */
113
+ export function mockLambderApiError<
114
+ TContract extends ApiContractShape,
115
+ TApiName extends keyof TContract & string
116
+ >(
117
+ apiName: TApiName,
118
+ errorMessage: string,
119
+ options?: {
120
+ /** Custom API path, defaults to '/api' */
121
+ apiPath?: string;
122
+ /** Delay in ms before responding */
123
+ delay?: number;
124
+ /** Custom apiVersion to include in response */
125
+ apiVersion?: string | null;
126
+ }
127
+ ): HttpHandler {
128
+ const apiPath = options?.apiPath ?? '/api';
129
+ const delay = options?.delay ?? 0;
130
+ const apiVersion = options?.apiVersion ?? null;
131
+
132
+ return (async ({ request }: any) => {
133
+ await ensureMSW();
134
+
135
+ const body = (await request.json()) as {
136
+ apiName: string;
137
+ payload?: any;
138
+ };
139
+
140
+ if (body.apiName !== apiName) {
141
+ return;
142
+ }
143
+
144
+ console.log(`[MSW Lambder] Mocking error for ${apiName}:`, errorMessage);
145
+
146
+ if (delay > 0) {
147
+ await new Promise((resolve) => setTimeout(resolve, delay));
148
+ }
149
+
150
+ return HttpResponse.json({
151
+ ...(apiVersion !== null ? { apiVersion } : {}),
152
+ payload: null,
153
+ errorMessage,
154
+ });
155
+ }) as any;
156
+ }
157
+
158
+ /**
159
+ * Creates a mock handler that simulates a session expired error
160
+ *
161
+ * @example
162
+ * ```typescript
163
+ * mockLambderSessionExpired('user.updateProfile')
164
+ * ```
165
+ */
166
+ export function mockLambderSessionExpired<
167
+ TContract extends ApiContractShape,
168
+ TApiName extends keyof TContract & string
169
+ >(
170
+ apiName: TApiName,
171
+ options?: {
172
+ /** Custom API path, defaults to '/api' */
173
+ apiPath?: string;
174
+ /** Custom apiVersion to include in response */
175
+ apiVersion?: string | null;
176
+ }
177
+ ): HttpHandler {
178
+ const apiPath = options?.apiPath ?? '/api';
179
+ const apiVersion = options?.apiVersion ?? null;
180
+
181
+ return (async ({ request }: any) => {
182
+ await ensureMSW();
183
+
184
+ const body = (await request.json()) as {
185
+ apiName: string;
186
+ payload?: any;
187
+ };
188
+
189
+ if (body.apiName !== apiName) {
190
+ return;
191
+ }
192
+
193
+ console.log(`[MSW Lambder] Mocking session expired for ${apiName}`);
194
+
195
+ return HttpResponse.json({
196
+ ...(apiVersion !== null ? { apiVersion } : {}),
197
+ payload: null,
198
+ sessionExpired: true,
199
+ errorMessage: 'Session expired',
200
+ });
201
+ }) as any;
202
+ }
203
+
204
+ /**
205
+ * Creates a mock handler that simulates a "not authorized" error
206
+ *
207
+ * @example
208
+ * ```typescript
209
+ * mockLambderNotAuthorized('admin.deleteUser')
210
+ * ```
211
+ */
212
+ export function mockLambderNotAuthorized<
213
+ TContract extends ApiContractShape,
214
+ TApiName extends keyof TContract & string
215
+ >(
216
+ apiName: TApiName,
217
+ options?: {
218
+ /** Custom API path, defaults to '/api' */
219
+ apiPath?: string;
220
+ /** Custom apiVersion to include in response */
221
+ apiVersion?: string | null;
222
+ }
223
+ ): HttpHandler {
224
+ const apiPath = options?.apiPath ?? '/api';
225
+ const apiVersion = options?.apiVersion ?? null;
226
+
227
+ return (async ({ request }: any) => {
228
+ await ensureMSW();
229
+
230
+ const body = (await request.json()) as {
231
+ apiName: string;
232
+ payload?: any;
233
+ };
234
+
235
+ if (body.apiName !== apiName) {
236
+ return;
237
+ }
238
+
239
+ console.log(`[MSW Lambder] Mocking not authorized for ${apiName}`);
240
+
241
+ return HttpResponse.json({
242
+ ...(apiVersion !== null ? { apiVersion } : {}),
243
+ payload: null,
244
+ notAuthorized: true,
245
+ errorMessage: 'Not authorized',
246
+ });
247
+ }) as any;
248
+ }
249
+
250
+ /**
251
+ * Creates a mock handler that simulates a version expired error
252
+ *
253
+ * @example
254
+ * ```typescript
255
+ * mockLambderVersionExpired('user.updateProfile')
256
+ * ```
257
+ */
258
+ export function mockLambderVersionExpired<
259
+ TContract extends ApiContractShape,
260
+ TApiName extends keyof TContract & string
261
+ >(
262
+ apiName: TApiName,
263
+ options?: {
264
+ /** Custom API path, defaults to '/api' */
265
+ apiPath?: string;
266
+ /** Custom apiVersion to include in response */
267
+ apiVersion?: string | null;
268
+ }
269
+ ): HttpHandler {
270
+ const apiPath = options?.apiPath ?? '/api';
271
+ const apiVersion = options?.apiVersion ?? null;
272
+
273
+ return (async ({ request }: any) => {
274
+ await ensureMSW();
275
+
276
+ const body = (await request.json()) as {
277
+ apiName: string;
278
+ payload?: any;
279
+ };
280
+
281
+ if (body.apiName !== apiName) {
282
+ return;
283
+ }
284
+
285
+ console.log(`[MSW Lambder] Mocking version expired for ${apiName}`);
286
+
287
+ return HttpResponse.json({
288
+ ...(apiVersion !== null ? { apiVersion } : {}),
289
+ payload: null,
290
+ versionExpired: true,
291
+ errorMessage: 'Version expired',
292
+ });
293
+ }) as any;
294
+ }
295
+
296
+ /**
297
+ * Helper function to create the MSW http.post handler with proper path matching
298
+ * This is useful when you need to set up multiple handlers with the same path
299
+ *
300
+ * @example
301
+ * ```typescript
302
+ * import { setupServer } from 'msw/node';
303
+ *
304
+ * const server = setupServer(
305
+ * createLambderApiHandler('/api', [
306
+ * mockLambderApi('user.getProfile', () => ({ name: 'Test User' })),
307
+ * mockLambderApi('user.updateProfile', (input) => ({ success: true }))
308
+ * ])
309
+ * );
310
+ * ```
311
+ */
312
+ export function createLambderApiHandler(
313
+ apiPath: string,
314
+ handlers: HttpHandler[]
315
+ ): HttpHandler {
316
+ return (async (info: any) => {
317
+ await ensureMSW();
318
+
319
+ // Call each handler in sequence until one handles the request
320
+ for (const handler of handlers) {
321
+ const result = await handler(info);
322
+ if (result) {
323
+ return result;
324
+ }
325
+ }
326
+
327
+ // No handler matched
328
+ return;
329
+ }) as any;
330
+ }
package/src/index.ts CHANGED
@@ -13,3 +13,14 @@ 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
+ type HttpHandler,
26
+ } from "./LambderMSW.js";