@spfn/core 0.2.0-beta.2 → 0.2.0-beta.21

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 (64) hide show
  1. package/README.md +262 -1092
  2. package/dist/{boss-D-fGtVgM.d.ts → boss-DI1r4kTS.d.ts} +68 -11
  3. package/dist/codegen/index.d.ts +55 -8
  4. package/dist/codegen/index.js +179 -5
  5. package/dist/codegen/index.js.map +1 -1
  6. package/dist/config/index.d.ts +204 -6
  7. package/dist/config/index.js +44 -11
  8. package/dist/config/index.js.map +1 -1
  9. package/dist/db/index.d.ts +13 -0
  10. package/dist/db/index.js +92 -33
  11. package/dist/db/index.js.map +1 -1
  12. package/dist/env/index.d.ts +83 -3
  13. package/dist/env/index.js +83 -15
  14. package/dist/env/index.js.map +1 -1
  15. package/dist/env/loader.d.ts +95 -0
  16. package/dist/env/loader.js +78 -0
  17. package/dist/env/loader.js.map +1 -0
  18. package/dist/event/index.d.ts +29 -70
  19. package/dist/event/index.js +15 -1
  20. package/dist/event/index.js.map +1 -1
  21. package/dist/event/sse/client.d.ts +157 -0
  22. package/dist/event/sse/client.js +169 -0
  23. package/dist/event/sse/client.js.map +1 -0
  24. package/dist/event/sse/index.d.ts +46 -0
  25. package/dist/event/sse/index.js +205 -0
  26. package/dist/event/sse/index.js.map +1 -0
  27. package/dist/job/index.d.ts +54 -8
  28. package/dist/job/index.js +61 -12
  29. package/dist/job/index.js.map +1 -1
  30. package/dist/middleware/index.d.ts +124 -11
  31. package/dist/middleware/index.js +41 -7
  32. package/dist/middleware/index.js.map +1 -1
  33. package/dist/nextjs/index.d.ts +2 -2
  34. package/dist/nextjs/index.js +37 -5
  35. package/dist/nextjs/index.js.map +1 -1
  36. package/dist/nextjs/server.d.ts +45 -24
  37. package/dist/nextjs/server.js +87 -66
  38. package/dist/nextjs/server.js.map +1 -1
  39. package/dist/route/index.d.ts +207 -14
  40. package/dist/route/index.js +304 -31
  41. package/dist/route/index.js.map +1 -1
  42. package/dist/route/types.d.ts +2 -31
  43. package/dist/router-Di7ENoah.d.ts +151 -0
  44. package/dist/server/index.d.ts +321 -10
  45. package/dist/server/index.js +798 -189
  46. package/dist/server/index.js.map +1 -1
  47. package/dist/{types-DRG2XMTR.d.ts → types-7Mhoxnnt.d.ts} +97 -4
  48. package/dist/types-DHQMQlcb.d.ts +305 -0
  49. package/docs/cache.md +133 -0
  50. package/docs/codegen.md +74 -0
  51. package/docs/database.md +346 -0
  52. package/docs/entity.md +539 -0
  53. package/docs/env.md +499 -0
  54. package/docs/errors.md +319 -0
  55. package/docs/event.md +432 -0
  56. package/docs/file-upload.md +717 -0
  57. package/docs/job.md +131 -0
  58. package/docs/logger.md +108 -0
  59. package/docs/middleware.md +337 -0
  60. package/docs/nextjs.md +247 -0
  61. package/docs/repository.md +496 -0
  62. package/docs/route.md +497 -0
  63. package/docs/server.md +429 -0
  64. package/package.json +19 -3
