@tamyla/clodo-framework 4.4.1 → 4.5.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +2 -1851
  2. package/README.md +44 -18
  3. package/dist/cli/commands/add.js +325 -0
  4. package/dist/config/service-schema-config.js +98 -5
  5. package/dist/index.js +22 -3
  6. package/dist/middleware/Composer.js +2 -1
  7. package/dist/middleware/factories.js +445 -0
  8. package/dist/middleware/index.js +4 -1
  9. package/dist/modules/ModuleManager.js +6 -2
  10. package/dist/routing/EnhancedRouter.js +185 -44
  11. package/dist/routing/RequestContext.js +393 -0
  12. package/dist/schema/SchemaManager.js +6 -2
  13. package/dist/service-management/generators/code/ServiceMiddlewareGenerator.js +79 -223
  14. package/dist/service-management/generators/code/WorkerIndexGenerator.js +241 -98
  15. package/dist/service-management/generators/config/WranglerTomlGenerator.js +130 -89
  16. package/dist/simple-api.js +4 -4
  17. package/dist/utilities/index.js +134 -1
  18. package/dist/validation/environmentGuard.js +172 -0
  19. package/package.json +4 -1
  20. package/scripts/repro-clodo.js +123 -0
  21. package/templates/ai-worker/package.json +19 -0
  22. package/templates/ai-worker/src/index.js +160 -0
  23. package/templates/cron-worker/package.json +19 -0
  24. package/templates/cron-worker/src/index.js +211 -0
  25. package/templates/edge-proxy/package.json +18 -0
  26. package/templates/edge-proxy/src/index.js +150 -0
  27. package/templates/minimal/package.json +17 -0
  28. package/templates/minimal/src/index.js +40 -0
  29. package/templates/queue-processor/package.json +19 -0
  30. package/templates/queue-processor/src/index.js +213 -0
  31. package/templates/rest-api/.dev.vars +2 -0
  32. package/templates/rest-api/package.json +19 -0
  33. package/templates/rest-api/src/index.js +124 -0
@@ -1,37 +1,61 @@
1
1
  import { createRouteHandlers } from '../handlers/GenericRouteHandler.js';
2
2
  import { schemaManager } from '../schema/SchemaManager.js';
3
+ import { MiddlewareComposer } from '../middleware/Composer.js';
4
+ import { RequestContext, createRequestContext } from './RequestContext.js';
3
5
 
4
6
  /**
5
7
  * Enhanced Router
6
- * Supports both domain-specific routes and generic CRUD routes
7
- *
8
- * Framework-ready version: All data-service specific imports and routes removed.
9
- * Domain-specific routes should be registered by the consuming application.
8
+ * Supports Express/Hono-style routing with middleware composition,
9
+ * parameterized routes, and optional auto-CRUD for schema models.
10
+ *
11
+ * Now works without a D1 client pass null for pure API routing.
12
+ *
13
+ * @example
14
+ * import { EnhancedRouter, createEnhancedRouter } from '@tamyla/clodo-framework';
15
+ *
16
+ * // Minimal router (no D1)
17
+ * const router = new EnhancedRouter();
18
+ *
19
+ * // With Hono-style RequestContext:
20
+ * router.get('/users/:id', async (c) => {
21
+ * return c.json({ user: c.req.param('id') });
22
+ * });
23
+ *
24
+ * // With classic (request, env, ctx) pattern:
25
+ * router.get('/legacy', async (request, env, ctx) => {
26
+ * return new Response('ok');
27
+ * });
10
28
  */
11
29
 
