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

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/job.md ADDED
@@ -0,0 +1,131 @@
1
+ # Job
2
+
3
+ Background job processing.
4
+
5
+ ## Define Jobs
6
+
7
+ ```typescript
8
+ // src/server/jobs/send-email.job.ts
9
+ import { defineJob } from '@spfn/core/job';
10
+
11
+ export const sendEmailJob = defineJob<{
12
+ to: string;
13
+ subject: string;
14
+ body: string;
15
+ }>({
16
+ name: 'send-email',
17
+ handler: async (payload) => {
18
+ await emailService.send({
19
+ to: payload.to,
20
+ subject: payload.subject,
21
+ body: payload.body
22
+ });
23
+ }
24
+ });
25
+ ```
26
+
27
+ ## Enqueue Jobs
28
+
29
+ ```typescript
30
+ import { enqueue } from '@spfn/core/job';
31
+ import { sendEmailJob } from './jobs/send-email.job';
32
+
33
+ // Enqueue for immediate processing
34
+ await enqueue(sendEmailJob, {
35
+ to: 'user@example.com',
36
+ subject: 'Welcome',
37
+ body: 'Welcome to our app!'
38
+ });
39
+
40
+ // Enqueue with delay
41
+ await enqueue(sendEmailJob, payload, {
42
+ delay: 60000 // 1 minute
43
+ });
44
+
45
+ // Enqueue with options
46
+ await enqueue(sendEmailJob, payload, {
47
+ priority: 'high',
48
+ attempts: 3,
49
+ backoff: 'exponential'
50
+ });
51
+ ```
52
+
53
+ ## Job Options
54
+
55
+ ```typescript
56
+ defineJob({
57
+ name: 'process-image',
58
+ concurrency: 5, // Max concurrent jobs
59
+ attempts: 3, // Retry attempts
60
+ backoff: 'exponential', // Backoff strategy
61
+ timeout: 30000, // Job timeout (ms)
62
+ handler: async (payload) => {
63
+ // ...
64
+ }
65
+ });
66
+ ```
67
+
68
+ ## Scheduled Jobs
69
+
70
+ ```typescript
71
+ import { schedule } from '@spfn/core/job';
72
+
73
+ // Run every hour
74
+ schedule('cleanup', '0 * * * *', async () => {
75
+ await cleanupExpiredSessions();
76
+ });
77
+
78
+ // Run daily at midnight
79
+ schedule('daily-report', '0 0 * * *', async () => {
80
+ await generateDailyReport();
81
+ });
82
+
83
+ // Run every 5 minutes
84
+ schedule('health-check', '*/5 * * * *', async () => {
85
+ await checkExternalServices();
86
+ });
87
+ ```
88
+
89
+ ## Job Registration
90
+
91
+ ```typescript
92
+ // src/server/jobs/index.ts
93
+ import { registerJobs } from '@spfn/core/job';
94
+ import { sendEmailJob } from './send-email.job';
95
+ import { processImageJob } from './process-image.job';
96
+
97
+ export function initializeJobs()
98
+ {
99
+ registerJobs([
100
+ sendEmailJob,
101
+ processImageJob
102
+ ]);
103
+ }
104
+ ```
105
+
106
+ ## Best Practices
107
+
108
+ ```typescript
109
+ // 1. Keep jobs idempotent
110
+ handler: async (payload) => {
111
+ // Check if already processed
112
+ const existing = await db.findProcessed(payload.id);
113
+ if (existing) return;
114
+
115
+ await processItem(payload);
116
+ }
117
+
118
+ // 2. Use appropriate timeout
119
+ timeout: 30000 // Don't set too high
120
+
121
+ // 3. Handle failures gracefully
122
+ attempts: 3,
123
+ backoff: 'exponential'
124
+
125
+ // 4. Log job progress
126
+ handler: async (payload) => {
127
+ logger.info('Processing job', { jobId: payload.id });
128
+ // ...
129
+ logger.info('Job completed', { jobId: payload.id });
130
+ }
131
+ ```
package/docs/logger.md ADDED
@@ -0,0 +1,108 @@
1
+ # Logger
2
+
3
+ Structured logging with context support.
4
+
5
+ ## Basic Usage
6
+
7
+ ```typescript
8
+ import { logger } from '@spfn/core/logger';
9
+
10
+ logger.info('User created', { userId: '123' });
11
+ logger.warn('Rate limit approaching', { remaining: 10 });
12
+ logger.error('Failed to process', { error: err.message });
13
+ logger.debug('Processing request', { path: '/api/users' });
14
+ ```
15
+
16
+ ## Log Levels
17
+
18
+ | Level | Usage |
19
+ |-------|-------|
20
+ | `debug` | Development debugging |
21
+ | `info` | General information |
22
+ | `warn` | Warning conditions |
23
+ | `error` | Error conditions |
24
+
25
+ ## Structured Logging
26
+
27
+ ```typescript
28
+ // Good - structured data
29
+ logger.info('User login', {
30
+ userId: user.id,
31
+ email: user.email,
32
+ ip: request.ip
33
+ });
34
+
35
+ // Bad - string concatenation
36
+ logger.info(`User ${user.id} logged in from ${request.ip}`);
37
+ ```
38
+
39
+ ## Create Scoped Logger
40
+
41
+ ```typescript
42
+ import { createLogger } from '@spfn/core/logger';
43
+
44
+ const userLogger = createLogger('user-service');
45
+
46
+ userLogger.info('Created user');
47
+ // Output: [user-service] Created user
48
+
49
+ const paymentLogger = createLogger('payment');
50
+ paymentLogger.error('Payment failed', { orderId: '123' });
51
+ // Output: [payment] Payment failed { orderId: '123' }
52
+ ```
53
+
54
+ ## Context Logging
55
+
56
+ ```typescript
57
+ import { withLogContext } from '@spfn/core/logger';
58
+
59
+ // Add context to all logs in scope
60
+ await withLogContext({ requestId: '123', userId: 'abc' }, async () => {
61
+ logger.info('Processing request');
62
+ // Output includes: { requestId: '123', userId: 'abc' }
63
+
64
+ await doSomething();
65
+ logger.info('Request complete');
66
+ // Also includes context
67
+ });
68
+ ```
69
+
70
+ ## Log Format
71
+
72
+ Development (pretty):
73
+ ```
74
+ 2024-01-15 10:30:45 INFO User created { userId: '123', email: 'user@example.com' }
75
+ ```
76
+
77
+ Production (JSON):
78
+ ```json
79
+ {"timestamp":"2024-01-15T10:30:45.123Z","level":"info","message":"User created","userId":"123","email":"user@example.com"}
80
+ ```
81
+
82
+ ## Best Practices
83
+
84
+ ```typescript
85
+ // 1. Use structured data
86
+ logger.info('Operation complete', { duration: 150, result: 'success' });
87
+
88
+ // 2. Include error details
89
+ logger.error('Request failed', {
90
+ error: err.message,
91
+ stack: err.stack,
92
+ path: req.path
93
+ });
94
+
95
+ // 3. Use appropriate levels
96
+ logger.debug(...) // Development only
97
+ logger.info(...) // Normal operations
98
+ logger.warn(...) // Potential issues
99
+ logger.error(...) // Errors requiring attention
100
+
101
+ // 4. Create scoped loggers for modules
102
+ const dbLogger = createLogger('database');
103
+ const authLogger = createLogger('auth');
104
+
105
+ // 5. Don't log sensitive data
106
+ logger.info('User login', { userId: '123' }); // Good
107
+ logger.info('User login', { password: '...' }); // Bad!
108
+ ```
@@ -0,0 +1,337 @@
1
+ # Middleware
2
+
3
+ Named middleware system with route-level skip control.
4
+
5
+ ## Define Middleware
6
+
7
+ ### Regular Middleware
8
+
9
+ ```typescript
10
+ import { defineMiddleware } from '@spfn/core/route';
11
+
12
+ export const authMiddleware = defineMiddleware('auth', async (c, next) => {
13
+ const token = c.req.header('authorization');
14
+
15
+ if (!token)
16
+ {
17
+ return c.json({ error: 'Unauthorized' }, 401);
18
+ }
19
+
20
+ const user = await verifyToken(token);
21
+ c.set('user', user);
22
+
23
+ await next();
24
+ });
25
+ ```
26
+
27
+ ### Factory Middleware
28
+
29
+ Middleware with parameters:
30
+
31
+ ```typescript
32
+ export const requireRole = defineMiddleware('role',
33
+ (...roles: string[]) => async (c, next) => {
34
+ const user = c.get('user');
35
+
36
+ if (!roles.includes(user.role))
37
+ {
38
+ return c.json({ error: 'Forbidden' }, 403);
39
+ }
40
+
41
+ await next();
42
+ }
43
+ );
44
+
45
+ // Usage
46
+ route.get('/admin')
47
+ .use([requireRole('admin', 'superadmin')])
48
+ .handler(...)
49
+ ```
50
+
51
+ ### Two-Parameter Factory
52
+
53
+ For factory with exactly 2 parameters, use `defineMiddlewareFactory`:
54
+
55
+ ```typescript
56
+ import { defineMiddlewareFactory } from '@spfn/core/route';
57
+
58
+ export const rateLimiter = defineMiddlewareFactory('rateLimit',
59
+ (limit: number, windowMs: number) => async (c, next) => {
60
+ // Rate limit logic
61
+ await next();
62
+ }
63
+ );
64
+
65
+ // Usage
66
+ route.get('/api')
67
+ .use([rateLimiter(100, 60000)]) // 100 requests per minute
68
+ .handler(...)
69
+ ```
70
+
71
+ ---
72
+
73
+ ## Register Global Middleware
74
+
75
+ ```typescript
76
+ // src/server/server.config.ts
77
+ import { defineServerConfig } from '@spfn/core/server';
78
+ import { authMiddleware, loggerMiddleware, corsMiddleware } from './middlewares';
79
+
80
+ export default defineServerConfig()
81
+ .middlewares([
82
+ loggerMiddleware,
83
+ corsMiddleware,
84
+ authMiddleware
85
+ ])
86
+ .routes(appRouter)
87
+ .build();
88
+ ```
89
+
90
+ **Execution order:**
91
+ 1. Global middlewares (in registration order)
92
+ 2. Route-level middlewares (from `.use()`)
93
+ 3. Validation middleware (automatic)
94
+ 4. Route handler
95
+
96
+ ---
97
+
98
+ ## Use in Routes
99
+
100
+ ### Add Middleware
101
+
102
+ ```typescript
103
+ import { Transactional } from '@spfn/core/db';
104
+ import { authMiddleware, requireRole } from './middlewares';
105
+
106
+ route.post('/admin/users')
107
+ .use([authMiddleware, requireRole('admin'), Transactional()])
108
+ .handler(async (c) => {
109
+ const user = c.raw.get('user'); // From authMiddleware
110
+ // ...
111
+ });
112
+ ```
113
+
114
+ ### Skip Global Middleware
115
+
116
+ ```typescript
117
+ // Skip specific middlewares by name
118
+ route.get('/public/health')
119
+ .skip(['auth', 'rateLimit'])
120
+ .handler(async (c) => {
121
+ return { status: 'ok' };
122
+ });
123
+
124
+ // Skip all global middlewares
125
+ route.get('/webhooks/stripe')
126
+ .skip('*')
127
+ .handler(async (c) => {
128
+ // No global middleware applied
129
+ });
130
+ ```
131
+
132
+ **Note:** Route-level middlewares (`.use()`) are never skipped.
133
+
134
+ ---
135
+
136
+ ## Common Middleware Patterns
137
+
138
+ ### Authentication
139
+
140
+ ```typescript
141
+ export const authMiddleware = defineMiddleware('auth', async (c, next) => {
142
+ const token = c.req.header('authorization')?.replace('Bearer ', '');
143
+
144
+ if (!token)
145
+ {
146
+ return c.json({ error: 'Missing token' }, 401);
147
+ }
148
+
149
+ try
150
+ {
151
+ const payload = await verifyJWT(token);
152
+ c.set('userId', payload.sub);
153
+ c.set('user', await userRepo.findById(payload.sub));
154
+ await next();
155
+ }
156
+ catch
157
+ {
158
+ return c.json({ error: 'Invalid token' }, 401);
159
+ }
160
+ });
161
+ ```
162
+
163
+ ### Role-based Access
164
+
165
+ ```typescript
166
+ export const requirePermissions = defineMiddleware('permission',
167
+ (...permissions: string[]) => async (c, next) => {
168
+ const user = c.get('user');
169
+
170
+ const hasPermission = permissions.every(p =>
171
+ user.permissions.includes(p)
172
+ );
173
+
174
+ if (!hasPermission)
175
+ {
176
+ return c.json({ error: 'Insufficient permissions' }, 403);
177
+ }
178
+
179
+ await next();
180
+ }
181
+ );
182
+
183
+ // Usage
184
+ route.delete('/posts/:id')
185
+ .use([requirePermissions('posts:delete')])
186
+ .handler(...)
187
+ ```
188
+
189
+ ### Rate Limiting
190
+
191
+ ```typescript
192
+ const requestCounts = new Map<string, { count: number; resetAt: number }>();
193
+
194
+ export const rateLimiter = defineMiddlewareFactory('rateLimit',
195
+ (limit: number, windowMs: number) => async (c, next) => {
196
+ const key = c.req.header('x-forwarded-for') || 'unknown';
197
+ const now = Date.now();
198
+
199
+ const record = requestCounts.get(key);
200
+
201
+ if (!record || now > record.resetAt)
202
+ {
203
+ requestCounts.set(key, { count: 1, resetAt: now + windowMs });
204
+ }
205
+ else if (record.count >= limit)
206
+ {
207
+ return c.json({ error: 'Too many requests' }, 429);
208
+ }
209
+ else
210
+ {
211
+ record.count++;
212
+ }
213
+
214
+ await next();
215
+ }
216
+ );
217
+ ```
218
+
219
+ ### Request Logging
220
+
221
+ ```typescript
222
+ export const requestLogger = defineMiddleware('requestLogger', async (c, next) => {
223
+ const start = Date.now();
224
+ const method = c.req.method;
225
+ const path = c.req.path;
226
+
227
+ await next();
228
+
229
+ const duration = Date.now() - start;
230
+ const status = c.res.status;
231
+
232
+ console.log(`${method} ${path} ${status} ${duration}ms`);
233
+ });
234
+ ```
235
+
236
+ ### CORS
237
+
238
+ ```typescript
239
+ export const corsMiddleware = defineMiddleware('cors', async (c, next) => {
240
+ const origin = c.req.header('origin');
241
+
242
+ if (origin && allowedOrigins.includes(origin))
243
+ {
244
+ c.header('Access-Control-Allow-Origin', origin);
245
+ c.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
246
+ c.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
247
+ }
248
+
249
+ if (c.req.method === 'OPTIONS')
250
+ {
251
+ return c.body(null, 204);
252
+ }
253
+
254
+ await next();
255
+ });
256
+ ```
257
+
258
+ ---
259
+
260
+ ## Middleware Deduplication
261
+
262
+ When the same middleware is registered both globally and in a route, it's automatically deduplicated:
263
+
264
+ ```typescript
265
+ // Registered globally
266
+ .middlewares([authMiddleware])
267
+
268
+ // Also used in route
269
+ route.get('/users')
270
+ .use([authMiddleware]) // Skipped (already applied globally)
271
+ .handler(...)
272
+ ```
273
+
274
+ ---
275
+
276
+ ## Access Context Data
277
+
278
+ ### Set Data
279
+
280
+ ```typescript
281
+ defineMiddleware('auth', async (c, next) => {
282
+ c.set('user', user);
283
+ c.set('sessionId', sessionId);
284
+ await next();
285
+ });
286
+ ```
287
+
288
+ ### Get Data in Handler
289
+
290
+ ```typescript
291
+ route.get('/profile')
292
+ .handler(async (c) => {
293
+ const user = c.raw.get('user');
294
+ const sessionId = c.raw.get('sessionId');
295
+ // ...
296
+ });
297
+ ```
298
+
299
+ ---
300
+
301
+ ## Best Practices
302
+
303
+ ### Do
304
+
305
+ ```typescript
306
+ // 1. Use meaningful names for skip control
307
+ export const authMiddleware = defineMiddleware('auth', ...);
308
+ export const rateLimiter = defineMiddleware('rateLimit', ...);
309
+
310
+ // 2. Set context data for downstream use
311
+ c.set('user', user);
312
+
313
+ // 3. Return early for unauthorized requests
314
+ if (!token) return c.json({ error: 'Unauthorized' }, 401);
315
+
316
+ // 4. Always call next() for successful middleware
317
+ await next();
318
+ ```
319
+
320
+ ### Don't
321
+
322
+ ```typescript
323
+ // 1. Don't forget to call next()
324
+ defineMiddleware('logger', async (c, next) => {
325
+ console.log(c.req.path);
326
+ // Missing next() - request hangs!
327
+ });
328
+
329
+ // 2. Don't use generic names
330
+ defineMiddleware('middleware1', ...); // Bad
331
+
332
+ // 3. Don't throw errors - return responses
333
+ defineMiddleware('auth', async (c, next) => {
334
+ if (!token) throw new Error('No token'); // Bad
335
+ if (!token) return c.json({ error: 'No token' }, 401); // Good
336
+ });
337
+ ```