yinzerflow 0.2.9 → 0.3.0

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.
@@ -51,6 +51,144 @@ Contains all incoming request data including headers, body, query parameters, ro
51
51
  ### Response Object (`ctx.response`)
52
52
  Provides methods to control the HTTP response including status codes, headers, and response formatting. See [Response Object Documentation](./response.md) for detailed information about response methods and capabilities.
53
53
 
54
+ ## Context State
55
+
56
+ The Context object provides a powerful state system that allows you to store and share custom data throughout the request lifecycle. This is perfect for authentication, middleware data, request-scoped variables, and custom context information.
57
+
58
+ ### What is Context State?
59
+
60
+ Context state is a request-scoped object (`ctx.state`) that persists data throughout the entire request lifecycle. Unlike global variables, state is isolated to each individual request and automatically garbage collected when the request completes.
61
+
62
+ ### Basic Usage
63
+
64
+ ```typescript
65
+ app.get('/api/users', async (ctx) => {
66
+ // Store custom data in state
67
+ ctx.state.user = { id: 1, name: 'John' };
68
+ ctx.state.requestId = generateRequestId();
69
+ ctx.state.timestamp = Date.now();
70
+
71
+ // Access the data later
72
+ console.log(ctx.state.user.name); // "John"
73
+ console.log(ctx.state.requestId); // "req-123"
74
+ console.log(ctx.state.timestamp); // 1703123456789
75
+
76
+ return { users: ['John', 'Jane'] };
77
+ });
78
+ ```
79
+
80
+ ### Advanced Usage with Type Safety
81
+
82
+ For full type safety, you can extend the context with custom state types:
83
+
84
+ ```typescript
85
+ // Define your custom state interface
86
+ interface AuthContext extends InternalHandlerCallbackGenerics {
87
+ state: {
88
+ user: User;
89
+ permissions: string[];
90
+ requestId: string;
91
+ session: Session;
92
+ };
93
+ }
94
+
95
+ // Use it in your route handler
96
+ const authHandler: HandlerCallback<AuthContext> = async (ctx) => {
97
+ // Fully type-safe access to state
98
+ console.log(ctx.state.user.name); // Type: string
99
+ console.log(ctx.state.permissions[0]); // Type: string
100
+ console.log(ctx.state.session.expiresAt); // Type: Date
101
+
102
+ // No type assertions needed!
103
+ const user = ctx.state.user; // Type: User
104
+ const permissions = ctx.state.permissions; // Type: string[]
105
+
106
+ return { message: 'Authenticated', user };
107
+ };
108
+ ```
109
+
110
+ ### State Lifecycle
111
+
112
+ State data follows the request lifecycle:
113
+
114
+ 1. **Request Start**: State object is created as empty object
115
+ 2. **Global Hooks**: `beforeAll` hooks can populate state
116
+ 3. **Route Hooks**: `beforeHooks` can access and modify state
117
+ 4. **Route Handler**: Your handler can access and modify state
118
+ 5. **Route Hooks**: `afterHooks` can access state and modify response
119
+ 6. **Global Hooks**: `afterAll` hooks can access state and modify response
120
+ 7. **Request End**: State is automatically garbage collected
121
+
122
+ ### State Inheritance in Route Groups
123
+
124
+ State can be shared across route groups and inherited by nested routes:
125
+
126
+ ```typescript
127
+ app.group('/api/v1', (api) => {
128
+ // Group-level middleware sets state
129
+ api.beforeAll([async (ctx) => {
130
+ ctx.state.apiVersion = 'v1';
131
+ ctx.state.environment = process.env.NODE_ENV;
132
+ }]);
133
+
134
+ api.group('/admin', (admin) => {
135
+ // Admin-specific middleware
136
+ admin.beforeAll([async (ctx) => {
137
+ ctx.state.requiresAuth = true;
138
+ ctx.state.adminOnly = true;
139
+ }]);
140
+
141
+ // Routes inherit all state from parent groups
142
+ admin.get('/users', async (ctx) => {
143
+ console.log(ctx.state.apiVersion); // "v1"
144
+ console.log(ctx.state.environment); // "production"
145
+ console.log(ctx.state.requiresAuth); // true
146
+ console.log(ctx.state.adminOnly); // true
147
+
148
+ return { users: ['Admin1', 'Admin2'] };
149
+ });
150
+ });
151
+ });
152
+ ```
153
+
154
+ ### Common Use Cases
155
+
156
+ - **Authentication**: Store user information and permissions
157
+ - **Request Tracking**: Generate and store request IDs for logging
158
+ - **Middleware Data**: Pass data between middleware and route handlers
159
+ - **Session Management**: Store session information and user preferences
160
+ - **Rate Limiting**: Track request counts and limits per user/IP
161
+ - **Custom Headers**: Store computed headers for later use
162
+ - **Validation Results**: Cache validation results to avoid re-validation
163
+ - **Database Connections**: Store database connection pools or transactions
164
+
165
+ ### Best Practices
166
+
167
+ - **Keep state minimal**: Only store data that's actually needed
168
+ - **Use descriptive keys**: `ctx.state.user` is better than `ctx.state.u`
169
+ - **Validate state access**: Check if properties exist before using them
170
+ - **Type your state**: Use TypeScript interfaces for complex state structures
171
+ - **Don't store sensitive data**: State is not encrypted or secured
172
+ - **Clean up resources**: Release any resources (like DB connections) when done
173
+
174
+ ### Security Considerations
175
+
176
+ YinzerFlow implements several security measures for context state:
177
+
178
+ #### 🛡️ Request Isolation
179
+ - **Problem**: State data could leak between requests
180
+ - **YinzerFlow Solution**: Each request gets its own isolated state object
181
+
182
+ #### 🛡️ Type Safety
183
+ - **Problem**: Unchecked state access can lead to runtime errors
184
+ - **YinzerFlow Solution**: TypeScript generics provide compile-time type checking
185
+
186
+ #### 🛡️ Memory Management
187
+ - **Problem**: State data could accumulate and cause memory leaks
188
+ - **YinzerFlow Solution**: State is automatically garbage collected after each request
189
+
190
+ These security measures ensure YinzerFlow's context state implementation follows security best practices and prevents common attack vectors while maintaining spec compliance.
191
+
54
192
  ## TypeScript Support
