@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
|
@@ -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
|
|
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}`;
|
|
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
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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';
|