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 +7 -9
- package/dist/LambderCaller.d.ts +6 -1
- package/dist/LambderCaller.js +15 -2
- package/package.json +1 -1
- package/src/Lambder.ts +1 -3
- package/src/LambderCaller.ts +17 -1
- package/tests/error-handling.test.ts +2 -2
- package/tests/routes.test.ts +5 -4
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) => (
|
|
124
|
-
(
|
|
125
|
-
|
|
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) => (
|
|
143
|
-
(
|
|
144
|
-
|
|
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:
|
|
178
|
+
statusCode: 422,
|
|
181
179
|
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
182
180
|
multiValueHeaders: { "Content-Type": ["application/json"] }
|
|
183
181
|
});
|
package/dist/LambderCaller.d.ts
CHANGED
|
@@ -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;
|
package/dist/LambderCaller.js
CHANGED
|
@@ -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: "
|
|
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
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:
|
|
243
|
+
statusCode: 422,
|
|
246
244
|
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
247
245
|
multiValueHeaders: { "Content-Type": ["application/json"] }
|
|
248
246
|
});
|
package/src/LambderCaller.ts
CHANGED
|
@@ -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: "
|
|
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(
|
|
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(
|
|
470
|
+
expect(result.statusCode).toBe(422);
|
|
471
471
|
});
|
|
472
472
|
});
|
|
473
473
|
|
package/tests/routes.test.ts
CHANGED
|
@@ -518,13 +518,13 @@ describe('Routes - Wildcard and Catch-all Routes', () => {
|
|
|
518
518
|
});
|
|
519
519
|
|
|
520
520
|
describe('Routes - Method Filtering', () => {
|
|
521
|
-
it('should
|
|
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(
|
|
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
|
|
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(
|
|
540
|
+
expect(postResult.statusCode).toBe(200);
|
|
541
|
+
expect(Buffer.from(postResult.body || '', 'base64').toString()).toBe('POST Response');
|
|
541
542
|
});
|
|
542
543
|
});
|