@spfn/core 0.2.0-beta.49 → 0.2.0-beta.50

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfn/core",
3
- "version": "0.2.0-beta.49",
3
+ "version": "0.2.0-beta.50",
4
4
  "description": "SPFN Framework Core - File-based routing, transactions, repository pattern",
5
5
  "type": "module",
6
6
  "exports": {
@@ -134,7 +134,7 @@
134
134
  "transaction",
135
135
  "repository-pattern"
136
136
  ],
137
- "author": "Ray Im <rayim@inflike.com>",
137
+ "author": "Ray Im <rayim@fxy.global>",
138
138
  "license": "MIT",
139
139
  "repository": {
140
140
  "type": "git",
@@ -203,7 +203,7 @@
203
203
  "dev": "tsup",
204
204
  "test": "vitest run",
205
205
  "test:watch": "vitest",
206
- "test:coverage": "vitest run --config vitest.unit.config.ts --coverage",
206
+ "test:coverage": "vitest run --coverage",
207
207
  "test:logger": "vitest run src/logger",
208
208
  "test:errors": "vitest run src/errors",
209
209
  "test:codegen": "vitest run src/codegen",
package/docs/cache.md DELETED
@@ -1,133 +0,0 @@
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
- ```
package/docs/codegen.md DELETED
@@ -1,74 +0,0 @@
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
- ```
package/docs/database.md DELETED
@@ -1,436 +0,0 @@
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
- ### After-Commit Hooks
225
-
226
- Schedule side effects to run only after the transaction commits.
227
-
228
- ```typescript
229
- import { onAfterCommit } from '@spfn/core/db';
230
-
231
- async function submitRequest(spaceId: string, chatId: string)
232
- {
233
- const publication = await publicationRepo.create({ spaceId, chatId });
234
- await requestRepo.updateStatusAtomically(requestId, 'submitted');
235
-
236
- // Runs after commit, fire-and-forget
237
- onAfterCommit(() => generateArticle(spaceId, chatId, publication.id));
238
-
239
- return publication;
240
- }
241
- ```
242
-
243
- - Inside transaction: queued, executed after root commit
244
- - Outside transaction: executed immediately
245
- - Nested transactions: callbacks bubble up to root
246
- - Errors are logged, never thrown
247
-
248
- ### Get Current Transaction
249
-
250
- Access the current transaction context.
251
-
252
- ```typescript
253
- import { getTransaction } from '@spfn/core/db';
254
-
255
- async function customDbOperation()
256
- {
257
- const tx = getTransaction();
258
- if (tx)
259
- {
260
- // Inside transaction
261
- await tx.insert(users).values(data);
262
- }
263
- else
264
- {
265
- // Not in transaction
266
- const db = getDatabase('write');
267
- await db.insert(users).values(data);
268
- }
269
- }
270
- ```
271
-
272
- ---
273
-
274
- ## Direct Database Access
275
-
276
- For complex queries not covered by helpers.
277
-
278
- ```typescript
279
- import { getDatabase } from '@spfn/core/db';
280
-
281
- // Read operations (uses replica if available)
282
- const db = getDatabase('read');
283
- const results = await db
284
- .select({
285
- user: users,
286
- postsCount: sql`count(${posts.id})`
287
- })
288
- .from(users)
289
- .leftJoin(posts, eq(users.id, posts.authorId))
290
- .groupBy(users.id);
291
-
292
- // Write operations (always uses primary)
293
- const db = getDatabase('write');
294
- await db.insert(users).values(data);
295
- ```
296
-
297
- ---
298
-
299
- ## Connection Info
300
-
301
- ```typescript
302
- import { getDatabaseInfo, checkConnection } from '@spfn/core/db';
303
-
304
- // Get connection status
305
- const info = getDatabaseInfo();
306
- // { hasWriteDb: true, hasReadDb: true, pattern: 'write-read' }
307
-
308
- // Health check
309
- const isHealthy = await checkConnection(getDatabase('write'));
310
- ```
311
-
312
- ---
313
-
314
- ## Pool Recovery
315
-
316
- `@spfn/core` rebuilds the entire `postgres.js` pool (atomic swap) in two situations:
317
-
318
- 1. **Periodic health check** — every `DB_HEALTH_CHECK_INTERVAL` (default 60s),
319
- `SELECT 1` runs on write/read. On failure the pool is destroyed and recreated.
320
- 2. **Query-error fast-path** — real query errors caught by `BaseRepository.withContext`
321
- and `@Transactional` middleware are classified; once `DB_RECONNECT_ERROR_THRESHOLD`
322
- (default 3) connection-level failures occur within `DB_RECONNECT_ERROR_WINDOW_MS`
323
- (default 10s), a rebuild fires without waiting for the periodic tick.
324
-
325
- Both paths share the same atomic-swap implementation: the new pool is created and
326
- validated *before* the global reference is replaced, and the old pool is torn down
327
- only after the swap completes. Concurrent triggers coalesce to a single rebuild.
328
-
329
- ### Manual trigger
330
-
331
- ```typescript
332
- import { forceReconnectDatabase } from '@spfn/core/db';
333
-
334
- // Admin endpoint
335
- route.post('/admin/db/reconnect')
336
- .handler(async (c) => {
337
- const ran = await forceReconnectDatabase('admin_request');
338
- return c.json({ reconnected: ran });
339
- });
340
- ```
341
-
342
- Returns `false` if the database is not initialized, is currently closing, or a
343
- reconnect is already in progress.
344
-
345
- ### Environment variables
346
-
347
- ```bash
348
- # Periodic health check
349
- DB_HEALTH_CHECK_INTERVAL=60000 # ms between SELECT 1 probes
350
- DB_HEALTH_CHECK_MAX_RETRIES=3 # retries per rebuild attempt
351
- DB_HEALTH_CHECK_RETRY_INTERVAL=5000 # delay between retries
352
-
353
- # Query-error fast-path
354
- DB_RECONNECT_ERROR_THRESHOLD=3 # errors needed to trigger rebuild
355
- DB_RECONNECT_ERROR_WINDOW_MS=10000 # sliding window length (min 1000ms)
356
- ```
357
-
358
- ### Advanced: custom catch sites
359
-
360
- Application code that executes drizzle queries outside `BaseRepository` and
361
- `@Transactional` can feed the fast-path manually:
362
-
363
- ```typescript
364
- import { reportDatabaseError } from '@spfn/core/db';
365
-
366
- try {
367
- await db.execute(sql`...`);
368
- }
369
- catch (error) {
370
- reportDatabaseError(error); // no-op for non-connection errors
371
- throw error;
372
- }
373
- ```
374
-
375
- Inside `BaseRepository` / `@Transactional` this is already automatic — no
376
- manual call needed.
377
-
378
- ---
379
-
380
- ## Cleanup
381
-
382
- ```typescript
383
- import { closeDatabase } from '@spfn/core/db';
384
-
385
- // Called automatically on graceful shutdown
386
- // Manual call for scripts/tests
387
- await closeDatabase();
388
- ```
389
-
390
- ---
391
-
392
- ## Best Practices
393
-
394
- ### Do
395
-
396
- ```typescript
397
- // 1. Use Transactional for write routes
398
- route.post('/users')
399
- .use([Transactional()])
400
- .handler(...)
401
-
402
- // 2. Use repository pattern for data access
403
- const user = await userRepo.findById(id);
404
-
405
- // 3. Use read database for read operations
406
- async findAll()
407
- {
408
- return this._findMany(users); // BaseRepository uses readDb
409
- }
410
-
411
- // 4. Close connections in tests
412
- afterAll(async () => {
413
- await closeDatabase();
414
- });
415
- ```
416
-
417
- ### Don't
418
-
419
- ```typescript
420
- // 1. Don't forget Transactional for writes
421
- route.post('/users')
422
- .handler(async (c) => { // Missing Transactional!
423
- await userRepo.create(body);
424
- });
425
-
426
- // 2. Don't bypass repository in routes
427
- route.get('/users')
428
- .handler(async (c) => {
429
- // Bad - use repository
430
- return getDatabase('read').select().from(users);
431
- });
432
-
433
- // 3. Don't use write database for reads
434
- const db = getDatabase('write'); // Bad for read queries
435
- await db.select().from(users);
436
- ```