@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.
Files changed (97) hide show
  1. package/README.md +298 -466
  2. package/dist/boss-DI1r4kTS.d.ts +244 -0
  3. package/dist/cache/index.d.ts +13 -33
  4. package/dist/cache/index.js +14 -703
  5. package/dist/cache/index.js.map +1 -1
  6. package/dist/codegen/index.d.ts +214 -17
  7. package/dist/codegen/index.js +231 -1420
  8. package/dist/codegen/index.js.map +1 -1
  9. package/dist/config/index.d.ts +1227 -0
  10. package/dist/config/index.js +273 -0
  11. package/dist/config/index.js.map +1 -0
  12. package/dist/db/index.d.ts +741 -59
  13. package/dist/db/index.js +1063 -1226
  14. package/dist/db/index.js.map +1 -1
  15. package/dist/env/index.d.ts +658 -308
  16. package/dist/env/index.js +503 -928
  17. package/dist/env/index.js.map +1 -1
  18. package/dist/env/loader.d.ts +87 -0
  19. package/dist/env/loader.js +70 -0
  20. package/dist/env/loader.js.map +1 -0
  21. package/dist/errors/index.d.ts +417 -29
  22. package/dist/errors/index.js +359 -98
  23. package/dist/errors/index.js.map +1 -1
  24. package/dist/event/index.d.ts +41 -0
  25. package/dist/event/index.js +131 -0
  26. package/dist/event/index.js.map +1 -0
  27. package/dist/event/sse/client.d.ts +82 -0
  28. package/dist/event/sse/client.js +115 -0
  29. package/dist/event/sse/client.js.map +1 -0
  30. package/dist/event/sse/index.d.ts +40 -0
  31. package/dist/event/sse/index.js +92 -0
  32. package/dist/event/sse/index.js.map +1 -0
  33. package/dist/job/index.d.ts +218 -0
  34. package/dist/job/index.js +410 -0
  35. package/dist/job/index.js.map +1 -0
  36. package/dist/logger/index.d.ts +20 -79
  37. package/dist/logger/index.js +82 -387
  38. package/dist/logger/index.js.map +1 -1
  39. package/dist/middleware/index.d.ts +102 -20
  40. package/dist/middleware/index.js +51 -705
  41. package/dist/middleware/index.js.map +1 -1
  42. package/dist/nextjs/index.d.ts +120 -0
  43. package/dist/nextjs/index.js +448 -0
  44. package/dist/nextjs/index.js.map +1 -0
  45. package/dist/{client/nextjs/index.d.ts → nextjs/server.d.ts} +335 -262
  46. package/dist/nextjs/server.js +637 -0
  47. package/dist/nextjs/server.js.map +1 -0
  48. package/dist/route/index.d.ts +879 -25
  49. package/dist/route/index.js +697 -1271
  50. package/dist/route/index.js.map +1 -1
  51. package/dist/route/types.d.ts +9 -0
  52. package/dist/route/types.js +3 -0
  53. package/dist/route/types.js.map +1 -0
  54. package/dist/router-Di7ENoah.d.ts +151 -0
  55. package/dist/server/index.d.ts +345 -64
  56. package/dist/server/index.js +1174 -3233
  57. package/dist/server/index.js.map +1 -1
  58. package/dist/types-B-e_f2dQ.d.ts +121 -0
  59. package/dist/types-BGl4QL1w.d.ts +77 -0
  60. package/dist/types-BOPTApC2.d.ts +245 -0
  61. package/docs/cache.md +133 -0
  62. package/docs/codegen.md +74 -0
  63. package/docs/database.md +346 -0
  64. package/docs/entity.md +539 -0
  65. package/docs/env.md +477 -0
  66. package/docs/errors.md +319 -0
  67. package/docs/event.md +116 -0
  68. package/docs/file-upload.md +717 -0
  69. package/docs/job.md +131 -0
  70. package/docs/logger.md +108 -0
  71. package/docs/middleware.md +337 -0
  72. package/docs/nextjs.md +241 -0
  73. package/docs/repository.md +496 -0
  74. package/docs/route.md +497 -0
  75. package/docs/server.md +307 -0
  76. package/package.json +68 -48
  77. package/dist/auto-loader-JFaZ9gON.d.ts +0 -80
  78. package/dist/client/index.d.ts +0 -358
  79. package/dist/client/index.js +0 -357
  80. package/dist/client/index.js.map +0 -1
  81. package/dist/client/nextjs/index.js +0 -371
  82. package/dist/client/nextjs/index.js.map +0 -1
  83. package/dist/codegen/generators/index.d.ts +0 -19
  84. package/dist/codegen/generators/index.js +0 -1404
  85. package/dist/codegen/generators/index.js.map +0 -1
  86. package/dist/database-errors-BNNmLTJE.d.ts +0 -86
  87. package/dist/events/index.d.ts +0 -183
  88. package/dist/events/index.js +0 -77
  89. package/dist/events/index.js.map +0 -1
  90. package/dist/index-DHiAqhKv.d.ts +0 -101
  91. package/dist/index.d.ts +0 -8
  92. package/dist/index.js +0 -3674
  93. package/dist/index.js.map +0 -1
  94. package/dist/types/index.d.ts +0 -121
  95. package/dist/types/index.js +0 -38
  96. package/dist/types/index.js.map +0 -1
  97. package/dist/types-BXibIEyj.d.ts +0 -60
