@spfn/core 0.1.0-alpha.88 → 0.2.0-beta.10
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/README.md +298 -466
- package/dist/boss-DI1r4kTS.d.ts +244 -0
- package/dist/cache/index.d.ts +13 -33
- package/dist/cache/index.js +14 -703
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +214 -17
- package/dist/codegen/index.js +231 -1420
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +1227 -0
- package/dist/config/index.js +273 -0
- package/dist/config/index.js.map +1 -0
- package/dist/db/index.d.ts +741 -59
- package/dist/db/index.js +1063 -1226
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +658 -308
- package/dist/env/index.js +503 -928
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +87 -0
- package/dist/env/loader.js +70 -0
- package/dist/env/loader.js.map +1 -0
- package/dist/errors/index.d.ts +417 -29
- package/dist/errors/index.js +359 -98
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +41 -0
- package/dist/event/index.js +131 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/sse/client.d.ts +82 -0
- package/dist/event/sse/client.js +115 -0
- package/dist/event/sse/client.js.map +1 -0
- package/dist/event/sse/index.d.ts +40 -0
- package/dist/event/sse/index.js +92 -0
- package/dist/event/sse/index.js.map +1 -0
- package/dist/job/index.d.ts +218 -0
- package/dist/job/index.js +410 -0
- package/dist/job/index.js.map +1 -0
- package/dist/logger/index.d.ts +20 -79
- package/dist/logger/index.js +82 -387
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +102 -20
- package/dist/middleware/index.js +51 -705
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +120 -0
- package/dist/nextjs/index.js +448 -0
- package/dist/nextjs/index.js.map +1 -0
- package/dist/{client/nextjs/index.d.ts → nextjs/server.d.ts} +335 -262
- package/dist/nextjs/server.js +637 -0
- package/dist/nextjs/server.js.map +1 -0
- package/dist/route/index.d.ts +879 -25
- package/dist/route/index.js +697 -1271
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +9 -0
- package/dist/route/types.js +3 -0
- package/dist/route/types.js.map +1 -0
- package/dist/router-Di7ENoah.d.ts +151 -0
- package/dist/server/index.d.ts +345 -64
- package/dist/server/index.js +1174 -3233
- package/dist/server/index.js.map +1 -1
- package/dist/types-B-e_f2dQ.d.ts +121 -0
- package/dist/types-BGl4QL1w.d.ts +77 -0
- package/dist/types-BOPTApC2.d.ts +245 -0
- package/docs/cache.md +133 -0
- package/docs/codegen.md +74 -0
- package/docs/database.md +346 -0
- package/docs/entity.md +539 -0
- package/docs/env.md +477 -0
- package/docs/errors.md +319 -0
- package/docs/event.md +116 -0
- package/docs/file-upload.md +717 -0
- package/docs/job.md +131 -0
- package/docs/logger.md +108 -0
- package/docs/middleware.md +337 -0
- package/docs/nextjs.md +241 -0
- package/docs/repository.md +496 -0
- package/docs/route.md +497 -0
- package/docs/server.md +307 -0
- package/package.json +68 -48
- package/dist/auto-loader-JFaZ9gON.d.ts +0 -80
- package/dist/client/index.d.ts +0 -358
- package/dist/client/index.js +0 -357
- package/dist/client/index.js.map +0 -1
- package/dist/client/nextjs/index.js +0 -371
- package/dist/client/nextjs/index.js.map +0 -1
- package/dist/codegen/generators/index.d.ts +0 -19
- package/dist/codegen/generators/index.js +0 -1404
- package/dist/codegen/generators/index.js.map +0 -1
- package/dist/database-errors-BNNmLTJE.d.ts +0 -86
- package/dist/events/index.d.ts +0 -183
- package/dist/events/index.js +0 -77
- package/dist/events/index.js.map +0 -1
- package/dist/index-DHiAqhKv.d.ts +0 -101
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -3674
- package/dist/index.js.map +0 -1
- package/dist/types/index.d.ts +0 -121
- package/dist/types/index.js +0 -38
- package/dist/types/index.js.map +0 -1
- package/dist/types-BXibIEyj.d.ts +0 -60
package/README.md
CHANGED
|
@@ -1,561 +1,393 @@
|
|
|
1
1
|
# @spfn/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Type-safe Node.js backend framework built on Hono + Drizzle ORM.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@spfn/core)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
|
|
9
|
-
>
|
|
9
|
+
> **Beta Release**: Core APIs are stable but may have minor changes before 1.0.
|
|
10
10
|
|
|
11
11
|
## Installation
|
|
12
12
|
|
|
13
|
-
**Recommended: Create New Project**
|
|
14
13
|
```bash
|
|
15
|
-
|
|
14
|
+
pnpm add @spfn/core
|
|
16
15
|
```
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
|
-
cd your-nextjs-project
|
|
21
|
-
npx spfn@alpha init
|
|
22
|
-
```
|
|
17
|
+
## Quick Start
|
|
23
18
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
### 1. Define Entity
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
// src/server/entities/users.ts
|
|
23
|
+
import { pgTable, text, boolean } from 'drizzle-orm/pg-core';
|
|
24
|
+
import { id, timestamps } from '@spfn/core/db';
|
|
25
|
+
|
|
26
|
+
export const users = pgTable('users', {
|
|
27
|
+
id: id(),
|
|
28
|
+
email: text('email').notNull().unique(),
|
|
29
|
+
name: text('name').notNull(),
|
|
30
|
+
isActive: boolean('is_active').notNull().default(true),
|
|
31
|
+
...timestamps()
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type User = typeof users.$inferSelect;
|
|
35
|
+
export type NewUser = typeof users.$inferInsert;
|
|
27
36
|
```
|
|
28
37
|
|
|
29
|
-
|
|
38
|
+
### 2. Create Repository
|
|
39
|
+
|
|
40
|
+
```typescript
|
|
41
|
+
// src/server/repositories/user.repository.ts
|
|
42
|
+
import { BaseRepository } from '@spfn/core/db';
|
|
43
|
+
import { users, type User, type NewUser } from '../entities/users';
|
|
44
|
+
|
|
45
|
+
export class UserRepository extends BaseRepository
|
|
46
|
+
{
|
|
47
|
+
async findById(id: string): Promise<User | null>
|
|
48
|
+
{
|
|
49
|
+
return this._findOne(users, { id });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async findByEmail(email: string): Promise<User | null>
|
|
53
|
+
{
|
|
54
|
+
return this._findOne(users, { email });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async findAll(): Promise<User[]>
|
|
58
|
+
{
|
|
59
|
+
return this._findMany(users);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async create(data: NewUser): Promise<User>
|
|
63
|
+
{
|
|
64
|
+
return this._create(users, data);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async update(id: string, data: Partial<NewUser>): Promise<User | null>
|
|
68
|
+
{
|
|
69
|
+
return this._updateOne(users, { id }, data);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async delete(id: string): Promise<User | null>
|
|
73
|
+
{
|
|
74
|
+
return this._deleteOne(users, { id });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
30
78
|
|
|
31
|
-
###
|
|
79
|
+
### 3. Define Routes
|
|
32
80
|
|
|
33
81
|
```typescript
|
|
34
|
-
// src/server/routes/users
|
|
82
|
+
// src/server/routes/users.ts
|
|
83
|
+
import { route } from '@spfn/core/route';
|
|
84
|
+
import { Transactional } from '@spfn/core/db';
|
|
35
85
|
import { Type } from '@sinclair/typebox';
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
86
|
+
import { UserRepository } from '../repositories/user.repository';
|
|
87
|
+
|
|
88
|
+
const userRepo = new UserRepository();
|
|
89
|
+
|
|
90
|
+
export const getUsers = route.get('/users')
|
|
91
|
+
.handler(async () => {
|
|
92
|
+
return userRepo.findAll();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const getUser = route.get('/users/:id')
|
|
96
|
+
.input({
|
|
97
|
+
params: Type.Object({ id: Type.String() })
|
|
98
|
+
})
|
|
99
|
+
.handler(async (c) => {
|
|
100
|
+
const { params } = await c.data();
|
|
101
|
+
const user = await userRepo.findById(params.id);
|
|
102
|
+
|
|
103
|
+
if (!user)
|
|
104
|
+
{
|
|
105
|
+
throw new Error('User not found');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return user;
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
export const createUser = route.post('/users')
|
|
112
|
+
.input({
|
|
113
|
+
body: Type.Object({
|
|
114
|
+
email: Type.String({ format: 'email' }),
|
|
115
|
+
name: Type.String({ minLength: 1 })
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
.use([Transactional()])
|
|
119
|
+
.handler(async (c) => {
|
|
120
|
+
const { body } = await c.data();
|
|
121
|
+
return userRepo.create(body);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
export const updateUser = route.patch('/users/:id')
|
|
125
|
+
.input({
|
|
126
|
+
params: Type.Object({ id: Type.String() }),
|
|
127
|
+
body: Type.Object({
|
|
128
|
+
email: Type.Optional(Type.String({ format: 'email' })),
|
|
129
|
+
name: Type.Optional(Type.String({ minLength: 1 }))
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
.use([Transactional()])
|
|
133
|
+
.handler(async (c) => {
|
|
134
|
+
const { params, body } = await c.data();
|
|
135
|
+
const user = await userRepo.update(params.id, body);
|
|
136
|
+
|
|
137
|
+
if (!user)
|
|
138
|
+
{
|
|
139
|
+
throw new Error('User not found');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return user;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
export const deleteUser = route.delete('/users/:id')
|
|
146
|
+
.input({
|
|
147
|
+
params: Type.Object({ id: Type.String() })
|
|
148
|
+
})
|
|
149
|
+
.use([Transactional()])
|
|
150
|
+
.handler(async (c) => {
|
|
151
|
+
const { params } = await c.data();
|
|
152
|
+
const user = await userRepo.delete(params.id);
|
|
153
|
+
|
|
154
|
+
if (!user)
|
|
155
|
+
{
|
|
156
|
+
throw new Error('User not found');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { success: true };
|
|
160
|
+
});
|
|
53
161
|
```
|
|
54
162
|
|
|
55
|
-
###
|
|
163
|
+
### 4. Configure Server
|
|
56
164
|
|
|
57
165
|
```typescript
|
|
58
|
-
// src/server/
|
|
59
|
-
import {
|
|
60
|
-
import
|
|
61
|
-
import { findMany } from '@spfn/core/db';
|
|
62
|
-
import { users } from '../../entities/users.js';
|
|
166
|
+
// src/server/server.config.ts
|
|
167
|
+
import { defineServerConfig, defineRouter } from '@spfn/core/server';
|
|
168
|
+
import * as userRoutes from './routes/users';
|
|
63
169
|
|
|
64
|
-
const
|
|
170
|
+
const appRouter = defineRouter({
|
|
171
|
+
...userRoutes
|
|
172
|
+
});
|
|
65
173
|
|
|
66
|
-
|
|
67
|
-
|
|
174
|
+
export default defineServerConfig()
|
|
175
|
+
.port(8790)
|
|
176
|
+
.routes(appRouter)
|
|
177
|
+
.build();
|
|
68
178
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const result = await findMany(users, { limit, offset });
|
|
179
|
+
export type AppRouter = typeof appRouter;
|
|
180
|
+
```
|
|
72
181
|
|
|
73
|
-
|
|
74
|
-
});
|
|
182
|
+
### 5. Start Server
|
|
75
183
|
|
|
76
|
-
|
|
184
|
+
```typescript
|
|
185
|
+
// src/server/index.ts
|
|
186
|
+
import { startServer } from '@spfn/core/server';
|
|
187
|
+
|
|
188
|
+
await startServer();
|
|
77
189
|
```
|
|
78
190
|
|
|
79
|
-
###
|
|
191
|
+
### 6. Environment Variables
|
|
80
192
|
|
|
81
193
|
```bash
|
|
82
|
-
|
|
83
|
-
|
|
194
|
+
# .env
|
|
195
|
+
DATABASE_URL=postgresql://localhost:5432/mydb
|
|
196
|
+
PORT=8790
|
|
84
197
|
```
|
|
85
198
|
|
|
86
|
-
|
|
199
|
+
---
|
|
87
200
|
|
|
88
|
-
|
|
201
|
+
## Architecture Overview
|
|
89
202
|
|
|
90
203
|
```
|
|
91
204
|
┌─────────────────────────────────────────┐
|
|
92
|
-
│ Routes Layer │
|
|
93
|
-
│
|
|
94
|
-
│ - Handle requests/responses │
|
|
95
|
-
│ - Thin handlers │
|
|
205
|
+
│ Routes Layer │ API endpoints + validation
|
|
206
|
+
│ route.get('/users/:id').handler(...) │
|
|
96
207
|
└──────────────┬──────────────────────────┘
|
|
97
208
|
│
|
|
98
209
|
┌──────────────▼──────────────────────────┐
|
|
99
|
-
│
|
|
100
|
-
│
|
|
101
|
-
│ - Implement business rules │
|
|
102
|
-
│ - Use helper functions or custom logic │
|
|
210
|
+
│ Repository Layer │ Business logic + data access
|
|
211
|
+
│ class UserRepository extends Base... │
|
|
103
212
|
└──────────────┬──────────────────────────┘
|
|
104
213
|
│
|
|
105
214
|
┌──────────────▼──────────────────────────┐
|
|
106
|
-
│
|
|
107
|
-
│
|
|
108
|
-
│ - Custom queries with Drizzle │
|
|
109
|
-
│ - Domain-specific wrappers │
|
|
110
|
-
└──────────────┬──────────────────────────┘
|
|
111
|
-
│
|
|
112
|
-
┌──────────────▼──────────────────────────┐
|
|
113
|
-
│ Entity Layer │ Database schema
|
|
114
|
-
│ - Table definitions (Drizzle) │
|
|
115
|
-
│ - Type inference │
|
|
116
|
-
│ - Schema helpers │
|
|
215
|
+
│ Entity Layer │ Database schema (Drizzle)
|
|
216
|
+
│ pgTable('users', { id, email, ... }) │
|
|
117
217
|
└─────────────────────────────────────────┘
|
|
118
218
|
```
|
|
119
219
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
**
|
|
123
|
-
|
|
124
|
-
```typescript
|
|
125
|
-
// src/server/entities/posts.ts
|
|
126
|
-
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
127
|
-
import { id, timestamps } from '@spfn/core/db';
|
|
128
|
-
|
|
129
|
-
export const posts = pgTable('posts', {
|
|
130
|
-
id: id(),
|
|
131
|
-
title: text('title').notNull(),
|
|
132
|
-
slug: text('slug').notNull().unique(),
|
|
133
|
-
content: text('content').notNull(),
|
|
134
|
-
status: text('status', {
|
|
135
|
-
enum: ['draft', 'published']
|
|
136
|
-
}).notNull().default('draft'),
|
|
137
|
-
...timestamps(),
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
export type Post = typeof posts.$inferSelect;
|
|
141
|
-
export type NewPost = typeof posts.$inferInsert;
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
**2. Data Access Layer** - Helper functions with domain-specific wrappers
|
|
220
|
+
**Key Principles:**
|
|
221
|
+
- **Route**: Thin layer, input validation only
|
|
222
|
+
- **Repository**: All business logic and DB access
|
|
223
|
+
- **Entity**: Schema definition only, no logic
|
|
145
224
|
|
|
146
|
-
|
|
147
|
-
// src/server/repositories/posts.repository.ts
|
|
148
|
-
import { findOne, findMany, create as createHelper } from '@spfn/core/db';
|
|
149
|
-
import { eq, desc } from 'drizzle-orm';
|
|
150
|
-
import { posts, type Post, type NewPost } from '../entities/posts';
|
|
151
|
-
|
|
152
|
-
// Domain-specific wrappers using helper functions
|
|
153
|
-
export async function findPostBySlug(slug: string): Promise<Post | null> {
|
|
154
|
-
return findOne(posts, { slug });
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export async function findPublishedPosts(): Promise<Post[]> {
|
|
158
|
-
return findMany(posts, {
|
|
159
|
-
where: { status: 'published' },
|
|
160
|
-
orderBy: desc(posts.createdAt)
|
|
161
|
-
});
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export async function createPost(data: NewPost): Promise<Post> {
|
|
165
|
-
return createHelper(posts, data);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Or use helper functions directly in routes for simple cases
|
|
169
|
-
// const post = await findOne(posts, { id: 1 });
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
**3. Routes Layer** - HTTP API
|
|
225
|
+
---
|
|
173
226
|
|
|
174
|
-
|
|
175
|
-
// src/server/routes/posts/contracts.ts
|
|
176
|
-
import { Type } from '@sinclair/typebox';
|
|
227
|
+
## Directory Structure
|
|
177
228
|
|
|
178
|
-
export const createPostContract = {
|
|
179
|
-
method: 'POST' as const,
|
|
180
|
-
path: '/',
|
|
181
|
-
body: Type.Object({
|
|
182
|
-
title: Type.String(),
|
|
183
|
-
content: Type.String(),
|
|
184
|
-
}),
|
|
185
|
-
response: Type.Object({
|
|
186
|
-
id: Type.String(),
|
|
187
|
-
title: Type.String(),
|
|
188
|
-
slug: Type.String(),
|
|
189
|
-
}),
|
|
190
|
-
};
|
|
191
|
-
|
|
192
|
-
export const listPostsContract = {
|
|
193
|
-
method: 'GET' as const,
|
|
194
|
-
path: '/',
|
|
195
|
-
response: Type.Array(Type.Object({
|
|
196
|
-
id: Type.String(),
|
|
197
|
-
title: Type.String(),
|
|
198
|
-
slug: Type.String(),
|
|
199
|
-
})),
|
|
200
|
-
};
|
|
201
229
|
```
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
app.bind(createPostContract, [Transactional()], async (c) => {
|
|
215
|
-
const body = await c.data();
|
|
216
|
-
|
|
217
|
-
// Generate slug from title
|
|
218
|
-
const slug = body.title.toLowerCase()
|
|
219
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
220
|
-
.replace(/(^-|-$)/g, '');
|
|
221
|
-
|
|
222
|
-
// Check if slug exists
|
|
223
|
-
const existing = await findPostBySlug(slug);
|
|
224
|
-
if (existing) {
|
|
225
|
-
throw new ConflictError('Post with this title already exists', { slug });
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Create post
|
|
229
|
-
const post = await createPost({
|
|
230
|
-
...body,
|
|
231
|
-
slug,
|
|
232
|
-
status: 'draft'
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// ✅ Auto-commit on success, auto-rollback on error
|
|
236
|
-
return c.json(post, 201);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// GET /posts - List published posts (no transaction needed)
|
|
240
|
-
app.bind(listPostsContract, async (c) => {
|
|
241
|
-
const posts = await findPublishedPosts();
|
|
242
|
-
return c.json(posts);
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
export default app;
|
|
230
|
+
src/server/
|
|
231
|
+
├── entities/ # Database schema
|
|
232
|
+
│ ├── users.ts
|
|
233
|
+
│ └── index.ts
|
|
234
|
+
├── repositories/ # Data access + business logic
|
|
235
|
+
│ ├── user.repository.ts
|
|
236
|
+
│ └── index.ts
|
|
237
|
+
├── routes/ # API routes
|
|
238
|
+
│ ├── users.ts
|
|
239
|
+
│ └── index.ts
|
|
240
|
+
├── server.config.ts # Server configuration
|
|
241
|
+
└── index.ts # Entry point
|
|
246
242
|
```
|
|
247
243
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
**✅ Separation of Concerns**
|
|
251
|
-
- Each layer has a single responsibility
|
|
252
|
-
- Easy to locate and modify code
|
|
253
|
-
|
|
254
|
-
**✅ Testability**
|
|
255
|
-
- Test each layer independently
|
|
256
|
-
- Mock dependencies easily
|
|
257
|
-
|
|
258
|
-
**✅ Reusability**
|
|
259
|
-
- Services can be used by multiple routes
|
|
260
|
-
- Data access functions can be shared across services
|
|
261
|
-
|
|
262
|
-
**✅ Type Safety**
|
|
263
|
-
- Types flow from Entity → Data Access → Service → Route
|
|
264
|
-
- Full IDE autocomplete and error checking
|
|
265
|
-
|
|
266
|
-
**✅ Maintainability**
|
|
267
|
-
- Add features without breaking existing code
|
|
268
|
-
- Clear boundaries prevent coupling
|
|
269
|
-
|
|
270
|
-
### Layer Responsibilities
|
|
271
|
-
|
|
272
|
-
| Layer | Responsibility | Examples |
|
|
273
|
-
|-------|---------------|----------|
|
|
274
|
-
| **Entity** | Define data structure | Schema, types, constraints |
|
|
275
|
-
| **Data Access** | Database operations | Helper functions, custom queries, joins |
|
|
276
|
-
| **Service** | Business logic | Validation, orchestration, rules |
|
|
277
|
-
| **Routes** | HTTP interface | Contracts, request handling |
|
|
278
|
-
|
|
279
|
-
### Best Practices
|
|
280
|
-
|
|
281
|
-
**Entity Layer:**
|
|
282
|
-
- ✅ Use schema helpers: `id()`, `timestamps()`
|
|
283
|
-
- ✅ Export inferred types: `Post`, `NewPost`
|
|
284
|
-
- ✅ Use TEXT with enum for status fields
|
|
285
|
-
|
|
286
|
-
**Data Access Layer:**
|
|
287
|
-
- ✅ Use helper functions for simple CRUD: `findOne()`, `create()`, etc.
|
|
288
|
-
- ✅ Create domain-specific wrappers in `src/server/repositories/*.repository.ts`
|
|
289
|
-
- ✅ Export functions (not classes): `export async function findPostBySlug()`
|
|
290
|
-
- ✅ Use object-based where for simple queries: `{ id: 1 }`
|
|
291
|
-
- ✅ Use SQL-based where for complex queries: `and(eq(...), gt(...))`
|
|
292
|
-
- ✅ Full TypeScript type inference from table schemas
|
|
293
|
-
|
|
294
|
-
**Routes Layer:**
|
|
295
|
-
- ✅ Keep handlers thin (delegate to services/data access)
|
|
296
|
-
- ✅ Define contracts with TypeBox
|
|
297
|
-
- ✅ Use `Transactional()` middleware for write operations
|
|
298
|
-
- ✅ Use `c.data()` for validated input
|
|
299
|
-
- ✅ Return `c.json()` responses
|
|
300
|
-
|
|
301
|
-
## Core Modules
|
|
302
|
-
|
|
303
|
-
### 📁 Routing
|
|
304
|
-
File-based routing with contract validation and type safety.
|
|
305
|
-
|
|
306
|
-
**[→ Read Routing Documentation](./src/route/README.md)**
|
|
307
|
-
|
|
308
|
-
**Key Features:**
|
|
309
|
-
- Automatic route discovery (`index.ts`, `[id].ts`, `[...slug].ts`)
|
|
310
|
-
- Contract-based validation with TypeBox
|
|
311
|
-
- Type-safe request/response handling
|
|
312
|
-
- Method-level middleware control (skip auth per HTTP method)
|
|
313
|
-
|
|
314
|
-
### 🗄️ Database
|
|
315
|
-
Drizzle ORM integration with type-safe helper functions and automatic transaction handling.
|
|
316
|
-
|
|
317
|
-
**[→ Read Database Documentation](./src/db/README.md)**
|
|
318
|
-
|
|
319
|
-
**Key Features:**
|
|
320
|
-
- Helper functions for type-safe CRUD operations
|
|
321
|
-
- Automatic transaction handling and read/write separation
|
|
322
|
-
- Schema helpers: `id()`, `timestamps()`, `foreignKey()`
|
|
323
|
-
- Hybrid where clause support (objects or SQL)
|
|
324
|
-
- **Function schema auto-discovery** (see below)
|
|
325
|
-
|
|
326
|
-
### 📦 Function Schema Discovery
|
|
327
|
-
Automatic discovery of database schemas from Superfunction ecosystem functions.
|
|
328
|
-
|
|
329
|
-
**[→ Read Database Manager Documentation](./src/db/manager/README.md)**
|
|
330
|
-
|
|
331
|
-
**Key Features:**
|
|
332
|
-
- Zero-config schema discovery from `@spfn/*` functions
|
|
333
|
-
- Functions declare schemas via `package.json`
|
|
334
|
-
- No hard dependencies between functions
|
|
335
|
-
- Efficient scanning (direct dependencies only)
|
|
336
|
-
- Function-specific migration support
|
|
244
|
+
---
|
|
337
245
|
|
|
338
|
-
|
|
246
|
+
## Core Concepts
|
|
339
247
|
|
|
340
|
-
|
|
341
|
-
```json
|
|
342
|
-
{
|
|
343
|
-
"name": "@spfn/cms",
|
|
344
|
-
"spfn": {
|
|
345
|
-
"schemas": ["./dist/entities/*.js"],
|
|
346
|
-
"setupMessage": "📚 Setup guide..."
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
```
|
|
248
|
+
### Route Definition
|
|
350
249
|
|
|
351
|
-
Superfunction automatically discovers and merges these schemas during migration generation:
|
|
352
250
|
```typescript
|
|
353
|
-
import {
|
|
251
|
+
import { route } from '@spfn/core/route';
|
|
252
|
+
import { Type } from '@sinclair/typebox';
|
|
354
253
|
|
|
355
|
-
//
|
|
356
|
-
|
|
254
|
+
// GET with params and query
|
|
255
|
+
route.get('/users/:id')
|
|
256
|
+
.input({
|
|
257
|
+
params: Type.Object({ id: Type.String() }),
|
|
258
|
+
query: Type.Object({ include: Type.Optional(Type.String()) })
|
|
259
|
+
})
|
|
260
|
+
.handler(async (c) => {
|
|
261
|
+
const { params, query } = await c.data();
|
|
262
|
+
// params.id, query.include are fully typed
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// POST with body
|
|
266
|
+
route.post('/users')
|
|
267
|
+
.input({
|
|
268
|
+
body: Type.Object({
|
|
269
|
+
email: Type.String(),
|
|
270
|
+
name: Type.String()
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
.handler(async (c) => {
|
|
274
|
+
const { body } = await c.data();
|
|
275
|
+
// body.email, body.name are fully typed
|
|
276
|
+
});
|
|
357
277
|
```
|
|
358
278
|
|
|
359
|
-
|
|
360
|
-
```bash
|
|
361
|
-
pnpm spfn add @spfn/cms
|
|
362
|
-
# ✅ Installs function
|
|
363
|
-
# ✅ Generates migrations
|
|
364
|
-
# ✅ Applies migrations
|
|
365
|
-
# ✅ Shows setup guide
|
|
366
|
-
```
|
|
279
|
+
### Repository Pattern
|
|
367
280
|
|
|
368
|
-
**Create your own Superfunction packages:**
|
|
369
281
|
```typescript
|
|
370
|
-
|
|
371
|
-
export const myTable = pgTable('my_table', { ... })
|
|
282
|
+
import { BaseRepository } from '@spfn/core/db';
|
|
372
283
|
|
|
373
|
-
|
|
284
|
+
export class UserRepository extends BaseRepository
|
|
374
285
|
{
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
286
|
+
// Protected helpers available:
|
|
287
|
+
// _findOne, _findMany, _create, _createMany
|
|
288
|
+
// _updateOne, _updateMany, _deleteOne, _deleteMany
|
|
289
|
+
// _count, _upsert
|
|
290
|
+
|
|
291
|
+
async findActive()
|
|
292
|
+
{
|
|
293
|
+
return this._findMany(users, {
|
|
294
|
+
where: { isActive: true },
|
|
295
|
+
orderBy: desc(users.createdAt),
|
|
296
|
+
limit: 10
|
|
297
|
+
});
|
|
298
|
+
}
|
|
378
299
|
}
|
|
379
|
-
|
|
380
|
-
// 3. Users install with one command
|
|
381
|
-
// pnpm spfn add @yourcompany/spfn-plugin
|
|
382
300
|
```
|
|
383
301
|
|
|
384
|
-
###
|
|
385
|
-
Automatic transaction management with async context propagation.
|
|
386
|
-
|
|
387
|
-
**[→ Read Transaction Documentation](./src/db/docs/transactions.md)**
|
|
388
|
-
|
|
389
|
-
**Key Features:**
|
|
390
|
-
- Auto-commit on success, auto-rollback on error
|
|
391
|
-
- AsyncLocalStorage-based context
|
|
392
|
-
- Transaction logging
|
|
393
|
-
|
|
394
|
-
### 💾 Cache
|
|
395
|
-
Redis integration with master-replica support.
|
|
396
|
-
|
|
397
|
-
**[→ Read Cache Documentation](./src/cache/README.md)**
|
|
398
|
-
|
|
399
|
-
### ⚠️ Error Handling
|
|
400
|
-
Custom error classes with unified HTTP responses.
|
|
401
|
-
|
|
402
|
-
**[→ Read Error Documentation](./src/errors/README.md)**
|
|
403
|
-
|
|
404
|
-
### 🔐 Middleware
|
|
405
|
-
Request logging, CORS, and error handling middleware.
|
|
302
|
+
### Transaction
|
|
406
303
|
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
### 🖥️ Server
|
|
410
|
-
Server configuration and lifecycle management.
|
|
411
|
-
|
|
412
|
-
**[→ Read Server Documentation](./src/server/README.md)**
|
|
413
|
-
|
|
414
|
-
### 📝 Logger
|
|
415
|
-
High-performance logging with multiple transports, sensitive data masking, and automatic validation.
|
|
416
|
-
|
|
417
|
-
**[→ Read Logger Documentation](./src/logger/README.md)**
|
|
418
|
-
|
|
419
|
-
**Key Features:**
|
|
420
|
-
- Adapter pattern (Pino for production, custom for full control)
|
|
421
|
-
- Sensitive data masking (passwords, tokens, API keys)
|
|
422
|
-
- File rotation (date and size-based) with automatic cleanup
|
|
423
|
-
- Configuration validation with clear error messages
|
|
424
|
-
- Multiple transports (Console, File, Slack, Email)
|
|
425
|
-
|
|
426
|
-
### ⚙️ Code Generation
|
|
427
|
-
Automatic code generation with pluggable generators and centralized file watching.
|
|
428
|
-
|
|
429
|
-
**[→ Read Codegen Documentation](./src/codegen/README.md)**
|
|
304
|
+
```typescript
|
|
305
|
+
import { Transactional } from '@spfn/core/db';
|
|
430
306
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
307
|
+
// Middleware-based (recommended)
|
|
308
|
+
route.post('/users')
|
|
309
|
+
.use([Transactional()])
|
|
310
|
+
.handler(async (c) => {
|
|
311
|
+
// Auto commit on success
|
|
312
|
+
// Auto rollback on error
|
|
313
|
+
});
|
|
437
314
|
|
|
438
|
-
|
|
315
|
+
// Manual control
|
|
316
|
+
import { runWithTransaction } from '@spfn/core/db';
|
|
439
317
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
318
|
+
await runWithTransaction(async () => {
|
|
319
|
+
await userRepo.create(userData);
|
|
320
|
+
await profileRepo.create(profileData);
|
|
321
|
+
});
|
|
443
322
|
```
|
|
444
323
|
|
|
445
|
-
###
|
|
446
|
-
```typescript
|
|
447
|
-
import { createApp, bind, loadRoutes } from '@spfn/core/route';
|
|
448
|
-
import type { RouteContext, RouteContract } from '@spfn/core/route';
|
|
449
|
-
```
|
|
324
|
+
### Schema Helpers
|
|
450
325
|
|
|
451
|
-
### Database
|
|
452
326
|
```typescript
|
|
453
327
|
import {
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
deleteMany,
|
|
463
|
-
count
|
|
328
|
+
id, // bigserial primary key
|
|
329
|
+
uuid, // uuid primary key
|
|
330
|
+
timestamps, // createdAt, updatedAt
|
|
331
|
+
foreignKey, // required FK with cascade
|
|
332
|
+
optionalForeignKey, // nullable FK
|
|
333
|
+
softDelete, // deletedAt, deletedBy
|
|
334
|
+
enumText, // type-safe enum
|
|
335
|
+
typedJsonb // type-safe JSONB
|
|
464
336
|
} from '@spfn/core/db';
|
|
465
|
-
```
|
|
466
337
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
338
|
+
export const posts = pgTable('posts', {
|
|
339
|
+
id: id(),
|
|
340
|
+
title: text('title').notNull(),
|
|
341
|
+
authorId: foreignKey('author', () => users.id),
|
|
342
|
+
status: enumText('status', ['draft', 'published']).default('draft'),
|
|
343
|
+
metadata: typedJsonb<{ views: number }>('metadata'),
|
|
344
|
+
...timestamps(),
|
|
345
|
+
...softDelete()
|
|
346
|
+
});
|
|
474
347
|
```
|
|
475
348
|
|
|
476
|
-
|
|
477
|
-
```typescript
|
|
478
|
-
import { initRedis, getRedis, getRedisRead } from '@spfn/core';
|
|
479
|
-
```
|
|
349
|
+
---
|
|
480
350
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
351
|
+
## Module Documentation
|
|
352
|
+
|
|
353
|
+
| Module | Description |
|
|
354
|
+
|--------|-------------|
|
|
355
|
+
| [Route](./docs/route.md) | Route definition, validation, response patterns |
|
|
356
|
+
| [Database](./docs/database.md) | Connection, helpers, transactions |
|
|
357
|
+
| [Entity](./docs/entity.md) | Schema definition, column helpers |
|
|
358
|
+
| [Repository](./docs/repository.md) | BaseRepository, CRUD patterns |
|
|
359
|
+
| [Middleware](./docs/middleware.md) | Named middleware, skip control |
|
|
360
|
+
| [Server](./docs/server.md) | Configuration, lifecycle hooks |
|
|
361
|
+
| [Errors](./docs/errors.md) | Error types, handling patterns |
|
|
362
|
+
| [Environment](./docs/env.md) | Type-safe environment variables |
|
|
363
|
+
| [Codegen](./docs/codegen.md) | API client generation |
|
|
364
|
+
| [Cache](./docs/cache.md) | Redis caching |
|
|
365
|
+
| [Event](./docs/event.md) | Event system |
|
|
366
|
+
| [Job](./docs/job.md) | Background jobs |
|
|
367
|
+
| [Logger](./docs/logger.md) | Logging |
|
|
368
|
+
| [Next.js](./docs/nextjs.md) | Next.js integration, RPC proxy |
|
|
485
369
|
|
|
486
|
-
|
|
487
|
-
```typescript
|
|
488
|
-
import { ContractClient, createClient } from '@spfn/core/client';
|
|
489
|
-
```
|
|
370
|
+
---
|
|
490
371
|
|
|
491
|
-
##
|
|
372
|
+
## CLI Commands
|
|
492
373
|
|
|
493
374
|
```bash
|
|
494
|
-
#
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
# Database Read Replica (optional)
|
|
498
|
-
DATABASE_READ_URL=postgresql://user:pass@replica:5432/db
|
|
499
|
-
|
|
500
|
-
# Redis (optional)
|
|
501
|
-
REDIS_URL=redis://localhost:6379
|
|
502
|
-
REDIS_WRITE_URL=redis://master:6379 # Master-replica setup
|
|
503
|
-
REDIS_READ_URL=redis://replica:6379
|
|
504
|
-
|
|
505
|
-
# Server
|
|
506
|
-
PORT=8790
|
|
507
|
-
HOST=localhost
|
|
508
|
-
NODE_ENV=development
|
|
509
|
-
|
|
510
|
-
# Server Timeouts (optional, in milliseconds)
|
|
511
|
-
SERVER_TIMEOUT=120000 # Request timeout (default: 120000)
|
|
512
|
-
SERVER_KEEPALIVE_TIMEOUT=65000 # Keep-alive timeout (default: 65000)
|
|
513
|
-
SERVER_HEADERS_TIMEOUT=60000 # Headers timeout (default: 60000)
|
|
514
|
-
SHUTDOWN_TIMEOUT=30000 # Graceful shutdown timeout (default: 30000)
|
|
515
|
-
|
|
516
|
-
# Logger (optional)
|
|
517
|
-
LOGGER_ADAPTER=pino # pino | custom (default: pino)
|
|
518
|
-
LOGGER_FILE_ENABLED=true # Enable file logging (production only)
|
|
519
|
-
LOG_DIR=/var/log/myapp # Log directory (required when file logging enabled)
|
|
520
|
-
```
|
|
521
|
-
|
|
522
|
-
## Requirements
|
|
375
|
+
# Migration
|
|
376
|
+
npx spfn db generate # Generate migration
|
|
377
|
+
npx spfn db migrate # Apply migration
|
|
523
378
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
- PostgreSQL
|
|
527
|
-
- Redis (optional)
|
|
379
|
+
# Development
|
|
380
|
+
pnpm spfn:dev # Start dev server (auto codegen)
|
|
528
381
|
|
|
529
|
-
|
|
382
|
+
# API Client Generation
|
|
383
|
+
pnpm spfn codegen run
|
|
530
384
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
npm test -- route # Run route tests only
|
|
534
|
-
npm test -- --coverage # With coverage
|
|
385
|
+
# Database Studio
|
|
386
|
+
pnpm drizzle-kit studio
|
|
535
387
|
```
|
|
536
388
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
## Documentation
|
|
540
|
-
|
|
541
|
-
### Guides
|
|
542
|
-
- [File-based Routing](./src/route/README.md)
|
|
543
|
-
- [Database & Helper Functions](./src/db/README.md)
|
|
544
|
-
- [Transaction Management](./src/db/docs/transactions.md)
|
|
545
|
-
- [Redis Cache](./src/cache/README.md)
|
|
546
|
-
- [Error Handling](./src/errors/README.md)
|
|
547
|
-
- [Middleware](./src/middleware/README.md)
|
|
548
|
-
- [Server Configuration](./src/server/README.md)
|
|
549
|
-
- [Logger](./src/logger/README.md)
|
|
550
|
-
- [Code Generation](./src/codegen/README.md)
|
|
551
|
-
|
|
552
|
-
### API Reference
|
|
553
|
-
- See module-specific README files linked above
|
|
389
|
+
---
|
|
554
390
|
|
|
555
391
|
## License
|
|
556
392
|
|
|
557
393
|
MIT
|
|
558
|
-
|
|
559
|
-
---
|
|
560
|
-
|
|
561
|
-
Part of the [Superfunction Framework](https://github.com/spfn/spfn)
|