Skip to content

Commit 271fce2

Browse files
committed
chore: backup
Signed-off-by: tunnckoCore <5038030+tunnckoCore@users.noreply.github.com>
1 parent c0cf91c commit 271fce2

20 files changed

+3248
-1
lines changed

IMPLEMENTATION_SUMMARY.md

Lines changed: 432 additions & 0 deletions
Large diffs are not rendered by default.

QUICK_START.md

Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
# Zagora v2 - Quick Start Guide
2+
3+
## Three Procedure Types
4+
5+
### 1. Basic (No Context) - Same as Original
6+
7+
```ts
8+
import { zagora } from './src-v2/index';
9+
import z from 'zod';
10+
11+
const basic = zagora();
12+
13+
const uppercase = basic
14+
.input(z.string())
15+
.output(z.string())
16+
.handler((input) => input.toUpperCase());
17+
18+
// Call it
19+
const result = await uppercase('hello');
20+
console.log(result.data); // "HELLO"
21+
```
22+
23+
**Handler receives**: Single argument based on input schema
24+
25+
### 2. Public (Context, No Middleware)
26+
27+
```ts
28+
interface PublicContext {
29+
requestId?: string;
30+
}
31+
32+
const pub = zagora().$context<PublicContext>();
33+
34+
const getInfo = pub
35+
.input(z.object({ id: z.string() }))
36+
.output(z.object({ message: z.string() }))
37+
.handler(({ input, context }) => {
38+
return {
39+
message: `Processing ${input.id} with request ${context.requestId}`
40+
};
41+
});
42+
43+
// Call it
44+
const result = await getInfo({
45+
input: { id: '123' },
46+
context: { requestId: 'req-456' }
47+
});
48+
console.log(result.data); // { message: "Processing 123 with request req-456" }
49+
```
50+
51+
**Handler receives**: Object with `{ input, context, errors? }`
52+
53+
### 3. Authenticated (Context + Middleware)
54+
55+
```ts
56+
interface User {
57+
id: string;
58+
email: string;
59+
}
60+
61+
interface AuthContext {
62+
user?: User;
63+
token?: string;
64+
}
65+
66+
// Define middleware
67+
const authMiddleware = async ({ context, next }) => {
68+
// Validate token and get user
69+
const user = await db.users.findByToken(context.token);
70+
71+
// Pass enriched context to next
72+
return next({
73+
context: { ...context, user }
74+
});
75+
};
76+
77+
// Create authenticated procedures
78+
const authed = zagora()
79+
.$context<AuthContext>()
80+
.use(authMiddleware);
81+
82+
const getProfile = authed
83+
.input(z.undefined().optional())
84+
.output(z.object({ id: z.string(), email: z.string() }))
85+
.handler(({ context }) => {
86+
// context.user is guaranteed to exist (validated by middleware)
87+
return context.user!;
88+
});
89+
90+
// Call it
91+
const result = await getProfile({
92+
context: { token: 'abc123' }
93+
});
94+
console.log(result.data); // { id: '...', email: '...' }
95+
```
96+
97+
**Handler receives**: Object with `{ input, context, errors? }`
98+
99+
## Middleware Pattern
100+
101+
Every middleware must:
102+
1. Receive `{ context, next }`
103+
2. Optionally modify context
104+
3. Call `next({ context: modifiedContext })`
105+
106+
```ts
107+
const loggingMiddleware = async ({ context, next }) => {
108+
console.log('Start:', context.requestId);
109+
const result = await next({ context });
110+
console.log('End:', context.requestId);
111+
return result;
112+
};
113+
114+
const rateLimitMiddleware = async ({ context, next }) => {
115+
const count = await redis.incr(`rate:${context.userId}`);
116+
if (count > LIMIT) {
117+
throw new Error('Rate limited');
118+
}
119+
return next({ context });
120+
};
121+
122+
// Stack middleware
123+
const api = zagora()
124+
.$context<AppContext>()
125+
.use(loggingMiddleware)
126+
.use(rateLimitMiddleware)
127+
.use(authMiddleware);
128+
```
129+
130+
**Execution order**: Middleware runs in the order defined with `.use()`
131+
132+
## With Error Handling
133+
134+
```ts
135+
const deleteUser = authed
136+
.input(z.object({ userId: z.string() }))
137+
.errors({
138+
NOT_AUTHORIZED: z.object({
139+
type: z.literal('NOT_AUTHORIZED'),
140+
message: z.string(),
141+
}),
142+
NOT_FOUND: z.object({
143+
type: z.literal('NOT_FOUND'),
144+
userId: z.string(),
145+
}),
146+
})
147+
.handler(({ input, context, errors }) => {
148+
if (!context.user) {
149+
return errors.NOT_AUTHORIZED({ message: 'Login required' });
150+
}
151+
152+
if (!userExists(input.userId)) {
153+
return errors.NOT_FOUND({ userId: input.userId });
154+
}
155+
156+
db.users.delete(input.userId);
157+
return { success: true };
158+
});
159+
160+
// Call it
161+
const result = await deleteUser({
162+
input: { userId: '123' },
163+
context: { token: 'abc123' }
164+
});
165+
166+
// Check result
167+
if (result.isDefined) {
168+
// Typed error
169+
console.log(result.error.type);
170+
} else if (result.error) {
171+
// Untyped error
172+
console.log(result.error.message);
173+
} else {
174+
// Success
175+
console.log(result.data);
176+
}
177+
```
178+
179+
## Type Safety
180+
181+
Everything is fully type-safe:
182+
183+
```ts
184+
// Context type flows through middleware
185+
const api = zagora()
186+
.$context<{ user: User; requestId: string }>();
187+
188+
// Input/output are inferred from schemas
189+
const proc = api
190+
.input(z.object({ name: z.string() }))
191+
.output(z.object({ id: z.number() }))
192+
.handler(({ input, context, errors }) => {
193+
// input.name: string
194+
// context.user: User
195+
// context.requestId: string
196+
return { id: 1 };
197+
});
198+
199+
// Handler call is type-checked
200+
proc({
201+
input: { name: 'John' }, // Must have input
202+
context: { user: {...}, requestId: 'req-1' } // Must have context
203+
});
204+
```
205+
206+
## Why Spreading?
207+
208+
Each method creates a **new instance** with a **new definition object**:
209+
210+
```ts
211+
const za = zagora();
212+
const proc1 = za.input(schemaA).output(outputA);
213+
const proc2 = za.input(schemaB).output(outputB);
214+
215+
// proc1 and proc2 have DIFFERENT definition objects
216+
// Updating proc2's definition doesn't affect proc1
217+
// This is why they don't "mess the instance"
218+
```
219+
220+
Without spreading, they'd share the same definition object and interfere with each other.
221+
222+
## Common Patterns
223+
224+
### Pattern: Role-Based Access
225+
226+
```ts
227+
const requireRole = (requiredRole: string) => async ({ context, next }) => {
228+
if (!context.user?.roles.includes(requiredRole)) {
229+
throw new Error(`Requires ${requiredRole} role`);
230+
}
231+
return next({ context });
232+
};
233+
234+
const admin = zagora()
235+
.$context<AuthContext>()
236+
.use(authMiddleware)
237+
.use(requireRole('admin'));
238+
239+
const deleteAnything = admin
240+
.input(z.object({ id: z.string() }))
241+
.handler(({ input, context }) => {
242+
// Only admins can reach here
243+
return { deleted: true };
244+
});
245+
```
246+
247+
### Pattern: Request Tracing
248+
249+
```ts
250+
const tracingMiddleware = async ({ context, next }) => {
251+
const traceId = context.traceId || crypto.randomUUID();
252+
context.traceId = traceId;
253+
254+
console.log(`[${traceId}] Request started`);
255+
const result = await next({ context });
256+
console.log(`[${traceId}] Request ended`);
257+
258+
return result;
259+
};
260+
261+
const api = zagora()
262+
.$context<{ traceId?: string }>()
263+
.use(tracingMiddleware);
264+
```
265+
266+
### Pattern: Database Transaction
267+
268+
```ts
269+
const transactionMiddleware = async ({ context, next }) => {
270+
const tx = await db.transaction();
271+
272+
try {
273+
const result = await next({ context: { ...context, db: tx } });
274+
await tx.commit();
275+
return result;
276+
} catch (error) {
277+
await tx.rollback();
278+
throw error;
279+
}
280+
};
281+
282+
const withTx = zagora()
283+
.$context<{ db?: Transaction }>()
284+
.use(transactionMiddleware);
285+
```
286+
287+
## Migration from v1
288+
289+
v1 code works unchanged:
290+
291+
```ts
292+
// Old code - still works
293+
import { zagora } from './src/index'; // Use original
294+
295+
const proc = zagora()
296+
.input(z.string())
297+
.handler(input => input.toUpperCase());
298+
```
299+
300+
New features are opt-in:
301+
302+
```ts
303+
// New code - use v2
304+
import { zagora } from './src-v2/index';
305+
306+
const proc = zagora()
307+
.$context<Context>()
308+
.use(middleware);
309+
```
310+
311+
Both can coexist!
312+
313+
## Key Differences: Handler Arguments
314+
315+
| Scenario | Handler Argument |
316+
|----------|------------------|
317+
| No context | `(input)` |
318+
| With context | `({ input, context })` |
319+
| With context + errors | `({ input, context, errors })` |
320+
| No context + errors | `(input, errors)` |
321+
322+
## Result Structure
323+
324+
All handlers return a result tuple with properties:
325+
326+
```ts
327+
const [data, error, isDefined] = await proc(...);
328+
329+
// Or access as properties
330+
result.data; // null or the actual data
331+
result.error; // null or the error object
332+
result.isDefined; // true if error is a typed error, false for success
333+
334+
// Check for typed error
335+
if (result.isDefined && result.error?.type === 'NOT_AUTHORIZED') {
336+
// Handle specific error
337+
}
338+
339+
// Check for untyped error
340+
if (result.error && !result.isDefined) {
341+
// Handle unexpected error
342+
}
343+
344+
// Check for success
345+
if (!result.error) {
346+
console.log(result.data);
347+
}
348+
```
349+
350+
## Files Overview
351+
352+
- `src-v2/index.ts` - Main Zagora class with `$context()` and `use()`
353+
- `src-v2/types.ts` - Type definitions
354+
- `src-v2/utils.ts` - Utilities including `executeMiddlewares()`
355+
- `src-v2/error.ts` - Error handling
356+
- `SRC_V2_README.md` - Comprehensive documentation
357+
- `SPREADING_EXPLAINED.md` - Deep dive into why spreading works
358+
- `example-v2.ts` - Full examples

0 commit comments

Comments
 (0)