lambder 1.0.147 → 2.0.2
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 +355 -410
- package/dist/Lambder.d.ts +47 -21
- package/dist/Lambder.js +79 -24
- package/dist/LambderApiContract.d.ts +10 -42
- package/dist/LambderApiContract.js +2 -22
- package/dist/LambderCaller.js +2 -2
- package/dist/LambderMSW.js +0 -4
- package/dist/LambderResolver.d.ts +16 -17
- package/dist/LambderResponseBuilder.d.ts +2 -3
- package/dist/LambderResponseBuilder.js +1 -3
- package/dist/LambderUtils.js +1 -3
- package/dist/index.d.ts +1 -1
- package/docs/LAMBDER_MSW.md +6 -6
- package/docs/TYPE_SAFE_QUICK_START.md +54 -177
- package/examples/msw-testing-example.ts +36 -33
- package/examples/secure-session-example.ts +50 -34
- package/examples/zod-chained-api-example.ts +63 -0
- package/package.json +3 -2
- package/src/Lambder.ts +124 -83
- package/src/LambderApiContract.ts +7 -50
- package/src/LambderCaller.ts +2 -2
- package/src/LambderMSW.ts +0 -7
- package/src/LambderResolver.ts +21 -24
- package/src/LambderResponseBuilder.ts +4 -7
- package/src/LambderUtils.ts +1 -3
- package/src/index.ts +0 -3
- package/tests/UNTESTED_FEATURES.md +263 -0
- package/tests/error-handling.test.ts +585 -0
- package/tests/hooks.test.ts +561 -0
- package/tests/output-type-runtime.test.ts +80 -64
- package/tests/routes.test.ts +542 -0
- package/tests/session.test.ts +38 -24
- package/tests/type-safety.test.ts +147 -97
- package/tests/use-plugin.test.ts +437 -0
- package/OUTPUT_TYPE_ENFORCEMENT_SUMMARY.md +0 -90
- package/examples/output-type-enforcement-example.ts +0 -218
- package/examples/simplified-typed-api-example.ts +0 -365
- package/examples/test-output-type-enforcement.ts +0 -101
- package/test-type-enforcement.ts +0 -111
package/dist/Lambder.d.ts
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import type { APIGatewayProxyEvent, APIGatewayProxyEventHeaders, Context } from "aws-lambda";
|
|
2
3
|
import LambderResolver from "./LambderResolver.js";
|
|
3
4
|
import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
|
|
4
5
|
import LambderUtils from "./LambderUtils.js";
|
|
5
6
|
import { type LambderSessionContext } from "./LambderSessionManager.js";
|
|
6
7
|
import LambderSessionController from "./LambderSessionController.js";
|
|
7
|
-
import type {
|
|
8
|
+
import type { MergeContract } from "./LambderApiContract.js";
|
|
8
9
|
type Path = `/${string}`;
|
|
9
10
|
export type LambderRenderContext<TApiPayload = any> = {
|
|
10
11
|
host: string;
|
|
@@ -37,7 +38,6 @@ export type LambderRenderContext<TApiPayload = any> = {
|
|
|
37
38
|
export type LambderSessionRenderContext<TApiPayload = any, SessionData = any> = Omit<LambderRenderContext<TApiPayload>, 'session'> & {
|
|
38
39
|
session: LambderSessionContext<SessionData>;
|
|
39
40
|
};
|
|
40
|
-
type LambderModuleFunction = (lambderInstance: Lambder) => void | Promise<void>;
|
|
41
41
|
type ConditionFunction = (ctx: LambderRenderContext<any>) => boolean;
|
|
42
42
|
type ActionFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>;
|
|
43
43
|
type SessionActionFunction<SessionData = any> = (ctx: LambderSessionRenderContext<any, SessionData>, resolver: LambderResolver) => LambderResolverResponse | Promise<LambderResolverResponse>;
|
|
@@ -49,12 +49,38 @@ type GlobalErrorHandlerFunction = (err: Error, ctx: LambderRenderContext<any> |
|
|
|
49
49
|
type RouteFallbackHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
50
50
|
type ApiFallbackHandlerFunction = (ctx: LambderRenderContext<any>, resolver: LambderResolver) => LambderResolverResponse;
|
|
51
51
|
export declare const createContext: (event: APIGatewayProxyEvent, lambdaContext: Context, apiPath: string) => LambderRenderContext<any>;
|
|
52
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Main Lambder class for building type-safe serverless APIs
|
|
54
|
+
*
|
|
55
|
+
* @typeParam TSessionData - Type of session data stored in DynamoDB
|
|
56
|
+
* @typeParam _TContract - @internal Accumulates API contract during chaining (do not pass manually)
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* interface SessionData { userId: string; role: string; }
|
|
61
|
+
*
|
|
62
|
+
* const lambder = new Lambder<SessionData>({ apiPath: '/api' })
|
|
63
|
+
* .addApi('getUser', { input: z.object({...}), output: z.object({...}) }, handler)
|
|
64
|
+
* .addApi('createUser', { input: z.object({...}), output: z.object({...}) }, handler);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export default class Lambder<TSessionData = any, _TContract extends Record<string, any> = {}> {
|
|
53
68
|
apiPath: string;
|
|
54
69
|
apiVersion: null | string;
|
|
55
70
|
isCorsEnabled: boolean;
|
|
56
71
|
publicPath: string;
|
|
57
72
|
ejsPath: string;
|
|
73
|
+
/**
|
|
74
|
+
* Type property for extracting the API contract
|
|
75
|
+
* Use this to export your API types to the frontend
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
80
|
+
* export type AppContract = typeof lambder.ApiContractType;
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
readonly ApiContractType: _TContract;
|
|
58
84
|
private actionList;
|
|
59
85
|
private hookList;
|
|
60
86
|
private globalErrorHandler;
|
|
@@ -70,7 +96,7 @@ export default class Lambder<TContract extends ApiContractShape = any, TSessionD
|
|
|
70
96
|
ejsPath?: string;
|
|
71
97
|
apiVersion?: string;
|
|
72
98
|
});
|
|
73
|
-
enableCors(isCorsEnabled: boolean):
|
|
99
|
+
enableCors(isCorsEnabled: boolean): this;
|
|
74
100
|
enableDdbSession({ tableName, tableRegion, sessionSalt, enableSlidingExpiration }: {
|
|
75
101
|
tableName: string;
|
|
76
102
|
tableRegion: string;
|
|
@@ -79,26 +105,25 @@ export default class Lambder<TContract extends ApiContractShape = any, TSessionD
|
|
|
79
105
|
}, { partitionKey, sortKey }?: {
|
|
80
106
|
partitionKey: string;
|
|
81
107
|
sortKey: string;
|
|
82
|
-
}):
|
|
83
|
-
setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string):
|
|
84
|
-
setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction):
|
|
85
|
-
setApiFallbackHandler(apiFallbackHandler: ApiFallbackHandlerFunction):
|
|
86
|
-
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction):
|
|
108
|
+
}): this;
|
|
109
|
+
setSessionCookieKey(sessionTokenCookieKey: string, sessionCsrfCookieKey: string): this;
|
|
110
|
+
setRouteFallbackHandler(routeFallbackHandler: RouteFallbackHandlerFunction): this;
|
|
111
|
+
setApiFallbackHandler(apiFallbackHandler: ApiFallbackHandlerFunction): this;
|
|
112
|
+
setGlobalErrorHandler(globalErrorHandler: GlobalErrorHandlerFunction): this;
|
|
87
113
|
private getPatternMatch;
|
|
88
114
|
private testPatternMatch;
|
|
89
115
|
private handleNoMatchedAction;
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
addSessionApi(apiName: string, actionFn: SessionActionFunction<TSessionData>): void;
|
|
116
|
+
addRoute(condition: Path | ConditionFunction | RegExp, actionFn: ActionFunction): this;
|
|
117
|
+
addSessionRoute(condition: Path | ConditionFunction | RegExp, actionFn: SessionActionFunction<TSessionData>): this;
|
|
118
|
+
use<_TNewContract extends Record<string, any>>(plugin: (lambder: Lambder<TSessionData, _TContract>) => Lambder<TSessionData, _TNewContract>): Lambder<TSessionData, _TNewContract>;
|
|
119
|
+
addApi<TName extends string, TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny>(name: TName, schema: {
|
|
120
|
+
input: TInput;
|
|
121
|
+
output: TOutput;
|
|
122
|
+
}, handler: (ctx: LambderRenderContext<z.infer<TInput>>, resolver: LambderResolver<z.infer<TOutput>>) => LambderResolverResponse | Promise<LambderResolverResponse>): Lambder<TSessionData, MergeContract<_TContract, TName, z.infer<TInput>, z.infer<TOutput>>>;
|
|
123
|
+
addSessionApi<TName extends string, TInput extends z.ZodTypeAny, TOutput extends z.ZodTypeAny>(name: TName, schema: {
|
|
124
|
+
input: TInput;
|
|
125
|
+
output: TOutput;
|
|
126
|
+
}, handler: (ctx: LambderSessionRenderContext<z.infer<TInput>, TSessionData>, resolver: LambderResolver<z.infer<TOutput>>) => LambderResolverResponse | Promise<LambderResolverResponse>): Lambder<TSessionData, MergeContract<_TContract, TName, z.infer<TInput>, z.infer<TOutput>>>;
|
|
102
127
|
addHook(hookEvent: 'created', hookFn: HookCreatedFunction, priority?: number): Promise<void>;
|
|
103
128
|
addHook(hookEvent: 'beforeRender', hookFn: HookBeforeRenderFunction, priority?: number): Promise<void>;
|
|
104
129
|
addHook(hookEvent: 'afterRender', hookFn: HookAfterRenderFunction, priority?: number): Promise<void>;
|
|
@@ -106,6 +131,7 @@ export default class Lambder<TContract extends ApiContractShape = any, TSessionD
|
|
|
106
131
|
getSessionController(ctx: LambderRenderContext<any> | LambderSessionRenderContext<any, TSessionData>): LambderSessionController<TSessionData>;
|
|
107
132
|
getResponseBuilder(): LambderResponseBuilder<any>;
|
|
108
133
|
private getResolver;
|
|
134
|
+
getHandler(): (event: APIGatewayProxyEvent, context: Context) => Promise<LambderResolverResponse>;
|
|
109
135
|
render(event: APIGatewayProxyEvent, lambdaContext: Context): Promise<LambderResolverResponse>;
|
|
110
136
|
}
|
|
111
137
|
export {};
|
package/dist/Lambder.js
CHANGED
|
@@ -48,12 +48,38 @@ export const createContext = (event, lambdaContext, apiPath) => {
|
|
|
48
48
|
}
|
|
49
49
|
};
|
|
50
50
|
};
|
|
51
|
+
/**
|
|
52
|
+
* Main Lambder class for building type-safe serverless APIs
|
|
53
|
+
*
|
|
54
|
+
* @typeParam TSessionData - Type of session data stored in DynamoDB
|
|
55
|
+
* @typeParam _TContract - @internal Accumulates API contract during chaining (do not pass manually)
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```typescript
|
|
59
|
+
* interface SessionData { userId: string; role: string; }
|
|
60
|
+
*
|
|
61
|
+
* const lambder = new Lambder<SessionData>({ apiPath: '/api' })
|
|
62
|
+
* .addApi('getUser', { input: z.object({...}), output: z.object({...}) }, handler)
|
|
63
|
+
* .addApi('createUser', { input: z.object({...}), output: z.object({...}) }, handler);
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
51
66
|
export default class Lambder {
|
|
52
67
|
apiPath;
|
|
53
68
|
apiVersion;
|
|
54
69
|
isCorsEnabled = false;
|
|
55
70
|
publicPath;
|
|
56
71
|
ejsPath;
|
|
72
|
+
/**
|
|
73
|
+
* Type property for extracting the API contract
|
|
74
|
+
* Use this to export your API types to the frontend
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const lambder = new Lambder().addApi(...).addApi(...);
|
|
79
|
+
* export type AppContract = typeof lambder.ApiContractType;
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
ApiContractType;
|
|
57
83
|
actionList;
|
|
58
84
|
hookList;
|
|
59
85
|
globalErrorHandler = null;
|
|
@@ -78,24 +104,30 @@ export default class Lambder {
|
|
|
78
104
|
}
|
|
79
105
|
enableCors(isCorsEnabled) {
|
|
80
106
|
this.isCorsEnabled = isCorsEnabled;
|
|
107
|
+
return this;
|
|
81
108
|
}
|
|
82
109
|
enableDdbSession({ tableName, tableRegion, sessionSalt, enableSlidingExpiration }, { partitionKey, sortKey } = { partitionKey: "pk", sortKey: "sk" }) {
|
|
83
110
|
this.lambderSessionManager = new LambderSessionManager({
|
|
84
111
|
tableName, tableRegion, partitionKey, sortKey, sessionSalt, enableSlidingExpiration
|
|
85
112
|
});
|
|
113
|
+
return this;
|
|
86
114
|
}
|
|
87
115
|
setSessionCookieKey(sessionTokenCookieKey, sessionCsrfCookieKey) {
|
|
88
116
|
this.sessionTokenCookieKey = sessionTokenCookieKey;
|
|
89
117
|
this.sessionCsrfCookieKey = sessionCsrfCookieKey;
|
|
118
|
+
return this;
|
|
90
119
|
}
|
|
91
120
|
setRouteFallbackHandler(routeFallbackHandler) {
|
|
92
121
|
this.routeFallbackHandler = routeFallbackHandler;
|
|
122
|
+
return this;
|
|
93
123
|
}
|
|
94
124
|
setApiFallbackHandler(apiFallbackHandler) {
|
|
95
125
|
this.apiFallbackHandler = apiFallbackHandler;
|
|
126
|
+
return this;
|
|
96
127
|
}
|
|
97
128
|
setGlobalErrorHandler(globalErrorHandler) {
|
|
98
129
|
this.globalErrorHandler = globalErrorHandler;
|
|
130
|
+
return this;
|
|
99
131
|
}
|
|
100
132
|
getPatternMatch(pattern, path) {
|
|
101
133
|
const result = (match(pattern, { decode: decodeURIComponent }))(path);
|
|
@@ -124,12 +156,6 @@ export default class Lambder {
|
|
|
124
156
|
resolver.resolve({ statusCode: 204, body: "Route handler not set.", });
|
|
125
157
|
}
|
|
126
158
|
}
|
|
127
|
-
async addModule(moduleFn) {
|
|
128
|
-
await moduleFn(this);
|
|
129
|
-
}
|
|
130
|
-
async importModule(moduleImport) {
|
|
131
|
-
await this.addModule((await moduleImport).default);
|
|
132
|
-
}
|
|
133
159
|
addRoute(condition, actionFn) {
|
|
134
160
|
this.actionList.push({
|
|
135
161
|
conditionFn: (ctx) => (ctx.method === "GET" &&
|
|
@@ -141,13 +167,14 @@ export default class Lambder {
|
|
|
141
167
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
142
168
|
}
|
|
143
169
|
else if (condition?.constructor == RegExp) {
|
|
144
|
-
|
|
170
|
+
const match = ctx.path.match(condition);
|
|
171
|
+
ctx.pathParams = match ? (match.groups || match) : {};
|
|
145
172
|
}
|
|
146
173
|
return await actionFn(ctx, resolver);
|
|
147
174
|
}
|
|
148
175
|
});
|
|
176
|
+
return this;
|
|
149
177
|
}
|
|
150
|
-
;
|
|
151
178
|
addSessionRoute(condition, actionFn) {
|
|
152
179
|
this.actionList.push({
|
|
153
180
|
conditionFn: (ctx) => (ctx.method === "GET" &&
|
|
@@ -159,7 +186,8 @@ export default class Lambder {
|
|
|
159
186
|
ctx.pathParams = this.getPatternMatch(condition, ctx.path);
|
|
160
187
|
}
|
|
161
188
|
else if (condition?.constructor == RegExp) {
|
|
162
|
-
|
|
189
|
+
const match = ctx.path.match(condition);
|
|
190
|
+
ctx.pathParams = match ? (match.groups || match) : {};
|
|
163
191
|
}
|
|
164
192
|
const sessionCtx = ctx;
|
|
165
193
|
await this.getSessionController(ctx).fetchSession();
|
|
@@ -169,35 +197,59 @@ export default class Lambder {
|
|
|
169
197
|
return await actionFn(sessionCtx, resolver);
|
|
170
198
|
}
|
|
171
199
|
});
|
|
200
|
+
return this;
|
|
172
201
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
202
|
+
// Plugin system
|
|
203
|
+
use(plugin) {
|
|
204
|
+
return plugin(this);
|
|
205
|
+
}
|
|
206
|
+
// Typed API with Zod
|
|
207
|
+
addApi(name, schema, handler) {
|
|
176
208
|
this.actionList.push({
|
|
177
|
-
conditionFn: (ctx) =>
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
209
|
+
conditionFn: (ctx) => ctx.apiName === name,
|
|
210
|
+
actionFn: async (ctx, resolver) => {
|
|
211
|
+
// Validate Input
|
|
212
|
+
const inputResult = schema.input.safeParse(ctx.apiPayload);
|
|
213
|
+
if (!inputResult.success) {
|
|
214
|
+
return resolver.raw({
|
|
215
|
+
statusCode: 400,
|
|
216
|
+
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
217
|
+
multiValueHeaders: { "Content-Type": ["application/json"] }
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
// Run Handler with validated data
|
|
221
|
+
ctx.apiPayload = inputResult.data;
|
|
222
|
+
return await handler(ctx, resolver);
|
|
223
|
+
},
|
|
181
224
|
});
|
|
225
|
+
return this;
|
|
182
226
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
addSessionApi(apiName, actionFn) {
|
|
227
|
+
// Typed Session API with Zod
|
|
228
|
+
addSessionApi(name, schema, handler) {
|
|
186
229
|
this.actionList.push({
|
|
187
|
-
conditionFn: (ctx) =>
|
|
188
|
-
(typeof apiName === "function" && apiName(ctx)) ||
|
|
189
|
-
(apiName?.constructor == RegExp && apiName.test(ctx.apiName)))),
|
|
230
|
+
conditionFn: (ctx) => ctx.apiName === name,
|
|
190
231
|
actionFn: async (ctx, resolver) => {
|
|
191
232
|
const sessionCtx = ctx;
|
|
192
233
|
await this.getSessionController(ctx).fetchSession();
|
|
193
234
|
if (!sessionCtx.session) {
|
|
194
235
|
throw new Error("Session not found.");
|
|
195
236
|
}
|
|
196
|
-
|
|
237
|
+
// Validate Input
|
|
238
|
+
const inputResult = schema.input.safeParse(ctx.apiPayload);
|
|
239
|
+
if (!inputResult.success) {
|
|
240
|
+
return resolver.raw({
|
|
241
|
+
statusCode: 400,
|
|
242
|
+
body: JSON.stringify({ error: "Input validation failed", zodError: inputResult.error }),
|
|
243
|
+
multiValueHeaders: { "Content-Type": ["application/json"] }
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
// Run Handler with validated data
|
|
247
|
+
ctx.apiPayload = inputResult.data;
|
|
248
|
+
return await handler(sessionCtx, resolver);
|
|
197
249
|
}
|
|
198
250
|
});
|
|
251
|
+
return this;
|
|
199
252
|
}
|
|
200
|
-
;
|
|
201
253
|
async addHook(hookEvent, hookFn, priority = 0) {
|
|
202
254
|
if (hookEvent === "created") {
|
|
203
255
|
await hookFn(this);
|
|
@@ -236,6 +288,9 @@ export default class Lambder {
|
|
|
236
288
|
});
|
|
237
289
|
}
|
|
238
290
|
;
|
|
291
|
+
getHandler() {
|
|
292
|
+
return (event, context) => this.render(event, context);
|
|
293
|
+
}
|
|
239
294
|
async render(event, lambdaContext) {
|
|
240
295
|
let eventRenderContext = null;
|
|
241
296
|
try {
|
|
@@ -1,53 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lambder API Contract System
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
* });
|
|
4
|
+
* Contracts are built via method chaining and inferred using typeof lambder.ApiContractType
|
|
25
5
|
*/
|
|
26
6
|
/**
|
|
27
|
-
* Base
|
|
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
|
-
* }
|
|
7
|
+
* Base shape for API contracts - used by LambderCaller and LambderMSW
|
|
40
8
|
*/
|
|
41
9
|
export type ApiContractShape = Record<string, {
|
|
42
10
|
input: any;
|
|
43
11
|
output: any;
|
|
44
12
|
}>;
|
|
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
13
|
/**
|
|
51
|
-
*
|
|
14
|
+
* Helper type for merging new API into existing contract during chaining
|
|
52
15
|
*/
|
|
53
|
-
export type
|
|
16
|
+
export type MergeContract<Old, Name extends string, In, Out> = Old & {
|
|
17
|
+
[K in Name]: {
|
|
18
|
+
input: In;
|
|
19
|
+
output: Out;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
@@ -1,26 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Lambder API Contract System
|
|
3
3
|
*
|
|
4
|
-
*
|
|
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
|
-
* });
|
|
4
|
+
* Contracts are built via method chaining and inferred using typeof lambder.ApiContractType
|
|
25
5
|
*/
|
|
26
6
|
export {};
|
package/dist/LambderCaller.js
CHANGED
|
@@ -71,9 +71,9 @@ export default class LambderCaller {
|
|
|
71
71
|
activeFetchList: this.fetchTrackerList.filter(v => !v.done),
|
|
72
72
|
});
|
|
73
73
|
}
|
|
74
|
-
if (data && data.logList
|
|
74
|
+
if (data && data.logList) {
|
|
75
75
|
for (const record of data.logList) {
|
|
76
|
-
|
|
76
|
+
// Log to API response for debugging
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
if (data && data.versionExpired) {
|
package/dist/LambderMSW.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createRequire } from 'module';
|
|
2
|
-
const require = createRequire(import.meta.url);
|
|
3
1
|
export default class LambderMSW {
|
|
4
2
|
apiPath;
|
|
5
3
|
apiVersion;
|
|
@@ -48,7 +46,6 @@ export default class LambderMSW {
|
|
|
48
46
|
if (body.apiName !== apiName) {
|
|
49
47
|
return;
|
|
50
48
|
}
|
|
51
|
-
console.log("LambderMSW called for:", body.apiName);
|
|
52
49
|
try {
|
|
53
50
|
// Add artificial delay if specified
|
|
54
51
|
if (options?.delay) {
|
|
@@ -56,7 +53,6 @@ export default class LambderMSW {
|
|
|
56
53
|
}
|
|
57
54
|
// Call the handler with the payload from the request
|
|
58
55
|
const payload = await handler(body.payload);
|
|
59
|
-
console.log("Matched! Returning payload for:", apiName);
|
|
60
56
|
const response = {
|
|
61
57
|
apiVersion: this.apiVersion,
|
|
62
58
|
payload,
|
|
@@ -1,26 +1,25 @@
|
|
|
1
1
|
import type { LambderRenderContext } from "./Lambder.js";
|
|
2
2
|
import LambderResponseBuilder, { LambderResolverResponse } from "./LambderResponseBuilder.js";
|
|
3
3
|
import LambderUtils from "./LambderUtils.js";
|
|
4
|
-
import type { ApiContractShape, ApiOutput } from "./LambderApiContract.js";
|
|
5
4
|
type MethodType<T, M extends keyof T> = T[M] extends (...args: any[]) => any ? T[M] : never;
|
|
6
|
-
interface DieResolverMethods<
|
|
7
|
-
raw: MethodType<LambderResponseBuilder
|
|
8
|
-
json: MethodType<LambderResponseBuilder
|
|
9
|
-
xml: MethodType<LambderResponseBuilder
|
|
10
|
-
html: MethodType<LambderResponseBuilder
|
|
11
|
-
status301: MethodType<LambderResponseBuilder
|
|
12
|
-
status404: MethodType<LambderResponseBuilder
|
|
13
|
-
cors: MethodType<LambderResponseBuilder
|
|
14
|
-
fileBase64: MethodType<LambderResponseBuilder
|
|
15
|
-
file: MethodType<LambderResponseBuilder
|
|
16
|
-
ejsFile: MethodType<LambderResponseBuilder
|
|
17
|
-
ejsTemplate: MethodType<LambderResponseBuilder
|
|
18
|
-
api: (payload:
|
|
5
|
+
interface DieResolverMethods<TOutput> {
|
|
6
|
+
raw: MethodType<LambderResponseBuilder, 'raw'>;
|
|
7
|
+
json: MethodType<LambderResponseBuilder, 'json'>;
|
|
8
|
+
xml: MethodType<LambderResponseBuilder, 'xml'>;
|
|
9
|
+
html: MethodType<LambderResponseBuilder, 'html'>;
|
|
10
|
+
status301: MethodType<LambderResponseBuilder, 'status301'>;
|
|
11
|
+
status404: MethodType<LambderResponseBuilder, 'status404'>;
|
|
12
|
+
cors: MethodType<LambderResponseBuilder, 'cors'>;
|
|
13
|
+
fileBase64: MethodType<LambderResponseBuilder, 'fileBase64'>;
|
|
14
|
+
file: MethodType<LambderResponseBuilder, 'file'>;
|
|
15
|
+
ejsFile: MethodType<LambderResponseBuilder, 'ejsFile'>;
|
|
16
|
+
ejsTemplate: MethodType<LambderResponseBuilder, 'ejsTemplate'>;
|
|
17
|
+
api: (payload: TOutput | null, config?: Parameters<LambderResponseBuilder['api']>[1], headers?: Parameters<LambderResponseBuilder['api']>[2]) => LambderResolverResponse;
|
|
19
18
|
}
|
|
20
|
-
export default class LambderResolver<
|
|
19
|
+
export default class LambderResolver<TOutput = any> extends LambderResponseBuilder<TOutput> {
|
|
21
20
|
resolve: (response: LambderResolverResponse) => void;
|
|
22
21
|
reject: (err: Error) => void;
|
|
23
|
-
die: DieResolverMethods<
|
|
22
|
+
die: DieResolverMethods<TOutput>;
|
|
24
23
|
constructor({ isCorsEnabled, publicPath, apiVersion, lambderUtils, ctx, resolve, reject }: {
|
|
25
24
|
isCorsEnabled: boolean;
|
|
26
25
|
publicPath: string;
|
|
@@ -30,7 +29,7 @@ export default class LambderResolver<TContract extends ApiContractShape = any, T
|
|
|
30
29
|
resolve: (response: LambderResolverResponse) => void;
|
|
31
30
|
reject: (err: Error) => void;
|
|
32
31
|
});
|
|
33
|
-
api(payload:
|
|
32
|
+
api(payload: TOutput | null, config?: Parameters<LambderResponseBuilder['api']>[1], headers?: Parameters<LambderResponseBuilder['api']>[2]): LambderResolverResponse;
|
|
34
33
|
private autoResolve;
|
|
35
34
|
private autoResolvePromise;
|
|
36
35
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import LambderUtils from "./LambderUtils.js";
|
|
2
2
|
import { LambderRenderContext } from "./Lambder.js";
|
|
3
|
-
import type { ApiContractShape } from "./LambderApiContract.js";
|
|
4
3
|
export type LambderResolverResponse = {
|
|
5
4
|
statusCode: number;
|
|
6
5
|
multiValueHeaders?: Record<string, string[]>;
|
|
@@ -18,7 +17,7 @@ export type LambderApiResponseConfig = {
|
|
|
18
17
|
export type LambderApiResponse<T> = LambderApiResponseConfig & {
|
|
19
18
|
payload?: T | null;
|
|
20
19
|
};
|
|
21
|
-
export default class LambderResponseBuilder<
|
|
20
|
+
export default class LambderResponseBuilder<TResponse = any> {
|
|
22
21
|
private isCorsEnabled;
|
|
23
22
|
private publicPath;
|
|
24
23
|
private apiVersion;
|
|
@@ -48,6 +47,6 @@ export default class LambderResponseBuilder<TContract extends ApiContractShape =
|
|
|
48
47
|
file(filePath: string, headers?: Record<string, string | string[]>, fallbackFilePath?: string): Promise<LambderResolverResponse>;
|
|
49
48
|
ejsTemplate(template: string, pageData: Record<string, any>, headers?: Record<string, string | string[]>): Promise<LambderResolverResponse>;
|
|
50
49
|
ejsFile(filePath: string, pageData: Record<string, any>, headers?: Record<string, string | string[]>): Promise<LambderResolverResponse>;
|
|
51
|
-
api
|
|
50
|
+
api(payload: TResponse | null, { versionExpired, sessionExpired, notAuthorized, message, errorMessage, logList, }?: LambderApiResponseConfig, headers?: Record<string, string | string[]>): LambderResolverResponse;
|
|
52
51
|
apiBinary<T = any>(payload: T | null, { versionExpired, sessionExpired, notAuthorized, message, errorMessage, logList, }?: LambderApiResponseConfig, headers?: Record<string, string | string[]>): LambderResolverResponse;
|
|
53
52
|
}
|
|
@@ -29,7 +29,6 @@ export default class LambderResponseBuilder {
|
|
|
29
29
|
}
|
|
30
30
|
const publicPath = path.resolve(this.publicPath);
|
|
31
31
|
const absolutePath = path.resolve(publicPath, filePath);
|
|
32
|
-
console.log("readPublicFileSync", { filePath, publicPath, absolutePath });
|
|
33
32
|
if (!absolutePath.startsWith(publicPath)) {
|
|
34
33
|
return "forbidden-public-path";
|
|
35
34
|
}
|
|
@@ -44,7 +43,6 @@ export default class LambderResponseBuilder {
|
|
|
44
43
|
}
|
|
45
44
|
const publicPath = path.resolve(this.publicPath);
|
|
46
45
|
const absolutePath = path.resolve(publicPath, filePath);
|
|
47
|
-
console.log("checkPublicFileExist", { filePath, publicPath, absolutePath });
|
|
48
46
|
if (!absolutePath.startsWith(publicPath)) {
|
|
49
47
|
return false;
|
|
50
48
|
}
|
|
@@ -166,7 +164,7 @@ export default class LambderResponseBuilder {
|
|
|
166
164
|
const mimeType = mimeTypeResolver.lookup(filePath);
|
|
167
165
|
const body = await this.readPublicFileSync(filePath);
|
|
168
166
|
if (body === "forbidden-public-path") {
|
|
169
|
-
throw
|
|
167
|
+
throw new Error("Forbidden public path: " + filePath);
|
|
170
168
|
}
|
|
171
169
|
const bodyBuffer = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
172
170
|
const bodyBase64 = bodyBuffer.toString("base64");
|
package/dist/LambderUtils.js
CHANGED
|
@@ -17,11 +17,10 @@ export default class LambderUtils {
|
|
|
17
17
|
}
|
|
18
18
|
const ejsPath = path.resolve(this.ejsPath);
|
|
19
19
|
const absolutePath = path.resolve(ejsPath, filePath);
|
|
20
|
-
console.log("readEjsFileSync", { filePath, ejsPath, absolutePath });
|
|
21
20
|
if (!absolutePath.startsWith(ejsPath)) {
|
|
22
21
|
return "forbidden-ejs-path";
|
|
23
22
|
}
|
|
24
|
-
return
|
|
23
|
+
return await fs.promises.readFile(absolutePath, 'utf-8');
|
|
25
24
|
}
|
|
26
25
|
;
|
|
27
26
|
async checkEjsFileExist(filePath) {
|
|
@@ -35,7 +34,6 @@ export default class LambderUtils {
|
|
|
35
34
|
}
|
|
36
35
|
const ejsPath = path.resolve(this.ejsPath);
|
|
37
36
|
const absolutePath = path.resolve(ejsPath, filePath);
|
|
38
|
-
console.log("checkEjsFileExist", { filePath, ejsPath, absolutePath });
|
|
39
37
|
if (!absolutePath.startsWith(ejsPath)) {
|
|
40
38
|
return false;
|
|
41
39
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,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
7
|
export { default as LambderMSW } from "./LambderMSW.js";
|
|
8
|
-
export { type ApiContractShape,
|
|
8
|
+
export { type ApiContractShape, } from "./LambderApiContract.js";
|
package/docs/LAMBDER_MSW.md
CHANGED
|
@@ -248,14 +248,14 @@ afterEach(() => server.resetHandlers());
|
|
|
248
248
|
afterAll(() => server.close());
|
|
249
249
|
|
|
250
250
|
// Setup LambderCaller
|
|
251
|
-
const
|
|
251
|
+
const lambderCaller = new LambderCaller<MyApiContract>({
|
|
252
252
|
apiPath: '/secure',
|
|
253
253
|
isCorsEnabled: false
|
|
254
254
|
});
|
|
255
255
|
|
|
256
256
|
describe('User APIs', () => {
|
|
257
257
|
it('should fetch user by id', async () => {
|
|
258
|
-
const result = await
|
|
258
|
+
const result = await lambderCaller.api('getUserById', { userId: '123' });
|
|
259
259
|
|
|
260
260
|
expect(result).toEqual({
|
|
261
261
|
id: '123',
|
|
@@ -265,7 +265,7 @@ describe('User APIs', () => {
|
|
|
265
265
|
});
|
|
266
266
|
|
|
267
267
|
it('should create a new user', async () => {
|
|
268
|
-
const result = await
|
|
268
|
+
const result = await lambderCaller.api('createUser', {
|
|
269
269
|
name: 'Jane Smith',
|
|
270
270
|
email: 'jane@example.com'
|
|
271
271
|
});
|
|
@@ -277,7 +277,7 @@ describe('User APIs', () => {
|
|
|
277
277
|
|
|
278
278
|
it('should handle session expired', async () => {
|
|
279
279
|
try {
|
|
280
|
-
await
|
|
280
|
+
await lambderCaller.api('getProtectedData', {});
|
|
281
281
|
// Should not reach here
|
|
282
282
|
expect(true).toBe(false);
|
|
283
283
|
} catch (error: any) {
|
|
@@ -287,7 +287,7 @@ describe('User APIs', () => {
|
|
|
287
287
|
|
|
288
288
|
it('should handle errors', async () => {
|
|
289
289
|
try {
|
|
290
|
-
await
|
|
290
|
+
await lambderCaller.api('failingApi', {});
|
|
291
291
|
// Should not reach here
|
|
292
292
|
expect(true).toBe(false);
|
|
293
293
|
} catch (error: any) {
|
|
@@ -386,7 +386,7 @@ it('should handle specific user', async () => {
|
|
|
386
386
|
})
|
|
387
387
|
);
|
|
388
388
|
|
|
389
|
-
const result = await
|
|
389
|
+
const result = await lambderCaller.api('getUserById', { userId: '999' });
|
|
390
390
|
expect(result?.name).toBe('Special User');
|
|
391
391
|
});
|
|
392
392
|
```
|