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 +60 -1
- package/dist/Lambder.d.ts +14 -4
- package/dist/Lambder.js +4 -2
- package/dist/LambderApiContract.d.ts +42 -0
- package/dist/LambderApiContract.js +26 -0
- package/dist/LambderCaller.d.ts +15 -4
- package/dist/LambderCaller.js +11 -5
- package/dist/LambderResponseBuilder.d.ts +1 -0
- package/dist/LambderResponseBuilder.js +32 -1
- package/dist/index.d.ts +1 -0
- package/docs/TYPE_SAFE_QUICK_START.md +123 -0
- package/examples/simplified-typed-api-example.ts +365 -0
- package/package.json +1 -1
- package/src/Lambder.ts +43 -3
- package/src/LambderApiContract.ts +51 -0
- package/src/LambderCaller.ts +48 -18
- package/src/LambderResponseBuilder.ts +40 -1
- package/src/index.ts +7 -0
- package/tests/type-safety.test.ts +224 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simplified Type-Safe API Example
|
|
3
|
+
*
|
|
4
|
+
* This example shows how to use Lambder's opt-in type-safe API system.
|
|
5
|
+
* Simply pass your API contract type to LambderCaller and Lambder constructors,
|
|
6
|
+
* and get full type safety with no extra wrapper functions needed!
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import Lambder from '../src/Lambder.js';
|
|
10
|
+
import LambderCaller from '../src/LambderCaller.js';
|
|
11
|
+
import type { ApiContract } from '../src/index.js';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Step 1: Define your data types
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
type User = {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
email: string;
|
|
21
|
+
role: 'admin' | 'user';
|
|
22
|
+
createdAt: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type CreateUserInput = {
|
|
26
|
+
name: string;
|
|
27
|
+
email: string;
|
|
28
|
+
password: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type UpdateUserInput = {
|
|
32
|
+
userId: string;
|
|
33
|
+
name?: string;
|
|
34
|
+
email?: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
type LoginInput = {
|
|
38
|
+
email: string;
|
|
39
|
+
password: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type LoginOutput = {
|
|
43
|
+
success: boolean;
|
|
44
|
+
user?: User;
|
|
45
|
+
token?: string;
|
|
46
|
+
error?: string;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// ============================================================================
|
|
50
|
+
// Step 2: Define your API contract (shared between frontend and backend)
|
|
51
|
+
// ============================================================================
|
|
52
|
+
|
|
53
|
+
export type MyApiContract = {
|
|
54
|
+
// API with input and output
|
|
55
|
+
getUserById: { input: { userId: string }, output: User },
|
|
56
|
+
|
|
57
|
+
// API with complex input/output
|
|
58
|
+
createUser: { input: CreateUserInput, output: User },
|
|
59
|
+
updateUser: { input: UpdateUserInput, output: User },
|
|
60
|
+
|
|
61
|
+
// API with void input (no parameters needed)
|
|
62
|
+
listUsers: { input: void, output: User[] },
|
|
63
|
+
getCurrentUser: { input: void, output: User },
|
|
64
|
+
|
|
65
|
+
// API with conditional output
|
|
66
|
+
login: { input: LoginInput, output: LoginOutput },
|
|
67
|
+
|
|
68
|
+
// API with primitive output
|
|
69
|
+
getUserCount: { input: void, output: number },
|
|
70
|
+
deleteUser: { input: { userId: string }, output: boolean },
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ============================================================================
|
|
74
|
+
// Step 3: Backend - Pass contract type to Lambder
|
|
75
|
+
// ============================================================================
|
|
76
|
+
|
|
77
|
+
export function setupBackend() {
|
|
78
|
+
// Pass the contract type as a generic parameter
|
|
79
|
+
const lambder = new Lambder<MyApiContract>({
|
|
80
|
+
publicPath: './public',
|
|
81
|
+
apiPath: '/api',
|
|
82
|
+
apiVersion: '1.0.0',
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// Now addApi is type-safe! ctx.apiPayload is automatically typed!
|
|
86
|
+
lambder.addApi('getUserById', async (ctx, resolver) => {
|
|
87
|
+
// ctx.apiPayload is typed as { userId: string }
|
|
88
|
+
const userId = ctx.apiPayload.userId; // ✅ TypeScript knows this!
|
|
89
|
+
|
|
90
|
+
// Mock database call
|
|
91
|
+
const user: User = {
|
|
92
|
+
id: userId,
|
|
93
|
+
name: 'John Doe',
|
|
94
|
+
email: 'john@example.com',
|
|
95
|
+
role: 'user',
|
|
96
|
+
createdAt: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return resolver.api(user);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// Session API with typed payload
|
|
103
|
+
lambder.addSessionApi('createUser', async (ctx, resolver) => {
|
|
104
|
+
// ctx.apiPayload is typed as CreateUserInput
|
|
105
|
+
const { name, email, password } = ctx.apiPayload;
|
|
106
|
+
|
|
107
|
+
// Validation with type safety
|
|
108
|
+
if (!name || !email || !password) {
|
|
109
|
+
return resolver.api(null, {
|
|
110
|
+
errorMessage: 'Missing required fields'
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Create user
|
|
115
|
+
const newUser: User = {
|
|
116
|
+
id: Math.random().toString(36).substr(2, 9),
|
|
117
|
+
name,
|
|
118
|
+
email,
|
|
119
|
+
role: 'user',
|
|
120
|
+
createdAt: new Date().toISOString(),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
return resolver.api(newUser);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// API with void input
|
|
127
|
+
lambder.addApi('listUsers', async (ctx, resolver) => {
|
|
128
|
+
// ctx.apiPayload is void/undefined
|
|
129
|
+
const users: User[] = [
|
|
130
|
+
{ id: '1', name: 'John', email: 'john@example.com', role: 'user', createdAt: new Date().toISOString() },
|
|
131
|
+
{ id: '2', name: 'Jane', email: 'jane@example.com', role: 'admin', createdAt: new Date().toISOString() },
|
|
132
|
+
];
|
|
133
|
+
|
|
134
|
+
return resolver.api(users);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Complex API with conditional response
|
|
138
|
+
lambder.addApi('login', async (ctx, resolver) => {
|
|
139
|
+
// ctx.apiPayload is typed as LoginInput
|
|
140
|
+
const { email, password } = ctx.apiPayload;
|
|
141
|
+
|
|
142
|
+
// Mock authentication
|
|
143
|
+
if (email === 'test@example.com' && password === 'password123') {
|
|
144
|
+
const result: LoginOutput = {
|
|
145
|
+
success: true,
|
|
146
|
+
user: {
|
|
147
|
+
id: '1',
|
|
148
|
+
name: 'Test User',
|
|
149
|
+
email: email,
|
|
150
|
+
role: 'user',
|
|
151
|
+
createdAt: new Date().toISOString(),
|
|
152
|
+
},
|
|
153
|
+
token: 'mock-jwt-token',
|
|
154
|
+
};
|
|
155
|
+
return resolver.api(result);
|
|
156
|
+
} else {
|
|
157
|
+
const result: LoginOutput = {
|
|
158
|
+
success: false,
|
|
159
|
+
error: 'Invalid credentials',
|
|
160
|
+
};
|
|
161
|
+
return resolver.api(result);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// You can still use RegExp or functions for dynamic patterns (untyped)
|
|
166
|
+
lambder.addApi(/^admin\./, async (ctx, resolver) => {
|
|
167
|
+
// ctx.apiPayload is any (untyped)
|
|
168
|
+
return resolver.api({ message: 'Admin API' });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return lambder;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ============================================================================
|
|
175
|
+
// Step 4: Frontend - Pass contract type to LambderCaller
|
|
176
|
+
// ============================================================================
|
|
177
|
+
|
|
178
|
+
export function setupFrontend() {
|
|
179
|
+
// Pass the contract type as a generic parameter
|
|
180
|
+
const caller = new LambderCaller<MyApiContract>({
|
|
181
|
+
apiPath: '/api',
|
|
182
|
+
apiVersion: '1.0.0',
|
|
183
|
+
isCorsEnabled: false,
|
|
184
|
+
errorHandler: (err) => {
|
|
185
|
+
console.error('API Error:', err);
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Now all API calls are type-safe!
|
|
190
|
+
|
|
191
|
+
// Example 1: Get user by ID
|
|
192
|
+
async function example1() {
|
|
193
|
+
// TypeScript knows:
|
|
194
|
+
// - First parameter is 'getUserById' (autocomplete shows all API names!)
|
|
195
|
+
// - Second parameter must be { userId: string }
|
|
196
|
+
// - Return type is User | null | undefined
|
|
197
|
+
const user = await caller.api('getUserById', { userId: '123' });
|
|
198
|
+
|
|
199
|
+
if (user) {
|
|
200
|
+
console.log(user.name); // ✅ TypeScript knows 'name' exists
|
|
201
|
+
console.log(user.email); // ✅ TypeScript knows 'email' exists
|
|
202
|
+
console.log(user.role); // ✅ TypeScript knows 'role' is 'admin' | 'user'
|
|
203
|
+
// console.log(user.age); // ✗ Error: Property 'age' does not exist
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Example 2: Create user
|
|
208
|
+
async function example2() {
|
|
209
|
+
// TypeScript enforces the CreateUserInput type
|
|
210
|
+
const newUser = await caller.api('createUser', {
|
|
211
|
+
name: 'Alice',
|
|
212
|
+
email: 'alice@example.com',
|
|
213
|
+
password: 'secret123',
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
if (newUser) {
|
|
217
|
+
console.log('Created user:', newUser.id);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// This would be a TypeScript error:
|
|
221
|
+
// await caller.api('createUser', { name: 'Bob' }); // ✗ Missing email and password
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Example 3: Login
|
|
225
|
+
async function example3() {
|
|
226
|
+
const result = await caller.api('login', {
|
|
227
|
+
email: 'test@example.com',
|
|
228
|
+
password: 'password123',
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
if (result?.success && result.user) {
|
|
232
|
+
console.log('Logged in as:', result.user.name);
|
|
233
|
+
console.log('Token:', result.token);
|
|
234
|
+
} else {
|
|
235
|
+
console.error('Login failed:', result?.error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Example 4: List users (void input)
|
|
240
|
+
async function example4() {
|
|
241
|
+
// For void input, pass undefined
|
|
242
|
+
const users = await caller.api('listUsers', undefined);
|
|
243
|
+
|
|
244
|
+
if (users) {
|
|
245
|
+
users.forEach(user => {
|
|
246
|
+
console.log(user.name); // ✅ TypeScript knows the array type
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Example 5: Get user count (primitive output)
|
|
252
|
+
async function example5() {
|
|
253
|
+
const count = await caller.api('getUserCount', undefined);
|
|
254
|
+
if (count !== null && count !== undefined) {
|
|
255
|
+
console.log(`Total users: ${count}`); // count is number
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Example 6: With custom headers
|
|
260
|
+
async function example6() {
|
|
261
|
+
const user = await caller.api(
|
|
262
|
+
'getUserById',
|
|
263
|
+
{ userId: '456' },
|
|
264
|
+
{ headers: { 'X-Custom-Header': 'value' } }
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Example 7: Using apiRaw for full response
|
|
269
|
+
async function example7() {
|
|
270
|
+
const response = await caller.apiRaw('getUserById', { userId: '123' });
|
|
271
|
+
|
|
272
|
+
if (response) {
|
|
273
|
+
console.log('Payload:', response.payload); // User | null
|
|
274
|
+
if (response.logList) {
|
|
275
|
+
console.log('Logs:', response.logList);
|
|
276
|
+
}
|
|
277
|
+
if (response.errorMessage) {
|
|
278
|
+
console.error('Error:', response.errorMessage);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return caller;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// ============================================================================
|
|
287
|
+
// Without Contract (Backward Compatibility)
|
|
288
|
+
// ============================================================================
|
|
289
|
+
|
|
290
|
+
export function setupWithoutContract() {
|
|
291
|
+
// If you don't pass a contract type, it works like before (untyped)
|
|
292
|
+
const caller = new LambderCaller({
|
|
293
|
+
apiPath: '/api',
|
|
294
|
+
isCorsEnabled: false,
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
// Still works, but no type safety
|
|
298
|
+
async function untypedExample() {
|
|
299
|
+
const user = await caller.api('getUserById', { userId: '123' });
|
|
300
|
+
// user is any
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const lambder = new Lambder({
|
|
304
|
+
publicPath: './public',
|
|
305
|
+
apiPath: '/api',
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
// Still works, but no type safety
|
|
309
|
+
lambder.addApi('getUserById', async (ctx, resolver) => {
|
|
310
|
+
// ctx.apiPayload is any
|
|
311
|
+
const user = { id: ctx.apiPayload.userId, name: 'User' };
|
|
312
|
+
return resolver.api(user);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// ============================================================================
|
|
317
|
+
// Type Safety Examples
|
|
318
|
+
// ============================================================================
|
|
319
|
+
|
|
320
|
+
export function typeSafetyExamples() {
|
|
321
|
+
const caller = new LambderCaller<MyApiContract>({ apiPath: '/api', isCorsEnabled: false });
|
|
322
|
+
|
|
323
|
+
async function examples() {
|
|
324
|
+
// ✓ VALID:
|
|
325
|
+
await caller.api('getUserById', { userId: '123' });
|
|
326
|
+
await caller.api('createUser', { name: 'Alice', email: 'alice@example.com', password: 'pass' });
|
|
327
|
+
await caller.api('listUsers', undefined);
|
|
328
|
+
|
|
329
|
+
// ✗ ERRORS (TypeScript prevents):
|
|
330
|
+
// await caller.api('getUserById'); // Missing required payload
|
|
331
|
+
// await caller.api('getUserById', { id: '123' }); // Wrong property name (should be userId)
|
|
332
|
+
// await caller.api('createUser', { name: 'Bob' }); // Missing email and password
|
|
333
|
+
// await caller.api('nonExistentApi', {}); // API doesn't exist in contract
|
|
334
|
+
|
|
335
|
+
// Type inference works:
|
|
336
|
+
const user = await caller.api('getUserById', { userId: '123' });
|
|
337
|
+
if (user) {
|
|
338
|
+
console.log(user.name); // ✓ TypeScript knows user has name
|
|
339
|
+
// console.log(user.age); // ✗ Error: Property 'age' does not exist
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const users = await caller.api('listUsers', undefined);
|
|
343
|
+
if (users) {
|
|
344
|
+
users.forEach(u => {
|
|
345
|
+
console.log(u.email); // ✓ TypeScript knows array item structure
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ============================================================================
|
|
352
|
+
// Key Benefits
|
|
353
|
+
// ============================================================================
|
|
354
|
+
//
|
|
355
|
+
// 1. ✅ Type Safety - Frontend and backend share the same types
|
|
356
|
+
// 2. ✅ Autocomplete - IDE suggests available APIs as you type
|
|
357
|
+
// 3. ✅ No Wrappers - Use existing api() and addApi() methods
|
|
358
|
+
// 4. ✅ Opt-In - Add types when you want, or don't use them at all
|
|
359
|
+
// 5. ✅ Backward Compatible - Existing code works without changes
|
|
360
|
+
// 6. ✅ Simple - Just pass type to constructor, that's it!
|
|
361
|
+
// 7. ✅ Zero Runtime Overhead - Pure TypeScript types
|
|
362
|
+
//
|
|
363
|
+
// ============================================================================
|
|
364
|
+
|
|
365
|
+
export { Lambder, LambderCaller, type ApiContract };
|
package/package.json
CHANGED
package/src/Lambder.ts
CHANGED
|
@@ -8,6 +8,7 @@ import LambderResponseBuilder, { LambderResolverResponse } from "./LambderRespon
|
|
|
8
8
|
import LambderUtils from "./LambderUtils.js";
|
|
9
9
|
import LambderSessionManager, { type LambderSessionContext } from "./LambderSessionManager.js";
|
|
10
10
|
import LambderSessionController from "./LambderSessionController.js";
|
|
11
|
+
import type { ApiContract } from "./LambderApiContract.js";
|
|
11
12
|
|
|
12
13
|
type Path = `/${string}`;
|
|
13
14
|
|
|
@@ -23,6 +24,7 @@ export type LambderRenderContext = {
|
|
|
23
24
|
apiPayload: any;
|
|
24
25
|
headers: APIGatewayProxyEventHeaders;
|
|
25
26
|
session: LambderSessionContext|null;
|
|
27
|
+
event: APIGatewayProxyEvent;
|
|
26
28
|
lambdaContext: Context;
|
|
27
29
|
_otherInternal: {
|
|
28
30
|
isApiCall: boolean,
|
|
@@ -78,7 +80,7 @@ export const createContext = (
|
|
|
78
80
|
|
|
79
81
|
return {
|
|
80
82
|
host, path, pathParams, method,
|
|
81
|
-
get, post, cookie,
|
|
83
|
+
get, post, cookie, event,
|
|
82
84
|
apiName, apiPayload,
|
|
83
85
|
headers, session, lambdaContext,
|
|
84
86
|
_otherInternal: {
|
|
@@ -91,7 +93,7 @@ export const createContext = (
|
|
|
91
93
|
}
|
|
92
94
|
|
|
93
95
|
|
|
94
|
-
export default class Lambder {
|
|
96
|
+
export default class Lambder<TContract extends ApiContract = any> {
|
|
95
97
|
public apiPath: string;
|
|
96
98
|
public apiVersion: null|string;
|
|
97
99
|
public isCorsEnabled: boolean = false;
|
|
@@ -133,7 +135,7 @@ export default class Lambder {
|
|
|
133
135
|
this.utils = new LambderUtils({ ejsPath });
|
|
134
136
|
}
|
|
135
137
|
|
|
136
|
-
|
|
138
|
+
enableCors(isCorsEnabled: boolean){
|
|
137
139
|
this.isCorsEnabled = isCorsEnabled;
|
|
138
140
|
}
|
|
139
141
|
|
|
@@ -237,6 +239,25 @@ export default class Lambder {
|
|
|
237
239
|
});
|
|
238
240
|
};
|
|
239
241
|
|
|
242
|
+
// Overload for untyped API with RegExp or function
|
|
243
|
+
addApi(
|
|
244
|
+
apiName: ConditionFunction|RegExp,
|
|
245
|
+
actionFn: ActionFunction
|
|
246
|
+
):void;
|
|
247
|
+
// Overload for typed API with string name (must come before untyped string overload)
|
|
248
|
+
addApi<TApiName extends keyof TContract & string>(
|
|
249
|
+
apiName: TApiName,
|
|
250
|
+
actionFn: (
|
|
251
|
+
ctx: LambderRenderContext & { apiPayload: TContract[TApiName]['input'] },
|
|
252
|
+
resolver: LambderResolver
|
|
253
|
+
) => LambderResolverResponse|Promise<LambderResolverResponse>
|
|
254
|
+
):void;
|
|
255
|
+
// Overload for untyped API with string (backward compatibility, must be last)
|
|
256
|
+
addApi(
|
|
257
|
+
apiName: string,
|
|
258
|
+
actionFn: ActionFunction
|
|
259
|
+
):void;
|
|
260
|
+
// Implementation
|
|
240
261
|
addApi(apiName: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
241
262
|
this.actionList.push({
|
|
242
263
|
conditionFn: (ctx:LambderRenderContext) => (
|
|
@@ -250,6 +271,25 @@ export default class Lambder {
|
|
|
250
271
|
});
|
|
251
272
|
};
|
|
252
273
|
|
|
274
|
+
// Overload for untyped session API with RegExp or function
|
|
275
|
+
addSessionApi(
|
|
276
|
+
apiName: ConditionFunction|RegExp,
|
|
277
|
+
actionFn: ActionFunction
|
|
278
|
+
):void;
|
|
279
|
+
// Overload for typed session API with string name (must come before untyped string overload)
|
|
280
|
+
addSessionApi<TApiName extends keyof TContract & string>(
|
|
281
|
+
apiName: TApiName,
|
|
282
|
+
actionFn: (
|
|
283
|
+
ctx: LambderRenderContext & { apiPayload: TContract[TApiName]['input'] },
|
|
284
|
+
resolver: LambderResolver
|
|
285
|
+
) => LambderResolverResponse|Promise<LambderResolverResponse>
|
|
286
|
+
):void;
|
|
287
|
+
// Overload for untyped session API with string (backward compatibility, must be last)
|
|
288
|
+
addSessionApi(
|
|
289
|
+
apiName: string,
|
|
290
|
+
actionFn: ActionFunction
|
|
291
|
+
):void;
|
|
292
|
+
// Implementation
|
|
253
293
|
addSessionApi(apiName: string|ConditionFunction|RegExp, actionFn: ActionFunction):void{
|
|
254
294
|
this.actionList.push({
|
|
255
295
|
conditionFn: (ctx:LambderRenderContext) => (
|
|
@@ -0,0 +1,51 @@
|
|
|
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
|
+
/**
|
|
28
|
+
* Base type for API contracts
|
|
29
|
+
*/
|
|
30
|
+
export type ApiContract = {
|
|
31
|
+
[apiName: string]: {
|
|
32
|
+
input: any;
|
|
33
|
+
output: any;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract input type from contract for a specific API
|
|
39
|
+
*/
|
|
40
|
+
export type ApiInput<
|
|
41
|
+
TContract extends ApiContract,
|
|
42
|
+
TApiName extends keyof TContract
|
|
43
|
+
> = TContract[TApiName]['input'];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Extract output type from contract for a specific API
|
|
47
|
+
*/
|
|
48
|
+
export type ApiOutput<
|
|
49
|
+
TContract extends ApiContract,
|
|
50
|
+
TApiName extends keyof TContract
|
|
51
|
+
> = TContract[TApiName]['output'];
|
package/src/LambderCaller.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import Cookies from 'js-cookie';
|
|
2
2
|
import { LambderApiResponse } from './LambderResponseBuilder';
|
|
3
|
+
import type { ApiContract } from './LambderApiContract';
|
|
3
4
|
|
|
4
5
|
type VoidFunction = ()=>void|Promise<void>;
|
|
5
6
|
type FetchTracker = { apiName: string, done: boolean, fetchEndCalled: boolean };
|
|
@@ -23,7 +24,7 @@ type FetchEndEventHandler = (params: {
|
|
|
23
24
|
type ErrorHandler = (err: Error) => void|Promise<void>;
|
|
24
25
|
type MessageHandler = (message:any) => void|Promise<void>;
|
|
25
26
|
|
|
26
|
-
export default class LambderCaller {
|
|
27
|
+
export default class LambderCaller<TContract extends ApiContract = any> {
|
|
27
28
|
private isCorsEnabled: boolean;
|
|
28
29
|
private apiPath: string;
|
|
29
30
|
private apiVersion?: string;
|
|
@@ -90,17 +91,24 @@ export default class LambderCaller {
|
|
|
90
91
|
this.sessionCsrfCookieKey = sessionCsrfCookieKey;
|
|
91
92
|
}
|
|
92
93
|
|
|
93
|
-
async apiRaw<
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
94
|
+
async apiRaw<
|
|
95
|
+
TApiName extends keyof TContract & string = string,
|
|
96
|
+
TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any
|
|
97
|
+
>(
|
|
98
|
+
apiName: TApiName,
|
|
99
|
+
payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any,
|
|
100
|
+
options?: {
|
|
101
|
+
headers?: Record<string, any>
|
|
102
|
+
versionExpiredHandler?: VoidFunction,
|
|
103
|
+
sessionExpiredHandler?: VoidFunction,
|
|
104
|
+
messageHandler?: MessageHandler,
|
|
105
|
+
errorMessageHandler?: MessageHandler,
|
|
106
|
+
notAuthorizedHandler?: VoidFunction,
|
|
107
|
+
errorHandler?: ErrorHandler,
|
|
108
|
+
fetchStartedHandler?: FetchStartEventHandler,
|
|
109
|
+
fetchEndedHandler?: FetchEndEventHandler,
|
|
110
|
+
}
|
|
111
|
+
): Promise<LambderApiResponse<TOutput>|null|undefined>{
|
|
104
112
|
const headers = options?.headers;
|
|
105
113
|
const fetchTracker: FetchTracker = { apiName, done: false, fetchEndCalled: false };
|
|
106
114
|
try {
|
|
@@ -117,10 +125,15 @@ export default class LambderCaller {
|
|
|
117
125
|
credentials: 'same-origin', redirect: 'follow', referrerPolicy: 'origin',
|
|
118
126
|
headers: { 'Content-Type': 'application/json', ...(headers || {}) },
|
|
119
127
|
body: JSON.stringify({ apiName, version, token, siteHost, payload, }),
|
|
120
|
-
}).then(res=>{
|
|
128
|
+
}).then(async (res)=>{
|
|
121
129
|
if(res.status >= 500) throw new Error("Request failed: " + res.status + " - " + res.statusText);
|
|
122
|
-
|
|
123
|
-
|
|
130
|
+
if(res.headers.get("Content-Type")?.includes("application/lambder-json-stream")){
|
|
131
|
+
const decompressed = res.json();
|
|
132
|
+
return decompressed;
|
|
133
|
+
}else{
|
|
134
|
+
return res.json();
|
|
135
|
+
}
|
|
136
|
+
}) as LambderApiResponse<TOutput>;
|
|
124
137
|
fetchTracker.done = true;
|
|
125
138
|
if(this.fetchEndedHandler){
|
|
126
139
|
fetchTracker.fetchEndCalled = true;
|
|
@@ -157,7 +170,7 @@ export default class LambderCaller {
|
|
|
157
170
|
if(this.notAuthorizedHandler){
|
|
158
171
|
await this.notAuthorizedHandler();
|
|
159
172
|
}else if(this.errorHandler){
|
|
160
|
-
await this.errorHandler(new Error("
|
|
173
|
+
await this.errorHandler(new Error("Not Authorized;"));
|
|
161
174
|
}
|
|
162
175
|
return null;
|
|
163
176
|
}
|
|
@@ -184,8 +197,25 @@ export default class LambderCaller {
|
|
|
184
197
|
};
|
|
185
198
|
|
|
186
199
|
// Use the same type for api but adjust the return type
|
|
187
|
-
async api<
|
|
188
|
-
|
|
200
|
+
async api<
|
|
201
|
+
TApiName extends keyof TContract & string = string,
|
|
202
|
+
TOutput = TApiName extends keyof TContract ? TContract[TApiName]['output'] : any
|
|
203
|
+
>(
|
|
204
|
+
apiName: TApiName,
|
|
205
|
+
payload?: TApiName extends keyof TContract ? TContract[TApiName]['input'] : any,
|
|
206
|
+
options?: {
|
|
207
|
+
headers?: Record<string, any>
|
|
208
|
+
versionExpiredHandler?: VoidFunction,
|
|
209
|
+
sessionExpiredHandler?: VoidFunction,
|
|
210
|
+
messageHandler?: MessageHandler,
|
|
211
|
+
errorMessageHandler?: MessageHandler,
|
|
212
|
+
notAuthorizedHandler?: VoidFunction,
|
|
213
|
+
errorHandler?: ErrorHandler,
|
|
214
|
+
fetchStartedHandler?: FetchStartEventHandler,
|
|
215
|
+
fetchEndedHandler?: FetchEndEventHandler,
|
|
216
|
+
}
|
|
217
|
+
): Promise<TOutput|null|undefined> {
|
|
218
|
+
const result = await this.apiRaw<TApiName, TOutput>(apiName, payload, options);
|
|
189
219
|
return result?.payload;
|
|
190
220
|
}
|
|
191
221
|
|
|
@@ -199,7 +199,11 @@ export default class LambderResponseBuilder {
|
|
|
199
199
|
}
|
|
200
200
|
const mimeType = mimeTypeResolver.lookup(filePath);
|
|
201
201
|
const body = this.readPublicFileSync(filePath);
|
|
202
|
-
|
|
202
|
+
if (body === "forbidden-public-path") {
|
|
203
|
+
throw { error: "Forbidden public path: " + filePath };
|
|
204
|
+
}
|
|
205
|
+
const bodyBuffer: Buffer = Buffer.isBuffer(body) ? body : Buffer.from(body);
|
|
206
|
+
const bodyBase64 = bodyBuffer.toString("base64");
|
|
203
207
|
console.log("bodyBase64.length",bodyBase64.length);
|
|
204
208
|
return this.fileBase64(bodyBase64, mimeType || "", headers);
|
|
205
209
|
};
|
|
@@ -258,4 +262,39 @@ export default class LambderResponseBuilder {
|
|
|
258
262
|
}, headers);
|
|
259
263
|
};
|
|
260
264
|
|
|
265
|
+
private apiBinary<T=any>(
|
|
266
|
+
payload: T | null,
|
|
267
|
+
{
|
|
268
|
+
versionExpired, sessionExpired, notAuthorized,
|
|
269
|
+
message, errorMessage, logList,
|
|
270
|
+
}: LambderApiResponseConfig = {
|
|
271
|
+
versionExpired: undefined, sessionExpired: undefined, notAuthorized: undefined,
|
|
272
|
+
message: null, errorMessage: null, logList: undefined
|
|
273
|
+
},
|
|
274
|
+
headers?: Record<string, string|string[]>,
|
|
275
|
+
): LambderResolverResponse {
|
|
276
|
+
const finalLogList = logList || this.ctx?._otherInternal?.logToApiResponseAccumulator;
|
|
277
|
+
const result = {
|
|
278
|
+
apiVersion: this.apiVersion,
|
|
279
|
+
payload,
|
|
280
|
+
...(versionExpired ? {versionExpired} : {}),
|
|
281
|
+
...(sessionExpired ? {sessionExpired} : {}),
|
|
282
|
+
...(notAuthorized ? {notAuthorized} : {}),
|
|
283
|
+
...(message ? {message} : {}),
|
|
284
|
+
...(errorMessage ? {errorMessage} : {}),
|
|
285
|
+
...(finalLogList?.length ? {logList: finalLogList} : {}),
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
return this.raw({
|
|
289
|
+
statusCode: 200,
|
|
290
|
+
isBase64Encoded: true,
|
|
291
|
+
multiValueHeaders: {
|
|
292
|
+
"Content-Type": ["application/lambder-json-stream"],
|
|
293
|
+
"Content-Encoding": ["gzip"],
|
|
294
|
+
...convertToMultiHeader(headers)
|
|
295
|
+
},
|
|
296
|
+
body: Buffer.from(JSON.stringify(result)).toString("base64"),
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
|
|
261
300
|
};
|
package/src/index.ts
CHANGED
|
@@ -5,3 +5,10 @@ export { default as LambderCaller } from "./LambderCaller.js";
|
|
|
5
5
|
export { default as LambderResponseBuilder } from "./LambderResponseBuilder.js";
|
|
6
6
|
export { default as LambderResolver } from "./LambderResolver.js";
|
|
7
7
|
export { default as LambderSessionManager } from "./LambderSessionManager.js";
|
|
8
|
+
|
|
9
|
+
// Type-safe API contract utilities
|
|
10
|
+
export {
|
|
11
|
+
type ApiContract,
|
|
12
|
+
type ApiInput,
|
|
13
|
+
type ApiOutput,
|
|
14
|
+
} from "./LambderApiContract.js";
|