lambder 1.0.125 → 1.0.127

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
@@ -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 = ApiContract<{
499
+ getUserById: { input: { userId: string }, output: User },
500
+ createUser: { input: CreateUserInput, output: User },
501
+ listUsers: { input: void, output: User[] }
502
+ }>;
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/deploy CHANGED
@@ -3,6 +3,11 @@
3
3
  # Exit immediately if a command exits with a non-zero status.
4
4
  set -e
5
5
 
6
+ echo "Running tests..."
7
+ npm run test
8
+
9
+ echo "Tests passed! Proceeding with deployment..."
10
+
6
11
  # Update the version, build the project, and publish
7
12
  rm -rf ./dist/*
8
13
  npm version patch --no-git-tag-version
package/dist/Lambder.d.ts CHANGED
@@ -4,8 +4,9 @@ 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 { ApiContractShape } from "./LambderApiContract.js";
7
8
  type Path = `/${string}`;
8
- export type LambderRenderContext = {
9
+ export type LambderRenderContext<TApiPayload = any> = {
9
10
  host: string;
10
11
  path: string;
11
12
  pathParams: Record<string, any> | null;
@@ -14,7 +15,7 @@ export type LambderRenderContext = {
14
15
  post: Record<string, any>;
15
16
  cookie: Record<string, any>;
16
17
  apiName: string;
17
- apiPayload: any;
18
+ apiPayload: TApiPayload;
18
19
  headers: APIGatewayProxyEventHeaders;
19
20
  session: LambderSessionContext | null;
20
21
  event: APIGatewayProxyEvent;
@@ -33,17 +34,17 @@ export type LambderRenderContext = {
33
34
  };
34
35
  };
35
36
  type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
36
- type ConditionFunction = (ctx: LambderRenderContext) => boolean;
37
- type ActionFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>;
37
+ type ConditionFunction = (ctx: LambderRenderContext<any>) => boolean;
38
+ type ActionFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>;
38
39
  type HookCreatedFunction = (lambderInstance: Lambder) => Promise<void>;
39
- type HookBeforeRenderFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => LambderRenderContext | Error | Promise<LambderRenderContext | Error>;
40
- type HookAfterRenderFunction = (ctx: LambderRenderContext, resolver: LambderResolver, response: LambderResolverResponse) => LambderResolverResponse | Error | Promise<LambderResolverResponse | Error>;
41
- type HookFallbackFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => void | Promise<void>;
42
- type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext | null, response: LambderResponseBuilder, logListToApiResponse?: any[]) => LambderResolverResponse | Promise<LambderResolverResponse>;
43
- type RouteFallbackHandlerFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse;
44
- type ApiFallbackHandlerFunction = (ctx: LambderRenderContext, resolver: LambderResolver) => LambderResolverResponse;
45
- export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext;
46
- export default class Lambder {
40
+ type HookBeforeRenderFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderRenderContext<any> | Error | Promise<LambderRenderContext<any> | Error>;
41
+ type HookAfterRenderFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver, response: LambderResolverResponse) => LambderResolverResponse | Error | Promise<LambderResolverResponse | Error>;
42
+ type HookFallbackFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => void | Promise<void>;
43
+ type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext<any> | null, response: LambderResponseBuilder, logListToApiResponse?: any[]) => LambderResolverResponse | Promise<LambderResolverResponse>;
44
+ type RouteFallbackHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
45
+ type ApiFallbackHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
46
+ export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext<any>;
47
+ export default class Lambder<TContract extends ApiContractShape = any> {
47
48
  apiPath: string;
48
49
  apiVersion: null | string;
49
50
  isCorsEnabled: boolean;
@@ -86,13 +87,17 @@ export default class Lambder {
86
87
  }>): Promise<void>;
87
88
  addRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
88
89
  addSessionRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): void;
89
- addApi(apiName: string | ConditionFunction | RegExp, actionFn: ActionFunction): void;
90
- 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<TContract[TApiName]['input']>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
92
+ addApi(apiName: string, actionFn: ActionFunction): void;
93
+ addSessionApi(apiName: ConditionFunction | RegExp, actionFn: ActionFunction): void;
94
+ addSessionApi<TApiName extends keyof TContract & string>(apiName: TApiName, actionFn: (ctx: LambderRenderContext<TContract[TApiName]['input']>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>): void;
95
+ addSessionApi(apiName: string, actionFn: ActionFunction): void;
91
96
  addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
92
97
  addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
93
98
  addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
94
99
  addHook(hookEvent: 'fallback', hookFn: HookFallbackFunction, priority?: number): Promise<void>;
95
- getSessionController(ctx: LambderRenderContext): LambderSessionController;
100
+ getSessionController(ctx: LambderRenderContext<any>): LambderSessionController;
96
101
  getResponseBuilder(): LambderResponseBuilder;
97
102
  private getResolver;
98
103
  render(event: APIGatewayProxyEvent, lambdaContext: Context): Promise<LambderResolverResponse>;
package/dist/Lambder.js CHANGED
@@ -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,53 @@
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
+ * Use this as a constraint when defining your API contract:
30
+ *
31
+ * export type MyApiContract = {
32
+ * echo: { input: { message: string }, output: { echo: string } }
33
+ * } satisfies ApiContract;
34
+ *
35
+ * Or for backward compatibility without satisfies:
36
+ *
37
+ * export type MyApiContract = ApiContract & {
38
+ * echo: { input: { message: string }, output: { echo: string } }
39
+ * }
40
+ */
41
+ export type ApiContractShape = Record<string, {
42
+ input: any;
43
+ output: any;
44
+ }>;
45
+ export type ApiContract<T extends ApiContractShape> = T;
46
+ /**
47
+ * Extract input type from contract for a specific API
48
+ */
49
+ export type ApiInput<TContract extends ApiContractShape, TApiName extends keyof TContract> = TContract[TApiName]['input'];
50
+ /**
51
+ * Extract output type from contract for a specific API
52
+ */
53
+ export type ApiOutput<TContract extends ApiContractShape, 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 { ApiContractShape } 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 ApiContractShape = 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 {};
@@ -131,8 +131,8 @@ export default class LambderCaller {
131
131
  }
