@spfn/core 0.1.0-alpha.1
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 +21 -0
- package/README.md +580 -0
- package/dist/auto-loader-C44TcLmM.d.ts +125 -0
- package/dist/bind-pssq1NRT.d.ts +34 -0
- package/dist/client/index.d.ts +174 -0
- package/dist/client/index.js +179 -0
- package/dist/client/index.js.map +1 -0
- package/dist/codegen/index.d.ts +126 -0
- package/dist/codegen/index.js +970 -0
- package/dist/codegen/index.js.map +1 -0
- package/dist/db/index.d.ts +83 -0
- package/dist/db/index.js +2099 -0
- package/dist/db/index.js.map +1 -0
- package/dist/index.d.ts +379 -0
- package/dist/index.js +13042 -0
- package/dist/index.js.map +1 -0
- package/dist/postgres-errors-CY_Es8EJ.d.ts +1703 -0
- package/dist/route/index.d.ts +72 -0
- package/dist/route/index.js +442 -0
- package/dist/route/index.js.map +1 -0
- package/dist/scripts/index.d.ts +24 -0
- package/dist/scripts/index.js +1157 -0
- package/dist/scripts/index.js.map +1 -0
- package/dist/scripts/templates/api-index.template.txt +10 -0
- package/dist/scripts/templates/api-tag.template.txt +11 -0
- package/dist/scripts/templates/contract.template.txt +87 -0
- package/dist/scripts/templates/entity-type.template.txt +31 -0
- package/dist/scripts/templates/entity.template.txt +19 -0
- package/dist/scripts/templates/index.template.txt +10 -0
- package/dist/scripts/templates/repository.template.txt +37 -0
- package/dist/scripts/templates/routes-id.template.txt +59 -0
- package/dist/scripts/templates/routes-index.template.txt +44 -0
- package/dist/server/index.d.ts +303 -0
- package/dist/server/index.js +12923 -0
- package/dist/server/index.js.map +1 -0
- package/dist/types-SlzTr8ZO.d.ts +143 -0
- package/package.json +119 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 INFLIKE Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
# @spfn/core
|
|
2
|
+
|
|
3
|
+
> Core framework for building type-safe backend APIs with Next.js and Hono
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@spfn/core)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
[](https://www.typescriptlang.org/)
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
**Recommended: Use CLI**
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g @spfn/cli
|
|
14
|
+
spfn init
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**Manual Installation**
|
|
18
|
+
```bash
|
|
19
|
+
npm install @spfn/core hono drizzle-orm postgres @sinclair/typebox
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Start
|
|
23
|
+
|
|
24
|
+
### 1. Define a Contract
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
// src/server/routes/users/contract.ts
|
|
28
|
+
import { Type } from '@sinclair/typebox';
|
|
29
|
+
|
|
30
|
+
export const getUsersContract = {
|
|
31
|
+
method: 'GET' as const,
|
|
32
|
+
path: '/',
|
|
33
|
+
query: Type.Object({
|
|
34
|
+
page: Type.Optional(Type.Number()),
|
|
35
|
+
limit: Type.Optional(Type.Number()),
|
|
36
|
+
}),
|
|
37
|
+
response: Type.Object({
|
|
38
|
+
users: Type.Array(Type.Object({
|
|
39
|
+
id: Type.Number(),
|
|
40
|
+
name: Type.String(),
|
|
41
|
+
email: Type.String(),
|
|
42
|
+
})),
|
|
43
|
+
total: Type.Number(),
|
|
44
|
+
}),
|
|
45
|
+
};
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. Create a Route
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// src/server/routes/users/index.ts
|
|
52
|
+
import { createApp } from '@spfn/core/route';
|
|
53
|
+
import { getUsersContract } from './contract.js';
|
|
54
|
+
import { getRepository } from '@spfn/core/db';
|
|
55
|
+
import { users } from '../../entities/users.js';
|
|
56
|
+
|
|
57
|
+
const app = createApp();
|
|
58
|
+
|
|
59
|
+
app.bind(getUsersContract, async (c) => {
|
|
60
|
+
const { page = 1, limit = 10 } = c.query;
|
|
61
|
+
|
|
62
|
+
// Get repository singleton - automatically cached
|
|
63
|
+
const repo = getRepository(users);
|
|
64
|
+
|
|
65
|
+
const result = await repo.findPage({
|
|
66
|
+
pagination: { page, limit }
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
return c.json(result);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
export default app;
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 3. Start Server
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
npm run spfn:dev
|
|
79
|
+
# Server starts on http://localhost:8790
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Architecture Pattern
|
|
83
|
+
|
|
84
|
+
SPFN follows a **layered architecture** that separates concerns and keeps code maintainable:
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
┌─────────────────────────────────────────┐
|
|
88
|
+
│ Routes Layer │ HTTP handlers, contracts
|
|
89
|
+
│ - Define API contracts (TypeBox) │
|
|
90
|
+
│ - Handle requests/responses │
|
|
91
|
+
│ - Thin handlers │
|
|
92
|
+
└──────────────┬──────────────────────────┘
|
|
93
|
+
│
|
|
94
|
+
┌──────────────▼──────────────────────────┐
|
|
95
|
+
│ Service Layer │ Business logic
|
|
96
|
+
│ - Orchestrate operations │
|
|
97
|
+
│ - Implement business rules │
|
|
98
|
+
│ - Use repositories │
|
|
99
|
+
└──────────────┬──────────────────────────┘
|
|
100
|
+
│
|
|
101
|
+
┌──────────────▼──────────────────────────┐
|
|
102
|
+
│ Repository Layer │ Data access
|
|
103
|
+
│ - CRUD operations │
|
|
104
|
+
│ - Custom queries │
|
|
105
|
+
│ - Extend base Repository │
|
|
106
|
+
└──────────────┬──────────────────────────┘
|
|
107
|
+
│
|
|
108
|
+
┌──────────────▼──────────────────────────┐
|
|
109
|
+
│ Entity Layer │ Database schema
|
|
110
|
+
│ - Table definitions (Drizzle) │
|
|
111
|
+
│ - Type inference │
|
|
112
|
+
│ - Schema helpers │
|
|
113
|
+
└─────────────────────────────────────────┘
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Complete Example: Blog Post System
|
|
117
|
+
|
|
118
|
+
**1. Entity Layer** - Define database schema
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// src/server/entities/posts.ts
|
|
122
|
+
import { pgTable, text } from 'drizzle-orm/pg-core';
|
|
123
|
+
import { id, timestamps } from '@spfn/core/db';
|
|
124
|
+
|
|
125
|
+
export const posts = pgTable('posts', {
|
|
126
|
+
id: id(),
|
|
127
|
+
title: text('title').notNull(),
|
|
128
|
+
slug: text('slug').notNull().unique(),
|
|
129
|
+
content: text('content').notNull(),
|
|
130
|
+
status: text('status', {
|
|
131
|
+
enum: ['draft', 'published']
|
|
132
|
+
}).notNull().default('draft'),
|
|
133
|
+
...timestamps(),
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
export type Post = typeof posts.$inferSelect;
|
|
137
|
+
export type NewPost = typeof posts.$inferInsert;
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**2. Repository Layer** - Data access with custom methods
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
// src/server/repositories/posts.repository.ts
|
|
144
|
+
import { eq } from 'drizzle-orm';
|
|
145
|
+
import { Repository } from '@spfn/core/db';
|
|
146
|
+
import { posts } from '../entities';
|
|
147
|
+
import type { Post } from '../entities';
|
|
148
|
+
|
|
149
|
+
export class PostRepository extends Repository<typeof posts>
|
|
150
|
+
{
|
|
151
|
+
async findBySlug(slug: string): Promise<Post | null>
|
|
152
|
+
{
|
|
153
|
+
return this.findOne(eq(this.table.slug, slug));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async findPublished(): Promise<Post[]>
|
|
157
|
+
{
|
|
158
|
+
const results = await this.db
|
|
159
|
+
.select()
|
|
160
|
+
.from(this.table)
|
|
161
|
+
.where(eq(this.table.status, 'published'))
|
|
162
|
+
.orderBy(this.table.createdAt);
|
|
163
|
+
|
|
164
|
+
return results;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**3. Service Layer** - Business logic (Function-based pattern)
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// src/server/services/posts.ts
|
|
173
|
+
import { getRepository } from '@spfn/core/db';
|
|
174
|
+
import { ValidationError, DatabaseError, NotFoundError } from '@spfn/core';
|
|
175
|
+
import { posts } from '../entities';
|
|
176
|
+
import { PostRepository } from '../repositories/posts.repository';
|
|
177
|
+
import type { NewPost, Post } from '../entities';
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create a new post
|
|
181
|
+
*/
|
|
182
|
+
export async function createPost(data: {
|
|
183
|
+
title: string;
|
|
184
|
+
content: string;
|
|
185
|
+
}): Promise<Post> {
|
|
186
|
+
try {
|
|
187
|
+
// Get repository singleton
|
|
188
|
+
const repo = getRepository(posts, PostRepository);
|
|
189
|
+
|
|
190
|
+
// Business logic: Generate slug from title
|
|
191
|
+
const slug = generateSlug(data.title);
|
|
192
|
+
|
|
193
|
+
// Validation: Check if slug already exists
|
|
194
|
+
const existing = await repo.findBySlug(slug);
|
|
195
|
+
if (existing) {
|
|
196
|
+
throw new ValidationError('Post with this title already exists', {
|
|
197
|
+
fields: [{
|
|
198
|
+
path: '/title',
|
|
199
|
+
message: 'A post with this title already exists',
|
|
200
|
+
value: data.title
|
|
201
|
+
}]
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Create post
|
|
206
|
+
return await repo.save({
|
|
207
|
+
...data,
|
|
208
|
+
slug,
|
|
209
|
+
status: 'draft',
|
|
210
|
+
});
|
|
211
|
+
} catch (error) {
|
|
212
|
+
// Re-throw ValidationError as-is
|
|
213
|
+
if (error instanceof ValidationError) {
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Wrap unexpected errors
|
|
218
|
+
throw new DatabaseError('Failed to create post', 500, {
|
|
219
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Publish a post
|
|
226
|
+
*/
|
|
227
|
+
export async function publishPost(id: string): Promise<Post> {
|
|
228
|
+
try {
|
|
229
|
+
const repo = getRepository(posts, PostRepository);
|
|
230
|
+
const post = await repo.update(id, { status: 'published' });
|
|
231
|
+
|
|
232
|
+
if (!post) {
|
|
233
|
+
throw new NotFoundError('Post not found');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return post;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error instanceof NotFoundError) {
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
throw new DatabaseError('Failed to publish post', 500, {
|
|
243
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get all published posts
|
|
250
|
+
*/
|
|
251
|
+
export async function getPublishedPosts(): Promise<Post[]> {
|
|
252
|
+
const repo = getRepository(posts, PostRepository);
|
|
253
|
+
return repo.findPublished();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Helper: Generate URL-friendly slug from title
|
|
258
|
+
*/
|
|
259
|
+
function generateSlug(title: string): string {
|
|
260
|
+
return title
|
|
261
|
+
.toLowerCase()
|
|
262
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
263
|
+
.replace(/(^-|-$)/g, '');
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**4. Routes Layer** - HTTP API
|
|
268
|
+
|
|
269
|
+
```typescript
|
|
270
|
+
// src/server/routes/posts/contracts.ts
|
|
271
|
+
import { Type } from '@sinclair/typebox';
|
|
272
|
+
|
|
273
|
+
export const createPostContract = {
|
|
274
|
+
method: 'POST' as const,
|
|
275
|
+
path: '/',
|
|
276
|
+
body: Type.Object({
|
|
277
|
+
title: Type.String(),
|
|
278
|
+
content: Type.String(),
|
|
279
|
+
}),
|
|
280
|
+
response: Type.Object({
|
|
281
|
+
id: Type.String(),
|
|
282
|
+
title: Type.String(),
|
|
283
|
+
slug: Type.String(),
|
|
284
|
+
}),
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
export const listPostsContract = {
|
|
288
|
+
method: 'GET' as const,
|
|
289
|
+
path: '/',
|
|
290
|
+
response: Type.Array(Type.Object({
|
|
291
|
+
id: Type.String(),
|
|
292
|
+
title: Type.String(),
|
|
293
|
+
slug: Type.String(),
|
|
294
|
+
})),
|
|
295
|
+
};
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
```typescript
|
|
299
|
+
// src/server/routes/posts/index.ts
|
|
300
|
+
import { createApp } from '@spfn/core/route';
|
|
301
|
+
import { Transactional } from '@spfn/core/db';
|
|
302
|
+
import { createPost, getPublishedPosts } from '../../services/posts';
|
|
303
|
+
import { createPostContract, listPostsContract } from './contracts';
|
|
304
|
+
|
|
305
|
+
const app = createApp();
|
|
306
|
+
|
|
307
|
+
// POST /posts - Create new post (with transaction)
|
|
308
|
+
app.bind(createPostContract, Transactional(), async (c) => {
|
|
309
|
+
const body = await c.data();
|
|
310
|
+
const post = await createPost(body);
|
|
311
|
+
// ✅ Auto-commit on success, auto-rollback on error
|
|
312
|
+
return c.json(post, 201);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// GET /posts - List published posts (no transaction needed)
|
|
316
|
+
app.bind(listPostsContract, async (c) => {
|
|
317
|
+
const posts = await getPublishedPosts();
|
|
318
|
+
return c.json(posts);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
export default app;
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### Why This Architecture?
|
|
325
|
+
|
|
326
|
+
**✅ Separation of Concerns**
|
|
327
|
+
- Each layer has a single responsibility
|
|
328
|
+
- Easy to locate and modify code
|
|
329
|
+
|
|
330
|
+
**✅ Testability**
|
|
331
|
+
- Test each layer independently
|
|
332
|
+
- Mock dependencies easily
|
|
333
|
+
|
|
334
|
+
**✅ Reusability**
|
|
335
|
+
- Services can be used by multiple routes
|
|
336
|
+
- Repositories can be shared across services
|
|
337
|
+
|
|
338
|
+
**✅ Type Safety**
|
|
339
|
+
- Types flow from Entity → Repository → Service → Route
|
|
340
|
+
- Full IDE autocomplete and error checking
|
|
341
|
+
|
|
342
|
+
**✅ Maintainability**
|
|
343
|
+
- Add features without breaking existing code
|
|
344
|
+
- Clear boundaries prevent coupling
|
|
345
|
+
|
|
346
|
+
### Layer Responsibilities
|
|
347
|
+
|
|
348
|
+
| Layer | Responsibility | Examples |
|
|
349
|
+
|-------|---------------|----------|
|
|
350
|
+
| **Entity** | Define data structure | Schema, types, constraints |
|
|
351
|
+
| **Repository** | Data access | CRUD, custom queries, joins |
|
|
352
|
+
| **Service** | Business logic | Validation, orchestration, rules |
|
|
353
|
+
| **Routes** | HTTP interface | Contracts, request handling |
|
|
354
|
+
|
|
355
|
+
### Best Practices
|
|
356
|
+
|
|
357
|
+
**Entity Layer:**
|
|
358
|
+
- ✅ Use schema helpers: `id()`, `timestamps()`
|
|
359
|
+
- ✅ Export inferred types: `Post`, `NewPost`
|
|
360
|
+
- ✅ Use TEXT with enum for status fields
|
|
361
|
+
|
|
362
|
+
**Repository Layer:**
|
|
363
|
+
- ✅ Extend `Repository<typeof table>` for custom methods
|
|
364
|
+
- ✅ Use `getRepository(table)` or `getRepository(table, CustomRepo)`
|
|
365
|
+
- ✅ Add domain-specific query methods
|
|
366
|
+
- ✅ Return typed results
|
|
367
|
+
|
|
368
|
+
**Service Layer:**
|
|
369
|
+
- ✅ Use function-based pattern (export async functions)
|
|
370
|
+
- ✅ Get repositories via `getRepository()` (singleton)
|
|
371
|
+
- ✅ Implement business logic and validation
|
|
372
|
+
- ✅ Throw descriptive errors
|
|
373
|
+
- ✅ Keep functions focused and small
|
|
374
|
+
|
|
375
|
+
**Routes Layer:**
|
|
376
|
+
- ✅ Keep handlers thin (delegate to services)
|
|
377
|
+
- ✅ Define contracts with TypeBox
|
|
378
|
+
- ✅ Use `Transactional()` middleware for write operations
|
|
379
|
+
- ✅ Use `c.data()` for validated input
|
|
380
|
+
- ✅ Return `c.json()` responses
|
|
381
|
+
|
|
382
|
+
## Core Modules
|
|
383
|
+
|
|
384
|
+
### 📁 Routing
|
|
385
|
+
File-based routing with contract validation and type safety.
|
|
386
|
+
|
|
387
|
+
**[→ Read Routing Documentation](./src/route/README.md)**
|
|
388
|
+
|
|
389
|
+
**Key Features:**
|
|
390
|
+
- Automatic route discovery (`index.ts`, `[id].ts`, `[...slug].ts`)
|
|
391
|
+
- Contract-based validation with TypeBox
|
|
392
|
+
- Type-safe request/response handling
|
|
393
|
+
- Method-level middleware control (skip auth per HTTP method)
|
|
394
|
+
|
|
395
|
+
### 🗄️ Database & Repository
|
|
396
|
+
Drizzle ORM integration with repository pattern and pagination.
|
|
397
|
+
|
|
398
|
+
**[→ Read Database Documentation](./src/db/README.md)**
|
|
399
|
+
|
|
400
|
+
**Guides:**
|
|
401
|
+
- [Repository Pattern](./src/db/docs/repository.md)
|
|
402
|
+
- [Schema Helpers](./src/db/docs/schema-helpers.md)
|
|
403
|
+
- [Database Manager](./src/db/docs/database-manager.md)
|
|
404
|
+
|
|
405
|
+
#### Choosing a Repository Pattern
|
|
406
|
+
|
|
407
|
+
SPFN offers two repository patterns. Choose based on your needs:
|
|
408
|
+
|
|
409
|
+
**Global Singleton (`getRepository`)**
|
|
410
|
+
```typescript
|
|
411
|
+
import { getRepository } from '@spfn/core/db';
|
|
412
|
+
|
|
413
|
+
const repo = getRepository(users);
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
- ✅ Simple API, minimal setup
|
|
417
|
+
- ✅ Maximum memory efficiency
|
|
418
|
+
- ⚠️ Requires manual `clearRepositoryCache()` in tests
|
|
419
|
+
- ⚠️ Global state across all requests
|
|
420
|
+
- 📝 **Use for:** Simple projects, prototypes, single-instance services
|
|
421
|
+
|
|
422
|
+
**Request-Scoped (`getScopedRepository` + `RepositoryScope()`)** ⭐ Recommended
|
|
423
|
+
```typescript
|
|
424
|
+
import { getScopedRepository, RepositoryScope } from '@spfn/core/db';
|
|
425
|
+
|
|
426
|
+
// Add middleware once (in server setup)
|
|
427
|
+
app.use(RepositoryScope());
|
|
428
|
+
|
|
429
|
+
// Use in routes/services
|
|
430
|
+
const repo = getScopedRepository(users);
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
- ✅ Automatic per-request isolation
|
|
434
|
+
- ✅ No manual cache clearing needed
|
|
435
|
+
- ✅ Test-friendly (each test gets fresh instances)
|
|
436
|
+
- ✅ Production-ready with graceful degradation
|
|
437
|
+
- 📝 **Use for:** Production apps, complex testing, team projects
|
|
438
|
+
|
|
439
|
+
**Comparison:**
|
|
440
|
+
|
|
441
|
+
| Feature | `getRepository` | `getScopedRepository` |
|
|
442
|
+
|---------|----------------|----------------------|
|
|
443
|
+
| Setup | Zero config | Add middleware |
|
|
444
|
+
| Test isolation | Manual | Automatic |
|
|
445
|
+
| Memory | Shared cache | Per-request cache |
|
|
446
|
+
| State | Global | Request-scoped |
|
|
447
|
+
| Best for | Prototypes | Production |
|
|
448
|
+
|
|
449
|
+
[→ See full request-scoped documentation](./src/db/repository/request-scope.ts)
|
|
450
|
+
|
|
451
|
+
### 🔄 Transactions
|
|
452
|
+
Automatic transaction management with async context propagation.
|
|
453
|
+
|
|
454
|
+
**[→ Read Transaction Documentation](./src/db/docs/transactions.md)**
|
|
455
|
+
|
|
456
|
+
**Key Features:**
|
|
457
|
+
- Auto-commit on success, auto-rollback on error
|
|
458
|
+
- AsyncLocalStorage-based context
|
|
459
|
+
- Transaction logging
|
|
460
|
+
|
|
461
|
+
### 💾 Cache
|
|
462
|
+
Redis integration with master-replica support.
|
|
463
|
+
|
|
464
|
+
**[→ Read Cache Documentation](./src/cache/README.md)**
|
|
465
|
+
|
|
466
|
+
### ⚠️ Error Handling
|
|
467
|
+
Custom error classes with unified HTTP responses.
|
|
468
|
+
|
|
469
|
+
**[→ Read Error Documentation](./src/errors/README.md)**
|
|
470
|
+
|
|
471
|
+
### 🔐 Middleware
|
|
472
|
+
Request logging, CORS, and error handling middleware.
|
|
473
|
+
|
|
474
|
+
**[→ Read Middleware Documentation](./src/middleware/README.md)**
|
|
475
|
+
|
|
476
|
+
### 🖥️ Server
|
|
477
|
+
Server configuration and lifecycle management.
|
|
478
|
+
|
|
479
|
+
**[→ Read Server Documentation](./src/server/README.md)**
|
|
480
|
+
|
|
481
|
+
## Module Exports
|
|
482
|
+
|
|
483
|
+
### Main Export
|
|
484
|
+
```typescript
|
|
485
|
+
import { startServer, createServer } from '@spfn/core';
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
### Routing
|
|
489
|
+
```typescript
|
|
490
|
+
import { createApp, bind, loadRoutes } from '@spfn/core/route';
|
|
491
|
+
import type { RouteContext, RouteContract } from '@spfn/core/route';
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### Database
|
|
495
|
+
```typescript
|
|
496
|
+
import {
|
|
497
|
+
getDb,
|
|
498
|
+
Repository,
|
|
499
|
+
getRepository
|
|
500
|
+
} from '@spfn/core/db';
|
|
501
|
+
import type { Pageable, Page } from '@spfn/core/db';
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Transactions
|
|
505
|
+
```typescript
|
|
506
|
+
import {
|
|
507
|
+
Transactional,
|
|
508
|
+
getTransaction,
|
|
509
|
+
runWithTransaction
|
|
510
|
+
} from '@spfn/core/db';
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### Cache
|
|
514
|
+
```typescript
|
|
515
|
+
import { initRedis, getRedis, getRedisRead } from '@spfn/core';
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Client (for frontend)
|
|
519
|
+
```typescript
|
|
520
|
+
import { ContractClient, createClient } from '@spfn/core/client';
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
## Environment Variables
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
# Database (required)
|
|
527
|
+
DATABASE_URL=postgresql://user:pass@localhost:5432/db
|
|
528
|
+
|
|
529
|
+
# Database Read Replica (optional)
|
|
530
|
+
DATABASE_READ_URL=postgresql://user:pass@replica:5432/db
|
|
531
|
+
|
|
532
|
+
# Redis (optional)
|
|
533
|
+
REDIS_URL=redis://localhost:6379
|
|
534
|
+
REDIS_WRITE_URL=redis://master:6379 # Master-replica setup
|
|
535
|
+
REDIS_READ_URL=redis://replica:6379
|
|
536
|
+
|
|
537
|
+
# Server
|
|
538
|
+
PORT=8790
|
|
539
|
+
HOST=localhost
|
|
540
|
+
NODE_ENV=development
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
## Requirements
|
|
544
|
+
|
|
545
|
+
- Node.js >= 18
|
|
546
|
+
- Next.js 15+ with App Router (when using with CLI)
|
|
547
|
+
- PostgreSQL
|
|
548
|
+
- Redis (optional)
|
|
549
|
+
|
|
550
|
+
## Testing
|
|
551
|
+
|
|
552
|
+
```bash
|
|
553
|
+
npm test # Run all tests
|
|
554
|
+
npm test -- route # Run route tests only
|
|
555
|
+
npm test -- --coverage # With coverage
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
**Test Coverage:** 120+ tests across all modules
|
|
559
|
+
|
|
560
|
+
## Documentation
|
|
561
|
+
|
|
562
|
+
### Guides
|
|
563
|
+
- [File-based Routing](./src/route/README.md)
|
|
564
|
+
- [Database & Repository](./src/db/README.md)
|
|
565
|
+
- [Transaction Management](./src/db/docs/transactions.md)
|
|
566
|
+
- [Redis Cache](./src/cache/README.md)
|
|
567
|
+
- [Error Handling](./src/errors/README.md)
|
|
568
|
+
- [Middleware](./src/middleware/README.md)
|
|
569
|
+
- [Server Configuration](./src/server/README.md)
|
|
570
|
+
|
|
571
|
+
### API Reference
|
|
572
|
+
- See module-specific README files linked above
|
|
573
|
+
|
|
574
|
+
## License
|
|
575
|
+
|
|
576
|
+
MIT
|
|
577
|
+
|
|
578
|
+
---
|
|
579
|
+
|
|
580
|
+
Part of the [SPFN Framework](https://github.com/spfn/spfn)
|