@spfn/core 0.2.0-beta.3 → 0.2.0-beta.30

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.
Files changed (65) hide show
  1. package/README.md +260 -1175
  2. package/dist/{boss-BO8ty33K.d.ts → boss-DI1r4kTS.d.ts} +24 -7
  3. package/dist/codegen/index.d.ts +55 -8
  4. package/dist/codegen/index.js +179 -5
  5. package/dist/codegen/index.js.map +1 -1
  6. package/dist/config/index.d.ts +204 -6
  7. package/dist/config/index.js +44 -11
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/db/index.d.ts +24 -3
  10. package/dist/db/index.js +118 -45
  11. package/dist/db/index.js.map +1 -1
  12. package/dist/env/index.d.ts +83 -3
  13. package/dist/env/index.js +83 -15
  14. package/dist/env/index.js.map +1 -1
  15. package/dist/env/loader.d.ts +95 -0
  16. package/dist/env/loader.js +78 -0
  17. package/dist/env/loader.js.map +1 -0
  18. package/dist/event/index.d.ts +29 -70
  19. package/dist/event/index.js +15 -1
  20. package/dist/event/index.js.map +1 -1
  21. package/dist/event/sse/client.d.ts +157 -0
  22. package/dist/event/sse/client.js +169 -0
  23. package/dist/event/sse/client.js.map +1 -0
  24. package/dist/event/sse/index.d.ts +46 -0
  25. package/dist/event/sse/index.js +238 -0
  26. package/dist/event/sse/index.js.map +1 -0
  27. package/dist/job/index.d.ts +23 -8
  28. package/dist/job/index.js +108 -23
  29. package/dist/job/index.js.map +1 -1
  30. package/dist/logger/index.js +9 -0
  31. package/dist/logger/index.js.map +1 -1
  32. package/dist/middleware/index.d.ts +23 -1
  33. package/dist/middleware/index.js +58 -5
  34. package/dist/middleware/index.js.map +1 -1
  35. package/dist/nextjs/index.d.ts +2 -2
  36. package/dist/nextjs/index.js +37 -5
  37. package/dist/nextjs/index.js.map +1 -1
  38. package/dist/nextjs/server.d.ts +44 -23
  39. package/dist/nextjs/server.js +87 -66
  40. package/dist/nextjs/server.js.map +1 -1
  41. package/dist/route/index.d.ts +168 -5
  42. package/dist/route/index.js +262 -17
  43. package/dist/route/index.js.map +1 -1
  44. package/dist/router-Di7ENoah.d.ts +151 -0
  45. package/dist/server/index.d.ts +316 -5
  46. package/dist/server/index.js +892 -200
  47. package/dist/server/index.js.map +1 -1
  48. package/dist/{types-BVxUIkcU.d.ts → types-7Mhoxnnt.d.ts} +68 -2
  49. package/dist/types-DAVwA-_7.d.ts +339 -0
  50. package/docs/cache.md +133 -0
  51. package/docs/codegen.md +74 -0
  52. package/docs/database.md +346 -0
  53. package/docs/entity.md +539 -0
  54. package/docs/env.md +499 -0
  55. package/docs/errors.md +319 -0
  56. package/docs/event.md +443 -0
  57. package/docs/file-upload.md +717 -0
  58. package/docs/job.md +131 -0
  59. package/docs/logger.md +108 -0
  60. package/docs/middleware.md +337 -0
  61. package/docs/nextjs.md +247 -0
  62. package/docs/repository.md +496 -0
  63. package/docs/route.md +497 -0
  64. package/docs/server.md +429 -0
  65. package/package.json +18 -2
@@ -2,6 +2,20 @@ import { TSchema, Static } from '@sinclair/typebox';
2
2
  import { ErrorRegistry, ErrorRegistryInput } from '@spfn/core/errors';
3
3
  import { RouteDef, RouteInput } from '@spfn/core/route';
4
4
 
