lambder 1.0.128 → 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.
- package/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +90 -0
- package/Readme.md +49 -0
- package/dist/Lambder.d.ts +5 -4
- package/dist/Lambder.js +2 -2
- package/dist/LambderMSW.d.ts +104 -0
- package/dist/LambderMSW.js +203 -0
- package/dist/LambderResolver.d.ts +17 -15
- package/dist/LambderResolver.js +4 -0
- package/dist/LambderResponseBuilder.d.ts +2 -1
- package/dist/LambderSessionController.d.ts +1 -0
- package/dist/LambderSessionController.js +10 -0
- package/dist/LambderSessionManager.d.ts +6 -1
- package/dist/LambderSessionManager.js +43 -12
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/docs/DYNAMODB_SETUP.md +96 -0
- package/docs/MSW_MOCKING.md +590 -0
- package/docs/TYPE_SAFE_QUICK_START.md +48 -4
- package/examples/msw-mocking-example.ts +241 -0
- package/examples/output-type-enforcement-example.ts +218 -0
- package/examples/secure-session-example.ts +191 -0
- package/examples/test-output-type-enforcement.ts +101 -0
- package/package.json +9 -1
- package/src/Lambder.ts +4 -4
- package/src/LambderMSW.ts +330 -0
- package/src/LambderResolver.ts +32 -15
- package/src/LambderResponseBuilder.ts +2 -1
- package/src/LambderSessionController.ts +9 -0
- package/src/LambderSessionManager.ts +51 -10
- package/src/index.ts +11 -0
- package/tests/msw-integration.test.ts +183 -0
- package/tests/output-type-runtime.test.ts +365 -0
- package/tests/type-safety.test.ts +312 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test file to verify output type enforcement
|
|
3
|
+
* This file should have TypeScript errors if output types are not correct
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Lambder from '../src/Lambder.js';
|
|
7
|
+
import type { ApiContract } from '../src/index.js';
|
|
8
|
+
|
|
9
|
+
// Define a simple API contract
|
|
10
|
+
type TestContract = ApiContract<{
|
|
11
|
+
getNumber: { input: void, output: number },
|
|
12
|
+
getString: { input: { id: string }, output: string },
|
|
13
|
+
getUser: { input: { userId: string }, output: { id: string; name: string; age: number } },
|
|
14
|
+
}>;
|
|
15
|
+
|
|
16
|
+
const lambder = new Lambder<TestContract>({
|
|
17
|
+
publicPath: './public',
|
|
18
|
+
apiPath: '/api',
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// ✅ CORRECT: Returns the right type
|
|
22
|
+
lambder.addApi('getNumber', async (ctx, resolver) => {
|
|
23
|
+
// This should work - returning number
|
|
24
|
+
return resolver.api(42);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
// ❌ INCORRECT: Should cause TypeScript error - returning wrong type
|
|
28
|
+
lambder.addApi('getNumber', async (ctx, resolver) => {
|
|
29
|
+
// @ts-expect-error - Testing type enforcement: should not accept string when output is number
|
|
30
|
+
return resolver.api("wrong type");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// ✅ CORRECT: Returns string
|
|
34
|
+
lambder.addApi('getString', async (ctx, resolver) => {
|
|
35
|
+
const id = ctx.apiPayload.id;
|
|
36
|
+
return resolver.api(`String for ${id}`);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// ❌ INCORRECT: Should cause TypeScript error - returning wrong type
|
|
40
|
+
lambder.addApi('getString', async (ctx, resolver) => {
|
|
41
|
+
// @ts-expect-error - Testing type enforcement: should not accept number when output is string
|
|
42
|
+
return resolver.api(123);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ✅ CORRECT: Returns correct object shape
|
|
46
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
47
|
+
return resolver.api({
|
|
48
|
+
id: ctx.apiPayload.userId,
|
|
49
|
+
name: "John Doe",
|
|
50
|
+
age: 30,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// ❌ INCORRECT: Should cause TypeScript error - missing required field
|
|
55
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
56
|
+
// @ts-expect-error - Testing type enforcement: missing 'age' field
|
|
57
|
+
return resolver.api({
|
|
58
|
+
id: ctx.apiPayload.userId,
|
|
59
|
+
name: "John Doe",
|
|
60
|
+
// age is missing
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ❌ INCORRECT: Should cause TypeScript error - wrong field type
|
|
65
|
+
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
66
|
+
return resolver.api({
|
|
67
|
+
id: ctx.apiPayload.userId,
|
|
68
|
+
name: "John Doe",
|
|
69
|
+
// @ts-expect-error - Testing type enforcement: age should be number, not string
|
|
70
|
+
age: "30", // wrong type
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ✅ CORRECT: Using null is allowed
|
|
75
|
+
lambder.addApi('getNumber', async (ctx, resolver) => {
|
|
76
|
+
return resolver.api(null);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// ✅ CORRECT: Using resolver.die.api also enforces types
|
|
80
|
+
lambder.addApi('getNumber', async (ctx, resolver) => {
|
|
81
|
+
return resolver.die.api(42);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ❌ INCORRECT: resolver.die.api should also enforce types
|
|
85
|
+
lambder.addApi('getNumber', async (ctx, resolver) => {
|
|
86
|
+
// @ts-expect-error - Testing type enforcement: die.api should also check types
|
|
87
|
+
return resolver.die.api("wrong type");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Test with addSessionApi
|
|
91
|
+
lambder.addSessionApi('getString', async (ctx, resolver) => {
|
|
92
|
+
// Should also enforce output type for session APIs
|
|
93
|
+
return resolver.api("correct string");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
lambder.addSessionApi('getString', async (ctx, resolver) => {
|
|
97
|
+
// @ts-expect-error - Testing type enforcement: session API should also enforce types
|
|
98
|
+
return resolver.api(123);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
console.log("If this file compiles with @ts-expect-error comments, output type enforcement is working!");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "1.0.
|
|
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",
|
package/src/Lambder.ts
CHANGED
|
@@ -140,11 +140,11 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
enableDdbSession(
|
|
143
|
-
{ tableName, tableRegion, sessionSalt }: { tableName: string; tableRegion: string; sessionSalt: string; },
|
|
143
|
+
{ tableName, tableRegion, sessionSalt, enableSlidingExpiration }: { tableName: string; tableRegion: string; sessionSalt: string; enableSlidingExpiration?: boolean; },
|
|
144
144
|
{ partitionKey, sortKey }: { partitionKey: string, sortKey: string } = { partitionKey: "pk", sortKey: "sk" }
|
|
145
145
|
){
|
|
146
146
|
this.lambderSessionManager = new LambderSessionManager({
|
|
147
|
-
tableName, tableRegion, partitionKey, sortKey, sessionSalt
|
|
147
|
+
tableName, tableRegion, partitionKey, sortKey, sessionSalt, enableSlidingExpiration
|
|
148
148
|
});
|
|
149
149
|
}
|
|
150
150
|
|
|
@@ -249,7 +249,7 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
249
249
|
apiName: TApiName,
|
|
250
250
|
actionFn: (
|
|
251
251
|
ctx: LambderRenderContext<TContract[TApiName]['input']>,
|
|
252
|
-
resolver: LambderResolver
|
|
252
|
+
resolver: LambderResolver<TContract, TApiName>
|
|
253
253
|
) => LambderResolverResponse|Promise<LambderResolverResponse>
|
|
254
254
|
):void;
|
|
255
255
|
// Overload for untyped API with string (backward compatibility, must be last)
|
|
@@ -281,7 +281,7 @@ export default class Lambder<TContract extends ApiContractShape = any> {
|
|
|
281
281
|
apiName: TApiName,
|
|
282
282
|
actionFn: (
|
|
283
283
|
ctx: LambderRenderContext<TContract[TApiName]['input']>,
|
|
284
|
-
resolver: LambderResolver
|
|
284
|
+
resolver: LambderResolver<TContract, TApiName>
|
|
285
285
|
) => LambderResolverResponse|Promise<LambderResolverResponse>
|
|
286
286
|
):void;
|
|
287
287
|
// Overload for untyped session API with string (backward compatibility, must be last)
|
|
@@ -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/LambderResolver.ts
CHANGED
|
@@ -1,28 +1,36 @@
|
|
|
1
1
|
import type { LambderRenderContext } from "./Lambder.js";
|
|
2
2
|
import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
|
|
3
3
|
import LambderUtils from "./LambderUtils.js";
|
|
4
|
+
import type { ApiContractShape, ApiOutput } from "./LambderApiContract.js";
|
|
4
5
|
|
|
5
6
|
type MethodType<T, M extends keyof T> = T[M] extends (...args: any[]) => any ? T[M] : never;
|
|
6
7
|
|
|
7
|
-
interface DieResolverMethods {
|
|
8
|
-
raw: MethodType<LambderResponseBuilder
|
|
9
|
-
json: MethodType<LambderResponseBuilder
|
|
10
|
-
xml: MethodType<LambderResponseBuilder
|
|
11
|
-
html: MethodType<LambderResponseBuilder
|
|
12
|
-
status301: MethodType<LambderResponseBuilder
|
|
13
|
-
status404: MethodType<LambderResponseBuilder
|
|
14
|
-
cors: MethodType<LambderResponseBuilder
|
|
15
|
-
fileBase64: MethodType<LambderResponseBuilder
|
|
16
|
-
file: MethodType<LambderResponseBuilder
|
|
17
|
-
ejsFile: MethodType<LambderResponseBuilder
|
|
18
|
-
ejsTemplate: MethodType<LambderResponseBuilder
|
|
19
|
-
api:
|
|
8
|
+
interface DieResolverMethods<TContract extends ApiContractShape, TApiName extends keyof TContract & string> {
|
|
9
|
+
raw: MethodType<LambderResponseBuilder<TContract>, 'raw'>;
|
|
10
|
+
json: MethodType<LambderResponseBuilder<TContract>, 'json'>;
|
|
11
|
+
xml: MethodType<LambderResponseBuilder<TContract>, 'xml'>;
|
|
12
|
+
html: MethodType<LambderResponseBuilder<TContract>, 'html'>;
|
|
13
|
+
status301: MethodType<LambderResponseBuilder<TContract>, 'status301'>;
|
|
14
|
+
status404: MethodType<LambderResponseBuilder<TContract>, 'status404'>;
|
|
15
|
+
cors: MethodType<LambderResponseBuilder<TContract>, 'cors'>;
|
|
16
|
+
fileBase64: MethodType<LambderResponseBuilder<TContract>, 'fileBase64'>;
|
|
17
|
+
file: MethodType<LambderResponseBuilder<TContract>, 'file'>;
|
|
18
|
+
ejsFile: MethodType<LambderResponseBuilder<TContract>, 'ejsFile'>;
|
|
19
|
+
ejsTemplate: MethodType<LambderResponseBuilder<TContract>, 'ejsTemplate'>;
|
|
20
|
+
api: (
|
|
21
|
+
payload: ApiOutput<TContract, TApiName> | null,
|
|
22
|
+
config?: Parameters<LambderResponseBuilder<TContract>['api']>[1],
|
|
23
|
+
headers?: Parameters<LambderResponseBuilder<TContract>['api']>[2]
|
|
24
|
+
) => LambderResolverResponse;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
|
-
export default class LambderResolver
|
|
27
|
+
export default class LambderResolver<
|
|
28
|
+
TContract extends ApiContractShape = any,
|
|
29
|
+
TApiName extends keyof TContract & string = any
|
|
30
|
+
> extends LambderResponseBuilder<TContract> {
|
|
23
31
|
public resolve: (response: LambderResolverResponse) => void;
|
|
24
32
|
public reject: (err: Error) => void;
|
|
25
|
-
public die: DieResolverMethods
|
|
33
|
+
public die: DieResolverMethods<TContract, TApiName>;
|
|
26
34
|
|
|
27
35
|
constructor(
|
|
28
36
|
{ isCorsEnabled, publicPath, apiVersion, lambderUtils, ctx, resolve, reject }:
|
|
@@ -56,6 +64,15 @@ export default class LambderResolver extends LambderResponseBuilder {
|
|
|
56
64
|
};
|
|
57
65
|
}
|
|
58
66
|
|
|
67
|
+
// Override api method with proper typing
|
|
68
|
+
api(
|
|
69
|
+
payload: ApiOutput<TContract, TApiName> | null,
|
|
70
|
+
config?: Parameters<LambderResponseBuilder<TContract>['api']>[1],
|
|
71
|
+
headers?: Parameters<LambderResponseBuilder<TContract>['api']>[2]
|
|
72
|
+
): LambderResolverResponse {
|
|
73
|
+
return super.api(payload, config, headers);
|
|
74
|
+
}
|
|
75
|
+
|
|
59
76
|
private autoResolve<
|
|
60
77
|
T extends (...args: any[]) => LambderResolverResponse
|
|
61
78
|
>(method: T): (...funcArgs: Parameters<T>) => LambderResolverResponse {
|
|
@@ -4,6 +4,7 @@ import ejs from "ejs";
|
|
|
4
4
|
import mimeTypeResolver from "mime-types";
|
|
5
5
|
import LambderUtils from "./LambderUtils.js";
|
|
6
6
|
import { LambderRenderContext } from "./Lambder.js";
|
|
7
|
+
import type { ApiContractShape } from "./LambderApiContract.js";
|
|
7
8
|
|
|
8
9
|
const convertToMultiHeader = (
|
|
9
10
|
headers: Record<string, string|string[]> | undefined
|
|
@@ -38,7 +39,7 @@ export type LambderApiResponse<T> = LambderApiResponseConfig & {
|
|
|
38
39
|
payload?: T | null;
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
export default class LambderResponseBuilder {
|
|
42
|
+
export default class LambderResponseBuilder<TContract extends ApiContractShape = any> {
|
|
42
43
|
private isCorsEnabled: boolean;
|
|
43
44
|
private publicPath: string;
|
|
44
45
|
private apiVersion: string|null;
|
|
@@ -48,6 +48,15 @@ export default class LambderSessionController {
|
|
|
48
48
|
return this.ctx.session;
|
|
49
49
|
};
|
|
50
50
|
|
|
51
|
+
async regenerateSession (): Promise<LambderSessionContext> {
|
|
52
|
+
if(!this.ctx.session) throw new Error("Session not found.");
|
|
53
|
+
const newSession = await this.lambderSessionManager.regenerateSession(this.ctx.session);
|
|
54
|
+
this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: `${this.sessionTokenCookieKey}=${newSession.sessionToken}; Expires=${new Date(newSession.expiresAt * 1000).toUTCString()}; Path=/; HttpOnly; SameSite=Lax; Secure` });
|
|
55
|
+
this.ctx._otherInternal.addHeaderFnAccumulator.push({ key: "Set-Cookie", value: `${this.sessionCsrfCookieKey}=${newSession.csrfToken}; Expires=${new Date(newSession.expiresAt * 1000).toUTCString()}; Path=/; SameSite=Lax; Secure` });
|
|
56
|
+
this.ctx.session = newSession;
|
|
57
|
+
return this.ctx.session;
|
|
58
|
+
};
|
|
59
|
+
|
|
51
60
|
async fetchSession (): Promise<LambderSessionContext>{
|
|
52
61
|
if(!this.areRequestSessionTokensValid()){ throw new Error("Session tokens are invalid"); }
|
|
53
62
|
|