@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +2 -1844
  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 +248 -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/utils/config/environment-var-normalizer.js +233 -0
  19. package/dist/validation/environmentGuard.js +172 -0
  20. package/docs/CHANGELOG.md +1877 -0
  21. package/docs/api-reference.md +153 -0
  22. package/package.json +4 -1
  23. package/scripts/repro-clodo.js +123 -0
  24. package/templates/ai-worker/package.json +19 -0
  25. package/templates/ai-worker/src/index.js +160 -0
  26. package/templates/cron-worker/package.json +19 -0
  27. package/templates/cron-worker/src/index.js +211 -0
  28. package/templates/edge-proxy/package.json +18 -0
  29. package/templates/edge-proxy/src/index.js +150 -0
  30. package/templates/minimal/package.json +17 -0
  31. package/templates/minimal/src/index.js +40 -0
  32. package/templates/queue-processor/package.json +19 -0
  33. package/templates/queue-processor/src/index.js +213 -0
  34. package/templates/rest-api/.dev.vars +2 -0
  35. package/templates/rest-api/package.json +19 -0
  36. 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
- console.log(`✅ Registered model: ${modelName}`);
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
- console.log('✅ Schema Manager initialized with existing models');
780
+
781
+ // Schema Manager initialized with 5 default models (users, magic_links, tokens, files, logs)
782
+ // Set DEBUG=true to see registration logs