5
+ /**
6
+ * Convert File types in schema to actual File for client usage
7
+ *
8
+ * TypeBox File schemas become actual File objects on the client side.
9
+ */
10
+ type ConvertFileTypes<T> = T extends File ? File : T extends File[] ? File[] : T;
11
+ /**
12
+ * Extract form data input type with File support
13
+ *
14
+ * Maps schema types to runtime types, converting FileSchema to File.
15
+ */
16
+ type FormDataInput<T> = {
17
+ [K in keyof T]: ConvertFileTypes<T[K]>;
18
+ };
5
19
  /**
6
20
  * Extract structured input from RouteInput
7
21
  *
@@ -11,6 +25,7 @@ type StructuredInput<TInput extends RouteInput> = {
11
25
  params: TInput['params'] extends TSchema ? Static<TInput['params']> : {};
12
26
  query: TInput['query'] extends TSchema ? Static<TInput['query']> : {};
13
27
  body: TInput['body'] extends TSchema ? Static<TInput['body']> : {};
28
+ formData: TInput['formData'] extends TSchema ? FormDataInput<Static<TInput['formData']>> : {};
14
29
  headers: TInput['headers'] extends TSchema ? Static<TInput['headers']> : {};
15
30
  cookies: TInput['cookies'] extends TSchema ? Static<TInput['cookies']> : {};
16
31
  };
@@ -47,6 +62,52 @@ type InferRouteInput<TRoute> = TRoute extends RouteDef<infer TInput, any, any> ?
47
62
  * ```
48
63
  */
49
64
  type InferRouteOutput<TRoute> = TRoute extends RouteDef<any, any, infer TResponse> ? TResponse : never;
65
+ /**
66
+ * Extract routes from Router type
67
+ * Router<TRoutes> has routes in `_routes` property
68
+ */
69
+ type ExtractRoutes<TRouter> = TRouter extends {
70
+ _routes: infer TRoutes;
71
+ } ? TRoutes : TRouter;
72
+ /**
73
+ * Extract output type for a specific route from router
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * import type { RouterOutput } from '@spfn/core/nextjs';
78
+ * import type { AppRouter } from '@/server/router';
79
+ *
80
+ * // Get output type for a specific route
81
+ * type ListData = RouterOutput<AppRouter, 'listExamples'>;
82
+ *
83
+ * // Use in props
84
+ * interface Props {
85
+ * data: RouterOutput<AppRouter, 'listExamples'>;
86
+ * }
87
+ *
88
+ * // Extract item type from paginated response
89
+ * type Example = RouterOutput<AppRouter, 'listExamples'>['items'][number];
90
+ * ```
91
+ */
92
+ type RouterOutput<TRouter, K extends keyof ExtractRoutes<TRouter>> = InferRouteOutput<ExtractRoutes<TRouter>[K]>;
93
+ /**
94
+ * Extract input type for a specific route from router
95
+ *
96
+ * @example
97
+ * ```typescript
98
+ * import type { RouterInput } from '@spfn/core/nextjs';
99
+ * import type { AppRouter } from '@/server/router';
100
+ *
101
+ * // Get input type for a specific route
102
+ * type CreateInput = RouterInput<AppRouter, 'createExample'>;
103
+ *
104
+ * // Use in function parameter
105
+ * function submitForm(data: RouterInput<AppRouter, 'createExample'>['body']) {
106
+ * // ...
107
+ * }
108
+ * ```
109
+ */
110
+ type RouterInput<TRouter, K extends keyof ExtractRoutes<TRouter>> = InferRouteInput<ExtractRoutes<TRouter>[K]>;
50
111
  /**
51
112
  * Cookie options for setCookie
52
113
  */
