lambder 1.0.147 → 2.0.2
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 +355 -410
- package/dist/Lambder.d.ts +47 -21
- package/dist/Lambder.js +79 -24
- package/dist/LambderApiContract.d.ts +10 -42
- package/dist/LambderApiContract.js +2 -22
- package/dist/LambderCaller.js +2 -2
- package/dist/LambderMSW.js +0 -4
- package/dist/LambderResolver.d.ts +16 -17
- package/dist/LambderResponseBuilder.d.ts +2 -3
- package/dist/LambderResponseBuilder.js +1 -3
- package/dist/LambderUtils.js +1 -3
- package/dist/index.d.ts +1 -1
- package/docs/LAMBDER_MSW.md +6 -6
- package/docs/TYPE_SAFE_QUICK_START.md +54 -177
- package/examples/msw-testing-example.ts +36 -33
- package/examples/secure-session-example.ts +50 -34
- package/examples/zod-chained-api-example.ts +63 -0
- package/package.json +3 -2
- package/src/Lambder.ts +124 -83
- package/src/LambderApiContract.ts +7 -50
- package/src/LambderCaller.ts +2 -2
- package/src/LambderMSW.ts +0 -7
- package/src/LambderResolver.ts +21 -24
- package/src/LambderResponseBuilder.ts +4 -7
- package/src/LambderUtils.ts +1 -3
- package/src/index.ts +0 -3
- package/tests/UNTESTED_FEATURES.md +263 -0
- package/tests/error-handling.test.ts +585 -0
- package/tests/hooks.test.ts +561 -0
- package/tests/output-type-runtime.test.ts +80 -64
- package/tests/routes.test.ts +542 -0
- package/tests/session.test.ts +38 -24
- package/tests/type-safety.test.ts +147 -97
- package/tests/use-plugin.test.ts +437 -0
- package/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +0 -90
- package/examples/output-type-enforcement-example.ts +0 -218
- package/examples/simplified-typed-api-example.ts +0 -365
- package/examples/test-output-type-enforcement.ts +0 -101
- package/test-type-enforcement.ts +0 -111
package/src/Lambder.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import cookieParser from "cookie";
|
|
2
2
|
import { match } from "path-to-regexp";
|
|
3
|
+
import { z } from "zod";
|
|
3
4
|
|
|
4
5
|
import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
|
|
5
6
|
import LambderResolver from "./LambderResolver.js";
|
|
@@ -7,7 +8,7 @@ import LambderResponseBuilder, { LambderResolverResponse } from "./LambderRespon
|
|
|
7
8
|
import LambderUtils from "./LambderUtils.js";
|
|
8
9
|
import LambderSessionManager, { type LambderSessionContext } from "./LambderSessionManager.js";
|
|
9
10
|
import LambderSessionController from "./LambderSessionController.js";
|
|
10
|
-
import type {
|
|
11
|
+
import type { MergeContract } from "./LambderApiContract.js";
|
|
11
12
|
|
|
12
13
|
type Path = `/${string}`;
|
|
13
14
|
|
|
@@ -38,8 +39,6 @@ export type LambderSessionRenderContext<
|
|
|
38
39
|
TApiPayload = any, SessionData = any
|
|
39
40
|
> = Omit<LambderRenderContext<TApiPayload>, 'session'> & { session: LambderSessionContext<SessionData> };
|
|
40
41
|
|
|
41
|
-
type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
|
|
42
|
-
|
|
43
42
|
type ConditionFunction = (ctx: LambderRenderContext<any>) => boolean;
|
|
44
43
|
type ActionFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
45
44
|
type SessionActionFunction<SessionData = any> = (ctx: LambderSessionRenderContext<any, SessionData>, resolver: LambderResolver) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
@@ -105,14 +104,40 @@ export const createContext = (
|
|
|
105
104
|
};
|
|
106
105
|
}
|
|
107
106
|
|
|
108
|
-
|
|
109
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Main Lambder class for building type-safe serverless APIs
|
|
109
|
+
*
|
|
110
|
+
* @typeParam TSessionData - Type of session data stored in DynamoDB
|
|
111
|
+
* @typeParam _TContract - @internal Accumulates API contract during chaining (do not pass manually)
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* ```typescript
|
|
115
|
+
* interface SessionData { userId: string; role: string; }
|
|
116
|
+
*
|
|
117
|
+
* const lambder = new Lambder<SessionData>({ apiPath: '/api' })
|
|
118
|
+
* .addApi('getUser', { input: z.object({...}), output: z.object({...}) }, handler)
|
|
119
|
+
* .addApi('createUser', { input: z.object({...}), output: z.object({...}) }, handler);
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
export default class Lambder<TSessionData = any, _TContract extends Record<string, any> = {}> {
|
|
110
123
|
public apiPath: string;
|
|
111
124
|
public apiVersion: null|string;
|
|
112
125
|
public isCorsEnabled: boolean = false;
|
|
113
126
|
public publicPath: string;
|
|
114
127
|
public ejsPath: string;
|
|
115
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Type property for extracting the API contract
|
|
131
|
+
* Use this to export your API types to the frontend
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* ```typescript
|
|
135
|
+
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
136
|
+
* export type AppContract = typeof lambder.ApiContractType;
|
|
137
|
+
* ```
|
|
138
|
+
*/
|
|
139
|
+
public readonly ApiContractType!: _TContract;
|
|
140
|
+
|
|
116
141
|
private actionList: ActionObject[];
|
|
117
142
|
private hookList: {
|
|
118
143
|
"beforeRender": { priority: number, hookFn: HookBeforeRenderFunction }[],
|
|
@@ -148,32 +173,38 @@ export default class Lambder<TContract extends ApiContractShape = any, TSessionD
|
|
|
148
173
|
this.utils = new LambderUtils({ ejsPath });
|
|
149
174
|
}
|
|
150
175
|
|
|
151
|
-
enableCors(isCorsEnabled: boolean){
|
|
176
|
+
enableCors(isCorsEnabled: boolean): this {
|
|
152
177
|
this.isCorsEnabled = isCorsEnabled;
|
|
178
|
+
return this;
|
|
153
179
|
}
|
|
154
180
|
|
|
155
181
|
enableDdbSession(
|
|
156
182
|
{ tableName, tableRegion, sessionSalt, enableSlidingExpiration }: { tableName: string; tableRegion: string; sessionSalt: string; enableSlidingExpiration?: boolean; },
|
|
157
183
|
{ partitionKey, sortKey }: { partitionKey: string, sortKey: string } = { partitionKey: "pk", sortKey: "sk" }
|
|
158
|
-
){
|
|
184
|
+
): this {
|
|
159
185
|
this.lambderSessionManager = new LambderSessionManager({
|
|
160
186
|
tableName, tableRegion, partitionKey, sortKey, sessionSalt, enableSlidingExpiration
|
|
161
187
|
});
|
|
188
|
+
return this;
|
|
162
189
|
}
|
|
163
190
|
|
|
164
|
-
setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string){
|
|
191
|
+
setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): this {
|
|
165
192
|
this.sessionTokenCookieKey = sessionTokenCookieKey;
|
|
166
193
|
this.sessionCsrfCookieKey = sessionCsrfCookieKey;
|
|
194
|
+
return this;
|
|
167
195
|
}
|
|
168
196
|
|
|
169
|
-
setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction){
|
|
197
|
+
setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction): this {
|
|
170
198
|
this.routeFallbackHandler = routeFallbackHandler;
|
|
199
|
+
return this;
|
|
171
200
|
}
|
|
172
|
-
setApiFallbackHandler(apiFallbackHandler: ApiFallbackHandlerFunction){
|
|
201
|
+
setApiFallbackHandler(apiFallbackHandler: ApiFallbackHandlerFunction): this {
|
|
173
202
|
this.apiFallbackHandler = apiFallbackHandler;
|
|
203
|
+
return this;
|
|
174
204
|
}
|
|
175
|
-
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction){
|
|
205
|
+
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction): this {
|
|
176
206
|
this.globalErrorHandler = globalErrorHandler;
|
|
207
|
+
return this;
|
|
177
208
|
}
|
|
178
209
|
private getPatternMatch(pattern: string, path: string): Record<string, any> {
|
|
179
210
|
const result = (match(pattern, { decode: decodeURIComponent }))(path);
|
|
@@ -199,15 +230,7 @@ export default class Lambder<TContract extends ApiContractShape = any, TSessionD
|
|
|
199
230
|
}
|
|
200
231
|
}
|
|
201
232
|
|
|
202
|
-
|
|
203
|
-
await moduleFn(this);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async importModule(moduleImport: Promise<{ default: LambderModuleFunction }>): Promise<void>{
|
|
207
|
-
await this.addModule((await moduleImport).default);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
addRoute(condition: Path|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
233
|
+
addRoute(condition: Path|ConditionFunction|RegExp, actionFn: ActionFunction): this {
|
|
211
234
|
this.actionList.push({
|
|
212
235
|
conditionFn: (ctx:LambderRenderContext<any>) => (
|
|
213
236
|
ctx.method === "GET" &&
|
|
@@ -221,15 +244,17 @@ export default class Lambder<TContract extends ApiContractShape = any, TSessionD
|
|
|
221
244
|
if(typeof condition === "string"){
|
|
222
245
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
223
246
|
}else if(condition?.constructor == RegExp){
|
|
224
|
-
|
|
247
|
+
const match = ctx.path.match(condition);
|
|
248
|
+
ctx.pathParams = match ? (match.groups || (match as unknown as Record<string, any>)) : {};
|
|
225
249
|
}
|
|
226
250
|
|
|
227
251
|
return await actionFn(ctx, resolver);
|
|
228
252
|
}
|
|
229
253
|
});
|
|
230
|
-
|
|
254
|
+
return this;
|
|
255
|
+
}
|
|
231
256
|
|
|
232
|
-
addSessionRoute(condition: Path|ConditionFunction|RegExp, actionFn: SessionActionFunction<TSessionData>):
|
|
257
|
+
addSessionRoute(condition: Path|ConditionFunction|RegExp, actionFn: SessionActionFunction<TSessionData>): this {
|
|
233
258
|
this.actionList.push({
|
|
234
259
|
conditionFn: (ctx:LambderRenderContext<any>) => (
|
|
235
260
|
ctx.method === "GET" &&
|
|
@@ -243,7 +268,8 @@ export default class Lambder<TContract extends ApiContractShape = any, TSessionD
|
|
|
243
268
|
if(typeof condition === "string"){
|
|
244
269
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
245
270
|
}else if(condition?.constructor == RegExp){
|
|
246
|
-
|
|
271
|
+
const match = ctx.path.match(condition);
|
|
272
|
+
ctx.pathParams = match ? (match.groups || (match as unknown as Record<string, any>)) : {};
|
|
247
273
|
}
|
|
248
274
|
|
|
249
275
|
const sessionCtx = ctx as unknown as LambderSessionRenderContext<any, TSessionData>;
|
|
@@ -253,76 +279,87 @@ export default class Lambder<TContract extends ApiContractShape = any, TSessionD
|
|
|
253
279
|
return await actionFn(sessionCtx, resolver);
|
|
254
280
|
}
|
|
255
281
|
});
|
|
256
|
-
|
|
282
|
+
return this;
|
|
283
|
+
}
|
|
257
284
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
285
|
+
// Plugin system
|
|
286
|
+
public use<_TNewContract extends Record<string, any>>(
|
|
287
|
+
plugin: (lambder: Lambder<TSessionData, _TContract>) => Lambder<TSessionData, _TNewContract>
|
|
288
|
+
): Lambder<TSessionData, _TNewContract> {
|
|
289
|
+
return plugin(this);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Typed API with Zod
|
|
293
|
+
public addApi<
|
|
294
|
+
TName extends string,
|
|
295
|
+
TInput extends z.ZodTypeAny,
|
|
296
|
+
TOutput extends z.ZodTypeAny
|
|
297
|
+
>(
|
|
298
|
+
name: TName,
|
|
299
|
+
schema: { input: TInput, output: TOutput },
|
|
300
|
+
handler: (
|
|
301
|
+
ctx: LambderRenderContext<z.infer<TInput>>,
|
|
302
|
+
resolver: LambderResolver<z.infer<TOutput>>
|
|
303
|
+
) => LambderResolverResponse | Promise<LambderResolverResponse>
|
|
304
|
+
): Lambder<TSessionData, MergeContract<_TContract, TName, z.infer<TInput>, z.infer<TOutput>>> {
|
|
278
305
|
this.actionList.push({
|
|
279
|
-
conditionFn: (ctx:LambderRenderContext<any>) =>
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
306
|
+
conditionFn: (ctx:LambderRenderContext<any>) => ctx.apiName === name,
|
|
307
|
+
actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
|
|
308
|
+
// Validate Input
|
|
309
|
+
const inputResult = schema.input.safeParse(ctx.apiPayload);
|
|
310
|
+
if (!inputResult.success) {
|
|
311
|
+
return resolver.raw({
|
|
312
|
+
statusCode: 400,
|
|
313
|
+
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
314
|
+
multiValueHeaders: { "Content-Type": ["application/json"] }
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Run Handler with validated data
|
|
319
|
+
ctx.apiPayload = inputResult.data;
|
|
320
|
+
return await handler(ctx, resolver);
|
|
321
|
+
},
|
|
287
322
|
});
|
|
288
|
-
|
|
323
|
+
return this as any;
|
|
324
|
+
}
|
|
289
325
|
|
|
290
|
-
//
|
|
291
|
-
addSessionApi
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
ctx: LambderSessionRenderContext<
|
|
300
|
-
resolver: LambderResolver<
|
|
301
|
-
) => LambderResolverResponse|Promise<LambderResolverResponse>
|
|
302
|
-
):
|
|
303
|
-
// Overload for untyped session API with string (backward compatibility, must be last)
|
|
304
|
-
addSessionApi(
|
|
305
|
-
apiName: string,
|
|
306
|
-
actionFn: SessionActionFunction<TSessionData>
|
|
307
|
-
):void;
|
|
308
|
-
// Implementation
|
|
309
|
-
addSessionApi(apiName: string|ConditionFunction|RegExp, actionFn: SessionActionFunction<TSessionData>):void{
|
|
326
|
+
// Typed Session API with Zod
|
|
327
|
+
public addSessionApi<
|
|
328
|
+
TName extends string,
|
|
329
|
+
TInput extends z.ZodTypeAny,
|
|
330
|
+
TOutput extends z.ZodTypeAny
|
|
331
|
+
>(
|
|
332
|
+
name: TName,
|
|
333
|
+
schema: { input: TInput, output: TOutput },
|
|
334
|
+
handler: (
|
|
335
|
+
ctx: LambderSessionRenderContext<z.infer<TInput>, TSessionData>,
|
|
336
|
+
resolver: LambderResolver<z.infer<TOutput>>
|
|
337
|
+
) => LambderResolverResponse | Promise<LambderResolverResponse>
|
|
338
|
+
): Lambder<TSessionData, MergeContract<_TContract, TName, z.infer<TInput>, z.infer<TOutput>>> {
|
|
310
339
|
this.actionList.push({
|
|
311
|
-
conditionFn: (ctx:LambderRenderContext<any>) =>
|
|
312
|
-
!!ctx.apiName && (
|
|
313
|
-
(typeof apiName === "string" && ctx.apiName === apiName) ||
|
|
314
|
-
(typeof apiName === "function" && apiName(ctx)) ||
|
|
315
|
-
(apiName?.constructor == RegExp && apiName.test(ctx.apiName))
|
|
316
|
-
)
|
|
317
|
-
),
|
|
340
|
+
conditionFn: (ctx:LambderRenderContext<any>) => ctx.apiName === name,
|
|
318
341
|
actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
|
|
319
342
|
const sessionCtx = ctx as unknown as LambderSessionRenderContext<any, TSessionData>;
|
|
320
343
|
await this.getSessionController(ctx).fetchSession();
|
|
321
344
|
if(!sessionCtx.session){ throw new Error("Session not found."); }
|
|
322
|
-
|
|
345
|
+
|
|
346
|
+
// Validate Input
|
|
347
|
+
const inputResult = schema.input.safeParse(ctx.apiPayload);
|
|
348
|
+
if (!inputResult.success) {
|
|
349
|
+
return resolver.raw({
|
|
350
|
+
statusCode: 400,
|
|
351
|
+
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
352
|
+
multiValueHeaders: { "Content-Type": ["application/json"] }
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Run Handler with validated data
|
|
357
|
+
ctx.apiPayload = inputResult.data;
|
|
358
|
+
return await handler(sessionCtx, resolver);
|
|
323
359
|
}
|
|
324
360
|
});
|
|
325
|
-
|
|
361
|
+
return this as any;
|
|
362
|
+
}
|
|
326
363
|
|
|
327
364
|
async addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
|
|
328
365
|
async addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
|
|
@@ -375,6 +412,10 @@ export default class Lambder<TContract extends ApiContractShape = any, TSessionD
|
|
|
375
412
|
});
|
|
376
413
|
};
|
|
377
414
|
|
|
415
|
+
getHandler() {
|
|
416
|
+
return (event: APIGatewayProxyEvent, context: Context) => this.render(event, context);
|
|
417
|
+
}
|
|
418
|
+
|
|
378
419
|
async render(
|
|
379
420
|
event: APIGatewayProxyEvent,
|
|
380
421
|
lambdaContext: Context
|
|
@@ -1,63 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lambder API Contract System
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* across frontend and backend with no runtime overhead.
|
|
6
|
-
*
|
|
7
|
-
* Example:
|
|
8
|
-
*
|
|
9
|
-
* export type MyApiContract = {
|
|
10
|
-
* getUserById: { input: { userId: string }, output: User },
|
|
11
|
-
* createUser: { input: CreateUserInput, output: User },
|
|
12
|
-
* listUsers: { input: void, output: User[] }
|
|
13
|
-
* }
|
|
14
|
-
*
|
|
15
|
-
* Frontend:
|
|
16
|
-
* const caller = new LambderCaller<MyApiContract>({ ... });
|
|
17
|
-
* const user = await caller.api('getUserById', { userId: '123' }); // typed!
|
|
18
|
-
*
|
|
19
|
-
* Backend:
|
|
20
|
-
* const lambder = new Lambder<MyApiContract>({ ... });
|
|
21
|
-
* lambder.addApi('getUserById', async (ctx, resolver) => {
|
|
22
|
-
* // ctx.apiPayload is typed as { userId: string }
|
|
23
|
-
* return resolver.api(user); // user is typed as User
|
|
24
|
-
* });
|
|
4
|
+
* Contracts are built via method chaining and inferred using typeof lambder.ApiContractType
|
|
25
5
|
*/
|
|
26
6
|
|
|
27
7
|
/**
|
|
28
|
-
* Base
|
|
29
|
-
*
|
|
30
|
-
* Use this as a constraint when defining your API contract:
|
|
31
|
-
*
|
|
32
|
-
* export type MyApiContract = {
|
|
33
|
-
* echo: { input: { message: string }, output: { echo: string } }
|
|
34
|
-
* } satisfies ApiContract;
|
|
35
|
-
*
|
|
36
|
-
* Or for backward compatibility without satisfies:
|
|
37
|
-
*
|
|
38
|
-
* export type MyApiContract = ApiContract & {
|
|
39
|
-
* echo: { input: { message: string }, output: { echo: string } }
|
|
40
|
-
* }
|
|
8
|
+
* Base shape for API contracts - used by LambderCaller and LambderMSW
|
|
41
9
|
*/
|
|
42
10
|
export type ApiContractShape = Record<string, {
|
|
43
11
|
input: any;
|
|
44
12
|
output: any;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export type ApiContract<T extends ApiContractShape> = T;
|
|
13
|
+
}>;
|
|
48
14
|
|
|
49
15
|
/**
|
|
50
|
-
*
|
|
16
|
+
* Helper type for merging new API into existing contract during chaining
|
|
51
17
|
*/
|
|
52
|
-
export type
|
|
53
|
-
|
|
54
|
-
TApiName extends keyof TContract
|
|
55
|
-
> = TContract[TApiName]['input'];
|
|
18
|
+
export type MergeContract<Old, Name extends string, In, Out> =
|
|
19
|
+
Old & { [K in Name]: { input: In, output: Out } };
|
|
56
20
|
|
|
57
|
-
/**
|
|
58
|
-
* Extract output type from contract for a specific API
|
|
59
|
-
*/
|
|
60
|
-
export type ApiOutput<
|
|
61
|
-
TContract extends ApiContractShape,
|
|
62
|
-
TApiName extends keyof TContract
|
|
63
|
-
> = TContract[TApiName]['output'];
|
package/src/LambderCaller.ts
CHANGED
|
@@ -143,9 +143,9 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
|
|
|
143
143
|
activeFetchList: this.fetchTrackerList.filter(v=>!v.done),
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
|
-
if(data && data.logList
|
|
146
|
+
if(data && data.logList){
|
|
147
147
|
for(const record of data.logList){
|
|
148
|
-
|
|
148
|
+
// Log to API response for debugging
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
if(data && data.versionExpired){
|
package/src/LambderMSW.ts
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import { createRequire } from 'module';
|
|
2
1
|
import type { ApiContractShape } from './LambderApiContract';
|
|
3
2
|
import type { LambderApiResponse } from './LambderResponseBuilder';
|
|
4
3
|
|
|
5
|
-
const require = createRequire(import.meta.url);
|
|
6
|
-
|
|
7
4
|
// MSW types - these will be resolved at runtime when msw is installed
|
|
8
5
|
type RequestHandler = any;
|
|
9
6
|
|
|
@@ -80,8 +77,6 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
|
|
|
80
77
|
if (!body || typeof body.apiName !== 'string') { return; }
|
|
81
78
|
if (body.apiName !== apiName) { return; }
|
|
82
79
|
|
|
83
|
-
console.log("LambderMSW called for:", body.apiName);
|
|
84
|
-
|
|
85
80
|
try {
|
|
86
81
|
// Add artificial delay if specified
|
|
87
82
|
if (options?.delay) {
|
|
@@ -91,8 +86,6 @@ export default class LambderMSW<TContract extends ApiContractShape = any> {
|
|
|
91
86
|
// Call the handler with the payload from the request
|
|
92
87
|
const payload = await handler(body.payload as TContract[TApiName]['input']);
|
|
93
88
|
|
|
94
|
-
console.log("Matched! Returning payload for:", apiName);
|
|
95
|
-
|
|
96
89
|
const response: MockApiResponse<TContract[TApiName]['output']> = {
|
|
97
90
|
apiVersion: this.apiVersion,
|
|
98
91
|
payload,
|
package/src/LambderResolver.ts
CHANGED
|
@@ -1,36 +1,32 @@
|
|
|
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";
|
|
5
4
|
|
|
6
5
|
type MethodType<T, M extends keyof T> = T[M] extends (...args: any[]) => any ? T[M] : never;
|
|
7
6
|
|
|
8
|
-
interface DieResolverMethods<
|
|
9
|
-
raw: MethodType<LambderResponseBuilder
|
|
10
|
-
json: MethodType<LambderResponseBuilder
|
|
11
|
-
xml: MethodType<LambderResponseBuilder
|
|
12
|
-
html: MethodType<LambderResponseBuilder
|
|
13
|
-
status301: MethodType<LambderResponseBuilder
|
|
14
|
-
status404: MethodType<LambderResponseBuilder
|
|
15
|
-
cors: MethodType<LambderResponseBuilder
|
|
16
|
-
fileBase64: MethodType<LambderResponseBuilder
|
|
17
|
-
file: MethodType<LambderResponseBuilder
|
|
18
|
-
ejsFile: MethodType<LambderResponseBuilder
|
|
19
|
-
ejsTemplate: MethodType<LambderResponseBuilder
|
|
7
|
+
interface DieResolverMethods<TOutput> {
|
|
8
|
+
raw: MethodType<LambderResponseBuilder, 'raw'>;
|
|
9
|
+
json: MethodType<LambderResponseBuilder, 'json'>;
|
|
10
|
+
xml: MethodType<LambderResponseBuilder, 'xml'>;
|
|
11
|
+
html: MethodType<LambderResponseBuilder, 'html'>;
|
|
12
|
+
status301: MethodType<LambderResponseBuilder, 'status301'>;
|
|
13
|
+
status404: MethodType<LambderResponseBuilder, 'status404'>;
|
|
14
|
+
cors: MethodType<LambderResponseBuilder, 'cors'>;
|
|
15
|
+
fileBase64: MethodType<LambderResponseBuilder, 'fileBase64'>;
|
|
16
|
+
file: MethodType<LambderResponseBuilder, 'file'>;
|
|
17
|
+
ejsFile: MethodType<LambderResponseBuilder, 'ejsFile'>;
|
|
18
|
+
ejsTemplate: MethodType<LambderResponseBuilder, 'ejsTemplate'>;
|
|
20
19
|
api: (
|
|
21
|
-
payload:
|
|
22
|
-
config?: Parameters<LambderResponseBuilder
|
|
23
|
-
headers?: Parameters<LambderResponseBuilder
|
|
20
|
+
payload: TOutput | null,
|
|
21
|
+
config?: Parameters<LambderResponseBuilder['api']>[1],
|
|
22
|
+
headers?: Parameters<LambderResponseBuilder['api']>[2]
|
|
24
23
|
) => LambderResolverResponse;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
export default class LambderResolver<
|
|
28
|
-
TContract extends ApiContractShape = any,
|
|
29
|
-
TApiName extends keyof TContract & string = any
|
|
30
|
-
> extends LambderResponseBuilder<TContract> {
|
|
26
|
+
export default class LambderResolver<TOutput = any> extends LambderResponseBuilder<TOutput> {
|
|
31
27
|
public resolve: (response: LambderResolverResponse) => void;
|
|
32
28
|
public reject: (err: Error) => void;
|
|
33
|
-
public die: DieResolverMethods<
|
|
29
|
+
public die: DieResolverMethods<TOutput>;
|
|
34
30
|
|
|
35
31
|
constructor(
|
|
36
32
|
{ isCorsEnabled, publicPath, apiVersion, lambderUtils, ctx, resolve, reject }:
|
|
@@ -66,13 +62,14 @@ export default class LambderResolver<
|
|
|
66
62
|
|
|
67
63
|
// Override api method with proper typing
|
|
68
64
|
api(
|
|
69
|
-
payload:
|
|
70
|
-
config?: Parameters<LambderResponseBuilder
|
|
71
|
-
headers?: Parameters<LambderResponseBuilder
|
|
65
|
+
payload: TOutput | null,
|
|
66
|
+
config?: Parameters<LambderResponseBuilder['api']>[1],
|
|
67
|
+
headers?: Parameters<LambderResponseBuilder['api']>[2]
|
|
72
68
|
): LambderResolverResponse {
|
|
73
69
|
return super.api(payload, config, headers);
|
|
74
70
|
}
|
|
75
71
|
|
|
72
|
+
|
|
76
73
|
private autoResolve<
|
|
77
74
|
T extends (...args: any[]) => LambderResolverResponse
|
|
78
75
|
>(method: T): (...funcArgs: Parameters<T>) => LambderResolverResponse {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import mimeTypeResolver from "mime-types";
|
|
2
2
|
import LambderUtils from "./LambderUtils.js";
|
|
3
3
|
import { LambderRenderContext } from "./Lambder.js";
|
|
4
|
-
import type { ApiContractShape } from "./LambderApiContract.js";
|
|
5
4
|
import { getFS, getPath } from "./node-polyfills.js";
|
|
6
5
|
|
|
7
6
|
const convertToMultiHeader = (
|
|
@@ -37,7 +36,7 @@ export type LambderApiResponse<T> = LambderApiResponseConfig & {
|
|
|
37
36
|
payload?: T | null;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
export default class LambderResponseBuilder<
|
|
39
|
+
export default class LambderResponseBuilder<TResponse = any> {
|
|
41
40
|
private isCorsEnabled: boolean;
|
|
42
41
|
private publicPath: string;
|
|
43
42
|
private apiVersion: string|null;
|
|
@@ -71,7 +70,6 @@ export default class LambderResponseBuilder<TContract extends ApiContractShape =
|
|
|
71
70
|
|
|
72
71
|
const publicPath = path.resolve(this.publicPath);
|
|
73
72
|
const absolutePath = path.resolve(publicPath, filePath);
|
|
74
|
-
console.log("readPublicFileSync", { filePath, publicPath, absolutePath });
|
|
75
73
|
if(!absolutePath.startsWith(publicPath)){ return "forbidden-public-path"; }
|
|
76
74
|
return await fs.promises.readFile(absolutePath);
|
|
77
75
|
};
|
|
@@ -84,7 +82,6 @@ export default class LambderResponseBuilder<TContract extends ApiContractShape =
|
|
|
84
82
|
|
|
85
83
|
const publicPath = path.resolve(this.publicPath);
|
|
86
84
|
const absolutePath = path.resolve(publicPath, filePath);
|
|
87
|
-
console.log("checkPublicFileExist", { filePath, publicPath, absolutePath });
|
|
88
85
|
if(!absolutePath.startsWith(publicPath)){ return false; }
|
|
89
86
|
try {
|
|
90
87
|
const stat = await fs.promises.stat(absolutePath);
|
|
@@ -224,7 +221,7 @@ export default class LambderResponseBuilder<TContract extends ApiContractShape =
|
|
|
224
221
|
const mimeType = mimeTypeResolver.lookup(filePath);
|
|
225
222
|
const body = await this.readPublicFileSync(filePath);
|
|
226
223
|
if (body === "forbidden-public-path") {
|
|
227
|
-
throw
|
|
224
|
+
throw new Error("Forbidden public path: " + filePath);
|
|
228
225
|
}
|
|
229
226
|
const bodyBuffer: Buffer = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
230
227
|
const bodyBase64 = bodyBuffer.toString("base64");
|
|
@@ -262,8 +259,8 @@ export default class LambderResponseBuilder<TContract extends ApiContractShape =
|
|
|
262
259
|
});
|
|
263
260
|
};
|
|
264
261
|
|
|
265
|
-
api
|
|
266
|
-
payload:
|
|
262
|
+
api(
|
|
263
|
+
payload: TResponse | null,
|
|
267
264
|
{
|
|
268
265
|
versionExpired, sessionExpired, notAuthorized,
|
|
269
266
|
message, errorMessage, logList,
|
package/src/LambderUtils.ts
CHANGED
|
@@ -22,9 +22,8 @@ export default class LambderUtils {
|
|
|
22
22
|
if(!this.ejsPath){ return "EJS PATH NOT SET!"; }
|
|
23
23
|
const ejsPath = path.resolve(this.ejsPath);
|
|
24
24
|
const absolutePath = path.resolve(ejsPath, filePath);
|
|
25
|
-
console.log("readEjsFileSync", { filePath, ejsPath, absolutePath });
|
|
26
25
|
if(!absolutePath.startsWith(ejsPath)){ return "forbidden-ejs-path"; }
|
|
27
|
-
return
|
|
26
|
+
return await fs.promises.readFile(absolutePath, 'utf-8');
|
|
28
27
|
};
|
|
29
28
|
|
|
30
29
|
private async checkEjsFileExist(filePath: string){
|
|
@@ -37,7 +36,6 @@ export default class LambderUtils {
|
|
|
37
36
|
if(!this.ejsPath){ return "EJS PATH NOT SET!"; }
|
|
38
37
|
const ejsPath = path.resolve(this.ejsPath);
|
|
39
38
|
const absolutePath = path.resolve(ejsPath, filePath);
|
|
40
|
-
console.log("checkEjsFileExist", { filePath, ejsPath, absolutePath });
|
|
41
39
|
if(!absolutePath.startsWith(ejsPath)){ return false; }
|
|
42
40
|
try {
|
|
43
41
|
const stat = await fs.promises.stat(absolutePath);
|