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.
Files changed (67) hide show
  1. package/Readme.md +162 -41
  2. package/dist/Lambder.d.ts +154 -46
  3. package/dist/Lambder.js +312 -166
  4. package/dist/LambderCaller.js +6 -3
  5. package/dist/LambderContext.d.ts +20 -9
  6. package/dist/LambderContext.js +57 -17
  7. package/dist/LambderCors.d.ts +12 -0
  8. package/dist/LambderCors.js +30 -0
  9. package/dist/LambderHtml.d.ts +33 -0
  10. package/dist/LambderHtml.js +62 -0
  11. package/dist/LambderMSW.d.ts +16 -1
  12. package/dist/LambderMSW.js +5 -9
  13. package/dist/LambderPublicFiles.d.ts +47 -0
  14. package/dist/LambderPublicFiles.js +108 -0
  15. package/dist/LambderResolver.d.ts +30 -31
  16. package/dist/LambderResolver.js +29 -43
  17. package/dist/LambderResponse.d.ts +71 -0
  18. package/dist/LambderResponse.js +196 -0
  19. package/dist/LambderResponseBuilder.d.ts +58 -33
  20. package/dist/LambderResponseBuilder.js +114 -167
  21. package/dist/LambderRouting.d.ts +23 -0
  22. package/dist/LambderRouting.js +67 -0
  23. package/dist/LambderSessionController.d.ts +13 -1
  24. package/dist/LambderSessionController.js +33 -10
  25. package/dist/LambderSessionManager.d.ts +3 -1
  26. package/dist/LambderSessionManager.js +15 -6
  27. package/dist/LambderTemplatingEngine.d.ts +87 -0
  28. package/dist/LambderTemplatingEngine.js +156 -0
  29. package/dist/index.d.ts +14 -2
  30. package/dist/index.js +10 -1
  31. package/dist/node-polyfills.d.ts +4 -2
  32. package/dist/node-polyfills.js +28 -0
  33. package/package.json +7 -5
  34. package/.eslintrc.cjs +0 -26
  35. package/.vscode/settings.json +0 -26
  36. package/deploy +0 -22
  37. package/dist/LambderUtils.d.ts +0 -10
  38. package/dist/LambderUtils.js +0 -70
  39. package/docs/DYNAMODB_SETUP.md +0 -96
  40. package/docs/LAMBDER_MSW.md +0 -409
  41. package/docs/TYPE_SAFE_QUICK_START.md +0 -77
  42. package/examples/msw-testing-example.ts +0 -280
  43. package/examples/secure-session-example.ts +0 -207
  44. package/examples/zod-chained-api-example.ts +0 -63
  45. package/src/Lambder.ts +0 -430
  46. package/src/LambderApiContract.ts +0 -20
  47. package/src/LambderCaller.ts +0 -238
  48. package/src/LambderContext.ts +0 -78
  49. package/src/LambderMSW.ts +0 -180
  50. package/src/LambderResolver.ts +0 -101
  51. package/src/LambderResponseBuilder.ts +0 -332
  52. package/src/LambderSessionController.ts +0 -114
  53. package/src/LambderSessionManager.ts +0 -217
  54. package/src/LambderUtils.ts +0 -75
  55. package/src/index.ts +0 -17
  56. package/src/node-polyfills.ts +0 -27
  57. package/tests/error-handling.test.ts +0 -585
  58. package/tests/file-serving.test.ts +0 -194
  59. package/tests/fixtures/public/index.html +0 -1
  60. package/tests/fixtures/public/main.css +0 -1
  61. package/tests/hooks.test.ts +0 -561
  62. package/tests/output-type-runtime.test.ts +0 -381
  63. package/tests/redirect.test.ts +0 -88
  64. package/tests/routes.test.ts +0 -543
  65. package/tests/session.test.ts +0 -1083
  66. package/tests/use-plugin.test.ts +0 -460
  67. 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
-
@@ -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
- }