@spfn/core 0.1.0-alpha.8 → 0.1.0-alpha.82
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 +169 -195
- package/dist/auto-loader-JFaZ9gON.d.ts +80 -0
- package/dist/cache/index.d.ts +211 -0
- package/dist/cache/index.js +1013 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/client/index.d.ts +131 -92
- package/dist/client/index.js +93 -85
- package/dist/client/index.js.map +1 -1
- package/dist/codegen/generators/index.d.ts +19 -0
- package/dist/codegen/generators/index.js +1521 -0
- package/dist/codegen/generators/index.js.map +1 -0
- package/dist/codegen/index.d.ts +76 -60
- package/dist/codegen/index.js +1506 -735
- package/dist/codegen/index.js.map +1 -1
- package/dist/database-errors-BNNmLTJE.d.ts +86 -0
- package/dist/db/index.d.ts +844 -44
- package/dist/db/index.js +1281 -1307
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +508 -0
- package/dist/env/index.js +1127 -0
- package/dist/env/index.js.map +1 -0
- package/dist/errors/index.d.ts +136 -0
- package/dist/errors/index.js +172 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/index-DHiAqhKv.d.ts +101 -0
- package/dist/index.d.ts +3 -374
- package/dist/index.js +2424 -2178
- package/dist/index.js.map +1 -1
- package/dist/logger/index.d.ts +94 -0
- package/dist/logger/index.js +795 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/middleware/index.d.ts +60 -0
- package/dist/middleware/index.js +918 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/route/index.d.ts +21 -53
- package/dist/route/index.js +1259 -219
- package/dist/route/index.js.map +1 -1
- package/dist/server/index.d.ts +18 -0
- package/dist/server/index.js +2419 -2059
- package/dist/server/index.js.map +1 -1
- package/dist/types/index.d.ts +121 -0
- package/dist/types/index.js +38 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types-BXibIEyj.d.ts +60 -0
- package/package.json +67 -17
- package/dist/auto-loader-C44TcLmM.d.ts +0 -125
- package/dist/bind-pssq1NRT.d.ts +0 -34
- package/dist/postgres-errors-CY_Es8EJ.d.ts +0 -1703
- package/dist/scripts/index.d.ts +0 -24
- package/dist/scripts/index.js +0 -1201
- package/dist/scripts/index.js.map +0 -1
- package/dist/scripts/templates/api-index.template.txt +0 -10
- package/dist/scripts/templates/api-tag.template.txt +0 -11
- package/dist/scripts/templates/contract.template.txt +0 -87
- package/dist/scripts/templates/entity-type.template.txt +0 -31
- package/dist/scripts/templates/entity.template.txt +0 -19
- package/dist/scripts/templates/index.template.txt +0 -10
- package/dist/scripts/templates/repository.template.txt +0 -37
- package/dist/scripts/templates/routes-id.template.txt +0 -59
- package/dist/scripts/templates/routes-index.template.txt +0 -44
- package/dist/types-SlzTr8ZO.d.ts +0 -143
package/README.md
CHANGED
|
@@ -58,7 +58,7 @@ export const getUsersContract = {
|
|
|
58
58
|
// src/server/routes/users/index.ts
|
|
59
59
|
import { createApp } from '@spfn/core/route';
|
|
60
60
|
import { getUsersContract } from './contract.js';
|
|
61
|
-
import {
|
|
61
|
+
import { findMany } from '@spfn/core/db';
|
|
62
62
|
import { users } from '../../entities/users.js';
|
|
63
63
|
|
|
64
64
|
const app = createApp();
|
|
@@ -66,14 +66,11 @@ const app = createApp();
|
|
|
66
66
|
app.bind(getUsersContract, async (c) => {
|
|
67
67
|
const { page = 1, limit = 10 } = c.query;
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
const
|
|
69
|
+
// Use helper function directly - no Repository needed
|
|
70
|
+
const offset = (page - 1) * limit;
|
|
71
|
+
const result = await findMany(users, { limit, offset });
|
|
71
72
|
|
|
72
|
-
|
|
73
|
-
pagination: { page, limit }
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
return c.json(result);
|
|
73
|
+
return c.json({ users: result, total: result.length });
|
|
77
74
|
});
|
|
78
75
|
|
|
79
76
|
export default app;
|
|
@@ -88,7 +85,7 @@ npm run spfn:dev
|
|
|
88
85
|
|
|
89
86
|
## Architecture Pattern
|
|
90
87
|
|
|
91
|
-
|
|
88
|
+
Superfunction follows a **layered architecture** that separates concerns and keeps code maintainable:
|
|
92
89
|
|
|
93
90
|
```
|
|
94
91
|
┌─────────────────────────────────────────┐
|
|
@@ -102,14 +99,14 @@ SPFN follows a **layered architecture** that separates concerns and keeps code m
|
|
|
102
99
|
│ Service Layer │ Business logic
|
|
103
100
|
│ - Orchestrate operations │
|
|
104
101
|
│ - Implement business rules │
|
|
105
|
-
│ - Use
|
|
102
|
+
│ - Use helper functions or custom logic │
|
|
106
103
|
└──────────────┬──────────────────────────┘
|
|
107
104
|
│
|
|
108
105
|
┌──────────────▼──────────────────────────┐
|
|
109
|
-
│
|
|
110
|
-
│ -
|
|
111
|
-
│ - Custom queries
|
|
112
|
-
│ -
|
|
106
|
+
│ Data Access Layer │ Database operations
|
|
107
|
+
│ - Use helper functions (findOne, etc) │
|
|
108
|
+
│ - Custom queries with Drizzle │
|
|
109
|
+
│ - Domain-specific wrappers │
|
|
113
110
|
└──────────────┬──────────────────────────┘
|
|
114
111
|
│
|
|
115
112
|
┌──────────────▼──────────────────────────┐
|
|
@@ -144,134 +141,35 @@ export type Post = typeof posts.$inferSelect;
|
|
|
144
141
|
export type NewPost = typeof posts.$inferInsert;
|
|
145
142
|
```
|
|
146
143
|
|
|
147
|
-
**2.
|
|
144
|
+
**2. Data Access Layer** - Helper functions with domain-specific wrappers
|
|
148
145
|
|
|
149
146
|
```typescript
|
|
150
147
|
// src/server/repositories/posts.repository.ts
|
|
151
|
-
import {
|
|
152
|
-
import {
|
|
153
|
-
import { posts } from '../entities';
|
|
154
|
-
import type { Post } from '../entities';
|
|
155
|
-
|
|
156
|
-
export class PostRepository extends Repository<typeof posts>
|
|
157
|
-
{
|
|
158
|
-
async findBySlug(slug: string): Promise<Post | null>
|
|
159
|
-
{
|
|
160
|
-
return this.findOne(eq(this.table.slug, slug));
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
async findPublished(): Promise<Post[]>
|
|
164
|
-
{
|
|
165
|
-
const results = await this.db
|
|
166
|
-
.select()
|
|
167
|
-
.from(this.table)
|
|
168
|
-
.where(eq(this.table.status, 'published'))
|
|
169
|
-
.orderBy(this.table.createdAt);
|
|
170
|
-
|
|
171
|
-
return results;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
```
|
|
175
|
-
|
|
176
|
-
**3. Service Layer** - Business logic (Function-based pattern)
|
|
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';
|
|
177
151
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
import { ValidationError, DatabaseError, NotFoundError } from '@spfn/core';
|
|
182
|
-
import { posts } from '../entities';
|
|
183
|
-
import { PostRepository } from '../repositories/posts.repository';
|
|
184
|
-
import type { NewPost, Post } from '../entities';
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Create a new post
|
|
188
|
-
*/
|
|
189
|
-
export async function createPost(data: {
|
|
190
|
-
title: string;
|
|
191
|
-
content: string;
|
|
192
|
-
}): Promise<Post> {
|
|
193
|
-
try {
|
|
194
|
-
// Get repository singleton
|
|
195
|
-
const repo = getRepository(posts, PostRepository);
|
|
196
|
-
|
|
197
|
-
// Business logic: Generate slug from title
|
|
198
|
-
const slug = generateSlug(data.title);
|
|
199
|
-
|
|
200
|
-
// Validation: Check if slug already exists
|
|
201
|
-
const existing = await repo.findBySlug(slug);
|
|
202
|
-
if (existing) {
|
|
203
|
-
throw new ValidationError('Post with this title already exists', {
|
|
204
|
-
fields: [{
|
|
205
|
-
path: '/title',
|
|
206
|
-
message: 'A post with this title already exists',
|
|
207
|
-
value: data.title
|
|
208
|
-
}]
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Create post
|
|
213
|
-
return await repo.save({
|
|
214
|
-
...data,
|
|
215
|
-
slug,
|
|
216
|
-
status: 'draft',
|
|
217
|
-
});
|
|
218
|
-
} catch (error) {
|
|
219
|
-
// Re-throw ValidationError as-is
|
|
220
|
-
if (error instanceof ValidationError) {
|
|
221
|
-
throw error;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
// Wrap unexpected errors
|
|
225
|
-
throw new DatabaseError('Failed to create post', 500, {
|
|
226
|
-
originalError: error instanceof Error ? error.message : String(error)
|
|
227
|
-
});
|
|
228
|
-
}
|
|
152
|
+
// Domain-specific wrappers using helper functions
|
|
153
|
+
export async function findPostBySlug(slug: string): Promise<Post | null> {
|
|
154
|
+
return findOne(posts, { slug });
|
|
229
155
|
}
|
|
230
156
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
const repo = getRepository(posts, PostRepository);
|
|
237
|
-
const post = await repo.update(id, { status: 'published' });
|
|
238
|
-
|
|
239
|
-
if (!post) {
|
|
240
|
-
throw new NotFoundError('Post not found');
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
return post;
|
|
244
|
-
} catch (error) {
|
|
245
|
-
if (error instanceof NotFoundError) {
|
|
246
|
-
throw error;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
throw new DatabaseError('Failed to publish post', 500, {
|
|
250
|
-
originalError: error instanceof Error ? error.message : String(error)
|
|
251
|
-
});
|
|
252
|
-
}
|
|
157
|
+
export async function findPublishedPosts(): Promise<Post[]> {
|
|
158
|
+
return findMany(posts, {
|
|
159
|
+
where: { status: 'published' },
|
|
160
|
+
orderBy: desc(posts.createdAt)
|
|
161
|
+
});
|
|
253
162
|
}
|
|
254
163
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
*/
|
|
258
|
-
export async function getPublishedPosts(): Promise<Post[]> {
|
|
259
|
-
const repo = getRepository(posts, PostRepository);
|
|
260
|
-
return repo.findPublished();
|
|
164
|
+
export async function createPost(data: NewPost): Promise<Post> {
|
|
165
|
+
return createHelper(posts, data);
|
|
261
166
|
}
|
|
262
167
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
*/
|
|
266
|
-
function generateSlug(title: string): string {
|
|
267
|
-
return title
|
|
268
|
-
.toLowerCase()
|
|
269
|
-
.replace(/[^a-z0-9]+/g, '-')
|
|
270
|
-
.replace(/(^-|-$)/g, '');
|
|
271
|
-
}
|
|
168
|
+
// Or use helper functions directly in routes for simple cases
|
|
169
|
+
// const post = await findOne(posts, { id: 1 });
|
|
272
170
|
```
|
|
273
171
|
|
|
274
|
-
**
|
|
172
|
+
**3. Routes Layer** - HTTP API
|
|
275
173
|
|
|
276
174
|
```typescript
|
|
277
175
|
// src/server/routes/posts/contracts.ts
|
|
@@ -306,22 +204,41 @@ export const listPostsContract = {
|
|
|
306
204
|
// src/server/routes/posts/index.ts
|
|
307
205
|
import { createApp } from '@spfn/core/route';
|
|
308
206
|
import { Transactional } from '@spfn/core/db';
|
|
309
|
-
import {
|
|
207
|
+
import { ConflictError } from '@spfn/core/errors';
|
|
208
|
+
import { findPostBySlug, createPost, findPublishedPosts } from '../../repositories/posts.repository';
|
|
310
209
|
import { createPostContract, listPostsContract } from './contracts';
|
|
311
210
|
|
|
312
211
|
const app = createApp();
|
|
313
212
|
|
|
314
213
|
// POST /posts - Create new post (with transaction)
|
|
315
|
-
app.bind(createPostContract, Transactional(), async (c) => {
|
|
214
|
+
app.bind(createPostContract, [Transactional()], async (c) => {
|
|
316
215
|
const body = await c.data();
|
|
317
|
-
|
|
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
|
+
|
|
318
235
|
// ✅ Auto-commit on success, auto-rollback on error
|
|
319
236
|
return c.json(post, 201);
|
|
320
237
|
});
|
|
321
238
|
|
|
322
239
|
// GET /posts - List published posts (no transaction needed)
|
|
323
240
|
app.bind(listPostsContract, async (c) => {
|
|
324
|
-
const posts = await
|
|
241
|
+
const posts = await findPublishedPosts();
|
|
325
242
|
return c.json(posts);
|
|
326
243
|
});
|
|
327
244
|
|
|
@@ -340,10 +257,10 @@ export default app;
|
|
|
340
257
|
|
|
341
258
|
**✅ Reusability**
|
|
342
259
|
- Services can be used by multiple routes
|
|
343
|
-
-
|
|
260
|
+
- Data access functions can be shared across services
|
|
344
261
|
|
|
345
262
|
**✅ Type Safety**
|
|
346
|
-
- Types flow from Entity →
|
|
263
|
+
- Types flow from Entity → Data Access → Service → Route
|
|
347
264
|
- Full IDE autocomplete and error checking
|
|
348
265
|
|
|
349
266
|
**✅ Maintainability**
|
|
@@ -355,7 +272,7 @@ export default app;
|
|
|
355
272
|
| Layer | Responsibility | Examples |
|
|
356
273
|
|-------|---------------|----------|
|
|
357
274
|
| **Entity** | Define data structure | Schema, types, constraints |
|
|
358
|
-
| **
|
|
275
|
+
| **Data Access** | Database operations | Helper functions, custom queries, joins |
|
|
359
276
|
| **Service** | Business logic | Validation, orchestration, rules |
|
|
360
277
|
| **Routes** | HTTP interface | Contracts, request handling |
|
|
361
278
|
|
|
@@ -366,21 +283,16 @@ export default app;
|
|
|
366
283
|
- ✅ Export inferred types: `Post`, `NewPost`
|
|
367
284
|
- ✅ Use TEXT with enum for status fields
|
|
368
285
|
|
|
369
|
-
**
|
|
370
|
-
- ✅
|
|
371
|
-
- ✅
|
|
372
|
-
- ✅
|
|
373
|
-
- ✅
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
- ✅ Use function-based pattern (export async functions)
|
|
377
|
-
- ✅ Get repositories via `getRepository()` (singleton)
|
|
378
|
-
- ✅ Implement business logic and validation
|
|
379
|
-
- ✅ Throw descriptive errors
|
|
380
|
-
- ✅ Keep functions focused and small
|
|
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
|
|
381
293
|
|
|
382
294
|
**Routes Layer:**
|
|
383
|
-
- ✅ Keep handlers thin (delegate to services)
|
|
295
|
+
- ✅ Keep handlers thin (delegate to services/data access)
|
|
384
296
|
- ✅ Define contracts with TypeBox
|
|
385
297
|
- ✅ Use `Transactional()` middleware for write operations
|
|
386
298
|
- ✅ Use `c.data()` for validated input
|
|
@@ -399,61 +311,75 @@ File-based routing with contract validation and type safety.
|
|
|
399
311
|
- Type-safe request/response handling
|
|
400
312
|
- Method-level middleware control (skip auth per HTTP method)
|
|
401
313
|
|
|
402
|
-
### 🗄️ Database
|
|
403
|
-
Drizzle ORM integration with
|
|
314
|
+
### 🗄️ Database
|
|
315
|
+
Drizzle ORM integration with type-safe helper functions and automatic transaction handling.
|
|
404
316
|
|
|
405
317
|
**[→ Read Database Documentation](./src/db/README.md)**
|
|
406
318
|
|
|
407
|
-
**
|
|
408
|
-
-
|
|
409
|
-
-
|
|
410
|
-
-
|
|
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.
|
|
411
328
|
|
|
412
|
-
|
|
329
|
+
**[→ Read Database Manager Documentation](./src/db/manager/README.md)**
|
|
413
330
|
|
|
414
|
-
|
|
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
|
|
415
337
|
|
|
416
|
-
**
|
|
417
|
-
```typescript
|
|
418
|
-
import { getRepository } from '@spfn/core/db';
|
|
338
|
+
**How it works:**
|
|
419
339
|
|
|
420
|
-
|
|
340
|
+
Functions declare their schemas in `package.json`:
|
|
341
|
+
```json
|
|
342
|
+
{
|
|
343
|
+
"name": "@spfn/cms",
|
|
344
|
+
"spfn": {
|
|
345
|
+
"schemas": ["./dist/entities/*.js"],
|
|
346
|
+
"setupMessage": "📚 Setup guide..."
|
|
347
|
+
}
|
|
348
|
+
}
|
|
421
349
|
```
|
|
422
350
|
|
|
423
|
-
|
|
424
|
-
- ✅ Maximum memory efficiency
|
|
425
|
-
- ⚠️ Requires manual `clearRepositoryCache()` in tests
|
|
426
|
-
- ⚠️ Global state across all requests
|
|
427
|
-
- 📝 **Use for:** Simple projects, prototypes, single-instance services
|
|
428
|
-
|
|
429
|
-
**Request-Scoped (`getScopedRepository` + `RepositoryScope()`)** ⭐ Recommended
|
|
351
|
+
Superfunction automatically discovers and merges these schemas during migration generation:
|
|
430
352
|
```typescript
|
|
431
|
-
import {
|
|
353
|
+
import { getDrizzleConfig } from '@spfn/core'
|
|
432
354
|
|
|
433
|
-
//
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
// Use in routes/services
|
|
437
|
-
const repo = getScopedRepository(users);
|
|
355
|
+
// Auto-discovers all function schemas
|
|
356
|
+
const config = getDrizzleConfig()
|
|
438
357
|
```
|
|
439
358
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
359
|
+
**Install functions with automatic DB setup:**
|
|
360
|
+
```bash
|
|
361
|
+
pnpm spfn add @spfn/cms
|
|
362
|
+
# ✅ Installs function
|
|
363
|
+
# ✅ Generates migrations
|
|
364
|
+
# ✅ Applies migrations
|
|
365
|
+
# ✅ Shows setup guide
|
|
366
|
+
```
|
|
445
367
|
|
|
446
|
-
**
|
|
368
|
+
**Create your own Superfunction packages:**
|
|
369
|
+
```typescript
|
|
370
|
+
// 1. Define entities
|
|
371
|
+
export const myTable = pgTable('my_table', { ... })
|
|
447
372
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
| Best for | Prototypes | Production |
|
|
373
|
+
// 2. Add to package.json
|
|
374
|
+
{
|
|
375
|
+
"spfn": {
|
|
376
|
+
"schemas": ["./dist/entities/*.js"]
|
|
377
|
+
}
|
|
378
|
+
}
|
|
455
379
|
|
|
456
|
-
|
|
380
|
+
// 3. Users install with one command
|
|
381
|
+
// pnpm spfn add @yourcompany/spfn-plugin
|
|
382
|
+
```
|
|
457
383
|
|
|
458
384
|
### 🔄 Transactions
|
|
459
385
|
Automatic transaction management with async context propagation.
|
|
@@ -485,6 +411,30 @@ Server configuration and lifecycle management.
|
|
|
485
411
|
|
|
486
412
|
**[→ Read Server Documentation](./src/server/README.md)**
|
|
487
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)**
|
|
430
|
+
|
|
431
|
+
**Key Features:**
|
|
432
|
+
- Orchestrator pattern for managing multiple generators
|
|
433
|
+
- Built-in contract generator for type-safe API clients
|
|
434
|
+
- Configuration-based setup (`.spfnrc.json` or `package.json`)
|
|
435
|
+
- Watch mode integrated into `spfn dev`
|
|
436
|
+
- Extensible with custom generators
|
|
437
|
+
|
|
488
438
|
## Module Exports
|
|
489
439
|
|
|
490
440
|
### Main Export
|
|
@@ -501,11 +451,17 @@ import type { RouteContext, RouteContract } from '@spfn/core/route';
|
|
|
501
451
|
### Database
|
|
502
452
|
```typescript
|
|
503
453
|
import {
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
454
|
+
getDatabase,
|
|
455
|
+
findOne,
|
|
456
|
+
findMany,
|
|
457
|
+
create,
|
|
458
|
+
createMany,
|
|
459
|
+
updateOne,
|
|
460
|
+
updateMany,
|
|
461
|
+
deleteOne,
|
|
462
|
+
deleteMany,
|
|
463
|
+
count
|
|
507
464
|
} from '@spfn/core/db';
|
|
508
|
-
import type { Pageable, Page } from '@spfn/core/db';
|
|
509
465
|
```
|
|
510
466
|
|
|
511
467
|
### Transactions
|
|
@@ -522,6 +478,11 @@ import {
|
|
|
522
478
|
import { initRedis, getRedis, getRedisRead } from '@spfn/core';
|
|
523
479
|
```
|
|
524
480
|
|
|
481
|
+
### Logger
|
|
482
|
+
```typescript
|
|
483
|
+
import { logger } from '@spfn/core';
|
|
484
|
+
```
|
|
485
|
+
|
|
525
486
|
### Client (for frontend)
|
|
526
487
|
```typescript
|
|
527
488
|
import { ContractClient, createClient } from '@spfn/core/client';
|
|
@@ -545,6 +506,17 @@ REDIS_READ_URL=redis://replica:6379
|
|
|
545
506
|
PORT=8790
|
|
546
507
|
HOST=localhost
|
|
547
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)
|
|
548
520
|
```
|
|
549
521
|
|
|
550
522
|
## Requirements
|
|
@@ -568,12 +540,14 @@ npm test -- --coverage # With coverage
|
|
|
568
540
|
|
|
569
541
|
### Guides
|
|
570
542
|
- [File-based Routing](./src/route/README.md)
|
|
571
|
-
- [Database &
|
|
543
|
+
- [Database & Helper Functions](./src/db/README.md)
|
|
572
544
|
- [Transaction Management](./src/db/docs/transactions.md)
|
|
573
545
|
- [Redis Cache](./src/cache/README.md)
|
|
574
546
|
- [Error Handling](./src/errors/README.md)
|
|
575
547
|
- [Middleware](./src/middleware/README.md)
|
|
576
548
|
- [Server Configuration](./src/server/README.md)
|
|
549
|
+
- [Logger](./src/logger/README.md)
|
|
550
|
+
- [Code Generation](./src/codegen/README.md)
|
|
577
551
|
|
|
578
552
|
### API Reference
|
|
579
553
|
- See module-specific README files linked above
|
|
@@ -584,4 +558,4 @@ MIT
|
|
|
584
558
|
|
|
585
559
|
---
|
|
586
560
|
|
|
587
|
-
Part of the [
|
|
561
|
+
Part of the [Superfunction Framework](https://github.com/spfn/spfn)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { MiddlewareHandler, Hono } from 'hono';
|
|
2
|
+
|
|
3
|
+
declare module 'hono' {
|
|
4
|
+
interface ContextVariableMap {
|
|
5
|
+
_skipMiddlewares?: string[];
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* AutoRouteLoader: Simplified File-based Routing System
|
|
10
|
+
*
|
|
11
|
+
* Features:
|
|
12
|
+
* - Auto-discovery: Scans routes directory and auto-registers
|
|
13
|
+
* - Dynamic routes: [id] → :id, [...slug] → *
|
|
14
|
+
* - Statistics: Route registration stats for dashboard
|
|
15
|
+
* - Grouping: Natural grouping by directory structure
|
|
16
|
+
*/
|
|
17
|
+
type RouteInfo = {
|
|
18
|
+
path: string;
|
|
19
|
+
file: string;
|
|
20
|
+
meta?: {
|
|
21
|
+
description?: string;
|
|
22
|
+
tags?: string[];
|
|
23
|
+
auth?: boolean;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
};
|
|
26
|
+
priority: number;
|
|
27
|
+
};
|
|
28
|
+
type RouteStats = {
|
|
29
|
+
total: number;
|
|
30
|
+
byPriority: {
|
|
31
|
+
static: number;
|
|
32
|
+
dynamic: number;
|
|
33
|
+
catchAll: number;
|
|
34
|
+
};
|
|
35
|
+
byTag: Record<string, number>;
|
|
36
|
+
routes: RouteInfo[];
|
|
37
|
+
};
|
|
38
|
+
declare class AutoRouteLoader {
|
|
39
|
+
private routesDir;
|
|
40
|
+
private routes;
|
|
41
|
+
private readonly debug;
|
|
42
|
+
private readonly middlewares;
|
|
43
|
+
constructor(routesDir: string, debug?: boolean, middlewares?: Array<{
|
|
44
|
+
name: string;
|
|
45
|
+
handler: MiddlewareHandler;
|
|
46
|
+
}>);
|
|
47
|
+
load(app: Hono): Promise<RouteStats>;
|
|
48
|
+
/**
|
|
49
|
+
* Load routes from an external directory (e.g., from SPFN function packages)
|
|
50
|
+
* Reads package.json spfn.prefix and mounts routes under that prefix
|
|
51
|
+
*
|
|
52
|
+
* @param app - Hono app instance
|
|
53
|
+
* @param routesDir - Directory containing route handlers
|
|
54
|
+
* @param packageName - Name of the package (for logging)
|
|
55
|
+
* @param prefix - Optional prefix to mount routes under (from package.json spfn.prefix)
|
|
56
|
+
* @returns Route statistics
|
|
57
|
+
*/
|
|
58
|
+
loadExternalRoutes(app: Hono, routesDir: string, packageName: string, prefix?: string): Promise<RouteStats>;
|
|
59
|
+
getStats(): RouteStats;
|
|
60
|
+
private scanFiles;
|
|
61
|
+
private isValidRouteFile;
|
|
62
|
+
private loadRoute;
|
|
63
|
+
private extractContractPaths;
|
|
64
|
+
private calculateContractPriority;
|
|
65
|
+
private validateModule;
|
|
66
|
+
private registerContractBasedMiddlewares;
|
|
67
|
+
private categorizeAndLogError;
|
|
68
|
+
private logStats;
|
|
69
|
+
}
|
|
70
|
+
declare function loadRoutes(app: Hono, options?: {
|
|
71
|
+
routesDir?: string;
|
|
72
|
+
debug?: boolean;
|
|
73
|
+
middlewares?: Array<{
|
|
74
|
+
name: string;
|
|
75
|
+
handler: MiddlewareHandler;
|
|
76
|
+
}>;
|
|
77
|
+
includeFunctionRoutes?: boolean;
|
|
78
|
+
}): Promise<RouteStats>;
|
|
79
|
+
|
|
80
|
+
export { AutoRouteLoader as A, type RouteInfo as R, type RouteStats as a, loadRoutes as l };
|