@@ -98,7 +159,7 @@ interface ApiConfig {
98
159
  /**
99
160
  * Request timeout in milliseconds
100
161
  *
101
- * @default 30000
162
+ * @default env.SERVER_TIMEOUT (120000)
102
163
  */
103
164
  timeout?: number;
104
165
  /**
@@ -141,6 +202,11 @@ interface ApiConfig {
141
202
  * Per-call options
142
203
  */
143
204
  interface CallOptions {
205
+ /**
206
+ * Request timeout in milliseconds
207
+ * Overrides the global timeout set in ApiConfig
208
+ */
209
+ timeout?: number;
144
210
  /**
145
211
  * Additional headers for this request
146
212
  */
@@ -181,4 +247,4 @@ interface CallOptions {
181
247
  };
182
248
  }
183
249
 
184
- export type { ApiConfig as A, CallOptions as C, InferRouteInput as I, RequestInterceptor as R, StructuredInput as S, ResponseInterceptor as a, InferRouteOutput as b, CookieOptions as c, SetCookie as d };
250
+ export type { ApiConfig as A, CallOptions as C, InferRouteInput as I, RequestInterceptor as R, StructuredInput as S, ResponseInterceptor as a, InferRouteOutput as b, RouterInput as c, RouterOutput as d, CookieOptions as e, SetCookie as f };
@@ -0,0 +1,339 @@
1
+ import { Context } from 'hono';
2
+ import { E as EventRouterDef, I as InferEventNames, b as InferEventPayload } from './router-Di7ENoah.js';
3
+
4
+ /**
5
+ * SSE Token Manager
6
+ *
7
+ * Auth-agnostic token issuance and verification for SSE connections.
8
+ * Issues one-time-use tokens with TTL for Token Exchange pattern.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * const manager = new SSETokenManager({ ttl: 30000 });
13
+ *
14
+ * // Issue token for authenticated user
15
+ * const token = await manager.issue('user-123');
16
+ *
17
+ * // Verify and consume token (one-time use)
18
+ * const subject = await manager.verify(token); // 'user-123'
19
+ * const again = await manager.verify(token); // null (already consumed)
20
+ *
21
+ * // Cleanup on shutdown
22
+ * manager.destroy();
23
+ * ```
24
+ */
25
+ /**
26
+ * Minimal cache client interface (compatible with ioredis Redis | Cluster)
27
+ */
28
+ type CacheClient = {
29
+ set(key: string, value: string, ...args: any[]): Promise<any>;
30
+ getdel?(key: string): Promise<string | null>;
31
+ get(key: string): Promise<string | null>;
32
+ del(key: string | string[]): Promise<number>;
33
+ };
34
+ /**
35
+ * Stored SSE token data
36
+ */
37
+ interface SSEToken {
38
+ token: string;
39
+ subject: string;
40
+ expiresAt: number;
41
+ }
42
+ /**
43
+ * Token storage interface
44
+ *
45
+ * Implement this for custom storage backends (e.g., Redis for multi-instance).
46
+ */
47
+ interface SSETokenStore {
48
+ /** Store a token */
49
+ set(token: string, data: SSEToken): Promise<void>;
50
+ /** Get and delete a token (one-time use) */
51
+ consume(token: string): Promise<SSEToken | null>;
52
+ /** Remove expired tokens */
53
+ cleanup(): Promise<void>;
54
+ }
55
+ /**
56
+ * SSETokenManager configuration
57
+ */
58
+ interface SSETokenManagerConfig {
59
+ /**
60
+ * Token time-to-live in milliseconds
61
+ * @default 30000
62
+ */
63
+ ttl?: number;
64
+ /**
65
+ * Custom token store (default: in-memory Map)
66
+ */
67
+ store?: SSETokenStore;
68
+ /**
69
+ * Cleanup interval in milliseconds
70
+ * @default 60000
71
+ */
72
+ cleanupInterval?: number;
73
+ }
74
+ /**
75
+ * Redis/Valkey-backed token store for multi-instance deployments.
76
+ *
77
+ * Uses SET EX for automatic TTL expiry and GETDEL for atomic one-time consumption.
78
+ * No cleanup needed — Redis handles expiration automatically.
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * import { getCache } from '@spfn/core/cache';
83
+ *
84
+ * const cache = getCache();
85
+ * if (cache) {
86
+ * const store = new CacheTokenStore(cache);
87
+ * const manager = new SSETokenManager({ store });
88
+ * }
89
+ * ```
90
+ */
91
+ declare class CacheTokenStore implements SSETokenStore {
92
+ private cache;
93
+ private prefix;
94
+ constructor(cache: CacheClient);
95
+ set(token: string, data: SSEToken): Promise<void>;
96
+ consume(token: string): Promise<SSEToken | null>;
97
+ cleanup(): Promise<void>;
98
+ }
99
+ declare class SSETokenManager {
100
+ private store;
101
+ private ttl;
102
+ private cleanupTimer;
103
+ constructor(config?: SSETokenManagerConfig);
104
+ /**
105
+ * Issue a new one-time-use token for the given subject
106
+ */
107
+ issue(subject: string): Promise<string>;
108
+ /**
109
+ * Verify and consume a token
110
+ * @returns subject string if valid, null if invalid/expired/already consumed
111
+ */
112
+ verify(token: string): Promise<string | null>;
113
+ /**
114
+ * Cleanup timer and resources
115
+ */
116
+ destroy(): void;
117
+ }
118
+
119
+ /**
120
+ * SSE Types
121
+ *
122
+ * Type definitions for Server-Sent Events
123
+ */
124
+
125
+ /**
126
+ * SSE message sent from server
127
+ */
128
+ interface SSEMessage<TEvent extends string = string, TPayload = unknown> {
129
+ /** Event name */
130
+ event: TEvent;
131
+ /** Event payload */
132
+ data: TPayload;
133
+ /** Optional message ID for reconnection */
134
+ id?: string;
135
+ }
136
+ /**
137
+ * SSE auth configuration (internal, non-generic)
138
+ *
139
+ * Stored in SSEHandlerConfig. Generic user-facing version is SSEAuthConfig.
140
+ */
141
+ interface SSEHandlerAuthConfig {
142
+ /**
143
+ * Enable SSE token authentication
144
+ * @default false
145
+ */
146
+ enabled?: boolean;
147
+ /**
148
+ * Token TTL in milliseconds
149
+ * @default 30000
150
+ */
151
+ tokenTtl?: number;
152
+ /**
153
+ * Custom token store (e.g., Redis for multi-instance)
154
+ */
155
+ store?: SSETokenStore;
156
+ /**
157
+ * Extract subject (user ID) from Hono context
158
+ * @default (c) => c.get('auth')?.userId ?? null
159
+ */
160
+ getSubject?: (c: Context) => string | null;
161
+ /**
162
+ * Subscription authorization hook (called once on connect)
163
+ *
164
+ * Return allowed events subset. Empty array = 403 rejection.
165
+ */
166
+ authorize?: (subject: string, events: string[]) => Promise<string[]> | string[];
167
+ /**
168
+ * Per-event payload filter map (called on every event emission)
169
+ *
170
+ * Return false to skip sending the event to this user.
171
+ */
172
+ filter?: Record<string, (subject: string, payload: unknown) => boolean>;
173
+ }
174
+ /**
175
+ * SSE auth configuration (user-facing, generic)
176
+ *
177
+ * Provides type-safe event names and payload inference from EventRouter.
178
+ *
179
+ * @example
180
+ * ```typescript
181
+ * .events(eventRouter, {
182
+ * auth: {
183
+ * enabled: true,
184
+ * authorize: async (subject, events) => {
185
+ * // events: ('userCreated' | 'orderUpdated')[]
186
+ * return events.filter(e => hasPermission(subject, e));
187
+ * },
188
+ * filter: {
189
+ * orderUpdated: (subject, payload) => {
190
+ * // payload: { orderId: string; userId: string }
191
+ * return payload.userId === subject;
192
+ * },
193
+ * },
194
+ * },
195
+ * })
196
+ * ```
197
+ */
198
+ interface SSEAuthConfig<TRouter extends EventRouterDef<any>> {
199
+ enabled?: boolean;
200
+ tokenTtl?: number;
201
+ store?: SSETokenStore;
202
+ getSubject?: (c: Context) => string | null;
203
+ authorize?: (subject: string, events: InferEventNames<TRouter>[]) => Promise<InferEventNames<TRouter>[]> | InferEventNames<TRouter>[];
204
+ filter?: {
205
+ [K in InferEventNames<TRouter>]?: (subject: string, payload: InferEventPayload<TRouter, K>) => boolean;
206
+ };
207
+ }
208
+ /**
209
+ * SSE Handler configuration
210
+ */
211
+ interface SSEHandlerConfig {
212
+ /**
213
+ * Keep-alive ping interval in milliseconds
214
+ * @default 30000
215
+ */
216
+ pingInterval?: number;
217
+ /**
218
+ * Custom headers for SSE response
219
+ */
220
+ headers?: Record<string, string>;
221
+ /**
222
+ * Authentication and authorization configuration
223
+ */
224
+ auth?: SSEHandlerAuthConfig;
225
+ }
226
+ /**
227
+ * SSE Client configuration
228
+ */
229
+ interface SSEClientConfig {
230
+ /**
231
+ * Backend API host URL
232
+ * @default NEXT_PUBLIC_SPFN_API_URL || 'http://localhost:8790'
233
+ * @example 'http://localhost:8790'
234
+ * @example 'https://api.example.com'
235
+ */
236
+ host?: string;
237
+ /**
238
+ * SSE endpoint pathname
239
+ * @default '/events/stream'
240
+ */
241
+ pathname?: string;
242
+ /**
243
+ * Full URL (overrides host + pathname)
244
+ * @deprecated Use host and pathname instead
245
+ * @example 'http://localhost:8790/events/stream'
246
+ */
247
+ url?: string;
248
+ /**
249
+ * Auto reconnect on disconnect
250
+ * @default true
251
+ */
252
+ reconnect?: boolean;
253
+ /**
254
+ * Reconnect delay in milliseconds
255
+ * @default 3000
256
+ */
257
+ reconnectDelay?: number;
258
+ /**
259
+ * Maximum reconnect attempts (0 = infinite)
260
+ * @default 0
261
+ */
262
+ maxReconnectAttempts?: number;
263
+ /**
264
+ * Include credentials (cookies) in request
265
+ * @default false
266
+ */
267
+ withCredentials?: boolean;
268
+ /**
269
+ * Acquire a one-time SSE token before connecting.
270
+ *
271
+ * Called on every (re)connect. The returned token is appended
272
+ * to the SSE URL as `?token=...`.
273
+ *
274
+ * For automatic token acquisition via RPC proxy, use `createAuthSSEClient` instead.
275
+ *
276
+ * @example
277
+ * ```typescript
278
+ * // Recommended: use createAuthSSEClient for automatic token handling
279
+ * import { createAuthSSEClient } from '@spfn/core/event/sse/client';
280
+ * const client = createAuthSSEClient<EventRouter>();
281
+ *
282
+ * // Manual: provide acquireToken directly
283
+ * acquireToken: async () => {
284
+ * const res = await fetch('/api/rpc/eventsToken', {
285
+ * method: 'POST',
286
+ * credentials: 'include',
287
+ * });
288
+ * const data = await res.json();
289
+ * return data.token;
290
+ * }
291
+ * ```
292
+ */
293
+ acquireToken?: () => Promise<string>;
294
+ }
295
+ /**
296
+ * Event handler function
297
+ */
298
+ type SSEEventHandler<TPayload> = (payload: TPayload) => void;
299
+ /**
300
+ * Event handlers map for EventRouter
301
+ */
302
+ type SSEEventHandlers<TRouter extends EventRouterDef<any>> = {
303
+ [K in InferEventNames<TRouter>]?: SSEEventHandler<InferEventPayload<TRouter, K>>;
304
+ };
305
+ /**
306
+ * Subscription options
307
+ */
308
+ interface SSESubscribeOptions<TRouter extends EventRouterDef<any>> {
309
+ /**
310
+ * Events to subscribe
311
+ */
312
+ events: InferEventNames<TRouter>[];
313
+ /**
314
+ * Event handlers
315
+ */
316
+ handlers: SSEEventHandlers<TRouter>;
317
+ /**
318
+ * Called when connection opens
319
+ */
320
+ onOpen?: () => void;
321
+ /**
322
+ * Called on connection error
323
+ */
324
+ onError?: (error: Event) => void;
325
+ /**
326
+ * Called when reconnecting
327
+ */
328
+ onReconnect?: (attempt: number) => void;
329
+ }
330
+ /**
331
+ * SSE connection state
332
+ */
333
+ type SSEConnectionState = 'connecting' | 'open' | 'closed' | 'error';
334
+ /**
335
+ * Unsubscribe function
336
+ */
337
+ type SSEUnsubscribe = () => void;
338
+
339
+ export { CacheTokenStore as C, type SSEHandlerConfig as S, type SSEAuthConfig as a, SSETokenManager as b, type SSEToken as c, type SSETokenStore as d, type SSETokenManagerConfig as e, type SSEMessage as f, type SSEHandlerAuthConfig as g, type SSEClientConfig as h, type SSEEventHandler as i, type SSEEventHandlers as j, type SSESubscribeOptions as k, type SSEConnectionState as l, type SSEUnsubscribe as m };
package/docs/cache.md ADDED
@@ -0,0 +1,133 @@
1
+ # Cache
2
+
3
+ Redis caching with type-safe operations.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ REDIS_URL=redis://localhost:6379
9
+ ```
10
+
11
+ ## Basic Usage
12
+
13
+ ```typescript
14
+ import { cache } from '@spfn/core/cache';
15
+
16
+ // Set
17
+ await cache.set('user:123', { id: '123', name: 'John' });
18
+ await cache.set('session:abc', data, { ttl: 3600 }); // 1 hour
19
+
20
+ // Get
21
+ const user = await cache.get<User>('user:123');
22
+
23
+ // Delete
24
+ await cache.del('user:123');
25
+
26
+ // Check existence
27
+ const exists = await cache.exists('user:123');
28
+ ```
29
+
30
+ ## TTL Options
31
+
32
+ ```typescript
33
+ // Set with TTL (seconds)
34
+ await cache.set('key', value, { ttl: 60 }); // 1 minute
35
+ await cache.set('key', value, { ttl: 3600 }); // 1 hour
36
+ await cache.set('key', value, { ttl: 86400 }); // 1 day
37
+
38
+ // No expiration
39
+ await cache.set('key', value); // Permanent until deleted
40
+ ```
41
+
42
+ ## Patterns
43
+
44
+ ### Cache-Aside
45
+
46
+ ```typescript
47
+ async function getUser(id: string): Promise<User>
48
+ {
49
+ const cached = await cache.get<User>(`user:${id}`);
50
+ if (cached)
51
+ {
52
+ return cached;
53
+ }
54
+
55
+ const user = await userRepo.findById(id);
56
+ if (user)
57
+ {
58
+ await cache.set(`user:${id}`, user, { ttl: 3600 });
59
+ }
60
+
61
+ return user;
62
+ }
63
+ ```
64
+
65
+ ### Cache Invalidation
66
+
67
+ ```typescript
68
+ async function updateUser(id: string, data: Partial<User>)
69
+ {
70
+ const user = await userRepo.update(id, data);
71
+ await cache.del(`user:${id}`); // Invalidate cache
72
+ return user;
73
+ }
74
+ ```
75
+
76
+ ### Cache with Prefix
77
+
78
+ ```typescript
79
+ const userCache = cache.prefix('user');
80
+
81
+ await userCache.set('123', user); // Key: user:123
82
+ await userCache.get('123');
83
+ await userCache.del('123');
84
+ ```
85
+
86
+ ## Hash Operations
87
+
88
+ ```typescript
89
+ // Set hash field
90
+ await cache.hset('user:123', 'name', 'John');
91
+
92
+ // Get hash field
93
+ const name = await cache.hget('user:123', 'name');
94
+
95
+ // Get all hash fields
96
+ const user = await cache.hgetall('user:123');
97
+
98
+ // Delete hash field
99
+ await cache.hdel('user:123', 'name');
100
+ ```
101
+
102
+ ## List Operations
103
+
104
+ ```typescript
105
+ // Push to list
106
+ await cache.lpush('queue', item);
107
+ await cache.rpush('queue', item);
108
+
109
+ // Pop from list
110
+ const item = await cache.lpop('queue');
111
+ const item = await cache.rpop('queue');
112
+
113
+ // Get list range
114
+ const items = await cache.lrange('queue', 0, -1); // All items
115
+ ```
116
+
117
+ ## Best Practices
118
+
119
+ ```typescript
120
+ // 1. Use consistent key naming
121
+ `user:${id}`
122
+ `session:${token}`
123
+ `cache:posts:${page}`
124
+
125
+ // 2. Set appropriate TTL
126
+ { ttl: 300 } // 5 min for frequently changing data
127
+ { ttl: 3600 } // 1 hour for stable data
128
+ { ttl: 86400 } // 1 day for rarely changing data
129
+
130
+ // 3. Invalidate on write
131
+ await userRepo.update(id, data);
132
+ await cache.del(`user:${id}`);
133
+ ```
@@ -0,0 +1,74 @@
1
+ # Codegen
2
+
3
+ Automatic API client generation from route definitions.
4
+
5
+ ## Setup
6
+
7
+ ### Configure Generator
8
+
9
+ ```typescript
10
+ // codegen.config.ts
11
+ import { defineCodegenConfig } from '@spfn/core/codegen';
12
+
13
+ export default defineCodegenConfig({
14
+ generators: [
15
+ {
16
+ name: 'api-client',
17
+ output: './src/client/api.ts',
18
+ router: './src/server/server.config.ts'
19
+ }
20
+ ]
21
+ });
22
+ ```
23
+
24
+ ## Generate Client
25
+
26
+ ```bash
27
+ # Generate once
28
+ pnpm spfn codegen run
29
+
30
+ # Watch mode (dev server includes this)
31
+ pnpm spfn:dev
32
+ ```
33
+
34
+ ## Generated Client
35
+
36
+ ```typescript
37
+ // src/client/api.ts (generated)
38
+ import { createApi } from '@spfn/core/nextjs';
39
+ import type { AppRouter } from '@/server/server.config';
40
+
41
+ export const api = createApi<AppRouter>();
42
+ ```
43
+
44
+ ## Usage
45
+
46
+ ```typescript
47
+ import { api } from '@/client/api';
48
+
49
+ // Type-safe API calls
50
+ const user = await api.getUser.call({
51
+ params: { id: '123' }
52
+ });
53
+
54
+ const users = await api.getUsers.call({
55
+ query: { page: 1, limit: 20 }
56
+ });
57
+
58
+ const created = await api.createUser.call({
59
+ body: { email: 'user@example.com', name: 'User' }
60
+ });
61
+ ```
62
+
63
+ ## CLI Commands
64
+
65
+ ```bash
66
+ # Generate API client
67
+ pnpm spfn codegen run
68
+
69
+ # List registered generators
70
+ pnpm spfn codegen list
71
+
72
+ # Run specific generator
73
+ pnpm spfn codegen run --name api-client
74
+ ```