@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
@@ -1,76 +1,213 @@
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}`;
38
62
  this.routes.set(key, handler);
39
63
  }
40
64
 
65
+ /**
66
+ * Express-like convenience method: Register GET route
67
+ * @param {string} path - Route path
68
+ * @param {Function} handler - Route handler
69
+ */
70
+ get(path, handler) {
71
+ return this.registerRoute('GET', path, handler);
72
+ }
73
+
74
+ /**
75
+ * Express-like convenience method: Register POST route
76
+ * @param {string} path - Route path
77
+ * @param {Function} handler - Route handler
78
+ */
79
+ post(path, handler) {
80
+ return this.registerRoute('POST', path, handler);
81
+ }
82
+
83
+ /**
84
+ * Express-like convenience method: Register PUT route
85
+ * @param {string} path - Route path
86
+ * @param {Function} handler - Route handler
87
+ */
88
+ put(path, handler) {
89
+ return this.registerRoute('PUT', path, handler);
90
+ }
91
+
92
+ /**
93
+ * Express-like convenience method: Register PATCH route
94
+ * @param {string} path - Route path
95
+ * @param {Function} handler - Route handler
96
+ */
97
+ patch(path, handler) {
98
+ return this.registerRoute('PATCH', path, handler);
99
+ }
100
+
101
+ /**
102
+ * Express-like convenience method: Register DELETE route
103
+ * @param {string} path - Route path
104
+ * @param {Function} handler - Route handler
105
+ */
106
+ delete(path, handler) {
107
+ return this.registerRoute('DELETE', path, handler);
108
+ }
109
+
110
+ /**
111
+ * Express-like convenience method: Register OPTIONS route
112
+ * @param {string} path - Route path
113
+ * @param {Function} handler - Route handler
114
+ */
115
+ options(path, handler) {
116
+ return this.registerRoute('OPTIONS', path, handler);
117
+ }
118
+
119
+ /**
120
+ * Express-like convenience method: Register HEAD route
121
+ * @param {string} path - Route path
122
+ * @param {Function} handler - Route handler
123
+ */
124
+ head(path, handler) {
125
+ return this.registerRoute('HEAD', path, handler);
126
+ }
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
+
41
139
  /**
42
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
+ *
43
146
  * @param {string} method - HTTP method
44
147
  * @param {string} path - Request path
45
148
  * @param {Request} request - HTTP request
149
+ * @param {Object} [env] - Worker environment bindings
150
+ * @param {Object} [ctx] - Worker execution context
46
151
  * @returns {Promise<Response>} HTTP response
47
152
  */
48
- async handleRequest(method, path, request) {
49
- 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 = {};
50
157
 
51
158
  // Check for exact match first
159
+ const key = `${method.toUpperCase()} ${path}`;
52
160
  if (this.routes.has(key)) {
53
- const handler = this.routes.get(key);
54
- 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
+ }
55
174
  }
56
175
 
57
- // Check for parameterized routes
58
- for (const [routeKey, handler] of this.routes.entries()) {
59
- const [routeMethod, routePath] = routeKey.split(' ');
60
- if (routeMethod !== method.toUpperCase()) continue;
61
- const match = this._matchRoute(routePath, path);
62
- if (match) {
63
- // Add route parameters to request
64
- // @ts-ignore - Extending Request object with params
65
- request.params = match.params;
66
- return await handler(request, ...match.args);
67
- }
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
+ });
68
187
  }
69
188
 
70
- // No route found
71
- return new Response('Not Found', {
72
- status: 404
73
- });
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
+ }
74
211
  }
75
212
 
76
213
  /**
@@ -78,8 +215,10 @@ export class EnhancedRouter {
78
215
  * @private
79
216
  */
80
217
  _registerGenericRoutes() {
218
+ if (!this.genericHandlers) return;
81
219
  for (const [modelName] of schemaManager.getAllModels()) {
82
220
  const handler = this.genericHandlers[modelName];
221
+ if (!handler) continue;
83
222
  const basePath = `/api/${modelName}`;
84
223
 
85
224
  // CRUD routes
@@ -88,7 +227,9 @@ export class EnhancedRouter {
88
227
  this.registerRoute('GET', `${basePath}/:id`, (req, id) => handler.handleGet(req, id));
89
228
  this.registerRoute('PATCH', `${basePath}/:id`, (req, id) => handler.handleUpdate(req, id));
90
229
  this.registerRoute('DELETE', `${basePath}/:id`, (req, id) => handler.handleDelete(req, id));
91
- 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
+ }
92
233
  }
93
234
  }
94
235
 
@@ -115,6 +256,11 @@ export class EnhancedRouter {
115
256
  const paramName = patternPart.slice(1);
116
257
  params[paramName] = pathPart;
117
258
  args.push(pathPart);
259
+ } else if (patternPart === '*') {
260
+ // Wildcard — matches anything
261
+ params['*'] = pathParts.slice(i).join('/');
262
+ args.push(params['*']);
263
+ break;
118
264
  } else if (patternPart !== pathPart) {
119
265
  // No match
120
266
  return null;
@@ -136,23 +282,81 @@ export class EnhancedRouter {
136
282
 
137
283
  /**
138
284
  * Add middleware to the router
139
- * @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)
140
292
  */
141
- use(middleware) {
142
- // Store middleware for later use
143
- if (!this.middleware) {
144
- this.middleware = [];
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);
145
304
  }
146
- this.middleware.push(middleware);
305
+
306
+ // Rebuild the middleware executor
307
+ this._rebuildMiddlewareExecutor();
308
+ }
309
+
310
+ /**
311
+ * Rebuild the middleware executor when middleware changes
312
+ * @private
313
+ */
314
+ _rebuildMiddlewareExecutor() {
315
+ if (this.middleware.length > 0) {
316
+ this.middlewareExecutor = MiddlewareComposer.compose(...this.middleware);
317
+ } else {
318
+ this.middlewareExecutor = null;
319
+ }
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;
147
348
  }
148
349
  }
149
350
 
150
351
  /**
151
352
  * Create an enhanced router instance
152
- * @param {Object} d1Client - D1 database client
153
- * @param {Object} options - Router options
353
+ * @param {Object} [d1Client=null] - Optional D1 database client
354
+ * @param {Object} [options={}] - Router options
154
355
  * @returns {EnhancedRouter} Router instance
155
356
  */
156
- export function createEnhancedRouter(d1Client, options = {}) {
357
+ export function createEnhancedRouter(d1Client = null, options = {}) {
157
358
  return new EnhancedRouter(d1Client, options);
158
- }
359
+ }
360
+
361
+ // Re-export RequestContext for direct usage
362
+ export { RequestContext, createRequestContext } from './RequestContext.js';