55
193
 
56
194
  YinzerFlow provides full TypeScript support for context objects with generic type parameters.
@@ -97,20 +97,269 @@ app.get('/api/users/:id', (ctx) => {
97
97
 
98
98
  ## Error Handler Pattern
99
99
 
100
+ ## Context State Examples
101
+
102
+ ### Basic State Usage
103
+
100
104
  ```typescript
101
- app.onError((ctx, error) => {
102
- // Access request context in error handler
103
- const { path, method, headers } = ctx.request;
105
+ app.get('/api/users', async (ctx) => {
106
+ // Store simple data in state
107
+ ctx.state.requestId = generateRequestId();
108
+ ctx.state.timestamp = Date.now();
109
+ ctx.state.clientIp = ctx.request.ipAddress;
104
110
 
105
- // Control error response
106
- ctx.response.setStatusCode(500);
111
+ // Access the stored data
112
+ console.log(`Request ${ctx.state.requestId} from ${ctx.state.clientIp}`);
113
+
114
+ return { users: ['John', 'Jane'] };
115
+ });
116
+ ```
117
+
118
+ ### Authentication State Pattern
119
+
120
+ ```typescript
121
+ // Authentication middleware
122
+ const authMiddleware: HandlerCallback = async (ctx) => {
123
+ const token = ctx.request.headers.authorization?.replace('Bearer ', '');
124
+
125
+ if (!token) {
126
+ throw new Error('Unauthorized');
127
+ }
128
+
129
+ try {
130
+ const user = await validateJWT(token);
131
+ const permissions = await getUserPermissions(user.id);
132
+
133
+ // Store authentication data in state
134
+ ctx.state.user = user;
135
+ ctx.state.permissions = permissions;
136
+ ctx.state.isAuthenticated = true;
137
+ ctx.state.authTimestamp = Date.now();
138
+ } catch (error) {
139
+ throw new Error('Invalid token');
140
+ }
141
+ };
142
+
143
+ // Protected route using the state
144
+ app.get('/api/admin/users', authMiddleware, async (ctx) => {
145
+ // Access the authenticated user data
146
+ const { user, permissions, isAuthenticated } = ctx.state;
147
+
148
+ if (!permissions.includes('admin')) {
149
+ throw new Error('Insufficient permissions');
150
+ }
107
151
 
108
152
  return {
109
- success: false,
110
- error: 'Internal server error',
111
- path,
112
- method,
113
- timestamp: new Date().toISOString()
153
+ message: 'Admin access granted',
154
+ user: { id: user.id, name: user.name },
155
+ permissions,
156
+ authenticatedAt: ctx.state.authTimestamp
157
+ };
158
+ });
159
+ ```
160
+
161
+ ### Typed State Pattern
162
+
163
+ ```typescript
164
+ // Define your state interface
165
+ interface UserContext extends InternalHandlerCallbackGenerics {
166
+ state: {
167
+ user: User;
168
+ permissions: string[];
169
+ requestId: string;
170
+ session: {
171
+ id: string;
172
+ expiresAt: Date;
173
+ lastActivity: Date;
174
+ };
175
+ };
176
+ }
177
+
178
+ // Use typed state in your handler
179
+ const userProfileHandler: HandlerCallback<UserContext> = async (ctx) => {
180
+ // Fully type-safe access to state
181
+ const { user, permissions, session } = ctx.state;
182
+
183
+ // No type assertions needed!
184
+ if (session.expiresAt < new Date()) {
185
+ throw new Error('Session expired');
186
+ }
187
+
188
+ // Update session activity
189
+ ctx.state.session.lastActivity = new Date();
190
+
191
+ return {
192
+ profile: {
193
+ id: user.id,
194
+ name: user.name,
195
+ email: user.email,
196
+ permissions
197
+ },
198
+ session: {
199
+ id: session.id,
200
+ expiresAt: session.expiresAt,
201
+ lastActivity: ctx.state.session.lastActivity
202
+ }
203
+ };
204
+ };
205
+ ```
206
+
207
+ ### Middleware Chain State Pattern
208
+
209
+ ```typescript
210
+ // Rate limiting middleware
211
+ const rateLimitMiddleware: HandlerCallback = async (ctx) => {
212
+ const clientIp = ctx.request.ipAddress;
213
+ const currentCount = await getRequestCount(clientIp);
214
+
215
+ if (currentCount > 100) {
216
+ throw new Error('Rate limit exceeded');
217
+ }
218
+
219
+ // Store rate limiting data in state
220
+ ctx.state.rateLimit = {
221
+ clientIp,
222
+ currentCount,
223
+ limit: 100,
224
+ resetTime: Date.now() + 60000 // 1 minute
225
+ };
226
+
227
+ await incrementRequestCount(clientIp);
228
+ };
229
+
230
+ // Logging middleware
231
+ const loggingMiddleware: HandlerCallback = async (ctx) => {
232
+ // Access state from previous middleware
233
+ const rateLimit = ctx.state.rateLimit;
234
+
235
+ ctx.state.logData = {
236
+ requestId: generateRequestId(),
237
+ timestamp: Date.now(),
238
+ clientIp: ctx.request.ipAddress,
239
+ rateLimitInfo: rateLimit,
240
+ userAgent: ctx.request.headers['user-agent']
241
+ };
242
+
243
+ console.log('Request started:', ctx.state.logData);
244
+ };
245
+
246
+ // Route using both middlewares
247
+ app.get('/api/data', rateLimitMiddleware, loggingMiddleware, async (ctx) => {
248
+ // Access state from all previous middleware
249
+ const { rateLimit, logData } = ctx.state;
250
+
251
+ // Add route-specific data to state
252
+ ctx.state.routeData = {
253
+ endpoint: '/api/data',
254
+ method: 'GET',
255
+ processingTime: Date.now() - logData.timestamp
256
+ };
257
+
258
+ return {
259
+ message: 'Data retrieved successfully',
260
+ rateLimit: {
261
+ remaining: rateLimit.limit - rateLimit.currentCount,
262
+ resetTime: rateLimit.resetTime
263
+ },
264
+ requestInfo: logData,
265
+ routeInfo: ctx.state.routeData
266
+ };
267
+ });
268
+ ```
269
+
270
+ ### Route Group State Pattern
271
+
272
+ ```typescript
273
+ app.group('/api/v1', (api) => {
274
+ // Global API state
275
+ api.beforeAll([async (ctx) => {
276
+ ctx.state.apiVersion = 'v1';
277
+ ctx.state.environment = process.env.NODE_ENV;
278
+ ctx.state.baseUrl = 'https://api.example.com';
279
+ }]);
280
+
281
+ api.group('/admin', (admin) => {
282
+ // Admin-specific state
283
+ admin.beforeAll([async (ctx) => {
284
+ ctx.state.requiresAuth = true;
285
+ ctx.state.adminOnly = true;
286
+ ctx.state.auditLog = true;
287
+ }]);
288
+
289
+ admin.group('/users', (users) => {
290
+ // User management state
291
+ users.beforeAll([async (ctx) => {
292
+ ctx.state.resourceType = 'user';
293
+ ctx.state.allowedOperations = ['create', 'read', 'update', 'delete'];
294
+ }]);
295
+
296
+ // Route inherits all state from parent groups
297
+ users.get('/', async (ctx) => {
298
+ const {
299
+ apiVersion, // "v1"
300
+ environment, // "production"
301
+ requiresAuth, // true
302
+ adminOnly, // true
303
+ auditLog, // true
304
+ resourceType, // "user"
305
+ allowedOperations // ["create", "read", "update", "delete"]
306
+ } = ctx.state;
307
+
308
+ return {
309
+ message: 'User list retrieved',
310
+ apiInfo: { version: apiVersion, environment },
311
+ security: { requiresAuth, adminOnly, auditLog },
312
+ resource: { type: resourceType, operations: allowedOperations }
313
+ };
314
+ });
315
+ });
316
+ });
317
+ });
318
+ ```
319
+
320
+ ### State Validation Pattern
321
+
322
+ ```typescript
323
+ // State validation helper
324
+ const validateState = (ctx: Context, requiredKeys: string[]) => {
325
+ const missing = requiredKeys.filter(key => !(key in ctx.state));
326
+
327
+ if (missing.length > 0) {
328
+ throw new Error(`Missing required state: ${missing.join(', ')}`);
329
+ }
330
+ };
331
+
332
+ // Middleware that sets required state
333
+ const userContextMiddleware: HandlerCallback = async (ctx) => {
334
+ const userId = ctx.request.params.userId;
335
+
336
+ if (!userId) {
337
+ throw new Error('User ID required');
338
+ }
339
+
340
+ const user = await getUserById(userId);
341
+ if (!user) {
342
+ throw new Error('User not found');
343
+ }
344
+
345
+ // Set required state
346
+ ctx.state.user = user;
347
+ ctx.state.userId = userId;
348
+ ctx.state.userPermissions = await getUserPermissions(userId);
349
+ };
350
+
351
+ // Route that validates state
352
+ app.get('/api/users/:userId/profile', userContextMiddleware, async (ctx) => {
353
+ // Validate required state
354
+ validateState(ctx, ['user', 'userId', 'userPermissions']);
355
+
356
+ // Now we can safely access state
357
+ const { user, userPermissions } = ctx.state;
358
+
359
+ return {
360
+ profile: user,
361
+ permissions: userPermissions,
362
+ lastAccessed: new Date().toISOString()
114
363
  };
115
364
  });
116
365
  ```
@@ -144,6 +144,83 @@ app.get('/users/:userId/posts/:postId', ({ request }) => {
144
144
  - Use consistent naming conventions
145
145
  - Avoid generic names that could cause confusion
146
146
 
147
+ ## Context State
148
+
149
+ YinzerFlow provides a powerful context state system that allows you to store and share custom data throughout the request lifecycle. This is perfect for authentication, middleware data, request-scoped variables, and custom context information.
150
+
151
+ ### Quick Reference
152
+
153
+ The context state is accessible via `ctx.state` and can store any data you need:
154
+
155
+ ```typescript
156
+ app.get('/api/users', async (ctx) => {
157
+ // Store custom data in state
158
+ ctx.state.user = { id: 1, name: 'John' };
159
+ ctx.state.requestId = generateRequestId();
160
+
161
+ // Access the data later
162
+ console.log(ctx.state.user.name); // "John"
163
+ console.log(ctx.state.requestId); // "req-123"
164
+
165
+ return { users: ['John', 'Jane'] };
166
+ });
167
+ ```
168
+
169
+ ### Middleware Integration
170
+
171
+ State is perfect for middleware that needs to pass data to route handlers:
172
+
173
+ ```typescript
174
+ // Authentication middleware
175
+ const authMiddleware: HandlerCallback = async (ctx) => {
176
+ const token = ctx.request.headers.authorization;
177
+ const user = await validateToken(token);
178
+
179
+ // Store user data in state for route handlers
180
+ ctx.state.user = user;
181
+ ctx.state.permissions = await getUserPermissions(user.id);
182
+ };
183
+
184
+ // Route that uses the authenticated state
185
+ app.get('/api/admin/users', authMiddleware, async (ctx) => {
186
+ const { user, permissions } = ctx.state;
187
+
188
+ if (!permissions.includes('admin')) {
189
+ throw new Error('Insufficient permissions');
190
+ }
191
+
192
+ return { message: 'Admin access granted', user };
193
+ });
194
+ ```
195
+
196
+ ### Route Group State
197
+
198
+ State can be shared across route groups and inherited by nested routes:
199
+
200
+ ```typescript
201
+ app.group('/api/v1', (api) => {
202
+ api.beforeAll([async (ctx) => {
203
+ ctx.state.apiVersion = 'v1';
204
+ ctx.state.environment = process.env.NODE_ENV;
205
+ }]);
206
+
207
+ api.group('/admin', (admin) => {
208
+ admin.beforeAll([async (ctx) => {
209
+ ctx.state.requiresAuth = true;
210
+ ctx.state.adminOnly = true;
211
+ }]);
212
+
213
+ // Routes inherit all state from parent groups
214
+ admin.get('/users', async (ctx) => {
215
+ const { apiVersion, environment, requiresAuth, adminOnly } = ctx.state;
216
+ return { users: ['Admin1', 'Admin2'] };
217
+ });
218
+ });
219
+ });
220
+ ```
221
+
222
+ **📚 For comprehensive Context State documentation including TypeScript patterns, security considerations, and best practices, see [Context State Documentation](./context.md#context-state).**
223
+
147
224
  ## Query Parameters
148
225
 
149
226
  URL query strings are automatically parsed and available in `ctx.request.query`:
@@ -305,10 +382,11 @@ Global hooks are executed in the following order:
305
382
 
306
383
  ## Route Groups
307
384
 
308
- Organize related routes with shared prefixes and hooks. Groups automatically merge hooks from the group level with individual route hooks:
385
+ Route groups allow you to organize related routes under a common prefix and apply shared middleware or hooks to all routes within the group. Groups can be nested to create hierarchical route structures.
386
+
387
+ ### Basic Group Usage
309
388
 
310
389
  ```typescript
311
- // API v1 routes with authentication
312
390
  app.group('/api/v1', (group) => {
313
391
  group.get('/users', ({ response }) => {
314
392
  return { users: ['John', 'Jane'] };
@@ -331,49 +409,134 @@ app.group('/api/v1', (group) => {
331
409
  }
332
410
  ]
333
411
  });
412
+ ```
413
+
414
+ ### Nested Groups
334
415
 
335
- // Admin routes with admin authentication
336
- app.group('/admin', (group) => {
337
- group.get('/dashboard', ({ response }) => {
338
- return { message: 'Admin dashboard' };
416
+ Groups can be nested to create hierarchical route structures like `/api/v1/admin/users`:
417
+
418
+ ```typescript
419
+ app.group('/api/v1', (api) => {
420
+ // /api/v1/users
421
+ api.group('/users', (users) => {
422
+ users.get('/', ({ response }) => {
423
+ return { users: ['John', 'Jane'] };
424
+ });
425
+
426
+ users.get('/:id', ({ request }) => {
427
+ const userId = request.params.id;
428
+ return { userId, name: 'John Doe' };
429
+ });
339
430
  });
340
431
 
341
- group.post('/settings', ({ request }) => {
342
- const settings = request.body;
343
- return { message: 'Settings updated', settings };
432
+ // /api/v1/admin
433
+ api.group('/admin', (admin) => {
434
+ // /api/v1/admin/users
435
+ admin.group('/users', (adminUsers) => {
436
+ adminUsers.get('/', ({ response }) => {
437
+ return { adminUsers: ['Admin1', 'Admin2'] };
438
+ });
439
+
440
+ adminUsers.post('/', ({ request }) => {
441
+ const userData = request.body;
442
+ return { message: 'Admin user created', data: userData };
443
+ });
444
+ }, {
445
+ beforeHooks: [
446
+ ({ request }) => {
447
+ // Admin authentication check
448
+ if (!request.headers.authorization) {
449
+ throw new Error('Unauthorized');
450
+ }
451
+ }
452
+ ]
453
+ });
454
+ }, {
455
+ beforeHooks: [
456
+ ({ request }) => {
457
+ // Admin role verification
458
+ console.log('Admin route accessed');
459
+ }
460
+ ]
344
461
  });
345
462
  }, {
346
463
  beforeHooks: [
347
- ({ request, response }) => {
348
- // Admin authentication
349
- const adminToken = request.headers['x-admin-token'];
350
- if (!adminToken) {
351
- response.setStatusCode(403);
352
- return { error: 'Admin access required' };
353
- }
464
+ ({ request }) => {
465
+ // Global API logging
466
+ console.log(`API request to: ${request.url}`);
354
467
  }
355
468
  ]
356
469
  });
470
+ ```
471
+
472
+ ### Hook Inheritance
473
+
474
+ Hooks are inherited and merged from parent groups to child groups:
357
475
 
358
- // Nested groups with complex hook merging
359
- app.group('/api/v1', (v1Group) => {
360
- v1Group.group('/admin', (adminGroup) => {
361
- adminGroup.get('/users', ({ response }) => {
362
- return { message: 'Admin users endpoint' };
476
+ ```typescript
477
+ app.group('/api', (api) => {
478
+ // This group inherits the 'api' beforeHooks
479
+
480
+ api.group('/v1', (v1) => {
481
+ // This group inherits both 'api' and 'v1' beforeHooks
482
+
483
+ v1.group('/admin', (admin) => {
484
+ // This group inherits 'api', 'v1', and 'admin' beforeHooks
485
+
486
+ admin.get('/dashboard', ({ response }) => {
487
+ // All three beforeHooks will execute in order
488
+ return { message: 'Admin dashboard' };
489
+ });
490
+ }, {
491
+ beforeHooks: [
492
+ ({ request }) => {
493
+ // Admin-specific hook
494
+ console.log('Admin route accessed');
495
+ }
496
+ ]
363
497
  });
364
498
  }, {
365
499
  beforeHooks: [
366
- () => {
367
- // Admin-specific hook
368
- console.log('Admin group hook');
500
+ ({ request }) => {
501
+ // Version-specific hook
502
+ console.log('API v1 accessed');
369
503
  }
370
504
  ]
371
505
  });
372
506
  }, {
373
507
  beforeHooks: [
374
- () => {
375
- // API v1 hook
376
- console.log('API v1 group hook');
508
+ ({ request }) => {
509
+ // Global API hook
510
+ console.log('API request');
511
+ }
512
+ ]
513
+ });
514
+ ```
515
+
516
+ ### Group Options
517
+
518
+ Groups support the same options as individual routes:
519
+
520
+ ```typescript
521
+ app.group('/api/v1', (group) => {
522
+ group.get('/public', ({ response }) => {
523
+ return { message: 'Public endpoint' };
524
+ });
525
+
526
+ group.get('/private', ({ response }) => {
527
+ return { message: 'Private endpoint' };
528
+ });
529
+ }, {
530
+ beforeHooks: [
531
+ ({ request }) => {
532
+ // Global group hooks
533
+ console.log('API v1 request');
534
+ }
535
+ ],
536
+ afterHooks: [
537
+ ({ response }) => {
538
+ // Global group after hooks
539
+ response.headers.set('X-API-Version', 'v1');
377
540
  }
378
541
  ]
379
542
  });