@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.
Files changed (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +580 -0
  3. package/dist/auto-loader-C44TcLmM.d.ts +125 -0
  4. package/dist/bind-pssq1NRT.d.ts +34 -0
  5. package/dist/client/index.d.ts +174 -0
  6. package/dist/client/index.js +179 -0
  7. package/dist/client/index.js.map +1 -0
  8. package/dist/codegen/index.d.ts +126 -0
  9. package/dist/codegen/index.js +970 -0
  10. package/dist/codegen/index.js.map +1 -0
  11. package/dist/db/index.d.ts +83 -0
  12. package/dist/db/index.js +2099 -0
  13. package/dist/db/index.js.map +1 -0
  14. package/dist/index.d.ts +379 -0
  15. package/dist/index.js +13042 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/postgres-errors-CY_Es8EJ.d.ts +1703 -0
  18. package/dist/route/index.d.ts +72 -0
  19. package/dist/route/index.js +442 -0
  20. package/dist/route/index.js.map +1 -0
  21. package/dist/scripts/index.d.ts +24 -0
  22. package/dist/scripts/index.js +1157 -0
  23. package/dist/scripts/index.js.map +1 -0
  24. package/dist/scripts/templates/api-index.template.txt +10 -0
  25. package/dist/scripts/templates/api-tag.template.txt +11 -0
  26. package/dist/scripts/templates/contract.template.txt +87 -0
  27. package/dist/scripts/templates/entity-type.template.txt +31 -0
  28. package/dist/scripts/templates/entity.template.txt +19 -0
  29. package/dist/scripts/templates/index.template.txt +10 -0
  30. package/dist/scripts/templates/repository.template.txt +37 -0
  31. package/dist/scripts/templates/routes-id.template.txt +59 -0
  32. package/dist/scripts/templates/routes-index.template.txt +44 -0
  33. package/dist/server/index.d.ts +303 -0
  34. package/dist/server/index.js +12923 -0
  35. package/dist/server/index.js.map +1 -0
  36. package/dist/types-SlzTr8ZO.d.ts +143 -0
  37. 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
+ [![npm version](https://badge.fury.io/js/@spfn%2Fcore.svg)](https://www.npmjs.com/package/@spfn/core)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue)](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)