lambder 1.0.124 → 1.0.126

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Readme.md CHANGED
@@ -43,7 +43,7 @@ lambder.enableDdbSession({
43
43
  });
44
44
 
45
45
  // Enable Cors
46
- lambder.setIsCorsEnabled(true);
46
+ lambder.enableCors(true);
47
47
 
48
48
  // Define a simple api
49
49
  lambder.addApi("getCompanyPage", async ({ apiPayload }, res) => {
@@ -485,6 +485,65 @@ const loadPageData = async () => {
485
485
  };
486
486
  ```
487
487
 
488
+ ## Type-Safe APIs (Optional)
489
+
490
+ Want compile-time type checking for your APIs? It's incredibly simple!
491
+
492
+ ### 1. Define Your API Contract
493
+
494
+ ```typescript
495
+ // shared/apiContract.ts
496
+ import type { ApiContract } from 'lambder';
497
+
498
+ export type MyApiContract = {
499
+ getUserById: { input: { userId: string }, output: User },
500
+ createUser: { input: CreateUserInput, output: User },
501
+ listUsers: { input: void, output: User[] }
502
+ } satisfies ApiContract;
503
+ ```
504
+
505
+ ### 2. Backend - Pass Type to Constructor
506
+
507
+ ```typescript
508
+ import Lambder from 'lambder';
509
+ import type { MyApiContract } from './shared/apiContract';
510
+
511
+ const lambder = new Lambder<MyApiContract>({ publicPath: './public', apiPath: '/api' });
512
+
513
+ // Now addApi is type-safe!
514
+ lambder.addApi('getUserById', async (ctx, resolver) => {
515
+ // ctx.apiPayload is automatically typed as { userId: string } ✨
516
+ const user = await db.getUser(ctx.apiPayload.userId);
517
+ return resolver.api(user);
518
+ });
519
+ ```
520
+
521
+ ### 3. Frontend - Pass Type to Constructor
522
+
523
+ ```typescript
524
+ import { LambderCaller } from 'lambder';
525
+ import type { MyApiContract } from './shared/apiContract';
526
+
527
+ const caller = new LambderCaller<MyApiContract>({ apiPath: '/api', isCorsEnabled: false });
528
+
529
+ // Now api() is type-safe with full autocomplete! ✨
530
+ const user = await caller.api('getUserById', { userId: '123' });
531
+ // ↑ IDE shows all available APIs
532
+ // ↑ Type-checked input
533
+ // user is typed as User | null | undefined
534
+ ```
535
+
536
+ ### Benefits
537
+
538
+ ✅ **Simple** - Just pass type to constructor, that's it!
539
+ ✅ **Autocomplete** - IDE suggests available APIs as you type
540
+ ✅ **Type Safety** - Inputs and outputs are fully typed
541
+ ✅ **No Wrappers** - Use existing `api()` and `addApi()` methods
542
+ ✅ **Opt-In** - Add when you want, skip when you don't
543
+ ✅ **Zero Overhead** - Pure TypeScript types, no runtime code
544
+
545
+ 📖 **[Read the Quick Start Guide](docs/TYPE_SAFE_QUICK_START.md)** for more details and examples!
546
+
488
547
  ## Contributing
489
548
 
490
549
  Contributions are welcome! Especially for documentation. If you have an idea for an improvement or have found a bug, please open an issue or submit a pull request.
package/dist/Lambder.d.ts CHANGED
@@ -4,6 +4,7 @@ import LambderResponseBuilder, { LambderResolverResponse } from "./LambderRespon
4
4
  import LambderUtils from "./LambderUtils.js";
5
5
  import { type LambderSessionContext } from "./LambderSessionManager.js";
6
6
  import LambderSessionController from "./LambderSessionController.js";
7
+ import type { ApiContract } from "./LambderApiContract.js";
7
8
  type Path = `/${string}`;
8
9
  export type LambderRenderContext = {
9
10
  host: string;
@@ -17,6 +18,7 @@ export type LambderRenderContext = {
17
18
  apiPayload: any;
18
19
  headers: APIGatewayProxyEventHeaders;
19
20
  session: LambderSessionContext | null;
21
+ event: APIGatewayProxyEvent;
20
22
  lambdaContext: Context;
21
23
  _otherInternal: {
22
24
  isApiCall: boolean;
@@ -42,7 +44,7 @@ type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext | null,
42
44
  type RouteFallbackHandlerFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse;
43
45
  type ApiFallbackHandlerFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse;
44
46
  export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext;
45
- export default class Lambder {
47
+ export default class Lambder<TContract extends ApiContract = any> {
46
48
  apiPath: string;
47
49
  apiVersion: null | string;
48
50
  isCorsEnabled: boolean;
@@ -63,7 +65,7 @@ export default class Lambder {
63
65
  ejsPath?: string;
64
66
  apiVersion?: string;
65
67
  });
66
- setIsCorsEnabled(isCorsEnabled: boolean): void;
68
+ enableCors(isCorsEnabled: boolean): void;
67
69
  enableDdbSession({ tableName, tableRegion, sessionSalt }: {
68
70
  tableName: string;
69
71
  tableRegion: string;
@@ -85,8 +87,16 @@ export default class Lambder {
85
87
  }>): Promise<void>;
86
88
  addRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
87
89
  addSessionRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
88
- addApi(apiName: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
89
- addSessionApi(apiName: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
90
+ addApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
91
+ addApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext & {
92
+ apiPayload: TContract[TApiName]['input'];
93
+ }, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
94
+ addApi(apiName: string, actionFn: ActionFunction): void;
95
+ addSessionApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
96
+ addSessionApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext & {
97
+ apiPayload: TContract[TApiName]['input'];
98
+ }, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
99
+ addSessionApi(apiName: string, actionFn: ActionFunction): void;
90
100
  addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
91
101
  addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
92
102
  addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
package/dist/Lambder.js CHANGED
@@ -33,7 +33,7 @@ export const createContext = (event, lambdaContext, apiPath) => {
33
33
  const apiPayload = isApiCall ? post.payload : null;
34
34
  return {
35
35
  host, path, pathParams, method,
36
- get, post, cookie,
36
+ get, post, cookie, event,
37
37
  apiName, apiPayload,
38
38
  headers, session, lambdaContext,
39
39
  _otherInternal: {
@@ -72,7 +72,7 @@ export default class Lambder {
72
72
  };
73
73
  this.utils = new LambderUtils({ ejsPath });
74
74
  }
75
- setIsCorsEnabled(isCorsEnabled) {
75
+ enableCors(isCorsEnabled) {
76
76
  this.isCorsEnabled = isCorsEnabled;
77
77
  }
78
78
  enableDdbSession({ tableName, tableRegion, sessionSalt }, { partitionKey, sortKey } = { partitionKey: "pk", sortKey: "sk" }) {
@@ -163,6 +163,7 @@ export default class Lambder {
163
163
  });
164
164
  }
165
165
  ;
166
+ // Implementation
166
167
  addApi(apiName, actionFn) {
167
168
  this.actionList.push({
168
169
  conditionFn: (ctx) => (!!ctx.apiName && ((typeof apiName === "string" && ctx.apiName === apiName) ||
@@ -172,6 +173,7 @@ export default class Lambder {
172
173
  });
173
174
  }
174
175
  ;
176
+ // Implementation
175
177
  addSessionApi(apiName, actionFn) {
176
178
  this.actionList.push({
177
179
  conditionFn: (ctx) => (!!ctx.apiName && ((typeof apiName === "string" && ctx.apiName === apiName) ||
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Type-safe API Contract System
3
+ *
4
+ * Define your API contract as a TypeScript type to get full type safety
5
+ * across frontend and backend with no runtime overhead.
6
+ *
7
+ * Example:
8
+ *
9
+ * export type MyApiContract = {
10
+ * getUserById: { input: { userId: string }, output: User },
11
+ * createUser: { input: CreateUserInput, output: User },
12
+ * listUsers: { input: void, output: User[] }
13
+ * }
14
+ *
15
+ * Frontend:
16
+ * const caller = new LambderCaller<MyApiContract>({ ... });
17
+ * const user = await caller.api('getUserById', { userId: '123' }); // typed!
18
+ *
19
+ * Backend:
20
+ * const lambder = new Lambder<MyApiContract>({ ... });
21
+ * lambder.addApi('getUserById', async (ctx, resolver) => {
22
+ * // ctx.apiPayload is typed as { userId: string }
23
+ * return resolver.api(user); // user is typed as User
24
+ * });
25
+ */
26
+ /**
27
+ * Base type for API contracts
28
+ */
29
+ export type ApiContract = {
30
+ [apiName: string]: {
31
+ input: any;
32
+ output: any;
33
+ };
34
+ };
35
+ /**
36
+ * Extract input type from contract for a specific API
37
+ */
38
+ export type ApiInput<TContract extends ApiContract, TApiName extends keyof TContract> = TContract[TApiName]['input'];
39
+ /**
40
+ * Extract output type from contract for a specific API
41
+ */
42
+ export type ApiOutput<TContract extends ApiContract, TApiName extends keyof TContract> = TContract[TApiName]['output'];
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Type-safe API Contract System
3
+ *
4
+ * Define your API contract as a TypeScript type to get full type safety
5
+ * across frontend and backend with no runtime overhead.
6
+ *
7
+ * Example:
8
+ *
9
+ * export type MyApiContract = {
10
+ * getUserById: { input: { userId: string }, output: User },
11
+ * createUser: { input: CreateUserInput, output: User },
12
+ * listUsers: { input: void, output: User[] }
13
+ * }
14
+ *
15
+ * Frontend:
16
+ * const caller = new LambderCaller<MyApiContract>({ ... });
17
+ * const user = await caller.api('getUserById', { userId: '123' }); // typed!
18
+ *
19
+ * Backend:
20
+ * const lambder = new Lambder<MyApiContract>({ ... });
21
+ * lambder.addApi('getUserById', async (ctx, resolver) => {
22
+ * // ctx.apiPayload is typed as { userId: string }
23
+ * return resolver.api(user); // user is typed as User
24
+ * });
25
+ */
26
+ export {};
@@ -1,4 +1,5 @@
1
1
  import { LambderApiResponse } from './LambderResponseBuilder';
2
+ import type { ApiContract } from './LambderApiContract';
2
3
  type VoidFunction = () => void | Promise<void>;
3
4
  type FetchTracker = {
4
5
  apiName: string;
@@ -21,7 +22,7 @@ type FetchEndEventHandler = (params: {
21
22
  }) => void | Promise<void>;
22
23
  type ErrorHandler = (err: Error) => void | Promise<void>;
23
24
  type MessageHandler = (message: any) => void | Promise<void>;
24
- export default class LambderCaller {
25
+ export default class LambderCaller<TContract extends ApiContract = any> {
25
26
  private isCorsEnabled;
26
27
  private apiPath;
27
28
  private apiVersion?;
@@ -51,7 +52,7 @@ export default class LambderCaller {
51
52
  fetchEndedHandler?: FetchEndEventHandler;
52
53
  });
53
54
  setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): void;
54
- apiRaw<T = any>(apiName: string, payload?: any, options?: {
55
+ 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?: {
55
56
  headers?: Record<string, any>;
56
57
  versionExpiredHandler?: VoidFunction;
57
58
  sessionExpiredHandler?: VoidFunction;
@@ -61,7 +62,17 @@ export default class LambderCaller {
61
62
  errorHandler?: ErrorHandler;
62
63
  fetchStartedHandler?: FetchStartEventHandler;
63
64
  fetchEndedHandler?: FetchEndEventHandler;
64
- }): Promise<LambderApiResponse<T> | null | undefined>;
65
- api<T = any>(...params: Parameters<typeof LambderCaller.prototype.apiRaw>): Promise<T | null | undefined>;
65
+ }): Promise<LambderApiResponse<TOutput> | null | undefined>;
66
+ api<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?: {
67
+ headers?: Record<string, any>;
68
+ versionExpiredHandler?: VoidFunction;
69
+ sessionExpiredHandler?: VoidFunction;
70
+ messageHandler?: MessageHandler;
71
+ errorMessageHandler?: MessageHandler;
72
+ notAuthorizedHandler?: VoidFunction;
73
+ errorHandler?: ErrorHandler;
74
+ fetchStartedHandler?: FetchStartEventHandler;
75
+ fetchEndedHandler?: FetchEndEventHandler;
76
+ }): Promise<TOutput | null | undefined>;
66
77
  }
67
78
  export {};
@@ -51,10 +51,16 @@ export default class LambderCaller {
51
51
  credentials: 'same-origin', redirect: 'follow', referrerPolicy: 'origin',
52
52
  headers: { 'Content-Type': 'application/json', ...(headers || {}) },
53
53
  body: JSON.stringify({ apiName, version, token, siteHost, payload, }),
54
- }).then(res => {
54
+ }).then(async (res) => {
55
55
  if (res.status >= 500)
56
56
  throw new Error("Request failed: " + res.status + " - " + res.statusText);
57
- return res.json();
57
+ if (res.headers.get("Content-Type")?.includes("application/lambder-json-stream")) {
58
+ const decompressed = res.json();
59
+ return decompressed;
60
+ }
61
+ else {
62
+ return res.json();
63
+ }
58
64
  });
59
65
  fetchTracker.done = true;
60
66
  if (this.fetchEndedHandler) {
@@ -95,7 +101,7 @@ export default class LambderCaller {
95
101
  await this.notAuthorizedHandler();
96
102
  }
97
103
  else if (this.errorHandler) {
98
- await this.errorHandler(new Error("Version Expired; Please refresh;"));
104
+ await this.errorHandler(new Error("Not Authorized;"));
99
105
  }
100
106
  return null;
101
107
  }
@@ -125,8 +131,8 @@ export default class LambderCaller {
125
131
  }
126
132
  ;
127
133
  // Use the same type for api but adjust the return type
128
- async api(...params) {
129
- const result = await this.apiRaw(...params);
134
+ async api(apiName, payload, options) {
135
+ const result = await this.apiRaw(apiName, payload, options);
130
136
  return result?.payload;
131
137
  }
132
138
  }
@@ -47,4 +47,5 @@ export default class LambderResponseBuilder {
47
47
  ejsTemplate(template: string, pageData: Record<string, any>, headers?: Record<string, string | string[]>): Promise<LambderResolverResponse>;
48
48
  ejsFile(filePath: string, pageData: Record<string, any>, headers?: Record<string, string | string[]>): Promise<LambderResolverResponse>;
49
49
  api<T = any>(payload: T | null, { versionExpired, sessionExpired, notAuthorized, message, errorMessage, logList, }?: LambderApiResponseConfig, headers?: Record<string, string | string[]>): LambderResolverResponse;
50
+ private apiBinary;
50
51
  }
@@ -144,7 +144,11 @@ export default class LambderResponseBuilder {
144
144
  }
145
145
  const mimeType = mimeTypeResolver.lookup(filePath);
146
146
  const body = this.readPublicFileSync(filePath);
147
- const bodyBase64 = Buffer.from(body).toString("base64");
147
+ if (body === "forbidden-public-path") {
148
+ throw { error: "Forbidden public path: " + filePath };
149
+ }
150
+ const bodyBuffer = Buffer.isBuffer(body) ? body : Buffer.from(body);
151
+ const bodyBase64 = bodyBuffer.toString("base64");
148
152
  console.log("bodyBase64.length", bodyBase64.length);
149
153
  return this.fileBase64(bodyBase64, mimeType || "", headers);
150
154
  }
@@ -186,5 +190,32 @@ export default class LambderResponseBuilder {
186
190
  }, headers);
187
191
  }
188
192
  ;
193
+ apiBinary(payload, { versionExpired, sessionExpired, notAuthorized, message, errorMessage, logList, } = {
194
+ versionExpired: undefined, sessionExpired: undefined, notAuthorized: undefined,
195
+ message: null, errorMessage: null, logList: undefined
196
+ }, headers) {
197
+ const finalLogList = logList || this.ctx?._otherInternal?.logToApiResponseAccumulator;
198
+ const result = {
199
+ apiVersion: this.apiVersion,
200
+ payload,
201
+ ...(versionExpired ? { versionExpired } : {}),
202
+ ...(sessionExpired ? { sessionExpired } : {}),
203
+ ...(notAuthorized ? { notAuthorized } : {}),
204
+ ...(message ? { message } : {}),
205
+ ...(errorMessage ? { errorMessage } : {}),
206
+ ...(finalLogList?.length ? { logList: finalLogList } : {}),
207
+ };
208
+ return this.raw({
209
+ statusCode: 200,
210
+ isBase64Encoded: true,
211
+ multiValueHeaders: {
212
+ "Content-Type": ["application/lambder-json-stream"],
213
+ "Content-Encoding": ["gzip"],
214
+ ...convertToMultiHeader(headers)
215
+ },
216
+ body: Buffer.from(JSON.stringify(result)).toString("base64"),
217
+ });
218
+ }
219
+ ;
189
220
  }
190
221
  ;
package/dist/index.d.ts CHANGED
@@ -4,3 +4,4 @@ export { default as LambderCaller } from "./LambderCaller.js";
4
4
  export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
5
5
  export { default as LambderResolver } from "./LambderResolver.js";
6
6
  export { default as LambderSessionManager } from "./LambderSessionManager.js";
7
+ export { type ApiContract, type ApiInput, type ApiOutput, } from "./LambderApiContract.js";
@@ -0,0 +1,123 @@
1
+ # Type-Safe API Quick Start
2
+
3
+ ## In 3 Simple Steps
4
+
5
+ ### 1. Define Your Contract Type
6
+
7
+ ```typescript
8
+ // shared/apiContract.ts
9
+ import type { ApiContract } from 'lambder';
10
+
11
+ export type MyApiContract = {
12
+ getUserById: { input: { userId: string }, output: User },
13
+ createUser: { input: CreateUserInput, output: User },
14
+ listUsers: { input: void, output: User[] }
15
+ } satisfies ApiContract;
16
+ ```
17
+
18
+ ### 2. Backend - Pass Type to Lambder
19
+
20
+ ```typescript
21
+ import Lambder from 'lambder';
22
+ import type { MyApiContract } from './shared/apiContract';
23
+
24
+ const lambder = new Lambder<MyApiContract>({
25
+ publicPath: './public',
26
+ apiPath: '/api'
27
+ });
28
+
29
+ // Now addApi is type-safe!
30
+ lambder.addApi('getUserById', async (ctx, resolver) => {
31
+ // ctx.apiPayload is automatically typed as { userId: string }
32
+ const user = await db.getUser(ctx.apiPayload.userId);
33
+ return resolver.api(user);
34
+ });
35
+ ```
36
+
37
+ ### 3. Frontend - Pass Type to LambderCaller
38
+
39
+ ```typescript
40
+ import { LambderCaller } from 'lambder';
41
+ import type { MyApiContract } from './shared/apiContract';
42
+
43
+ const caller = new LambderCaller<MyApiContract>({
44
+ apiPath: '/api',
45
+ isCorsEnabled: false
46
+ });
47
+
48
+ // Now api() is type-safe!
49
+ const user = await caller.api('getUserById', { userId: '123' });
50
+ // user is typed as User | null | undefined
51
+ ```
52
+
53
+ ## That's It!
54
+
55
+ - ✅ **No wrapper functions needed**
56
+ - ✅ **Use existing `api()` and `addApi()` methods**
57
+ - ✅ **Full autocomplete in IDE**
58
+ - ✅ **Type-safe inputs and outputs**
59
+ - ✅ **Backward compatible**
60
+
61
+ ## Contract Type Format
62
+
63
+ ```typescript
64
+ type MyApiContract = {
65
+ apiName: { input: InputType, output: OutputType }
66
+ }
67
+ ```
68
+
69
+ ## Examples
70
+
71
+ ### API with parameters
72
+ ```typescript
73
+ getUserById: { input: { userId: string }, output: User }
74
+ ```
75
+
76
+ ### API with no input
77
+ ```typescript
78
+ listAll: { input: void, output: Item[] }
79
+ ```
80
+
81
+ ### API with complex types
82
+ ```typescript
83
+ updateUser: {
84
+ input: { id: string } & Partial<User>,
85
+ output: User
86
+ }
87
+ ```
88
+
89
+ ### API with conditional output
90
+ ```typescript
91
+ login: {
92
+ input: { email: string, password: string },
93
+ output: { success: boolean, user?: User, error?: string }
94
+ }
95
+ ```
96
+
97
+ ## Without Type Safety (Still Works!)
98
+
99
+ Don't want type safety? Just don't pass the generic type:
100
+
101
+ ```typescript
102
+ // Frontend
103
+ const caller = new LambderCaller({ ... }); // No generic
104
+ await caller.api('anyApi', { anything: true }); // Works, but untyped
105
+
106
+ // Backend
107
+ const lambder = new Lambder({ ... }); // No generic
108
+ lambder.addApi('anyApi', async (ctx, resolver) => {
109
+ // ctx.apiPayload is any
110
+ });
111
+ ```
112
+
113
+ ## Full Example
114
+
115
+ See [simplified-typed-api-example.ts](../examples/simplified-typed-api-example.ts) for a complete working example.
116
+
117
+ ## Key Points
118
+
119
+ - **Contract is just a TypeScript type** - No runtime code!
120
+ - **Zero overhead** - All type checking happens at compile time
121
+ - **Opt-in** - Use types when you want them
122
+ - **Simple** - Just pass type to constructor
123
+ - **Autocomplete** - IDE shows available APIs as you type