@@ -0,0 +1,496 @@
1
+ # Repository
2
+
3
+ Data access layer with BaseRepository class and domain-specific patterns.
4
+
5
+ ## Basic Repository
6
+
7
+ ```typescript
8
+ // src/server/repositories/user.repository.ts
9
+ import { BaseRepository } from '@spfn/core/db';
10
+ import { users, type User, type NewUser } from '../entities/users';
11
+
12
+ export class UserRepository extends BaseRepository
13
+ {
14
+ async findById(id: string): Promise<User | null>
15
+ {
16
+ return this._findOne(users, { id });
17
+ }
18
+
19
+ async findByEmail(email: string): Promise<User | null>
20
+ {
21
+ return this._findOne(users, { email });
22
+ }
23
+
24
+ async findAll(): Promise<User[]>
25
+ {
26
+ return this._findMany(users);
27
+ }
28
+
29
+ async create(data: NewUser): Promise<User>
30
+ {
31
+ return this._create(users, data);
32
+ }
33
+
34
+ async update(id: string, data: Partial<NewUser>): Promise<User | null>
35
+ {
36
+ return this._updateOne(users, { id }, data);
37
+ }
38
+
39
+ async delete(id: string): Promise<User | null>
40
+ {
41
+ return this._deleteOne(users, { id });
42
+ }
43
+ }
44
+
45
+ // Export singleton instance
46
+ export const userRepo = new UserRepository();
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Export Patterns
52
+
53
+ ### Singleton Instance (Recommended)
54
+
55
+ ```typescript
56
+ // src/server/repositories/user.repository.ts
57
+ export class UserRepository extends BaseRepository
58
+ {
59
+ // ... methods
60
+ }
61
+
62
+ // Export singleton instance
63
+ export const userRepo = new UserRepository();
64
+ ```
65
+
66
+ **Usage in routes:**
67
+
68
+ ```typescript
69
+ // src/server/routes/users.ts
70
+ import { userRepo } from '../repositories/user.repository';
71
+
72
+ export const getUser = route.get('/users/:id')
73
+ .handler(async (c) => {
74
+ const { params } = await c.data();
75
+ return userRepo.findById(params.id);
76
+ });
77
+ ```
78
+
79
+ ### Index File Export
80
+
81
+ ```typescript
82
+ // src/server/repositories/index.ts
83
+ export { userRepo } from './user.repository';
84
+ export { postRepo } from './post.repository';
85
+ export { categoryRepo } from './category.repository';
86
+
87
+ // Optional: also export classes for testing
88
+ export { UserRepository } from './user.repository';
89
+ export { PostRepository } from './post.repository';
90
+ ```
91
+
92
+ **Usage:**
93
+
94
+ ```typescript
95
+ import { userRepo, postRepo } from '../repositories';
96
+
97
+ const user = await userRepo.findById(id);
98
+ const posts = await postRepo.findByAuthor(user.id);
99
+ ```
100
+
101
+ ### Why Singleton?
102
+
103
+ - **Transaction propagation**: BaseRepository uses AsyncLocalStorage to detect active transactions
104
+ - **No manual DB passing**: Instance automatically uses correct connection
105
+ - **Testable**: Can still instantiate class directly in tests
106
+
107
+ ```typescript
108
+ // In tests - create fresh instance
109
+ const testRepo = new UserRepository();
110
+
111
+ // In application - use singleton
112
+ import { userRepo } from '../repositories';
113
+ ```
114
+
115
+ ---
116
+
117
+ ## Protected Helper Methods
118
+
119
+ BaseRepository provides these protected methods:
120
+
121
+ | Method | Description | Returns |
122
+ |--------|-------------|---------|
123
+ | `_findOne(table, where)` | Find single record | `T \| null` |
124
+ | `_findMany(table, options?)` | Find multiple records | `T[]` |
125
+ | `_create(table, data)` | Create single record | `T` |
126
+ | `_createMany(table, data[])` | Create multiple records | `T[]` |
127
+ | `_upsert(table, data, options)` | Insert or update | `T` |
128
+ | `_updateOne(table, where, data)` | Update single record | `T \| null` |
129
+ | `_updateMany(table, where, data)` | Update multiple records | `T[]` |
130
+ | `_deleteOne(table, where)` | Delete single record | `T \| null` |
131
+ | `_deleteMany(table, where)` | Delete multiple records | `T[]` |
132
+ | `_count(table, where?)` | Count records | `number` |
133
+
134
+ ---
135
+
136
+ ## Where Clause
137
+
138
+ ### Object-based (Simple Equality)
139
+
140
+ ```typescript
141
+ // Single field
142
+ await this._findOne(users, { id: '1' });
143
+
144
+ // Multiple fields (AND)
145
+ await this._findOne(users, { email: 'test@example.com', isActive: true });
146
+ ```
147
+
148
+ ### SQL-based (Complex Conditions)
149
+
150
+ ```typescript
151
+ import { eq, and, or, gt, lt, like, isNull, inArray } from 'drizzle-orm';
152
+
153
+ // Complex AND
154
+ await this._findMany(users, {
155
+ where: and(
156
+ eq(users.isActive, true),
157
+ gt(users.createdAt, lastWeek)
158
+ )
159
+ });
160
+
161
+ // OR condition
162
+ await this._findMany(users, {
163
+ where: or(
164
+ eq(users.role, 'admin'),
165
+ eq(users.role, 'moderator')
166
+ )
167
+ });
168
+
169
+ // LIKE
170
+ await this._findMany(users, {
171
+ where: like(users.email, '%@example.com')
172
+ });
173
+
174
+ // IN
175
+ await this._findMany(users, {
176
+ where: inArray(users.id, ['1', '2', '3'])
177
+ });
178
+
179
+ // IS NULL
180
+ await this._findMany(users, {
181
+ where: isNull(users.deletedAt)
182
+ });
183
+ ```
184
+
185
+ ---
186
+
187
+ ## Query Options
188
+
189
+ ### Ordering
190
+
191
+ ```typescript
192
+ import { desc, asc } from 'drizzle-orm';
193
+
194
+ await this._findMany(users, {
195
+ orderBy: desc(users.createdAt)
196
+ });
197
+
198
+ // Multiple columns
199
+ await this._findMany(users, {
200
+ orderBy: [desc(users.createdAt), asc(users.name)]
201
+ });
202
+ ```
203
+
204
+ ### Pagination
205
+
206
+ ```typescript
207
+ await this._findMany(users, {
208
+ limit: 20,
209
+ offset: 40 // Skip first 40
210
+ });
211
+ ```
212
+
213
+ ### Combined
214
+
215
+ ```typescript
216
+ await this._findMany(users, {
217
+ where: eq(users.isActive, true),
218
+ orderBy: desc(users.createdAt),
219
+ limit: 10,
220
+ offset: 0
221
+ });
222
+ ```
223
+
224
+ ---
225
+
226
+ ## Business Logic Patterns
227
+
228
+ ### Validation in Create
229
+
230
+ ```typescript
231
+ export class UserRepository extends BaseRepository
232
+ {
233
+ async createWithValidation(data: NewUser): Promise<User>
234
+ {
235
+ // Check duplicate
236
+ const existing = await this._findOne(users, { email: data.email });
237
+ if (existing)
238
+ {
239
+ throw new Error('Email already exists');
240
+ }
241
+
242
+ return this._create(users, data);
243
+ }
244
+ }
245
+ ```
246
+
247
+ ### Soft Delete
248
+
249
+ ```typescript
250
+ export class PostRepository extends BaseRepository
251
+ {
252
+ async softDelete(id: string, deletedBy: string): Promise<Post | null>
253
+ {
254
+ return this._updateOne(posts, { id }, {
255
+ deletedAt: new Date(),
256
+ deletedBy
257
+ });
258
+ }
259
+
260
+ async findActive(): Promise<Post[]>
261
+ {
262
+ return this._findMany(posts, {
263
+ where: isNull(posts.deletedAt)
264
+ });
265
+ }
266
+ }
267
+ ```
268
+
269
+ ### Paginated Query
270
+
271
+ ```typescript
272
+ export class UserRepository extends BaseRepository
273
+ {
274
+ async findPaginated(page: number, limit: number)
275
+ {
276
+ const offset = (page - 1) * limit;
277
+
278
+ const [items, total] = await Promise.all([
279
+ this._findMany(users, {
280
+ orderBy: desc(users.createdAt),
281
+ limit,
282
+ offset
283
+ }),
284
+ this._count(users)
285
+ ]);
286
+
287
+ return {
288
+ items,
289
+ total,
290
+ page,
291
+ limit,
292
+ totalPages: Math.ceil(total / limit)
293
+ };
294
+ }
295
+ }
296
+ ```
297
+
298
+ ### Filtered Query
299
+
300
+ ```typescript
301
+ export class UserRepository extends BaseRepository
302
+ {
303
+ async findByFilters(filters: {
304
+ role?: string;
305
+ isActive?: boolean;
306
+ search?: string;
307
+ }): Promise<User[]>
308
+ {
309
+ const conditions = [];
310
+
311
+ if (filters.role)
312
+ {
313
+ conditions.push(eq(users.role, filters.role));
314
+ }
315
+
316
+ if (filters.isActive !== undefined)
317
+ {
318
+ conditions.push(eq(users.isActive, filters.isActive));
319
+ }
320
+
321
+ if (filters.search)
322
+ {
323
+ conditions.push(or(
324
+ like(users.name, `%${filters.search}%`),
325
+ like(users.email, `%${filters.search}%`)
326
+ ));
327
+ }
328
+
329
+ return this._findMany(users, {
330
+ where: conditions.length > 0 ? and(...conditions) : undefined,
331
+ orderBy: desc(users.createdAt)
332
+ });
333
+ }
334
+ }
335
+ ```
336
+
337
+ ---
338
+
339
+ ## Complex Queries
340
+
341
+ For queries that can't be expressed with helpers, use direct database access.
342
+
343
+ ```typescript
344
+ export class UserRepository extends BaseRepository
345
+ {
346
+ async findWithPostCounts(): Promise<Array<User & { postCount: number }>>
347
+ {
348
+ return this.readDb
349
+ .select({
350
+ ...getTableColumns(users),
351
+ postCount: sql<number>`count(${posts.id})::int`
352
+ })
353
+ .from(users)
354
+ .leftJoin(posts, eq(users.id, posts.authorId))
355
+ .where(eq(users.isActive, true))
356
+ .groupBy(users.id)
357
+ .orderBy(desc(sql`count(${posts.id})`));
358
+ }
359
+
360
+ async findUsersWithRecentPosts(since: Date)
361
+ {
362
+ return this.readDb
363
+ .selectDistinct({ user: users })
364
+ .from(users)
365
+ .innerJoin(posts, eq(users.id, posts.authorId))
366
+ .where(gt(posts.createdAt, since));
367
+ }
368
+ }
369
+ ```
370
+
371
+ ### Database Access
372
+
373
+ ```typescript
374
+ export class UserRepository extends BaseRepository
375
+ {
376
+ // Protected getters from BaseRepository:
377
+ // this.db - Write database (transaction-aware)
378
+ // this.readDb - Read database (uses replica if available)
379
+
380
+ async customQuery()
381
+ {
382
+ // Use readDb for SELECT
383
+ const results = await this.readDb
384
+ .select()
385
+ .from(users)
386
+ .where(...);
387
+
388
+ // Use db for INSERT/UPDATE/DELETE
389
+ await this.db
390
+ .insert(users)
391
+ .values(data);
392
+ }
393
+ }
394
+ ```
395
+
396
+ ---
397
+
398
+ ## Error Handling
399
+
400
+ ### withContext
401
+
402
+ Wrap operations with context for better error tracking.
403
+
404
+ ```typescript
405
+ export class UserRepository extends BaseRepository
406
+ {
407
+ async findById(id: string)
408
+ {
409
+ return this.withContext(
410
+ () => this._findOne(users, { id }),
411
+ { method: 'findById', table: 'users' }
412
+ );
413
+ }
414
+ }
415
+
416
+ // On error: RepositoryError with repository name, method, table context
417
+ ```
418
+
419
+ ---
420
+
421
+ ## Repository Export
422
+
423
+ ```typescript
424
+ // src/server/repositories/index.ts
425
+ export { UserRepository } from './user.repository';
426
+ export { PostRepository } from './post.repository';
427
+ export { CategoryRepository } from './category.repository';
428
+ ```
429
+
430
+ ---
431
+
432
+ ## Best Practices
433
+
434
+ ### Do
435
+
436
+ ```typescript
437
+ // 1. Encapsulate business logic
438
+ async createWithValidation(data: NewUser)
439
+ {
440
+ const existing = await this._findOne(users, { email: data.email });
441
+ if (existing) throw new Error('Email exists');
442
+ return this._create(users, data);
443
+ }
444
+
445
+ // 2. Use object-based where for simple queries
446
+ await this._findOne(users, { id });
447
+
448
+ // 3. Use SQL-based where for complex queries
449
+ await this._findMany(users, {
450
+ where: and(eq(users.role, 'admin'), gt(users.createdAt, date))
451
+ });
452
+
453
+ // 4. Use readDb for read operations
454
+ async findAll()
455
+ {
456
+ return this.readDb.select().from(users);
457
+ }
458
+
459
+ // 5. Create domain-specific methods
460
+ async findActiveAdmins()
461
+ {
462
+ return this._findMany(users, {
463
+ where: and(eq(users.role, 'admin'), eq(users.isActive, true))
464
+ });
465
+ }
466
+ ```
467
+
468
+ ### Don't
469
+
470
+ ```typescript
471
+ // 1. Don't expose protected methods
472
+ const user = await userRepo._findOne(users, { id }); // Error
473
+
474
+ // 2. Don't use db for read operations
475
+ async findAll()
476
+ {
477
+ return this.db.select().from(users); // Bad - use readDb
478
+ }
479
+
480
+ // 3. Don't put repository logic in routes
481
+ route.get('/users/:id')
482
+ .handler(async (c) => {
483
+ // Bad - business logic in route
484
+ const user = await findOne(users, { id });
485
+ if (!user.isActive) throw new Error('Inactive');
486
+ return user;
487
+ });
488
+
489
+ // 4. Don't bypass transactions in write methods
490
+ async create(data: NewUser)
491
+ {
492
+ // Transactions are handled by route middleware
493
+ // Don't create your own transactions here
494
+ return this._create(users, data);
495
+ }
496
+ ```