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
|
@@ -1,200 +1,77 @@
|
|
|
1
|
-
# Type-Safe API Quick Start
|
|
1
|
+
# Type-Safe API Quick Start (v2.0)
|
|
2
2
|
|
|
3
3
|
## In 3 Simple Steps
|
|
4
4
|
|
|
5
|
-
### 1. Define
|
|
5
|
+
### 1. Define & Implement APIs (Backend)
|
|
6
6
|
|
|
7
|
-
|
|
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
|
|
7
|
+
Use Zod schemas to define your API contract inline. Lambder will automatically validate inputs at runtime and infer types for compile-time safety.
|
|
19
8
|
|
|
20
9
|
```typescript
|
|
21
|
-
import
|
|
22
|
-
import
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import Lambder from "lambder";
|
|
12
|
+
|
|
13
|
+
// Initialize
|
|
14
|
+
const lambder = new Lambder({
|
|
15
|
+
publicPath: "./public",
|
|
16
|
+
apiPath: "/api"
|
|
17
|
+
})
|
|
18
|
+
// Chain APIs
|
|
19
|
+
.addApi("getUser", {
|
|
20
|
+
input: z.object({ userId: z.string() }),
|
|
21
|
+
output: z.object({ id: z.string(), name: z.string() })
|
|
22
|
+
}, async (ctx, resolver) => {
|
|
23
|
+
// ctx.apiPayload is typed as { userId: string }
|
|
24
|
+
// Runtime validation is already performed!
|
|
25
|
+
return resolver.api({
|
|
26
|
+
id: ctx.apiPayload.userId,
|
|
27
|
+
name: "John Doe"
|
|
28
|
+
});
|
|
27
29
|
});
|
|
28
30
|
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
// ✅ ctx.apiPayload is automatically typed as { userId: string }
|
|
32
|
-
const user = await db.getUser(ctx.apiPayload.userId);
|
|
33
|
-
|
|
34
|
-
// ✅ resolver.api() enforces the output type (User)
|
|
35
|
-
return resolver.api(user); // TypeScript checks that user matches User type!
|
|
36
|
-
});
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
### 3. Frontend - Pass Type to LambderCaller
|
|
40
|
-
|
|
41
|
-
```typescript
|
|
42
|
-
import { LambderCaller } from 'lambder';
|
|
43
|
-
import type { MyApiContract } from './shared/apiContract';
|
|
44
|
-
|
|
45
|
-
const caller = new LambderCaller<MyApiContract>({
|
|
46
|
-
apiPath: '/api',
|
|
47
|
-
isCorsEnabled: false
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
// Now api() is type-safe!
|
|
51
|
-
const user = await caller.api('getUserById', { userId: '123' });
|
|
52
|
-
// user is typed as User | null | undefined
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
## That's It!
|
|
56
|
-
|
|
57
|
-
- ✅ **No wrapper functions needed**
|
|
58
|
-
- ✅ **Use existing `api()` and `addApi()` methods**
|
|
59
|
-
- ✅ **Full autocomplete in IDE**
|
|
60
|
-
- ✅ **Type-safe inputs AND outputs**
|
|
61
|
-
- ✅ **Compile-time validation**
|
|
62
|
-
- ✅ **Backward compatible**
|
|
31
|
+
// Export the inferred contract type
|
|
32
|
+
export type AppContract = typeof lambder.ApiContractType;
|
|
63
33
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
```typescript
|
|
67
|
-
type MyApiContract = {
|
|
68
|
-
apiName: { input: InputType, output: OutputType }
|
|
69
|
-
}
|
|
34
|
+
export const handler = lambder.getHandler();
|
|
70
35
|
```
|
|
71
36
|
|
|
72
|
-
|
|
37
|
+
### 2. Use in Frontend
|
|
73
38
|
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
getUserById: { input: { userId: string }, output: User }
|
|
77
|
-
```
|
|
39
|
+
Import the type (not the code) and use `LambderCaller`.
|
|
78
40
|
|
|
79
|
-
### API with no input
|
|
80
41
|
```typescript
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
### API with complex types
|
|
85
|
-
```typescript
|
|
86
|
-
updateUser: {
|
|
87
|
-
input: { id: string } & Partial<User>,
|
|
88
|
-
output: User
|
|
89
|
-
}
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
### API with conditional output
|
|
93
|
-
```typescript
|
|
94
|
-
login: {
|
|
95
|
-
input: { email: string, password: string },
|
|
96
|
-
output: { success: boolean, user?: User, error?: string }
|
|
97
|
-
}
|
|
98
|
-
```
|
|
42
|
+
import { LambderCaller } from "lambder";
|
|
43
|
+
import type { AppContract } from "./backend"; // Type-only import
|
|
99
44
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
The type system now enforces that `resolver.api()` returns data matching your contract's output type:
|
|
103
|
-
|
|
104
|
-
```typescript
|
|
105
|
-
type MyContract = ApiContract<{
|
|
106
|
-
getNumber: { input: void, output: number },
|
|
107
|
-
getUser: { input: { id: string }, output: User }
|
|
108
|
-
}>;
|
|
109
|
-
|
|
110
|
-
const lambder = new Lambder<MyContract>({ ... });
|
|
111
|
-
|
|
112
|
-
// ✅ CORRECT
|
|
113
|
-
lambder.addApi('getNumber', async (ctx, resolver) => {
|
|
114
|
-
return resolver.api(42); // number - matches output type
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
// ❌ ERROR: Type 'string' is not assignable to type 'number'
|
|
118
|
-
lambder.addApi('getNumber', async (ctx, resolver) => {
|
|
119
|
-
return resolver.api("wrong"); // TypeScript error!
|
|
45
|
+
const lambderCaller = new LambderCaller<AppContract>({
|
|
46
|
+
apiPath: "/api"
|
|
120
47
|
});
|
|
121
48
|
|
|
122
|
-
//
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
// ❌ ERROR: Missing required properties
|
|
128
|
-
lambder.addApi('getUser', async (ctx, resolver) => {
|
|
129
|
-
return resolver.api({ id: "123" }); // TypeScript error - incomplete User!
|
|
130
|
-
});
|
|
49
|
+
// Fully typed!
|
|
50
|
+
// TypeScript knows 'getUser' takes { userId: string } and returns { id: string, name: string }
|
|
51
|
+
const user = await lambderCaller.api("getUser", { userId: "123" });
|
|
131
52
|
```
|
|
132
53
|
|
|
133
|
-
|
|
134
|
-
- `resolver.api()` - typed return value
|
|
135
|
-
- `resolver.die.api()` - typed return value
|
|
136
|
-
- `addApi()` - typed for regular APIs
|
|
137
|
-
- `addSessionApi()` - typed for session APIs
|
|
54
|
+
### 3. Modular APIs (Optional)
|
|
138
55
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
Don't want type safety? Just don't pass the generic type:
|
|
56
|
+
For larger apps, split your APIs into modules using `.use()`.
|
|
142
57
|
|
|
143
58
|
```typescript
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
})
|
|
59
|
+
// api.user.ts
|
|
60
|
+
import { z } from "zod";
|
|
61
|
+
import Lambder from "lambder";
|
|
62
|
+
|
|
63
|
+
export const userApi = <T>(l: Lambder<T>) => {
|
|
64
|
+
return l.addApi("login", {
|
|
65
|
+
input: z.object({ email: z.string() }),
|
|
66
|
+
output: z.boolean()
|
|
67
|
+
}, async (ctx, resolver) => {
|
|
68
|
+
return resolver.api(true);
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// index.ts
|
|
73
|
+
import { userApi } from "./api.user";
|
|
74
|
+
|
|
75
|
+
const lambder = new Lambder()
|
|
76
|
+
.use(userApi); // Types are preserved!
|
|
153
77
|
```
|
|
154
|
-
|
|
155
|
-
## Full Example
|
|
156
|
-
|
|
157
|
-
See [simplified-typed-api-example.ts](../examples/simplified-typed-api-example.ts) for a complete working example.
|
|
158
|
-
|
|
159
|
-
## Testing Your Type-Safe APIs
|
|
160
|
-
|
|
161
|
-
LambderMSW provides full type safety for testing your APIs with MSW (Mock Service Worker):
|
|
162
|
-
|
|
163
|
-
```typescript
|
|
164
|
-
import { LambderMSW } from 'lambder';
|
|
165
|
-
import { setupServer } from 'msw/node';
|
|
166
|
-
import type { MyApiContract } from './shared/apiContract';
|
|
167
|
-
|
|
168
|
-
// Create type-safe MSW instance
|
|
169
|
-
const lambderMSW = new LambderMSW<MyApiContract>({
|
|
170
|
-
apiPath: '/api'
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// Mock with full type safety! ✨
|
|
174
|
-
const handlers = [
|
|
175
|
-
lambderMSW.mockApi('getUserById', async (payload) => {
|
|
176
|
-
// payload is typed as { userId: string }
|
|
177
|
-
// Return value is type-checked against output
|
|
178
|
-
return {
|
|
179
|
-
id: payload.userId,
|
|
180
|
-
name: 'John Doe',
|
|
181
|
-
email: 'john@example.com'
|
|
182
|
-
};
|
|
183
|
-
})
|
|
184
|
-
];
|
|
185
|
-
|
|
186
|
-
const server = setupServer(...handlers);
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
📖 See [LAMBDER_MSW.md](./LAMBDER_MSW.md) for complete testing documentation.
|
|
190
|
-
|
|
191
|
-
## Key Points
|
|
192
|
-
|
|
193
|
-
- **Contract is just a TypeScript type** - No runtime code!
|
|
194
|
-
- **Zero overhead** - All type checking happens at compile time
|
|
195
|
-
- **Input AND output validation** - Both sides of your API are type-safe
|
|
196
|
-
- **Compile-time safety** - Catch type mismatches before deployment
|
|
197
|
-
- **Opt-in** - Use types when you want them
|
|
198
|
-
- **Simple** - Just pass type to constructor
|
|
199
|
-
- **Autocomplete** - IDE shows available APIs as you type
|
|
200
|
-
- **Testing support** - LambderMSW provides type-safe mocking
|
|
@@ -17,27 +17,30 @@
|
|
|
17
17
|
// @ts-nocheck - Example file, types may not be available
|
|
18
18
|
import { describe, it, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
19
19
|
import { setupServer } from 'msw/node';
|
|
20
|
-
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
import { LambderMSW, LambderCaller } from '../src/index.ts';
|
|
22
|
+
import Lambder from '../src/Lambder.js';
|
|
23
|
+
|
|
24
|
+
// Define your API contract using Lambder chaining
|
|
25
|
+
const lambder = new Lambder()
|
|
26
|
+
.addApi('getUserById', {
|
|
27
|
+
input: z.object({ userId: z.string() }),
|
|
28
|
+
output: z.object({ id: z.string(), name: z.string(), email: z.string() }).nullable()
|
|
29
|
+
}, async () => ({} as any)) // Dummy handler for type inference
|
|
30
|
+
.addApi('createUser', {
|
|
31
|
+
input: z.object({ name: z.string(), email: z.string() }),
|
|
32
|
+
output: z.object({ id: z.string(), name: z.string(), email: z.string() })
|
|
33
|
+
}, async () => ({} as any))
|
|
34
|
+
.addApi('getUsers', {
|
|
35
|
+
input: z.object({ limit: z.number().optional() }),
|
|
36
|
+
output: z.array(z.object({ id: z.string(), name: z.string(), email: z.string() }))
|
|
37
|
+
}, async () => ({} as any))
|
|
38
|
+
.addApi('deleteUser', {
|
|
39
|
+
input: z.object({ userId: z.string() }),
|
|
40
|
+
output: z.object({ success: z.boolean() })
|
|
41
|
+
}, async () => ({} as any));
|
|
42
|
+
|
|
43
|
+
type TestApiContract = typeof lambder.ApiContractType;
|
|
41
44
|
|
|
42
45
|
// Setup LambderMSW with type safety
|
|
43
46
|
const lambderMSW = new LambderMSW<TestApiContract>({
|
|
@@ -89,7 +92,7 @@ const handlers = [
|
|
|
89
92
|
const server = setupServer(...handlers);
|
|
90
93
|
|
|
91
94
|
// Setup LambderCaller
|
|
92
|
-
const
|
|
95
|
+
const lambderCaller = new LambderCaller<TestApiContract>({
|
|
93
96
|
apiPath: '/secure',
|
|
94
97
|
isCorsEnabled: false,
|
|
95
98
|
});
|
|
@@ -109,7 +112,7 @@ describe('LambderMSW Testing Example', () => {
|
|
|
109
112
|
});
|
|
110
113
|
|
|
111
114
|
it('should fetch user by id', async () => {
|
|
112
|
-
const user = await
|
|
115
|
+
const user = await lambderCaller.api('getUserById', { userId: '1' });
|
|
113
116
|
|
|
114
117
|
expect(user).toEqual({
|
|
115
118
|
id: '1',
|
|
@@ -119,7 +122,7 @@ describe('LambderMSW Testing Example', () => {
|
|
|
119
122
|
});
|
|
120
123
|
|
|
121
124
|
it('should return null for non-existent user', async () => {
|
|
122
|
-
const user = await
|
|
125
|
+
const user = await lambderCaller.api('getUserById', { userId: '999' });
|
|
123
126
|
|
|
124
127
|
expect(user).toBeNull();
|
|
125
128
|
});
|
|
@@ -127,7 +130,7 @@ describe('LambderMSW Testing Example', () => {
|
|
|
127
130
|
it('should create a new user with delay', async () => {
|
|
128
131
|
const startTime = Date.now();
|
|
129
132
|
|
|
130
|
-
const newUser = await
|
|
133
|
+
const newUser = await lambderCaller.api('createUser', {
|
|
131
134
|
name: 'Alice Wonder',
|
|
132
135
|
email: 'alice@example.com',
|
|
133
136
|
});
|
|
@@ -143,7 +146,7 @@ describe('LambderMSW Testing Example', () => {
|
|
|
143
146
|
});
|
|
144
147
|
|
|
145
148
|
it('should fetch list of users', async () => {
|
|
146
|
-
const users = await
|
|
149
|
+
const users = await lambderCaller.api('getUsers', { limit: 2 });
|
|
147
150
|
|
|
148
151
|
expect(users).toHaveLength(2);
|
|
149
152
|
expect(users?.[0]?.name).toBe('John Doe');
|
|
@@ -154,7 +157,7 @@ describe('LambderMSW Testing Example', () => {
|
|
|
154
157
|
let errorCaught = false;
|
|
155
158
|
|
|
156
159
|
try {
|
|
157
|
-
await
|
|
160
|
+
await lambderCaller.api('deleteUser', { userId: '1' });
|
|
158
161
|
} catch (error: any) {
|
|
159
162
|
errorCaught = true;
|
|
160
163
|
expect(error.notAuthorized).toBe(true);
|
|
@@ -175,7 +178,7 @@ describe('LambderMSW Testing Example', () => {
|
|
|
175
178
|
})
|
|
176
179
|
);
|
|
177
180
|
|
|
178
|
-
const user = await
|
|
181
|
+
const user = await lambderCaller.api('getUserById', { userId: '999' });
|
|
179
182
|
|
|
180
183
|
expect(user?.name).toBe('Override User');
|
|
181
184
|
});
|
|
@@ -189,7 +192,7 @@ describe('LambderMSW Testing Example', () => {
|
|
|
189
192
|
let errorCaught = false;
|
|
190
193
|
|
|
191
194
|
try {
|
|
192
|
-
await
|
|
195
|
+
await lambderCaller.api('getUserById', { userId: '1' });
|
|
193
196
|
} catch (error: any) {
|
|
194
197
|
errorCaught = true;
|
|
195
198
|
expect(error.sessionExpired).toBe(true);
|
|
@@ -206,7 +209,7 @@ describe('LambderMSW Testing Example', () => {
|
|
|
206
209
|
let errorCaught = false;
|
|
207
210
|
|
|
208
211
|
try {
|
|
209
|
-
await
|
|
212
|
+
await lambderCaller.api('createUser', {
|
|
210
213
|
name: 'Duplicate',
|
|
211
214
|
email: 'john@example.com',
|
|
212
215
|
});
|
|
@@ -239,10 +242,10 @@ describe('Dynamic Response Testing', () => {
|
|
|
239
242
|
})
|
|
240
243
|
);
|
|
241
244
|
|
|
242
|
-
const users3 = await
|
|
245
|
+
const users3 = await lambderCaller.api('getUsers', { limit: 3 });
|
|
243
246
|
expect(users3).toHaveLength(3);
|
|
244
247
|
|
|
245
|
-
const users5 = await
|
|
248
|
+
const users5 = await lambderCaller.api('getUsers', { limit: 5 });
|
|
246
249
|
expect(users5).toHaveLength(5);
|
|
247
250
|
});
|
|
248
251
|
});
|
|
@@ -263,7 +266,7 @@ describe('Error Handling', () => {
|
|
|
263
266
|
let errorCaught = false;
|
|
264
267
|
|
|
265
268
|
try {
|
|
266
|
-
await
|
|
269
|
+
await lambderCaller.api('getUserById', { userId: '1' });
|
|
267
270
|
} catch (error: any) {
|
|
268
271
|
errorCaught = true;
|
|
269
272
|
expect(error.errorMessage).toBe('Database connection failed');
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import Lambder from "../src/Lambder.js";
|
|
2
3
|
|
|
3
4
|
// Example: Secure session handling with all security fixes applied
|
|
@@ -6,10 +7,9 @@ const lambder = new Lambder({
|
|
|
6
7
|
publicPath: "/public",
|
|
7
8
|
apiPath: "/api",
|
|
8
9
|
ejsPath: "/views",
|
|
9
|
-
})
|
|
10
|
-
|
|
10
|
+
})
|
|
11
11
|
// Enable session management with sliding expiration
|
|
12
|
-
|
|
12
|
+
.enableDdbSession(
|
|
13
13
|
{
|
|
14
14
|
tableName: process.env.SESSION_TABLE || "sessions",
|
|
15
15
|
tableRegion: process.env.AWS_REGION || "us-east-1",
|
|
@@ -17,10 +17,12 @@ lambder.enableDdbSession(
|
|
|
17
17
|
enableSlidingExpiration: true, // Sessions extend on each access
|
|
18
18
|
},
|
|
19
19
|
{ partitionKey: "pk", sortKey: "sk" }
|
|
20
|
-
)
|
|
21
|
-
|
|
20
|
+
)
|
|
22
21
|
// Example: Login API with session regeneration
|
|
23
|
-
|
|
22
|
+
.addApi("user.login", {
|
|
23
|
+
input: z.object({ username: z.string(), password: z.string() }),
|
|
24
|
+
output: z.object({ success: z.boolean(), csrfToken: z.string().optional(), error: z.string().optional() })
|
|
25
|
+
}, async (ctx, resolver) => {
|
|
24
26
|
const { username, password } = ctx.apiPayload;
|
|
25
27
|
|
|
26
28
|
// Validate credentials (implement your own logic)
|
|
@@ -41,10 +43,12 @@ lambder.addApi("user.login", async (ctx, resolver) => {
|
|
|
41
43
|
success: true,
|
|
42
44
|
csrfToken: session.csrfToken,
|
|
43
45
|
});
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
+
})
|
|
46
47
|
// Example: Protected API that requires session
|
|
47
|
-
|
|
48
|
+
.addSessionApi("user.profile", {
|
|
49
|
+
input: z.void(),
|
|
50
|
+
output: z.object({ userId: z.string(), username: z.string(), role: z.string() })
|
|
51
|
+
}, async (ctx, resolver) => {
|
|
48
52
|
// Session is automatically fetched and validated
|
|
49
53
|
const sessionData = ctx.session.data;
|
|
50
54
|
|
|
@@ -53,10 +57,12 @@ lambder.addSessionApi("user.profile", async (ctx, resolver) => {
|
|
|
53
57
|
username: sessionData.username,
|
|
54
58
|
role: sessionData.role,
|
|
55
59
|
});
|
|
56
|
-
})
|
|
57
|
-
|
|
60
|
+
})
|
|
58
61
|
// Example: Sensitive operation that regenerates session
|
|
59
|
-
|
|
62
|
+
.addSessionApi("user.changePassword", {
|
|
63
|
+
input: z.object({ oldPassword: z.string(), newPassword: z.string() }),
|
|
64
|
+
output: z.object({ success: z.boolean(), message: z.string().optional(), csrfToken: z.string().optional(), error: z.string().optional() })
|
|
65
|
+
}, async (ctx, resolver) => {
|
|
60
66
|
const { oldPassword, newPassword } = ctx.apiPayload;
|
|
61
67
|
const sessionController = lambder.getSessionController(ctx);
|
|
62
68
|
|
|
@@ -83,10 +89,12 @@ lambder.addSessionApi("user.changePassword", async (ctx, resolver) => {
|
|
|
83
89
|
message: "Password changed successfully",
|
|
84
90
|
csrfToken: newSession.csrfToken, // Send new CSRF token
|
|
85
91
|
});
|
|
86
|
-
})
|
|
87
|
-
|
|
92
|
+
})
|
|
88
93
|
// Example: Update session data
|
|
89
|
-
|
|
94
|
+
.addSessionApi("user.updatePreferences", {
|
|
95
|
+
input: z.object({ theme: z.string(), language: z.string() }),
|
|
96
|
+
output: z.object({ success: z.boolean(), message: z.string() })
|
|
97
|
+
}, async (ctx, resolver) => {
|
|
90
98
|
const { theme, language } = ctx.apiPayload;
|
|
91
99
|
const sessionController = lambder.getSessionController(ctx);
|
|
92
100
|
|
|
@@ -100,10 +108,12 @@ lambder.addSessionApi("user.updatePreferences", async (ctx, resolver) => {
|
|
|
100
108
|
success: true,
|
|
101
109
|
message: "Preferences updated",
|
|
102
110
|
});
|
|
103
|
-
})
|
|
104
|
-
|
|
111
|
+
})
|
|
105
112
|
// Example: Logout
|
|
106
|
-
|
|
113
|
+
.addSessionApi("user.logout", {
|
|
114
|
+
input: z.void(),
|
|
115
|
+
output: z.object({ success: z.boolean(), message: z.string() })
|
|
116
|
+
}, async (ctx, resolver) => {
|
|
107
117
|
const sessionController = lambder.getSessionController(ctx);
|
|
108
118
|
|
|
109
119
|
// End current session
|
|
@@ -113,10 +123,12 @@ lambder.addSessionApi("user.logout", async (ctx, resolver) => {
|
|
|
113
123
|
success: true,
|
|
114
124
|
message: "Logged out successfully",
|
|
115
125
|
});
|
|
116
|
-
})
|
|
117
|
-
|
|
126
|
+
})
|
|
118
127
|
// Example: Logout from all devices
|
|
119
|
-
|
|
128
|
+
.addSessionApi("user.logoutAll", {
|
|
129
|
+
input: z.void(),
|
|
130
|
+
output: z.object({ success: z.boolean(), message: z.string() })
|
|
131
|
+
}, async (ctx, resolver) => {
|
|
120
132
|
const sessionController = lambder.getSessionController(ctx);
|
|
121
133
|
|
|
122
134
|
// End all sessions for this user (same sessionKey)
|
|
@@ -126,10 +138,16 @@ lambder.addSessionApi("user.logoutAll", async (ctx, resolver) => {
|
|
|
126
138
|
success: true,
|
|
127
139
|
message: "Logged out from all devices",
|
|
128
140
|
});
|
|
129
|
-
})
|
|
130
|
-
|
|
141
|
+
})
|
|
131
142
|
// Example: Optional session (check if logged in)
|
|
132
|
-
|
|
143
|
+
.addApi("user.checkAuth", {
|
|
144
|
+
input: z.object({}),
|
|
145
|
+
output: z.object({
|
|
146
|
+
authenticated: z.boolean(),
|
|
147
|
+
userId: z.string().optional(),
|
|
148
|
+
username: z.string().optional(),
|
|
149
|
+
})
|
|
150
|
+
}, async (ctx, resolver) => {
|
|
133
151
|
const sessionController = lambder.getSessionController(ctx);
|
|
134
152
|
|
|
135
153
|
// Try to fetch session without throwing error
|
|
@@ -141,15 +159,14 @@ lambder.addApi("user.checkAuth", async (ctx, resolver) => {
|
|
|
141
159
|
userId: session.data.userId,
|
|
142
160
|
username: session.data.username,
|
|
143
161
|
});
|
|
162
|
+
} else {
|
|
163
|
+
return resolver.api({
|
|
164
|
+
authenticated: false,
|
|
165
|
+
});
|
|
144
166
|
}
|
|
145
|
-
|
|
146
|
-
return resolver.api({
|
|
147
|
-
authenticated: false,
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
167
|
+
})
|
|
151
168
|
// Example: Route with session
|
|
152
|
-
|
|
169
|
+
.addSessionRoute("/dashboard", async (ctx, resolver) => {
|
|
153
170
|
// Session is automatically fetched and validated
|
|
154
171
|
const userData = ctx.session.data;
|
|
155
172
|
|
|
@@ -157,10 +174,9 @@ lambder.addSessionRoute("/dashboard", async (ctx, resolver) => {
|
|
|
157
174
|
user: userData,
|
|
158
175
|
csrfToken: ctx.session.csrfToken,
|
|
159
176
|
});
|
|
160
|
-
})
|
|
161
|
-
|
|
177
|
+
})
|
|
162
178
|
// Example: Route with optional session
|
|
163
|
-
|
|
179
|
+
.addRoute("/", async (ctx, resolver) => {
|
|
164
180
|
const sessionController = lambder.getSessionController(ctx);
|
|
165
181
|
const session = await sessionController.fetchSessionIfExists();
|
|
166
182
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import Lambder from "../src/index.js";
|
|
3
|
+
|
|
4
|
+
// 1. Define reusable schemas
|
|
5
|
+
const UserSchema = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
email: z.string().email(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const CreateUserSchema = z.object({
|
|
12
|
+
name: z.string(),
|
|
13
|
+
email: z.string().email(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// 2. Initialize Lambder and chain APIs
|
|
17
|
+
const lambder = new Lambder({
|
|
18
|
+
publicPath: "./public",
|
|
19
|
+
apiPath: "/api"
|
|
20
|
+
})
|
|
21
|
+
.addApi("getUser", {
|
|
22
|
+
input: z.object({ userId: z.string() }),
|
|
23
|
+
output: UserSchema
|
|
24
|
+
}, async (ctx, resolver) => {
|
|
25
|
+
// ctx.apiPayload is typed as { userId: string }
|
|
26
|
+
const { userId } = ctx.apiPayload;
|
|
27
|
+
|
|
28
|
+
return resolver.api({
|
|
29
|
+
id: userId,
|
|
30
|
+
name: "John Doe",
|
|
31
|
+
email: "john@example.com"
|
|
32
|
+
});
|
|
33
|
+
})
|
|
34
|
+
.addApi("createUser", {
|
|
35
|
+
input: CreateUserSchema,
|
|
36
|
+
output: UserSchema
|
|
37
|
+
}, async (ctx, resolver) => {
|
|
38
|
+
// ctx.apiPayload is typed as { name: string, email: string }
|
|
39
|
+
const { name, email } = ctx.apiPayload;
|
|
40
|
+
|
|
41
|
+
return resolver.api({
|
|
42
|
+
id: "123",
|
|
43
|
+
name,
|
|
44
|
+
email
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// 3. Export the inferred contract type for Frontend
|
|
49
|
+
export type AppContract = typeof lambder.ApiContractType;
|
|
50
|
+
|
|
51
|
+
// 4. Modular example using .use()
|
|
52
|
+
const authApi = <T>(l: Lambder<T>) => {
|
|
53
|
+
return l.addApi("login", {
|
|
54
|
+
input: z.object({ username: z.string(), password: z.string() }),
|
|
55
|
+
output: z.object({ token: z.string() })
|
|
56
|
+
}, async (ctx, resolver) => {
|
|
57
|
+
return resolver.api({ token: "abc-123" });
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const lambderWithAuth = lambder.use(authApi);
|
|
62
|
+
|
|
63
|
+
export type AuthContract = typeof lambderWithAuth.ApiContractType;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lambder",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"js-cookie": "^3.0.5",
|
|
36
36
|
"mime-types": "^2.1.35",
|
|
37
37
|
"path-to-regexp": "^6.2.1",
|
|
38
|
-
"querystring": "^0.2.1"
|
|
38
|
+
"querystring": "^0.2.1",
|
|
39
|
+
"zod": "^4.1.12"
|
|
39
40
|
},
|
|
40
41
|
"peerDependencies": {
|
|
41
42
|
"msw": "^2.0.0"
|