@tamyla/clodo-framework 4.4.0 → 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 -1844
- 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 +248 -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/utils/config/environment-var-normalizer.js +233 -0
- package/dist/validation/environmentGuard.js +172 -0
- package/docs/CHANGELOG.md +1877 -0
- package/docs/api-reference.md +153 -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
|
@@ -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
|
+
}
|
|
@@ -56,7 +56,9 @@ export class SchemaManager {
|
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
-
|
|
59
|
+
if (typeof process !== 'undefined' && process.env?.DEBUG) {
|
|
60
|
+
console.log(`✅ Registered model: ${modelName}`);
|
|
61
|
+
}
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
/**
|
|
@@ -775,4 +777,6 @@ schemaManager.registerModel('logs', {
|
|
|
775
777
|
},
|
|
776
778
|
indexes: ['level', 'user_id', 'timestamp']
|
|
777
779
|
});
|
|
778
|
-
|
|
780
|
+
|
|
781
|
+
// Schema Manager initialized with 5 default models (users, magic_links, tokens, files, logs)
|
|
782
|
+
// Set DEBUG=true to see registration logs
|