12
30
  export class EnhancedRouter {
13
31
  /**
14
32
  * Create a new EnhancedRouter instance
15
- * @param {Object} d1Client - D1 database client
16
- * @param {Object} options - Router options
33
+ * @param {Object} [d1Client=null] - Optional D1 database client (null for pure routing)
34
+ * @param {Object} [options={}] - Router options
35
+ * @param {boolean} [options.autoRegisterGenericRoutes=true] - Auto-register CRUD routes for schema models
36
+ * @param {boolean} [options.useRequestContext=true] - Pass RequestContext to handlers instead of raw request
17
37
  */
18
- constructor(d1Client, options = {}) {
38
+ constructor(d1Client = null, options = {}) {
19
39
  this.d1Client = d1Client;
20
40
  this.options = options;
21
41
  this.routes = new Map();
22
- this.genericHandlers = createRouteHandlers(d1Client, options);
42
+ this.middleware = [];
43
+ this.scopedMiddleware = new Map(); // path prefix → middleware[]
44
+ this.middlewareExecutor = null;
23
45
 
24
- // Register generic routes for all models
25
- this._registerGenericRoutes();
26
-
27
- // Note: Domain-specific routes should be registered by the consuming application
46
+ // Only auto-register CRUD routes if D1 client is provided and option is enabled
47
+ const autoRegister = options.autoRegisterGenericRoutes !== false && d1Client;
48
+ if (autoRegister) {
49
+ this.genericHandlers = createRouteHandlers(d1Client, options);
50
+ this._registerGenericRoutes();
51
+ }
28
52
  }
29
53
 
30
54
  /**
31
55
  * Register a custom route
32
56
  * @param {string} method - HTTP method
33
- * @param {string} path - Route path
34
- * @param {Function} handler - Route handler
57
+ * @param {string} path - Route path (supports :params and * wildcards)
58
+ * @param {Function} handler - Route handler: (c: RequestContext) => Response or (request, env, ctx) => Response
35
59
  */
36
60
  registerRoute(method, path, handler) {
37
61
  const key = `${method.toUpperCase()} ${path}`;
@@ -101,39 +125,89 @@ export class EnhancedRouter {
101
125
  return this.registerRoute('HEAD', path, handler);
102
126
  }
103
127
 
128
+ /**
129
+ * Register a route handler for ALL HTTP methods
130
+ * @param {string} path - Route path
131
+ * @param {Function} handler - Route handler
132
+ */
133
+ all(path, handler) {
134
+ for (const method of ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']) {
135
+ this.registerRoute(method, path, handler);
136
+ }
137
+ }
138
+
104
139
  /**
105
140
  * Find and execute a route handler
141
+ *
142
+ * Supports two call signatures:
143
+ * - handleRequest(method, path, request) — legacy
144
+ * - handleRequest(method, path, request, env, ctx) — full Workers signature
145
+ *
106
146
  * @param {string} method - HTTP method
107
147
  * @param {string} path - Request path
108
148
  * @param {Request} request - HTTP request
149
+ * @param {Object} [env] - Worker environment bindings
150
+ * @param {Object} [ctx] - Worker execution context
109
151
  * @returns {Promise<Response>} HTTP response
110
152
  */
111
- async handleRequest(method, path, request) {
112
- const key = `${method.toUpperCase()} ${path}`;
153
+ async handleRequest(method, path, request, env = {}, ctx = {}) {
154
+ // Create a handler function that will be executed with middleware
155
+ let handler = null;
156
+ let params = {};
113
157
 
114
158
  // Check for exact match first
159
+ const key = `${method.toUpperCase()} ${path}`;
115
160
  if (this.routes.has(key)) {
116
- const handler = this.routes.get(key);
117
- return await handler(request);
161
+ handler = this.routes.get(key);
162
+ } else {
163
+ // Check for parameterized routes
164
+ for (const [routeKey, routeHandler] of this.routes.entries()) {
165
+ const [routeMethod, routePath] = routeKey.split(' ');
166
+ if (routeMethod !== method.toUpperCase()) continue;
167
+ const match = this._matchRoute(routePath, path);
168
+ if (match) {
169
+ params = match.params;
170
+ handler = routeHandler;
171
+ break;
172
+ }
173
+ }
118
174
  }
119
175
 
120
- // Check for parameterized routes
121
- for (const [routeKey, handler] of this.routes.entries()) {
122
- const [routeMethod, routePath] = routeKey.split(' ');
123
- if (routeMethod !== method.toUpperCase()) continue;
124
- const match = this._matchRoute(routePath, path);
125
- if (match) {
126
- // Add route parameters to request
127
- // @ts-ignore - Extending Request object with params
128
- request.params = match.params;
129
- return await handler(request, ...match.args);
130
- }
176
+ // If no handler found, return 404
177
+ if (!handler) {
178
+ return new Response(JSON.stringify({
179
+ error: 'Not Found',
180
+ path
181
+ }), {
182
+ status: 404,
183
+ headers: {
184
+ 'Content-Type': 'application/json'
185
+ }
186
+ });
131
187
  }
132
188
 
133
- // No route found
134
- return new Response('Not Found', {
135
- status: 404
136
- });
189
+ // Detect handler arity to decide: RequestContext vs raw (request, env, ctx)
190
+ const useContext = this.options.useRequestContext !== false;
191
+
192
+ // Build the actual handler execution function
193
+ const executeHandler = async req => {
194
+ if (useContext && handler.length <= 1) {
195
+ // Hono-style: single argument = RequestContext
196
+ const c = new RequestContext(req, env, ctx, params);
197
+ return await handler(c);
198
+ } else {
199
+ // Classic style: (request, env, ctx) — attach params to request
200
+ req.params = params;
201
+ return await handler(req, env, ctx);
202
+ }
203
+ };
204
+
205
+ // Execute with middleware if available, otherwise execute directly
206
+ if (this.middlewareExecutor) {
207
+ return await this.middlewareExecutor.execute(request, executeHandler);
208
+ } else {
209
+ return await executeHandler(request);
210
+ }
137
211
  }
138
212
 
139
213
  /**
@@ -141,8 +215,10 @@ export class EnhancedRouter {
141
215
  * @private
142
216
  */
143
217
  _registerGenericRoutes() {
218
+ if (!this.genericHandlers) return;
144
219
  for (const [modelName] of schemaManager.getAllModels()) {
145
220
  const handler = this.genericHandlers[modelName];
221
+ if (!handler) continue;
146
222
  const basePath = `/api/${modelName}`;
147
223
 
148
224
  // CRUD routes
@@ -151,7 +227,9 @@ export class EnhancedRouter {
151
227
  this.registerRoute('GET', `${basePath}/:id`, (req, id) => handler.handleGet(req, id));
152
228
  this.registerRoute('PATCH', `${basePath}/:id`, (req, id) => handler.handleUpdate(req, id));
153
229
  this.registerRoute('DELETE', `${basePath}/:id`, (req, id) => handler.handleDelete(req, id));
154
- console.log(`✅ Registered generic routes for: ${modelName}`);
230
+ if (this.options.verbose || typeof process !== 'undefined' && process.env?.DEBUG) {
231
+ console.log(`✅ Registered generic routes for: ${modelName}`);
232
+ }
155
233
  }
156
234
  }
157
235
 
@@ -178,6 +256,11 @@ export class EnhancedRouter {
178
256
  const paramName = patternPart.slice(1);
179
257
  params[paramName] = pathPart;
180
258
  args.push(pathPart);
259
+ } else if (patternPart === '*') {
260
+ // Wildcard — matches anything
261
+ params['*'] = pathParts.slice(i).join('/');
262
+ args.push(params['*']);
263
+ break;
181
264
  } else if (patternPart !== pathPart) {
182
265
  // No match
183
266
  return null;
@@ -199,23 +282,81 @@ export class EnhancedRouter {
199
282
 
200
283
  /**
201
284
  * Add middleware to the router
202
- * @param {Function} middleware - Middleware function
285
+ *
286
+ * Supports two signatures:
287
+ * - use(middleware) — global middleware
288
+ * - use('/path', middleware) — scoped to a path prefix
289
+ *
290
+ * @param {string|Function|Object} pathOrMiddleware - Path prefix or middleware
291
+ * @param {Function|Object} [middleware] - Middleware (when first arg is a path)
292
+ */
293
+ use(pathOrMiddleware, middleware) {
294
+ if (typeof pathOrMiddleware === 'string' && middleware) {
295
+ // Scoped middleware: use('/api', corsMiddleware)
296
+ const prefix = pathOrMiddleware;
297
+ if (!this.scopedMiddleware.has(prefix)) {
298
+ this.scopedMiddleware.set(prefix, []);
299
+ }
300
+ this.scopedMiddleware.get(prefix).push(middleware);
301
+ } else {
302
+ // Global middleware
303
+ this.middleware.push(pathOrMiddleware);
304
+ }
305
+
306
+ // Rebuild the middleware executor
307
+ this._rebuildMiddlewareExecutor();
308
+ }
309
+
310
+ /**
311
+ * Rebuild the middleware executor when middleware changes
312
+ * @private
203
313
  */
204
- use(middleware) {
205
- // Store middleware for later use
206
- if (!this.middleware) {
207
- this.middleware = [];
314
+ _rebuildMiddlewareExecutor() {
315
+ if (this.middleware.length > 0) {
316
+ this.middlewareExecutor = MiddlewareComposer.compose(...this.middleware);
317
+ } else {
318
+ this.middlewareExecutor = null;
208
319
  }
209
- this.middleware.push(middleware);
320
+ }
321
+
322
+ /**
323
+ * Create a route group with a shared prefix
324
+ * @param {string} prefix - Path prefix for all routes in the group
325
+ * @param {Function} callback - (group: EnhancedRouter) => void
326
+ * @returns {EnhancedRouter} this — for chaining
327
+ *
328
+ * @example
329
+ * router.group('/api/v1', (api) => {
330
+ * api.get('/users', listUsers);
331
+ * api.post('/users', createUser);
332
+ * api.get('/users/:id', getUser);
333
+ * });
334
+ */
335
+ group(prefix, callback) {
336
+ const groupRouter = {
337
+ get: (path, handler) => this.get(prefix + path, handler),
338
+ post: (path, handler) => this.post(prefix + path, handler),
339
+ put: (path, handler) => this.put(prefix + path, handler),
340
+ patch: (path, handler) => this.patch(prefix + path, handler),
341
+ delete: (path, handler) => this.delete(prefix + path, handler),
342
+ options: (path, handler) => this.options(prefix + path, handler),
343
+ head: (path, handler) => this.head(prefix + path, handler),
344
+ all: (path, handler) => this.all(prefix + path, handler)
345
+ };
346
+ callback(groupRouter);
347
+ return this;
210
348
  }
211
349
  }
212
350
 
213
351
  /**
214
352
  * Create an enhanced router instance
215
- * @param {Object} d1Client - D1 database client
216
- * @param {Object} options - Router options
353
+ * @param {Object} [d1Client=null] - Optional D1 database client
354
+ * @param {Object} [options={}] - Router options
217
355
  * @returns {EnhancedRouter} Router instance
218
356
  */
219
- export function createEnhancedRouter(d1Client, options = {}) {
357
+ export function createEnhancedRouter(d1Client = null, options = {}) {
220
358
  return new EnhancedRouter(d1Client, options);
221
- }
359
+ }
360
+
361
+ // Re-export RequestContext for direct usage
362
+ export { RequestContext, createRequestContext } from './RequestContext.js';
@@ -0,0 +1,393 @@
1
+ /**
2
+ * RequestContext — Hono-style request/response context for Cloudflare Workers
3
+ *
4
+ * Wraps the standard (request, env, ctx) triplet into a single ergonomic object.
5
+ *
6
+ * @example
7
+ * import { RequestContext } from '@tamyla/clodo-framework/routing';
8
+ *
9
+ * router.get('/users/:id', async (c) => {
10
+ * const id = c.req.param('id');
11
+ * const data = await c.env.KV_DATA.get(id);
12
+ * return c.json({ id, data });
13
+ * });
14
+ *
15
+ * @module @tamyla/clodo-framework/routing/RequestContext
16
+ */
17
+
18
+ export class RequestContext {
19
+ /**
20
+ * @param {Request} request - The incoming request
21
+ * @param {Object} env - Cloudflare Worker environment bindings
22
+ * @param {ExecutionContext} executionCtx - Worker execution context
23
+ * @param {Object} [params={}] - Route parameters extracted by the router
24
+ */
25
+ constructor(request, env, executionCtx, params = {}) {
26
+ this._request = request;
27
+ this._env = env;
28
+ this._executionCtx = executionCtx;
29
+ this._params = params;
30
+ this._url = null; // lazy-parsed
31
+ this._headers = new Headers();
32
+ this._status = 200;
33
+ this._store = new Map(); // per-request storage for middleware data sharing
34
+ }
35
+
36
+ // ─── Request Accessors ──────────────────────────────────────────────
37
+
38
+ /** The raw Request object */
39
+ get request() {
40
+ return this._request;
41
+ }
42
+
43
+ /** Alias — matches Hono's `c.req` */
44
+ get req() {
45
+ return this._reqProxy || (this._reqProxy = this._buildReqProxy());
46
+ }
47
+
48
+ /** Worker environment bindings */
49
+ get env() {
50
+ return this._env;
51
+ }
52
+
53
+ /** Worker execution context (for waitUntil, passThroughOnException, etc.) */
54
+ get executionCtx() {
55
+ return this._executionCtx;
56
+ }
57
+
58
+ /** Parsed URL (lazy) */
59
+ get url() {
60
+ return this._url || (this._url = new URL(this._request.url));
61
+ }
62
+
63
+ // ─── Per-request store (for middleware data sharing) ─────────────────
64
+
65
+ /**
66
+ * Set a value in the per-request store
67
+ * @param {string} key
68
+ * @param {*} value
69
+ */
70
+ set(key, value) {
71
+ this._store.set(key, value);
72
+ }
73
+
74
+ /**
75
+ * Get a value from the per-request store
76
+ * @param {string} key
77
+ * @returns {*}
78
+ */
79
+ get(key) {
80
+ return this._store.get(key);
81
+ }
82
+
83
+ // ─── Response Helpers ───────────────────────────────────────────────
84
+
85
+ /**
86
+ * Return a JSON response
87
+ * @param {*} data - Data to serialize
88
+ * @param {number} [status=200] - HTTP status code
89
+ * @param {Object} [headers={}] - Additional response headers
90
+ * @returns {Response}
91
+ */
92
+ json(data, status = 200, headers = {}) {
93
+ return new Response(JSON.stringify(data), {
94
+ status,
95
+ headers: {
96
+ 'Content-Type': 'application/json; charset=utf-8',
97
+ ...Object.fromEntries(this._headers),
98
+ ...headers
99
+ }
100
+ });
101
+ }
102
+
103
+ /**
104
+ * Return a plain text response
105
+ * @param {string} text
106
+ * @param {number} [status=200]
107
+ * @param {Object} [headers={}]
108
+ * @returns {Response}
109
+ */
110
+ text(text, status = 200, headers = {}) {
111
+ return new Response(String(text), {
112
+ status,
113
+ headers: {
114
+ 'Content-Type': 'text/plain; charset=utf-8',
115
+ ...Object.fromEntries(this._headers),
116
+ ...headers
117
+ }
118
+ });
119
+ }
120
+
121
+ /**
122
+ * Return an HTML response
123
+ * @param {string} html
124
+ * @param {number} [status=200]
125
+ * @param {Object} [headers={}]
126
+ * @returns {Response}
127
+ */
128
+ html(html, status = 200, headers = {}) {
129
+ return new Response(String(html), {
130
+ status,
131
+ headers: {
132
+ 'Content-Type': 'text/html; charset=utf-8',
133
+ ...Object.fromEntries(this._headers),
134
+ ...headers
135
+ }
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Return a redirect response
141
+ * @param {string} url - Target URL
142
+ * @param {number} [status=302] - 301 or 302
143
+ * @returns {Response}
144
+ */
145
+ redirect(url, status = 302) {
146
+ return Response.redirect(url, status);
147
+ }
148
+
149
+ /**
150
+ * Return a streaming response — ideal for Workers AI streaming
151
+ * @param {Function} callback - async (stream: WritableStreamDefaultWriter) => void
152
+ * @param {Object} [headers={}] - Additional response headers
153
+ * @returns {Response}
154
+ *
155
+ * @example
156
+ * return c.stream(async (stream) => {
157
+ * const aiStream = await env.AI.run(model, { stream: true, messages });
158
+ * for await (const chunk of aiStream) {
159
+ * await stream.write(new TextEncoder().encode(chunk.response));
160
+ * }
161
+ * });
162
+ */
163
+ stream(callback, headers = {}) {
164
+ const {
165
+ readable,
166
+ writable
167
+ } = new TransformStream();
168
+ const writer = writable.getWriter();
169
+
170
+ // Wrap writer with convenience methods
171
+ const stream = {
172
+ write: async data => {
173
+ const chunk = typeof data === 'string' ? new TextEncoder().encode(data) : data;
174
+ await writer.write(chunk);
175
+ },
176
+ writeLine: async data => {
177
+ await stream.write(data + '\n');
178
+ },
179
+ close: async () => {
180
+ await writer.close();
181
+ },
182
+ abort: async reason => {
183
+ await writer.abort(reason);
184
+ }
185
+ };
186
+
187
+ // Run the callback in the background via waitUntil if available
188
+ const promise = (async () => {
189
+ try {
190
+ await callback(stream);
191
+ } catch (err) {
192
+ await stream.write(`Error: ${err.message}`);
193
+ } finally {
194
+ try {
195
+ await stream.close();
196
+ } catch {/* already closed */}
197
+ }
198
+ })();
199
+ if (this._executionCtx?.waitUntil) {
200
+ this._executionCtx.waitUntil(promise);
201
+ }
202
+ return new Response(readable, {
203
+ status: 200,
204
+ headers: {
205
+ 'Content-Type': 'text/event-stream',
206
+ 'Cache-Control': 'no-cache',
207
+ 'Connection': 'keep-alive',
208
+ ...Object.fromEntries(this._headers),
209
+ ...headers
210
+ }
211
+ });
212
+ }
213
+
214
+ /**
215
+ * Return a Server-Sent Events (SSE) streaming response
216
+ * @param {Function} callback - async (sse: { send, close }) => void
217
+ * @param {Object} [headers={}]
218
+ * @returns {Response}
219
+ *
220
+ * @example
221
+ * return c.sse(async (sse) => {
222
+ * for (let i = 0; i < 10; i++) {
223
+ * await sse.send({ data: `chunk ${i}`, event: 'progress' });
224
+ * }
225
+ * });
226
+ */
227
+ sse(callback, headers = {}) {
228
+ return this.stream(async stream => {
229
+ const sse = {
230
+ send: async ({
231
+ data,
232
+ event,
233
+ id,
234
+ retry
235
+ }) => {
236
+ let message = '';
237
+ if (id) message += `id: ${id}\n`;
238
+ if (event) message += `event: ${event}\n`;
239
+ if (retry) message += `retry: ${retry}\n`;
240
+ message += `data: ${typeof data === 'object' ? JSON.stringify(data) : data}\n\n`;
241
+ await stream.write(message);
242
+ },
243
+ close: async () => {
244
+ await stream.close();
245
+ }
246
+ };
247
+ await callback(sse);
248
+ }, {
249
+ 'Content-Type': 'text/event-stream',
250
+ ...headers
251
+ });
252
+ }
253
+
254
+ /**
255
+ * Return a 404 Not Found response
256
+ * @param {string} [message='Not Found']
257
+ * @returns {Response}
258
+ */
259
+ notFound(message = 'Not Found') {
260
+ return this.json({
261
+ error: message
262
+ }, 404);
263
+ }
264
+
265
+ /**
266
+ * Set a response header (applied to all subsequent response helpers)
267
+ * @param {string} key
268
+ * @param {string} value
269
+ * @returns {RequestContext} this — for chaining
270
+ */
271
+ header(key, value) {
272
+ this._headers.set(key, value);
273
+ return this;
274
+ }
275
+
276
+ /**
277
+ * Schedule work after the response is sent via ctx.waitUntil
278
+ * @param {Promise} promise
279
+ */
280
+ waitUntil(promise) {
281
+ if (this._executionCtx?.waitUntil) {
282
+ this._executionCtx.waitUntil(promise);
283
+ }
284
+ }
285
+
286
+ // ─── Internal ──────────────────────────────────────────────────────
287
+
288
+ /** Build a proxy object for c.req that provides Hono-style accessors */
289
+ _buildReqProxy() {
290
+ const ctx = this;
291
+ return {
292
+ /** The raw Request object */
293
+ get raw() {
294
+ return ctx._request;
295
+ },
296
+ /** HTTP method */
297
+ get method() {
298
+ return ctx._request.method;
299
+ },
300
+ /** Full URL string */
301
+ get url() {
302
+ return ctx._request.url;
303
+ },
304
+ /** Request headers */
305
+ get headers() {
306
+ return ctx._request.headers;
307
+ },
308
+ /**
309
+ * Get a route parameter
310
+ * @param {string} name
311
+ * @returns {string|undefined}
312
+ */
313
+ param(name) {
314
+ return name ? ctx._params[name] : {
315
+ ...ctx._params
316
+ };
317
+ },
318
+ /**
319
+ * Get a query string parameter
320
+ * @param {string} name
321
+ * @returns {string|null}
322
+ */
323
+ query(name) {
324
+ if (!name) {
325
+ return Object.fromEntries(ctx.url.searchParams.entries());
326
+ }
327
+ return ctx.url.searchParams.get(name);
328
+ },
329
+ /**
330
+ * Get a request header
331
+ * @param {string} name
332
+ * @returns {string|null}
333
+ */
334
+ header(name) {
335
+ return ctx._request.headers.get(name);
336
+ },
337
+ /**
338
+ * Parse body as JSON
339
+ * @returns {Promise<*>}
340
+ */
341
+ async json() {
342
+ return ctx._request.json();
343
+ },
344
+ /**
345
+ * Parse body as text
346
+ * @returns {Promise<string>}
347
+ */
348
+ async text() {
349
+ return ctx._request.text();
350
+ },
351
+ /**
352
+ * Parse body as FormData
353
+ * @returns {Promise<FormData>}
354
+ */
355
+ async formData() {
356
+ return ctx._request.formData();
357
+ },
358
+ /**
359
+ * Parse body as ArrayBuffer
360
+ * @returns {Promise<ArrayBuffer>}
361
+ */
362
+ async arrayBuffer() {
363
+ return ctx._request.arrayBuffer();
364
+ },
365
+ /**
366
+ * Get the body as a ReadableStream
367
+ * @returns {ReadableStream|null}
368
+ */
369
+ get body() {
370
+ return ctx._request.body;
371
+ },
372
+ /**
373
+ * Get the pathname
374
+ * @returns {string}
375
+ */
376
+ get path() {
377
+ return ctx.url.pathname;
378
+ }
379
+ };
380
+ }
381
+ }
382
+
383
+ /**
384
+ * Create a RequestContext instance — factory function alternative
385
+ * @param {Request} request
386
+ * @param {Object} env
387
+ * @param {ExecutionContext} ctx
388
+ * @param {Object} [params={}]
389
+ * @returns {RequestContext}
390
+ */
391
+ export function createRequestContext(request, env, ctx, params = {}) {
392
+ return new RequestContext(request, env, ctx, params);
393
+ }