lambder 2.0.16 → 3.0.0
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 +162 -41
- package/dist/Lambder.d.ts +154 -46
- package/dist/Lambder.js +312 -166
- package/dist/LambderCaller.js +6 -3
- package/dist/LambderContext.d.ts +20 -9
- package/dist/LambderContext.js +57 -17
- package/dist/LambderCors.d.ts +12 -0
- package/dist/LambderCors.js +30 -0
- package/dist/LambderHtml.d.ts +33 -0
- package/dist/LambderHtml.js +62 -0
- package/dist/LambderMSW.d.ts +16 -1
- package/dist/LambderMSW.js +5 -9
- package/dist/LambderPublicFiles.d.ts +47 -0
- package/dist/LambderPublicFiles.js +108 -0
- package/dist/LambderResolver.d.ts +30 -31
- package/dist/LambderResolver.js +29 -43
- package/dist/LambderResponse.d.ts +71 -0
- package/dist/LambderResponse.js +196 -0
- package/dist/LambderResponseBuilder.d.ts +58 -33
- package/dist/LambderResponseBuilder.js +114 -167
- package/dist/LambderRouting.d.ts +23 -0
- package/dist/LambderRouting.js +67 -0
- package/dist/LambderSessionController.d.ts +13 -1
- package/dist/LambderSessionController.js +33 -10
- package/dist/LambderSessionManager.d.ts +3 -1
- package/dist/LambderSessionManager.js +15 -6
- package/dist/LambderTemplatingEngine.d.ts +87 -0
- package/dist/LambderTemplatingEngine.js +156 -0
- package/dist/index.d.ts +14 -2
- package/dist/index.js +10 -1
- package/dist/node-polyfills.d.ts +4 -2
- package/dist/node-polyfills.js +28 -0
- package/package.json +7 -5
- package/.eslintrc.cjs +0 -26
- package/.vscode/settings.json +0 -26
- package/deploy +0 -22
- package/dist/LambderUtils.d.ts +0 -10
- package/dist/LambderUtils.js +0 -70
- package/docs/DYNAMODB_SETUP.md +0 -96
- package/docs/LAMBDER_MSW.md +0 -409
- package/docs/TYPE_SAFE_QUICK_START.md +0 -77
- package/examples/msw-testing-example.ts +0 -280
- package/examples/secure-session-example.ts +0 -207
- package/examples/zod-chained-api-example.ts +0 -63
- package/src/Lambder.ts +0 -430
- package/src/LambderApiContract.ts +0 -20
- package/src/LambderCaller.ts +0 -238
- package/src/LambderContext.ts +0 -78
- package/src/LambderMSW.ts +0 -180
- package/src/LambderResolver.ts +0 -101
- package/src/LambderResponseBuilder.ts +0 -332
- package/src/LambderSessionController.ts +0 -114
- package/src/LambderSessionManager.ts +0 -217
- package/src/LambderUtils.ts +0 -75
- package/src/index.ts +0 -17
- package/src/node-polyfills.ts +0 -27
- package/tests/error-handling.test.ts +0 -585
- package/tests/file-serving.test.ts +0 -194
- package/tests/fixtures/public/index.html +0 -1
- package/tests/fixtures/public/main.css +0 -1
- package/tests/hooks.test.ts +0 -561
- package/tests/output-type-runtime.test.ts +0 -381
- package/tests/redirect.test.ts +0 -88
- package/tests/routes.test.ts +0 -543
- package/tests/session.test.ts +0 -1083
- package/tests/use-plugin.test.ts +0 -460
- package/tsconfig.json +0 -24
package/src/Lambder.ts
DELETED
|
@@ -1,430 +0,0 @@
|
|
|
1
|
-
import { match } from "path-to-regexp";
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
import type { APIGatewayProxyEvent, Context } from "aws-lambda";
|
|
5
|
-
import LambderResolver from "./LambderResolver.js";
|
|
6
|
-
import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
|
|
7
|
-
import LambderUtils from "./LambderUtils.js";
|
|
8
|
-
import LambderSessionManager from "./LambderSessionManager.js";
|
|
9
|
-
import LambderSessionController from "./LambderSessionController.js";
|
|
10
|
-
import type { MergeContract } from "./LambderApiContract.js";
|
|
11
|
-
import { createContext, type LambderRenderContext, type LambderSessionRenderContext } from "./LambderContext.js";
|
|
12
|
-
|
|
13
|
-
type Path = `/${string}`;
|
|
14
|
-
|
|
15
|
-
type ConditionFunction = (ctx: LambderRenderContext<any>) => boolean;
|
|
16
|
-
type ActionFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
17
|
-
type SessionActionFunction<SessionData = any> = (ctx: LambderSessionRenderContext<any, SessionData>, resolver: LambderResolver) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
18
|
-
|
|
19
|
-
type ActionObject = { conditionFn: ConditionFunction, actionFn: ActionFunction | SessionActionFunction };
|
|
20
|
-
|
|
21
|
-
type HookEventType = "created"|"beforeRender"|"afterRender"|"fallback";
|
|
22
|
-
type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
|
|
23
|
-
type HookBeforeRenderFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderRenderContext<any>|Error|Promise<LambderRenderContext<any>|Error>;
|
|
24
|
-
type HookAfterRenderFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver, response: LambderResolverResponse) => LambderResolverResponse|Error|Promise<LambderResolverResponse|Error>;
|
|
25
|
-
type HookFallbackFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => void|Promise<void>;
|
|
26
|
-
|
|
27
|
-
type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext<any>|null, response: LambderResponseBuilder, logListToApiResponse?: any[]) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
28
|
-
type RouteFallbackHandlerFunction = (ctx:LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
29
|
-
type ApiFallbackHandlerFunction = (ctx:LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
30
|
-
type ApiInputValidationErrorHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver, zodError: z.ZodError) => LambderResolverResponse|Promise<LambderResolverResponse>;
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Main Lambder class for building type-safe serverless APIs
|
|
34
|
-
*
|
|
35
|
-
* @typeParam TSessionData - Type of session data stored in DynamoDB
|
|
36
|
-
* @typeParam _TContract - @internal Accumulates API contract during chaining (do not pass manually)
|
|
37
|
-
*
|
|
38
|
-
* @example
|
|
39
|
-
* ```typescript
|
|
40
|
-
* interface SessionData { userId: string; role: string; }
|
|
41
|
-
*
|
|
42
|
-
* const lambder = new Lambder<SessionData>({ apiPath: '/api' })
|
|
43
|
-
* .addApi('getUser', { input: z.object({...}), output: z.object({...}) }, handler)
|
|
44
|
-
* .addApi('createUser', { input: z.object({...}), output: z.object({...}) }, handler);
|
|
45
|
-
* ```
|
|
46
|
-
*/
|
|
47
|
-
export default class Lambder<TSessionData = any, _TContract extends Record<string, any> = {}> {
|
|
48
|
-
public apiPath: string;
|
|
49
|
-
public apiVersion: null|string;
|
|
50
|
-
public isCorsEnabled: boolean = false;
|
|
51
|
-
public publicPath: string;
|
|
52
|
-
public ejsPath: string;
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* Type property for extracting the API contract
|
|
56
|
-
* Use this to export your API types to the frontend
|
|
57
|
-
*
|
|
58
|
-
* @example
|
|
59
|
-
* ```typescript
|
|
60
|
-
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
61
|
-
* export type ApiContractType = typeof lambder.ApiContract;
|
|
62
|
-
* ```
|
|
63
|
-
*/
|
|
64
|
-
public readonly ApiContract!: _TContract;
|
|
65
|
-
|
|
66
|
-
private actionList: ActionObject[];
|
|
67
|
-
private hookList: {
|
|
68
|
-
"beforeRender": { priority: number, hookFn: HookBeforeRenderFunction }[],
|
|
69
|
-
"afterRender": { priority: number, hookFn: HookAfterRenderFunction }[],
|
|
70
|
-
"fallback": { priority: number, hookFn: HookFallbackFunction }[],
|
|
71
|
-
};
|
|
72
|
-
private globalErrorHandler: GlobalErrorHandlerFunction|null = null;
|
|
73
|
-
private routeFallbackHandler: RouteFallbackHandlerFunction|null = null;
|
|
74
|
-
private apiFallbackHandler: ApiFallbackHandlerFunction|null = null;
|
|
75
|
-
private apiInputValidationErrorHandler: ApiInputValidationErrorHandlerFunction|null = null;
|
|
76
|
-
|
|
77
|
-
public utils: LambderUtils;
|
|
78
|
-
|
|
79
|
-
private lambderSessionManager?: LambderSessionManager;
|
|
80
|
-
private sessionTokenCookieKey = "LMDRSESSIONTKID";
|
|
81
|
-
private sessionCsrfCookieKey = "LMDRSESSIONCSTK";
|
|
82
|
-
|
|
83
|
-
constructor(
|
|
84
|
-
{ publicPath, apiPath, ejsPath, apiVersion }:
|
|
85
|
-
{ publicPath: string, apiPath?: string, ejsPath?: string, apiVersion?: string }
|
|
86
|
-
){
|
|
87
|
-
this.publicPath = publicPath || "/incorrect-path-not-found";
|
|
88
|
-
this.ejsPath = ejsPath || "/incorrect-ejs-path-not-found";
|
|
89
|
-
this.apiPath = apiPath ?? "/api";
|
|
90
|
-
this.apiVersion = apiVersion ?? null;
|
|
91
|
-
|
|
92
|
-
this.actionList = [];
|
|
93
|
-
this.hookList = {
|
|
94
|
-
"beforeRender": [],
|
|
95
|
-
"afterRender": [],
|
|
96
|
-
"fallback": [],
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
this.utils = new LambderUtils({ ejsPath });
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
enableCors(isCorsEnabled: boolean): this {
|
|
103
|
-
this.isCorsEnabled = isCorsEnabled;
|
|
104
|
-
return this;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
enableDdbSession(
|
|
108
|
-
{ tableName, tableRegion, sessionSalt, enableSlidingExpiration }: { tableName: string; tableRegion: string; sessionSalt: string; enableSlidingExpiration?: boolean; },
|
|
109
|
-
{ partitionKey, sortKey }: { partitionKey: string, sortKey: string } = { partitionKey: "pk", sortKey: "sk" }
|
|
110
|
-
): this {
|
|
111
|
-
this.lambderSessionManager = new LambderSessionManager({
|
|
112
|
-
tableName, tableRegion, partitionKey, sortKey, sessionSalt, enableSlidingExpiration
|
|
113
|
-
});
|
|
114
|
-
return this;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): this {
|
|
118
|
-
this.sessionTokenCookieKey = sessionTokenCookieKey;
|
|
119
|
-
this.sessionCsrfCookieKey = sessionCsrfCookieKey;
|
|
120
|
-
return this;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction): this {
|
|
124
|
-
this.routeFallbackHandler = routeFallbackHandler;
|
|
125
|
-
return this;
|
|
126
|
-
}
|
|
127
|
-
setApiFallbackHandler(apiFallbackHandler: ApiFallbackHandlerFunction): this {
|
|
128
|
-
this.apiFallbackHandler = apiFallbackHandler;
|
|
129
|
-
return this;
|
|
130
|
-
}
|
|
131
|
-
setApiInputValidationErrorHandler(apiInputValidationErrorHandler: ApiInputValidationErrorHandlerFunction): this {
|
|
132
|
-
this.apiInputValidationErrorHandler = apiInputValidationErrorHandler;
|
|
133
|
-
return this;
|
|
134
|
-
}
|
|
135
|
-
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction): this {
|
|
136
|
-
this.globalErrorHandler = globalErrorHandler;
|
|
137
|
-
return this;
|
|
138
|
-
}
|
|
139
|
-
private getPatternMatch(pattern: string, path: string): Record<string, any> {
|
|
140
|
-
const result = (match(pattern, { decode: decodeURIComponent }))(path);
|
|
141
|
-
if(!result) return {};
|
|
142
|
-
return result?.params || {};
|
|
143
|
-
}
|
|
144
|
-
private testPatternMatch(pattern: string, path: string): boolean{
|
|
145
|
-
return (match(pattern, { decode: decodeURIComponent }))(path) !== false;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private async handleNoMatchedAction(ctx: LambderRenderContext<any>, resolver: LambderResolver){
|
|
149
|
-
for(const hook of this.hookList["fallback"]){ await hook.hookFn(ctx, resolver); }
|
|
150
|
-
|
|
151
|
-
const isAPI = ctx.path === this.apiPath;
|
|
152
|
-
if(isAPI && this.apiFallbackHandler){
|
|
153
|
-
resolver.resolve(await this.apiFallbackHandler(ctx, resolver));
|
|
154
|
-
}else if(isAPI){
|
|
155
|
-
resolver.resolve({ statusCode: 204, body: "API handler not set.", })
|
|
156
|
-
}else if(this.routeFallbackHandler){
|
|
157
|
-
resolver.resolve(await this.routeFallbackHandler(ctx, resolver));
|
|
158
|
-
}else{
|
|
159
|
-
resolver.resolve({ statusCode: 204, body: "Route handler not set.", })
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
addRoute(condition: Path|ConditionFunction|RegExp, actionFn: ActionFunction): this {
|
|
164
|
-
this.actionList.push({
|
|
165
|
-
conditionFn: (ctx:LambderRenderContext<any>) => (
|
|
166
|
-
(
|
|
167
|
-
(typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
|
|
168
|
-
(typeof condition === "function" && condition(ctx)) ||
|
|
169
|
-
(condition?.constructor == RegExp && condition.test(ctx.path))
|
|
170
|
-
)
|
|
171
|
-
),
|
|
172
|
-
actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
|
|
173
|
-
if(typeof condition === "string"){
|
|
174
|
-
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
175
|
-
}else if(condition?.constructor == RegExp){
|
|
176
|
-
const match = ctx.path.match(condition);
|
|
177
|
-
ctx.pathParams = match ? (match.groups || (match as unknown as Record<string, any>)) : {};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return await actionFn(ctx, resolver);
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
return this;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
addSessionRoute(condition: Path|ConditionFunction|RegExp, actionFn: SessionActionFunction<TSessionData>): this {
|
|
187
|
-
this.actionList.push({
|
|
188
|
-
conditionFn: (ctx:LambderRenderContext<any>) => (
|
|
189
|
-
(
|
|
190
|
-
(typeof condition === "string" && this.testPatternMatch(condition, ctx.path)) ||
|
|
191
|
-
(typeof condition === "function" && condition(ctx)) ||
|
|
192
|
-
(condition?.constructor == RegExp && condition.test(ctx.path))
|
|
193
|
-
)
|
|
194
|
-
),
|
|
195
|
-
actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
|
|
196
|
-
if(typeof condition === "string"){
|
|
197
|
-
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
198
|
-
}else if(condition?.constructor == RegExp){
|
|
199
|
-
const match = ctx.path.match(condition);
|
|
200
|
-
ctx.pathParams = match ? (match.groups || (match as unknown as Record<string, any>)) : {};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
const sessionCtx = ctx as unknown as LambderSessionRenderContext<any, TSessionData>;
|
|
204
|
-
await this.getSessionController(ctx).fetchSession();
|
|
205
|
-
if(!sessionCtx.session){ throw new Error("Session not found."); }
|
|
206
|
-
|
|
207
|
-
return await actionFn(sessionCtx, resolver);
|
|
208
|
-
}
|
|
209
|
-
});
|
|
210
|
-
return this;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Plugin system
|
|
214
|
-
public use<_TNewContract extends Record<string, any>>(
|
|
215
|
-
plugin: (lambder: Lambder<TSessionData, _TContract>) => Lambder<TSessionData, _TNewContract>
|
|
216
|
-
): Lambder<TSessionData, _TNewContract extends _TContract ? _TNewContract : (_TContract & _TNewContract)> {
|
|
217
|
-
return plugin(this) as any;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Typed API with Zod
|
|
221
|
-
public addApi<
|
|
222
|
-
TName extends string,
|
|
223
|
-
TInput extends z.ZodTypeAny,
|
|
224
|
-
TOutput extends z.ZodTypeAny
|
|
225
|
-
>(
|
|
226
|
-
name: TName,
|
|
227
|
-
schema: { input: TInput, output: TOutput },
|
|
228
|
-
handler: (
|
|
229
|
-
ctx: LambderRenderContext<z.infer<TInput>>,
|
|
230
|
-
resolver: LambderResolver<z.infer<TOutput>>
|
|
231
|
-
) => LambderResolverResponse | Promise<LambderResolverResponse>
|
|
232
|
-
): Lambder<TSessionData, MergeContract<_TContract, TName, z.infer<TInput>, z.infer<TOutput>>> {
|
|
233
|
-
this.actionList.push({
|
|
234
|
-
conditionFn: (ctx:LambderRenderContext<any>) => ctx.apiName === name,
|
|
235
|
-
actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
|
|
236
|
-
// Validate Input
|
|
237
|
-
const inputResult = schema.input.safeParse(ctx.apiPayload);
|
|
238
|
-
if (!inputResult.success) {
|
|
239
|
-
if (this.apiInputValidationErrorHandler) {
|
|
240
|
-
return await this.apiInputValidationErrorHandler(ctx, resolver, inputResult.error);
|
|
241
|
-
}
|
|
242
|
-
return resolver.raw({
|
|
243
|
-
statusCode: 422,
|
|
244
|
-
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
245
|
-
multiValueHeaders: { "Content-Type": ["application/json"] }
|
|
246
|
-
});
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Run Handler with validated data
|
|
250
|
-
ctx.apiPayload = inputResult.data;
|
|
251
|
-
return await handler(ctx, resolver);
|
|
252
|
-
},
|
|
253
|
-
});
|
|
254
|
-
return this as any;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// Typed Session API with Zod
|
|
258
|
-
public addSessionApi<
|
|
259
|
-
TName extends string,
|
|
260
|
-
TInput extends z.ZodTypeAny,
|
|
261
|
-
TOutput extends z.ZodTypeAny
|
|
262
|
-
>(
|
|
263
|
-
name: TName,
|
|
264
|
-
schema: { input: TInput, output: TOutput },
|
|
265
|
-
handler: (
|
|
266
|
-
ctx: LambderSessionRenderContext<z.infer<TInput>, TSessionData>,
|
|
267
|
-
resolver: LambderResolver<z.infer<TOutput>>
|
|
268
|
-
) => LambderResolverResponse | Promise<LambderResolverResponse>
|
|
269
|
-
): Lambder<TSessionData, MergeContract<_TContract, TName, z.infer<TInput>, z.infer<TOutput>>> {
|
|
270
|
-
this.actionList.push({
|
|
271
|
-
conditionFn: (ctx:LambderRenderContext<any>) => ctx.apiName === name,
|
|
272
|
-
actionFn: async (ctx:LambderRenderContext<any>, resolver: LambderResolver) => {
|
|
273
|
-
const sessionCtx = ctx as unknown as LambderSessionRenderContext<any, TSessionData>;
|
|
274
|
-
await this.getSessionController(ctx).fetchSession();
|
|
275
|
-
if(!sessionCtx.session){ throw new Error("Session not found."); }
|
|
276
|
-
|
|
277
|
-
// Validate Input
|
|
278
|
-
const inputResult = schema.input.safeParse(ctx.apiPayload);
|
|
279
|
-
if (!inputResult.success) {
|
|
280
|
-
if (this.apiInputValidationErrorHandler) {
|
|
281
|
-
return await this.apiInputValidationErrorHandler(ctx, resolver, inputResult.error);
|
|
282
|
-
}
|
|
283
|
-
return resolver.raw({
|
|
284
|
-
statusCode: 400,
|
|
285
|
-
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
286
|
-
multiValueHeaders: { "Content-Type": ["application/json"] }
|
|
287
|
-
});
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
// Run Handler with validated data
|
|
291
|
-
ctx.apiPayload = inputResult.data;
|
|
292
|
-
return await handler(sessionCtx, resolver);
|
|
293
|
-
}
|
|
294
|
-
});
|
|
295
|
-
return this as any;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<this>;
|
|
299
|
-
addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): this;
|
|
300
|
-
addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): this;
|
|
301
|
-
addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): this;
|
|
302
|
-
addHook(
|
|
303
|
-
hookEvent:HookEventType,
|
|
304
|
-
hookFn: HookCreatedFunction & HookBeforeRenderFunction & HookAfterRenderFunction & HookFallbackFunction,
|
|
305
|
-
priority = 0
|
|
306
|
-
): this | Promise<this> {
|
|
307
|
-
if(hookEvent === "created"){
|
|
308
|
-
return hookFn(this).then(() => this);
|
|
309
|
-
}else{
|
|
310
|
-
this.hookList[hookEvent].push({ priority, hookFn });
|
|
311
|
-
this.hookList[hookEvent].sort((a, b) => a.priority - b.priority);
|
|
312
|
-
return this;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
getSessionController(ctx: LambderRenderContext<any> | LambderSessionRenderContext<any, TSessionData>): LambderSessionController<TSessionData>{
|
|
317
|
-
if(!this.lambderSessionManager) throw new Error("Session is not enabled. Use lambder.enableDdbSession(...) to enable.");
|
|
318
|
-
|
|
319
|
-
return new LambderSessionController<TSessionData>({
|
|
320
|
-
lambderSessionManager: this.lambderSessionManager,
|
|
321
|
-
sessionTokenCookieKey: this.sessionTokenCookieKey,
|
|
322
|
-
sessionCsrfCookieKey: this.sessionCsrfCookieKey,
|
|
323
|
-
ctx,
|
|
324
|
-
});
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
getResponseBuilder(){
|
|
328
|
-
return new LambderResponseBuilder({
|
|
329
|
-
isCorsEnabled: this.isCorsEnabled,
|
|
330
|
-
publicPath: this.publicPath,
|
|
331
|
-
apiVersion: this.apiVersion,
|
|
332
|
-
lambderUtils: this.utils,
|
|
333
|
-
});
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
private getResolver(
|
|
337
|
-
ctx: LambderRenderContext<any>,
|
|
338
|
-
resolve: (response: LambderResolverResponse) => void,
|
|
339
|
-
reject: (err: Error) => void
|
|
340
|
-
){
|
|
341
|
-
return new LambderResolver({
|
|
342
|
-
isCorsEnabled: this.isCorsEnabled,
|
|
343
|
-
publicPath: this.publicPath,
|
|
344
|
-
apiVersion: this.apiVersion,
|
|
345
|
-
lambderUtils: this.utils,
|
|
346
|
-
ctx, resolve, reject
|
|
347
|
-
});
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
getHandler() {
|
|
351
|
-
return (event: APIGatewayProxyEvent, context: Context) => this.render(event, context);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
async render(
|
|
355
|
-
event: APIGatewayProxyEvent,
|
|
356
|
-
lambdaContext: Context
|
|
357
|
-
): Promise<LambderResolverResponse>{
|
|
358
|
-
let eventRenderContext:LambderRenderContext<any>|null = null;
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
let ctx = createContext(event, lambdaContext, this.apiPath);
|
|
362
|
-
eventRenderContext = ctx;
|
|
363
|
-
|
|
364
|
-
return await new Promise(async (
|
|
365
|
-
resolve:(response: LambderResolverResponse)=>void,
|
|
366
|
-
reject:(err: Error)=>void
|
|
367
|
-
)=> {
|
|
368
|
-
try{
|
|
369
|
-
const resolver = this.getResolver(ctx, resolve, reject);
|
|
370
|
-
if(ctx.method === "OPTIONS") return resolver.cors();
|
|
371
|
-
|
|
372
|
-
const firstMatchedAction = this.actionList.find(action => action.conditionFn(ctx));
|
|
373
|
-
if(firstMatchedAction){
|
|
374
|
-
// Check version if provided by the client and the server
|
|
375
|
-
if(this.apiVersion && ctx._otherInternal.requestVersion){
|
|
376
|
-
if(ctx._otherInternal.requestVersion !== this.apiVersion){
|
|
377
|
-
const responseBuilder = this.getResponseBuilder();
|
|
378
|
-
return resolve(responseBuilder.versionExpired());
|
|
379
|
-
}
|
|
380
|
-
};
|
|
381
|
-
// Run beforeRender hooks
|
|
382
|
-
for(const hook of this.hookList["beforeRender"]){
|
|
383
|
-
const hookCtx = await hook.hookFn(ctx, resolver);
|
|
384
|
-
if(hookCtx instanceof Error){ throw hookCtx; }
|
|
385
|
-
ctx = hookCtx;
|
|
386
|
-
}
|
|
387
|
-
// Run matched action
|
|
388
|
-
let response = await firstMatchedAction.actionFn(ctx as any, resolver);
|
|
389
|
-
// Run afterRender hooks
|
|
390
|
-
for(const hook of this.hookList["afterRender"]){
|
|
391
|
-
const hookResponse = await hook.hookFn(ctx, resolver, response);
|
|
392
|
-
if(hookResponse instanceof Error){ throw hookResponse; }
|
|
393
|
-
response = hookResponse;
|
|
394
|
-
}
|
|
395
|
-
// Apply setHeader, addHeader values.
|
|
396
|
-
response.multiValueHeaders = response.multiValueHeaders || {};
|
|
397
|
-
for(const header of ctx._otherInternal.setHeaderFnAccumulator){
|
|
398
|
-
response.multiValueHeaders[header.key] = Array.isArray(header.value) ? header.value: [header.value];
|
|
399
|
-
}
|
|
400
|
-
for(const header of ctx._otherInternal.addHeaderFnAccumulator){
|
|
401
|
-
response.multiValueHeaders[header.key] = response.multiValueHeaders[header.key] || [];
|
|
402
|
-
response.multiValueHeaders[header.key].push(header.value);
|
|
403
|
-
}
|
|
404
|
-
resolve(response);
|
|
405
|
-
}else{
|
|
406
|
-
return this.handleNoMatchedAction(ctx, resolver);
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
} catch(err){
|
|
410
|
-
const wrappedError = err instanceof Error ? err : new Error("Error: " + String(err));
|
|
411
|
-
reject(wrappedError);
|
|
412
|
-
}
|
|
413
|
-
})
|
|
414
|
-
}catch(err){
|
|
415
|
-
if(this.globalErrorHandler){
|
|
416
|
-
const wrappedError = err instanceof Error ? err : new Error("Error: " + String(err));
|
|
417
|
-
const responseBuilder = this.getResponseBuilder();
|
|
418
|
-
return this.globalErrorHandler(
|
|
419
|
-
wrappedError,
|
|
420
|
-
eventRenderContext,
|
|
421
|
-
responseBuilder,
|
|
422
|
-
eventRenderContext?._otherInternal.logToApiResponseAccumulator
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
return { statusCode: 500, body: "Internal Server Error.", }
|
|
426
|
-
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
}
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lambder API Contract System
|
|
3
|
-
*
|
|
4
|
-
* Contracts are built via method chaining and inferred using typeof lambder.ApiContract
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Base shape for API contracts - used by LambderCaller and LambderMSW
|
|
9
|
-
*/
|
|
10
|
-
export type ApiContractShape = Record<string, {
|
|
11
|
-
input: any;
|
|
12
|
-
output: any;
|
|
13
|
-
}>;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Helper type for merging new API into existing contract during chaining
|
|
17
|
-
*/
|
|
18
|
-
export type MergeContract<Old, Name extends string, In, Out> =
|
|
19
|
-
Old & { [K in Name]: { input: In, output: Out } };
|
|
20
|
-
|
package/src/LambderCaller.ts
DELETED
|
@@ -1,238 +0,0 @@
|
|
|
1
|
-
import Cookies from 'js-cookie';
|
|
2
|
-
import { LambderApiResponse } from './LambderResponseBuilder';
|
|
3
|
-
import type { ApiContractShape } from './LambderApiContract';
|
|
4
|
-
import type { z } from "zod";
|
|
5
|
-
|
|
6
|
-
type VoidFunction = ()=>void|Promise<void>;
|
|
7
|
-
type FetchTracker = { apiName: string, done: boolean, fetchEndCalled: boolean };
|
|
8
|
-
type EventHandlerFetchParams = {
|
|
9
|
-
apiName: string,
|
|
10
|
-
payload?: any,
|
|
11
|
-
headers?: Record<string, any>
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
type FetchStartEventHandler = (params: {
|
|
15
|
-
fetchParams: EventHandlerFetchParams,
|
|
16
|
-
activeFetchList: FetchTracker[],
|
|
17
|
-
})=>void|Promise<void>;
|
|
18
|
-
|
|
19
|
-
type FetchEndEventHandler = (params: {
|
|
20
|
-
fetchParams: EventHandlerFetchParams,
|
|
21
|
-
fetchResult: any,
|
|
22
|
-
activeFetchList: FetchTracker[],
|
|
23
|
-
})=>void|Promise<void>;
|
|
24
|
-
|
|
25
|
-
type ErrorHandler = (err: Error) => void|Promise<void>;
|
|
26
|
-
type ValidationErrorHandler = (zodError: z.ZodError) => (void|false)|Promise<(void|false)>;
|
|
27
|
-
type MessageHandler = (message:any) => void|Promise<void>;
|
|
28
|
-
|
|
29
|
-
export default class LambderCaller<TContract extends ApiContractShape = any> {
|
|
30
|
-
private isCorsEnabled: boolean;
|
|
31
|
-
private apiPath: string;
|
|
32
|
-
private apiVersion?: string;
|
|
33
|
-
|
|
34
|
-
fetchTrackerList: FetchTracker[] = [];
|
|
35
|
-
isLoading: boolean = false;
|
|
36
|
-
|
|
37
|
-
private versionExpiredHandler?: VoidFunction;
|
|
38
|
-
private sessionExpiredHandler?: VoidFunction;
|
|
39
|
-
|
|
40
|
-
private messageHandler?: MessageHandler;
|
|
41
|
-
private errorMessageHandler?: MessageHandler;
|
|
42
|
-
private notAuthorizedHandler?: VoidFunction;
|
|
43
|
-
private errorHandler?: ErrorHandler;
|
|
44
|
-
private apiInputValidationErrorHandler?: ValidationErrorHandler;
|
|
45
|
-
|
|
46
|
-
private fetchStartedHandler?: FetchStartEventHandler;
|
|
47
|
-
private fetchEndedHandler?: FetchEndEventHandler;
|
|
48
|
-
|
|
49
|
-
private sessionTokenCookieKey = "LMDRSESSIONTKID";
|
|
50
|
-
private sessionCsrfCookieKey = "LMDRSESSIONCSTK";
|
|
51
|
-
|
|
52
|
-
constructor(
|
|
53
|
-
{
|
|
54
|
-
apiPath, apiVersion,
|
|
55
|
-
isCorsEnabled = false,
|
|
56
|
-
versionExpiredHandler, sessionExpiredHandler,
|
|
57
|
-
messageHandler, errorMessageHandler,
|
|
58
|
-
notAuthorizedHandler, errorHandler,
|
|
59
|
-
fetchStartedHandler, fetchEndedHandler,
|
|
60
|
-
apiInputValidationErrorHandler,
|
|
61
|
-
}:
|
|
62
|
-
{
|
|
63
|
-
apiPath: string,
|
|
64
|
-
apiVersion?: string,
|
|
65
|
-
isCorsEnabled: boolean,
|
|
66
|
-
versionExpiredHandler?: VoidFunction,
|
|
67
|
-
sessionExpiredHandler?: VoidFunction,
|
|
68
|
-
messageHandler?: MessageHandler,
|
|
69
|
-
errorMessageHandler?: MessageHandler,
|
|
70
|
-
notAuthorizedHandler?: VoidFunction,
|
|
71
|
-
errorHandler?: ErrorHandler,
|
|
72
|
-
fetchStartedHandler?: FetchStartEventHandler,
|
|
73
|
-
fetchEndedHandler?: FetchEndEventHandler,
|
|
74
|
-
apiInputValidationErrorHandler?: ValidationErrorHandler,
|
|
75
|
-
}
|
|
76
|
-
){
|
|
77
|
-
this.apiPath = apiPath ?? "/api";
|
|
78
|
-
this.apiVersion = apiVersion;
|
|
79
|
-
this.isCorsEnabled = isCorsEnabled;
|
|
80
|
-
|
|
81
|
-
this.versionExpiredHandler = versionExpiredHandler;
|
|
82
|
-
this.sessionExpiredHandler = sessionExpiredHandler;
|
|
83
|
-
|
|
84
|
-
this.messageHandler = messageHandler;
|
|
85
|
-
this.errorMessageHandler = errorMessageHandler;
|
|
86
|
-
this.notAuthorizedHandler = notAuthorizedHandler;
|
|
87
|
-
this.errorHandler = errorHandler;
|
|
88
|
-
this.apiInputValidationErrorHandler = apiInputValidationErrorHandler;
|
|
89
|
-
|
|
90
|
-
this.fetchStartedHandler = fetchStartedHandler;
|
|
91
|
-
this.fetchEndedHandler = fetchEndedHandler;
|
|
92
|
-
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string){
|
|
96
|
-
this.sessionTokenCookieKey = sessionTokenCookieKey;
|
|
97
|
-
this.sessionCsrfCookieKey = sessionCsrfCookieKey;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async apiRaw<
|
|
101
|
-
TApiName extends keyof TContract & string = string,
|
|
102
|
-
TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any
|
|
103
|
-
>(
|
|
104
|
-
apiName: TApiName,
|
|
105
|
-
payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any,
|
|
106
|
-
options?: {
|
|
107
|
-
headers?: Record<string, any>
|
|
108
|
-
versionExpiredHandler?: VoidFunction,
|
|
109
|
-
sessionExpiredHandler?: VoidFunction,
|
|
110
|
-
messageHandler?: MessageHandler,
|
|
111
|
-
errorMessageHandler?: MessageHandler,
|
|
112
|
-
apiInputValidationErrorHandler?: ValidationErrorHandler,
|
|
113
|
-
notAuthorizedHandler?: VoidFunction,
|
|
114
|
-
errorHandler?: ErrorHandler,
|
|
115
|
-
fetchStartedHandler?: FetchStartEventHandler,
|
|
116
|
-
fetchEndedHandler?: FetchEndEventHandler,
|
|
117
|
-
}
|
|
118
|
-
): Promise<LambderApiResponse<TOutput>|null|undefined>{
|
|
119
|
-
const headers = options?.headers;
|
|
120
|
-
const fetchTracker: FetchTracker = { apiName, done: false, fetchEndCalled: false };
|
|
121
|
-
try {
|
|
122
|
-
this.fetchTrackerList.push(fetchTracker);
|
|
123
|
-
if(this.fetchStartedHandler) await this.fetchStartedHandler({
|
|
124
|
-
fetchParams: { apiName, payload, headers, },
|
|
125
|
-
activeFetchList: this.fetchTrackerList.filter(v=>!v.done)
|
|
126
|
-
});
|
|
127
|
-
const version = this.apiVersion;
|
|
128
|
-
const token = Cookies.get(this.sessionCsrfCookieKey) || "";
|
|
129
|
-
const siteHost = window.location.hostname;
|
|
130
|
-
let data = await fetch(this.apiPath, {
|
|
131
|
-
method: 'POST', mode: 'same-origin', cache: 'no-cache',
|
|
132
|
-
credentials: 'same-origin', redirect: 'follow', referrerPolicy: 'origin',
|
|
133
|
-
headers: { 'Content-Type': 'application/json', ...(headers || {}) },
|
|
134
|
-
body: JSON.stringify({ apiName, version, token, siteHost, payload, }),
|
|
135
|
-
}).then(async (res)=>{
|
|
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
|
-
};
|
|
146
|
-
if(res.headers.get("Content-Type")?.includes("application/lambder-json-stream")){
|
|
147
|
-
const decompressed = res.json();
|
|
148
|
-
return decompressed;
|
|
149
|
-
}else{
|
|
150
|
-
return res.json();
|
|
151
|
-
}
|
|
152
|
-
}) as LambderApiResponse<TOutput>;
|
|
153
|
-
fetchTracker.done = true;
|
|
154
|
-
if(this.fetchEndedHandler){
|
|
155
|
-
fetchTracker.fetchEndCalled = true;
|
|
156
|
-
await this.fetchEndedHandler({
|
|
157
|
-
fetchParams: { apiName, payload, headers },
|
|
158
|
-
fetchResult: data,
|
|
159
|
-
activeFetchList: this.fetchTrackerList.filter(v=>!v.done),
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
if(data && data.logList){
|
|
163
|
-
for(const record of data.logList){
|
|
164
|
-
// Log to API response for debugging
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
if(data && data.versionExpired){
|
|
168
|
-
if(this.versionExpiredHandler){
|
|
169
|
-
await this.versionExpiredHandler();
|
|
170
|
-
}else if(this.errorHandler){
|
|
171
|
-
await this.errorHandler(new Error("Version Expired; Please refresh;"));
|
|
172
|
-
}
|
|
173
|
-
return null;
|
|
174
|
-
}
|
|
175
|
-
if(data && data.sessionExpired){
|
|
176
|
-
Cookies.set(this.sessionTokenCookieKey, '', { expires: -1 });
|
|
177
|
-
Cookies.set(this.sessionCsrfCookieKey, '', { expires: -1 });
|
|
178
|
-
if(this.sessionExpiredHandler){
|
|
179
|
-
await this.sessionExpiredHandler();
|
|
180
|
-
}else if(this.errorHandler){
|
|
181
|
-
await this.errorHandler(new Error("Version Expired; Please refresh;"));
|
|
182
|
-
}
|
|
183
|
-
return null;
|
|
184
|
-
}
|
|
185
|
-
if(data && data.notAuthorized){
|
|
186
|
-
if(this.notAuthorizedHandler){
|
|
187
|
-
await this.notAuthorizedHandler();
|
|
188
|
-
}else if(this.errorHandler){
|
|
189
|
-
await this.errorHandler(new Error("Not Authorized;"));
|
|
190
|
-
}
|
|
191
|
-
return null;
|
|
192
|
-
}
|
|
193
|
-
if(data && data.message && this.messageHandler){
|
|
194
|
-
await this.messageHandler(data.message);
|
|
195
|
-
}
|
|
196
|
-
if(data && data.errorMessage && this.errorMessageHandler){
|
|
197
|
-
await this.errorMessageHandler(data.errorMessage);
|
|
198
|
-
}
|
|
199
|
-
return data;
|
|
200
|
-
}catch(err){
|
|
201
|
-
const wrappedError = err instanceof Error ? err : new Error("Error: ", { cause: err });
|
|
202
|
-
fetchTracker.done = true;
|
|
203
|
-
if(!fetchTracker.fetchEndCalled && this.fetchEndedHandler){
|
|
204
|
-
await this.fetchEndedHandler({
|
|
205
|
-
fetchParams: { apiName, payload, headers, },
|
|
206
|
-
fetchResult: wrappedError,
|
|
207
|
-
activeFetchList: this.fetchTrackerList.filter(v=>!v.done)
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
if(this.errorHandler){ this.errorHandler(wrappedError); }
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
};
|
|
214
|
-
|
|
215
|
-
// Use the same type for api but adjust the return type
|
|
216
|
-
async api<
|
|
217
|
-
TApiName extends keyof TContract & string = string,
|
|
218
|
-
TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any
|
|
219
|
-
>(
|
|
220
|
-
apiName: TApiName,
|
|
221
|
-
payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any,
|
|
222
|
-
options?: {
|
|
223
|
-
headers?: Record<string, any>
|
|
224
|
-
versionExpiredHandler?: VoidFunction,
|
|
225
|
-
sessionExpiredHandler?: VoidFunction,
|
|
226
|
-
messageHandler?: MessageHandler,
|
|
227
|
-
errorMessageHandler?: MessageHandler,
|
|
228
|
-
notAuthorizedHandler?: VoidFunction,
|
|
229
|
-
errorHandler?: ErrorHandler,
|
|
230
|
-
fetchStartedHandler?: FetchStartEventHandler,
|
|
231
|
-
fetchEndedHandler?: FetchEndEventHandler,
|
|
232
|
-
}
|
|
233
|
-
): Promise<TOutput|null|undefined> {
|
|
234
|
-
const result = await this.apiRaw<TApiName, TOutput>(apiName, payload, options);
|
|
235
|
-
return result?.payload;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
}
|