132
132
  ;
133
133
  // Use the same type for api but adjust the return type
134
- async api(...params) {
135
- const result = await this.apiRaw(...params);
134
+ async api(apiName, payload, options) {
135
+ const result = await this.apiRaw(apiName, payload, options);
136
136
  return result?.payload;
137
137
  }
138
138
  }
@@ -25,7 +25,7 @@ export default class LambderResolver extends LambderResponseBuilder {
25
25
  publicPath: string;
26
26
  apiVersion?: string | null;
27
27
  lambderUtils: LambderUtils;
28
- ctx: LambderRenderContext;
28
+ ctx: LambderRenderContext<any>;
29
29
  resolve: (response: LambderResolverResponse) => void;
30
30
  reject: (err: Error) => void;
31
31
  });
@@ -28,7 +28,7 @@ export default class LambderResponseBuilder {
28
28
  publicPath: string;
29
29
  apiVersion?: string | null;
30
30
  lambderUtils: LambderUtils;
31
- ctx?: LambderRenderContext;
31
+ ctx?: LambderRenderContext<any>;
32
32
  });
33
33
  private readPublicFileSync;
34
34
  private checkPublicFileExist;
@@ -5,12 +5,12 @@ export default class LambderSessionController {
5
5
  lambderSessionManager: LambderSessionManager;
6
6
  sessionTokenCookieKey: string;
7
7
  sessionCsrfCookieKey: string;
8
- ctx: LambderRenderContext;
8
+ ctx: LambderRenderContext<any>;
9
9
  constructor({ lambderSessionManager, sessionTokenCookieKey, sessionCsrfCookieKey, ctx, }: {
10
10
  lambderSessionManager: LambderSessionManager;
11
11
  sessionTokenCookieKey: string;
12
12
  sessionCsrfCookieKey: string;
13
- ctx: LambderRenderContext;
13
+ ctx: LambderRenderContext<any>;
14
14
  });
15
15
  private areRequestSessionTokensValid;
16
16
  createSession(sessionKey: string, data?: any, ttlInSeconds?: number): Promise<LambderSessionContext>;
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 = ApiContract<{
12
+ getUserById: { input: { userId: string }, output: User },
13
+ createUser: { input: CreateUserInput, output: User },
14
+ listUsers: { input: void, output: User[] }
15
+ }>;
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