lambder 2.0.14 → 2.0.16

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/dist/Lambder.js CHANGED
@@ -120,10 +120,9 @@ export default class Lambder {
120
120
  }
121
121
  addRoute(condition, actionFn) {
122
122
  this.actionList.push({
123
- conditionFn: (ctx) => (ctx.method === "GET" &&
124
- ((typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
125
- (typeof condition === "function" && condition(ctx)) ||
126
- (condition?.constructor == RegExp && condition.test(ctx.path)))),
123
+ conditionFn: (ctx) => (((typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
124
+ (typeof condition === "function" && condition(ctx)) ||
125
+ (condition?.constructor == RegExp && condition.test(ctx.path)))),
127
126
  actionFn: async (ctx, resolver) => {
128
127
  if (typeof condition === "string") {
129
128
  ctx.pathParams = this.getPatternMatch(condition, ctx.path);
@@ -139,10 +138,9 @@ export default class Lambder {
139
138
  }
140
139
  addSessionRoute(condition, actionFn) {
141
140
  this.actionList.push({
142
- conditionFn: (ctx) => (ctx.method === "GET" &&
143
- ((typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
144
- (typeof condition === "function" && condition(ctx)) ||
145
- (condition?.constructor == RegExp && condition.test(ctx.path)))),
141
+ conditionFn: (ctx) => (((typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
142
+ (typeof condition === "function" && condition(ctx)) ||
143
+ (condition?.constructor == RegExp && condition.test(ctx.path)))),
146
144
  actionFn: async (ctx, resolver) => {
147
145
  if (typeof condition === "string") {
148
146
  ctx.pathParams = this.getPatternMatch(condition, ctx.path);
@@ -177,7 +175,7 @@ export default class Lambder {
177
175
  return await this.apiInputValidationErrorHandler(ctx, resolver, inputResult.error);
178
176
  }
179
177
  return resolver.raw({
180
- statusCode: 400,
178
+ statusCode: 422,
181
179
  body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
182
180
  multiValueHeaders: { "Content-Type": ["application/json"] }
183
181
  });
@@ -1,5 +1,6 @@
1
1
  import { LambderApiResponse } from './LambderResponseBuilder';
2
2
  import type { ApiContractShape } from './LambderApiContract';
3
+ import type { z } from "zod";
3
4
  type VoidFunction = () => void | Promise<void>;
4
5
  type FetchTracker = {
5
6
  apiName: string;
@@ -21,6 +22,7 @@ type FetchEndEventHandler = (params: {
21
22
  activeFetchList: FetchTracker[];
22
23
  }) => void | Promise<void>;
23
24
  type ErrorHandler = (err: Error) => void | Promise<void>;
25
+ type ValidationErrorHandler = (zodError: z.ZodError) => (void | false) | Promise<(void | false)>;
24
26
  type MessageHandler = (message: any) => void | Promise<void>;
25
27
  export default class LambderCaller<TContract extends ApiContractShape = any> {
26
28
  private isCorsEnabled;
@@ -34,11 +36,12 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
34
36
  private errorMessageHandler?;
35
37
  private notAuthorizedHandler?;
36
38
  private errorHandler?;
39
+ private apiInputValidationErrorHandler?;
37
40
  private fetchStartedHandler?;
38
41
  private fetchEndedHandler?;
39
42
  private sessionTokenCookieKey;
40
43
  private sessionCsrfCookieKey;
41
- constructor({ apiPath, apiVersion, isCorsEnabled, versionExpiredHandler, sessionExpiredHandler, messageHandler, errorMessageHandler, notAuthorizedHandler, errorHandler, fetchStartedHandler, fetchEndedHandler, }: {
44
+ constructor({ apiPath, apiVersion, isCorsEnabled, versionExpiredHandler, sessionExpiredHandler, messageHandler, errorMessageHandler, notAuthorizedHandler, errorHandler, fetchStartedHandler, fetchEndedHandler, apiInputValidationErrorHandler, }: {
42
45
  apiPath: string;
43
46
  apiVersion?: string;
44
47
  isCorsEnabled: boolean;
@@ -50,6 +53,7 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
50
53
  errorHandler?: ErrorHandler;
51
54
  fetchStartedHandler?: FetchStartEventHandler;
52
55
  fetchEndedHandler?: FetchEndEventHandler;
56
+ apiInputValidationErrorHandler?: ValidationErrorHandler;
53
57
  });
54
58
  setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): void;
55
59
  apiRaw<TApiName extends keyof TContract & string = string, TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any>(apiName: TApiName, payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any, options?: {
@@ -58,6 +62,7 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
58
62
  sessionExpiredHandler?: VoidFunction;
59
63
  messageHandler?: MessageHandler;
60
64
  errorMessageHandler?: MessageHandler;
65
+ apiInputValidationErrorHandler?: ValidationErrorHandler;
61
66
  notAuthorizedHandler?: VoidFunction;
62
67
  errorHandler?: ErrorHandler;
63
68
  fetchStartedHandler?: FetchStartEventHandler;
@@ -11,11 +11,12 @@ export default class LambderCaller {
11
11
  errorMessageHandler;
12
12
  notAuthorizedHandler;
13
13
  errorHandler;
14
+ apiInputValidationErrorHandler;
14
15
  fetchStartedHandler;
15
16
  fetchEndedHandler;
16
17
  sessionTokenCookieKey = "LMDRSESSIONTKID";
17
18
  sessionCsrfCookieKey = "LMDRSESSIONCSTK";
18
- constructor({ apiPath, apiVersion, isCorsEnabled = false, versionExpiredHandler, sessionExpiredHandler, messageHandler, errorMessageHandler, notAuthorizedHandler, errorHandler, fetchStartedHandler, fetchEndedHandler, }) {
19
+ constructor({ apiPath, apiVersion, isCorsEnabled = false, versionExpiredHandler, sessionExpiredHandler, messageHandler, errorMessageHandler, notAuthorizedHandler, errorHandler, fetchStartedHandler, fetchEndedHandler, apiInputValidationErrorHandler, }) {
19
20
  this.apiPath = apiPath ?? "/api";
20
21
  this.apiVersion = apiVersion;
21
22
  this.isCorsEnabled = isCorsEnabled;
@@ -25,6 +26,7 @@ export default class LambderCaller {
25
26
  this.errorMessageHandler = errorMessageHandler;
26
27
  this.notAuthorizedHandler = notAuthorizedHandler;
27
28
  this.errorHandler = errorHandler;
29
+ this.apiInputValidationErrorHandler = apiInputValidationErrorHandler;
28
30
  this.fetchStartedHandler = fetchStartedHandler;
29
31
  this.fetchEndedHandler = fetchEndedHandler;
30
32
  }
@@ -54,6 +56,17 @@ export default class LambderCaller {
54
56
  }).then(async (res) => {
55
57
  if (res.status >= 500)
56
58
  throw new Error("Request failed: " + res.status + " - " + res.statusText);
59
+ if (res.status === 422) {
60
+ const errorData = await res.json();
61
+ if (this.apiInputValidationErrorHandler) {
62
+ await this.apiInputValidationErrorHandler(errorData.zodError);
63
+ }
64
+ else if (this.errorHandler) {
65
+ await this.errorHandler(new Error("API Input Validation Error", { cause: errorData.zodError }));
66
+ }
67
+ return null;
68
+ }
69
+ ;
57
70
  if (res.headers.get("Content-Type")?.includes("application/lambder-json-stream")) {
58
71
  const decompressed = res.json();
59
72
  return decompressed;
@@ -114,7 +127,7 @@ export default class LambderCaller {
114
127
  return data;
115
128
  }
116
129
  catch (err) {
117
- const wrappedError = err instanceof Error ? err : new Error("Error: " + String(err));
130
+ const wrappedError = err instanceof Error ? err : new Error("Error: ", { cause: err });
118
131
  fetchTracker.done = true;
119
132
  if (!fetchTracker.fetchEndCalled && this.fetchEndedHandler) {
120
133
  await this.fetchEndedHandler({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lambder",
3
- "version": "2.0.14",
3
+ "version": "2.0.16",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/src/Lambder.ts CHANGED
@@ -163,7 +163,6 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
163
163
  addRoute(condition: Path|ConditionFunction|RegExp, actionFn: ActionFunction): this {
164
164
  this.actionList.push({
165
165
  conditionFn: (ctx:LambderRenderContext<any>) => (
166
- ctx.method === "GET" &&
167
166
  (
168
167
  (typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
169
168
  (typeof condition === "function" && condition(ctx)) ||
@@ -187,7 +186,6 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
187
186
  addSessionRoute(condition: Path|ConditionFunction|RegExp, actionFn: SessionActionFunction<TSessionData>): this {
188
187
  this.actionList.push({
189
188
  conditionFn: (ctx:LambderRenderContext<any>) => (
190
- ctx.method === "GET" &&
191
189
  (
192
190
  (typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
193
191
  (typeof condition === "function" && condition(ctx)) ||
@@ -242,7 +240,7 @@ export default class Lambder<TSessionData = any, _TContract extends Record<strin
242
240
  return await this.apiInputValidationErrorHandler(ctx, resolver, inputResult.error);
243
241
  }
244
242
  return resolver.raw({
245
- statusCode: 400,
243
+ statusCode: 422,
246
244
  body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
247
245
  multiValueHeaders: { "Content-Type": ["application/json"] }
248
246
  });
@@ -1,6 +1,7 @@
1
1
  import Cookies from 'js-cookie';
2
2
  import { LambderApiResponse } from './LambderResponseBuilder';
3
3
  import type { ApiContractShape } from './LambderApiContract';
4
+ import type { z } from "zod";
4
5
 
5
6
  type VoidFunction = ()=>void|Promise<void>;
6
7
  type FetchTracker = { apiName: string, done: boolean, fetchEndCalled: boolean };
@@ -22,6 +23,7 @@ type FetchEndEventHandler = (params: {
22
23
  })=>void|Promise<void>;
23
24
 
24
25
  type ErrorHandler = (err: Error) => void|Promise<void>;
26
+ type ValidationErrorHandler = (zodError: z.ZodError) => (void|false)|Promise<(void|false)>;
25
27
  type MessageHandler = (message:any) => void|Promise<void>;
26
28
 
27
29
  export default class LambderCaller<TContract extends ApiContractShape = any> {
@@ -39,6 +41,7 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
39
41
  private errorMessageHandler?: MessageHandler;
40
42
  private notAuthorizedHandler?: VoidFunction;
41
43
  private errorHandler?: ErrorHandler;
44
+ private apiInputValidationErrorHandler?: ValidationErrorHandler;
42
45
 
43
46
  private fetchStartedHandler?: FetchStartEventHandler;
44
47
  private fetchEndedHandler?: FetchEndEventHandler;
@@ -54,6 +57,7 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
54
57
  messageHandler, errorMessageHandler,
55
58
  notAuthorizedHandler, errorHandler,
56
59
  fetchStartedHandler, fetchEndedHandler,
60
+ apiInputValidationErrorHandler,
57
61
  }:
58
62
  {
59
63
  apiPath: string,
@@ -67,6 +71,7 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
67
71
  errorHandler?: ErrorHandler,
68
72
  fetchStartedHandler?: FetchStartEventHandler,
69
73
  fetchEndedHandler?: FetchEndEventHandler,
74
+ apiInputValidationErrorHandler?: ValidationErrorHandler,
70
75
  }
71
76
  ){
72
77
  this.apiPath = apiPath ?? "/api";
@@ -80,6 +85,7 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
80
85
  this.errorMessageHandler = errorMessageHandler;
81
86
  this.notAuthorizedHandler = notAuthorizedHandler;
82
87
  this.errorHandler = errorHandler;
88
+ this.apiInputValidationErrorHandler = apiInputValidationErrorHandler;
83
89
 
84
90
  this.fetchStartedHandler = fetchStartedHandler;
85
91
  this.fetchEndedHandler = fetchEndedHandler;
@@ -103,6 +109,7 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
103
109
  sessionExpiredHandler?: VoidFunction,
104
110
  messageHandler?: MessageHandler,
105
111
  errorMessageHandler?: MessageHandler,
112
+ apiInputValidationErrorHandler?: ValidationErrorHandler,
106
113
  notAuthorizedHandler?: VoidFunction,
107
114
  errorHandler?: ErrorHandler,
108
115
  fetchStartedHandler?: FetchStartEventHandler,
@@ -127,6 +134,15 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
127
134
  body: JSON.stringify({ apiName, version, token, siteHost, payload, }),
128
135
  }).then(async (res)=>{
129
136
  if(res.status >= 500) throw new Error("Request failed: " + res.status + " - " + res.statusText);
137
+ if(res.status === 422){
138
+ const errorData: { error: string, zodError: z.ZodError } = await res.json();
139
+ if(this.apiInputValidationErrorHandler){
140
+ await this.apiInputValidationErrorHandler(errorData.zodError);
141
+ }else if(this.errorHandler){
142
+ await this.errorHandler(new Error("API Input Validation Error", { cause: errorData.zodError }));
143
+ }
144
+ return null;
145
+ };
130
146
  if(res.headers.get("Content-Type")?.includes("application/lambder-json-stream")){
131
147
  const decompressed = res.json();
132
148
  return decompressed;
@@ -182,7 +198,7 @@ export default class LambderCaller<TContract extends ApiContractShape = any> {
182
198
  }
183
199
  return data;
184
200
  }catch(err){
185
- const wrappedError = err instanceof Error ? err : new Error("Error: " + String(err));
201
+ const wrappedError = err instanceof Error ? err : new Error("Error: ", { cause: err });
186
202
  fetchTracker.done = true;
187
203
  if(!fetchTracker.fetchEndCalled && this.fetchEndedHandler){
188
204
  await this.fetchEndedHandler({
@@ -441,7 +441,7 @@ describe('Error Handling - Input Validation Errors', () => {
441
441
  });
442
442
  const result = await handler(event, createMockContext());
443
443
 
444
- expect(result.statusCode).toBe(400);
444
+ expect(result.statusCode).toBe(422);
445
445
  const body = JSON.parse(result.body || '{}');
446
446
  expect(body.error).toBe('Input validation failed');
447
447
  expect(body.zodError).toBeDefined();
@@ -467,7 +467,7 @@ describe('Error Handling - Input Validation Errors', () => {
467
467
  const event = createMockEvent('/api', 'POST', 'testApi', { value: 'abc' }); // Too short
468
468
  const result = await handler(event, createMockContext());
469
469
 
470
- expect(result.statusCode).toBe(400);
470
+ expect(result.statusCode).toBe(422);
471
471
  });
472
472
  });
473
473
 
@@ -518,13 +518,13 @@ describe('Routes - Wildcard and Catch-all Routes', () => {
518
518
  });
519
519
 
520
520
  describe('Routes - Method Filtering', () => {
521
- it('should only match GET requests', async () => {
521
+ it('should match all HTTP methods when no method is specified', async () => {
522
522
  const lambder = new Lambder({
523
523
  publicPath: './public',
524
524
  apiPath: '/api'
525
525
  })
526
526
  .addRoute('/resource', (ctx, res) => {
527
- return res.html('GET Response');
527
+ return res.html(`${ctx.method} Response`);
528
528
  });
529
529
 
530
530
  const handler = lambder.getHandler();
@@ -534,9 +534,10 @@ describe('Routes - Method Filtering', () => {
534
534
  const getResult = await handler(getEvent, createMockContext());
535
535
  expect(Buffer.from(getResult.body || '', 'base64').toString()).toBe('GET Response');
536
536
 
537
- // POST should not match routes (should hit fallback)
537
+ // POST should also work (no method restriction)
538
538
  const postEvent = createMockEvent('/resource', 'POST');
539
539
  const postResult = await handler(postEvent, createMockContext());
540
- expect(postResult.statusCode).toBe(204); // Default fallback
540
+ expect(postResult.statusCode).toBe(200);
541
+ expect(Buffer.from(postResult.body || '', 'base64').toString()).toBe('POST Response');
541
542
  });
542
543
  });