lambder 2.0.4 → 2.0.7
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 +3 -3
- package/dist/Lambder.d.ts +7 -36
- package/dist/Lambder.js +14 -46
- package/dist/LambderApiContract.d.ts +1 -1
- package/dist/LambderApiContract.js +1 -1
- package/dist/LambderContext.d.ts +34 -0
- package/dist/LambderContext.js +44 -0
- package/dist/LambderResolver.d.ts +1 -1
- package/dist/LambderResponseBuilder.d.ts +1 -1
- package/dist/LambderResponseBuilder.js +1 -1
- package/dist/LambderSessionController.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/docs/LAMBDER_MSW.md +25 -16
- package/docs/TYPE_SAFE_QUICK_START.md +1 -1
- package/examples/msw-testing-example.ts +1 -1
- package/examples/zod-chained-api-example.ts +2 -2
- package/package.json +1 -1
- package/src/Lambder.ts +17 -81
- package/src/LambderApiContract.ts +1 -1
- package/src/LambderContext.ts +78 -0
- package/src/LambderResolver.ts +1 -1
- package/src/LambderResponseBuilder.ts +2 -2
- package/src/LambderSessionController.ts +1 -1
- package/src/index.ts +4 -0
- package/tests/type-safety.test.ts +2 -2
- package/tests/use-plugin.test.ts +13 -11
package/Readme.md
CHANGED
|
@@ -70,7 +70,7 @@ const app = lambder
|
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
// Export the inferred contract for the frontend
|
|
73
|
-
export type AppContract = typeof lambder.
|
|
73
|
+
export type AppContract = typeof lambder.ApiContract;
|
|
74
74
|
|
|
75
75
|
// Export the handler
|
|
76
76
|
export const handler = lambder.getHandler();
|
|
@@ -196,7 +196,7 @@ import { userApi } from "./user-api";
|
|
|
196
196
|
const lambder = new Lambder()
|
|
197
197
|
.use(userApi);
|
|
198
198
|
|
|
199
|
-
export type AppContract = typeof lambder.
|
|
199
|
+
export type AppContract = typeof lambder.ApiContract;
|
|
200
200
|
```
|
|
201
201
|
|
|
202
202
|
|
|
@@ -457,7 +457,7 @@ const lambder = new Lambder({ apiPath: '/api' })
|
|
|
457
457
|
return res.api({ id: ctx.apiPayload.userId, name: "John" });
|
|
458
458
|
});
|
|
459
459
|
|
|
460
|
-
export type AppContract = typeof lambder.
|
|
460
|
+
export type AppContract = typeof lambder.ApiContract;
|
|
461
461
|
export const handler = lambder.getHandler();
|
|
462
462
|
```
|
|
463
463
|
|
package/dist/Lambder.d.ts
CHANGED
|
@@ -1,43 +1,12 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type { APIGatewayProxyEvent,
|
|
2
|
+
import type { APIGatewayProxyEvent, Context } from "aws-lambda";
|
|
3
3
|
import LambderResolver from "./LambderResolver.js";
|
|
4
4
|
import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
|
|
5
5
|
import LambderUtils from "./LambderUtils.js";
|
|
6
|
-
import { type LambderSessionContext } from "./LambderSessionManager.js";
|
|
7
6
|
import LambderSessionController from "./LambderSessionController.js";
|
|
8
7
|
import type { MergeContract } from "./LambderApiContract.js";
|
|
8
|
+
import { type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
|
|
9
9
|
type Path = `/${string}`;
|
|
10
|
-
export type LambderRenderContext<TApiPayload = any> = {
|
|
11
|
-
host: string;
|
|
12
|
-
path: string;
|
|
13
|
-
pathParams: Record<string, any> | null;
|
|
14
|
-
method: string;
|
|
15
|
-
get: Record<string, any>;
|
|
16
|
-
post: Record<string, any>;
|
|
17
|
-
cookie: Record<string, any>;
|
|
18
|
-
session: null;
|
|
19
|
-
apiName: string;
|
|
20
|
-
apiPayload: TApiPayload;
|
|
21
|
-
headers: APIGatewayProxyEventHeaders;
|
|
22
|
-
event: APIGatewayProxyEvent;
|
|
23
|
-
lambdaContext: Context;
|
|
24
|
-
_otherInternal: {
|
|
25
|
-
isApiCall: boolean;
|
|
26
|
-
requestVersion: string | null;
|
|
27
|
-
setHeaderFnAccumulator: {
|
|
28
|
-
key: string;
|
|
29
|
-
value: string | string[];
|
|
30
|
-
}[];
|
|
31
|
-
addHeaderFnAccumulator: {
|
|
32
|
-
key: string;
|
|
33
|
-
value: string;
|
|
34
|
-
}[];
|
|
35
|
-
logToApiResponseAccumulator: any[];
|
|
36
|
-
};
|
|
37
|
-
};
|
|
38
|
-
export type LambderSessionRenderContext<TApiPayload = any, SessionData = any> = Omit<LambderRenderContext<TApiPayload>, 'session'> & {
|
|
39
|
-
session: LambderSessionContext<SessionData>;
|
|
40
|
-
};
|
|
41
10
|
type ConditionFunction = (ctx: LambderRenderContext<any>) => boolean;
|
|
42
11
|
type ActionFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>;
|
|
43
12
|
type SessionActionFunction<SessionData = any> = (ctx: LambderSessionRenderContext<any, SessionData>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>;
|
|
@@ -48,7 +17,7 @@ type HookFallbackFunction = (ctx: LambderRenderContext<any>, resolver: LambderRe
|
|
|
48
17
|
type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext<any> | null, response: LambderResponseBuilder, logListToApiResponse?: any[]) => LambderResolverResponse | Promise<LambderResolverResponse>;
|
|
49
18
|
type RouteFallbackHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
50
19
|
type ApiFallbackHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
51
|
-
|
|
20
|
+
type ApiInputValidationErrorHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver, zodError: z.ZodError) => LambderResolverResponse | Promise<LambderResolverResponse>;
|
|
52
21
|
/**
|
|
53
22
|
* Main Lambder class for building type-safe serverless APIs
|
|
54
23
|
*
|
|
@@ -77,15 +46,16 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
77
46
|
* @example
|
|
78
47
|
* ```typescript
|
|
79
48
|
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
80
|
-
* export type AppContract = typeof lambder.
|
|
49
|
+
* export type AppContract = typeof lambder.ApiContract;
|
|
81
50
|
* ```
|
|
82
51
|
*/
|
|
83
|
-
readonly
|
|
52
|
+
readonly ApiContract: _TContract;
|
|
84
53
|
private actionList;
|
|
85
54
|
private hookList;
|
|
86
55
|
private globalErrorHandler;
|
|
87
56
|
private routeFallbackHandler;
|
|
88
57
|
private apiFallbackHandler;
|
|
58
|
+
private apiInputValidationErrorHandler;
|
|
89
59
|
utils: LambderUtils;
|
|
90
60
|
private lambderSessionManager?;
|
|
91
61
|
private sessionTokenCookieKey;
|
|
@@ -109,6 +79,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
109
79
|
setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): this;
|
|
110
80
|
setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction): this;
|
|
111
81
|
setApiFallbackHandler(apiFallbackHandler: ApiFallbackHandlerFunction): this;
|
|
82
|
+
setApiInputValidationErrorHandler(apiInputValidationErrorHandler: ApiInputValidationErrorHandlerFunction): this;
|
|
112
83
|
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction): this;
|
|
113
84
|
private getPatternMatch;
|
|
114
85
|
private testPatternMatch;
|
package/dist/Lambder.js
CHANGED
|
@@ -1,53 +1,10 @@
|
|
|
1
|
-
import cookieParser from "cookie";
|
|
2
1
|
import { match } from "path-to-regexp";
|
|
3
2
|
import LambderResolver from "./LambderResolver.js";
|
|
4
3
|
import LambderResponseBuilder from "./LambderResponseBuilder.js";
|
|
5
4
|
import LambderUtils from "./LambderUtils.js";
|
|
6
5
|
import LambderSessionManager from "./LambderSessionManager.js";
|
|
7
6
|
import LambderSessionController from "./LambderSessionController.js";
|
|
8
|
-
|
|
9
|
-
const host = event.headers.Host || event.headers.host || "";
|
|
10
|
-
const path = event.path;
|
|
11
|
-
const pathParams = null;
|
|
12
|
-
const get = event.queryStringParameters || {};
|
|
13
|
-
const method = event.httpMethod;
|
|
14
|
-
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
15
|
-
const headers = event.headers;
|
|
16
|
-
// Decode body for the post
|
|
17
|
-
let post = {};
|
|
18
|
-
try {
|
|
19
|
-
const decodedBody = event.isBase64Encoded ? (event.body ? Buffer.from(event.body, "base64").toString() : "{}") : (event.body || "{}");
|
|
20
|
-
try {
|
|
21
|
-
post = JSON.parse(decodedBody) || {};
|
|
22
|
-
}
|
|
23
|
-
catch (e) {
|
|
24
|
-
const params = new URLSearchParams(decodedBody);
|
|
25
|
-
post = {};
|
|
26
|
-
for (const [key, value] of params.entries()) {
|
|
27
|
-
post[key] = value;
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
catch (e) { }
|
|
32
|
-
// Parse api variables
|
|
33
|
-
const isApiCall = method === "POST" && apiPath && path === apiPath && post.apiName;
|
|
34
|
-
const apiName = isApiCall ? post.apiName : null;
|
|
35
|
-
const apiPayload = isApiCall ? post.payload : null;
|
|
36
|
-
const requestVersion = isApiCall ? post.version : null;
|
|
37
|
-
return {
|
|
38
|
-
host, path, pathParams, method,
|
|
39
|
-
get, post, cookie, event,
|
|
40
|
-
session: null,
|
|
41
|
-
apiName, apiPayload,
|
|
42
|
-
headers, lambdaContext,
|
|
43
|
-
_otherInternal: {
|
|
44
|
-
isApiCall, requestVersion,
|
|
45
|
-
setHeaderFnAccumulator: [],
|
|
46
|
-
addHeaderFnAccumulator: [],
|
|
47
|
-
logToApiResponseAccumulator: [],
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
};
|
|
7
|
+
import { createContext } from "./LambderContext.js";
|
|
51
8
|
/**
|
|
52
9
|
* Main Lambder class for building type-safe serverless APIs
|
|
53
10
|
*
|
|
@@ -76,15 +33,16 @@ export default class Lambder {
|
|
|
76
33
|
* @example
|
|
77
34
|
* ```typescript
|
|
78
35
|
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
79
|
-
* export type AppContract = typeof lambder.
|
|
36
|
+
* export type AppContract = typeof lambder.ApiContract;
|
|
80
37
|
* ```
|
|
81
38
|
*/
|
|
82
|
-
|
|
39
|
+
ApiContract;
|
|
83
40
|
actionList;
|
|
84
41
|
hookList;
|
|
85
42
|
globalErrorHandler = null;
|
|
86
43
|
routeFallbackHandler = null;
|
|
87
44
|
apiFallbackHandler = null;
|
|
45
|
+
apiInputValidationErrorHandler = null;
|
|
88
46
|
utils;
|
|
89
47
|
lambderSessionManager;
|
|
90
48
|
sessionTokenCookieKey = "LMDRSESSIONTKID";
|
|
@@ -125,6 +83,10 @@ export default class Lambder {
|
|
|
125
83
|
this.apiFallbackHandler = apiFallbackHandler;
|
|
126
84
|
return this;
|
|
127
85
|
}
|
|
86
|
+
setApiInputValidationErrorHandler(apiInputValidationErrorHandler) {
|
|
87
|
+
this.apiInputValidationErrorHandler = apiInputValidationErrorHandler;
|
|
88
|
+
return this;
|
|
89
|
+
}
|
|
128
90
|
setGlobalErrorHandler(globalErrorHandler) {
|
|
129
91
|
this.globalErrorHandler = globalErrorHandler;
|
|
130
92
|
return this;
|
|
@@ -211,6 +173,9 @@ export default class Lambder {
|
|
|
211
173
|
// Validate Input
|
|
212
174
|
const inputResult = schema.input.safeParse(ctx.apiPayload);
|
|
213
175
|
if (!inputResult.success) {
|
|
176
|
+
if (this.apiInputValidationErrorHandler) {
|
|
177
|
+
return await this.apiInputValidationErrorHandler(ctx, resolver, inputResult.error);
|
|
178
|
+
}
|
|
214
179
|
return resolver.raw({
|
|
215
180
|
statusCode: 400,
|
|
216
181
|
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
@@ -237,6 +202,9 @@ export default class Lambder {
|
|
|
237
202
|
// Validate Input
|
|
238
203
|
const inputResult = schema.input.safeParse(ctx.apiPayload);
|
|
239
204
|
if (!inputResult.success) {
|
|
205
|
+
if (this.apiInputValidationErrorHandler) {
|
|
206
|
+
return await this.apiInputValidationErrorHandler(ctx, resolver, inputResult.error);
|
|
207
|
+
}
|
|
240
208
|
return resolver.raw({
|
|
241
209
|
statusCode: 400,
|
|
242
210
|
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Lambder API Contract System
|
|
3
3
|
*
|
|
4
|
-
* Contracts are built via method chaining and inferred using typeof lambder.
|
|
4
|
+
* Contracts are built via method chaining and inferred using typeof lambder.ApiContract
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* Base shape for API contracts - used by LambderCaller and LambderMSW
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
|
|
2
|
+
import type { LambderSessionContext } from "./LambderSessionManager.js";
|
|
3
|
+
export type LambderRenderContext<TApiPayload = any> = {
|
|
4
|
+
host: string;
|
|
5
|
+
path: string;
|
|
6
|
+
pathParams: Record<string, any> | null;
|
|
7
|
+
method: string;
|
|
8
|
+
get: Record<string, any>;
|
|
9
|
+
post: Record<string, any>;
|
|
10
|
+
cookie: Record<string, any>;
|
|
11
|
+
session: null;
|
|
12
|
+
apiName: string;
|
|
13
|
+
apiPayload: TApiPayload;
|
|
14
|
+
headers: APIGatewayProxyEventHeaders;
|
|
15
|
+
event: APIGatewayProxyEvent;
|
|
16
|
+
lambdaContext: Context;
|
|
17
|
+
_otherInternal: {
|
|
18
|
+
isApiCall: boolean;
|
|
19
|
+
requestVersion: string | null;
|
|
20
|
+
setHeaderFnAccumulator: {
|
|
21
|
+
key: string;
|
|
22
|
+
value: string | string[];
|
|
23
|
+
}[];
|
|
24
|
+
addHeaderFnAccumulator: {
|
|
25
|
+
key: string;
|
|
26
|
+
value: string;
|
|
27
|
+
}[];
|
|
28
|
+
logToApiResponseAccumulator: any[];
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
export type LambderSessionRenderContext<TApiPayload = any, SessionData = any> = Omit<LambderRenderContext<TApiPayload>, 'session'> & {
|
|
32
|
+
session: LambderSessionContext<SessionData>;
|
|
33
|
+
};
|
|
34
|
+
export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext<any>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import cookieParser from "cookie";
|
|
2
|
+
export const createContext = (event, lambdaContext, apiPath) => {
|
|
3
|
+
const host = event.headers.Host || event.headers.host || "";
|
|
4
|
+
const path = event.path;
|
|
5
|
+
const pathParams = null;
|
|
6
|
+
const get = event.queryStringParameters || {};
|
|
7
|
+
const method = event.httpMethod;
|
|
8
|
+
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
9
|
+
const headers = event.headers;
|
|
10
|
+
// Decode body for the post
|
|
11
|
+
let post = {};
|
|
12
|
+
try {
|
|
13
|
+
const decodedBody = event.isBase64Encoded ? (event.body ? Buffer.from(event.body, "base64").toString() : "{}") : (event.body || "{}");
|
|
14
|
+
try {
|
|
15
|
+
post = JSON.parse(decodedBody) || {};
|
|
16
|
+
}
|
|
17
|
+
catch (e) {
|
|
18
|
+
const params = new URLSearchParams(decodedBody);
|
|
19
|
+
post = {};
|
|
20
|
+
for (const [key, value] of params.entries()) {
|
|
21
|
+
post[key] = value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
catch (e) { }
|
|
26
|
+
// Parse api variables
|
|
27
|
+
const isApiCall = method === "POST" && apiPath && path === apiPath && post.apiName;
|
|
28
|
+
const apiName = isApiCall ? post.apiName : null;
|
|
29
|
+
const apiPayload = isApiCall ? post.payload : null;
|
|
30
|
+
const requestVersion = isApiCall ? post.version : null;
|
|
31
|
+
return {
|
|
32
|
+
host, path, pathParams, method,
|
|
33
|
+
get, post, cookie, event,
|
|
34
|
+
session: null,
|
|
35
|
+
apiName, apiPayload,
|
|
36
|
+
headers, lambdaContext,
|
|
37
|
+
_otherInternal: {
|
|
38
|
+
isApiCall, requestVersion,
|
|
39
|
+
setHeaderFnAccumulator: [],
|
|
40
|
+
addHeaderFnAccumulator: [],
|
|
41
|
+
logToApiResponseAccumulator: [],
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LambderRenderContext } from "./
|
|
1
|
+
import type { LambderRenderContext } from "./LambderContext.js";
|
|
2
2
|
import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
|
|
3
3
|
import LambderUtils from "./LambderUtils.js";
|
|
4
4
|
type MethodType<T, M extends keyof T> = T[M] extends (...args: any[]) => any ? T[M] : never;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import LambderUtils from "./LambderUtils.js";
|
|
2
|
-
import { LambderRenderContext } from "./
|
|
2
|
+
import { LambderRenderContext } from "./LambderContext.js";
|
|
3
3
|
export type LambderResolverResponse = {
|
|
4
4
|
statusCode: number;
|
|
5
5
|
multiValueHeaders?: Record<string, string[]>;
|
|
@@ -68,7 +68,7 @@ export default class LambderResponseBuilder {
|
|
|
68
68
|
throw new Error(".setHeader function is not available within this hook");
|
|
69
69
|
else {
|
|
70
70
|
this.ctx._otherInternal.addHeaderFnAccumulator = this.ctx._otherInternal.addHeaderFnAccumulator
|
|
71
|
-
.filter(header => header.key !== key);
|
|
71
|
+
.filter((header) => header.key !== key);
|
|
72
72
|
this.ctx._otherInternal.setHeaderFnAccumulator.push({ key, value });
|
|
73
73
|
}
|
|
74
74
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LambderRenderContext, LambderSessionRenderContext } from "./
|
|
1
|
+
import { LambderRenderContext, LambderSessionRenderContext } from "./LambderContext.js";
|
|
2
2
|
import type LambderSessionManager from "./LambderSessionManager.js";
|
|
3
3
|
import type { LambderSessionContext } from "./LambderSessionManager.js";
|
|
4
4
|
export default class LambderSessionController<TSessionData = any> {
|
package/dist/index.d.ts
CHANGED
|
@@ -6,3 +6,5 @@ export { default as LambderResolver } from "./LambderResolver.js";
|
|
|
6
6
|
export { default as LambderSessionManager } from "./LambderSessionManager.js";
|
|
7
7
|
export { default as LambderMSW } from "./LambderMSW.js";
|
|
8
8
|
export { type ApiContractShape, } from "./LambderApiContract.js";
|
|
9
|
+
export type { LambderRenderContext, LambderSessionRenderContext } from "./LambderContext.js";
|
|
10
|
+
export { createContext } from "./LambderContext.js";
|
package/dist/index.js
CHANGED
|
@@ -5,3 +5,4 @@ export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
|
|
|
5
5
|
export { default as LambderResolver } from "./LambderResolver.js";
|
|
6
6
|
export { default as LambderSessionManager } from "./LambderSessionManager.js";
|
|
7
7
|
export { default as LambderMSW } from "./LambderMSW.js";
|
|
8
|
+
export { createContext } from "./LambderContext.js";
|
package/docs/LAMBDER_MSW.md
CHANGED
|
@@ -56,23 +56,32 @@ afterAll(() => server.close());
|
|
|
56
56
|
When using TypeScript API contracts, LambderMSW provides full type safety:
|
|
57
57
|
|
|
58
58
|
```typescript
|
|
59
|
-
//
|
|
60
|
-
import
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
59
|
+
// backend/index.ts (define your APIs with Lambder)
|
|
60
|
+
import { z } from 'zod';
|
|
61
|
+
import Lambder from 'lambder';
|
|
62
|
+
|
|
63
|
+
const lambder = new Lambder({ apiPath: '/secure' })
|
|
64
|
+
.addApi('getUserById', {
|
|
65
|
+
input: z.object({ userId: z.string() }),
|
|
66
|
+
output: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
67
|
+
}, async (ctx, resolver) => {
|
|
68
|
+
// Implementation...
|
|
69
|
+
return resolver.api({ id: ctx.apiPayload.userId, name: 'John', email: 'john@example.com' });
|
|
70
|
+
})
|
|
71
|
+
.addApi('createUser', {
|
|
72
|
+
input: z.object({ name: z.string(), email: z.string() }),
|
|
73
|
+
output: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
74
|
+
}, async (ctx, resolver) => {
|
|
75
|
+
// Implementation...
|
|
76
|
+
return resolver.api({ id: '123', ...ctx.apiPayload });
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Export the inferred contract type
|
|
80
|
+
export type MyApiContract = typeof lambder.ApiContract;
|
|
72
81
|
|
|
73
82
|
// test/setup.ts
|
|
74
83
|
import { LambderMSW } from 'lambder';
|
|
75
|
-
import type { MyApiContract } from '../
|
|
84
|
+
import type { MyApiContract } from '../backend';
|
|
76
85
|
|
|
77
86
|
const lambderMSW = new LambderMSW<MyApiContract>({
|
|
78
87
|
apiPath: '/secure',
|
|
@@ -208,7 +217,7 @@ import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
|
208
217
|
import { setupServer } from 'msw/node';
|
|
209
218
|
import { LambderMSW } from 'lambder';
|
|
210
219
|
import { LambderCaller } from 'lambder';
|
|
211
|
-
import type { MyApiContract } from '../
|
|
220
|
+
import type { MyApiContract } from '../backend'; // Type-only import from your backend
|
|
212
221
|
|
|
213
222
|
// Setup MSW
|
|
214
223
|
const lambderMSW = new LambderMSW<MyApiContract>({
|
|
@@ -305,7 +314,7 @@ LambderMSW also works in browser environments with MSW's browser integration:
|
|
|
305
314
|
// test/browser-setup.ts
|
|
306
315
|
import { setupWorker } from 'msw/browser';
|
|
307
316
|
import { LambderMSW } from 'lambder';
|
|
308
|
-
import type { MyApiContract } from '../
|
|
317
|
+
import type { MyApiContract } from '../backend'; // Type-only import from your backend
|
|
309
318
|
|
|
310
319
|
const lambderMSW = new LambderMSW<MyApiContract>({
|
|
311
320
|
apiPath: '/secure'
|
|
@@ -40,7 +40,7 @@ const lambder = new Lambder()
|
|
|
40
40
|
output: z.object({ success: z.boolean() })
|
|
41
41
|
}, async () => ({} as any));
|
|
42
42
|
|
|
43
|
-
type TestApiContract = typeof lambder.
|
|
43
|
+
type TestApiContract = typeof lambder.ApiContract;
|
|
44
44
|
|
|
45
45
|
// Setup LambderMSW with type safety
|
|
46
46
|
const lambderMSW = new LambderMSW<TestApiContract>({
|
|
@@ -46,7 +46,7 @@ const lambder = new Lambder({
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// 3. Export the inferred contract type for Frontend
|
|
49
|
-
export type AppContract = typeof lambder.
|
|
49
|
+
export type AppContract = typeof lambder.ApiContract;
|
|
50
50
|
|
|
51
51
|
// 4. Modular example using .use()
|
|
52
52
|
const authApi = <T>(l: Lambder<T>) => {
|
|
@@ -60,4 +60,4 @@ const authApi = <T>(l: Lambder<T>) => {
|
|
|
60
60
|
|
|
61
61
|
const lambderWithAuth = lambder.use(authApi);
|
|
62
62
|
|
|
63
|
-
export type AuthContract = typeof lambderWithAuth.
|
|
63
|
+
export type AuthContract = typeof lambderWithAuth.ApiContract;
|
package/package.json
CHANGED
package/src/Lambder.ts
CHANGED
|
@@ -1,44 +1,17 @@
|
|
|
1
|
-
import cookieParser from "cookie";
|
|
2
1
|
import { match } from "path-to-regexp";
|
|
3
2
|
import { z } from "zod";
|
|
4
3
|
|
|
5
|
-
import type { APIGatewayProxyEvent,
|
|
4
|
+
import type { APIGatewayProxyEvent, Context } from "aws-lambda";
|
|
6
5
|
import LambderResolver from "./LambderResolver.js";
|
|
7
6
|
import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
|
|
8
7
|
import LambderUtils from "./LambderUtils.js";
|
|
9
|
-
import LambderSessionManager
|
|
8
|
+
import LambderSessionManager from "./LambderSessionManager.js";
|
|
10
9
|
import LambderSessionController from "./LambderSessionController.js";
|
|
11
10
|
import type { MergeContract } from "./LambderApiContract.js";
|
|
11
|
+
import { createContext, type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
|
|
12
12
|
|
|
13
13
|
type Path = `/${string}`;
|
|
14
14
|
|
|
15
|
-
export type LambderRenderContext<TApiPayload = any> = {
|
|
16
|
-
host: string;
|
|
17
|
-
path: string;
|
|
18
|
-
pathParams: Record<string, any> | null;
|
|
19
|
-
method: string;
|
|
20
|
-
get: Record<string, any>;
|
|
21
|
-
post: Record<string, any>;
|
|
22
|
-
cookie: Record<string, any>;
|
|
23
|
-
session: null;
|
|
24
|
-
apiName: string;
|
|
25
|
-
apiPayload: TApiPayload;
|
|
26
|
-
headers: APIGatewayProxyEventHeaders;
|
|
27
|
-
event: APIGatewayProxyEvent;
|
|
28
|
-
lambdaContext: Context;
|
|
29
|
-
_otherInternal: {
|
|
30
|
-
isApiCall: boolean,
|
|
31
|
-
requestVersion: string|null;
|
|
32
|
-
setHeaderFnAccumulator: { key:string, value:string|string[] }[];
|
|
33
|
-
addHeaderFnAccumulator: { key:string, value:string }[];
|
|
34
|
-
logToApiResponseAccumulator: any[];
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type LambderSessionRenderContext<
|
|
39
|
-
TApiPayload = any, SessionData = any
|
|
40
|
-
> = Omit<LambderRenderContext<TApiPayload>, 'session'> & { session: LambderSessionContext<SessionData> };
|
|
41
|
-
|
|
42
15
|
type ConditionFunction = (ctx: LambderRenderContext<any>) => boolean;
|
|
43
16
|
type ActionFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
44
17
|
type SessionActionFunction<SessionData = any> = (ctx: LambderSessionRenderContext<any, SessionData>, resolver: LambderResolver) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
@@ -54,55 +27,7 @@ type HookFallbackFunction = (ctx: LambderRenderContext<any>, resolver: LambderRe
|
|
|
54
27
|
type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext<any>|null, response: LambderResponseBuilder, logListToApiResponse?: any[]) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
55
28
|
type RouteFallbackHandlerFunction = (ctx:LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
56
29
|
type ApiFallbackHandlerFunction = (ctx:LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
export const createContext = (
|
|
60
|
-
event: APIGatewayProxyEvent,
|
|
61
|
-
lambdaContext: Context,
|
|
62
|
-
apiPath: string,
|
|
63
|
-
):LambderRenderContext<any> => {
|
|
64
|
-
const host = event.headers.Host || event.headers.host || "";
|
|
65
|
-
const path = event.path;
|
|
66
|
-
const pathParams = null;
|
|
67
|
-
const get: Record<string, any> = event.queryStringParameters || {};
|
|
68
|
-
const method = event.httpMethod;
|
|
69
|
-
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
70
|
-
const headers = event.headers;
|
|
71
|
-
|
|
72
|
-
// Decode body for the post
|
|
73
|
-
let post: Record<string, any> = {};
|
|
74
|
-
try {
|
|
75
|
-
const decodedBody = event.isBase64Encoded ? ( event.body ? Buffer.from(event.body,"base64").toString() : "{}" ) : ( event.body || "{}" );
|
|
76
|
-
try { post = JSON.parse(decodedBody) || {}; }
|
|
77
|
-
catch(e){
|
|
78
|
-
const params = new URLSearchParams(decodedBody);
|
|
79
|
-
post = {};
|
|
80
|
-
for(const [key, value] of params.entries()){
|
|
81
|
-
post[key] = value;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}catch(e){}
|
|
85
|
-
// Parse api variables
|
|
86
|
-
|
|
87
|
-
const isApiCall = method === "POST" && apiPath && path === apiPath && post.apiName;
|
|
88
|
-
const apiName:string = isApiCall ? post.apiName : null;
|
|
89
|
-
const apiPayload:string = isApiCall ? post.payload : null;
|
|
90
|
-
const requestVersion:string = isApiCall ? post.version : null;
|
|
91
|
-
|
|
92
|
-
return {
|
|
93
|
-
host, path, pathParams, method,
|
|
94
|
-
get, post, cookie, event,
|
|
95
|
-
session: null,
|
|
96
|
-
apiName, apiPayload,
|
|
97
|
-
headers, lambdaContext,
|
|
98
|
-
_otherInternal: {
|
|
99
|
-
isApiCall, requestVersion,
|
|
100
|
-
setHeaderFnAccumulator: [],
|
|
101
|
-
addHeaderFnAccumulator: [],
|
|
102
|
-
logToApiResponseAccumulator: [],
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
}
|
|
30
|
+
type ApiInputValidationErrorHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver, zodError: z.ZodError) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
106
31
|
|
|
107
32
|
/**
|
|
108
33
|
* Main Lambder class for building type-safe serverless APIs
|
|
@@ -133,10 +58,10 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
133
58
|
* @example
|
|
134
59
|
* ```typescript
|
|
135
60
|
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
136
|
-
* export type AppContract = typeof lambder.
|
|
61
|
+
* export type AppContract = typeof lambder.ApiContract;
|
|
137
62
|
* ```
|
|
138
63
|
*/
|
|
139
|
-
public readonly
|
|
64
|
+
public readonly ApiContract!: _TContract;
|
|
140
65
|
|
|
141
66
|
private actionList: ActionObject[];
|
|
142
67
|
private hookList: {
|
|
@@ -147,6 +72,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
147
72
|
private globalErrorHandler: GlobalErrorHandlerFunction|null = null;
|
|
148
73
|
private routeFallbackHandler: RouteFallbackHandlerFunction|null = null;
|
|
149
74
|
private apiFallbackHandler: ApiFallbackHandlerFunction|null = null;
|
|
75
|
+
private apiInputValidationErrorHandler: ApiInputValidationErrorHandlerFunction|null = null;
|
|
150
76
|
|
|
151
77
|
public utils: LambderUtils;
|
|
152
78
|
|
|
@@ -202,6 +128,10 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
202
128
|
this.apiFallbackHandler = apiFallbackHandler;
|
|
203
129
|
return this;
|
|
204
130
|
}
|
|
131
|
+
setApiInputValidationErrorHandler(apiInputValidationErrorHandler: ApiInputValidationErrorHandlerFunction): this {
|
|
132
|
+
this.apiInputValidationErrorHandler = apiInputValidationErrorHandler;
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
205
135
|
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction): this {
|
|
206
136
|
this.globalErrorHandler = globalErrorHandler;
|
|
207
137
|
return this;
|
|
@@ -308,6 +238,9 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
308
238
|
// Validate Input
|
|
309
239
|
const inputResult = schema.input.safeParse(ctx.apiPayload);
|
|
310
240
|
if (!inputResult.success) {
|
|
241
|
+
if (this.apiInputValidationErrorHandler) {
|
|
242
|
+
return await this.apiInputValidationErrorHandler(ctx, resolver, inputResult.error);
|
|
243
|
+
}
|
|
311
244
|
return resolver.raw({
|
|
312
245
|
statusCode: 400,
|
|
313
246
|
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
@@ -346,6 +279,9 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
|
|
|
346
279
|
// Validate Input
|
|
347
280
|
const inputResult = schema.input.safeParse(ctx.apiPayload);
|
|
348
281
|
if (!inputResult.success) {
|
|
282
|
+
if (this.apiInputValidationErrorHandler) {
|
|
283
|
+
return await this.apiInputValidationErrorHandler(ctx, resolver, inputResult.error);
|
|
284
|
+
}
|
|
349
285
|
return resolver.raw({
|
|
350
286
|
statusCode: 400,
|
|
351
287
|
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import cookieParser from "cookie";
|
|
2
|
+
import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
|
|
3
|
+
import type { LambderSessionContext } from "./LambderSessionManager.js";
|
|
4
|
+
|
|
5
|
+
export type LambderRenderContext<TApiPayload = any> = {
|
|
6
|
+
host: string;
|
|
7
|
+
path: string;
|
|
8
|
+
pathParams: Record<string, any> | null;
|
|
9
|
+
method: string;
|
|
10
|
+
get: Record<string, any>;
|
|
11
|
+
post: Record<string, any>;
|
|
12
|
+
cookie: Record<string, any>;
|
|
13
|
+
session: null;
|
|
14
|
+
apiName: string;
|
|
15
|
+
apiPayload: TApiPayload;
|
|
16
|
+
headers: APIGatewayProxyEventHeaders;
|
|
17
|
+
event: APIGatewayProxyEvent;
|
|
18
|
+
lambdaContext: Context;
|
|
19
|
+
_otherInternal: {
|
|
20
|
+
isApiCall: boolean,
|
|
21
|
+
requestVersion: string|null;
|
|
22
|
+
setHeaderFnAccumulator: { key:string, value:string|string[] }[];
|
|
23
|
+
addHeaderFnAccumulator: { key:string, value:string }[];
|
|
24
|
+
logToApiResponseAccumulator: any[];
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type LambderSessionRenderContext<
|
|
29
|
+
TApiPayload = any, SessionData = any
|
|
30
|
+
> = Omit<LambderRenderContext<TApiPayload>, 'session'> & { session: LambderSessionContext<SessionData> };
|
|
31
|
+
|
|
32
|
+
export const createContext = (
|
|
33
|
+
event: APIGatewayProxyEvent,
|
|
34
|
+
lambdaContext: Context,
|
|
35
|
+
apiPath: string,
|
|
36
|
+
):LambderRenderContext<any> => {
|
|
37
|
+
const host = event.headers.Host || event.headers.host || "";
|
|
38
|
+
const path = event.path;
|
|
39
|
+
const pathParams = null;
|
|
40
|
+
const get: Record<string, any> = event.queryStringParameters || {};
|
|
41
|
+
const method = event.httpMethod;
|
|
42
|
+
const cookie = cookieParser.parse(event.headers.Cookie || event.headers.cookie || "");
|
|
43
|
+
const headers = event.headers;
|
|
44
|
+
|
|
45
|
+
// Decode body for the post
|
|
46
|
+
let post: Record<string, any> = {};
|
|
47
|
+
try {
|
|
48
|
+
const decodedBody = event.isBase64Encoded ? ( event.body ? Buffer.from(event.body,"base64").toString() : "{}" ) : ( event.body || "{}" );
|
|
49
|
+
try { post = JSON.parse(decodedBody) || {}; }
|
|
50
|
+
catch(e){
|
|
51
|
+
const params = new URLSearchParams(decodedBody);
|
|
52
|
+
post = {};
|
|
53
|
+
for(const [key, value] of params.entries()){
|
|
54
|
+
post[key] = value;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}catch(e){}
|
|
58
|
+
// Parse api variables
|
|
59
|
+
|
|
60
|
+
const isApiCall = method === "POST" && apiPath && path === apiPath && post.apiName;
|
|
61
|
+
const apiName:string = isApiCall ? post.apiName : null;
|
|
62
|
+
const apiPayload:string = isApiCall ? post.payload : null;
|
|
63
|
+
const requestVersion:string = isApiCall ? post.version : null;
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
host, path, pathParams, method,
|
|
67
|
+
get, post, cookie, event,
|
|
68
|
+
session: null,
|
|
69
|
+
apiName, apiPayload,
|
|
70
|
+
headers, lambdaContext,
|
|
71
|
+
_otherInternal: {
|
|
72
|
+
isApiCall, requestVersion,
|
|
73
|
+
setHeaderFnAccumulator: [],
|
|
74
|
+
addHeaderFnAccumulator: [],
|
|
75
|
+
logToApiResponseAccumulator: [],
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
package/src/LambderResolver.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import mimeTypeResolver from "mime-types";
|
|
2
2
|
import LambderUtils from "./LambderUtils.js";
|
|
3
|
-
import { LambderRenderContext } from "./
|
|
3
|
+
import { LambderRenderContext } from "./LambderContext.js";
|
|
4
4
|
import { getFS, getPath } from "./node-polyfills.js";
|
|
5
5
|
|
|
6
6
|
const convertToMultiHeader = (
|
|
@@ -102,7 +102,7 @@ export default class LambderResponseBuilder<TResponse = any> {
|
|
|
102
102
|
if(!this.ctx) throw new Error(".setHeader function is not available within this hook");
|
|
103
103
|
else{
|
|
104
104
|
this.ctx._otherInternal.addHeaderFnAccumulator = this.ctx._otherInternal.addHeaderFnAccumulator
|
|
105
|
-
.filter(header=>header.key !== key);
|
|
105
|
+
.filter((header: { key: string, value: string }) => header.key !== key);
|
|
106
106
|
this.ctx._otherInternal.setHeaderFnAccumulator.push({ key, value });
|
|
107
107
|
}
|
|
108
108
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LambderRenderContext, LambderSessionRenderContext } from "./
|
|
1
|
+
import { LambderRenderContext, LambderSessionRenderContext } from "./LambderContext.js";
|
|
2
2
|
import type LambderSessionManager from "./LambderSessionManager.js";
|
|
3
3
|
import type { LambderSessionContext } from "./LambderSessionManager.js";
|
|
4
4
|
|
package/src/index.ts
CHANGED
|
@@ -11,3 +11,7 @@ export { default as LambderMSW } from "./LambderMSW.js";
|
|
|
11
11
|
export {
|
|
12
12
|
type ApiContractShape,
|
|
13
13
|
} from "./LambderApiContract.js";
|
|
14
|
+
|
|
15
|
+
// Context types and utilities
|
|
16
|
+
export type { LambderRenderContext, LambderSessionRenderContext } from "./LambderContext.js";
|
|
17
|
+
export { createContext } from "./LambderContext.js";
|
|
@@ -34,7 +34,7 @@ describe('LambderCaller Type Safety', () => {
|
|
|
34
34
|
return resolver.api({ id: '1', ...ctx.apiPayload });
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
-
type TestApiContract = typeof lambder.
|
|
37
|
+
type TestApiContract = typeof lambder.ApiContract;
|
|
38
38
|
|
|
39
39
|
const caller = new LambderCaller<TestApiContract>({
|
|
40
40
|
apiPath: '/api',
|
|
@@ -181,7 +181,7 @@ describe('apiRaw Method Type Safety', () => {
|
|
|
181
181
|
return resolver.api({ id: ctx.apiPayload.userId, name: 'Test' });
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
-
type TestApiContract = typeof lambder.
|
|
184
|
+
type TestApiContract = typeof lambder.ApiContract;
|
|
185
185
|
|
|
186
186
|
const caller = new LambderCaller<TestApiContract>({
|
|
187
187
|
apiPath: '/api',
|
package/tests/use-plugin.test.ts
CHANGED
|
@@ -70,7 +70,7 @@ describe('Plugin System - Basic Usage', () => {
|
|
|
70
70
|
const result = await handler(event, createMockContext());
|
|
71
71
|
|
|
72
72
|
expect(result.statusCode).toBe(200);
|
|
73
|
-
const body = JSON.parse(result.body);
|
|
73
|
+
const body = JSON.parse(result.body || '{}');
|
|
74
74
|
expect(body.payload).toEqual({ id: '123', name: 'John Doe' });
|
|
75
75
|
});
|
|
76
76
|
|
|
@@ -90,7 +90,7 @@ describe('Plugin System - Basic Usage', () => {
|
|
|
90
90
|
apiPath: '/api'
|
|
91
91
|
}).use(userPlugin);
|
|
92
92
|
|
|
93
|
-
type AppContract = typeof lambder.
|
|
93
|
+
type AppContract = typeof lambder.ApiContract;
|
|
94
94
|
|
|
95
95
|
const caller = new LambderCaller<AppContract>({
|
|
96
96
|
apiPath: '/api',
|
|
@@ -163,19 +163,19 @@ describe('Plugin System - Multiple Plugins', () => {
|
|
|
163
163
|
const userEvent = createMockEvent('getUser', { userId: '123' });
|
|
164
164
|
const userResult = await handler(userEvent, createMockContext());
|
|
165
165
|
expect(userResult.statusCode).toBe(200);
|
|
166
|
-
expect(JSON.parse(userResult.body).payload.name).toBe('John');
|
|
166
|
+
expect(JSON.parse(userResult.body || '{}').payload.name).toBe('John');
|
|
167
167
|
|
|
168
168
|
// Test product API
|
|
169
169
|
const productEvent = createMockEvent('getProduct', { productId: 'prod-456' });
|
|
170
170
|
const productResult = await handler(productEvent, createMockContext());
|
|
171
171
|
expect(productResult.statusCode).toBe(200);
|
|
172
|
-
expect(JSON.parse(productResult.body).payload.title).toBe('Test Product');
|
|
172
|
+
expect(JSON.parse(productResult.body || '{}').payload.title).toBe('Test Product');
|
|
173
173
|
|
|
174
174
|
// Test order API
|
|
175
175
|
const orderEvent = createMockEvent('createOrder', { userId: '123', productId: 'prod-456' });
|
|
176
176
|
const orderResult = await handler(orderEvent, createMockContext());
|
|
177
177
|
expect(orderResult.statusCode).toBe(200);
|
|
178
|
-
expect(JSON.parse(orderResult.body).payload.orderId).toBe('order-123');
|
|
178
|
+
expect(JSON.parse(orderResult.body || '{}').payload.orderId).toBe('order-123');
|
|
179
179
|
});
|
|
180
180
|
|
|
181
181
|
it('should accumulate types from multiple plugins', () => {
|
|
@@ -206,7 +206,7 @@ describe('Plugin System - Multiple Plugins', () => {
|
|
|
206
206
|
.use(userPlugin)
|
|
207
207
|
.use(productPlugin);
|
|
208
208
|
|
|
209
|
-
type AppContract = typeof lambder.
|
|
209
|
+
type AppContract = typeof lambder.ApiContract;
|
|
210
210
|
|
|
211
211
|
const caller = new LambderCaller<AppContract>({
|
|
212
212
|
apiPath: '/api',
|
|
@@ -263,17 +263,17 @@ describe('Plugin System - Mixed Usage', () => {
|
|
|
263
263
|
// Test direct API before plugin
|
|
264
264
|
const healthEvent = createMockEvent('healthCheck', undefined);
|
|
265
265
|
const healthResult = await handler(healthEvent, createMockContext());
|
|
266
|
-
expect(JSON.parse(healthResult.body).payload.status).toBe('ok');
|
|
266
|
+
expect(JSON.parse(healthResult.body || '{}').payload.status).toBe('ok');
|
|
267
267
|
|
|
268
268
|
// Test plugin API
|
|
269
269
|
const userEvent = createMockEvent('getUser', { userId: '123' });
|
|
270
270
|
const userResult = await handler(userEvent, createMockContext());
|
|
271
|
-
expect(JSON.parse(userResult.body).payload.name).toBe('John');
|
|
271
|
+
expect(JSON.parse(userResult.body || '{}').payload.name).toBe('John');
|
|
272
272
|
|
|
273
273
|
// Test direct API after plugin
|
|
274
274
|
const versionEvent = createMockEvent('getVersion', undefined);
|
|
275
275
|
const versionResult = await handler(versionEvent, createMockContext());
|
|
276
|
-
expect(JSON.parse(versionResult.body).payload.version).toBe('2.0');
|
|
276
|
+
expect(JSON.parse(versionResult.body || '{}').payload.version).toBe('2.0');
|
|
277
277
|
});
|
|
278
278
|
});
|
|
279
279
|
|
|
@@ -424,7 +424,7 @@ describe('Plugin System - Type Safety', () => {
|
|
|
424
424
|
.use(plugin1)
|
|
425
425
|
.use(plugin2);
|
|
426
426
|
|
|
427
|
-
type Contract = typeof lambder.
|
|
427
|
+
type Contract = typeof lambder.ApiContract;
|
|
428
428
|
|
|
429
429
|
// Type assertions - both api1 and api2 should be in the contract
|
|
430
430
|
// We test this at runtime by creating a caller
|
|
@@ -446,12 +446,14 @@ describe('Plugin System - Non-Generic Plugins', () => {
|
|
|
446
446
|
const plugin2 = (l: Lambder) => l.addApi('api2', { input: z.void(), output: z.void() }, async (ctx, res) => res.raw({ statusCode: 200, body: '' }));
|
|
447
447
|
|
|
448
448
|
const lambder = new Lambder({ publicPath: '', apiPath: '' })
|
|
449
|
+
.addApi('initialApi', { input: z.void(), output: z.void() }, async (ctx, res) => res.raw({ statusCode: 200, body: '' }))
|
|
449
450
|
.use(plugin1)
|
|
450
451
|
.use(plugin2);
|
|
451
452
|
|
|
452
|
-
type Contract = typeof lambder.
|
|
453
|
+
type Contract = typeof lambder.ApiContract;
|
|
453
454
|
|
|
454
455
|
// Check if both api1 and api2 exist in Contract
|
|
456
|
+
expectTypeOf<Contract>().toHaveProperty('initialApi');
|
|
455
457
|
expectTypeOf<Contract>().toHaveProperty('api1');
|
|
456
458
|
expectTypeOf<Contract>().toHaveProperty('api2');
|
|
457
459
|
});
|