@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.
- package/CHANGELOG.md +2 -1851
- package/README.md +44 -18
- package/dist/cli/commands/add.js +325 -0
- package/dist/config/service-schema-config.js +98 -5
- package/dist/index.js +22 -3
- package/dist/middleware/Composer.js +2 -1
- package/dist/middleware/factories.js +445 -0
- package/dist/middleware/index.js +4 -1
- package/dist/modules/ModuleManager.js +6 -2
- package/dist/routing/EnhancedRouter.js +185 -44
- package/dist/routing/RequestContext.js +393 -0
- package/dist/schema/SchemaManager.js +6 -2
- package/dist/service-management/generators/code/ServiceMiddlewareGenerator.js +79 -223
- package/dist/service-management/generators/code/WorkerIndexGenerator.js +241 -98
- package/dist/service-management/generators/config/WranglerTomlGenerator.js +130 -89
- package/dist/simple-api.js +4 -4
- package/dist/utilities/index.js +134 -1
- package/dist/validation/environmentGuard.js +172 -0
- package/package.json +4 -1
- package/scripts/repro-clodo.js +123 -0
- package/templates/ai-worker/package.json +19 -0
- package/templates/ai-worker/src/index.js +160 -0
- package/templates/cron-worker/package.json +19 -0
- package/templates/cron-worker/src/index.js +211 -0
- package/templates/edge-proxy/package.json +18 -0
- package/templates/edge-proxy/src/index.js +150 -0
- package/templates/minimal/package.json +17 -0
- package/templates/minimal/src/index.js +40 -0
- package/templates/queue-processor/package.json +19 -0
- package/templates/queue-processor/src/index.js +213 -0
- package/templates/rest-api/.dev.vars +2 -0
- package/templates/rest-api/package.json +19 -0
- 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
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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.
|
|
42
|
+
this.middleware = [];
|
|
43
|
+
this.scopedMiddleware = new Map(); // path prefix → middleware[]
|
|
44
|
+
this.middlewareExecutor = null;
|
|
23
45
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
-
|
|
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
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
314
|
+
_rebuildMiddlewareExecutor() {
|
|
315
|
+
if (this.middleware.length > 0) {
|
|
316
|
+
this.middlewareExecutor = MiddlewareComposer.compose(...this.middleware);
|
|
317
|
+
} else {
|
|
318
|
+
this.middlewareExecutor = null;
|
|
208
319
|
}
|
|
209
|
-
|
|
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
|
+
}
|