@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/LICENSE +1 -1
- package/README.md +181 -366
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.js.map +1 -1
- package/dist/db/index.d.ts +7 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +1 -1
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +2 -3
- package/dist/env/loader.js +0 -1
- package/dist/env/loader.js.map +1 -1
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.js.map +1 -1
- package/dist/event/sse/client.js.map +1 -1
- package/dist/event/sse/index.js.map +1 -1
- package/dist/event/ws/client.js.map +1 -1
- package/dist/event/ws/index.js.map +1 -1
- package/dist/job/index.js.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.js.map +1 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/route/index.js.map +1 -1
- package/dist/server/index.js +0 -1
- package/dist/server/index.js.map +1 -1
- package/package.json +3 -3
- package/docs/cache.md +0 -133
- package/docs/codegen.md +0 -74
- package/docs/database.md +0 -436
- package/docs/entity.md +0 -539
- package/docs/env.md +0 -499
- package/docs/errors.md +0 -319
- package/docs/event.md +0 -443
- package/docs/job.md +0 -131
- package/docs/logger.md +0 -108
- package/docs/middleware.md +0 -337
- package/docs/nextjs.md +0 -247
- package/docs/repository.md +0 -496
- package/docs/route.md +0 -497
- package/docs/server.md +0 -429
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spfn/core",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
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@
|
|
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 --
|
|
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
|
-
```
|