@spfn/core 0.2.0-beta.6 → 0.2.0-beta.8

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/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
+ ```