@spfn/core 0.2.0-beta.5 → 0.2.0-beta.8

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.
@@ -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
  };
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
+ ```
@@ -0,0 +1,346 @@
1
+ # Database
2
+
3
+ PostgreSQL database layer with Drizzle ORM, automatic transaction management, and read/write separation.
4
+
5
+ ## Setup
6
+
7
+ ### Environment Variables
8
+
9
+ ```bash
10
+ # Single database
11
+ DATABASE_URL=postgresql://localhost:5432/mydb
12
+
13
+ # Primary + Replica (recommended for production)
14
+ DATABASE_WRITE_URL=postgresql://primary:5432/mydb
15
+ DATABASE_READ_URL=postgresql://replica:5432/mydb
16
+ ```
17
+
18
+ ### Initialize
19
+
20
+ ```typescript
21
+ import { initDatabase } from '@spfn/core/db';
22
+
23
+ // Called automatically by startServer()
24
+ // Manual call only needed for scripts
25
+ await initDatabase();
26
+ ```
27
+
28
+ ---
29
+
30
+ ## Helper Functions
31
+
32
+ Standalone functions for simple database operations.
33
+
34
+ ```typescript
35
+ import {
36
+ findOne,
37
+ findMany,
38
+ create,
39
+ createMany,
40
+ upsert,
41
+ updateOne,
42
+ updateMany,
43
+ deleteOne,
44
+ deleteMany,
45
+ count
46
+ } from '@spfn/core/db';
47
+ ```
48
+
49
+ ### findOne
50
+
51
+ Find a single record.
52
+
53
+ ```typescript
54
+ // Object-based where (simple equality)
55
+ const user = await findOne(users, { id: '1' });
56
+ const user = await findOne(users, { email: 'test@example.com' });
57
+
58
+ // SQL-based where (complex conditions)
59
+ import { eq, and, gt } from 'drizzle-orm';
60
+ const user = await findOne(users, and(
61
+ eq(users.email, 'test@example.com'),
62
+ eq(users.isActive, true)
63
+ ));
64
+ ```
65
+
66
+ ### findMany
67
+
68
+ Find multiple records with filtering, ordering, and pagination.
69
+
70
+ ```typescript
71
+ // Simple
72
+ const allUsers = await findMany(users);
73
+
74
+ // With options
75
+ const activeUsers = await findMany(users, {
76
+ where: { isActive: true },
77
+ orderBy: desc(users.createdAt),
78
+ limit: 10,
79
+ offset: 0
80
+ });
81
+
82
+ // Complex where
83
+ const recentAdmins = await findMany(users, {
84
+ where: and(
85
+ eq(users.role, 'admin'),
86
+ gt(users.createdAt, lastWeek)
87
+ ),
88
+ orderBy: [desc(users.createdAt), asc(users.name)],
89
+ limit: 20
90
+ });
91
+ ```
92
+
93
+ ### create
94
+
95
+ Create a single record.
96
+
97
+ ```typescript
98
+ const user = await create(users, {
99
+ email: 'new@example.com',
100
+ name: 'New User'
101
+ });
102
+ ```
103
+
104
+ ### createMany
105
+
106
+ Create multiple records.
107
+
108
+ ```typescript
109
+ const newUsers = await createMany(users, [
110
+ { email: 'user1@example.com', name: 'User 1' },
111
+ { email: 'user2@example.com', name: 'User 2' }
112
+ ]);
113
+ ```
114
+
115
+ ### upsert
116
+
117
+ Insert or update on conflict.
118
+
119
+ ```typescript
120
+ const cache = await upsert(cmsCache, data, {
121
+ target: [cmsCache.section, cmsCache.locale],
122
+ set: {
123
+ content: data.content,
124
+ updatedAt: new Date()
125
+ }
126
+ });
127
+ ```
128
+
129
+ ### updateOne
130
+
131
+ Update a single record. Returns updated record or null.
132
+
133
+ ```typescript
134
+ const updated = await updateOne(users, { id: '1' }, { name: 'Updated Name' });
135
+ if (!updated)
136
+ {
137
+ throw new Error('User not found');
138
+ }
139
+ ```
140
+
141
+ ### updateMany
142
+
143
+ Update multiple records. Returns array of updated records.
144
+
145
+ ```typescript
146
+ const updated = await updateMany(
147
+ users,
148
+ { role: 'guest' },
149
+ { isActive: false }
150
+ );
151
+ ```
152
+
153
+ ### deleteOne
154
+
155
+ Delete a single record. Returns deleted record or null.
156
+
157
+ ```typescript
158
+ const deleted = await deleteOne(users, { id: '1' });
159
+ ```
160
+
161
+ ### deleteMany
162
+
163
+ Delete multiple records. Returns array of deleted records.
164
+
165
+ ```typescript
166
+ const deleted = await deleteMany(users, { isActive: false });
167
+ ```
168
+
169
+ ### count
170
+
171
+ Count records.
172
+
173
+ ```typescript
174
+ const total = await count(users);
175
+ const activeCount = await count(users, { isActive: true });
176
+ const adminCount = await count(users, eq(users.role, 'admin'));
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Transaction
182
+
183
+ ### Transactional Middleware
184
+
185
+ Use in routes for automatic commit/rollback.
186
+
187
+ ```typescript
188
+ import { Transactional } from '@spfn/core/db';
189
+
190
+ route.post('/users')
191
+ .use([Transactional()])
192
+ .handler(async (c) => {
193
+ // Auto commit on success
194
+ // Auto rollback on error
195
+ return userRepo.create(body);
196
+ });
197
+ ```
198
+
199
+ **With options:**
200
+
201
+ ```typescript
202
+ Transactional({
203
+ timeout: 30000, // Transaction timeout (ms)
204
+ logSuccess: false, // Log successful transactions
205
+ logErrors: true // Log failed transactions
206
+ })
207
+ ```
208
+
209
+ ### Manual Transaction
210
+
211
+ For complex multi-operation scenarios.
212
+
213
+ ```typescript
214
+ import { runWithTransaction } from '@spfn/core/db';
215
+
216
+ await runWithTransaction(async () => {
217
+ const user = await userRepo.create(userData);
218
+ await profileRepo.create({ userId: user.id, ...profileData });
219
+ await emailService.sendWelcome(user.email);
220
+ // All succeed or all rollback
221
+ });
222
+ ```
223
+
224
+ ### Get Current Transaction
225
+
226
+ Access the current transaction context.
227
+
228
+ ```typescript
229
+ import { getTransaction } from '@spfn/core/db';
230
+
231
+ async function customDbOperation()
232
+ {
233
+ const tx = getTransaction();
234
+ if (tx)
235
+ {
236
+ // Inside transaction
237
+ await tx.insert(users).values(data);
238
+ }
239
+ else
240
+ {
241
+ // Not in transaction
242
+ const db = getDatabase('write');
243
+ await db.insert(users).values(data);
244
+ }
245
+ }
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Direct Database Access
251
+
252
+ For complex queries not covered by helpers.
253
+
254
+ ```typescript
255
+ import { getDatabase } from '@spfn/core/db';
256
+
257
+ // Read operations (uses replica if available)
258
+ const db = getDatabase('read');
259
+ const results = await db
260
+ .select({
261
+ user: users,
262
+ postsCount: sql`count(${posts.id})`
263
+ })
264
+ .from(users)
265
+ .leftJoin(posts, eq(users.id, posts.authorId))
266
+ .groupBy(users.id);
267
+
268
+ // Write operations (always uses primary)
269
+ const db = getDatabase('write');
270
+ await db.insert(users).values(data);
271
+ ```
272
+
273
+ ---
274
+
275
+ ## Connection Info
276
+
277
+ ```typescript
278
+ import { getDatabaseInfo, checkConnection } from '@spfn/core/db';
279
+
280
+ // Get connection status
281
+ const info = getDatabaseInfo();
282
+ // { hasWriteDb: true, hasReadDb: true, pattern: 'write-read' }
283
+
284
+ // Health check
285
+ const isHealthy = await checkConnection(getDatabase('write'));
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Cleanup
291
+
292
+ ```typescript
293
+ import { closeDatabase } from '@spfn/core/db';
294
+
295
+ // Called automatically on graceful shutdown
296
+ // Manual call for scripts/tests
297
+ await closeDatabase();
298
+ ```
299
+
300
+ ---
301
+
302
+ ## Best Practices
303
+
304
+ ### Do
305
+
306
+ ```typescript
307
+ // 1. Use Transactional for write routes
308
+ route.post('/users')
309
+ .use([Transactional()])
310
+ .handler(...)
311
+
312
+ // 2. Use repository pattern for data access
313
+ const user = await userRepo.findById(id);
314
+
315
+ // 3. Use read database for read operations
316
+ async findAll()
317
+ {
318
+ return this._findMany(users); // BaseRepository uses readDb
319
+ }
320
+
321
+ // 4. Close connections in tests
322
+ afterAll(async () => {
323
+ await closeDatabase();
324
+ });
325
+ ```
326
+
327
+ ### Don't
328
+
329
+ ```typescript
330
+ // 1. Don't forget Transactional for writes
331
+ route.post('/users')
332
+ .handler(async (c) => { // Missing Transactional!
333
+ await userRepo.create(body);
334
+ });
335
+
336
+ // 2. Don't bypass repository in routes
337
+ route.get('/users')
338
+ .handler(async (c) => {
339
+ // Bad - use repository
340
+ return getDatabase('read').select().from(users);
341
+ });
342
+
343
+ // 3. Don't use write database for reads
344
+ const db = getDatabase('write'); // Bad for read queries
345
+ await db.select().from(users);
346
+ ```