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 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.ApiContractType;
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.ApiContractType;
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.ApiContractType;
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, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
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
- export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext<any>;
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.ApiContractType;
49
+ * export type AppContract = typeof lambder.ApiContract;
81
50
  * ```
82
51
  */
83
- readonly ApiContractType: _TContract;
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
- export const createContext = (event, lambdaContext, apiPath) => {
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.ApiContractType;
36
+ * export type AppContract = typeof lambder.ApiContract;
80
37
  * ```
81
38
  */
82
- ApiContractType;
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.ApiContractType
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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Lambder API Contract System
3
3
  *
4
- * Contracts are built via method chaining and inferred using typeof lambder.ApiContractType
4
+ * Contracts are built via method chaining and inferred using typeof lambder.ApiContract
5
5
  */
6
6
  export {};
@@ -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 "./Lambder.js";
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 "./Lambder.js";
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 "./Lambder.js";
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";
@@ -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
- // shared/apiContract.ts
60
- import type { ApiContract } from 'lambder';
61
-
62
- export type MyApiContract = ApiContract<{
63
- getUserById: {
64
- input: { userId: string },
65
- output: { id: string, name: string, email: string }
66
- },
67
- createUser: {
68
- input: { name: string, email: string },
69
- output: { id: string, name: string, email: string }
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 '../shared/apiContract';
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 '../shared/apiContract';
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 '../shared/apiContract';
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'
@@ -29,7 +29,7 @@ const lambder = new Lambder({
29
29
  });
30
30
 
31
31
  // Export the inferred contract type
32
- export type AppContract = typeof lambder.ApiContractType;
32
+ export type AppContract = typeof lambder.ApiContract;
33
33
 
34
34
  export const handler = lambder.getHandler();
35
35
  ```
@@ -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.ApiContractType;
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.ApiContractType;
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.ApiContractType;
63
+ export type AuthContract = typeof lambderWithAuth.ApiContract;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "2.0.4",
3
+ "version": "2.0.7",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
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, { type LambderSessionContext } from "./LambderSessionManager.js";
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.ApiContractType;
61
+ * export type AppContract = typeof lambder.ApiContract;
137
62
  * ```
138
63
  */
139
- public readonly ApiContractType!: _TContract;
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 }),
@@ -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.ApiContractType
4
+ * Contracts are built via method chaining and inferred using typeof lambder.ApiContract
5
5
  */
6
6
 
7
7
  /**
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import type { LambderRenderContext } from "./Lambder.js";
1
+ import type { LambderRenderContext } from "./LambderContext.js";
2
2
  import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
3
3
  import LambderUtils from "./LambderUtils.js";
4
4
 
@@ -1,6 +1,6 @@
1
1
  import mimeTypeResolver from "mime-types";
2
2
  import LambderUtils from "./LambderUtils.js";
3
- import { LambderRenderContext } from "./Lambder.js";
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 "./Lambder.js";
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.ApiContractType;
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.ApiContractType;
184
+ type TestApiContract = typeof lambder.ApiContract;
185
185
 
186
186
  const caller = new LambderCaller<TestApiContract>({
187
187
  apiPath: '/api',
@@ -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.ApiContractType;
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.ApiContractType;
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.ApiContractType;
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.ApiContractType;
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
  });