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.
@@ -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.129",
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";