@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.
- package/README.md +298 -466
- package/dist/boss-DI1r4kTS.d.ts +244 -0
- package/dist/cache/index.d.ts +13 -33
- package/dist/cache/index.js +14 -703
- package/dist/cache/index.js.map +1 -1
- package/dist/codegen/index.d.ts +214 -17
- package/dist/codegen/index.js +231 -1420
- package/dist/codegen/index.js.map +1 -1
- package/dist/config/index.d.ts +1227 -0
- package/dist/config/index.js +273 -0
- package/dist/config/index.js.map +1 -0
- package/dist/db/index.d.ts +741 -59
- package/dist/db/index.js +1063 -1226
- package/dist/db/index.js.map +1 -1
- package/dist/env/index.d.ts +658 -308
- package/dist/env/index.js +503 -928
- package/dist/env/index.js.map +1 -1
- package/dist/env/loader.d.ts +87 -0
- package/dist/env/loader.js +70 -0
- package/dist/env/loader.js.map +1 -0
- package/dist/errors/index.d.ts +417 -29
- package/dist/errors/index.js +359 -98
- package/dist/errors/index.js.map +1 -1
- package/dist/event/index.d.ts +41 -0
- package/dist/event/index.js +131 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/sse/client.d.ts +82 -0
- package/dist/event/sse/client.js +115 -0
- package/dist/event/sse/client.js.map +1 -0
- package/dist/event/sse/index.d.ts +40 -0
- package/dist/event/sse/index.js +92 -0
- package/dist/event/sse/index.js.map +1 -0
- package/dist/job/index.d.ts +218 -0
- package/dist/job/index.js +410 -0
- package/dist/job/index.js.map +1 -0
- package/dist/logger/index.d.ts +20 -79
- package/dist/logger/index.js +82 -387
- package/dist/logger/index.js.map +1 -1
- package/dist/middleware/index.d.ts +102 -20
- package/dist/middleware/index.js +51 -705
- package/dist/middleware/index.js.map +1 -1
- package/dist/nextjs/index.d.ts +120 -0
- package/dist/nextjs/index.js +448 -0
- package/dist/nextjs/index.js.map +1 -0
- package/dist/{client/nextjs/index.d.ts → nextjs/server.d.ts} +335 -262
- package/dist/nextjs/server.js +637 -0
- package/dist/nextjs/server.js.map +1 -0
- package/dist/route/index.d.ts +879 -25
- package/dist/route/index.js +697 -1271
- package/dist/route/index.js.map +1 -1
- package/dist/route/types.d.ts +9 -0
- package/dist/route/types.js +3 -0
- package/dist/route/types.js.map +1 -0
- package/dist/router-Di7ENoah.d.ts +151 -0
- package/dist/server/index.d.ts +345 -64
- package/dist/server/index.js +1174 -3233
- package/dist/server/index.js.map +1 -1
- package/dist/types-B-e_f2dQ.d.ts +121 -0
- package/dist/types-BGl4QL1w.d.ts +77 -0
- package/dist/types-BOPTApC2.d.ts +245 -0
- package/docs/cache.md +133 -0
- package/docs/codegen.md +74 -0
- package/docs/database.md +346 -0
- package/docs/entity.md +539 -0
- package/docs/env.md +477 -0
- package/docs/errors.md +319 -0
- package/docs/event.md +116 -0
- package/docs/file-upload.md +717 -0
- package/docs/job.md +131 -0
- package/docs/logger.md +108 -0
- package/docs/middleware.md +337 -0
- package/docs/nextjs.md +241 -0
- package/docs/repository.md +496 -0
- package/docs/route.md +497 -0
- package/docs/server.md +307 -0
- package/package.json +68 -48
- package/dist/auto-loader-JFaZ9gON.d.ts +0 -80
- package/dist/client/index.d.ts +0 -358
- package/dist/client/index.js +0 -357
- package/dist/client/index.js.map +0 -1
- package/dist/client/nextjs/index.js +0 -371
- package/dist/client/nextjs/index.js.map +0 -1
- package/dist/codegen/generators/index.d.ts +0 -19
- package/dist/codegen/generators/index.js +0 -1404
- package/dist/codegen/generators/index.js.map +0 -1
- package/dist/database-errors-BNNmLTJE.d.ts +0 -86
- package/dist/events/index.d.ts +0 -183
- package/dist/events/index.js +0 -77
- package/dist/events/index.js.map +0 -1
- package/dist/index-DHiAqhKv.d.ts +0 -101
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -3674
- package/dist/index.js.map +0 -1
- package/dist/types/index.d.ts +0 -121
- package/dist/types/index.js +0 -38
- package/dist/types/index.js.map +0 -1
- package/dist/types-BXibIEyj.d.ts +0 -60
package/docs/errors.md
ADDED
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
# Errors
|
|
2
|
+
|
|
3
|
+
Error types and handling patterns.
|
|
4
|
+
|
|
5
|
+
## Error Types
|
|
6
|
+
|
|
7
|
+
### HttpError
|
|
8
|
+
|
|
9
|
+
General HTTP error with status code.
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { HttpError } from '@spfn/core/errors';
|
|
13
|
+
|
|
14
|
+
throw new HttpError(404, 'User not found');
|
|
15
|
+
throw new HttpError(403, 'Access denied');
|
|
16
|
+
throw new HttpError(500, 'Internal server error');
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### ValidationError
|
|
20
|
+
|
|
21
|
+
Input validation error with field details.
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
import { ValidationError } from '@spfn/core/errors';
|
|
25
|
+
|
|
26
|
+
throw new ValidationError({
|
|
27
|
+
message: 'Validation failed',
|
|
28
|
+
fields: [
|
|
29
|
+
{ path: '/email', message: 'Invalid email format' },
|
|
30
|
+
{ path: '/name', message: 'Name is required' }
|
|
31
|
+
]
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Response format:**
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"error": "Validation failed",
|
|
40
|
+
"fields": [
|
|
41
|
+
{ "path": "/email", "message": "Invalid email format" },
|
|
42
|
+
{ "path": "/name", "message": "Name is required" }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### NotFoundError
|
|
48
|
+
|
|
49
|
+
Resource not found error.
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { NotFoundError } from '@spfn/core/errors';
|
|
53
|
+
|
|
54
|
+
throw new NotFoundError('User');
|
|
55
|
+
// → 404: "User not found"
|
|
56
|
+
|
|
57
|
+
throw new NotFoundError('Post', '123');
|
|
58
|
+
// → 404: "Post with id 123 not found"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### UnauthorizedError
|
|
62
|
+
|
|
63
|
+
Authentication required error.
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
import { UnauthorizedError } from '@spfn/core/errors';
|
|
67
|
+
|
|
68
|
+
throw new UnauthorizedError();
|
|
69
|
+
// → 401: "Unauthorized"
|
|
70
|
+
|
|
71
|
+
throw new UnauthorizedError('Invalid token');
|
|
72
|
+
// → 401: "Invalid token"
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### ForbiddenError
|
|
76
|
+
|
|
77
|
+
Permission denied error.
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
import { ForbiddenError } from '@spfn/core/errors';
|
|
81
|
+
|
|
82
|
+
throw new ForbiddenError();
|
|
83
|
+
// → 403: "Forbidden"
|
|
84
|
+
|
|
85
|
+
throw new ForbiddenError('Admin access required');
|
|
86
|
+
// → 403: "Admin access required"
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### ConflictError
|
|
90
|
+
|
|
91
|
+
Resource conflict error.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
import { ConflictError } from '@spfn/core/errors';
|
|
95
|
+
|
|
96
|
+
throw new ConflictError('Email already exists');
|
|
97
|
+
// → 409: "Email already exists"
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### BadRequestError
|
|
101
|
+
|
|
102
|
+
Invalid request error.
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { BadRequestError } from '@spfn/core/errors';
|
|
106
|
+
|
|
107
|
+
throw new BadRequestError('Invalid date format');
|
|
108
|
+
// → 400: "Invalid date format"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Database Errors
|
|
114
|
+
|
|
115
|
+
### RepositoryError
|
|
116
|
+
|
|
117
|
+
Error from repository operations with context.
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { RepositoryError } from '@spfn/core/db';
|
|
121
|
+
|
|
122
|
+
// Automatically thrown by BaseRepository.withContext()
|
|
123
|
+
// Contains: repository name, method, table, original error
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### PostgreSQL Error Conversion
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
import { fromPostgresError } from '@spfn/core/db';
|
|
130
|
+
|
|
131
|
+
try
|
|
132
|
+
{
|
|
133
|
+
await db.insert(users).values(data);
|
|
134
|
+
}
|
|
135
|
+
catch (error)
|
|
136
|
+
{
|
|
137
|
+
const customError = fromPostgresError(error);
|
|
138
|
+
// 23505 → DuplicateEntryError
|
|
139
|
+
// 23503 → ConstraintViolationError
|
|
140
|
+
// 40P01 → DeadlockError
|
|
141
|
+
throw customError;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
## Error Handling in Routes
|
|
148
|
+
|
|
149
|
+
### Simple Throw
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
route.get('/users/:id')
|
|
153
|
+
.handler(async (c) => {
|
|
154
|
+
const user = await userRepo.findById(id);
|
|
155
|
+
|
|
156
|
+
if (!user)
|
|
157
|
+
{
|
|
158
|
+
throw new NotFoundError('User');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return user;
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### With HttpError
|
|
166
|
+
|
|
167
|
+
```typescript
|
|
168
|
+
route.post('/login')
|
|
169
|
+
.handler(async (c) => {
|
|
170
|
+
const { body } = await c.data();
|
|
171
|
+
const user = await userRepo.findByEmail(body.email);
|
|
172
|
+
|
|
173
|
+
if (!user || !await verifyPassword(body.password, user.password))
|
|
174
|
+
{
|
|
175
|
+
throw new UnauthorizedError('Invalid credentials');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { token: generateToken(user) };
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Validation in Repository
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
// repository
|
|
186
|
+
async createUser(data: NewUser)
|
|
187
|
+
{
|
|
188
|
+
const existing = await this._findOne(users, { email: data.email });
|
|
189
|
+
if (existing)
|
|
190
|
+
{
|
|
191
|
+
throw new ConflictError('Email already exists');
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return this._create(users, data);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// route
|
|
198
|
+
route.post('/users')
|
|
199
|
+
.handler(async (c) => {
|
|
200
|
+
const { body } = await c.data();
|
|
201
|
+
return userRepo.createUser(body); // Throws ConflictError if exists
|
|
202
|
+
});
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Error Response Format
|
|
208
|
+
|
|
209
|
+
All errors are converted to JSON response:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
{
|
|
213
|
+
"error": "Error message",
|
|
214
|
+
"code": "ERROR_CODE",
|
|
215
|
+
"statusCode": 404
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**Validation errors:**
|
|
220
|
+
|
|
221
|
+
```json
|
|
222
|
+
{
|
|
223
|
+
"error": "Validation failed",
|
|
224
|
+
"fields": [
|
|
225
|
+
{ "path": "/email", "message": "Invalid format" }
|
|
226
|
+
],
|
|
227
|
+
"statusCode": 400
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## Global Error Handler
|
|
234
|
+
|
|
235
|
+
Errors are caught by global error middleware:
|
|
236
|
+
|
|
237
|
+
```typescript
|
|
238
|
+
// Automatic - no setup needed
|
|
239
|
+
// Standard Error → 500 Internal Server Error
|
|
240
|
+
// HttpError → Custom status code
|
|
241
|
+
// ValidationError → 400 with field details
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## Custom Error Classes
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
import { HttpError } from '@spfn/core/errors';
|
|
250
|
+
|
|
251
|
+
export class PaymentRequiredError extends HttpError
|
|
252
|
+
{
|
|
253
|
+
constructor(message = 'Payment required')
|
|
254
|
+
{
|
|
255
|
+
super(402, message);
|
|
256
|
+
this.name = 'PaymentRequiredError';
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export class TooManyRequestsError extends HttpError
|
|
261
|
+
{
|
|
262
|
+
constructor(retryAfter?: number)
|
|
263
|
+
{
|
|
264
|
+
super(429, 'Too many requests');
|
|
265
|
+
this.name = 'TooManyRequestsError';
|
|
266
|
+
if (retryAfter)
|
|
267
|
+
{
|
|
268
|
+
this.headers = { 'Retry-After': String(retryAfter) };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
---
|
|
275
|
+
|
|
276
|
+
## Best Practices
|
|
277
|
+
|
|
278
|
+
### Do
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
// 1. Use specific error types
|
|
282
|
+
throw new NotFoundError('User'); // Not: throw new Error('User not found');
|
|
283
|
+
|
|
284
|
+
// 2. Provide meaningful messages
|
|
285
|
+
throw new ForbiddenError('Only admins can delete users');
|
|
286
|
+
|
|
287
|
+
// 3. Throw errors from repository for business logic
|
|
288
|
+
async createUser(data) {
|
|
289
|
+
if (await this.emailExists(data.email)) {
|
|
290
|
+
throw new ConflictError('Email already exists');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// 4. Let errors propagate - don't catch and re-throw
|
|
295
|
+
route.handler(async (c) => {
|
|
296
|
+
return userRepo.create(data); // Let errors propagate
|
|
297
|
+
});
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Don't
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// 1. Don't use generic Error for HTTP errors
|
|
304
|
+
throw new Error('Not found'); // Use NotFoundError
|
|
305
|
+
|
|
306
|
+
// 2. Don't catch errors just to log
|
|
307
|
+
try {
|
|
308
|
+
await userRepo.create(data);
|
|
309
|
+
} catch (e) {
|
|
310
|
+
console.log(e); // Bad - error handling does this
|
|
311
|
+
throw e;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// 3. Don't return error objects
|
|
315
|
+
return { error: 'Not found' }; // Throw instead
|
|
316
|
+
|
|
317
|
+
// 4. Don't expose internal error details
|
|
318
|
+
throw new HttpError(500, error.stack); // Bad - security risk
|
|
319
|
+
```
|
package/docs/event.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Event
|
|
2
|
+
|
|
3
|
+
In-process event system for decoupled communication.
|
|
4
|
+
|
|
5
|
+
## Define Events
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
// src/server/events/index.ts
|
|
9
|
+
import { defineEvent, defineEventHandler } from '@spfn/core/event';
|
|
10
|
+
|
|
11
|
+
// Define event types
|
|
12
|
+
export const userCreated = defineEvent<{
|
|
13
|
+
userId: string;
|
|
14
|
+
email: string;
|
|
15
|
+
}>('user.created');
|
|
16
|
+
|
|
17
|
+
export const userUpdated = defineEvent<{
|
|
18
|
+
userId: string;
|
|
19
|
+
changes: Record<string, any>;
|
|
20
|
+
}>('user.updated');
|
|
21
|
+
|
|
22
|
+
export const userDeleted = defineEvent<{
|
|
23
|
+
userId: string;
|
|
24
|
+
}>('user.deleted');
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Emit Events
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { emit } from '@spfn/core/event';
|
|
31
|
+
import { userCreated } from './events';
|
|
32
|
+
|
|
33
|
+
// In repository or service
|
|
34
|
+
async function createUser(data: NewUser)
|
|
35
|
+
{
|
|
36
|
+
const user = await this._create(users, data);
|
|
37
|
+
|
|
38
|
+
await emit(userCreated, {
|
|
39
|
+
userId: user.id,
|
|
40
|
+
email: user.email
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return user;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Handle Events
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { on } from '@spfn/core/event';
|
|
51
|
+
import { userCreated, userDeleted } from './events';
|
|
52
|
+
|
|
53
|
+
// Register handlers
|
|
54
|
+
on(userCreated, async (payload) => {
|
|
55
|
+
// Send welcome email
|
|
56
|
+
await emailService.sendWelcome(payload.email);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
on(userCreated, async (payload) => {
|
|
60
|
+
// Create default settings
|
|
61
|
+
await settingsRepo.createDefaults(payload.userId);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
on(userDeleted, async (payload) => {
|
|
65
|
+
// Cleanup related data
|
|
66
|
+
await cleanupUserData(payload.userId);
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Handler Registration
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
// src/server/events/handlers.ts
|
|
74
|
+
import { on } from '@spfn/core/event';
|
|
75
|
+
import { userCreated, userUpdated, userDeleted } from './index';
|
|
76
|
+
|
|
77
|
+
// Register all handlers
|
|
78
|
+
export function registerEventHandlers()
|
|
79
|
+
{
|
|
80
|
+
on(userCreated, handleUserCreated);
|
|
81
|
+
on(userUpdated, handleUserUpdated);
|
|
82
|
+
on(userDeleted, handleUserDeleted);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Call in server startup
|
|
86
|
+
import { registerEventHandlers } from './events/handlers';
|
|
87
|
+
registerEventHandlers();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Best Practices
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// 1. Define events in a central location
|
|
94
|
+
// src/server/events/index.ts
|
|
95
|
+
|
|
96
|
+
// 2. Use descriptive event names
|
|
97
|
+
defineEvent('user.created')
|
|
98
|
+
defineEvent('order.completed')
|
|
99
|
+
defineEvent('payment.failed')
|
|
100
|
+
|
|
101
|
+
// 3. Keep payloads minimal
|
|
102
|
+
defineEvent<{ userId: string }>('user.deleted') // Just ID, not full user
|
|
103
|
+
|
|
104
|
+
// 4. Handle errors in handlers
|
|
105
|
+
on(userCreated, async (payload) => {
|
|
106
|
+
try {
|
|
107
|
+
await sendEmail(payload.email);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
logger.error('Failed to send email', { error });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
// 5. Use events for side effects, not core logic
|
|
114
|
+
// Core: await userRepo.create(data);
|
|
115
|
+
// Side effect: emit(userCreated, { ... });
|
|
116
|
+
```
|