package/docs/route.md ADDED
@@ -0,0 +1,497 @@
1
+ # Route
2
+
3
+ Type-safe route definition with automatic validation and tRPC-style developer experience.
4
+
5
+ ## Basic Usage
6
+
7
+ ```typescript
8
+ import { route } from '@spfn/core/route';
9
+ import { Type } from '@sinclair/typebox';
10
+
11
+ export const getUser = route.get('/users/:id')
12
+ .input({
13
+ params: Type.Object({ id: Type.String() })
14
+ })
15
+ .handler(async (c) => {
16
+ const { params } = await c.data();
17
+ return { id: params.id, name: 'John' };
18
+ });
19
+ ```
20
+
21
+ ## HTTP Methods
22
+
23
+ ```typescript
24
+ route.get('/path') // GET
25
+ route.post('/path') // POST
26
+ route.put('/path') // PUT
27
+ route.patch('/path') // PATCH
28
+ route.delete('/path') // DELETE
29
+ ```
30
+
31
+ ## Input Definition
32
+
33
+ ### Path Parameters
34
+
35
+ ```typescript
36
+ route.get('/users/:id/posts/:postId')
37
+ .input({
38
+ params: Type.Object({
39
+ id: Type.String(),
40
+ postId: Type.String()
41
+ })
42
+ })
43
+ .handler(async (c) => {
44
+ const { params } = await c.data();
45
+ // params.id, params.postId
46
+ });
47
+ ```
48
+
49
+ ### Query Parameters
50
+
51
+ ```typescript
52
+ route.get('/users')
53
+ .input({
54
+ query: Type.Object({
55
+ page: Type.Number({ default: 1 }),
56
+ limit: Type.Number({ default: 20 }),
57
+ search: Type.Optional(Type.String())
58
+ })
59
+ })
60
+ .handler(async (c) => {
61
+ const { query } = await c.data();
62
+ // query.page, query.limit, query.search
63
+ });
64
+ ```
65
+
66
+ ### Request Body
67
+
68
+ ```typescript
69
+ route.post('/users')
70
+ .input({
71
+ body: Type.Object({
72
+ email: Type.String({ format: 'email' }),
73
+ name: Type.String({ minLength: 1, maxLength: 100 }),
74
+ role: Type.Optional(Type.Union([
75
+ Type.Literal('admin'),
76
+ Type.Literal('user')
77
+ ]))
78
+ })
79
+ })
80
+ .handler(async (c) => {
81
+ const { body } = await c.data();
82
+ // body.email, body.name, body.role
83
+ });
84
+ ```
85
+
86
+ ### Headers
87
+
88
+ ```typescript
89
+ route.get('/protected')
90
+ .input({
91
+ headers: Type.Object({
92
+ authorization: Type.String()
93
+ })
94
+ })
95
+ .handler(async (c) => {
96
+ const { headers } = await c.data();
97
+ // headers.authorization
98
+ });
99
+ ```
100
+
101
+ ### Cookies
102
+
103
+ ```typescript
104
+ route.get('/session')
105
+ .input({
106
+ cookies: Type.Object({
107
+ sessionId: Type.String()
108
+ })
109
+ })
110
+ .handler(async (c) => {
111
+ const { cookies } = await c.data();
112
+ // cookies.sessionId
113
+ });
114
+ ```
115
+
116
+ ### Form Data (File Upload)
117
+
118
+ ```typescript
119
+ import { route, FileSchema, FileArraySchema } from '@spfn/core/route';
120
+
121
+ // Single file
122
+ route.post('/upload')
123
+ .input({
124
+ formData: Type.Object({
125
+ file: FileSchema,
126
+ description: Type.Optional(Type.String())
127
+ })
128
+ })
129
+ .handler(async (c) => {
130
+ const { formData } = await c.data();
131
+ const file = formData.file as File;
132
+ // file.name, file.size, file.type
133
+ });
134
+
135
+ // Multiple files
136
+ route.post('/upload-multiple')
137
+ .input({
138
+ formData: Type.Object({
139
+ files: FileArraySchema
140
+ })
141
+ })
142
+ .handler(async (c) => {
143
+ const { formData } = await c.data();
144
+ const files = formData.files as File[];
145
+ });
146
+ ```
147
+
148
+ > **Note:** For detailed file upload patterns including validation, storage, and security, see [File Upload Guide](./file-upload.md).
149
+
150
+ ### Combined Input
151
+
152
+ ```typescript
153
+ route.patch('/users/:id')
154
+ .input({
155
+ params: Type.Object({ id: Type.String() }),
156
+ query: Type.Object({ notify: Type.Optional(Type.Boolean()) }),
157
+ body: Type.Object({ name: Type.String() })
158
+ })
159
+ .handler(async (c) => {
160
+ const { params, query, body } = await c.data();
161
+ // All inputs are typed and validated
162
+ });
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Response Patterns
168
+
169
+ ### Direct Return (Recommended)
170
+
171
+ Simply return data from handler - automatic JSON response:
172
+
173
+ ```typescript
174
+ route.get('/users/:id')
175
+ .handler(async (c) => {
176
+ const user = await userRepo.findById(id);
177
+ return user; // Automatic c.json(user)
178
+ });
179
+ ```
180
+
181
+ ### Response Helpers
182
+
183
+ For custom status codes and headers:
184
+
185
+ ```typescript
186
+ route.post('/users')
187
+ .handler(async (c) => {
188
+ const user = await userRepo.create(data);
189
+
190
+ // 201 Created with Location header
191
+ return c.created(user, `/users/${user.id}`);
192
+ });
193
+
194
+ route.delete('/users/:id')
195
+ .handler(async (c) => {
196
+ await userRepo.delete(id);
197
+
198
+ // 204 No Content
199
+ return c.noContent();
200
+ });
201
+
202
+ route.put('/users/:id')
203
+ .handler(async (c) => {
204
+ // Custom status code
205
+ return c.json({ updated: true }, 202);
206
+ });
207
+ ```
208
+
209
+ **Available Helpers:**
210
+
211
+ | Helper | Status | Description |
212
+ |--------|--------|-------------|
213
+ | `c.json(data, status?)` | Custom | JSON with optional status |
214
+ | `c.created(data, location?)` | 201 | Created with Location header |
215
+ | `c.accepted(data?)` | 202 | Accepted |
216
+ | `c.noContent()` | 204 | No Content |
217
+ | `c.notModified()` | 304 | Not Modified |
218
+ | `c.paginated(items, page, limit, total)` | 200 | Paginated response |
219
+
220
+ ### Paginated Response
221
+
222
+ ```typescript
223
+ route.get('/users')
224
+ .input({
225
+ query: Type.Object({
226
+ page: Type.Number({ default: 1 }),
227
+ limit: Type.Number({ default: 20 })
228
+ })
229
+ })
230
+ .handler(async (c) => {
231
+ const { query } = await c.data();
232
+ const { items, total } = await userRepo.findPaginated(query);
233
+
234
+ return c.paginated(items, query.page, query.limit, total);
235
+ // Response: { items: [...], pagination: { page, limit, total, totalPages } }
236
+ });
237
+ ```
238
+
239
+ ---
240
+
241
+ ## Middleware
242
+
243
+ ### Using Middleware
244
+
245
+ ```typescript
246
+ import { Transactional } from '@spfn/core/db';
247
+ import { authMiddleware } from './middlewares/auth';
248
+
249
+ route.post('/users')
250
+ .use([Transactional(), authMiddleware])
251
+ .handler(async (c) => {
252
+ // Runs after middleware chain
253
+ });
254
+ ```
255
+
256
+ ### Skip Global Middleware
257
+
258
+ ```typescript
259
+ // Skip specific middlewares
260
+ route.get('/public')
261
+ .skip(['auth', 'rateLimit'])
262
+ .handler(async (c) => { ... });
263
+
264
+ // Skip all global middlewares
265
+ route.get('/health')
266
+ .skip('*')
267
+ .handler(async (c) => { ... });
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Router Composition
273
+
274
+ ### Define Router
275
+
276
+ ```typescript
277
+ import { defineRouter } from '@spfn/core/route';
278
+
279
+ // Flat structure
280
+ export const appRouter = defineRouter({
281
+ getUser,
282
+ createUser,
283
+ updateUser,
284
+ deleteUser
285
+ });
286
+
287
+ // Nested structure
288
+ export const appRouter = defineRouter({
289
+ users: defineRouter({
290
+ get: getUser,
291
+ create: createUser
292
+ }),
293
+ posts: defineRouter({
294
+ list: getPosts,
295
+ create: createPost
296
+ })
297
+ });
298
+ ```
299
+
300
+ ### Spread Pattern
301
+
302
+ ```typescript
303
+ import * as userRoutes from './routes/users';
304
+ import * as postRoutes from './routes/posts';
305
+
306
+ export const appRouter = defineRouter({
307
+ ...userRoutes,
308
+ ...postRoutes
309
+ });
310
+ ```
311
+
312
+ ---
313
+
314
+ ## Error Handling
315
+
316
+ ### Throwing Errors
317
+
318
+ ```typescript
319
+ route.get('/users/:id')
320
+ .handler(async (c) => {
321
+ const user = await userRepo.findById(id);
322
+
323
+ if (!user)
324
+ {
325
+ throw new Error('User not found');
326
+ }
327
+
328
+ return user;
329
+ });
330
+ ```
331
+
332
+ ### Using HttpError
333
+
334
+ ```typescript
335
+ import { HttpError } from '@spfn/core/errors';
336
+
337
+ route.get('/protected')
338
+ .handler(async (c) => {
339
+ if (!isAuthenticated)
340
+ {
341
+ throw new HttpError(401, 'Unauthorized');
342
+ }
343
+
344
+ return { data: 'secret' };
345
+ });
346
+ ```
347
+
348
+ ### Validation Errors
349
+
350
+ Validation errors are automatically thrown when input doesn't match schema:
351
+
352
+ ```typescript
353
+ // POST /users with { email: "invalid" }
354
+ // → 400 Bad Request
355
+ // → { error: "Validation failed", fields: [{ path: "/email", message: "Invalid email format" }] }
356
+ ```
357
+
358
+ ---
359
+
360
+ ## TypeBox Schema Reference
361
+
362
+ ### Basic Types
363
+
364
+ ```typescript
365
+ import { Type } from '@sinclair/typebox';
366
+
367
+ Type.String() // string
368
+ Type.Number() // number
369
+ Type.Integer() // integer
370
+ Type.Boolean() // boolean
371
+ Type.Null() // null
372
+ Type.Array(Type.String()) // string[]
373
+ Type.Object({ key: Type.String() }) // { key: string }
374
+ ```
375
+
376
+ ### String Constraints
377
+
378
+ ```typescript
379
+ Type.String({ format: 'email' })
380
+ Type.String({ format: 'uri' })
381
+ Type.String({ format: 'uuid' })
382
+ Type.String({ minLength: 1, maxLength: 100 })
383
+ Type.String({ pattern: '^[a-z]+$' })
384
+ ```
385
+
386
+ ### Number Constraints
387
+
388
+ ```typescript
389
+ Type.Number({ minimum: 0, maximum: 100 })
390
+ Type.Integer({ minimum: 1 })
391
+ Type.Number({ default: 20 })
392
+ ```
393
+
394
+ ### Optional & Nullable
395
+
396
+ ```typescript
397
+ import { Nullable, OptionalNullable } from '@spfn/core/route';
398
+
399
+ Type.Optional(Type.String()) // string | undefined
400
+ Nullable(Type.String()) // string | null
401
+ OptionalNullable(Type.String()) // string | null | undefined
402
+ ```
403
+
404
+ ### Union & Literal
405
+
406
+ ```typescript
407
+ // Enum-like
408
+ Type.Union([
409
+ Type.Literal('draft'),
410
+ Type.Literal('published'),
411
+ Type.Literal('archived')
412
+ ])
413
+
414
+ // Multiple types
415
+ Type.Union([Type.String(), Type.Number()])
416
+ ```
417
+
418
+ ---
419
+
420
+ ## Raw Context Access
421
+
422
+ For advanced Hono features:
423
+
424
+ ```typescript
425
+ route.get('/advanced')
426
+ .handler(async (c) => {
427
+ // Access raw Hono context
428
+ const raw = c.raw;
429
+
430
+ // Get custom header
431
+ const customHeader = raw.req.header('x-custom');
432
+
433
+ // Set response header
434
+ raw.header('x-response', 'value');
435
+
436
+ // Get context variable (set by middleware)
437
+ const user = raw.get('user');
438
+
439
+ return { data: 'ok' };
440
+ });
441
+ ```
442
+
443
+ ---
444
+
445
+ ## Best Practices
446
+
447
+ ### Do
448
+
449
+ ```typescript
450
+ // 1. Keep handlers thin - delegate to repository
451
+ route.post('/users')
452
+ .handler(async (c) => {
453
+ const { body } = await c.data();
454
+ return userRepo.create(body); // Simple delegation
455
+ });
456
+
457
+ // 2. Use Transactional for write operations
458
+ route.post('/users')
459
+ .use([Transactional()])
460
+ .handler(async (c) => { ... });
461
+
462
+ // 3. Define reusable schemas
463
+ const UserIdParams = Type.Object({ id: Type.String() });
464
+
465
+ route.get('/users/:id').input({ params: UserIdParams })...
466
+ route.patch('/users/:id').input({ params: UserIdParams })...
467
+ route.delete('/users/:id').input({ params: UserIdParams })...
468
+ ```
469
+
470
+ ### Don't
471
+
472
+ ```typescript
473
+ // 1. Don't put business logic in handlers
474
+ route.post('/users')
475
+ .handler(async (c) => {
476
+ const { body } = await c.data();
477
+
478
+ // Bad - business logic in handler
479
+ const existing = await db.select().from(users).where(eq(users.email, body.email));
480
+ if (existing.length > 0) throw new Error('Email exists');
481
+
482
+ return db.insert(users).values(body);
483
+ });
484
+
485
+ // 2. Don't forget Transactional for writes
486
+ route.post('/users')
487
+ .handler(async (c) => { // Missing Transactional!
488
+ return userRepo.create(body);
489
+ });
490
+
491
+ // 3. Don't access database directly in routes
492
+ route.get('/users')
493
+ .handler(async (c) => {
494
+ // Bad - use repository instead
495
+ return db.select().from(users);
496
+ });
497
+ ```