@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
@@ -296,6 +296,40 @@ router.registerRoute('GET', '/api/users/:id', async (request, id) => {
296
296
  });
297
297
  ```
298
298
 
299
+ ##### Express-like Convenience Methods
300
+
301
+ For familiarity with Express patterns, use these shorthand methods:
302
+
303
+ ```javascript
304
+ // GET route
305
+ router.get('/api/users', async (request) => {
306
+ return new Response(JSON.stringify(users));
307
+ });
308
+
309
+ // POST route
310
+ router.post('/api/users', async (request) => {
311
+ const userData = await request.json();
312
+ return new Response(JSON.stringify(newUser), { status: 201 });
313
+ });
314
+
315
+ // PUT route
316
+ router.put('/api/users/:id', async (request, id) => {
317
+ return new Response(JSON.stringify(updatedUser));
318
+ });
319
+
320
+ // PATCH route
321
+ router.patch('/api/users/:id', async (request, id) => {
322
+ return new Response(JSON.stringify(patchedUser));
323
+ });
324
+
325
+ // DELETE route
326
+ router.delete('/api/users/:id', async (request, id) => {
327
+ return new Response(JSON.stringify({ success: true }));
328
+ });
329
+ ```
330
+
331
+ These methods are aliases to `registerRoute()` and support the same handler signature.
332
+
299
333
  ##### `handleRequest(method, path, request)`
300
334
  ```javascript
301
335
  const response = await router.handleRequest('GET', '/api/users/123', request);
@@ -441,6 +475,125 @@ const config = DeploymentManager.generateSecureConfig('client', 'prod');
441
475
 
442
476
  ## ⚙️ Configuration Management
443
477
 
478
+ ### Service Environment Variables (v4.4.1+)
479
+
480
+ The framework standardizes how environment variables are configured in services using a single recommended flat format.
481
+
482
+ #### Recommended Format: Flat Structure
483
+
484
+ ```javascript
485
+ // services-config.json
486
+ {
487
+ "services": [
488
+ {
489
+ "name": "api-service",
490
+ "vars": {
491
+ "API_KEY": "my-api-key",
492
+ "DEBUG": "true",
493
+ "LOG_LEVEL": "info"
494
+ },
495
+ "secrets": ["DB_PASSWORD", "JWT_SECRET"]
496
+ }
497
+ ]
498
+ }
499
+ ```
500
+
501
+ **Benefits**:
502
+ - Simple and clear
503
+ - Aligns with Wrangler.toml conventions
504
+ - Type-safe with validation
505
+ - No version-specific migrations needed
506
+
507
+ #### Variable Naming Conventions
508
+
509
+ All environment variable names must follow SCREAMING_SNAKE_CASE:
510
+
511
+ ```javascript
512
+ // ✅ Valid
513
+ API_KEY // Uppercase, underscores
514
+ DATABASE_URL // Multi-word with underscores
515
+ _INTERNAL_FLAG // Starting with underscore
516
+ VAR_123 // With numbers
517
+
518
+ // ❌ Invalid
519
+ api-key // No hyphens
520
+ api.key // No dots
521
+ apiKey // No camelCase
522
+ ```
523
+
524
+ #### Secrets Management
525
+
526
+ Specify which variables contain sensitive data:
527
+
528
+ ```javascript
529
+ {
530
+ "services": [
531
+ {
532
+ "name": "api-service",
533
+ "vars": {
534
+ "API_URL": "https://api.example.com", // Non-secret
535
+ "ENVIRONMENT": "production" // Non-secret
536
+ },
537
+ "secrets": ["API_KEY", "DB_PASSWORD"] // Sensitive values
538
+ }
539
+ ]
540
+ }
541
+ ```
542
+
543
+ Then provide secret values at deployment time via environment variables or `.dev.vars` file.
544
+
545
+ #### Deprecation Notice (v4.4.1+)
546
+
547
+ The following formats are deprecated and will be removed in v5.0.0:
548
+
549
+ **Nested Format** (Deprecated):
550
+ ```javascript
551
+ // ⚠️ Avoid: Will be removed in v5.0.0
552
+ {
553
+ "environment": {
554
+ "vars": { "API_KEY": "value" },
555
+ "secrets": ["DB_PASSWORD"]
556
+ }
557
+ }
558
+ ```
559
+
560
+ **Per-Environment Format** (Deprecated):
561
+ ```javascript
562
+ // ⚠️ Avoid: Will be removed in v5.0.0
563
+ {
564
+ "env": {
565
+ "production": { "vars": { "API_KEY": "prod-key" } },
566
+ "staging": { "vars": { "API_KEY": "staging-key" } }
567
+ }
568
+ }
569
+ ```
570
+
571
+ **Migration Path**: See [Environment Variable Standardization Guide](./env-var-standardization.md) for complete migration instructions.
572
+
573
+ #### Using `EnvironmentVarNormalizer`
574
+
575
+ The framework provides automatic normalization for backward compatibility:
576
+
577
+ ```javascript
578
+ import { EnvironmentVarNormalizer } from '@tamyla/clodo-framework';
579
+
580
+ // Normalize any config format to standard flat structure
581
+ const normalized = EnvironmentVarNormalizer.normalize(service, {
582
+ warnOnDeprecated: true, // Log deprecation warnings
583
+ throwOnConflict: false // Merge conflicting formats
584
+ });
585
+
586
+ // Validate variable naming conventions
587
+ const validation = EnvironmentVarNormalizer.validateNamingConventions(vars);
588
+ if (!validation.valid) {
589
+ console.error('Variable naming issues:', validation.issues);
590
+ }
591
+
592
+ // Check deprecation timeline
593
+ const timeline = EnvironmentVarNormalizer.getDeprecationTimeline('4.4.1');
594
+ console.log(timeline.v5_0_0.status); // "REMOVAL"
595
+ ```
596
+
444
597
  ### Domain Configuration
445
598
 
446
599
  #### `createDomainConfigSchema(domains)`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "4.4.0",
3
+ "version": "4.5.0",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -17,6 +17,7 @@
17
17
  "./schema": "./dist/schema/SchemaManager.js",
18
18
  "./modules": "./dist/modules/ModuleManager.js",
19
19
  "./routing": "./dist/routing/EnhancedRouter.js",
20
+ "./routing/context": "./dist/routing/RequestContext.js",
20
21
  "./handlers": "./dist/handlers/GenericRouteHandler.js",
21
22
  "./config": "./dist/config/index.js",
22
23
  "./config/customers": "./dist/config/customers.js",
@@ -34,9 +35,11 @@
34
35
  "./programmatic": "./dist/programmatic/index.js",
35
36
  "./api": "./dist/api/index.js",
36
37
  "./validation": "./dist/validation/index.js",
38
+ "./validation/env": "./dist/validation/environmentGuard.js",
37
39
  "./errors": "./dist/errors/index.js",
38
40
  "./testing": "./dist/testing/index.js",
39
41
  "./middleware": "./dist/middleware/index.js",
42
+ "./middleware/factories": "./dist/middleware/factories.js",
40
43
  "./modules/security": "./dist/modules/security.js",
41
44
  "./utilities": "./dist/utilities/index.js",
42
45
  "./utilities/storage": "./dist/utilities/storage/r2.js",
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Reproduction script for Express-like router.get/router.post API
4
+ *
5
+ * This script demonstrates both the original registerRoute() API
6
+ * and the new Express-like convenience methods (router.get(), router.post(), etc.)
7
+ *
8
+ * Run: node scripts/repro-clodo.js
9
+ */
10
+
11
+ import { createEnhancedRouter } from '../src/routing/EnhancedRouter.js';
12
+
13
+ console.log('🧪 CLODO Framework Router API Compatibility Test\n');
14
+ console.log('=' .repeat(60));
15
+
16
+ // Mock D1 Client
17
+ const mockD1Client = {
18
+ prepare: () => ({ bind: () => ({}) }),
19
+ };
20
+
21
+ // Create router instance
22
+ const router = createEnhancedRouter(mockD1Client, {
23
+ requireAuth: false,
24
+ allowPublicRead: true
25
+ });
26
+
27
+ console.log('\n📋 Router Instance Properties:');
28
+ console.log(' Keys:', Object.keys(router).sort());
29
+ console.log(' d1Client present:', !!router.d1Client);
30
+ console.log(' options present:', !!router.options);
31
+ console.log(' routes Map present:', router.routes instanceof Map);
32
+ console.log(' genericHandlers present:', !!router.genericHandlers);
33
+
34
+ console.log('\n📋 Express-like Methods Available:');
35
+ const expressLikeMethods = ['get', 'post', 'put', 'patch', 'delete'];
36
+ for (const method of expressLikeMethods) {
37
+ console.log(` typeof router.${method}:`, typeof router[method]);
38
+ }
39
+
40
+ console.log('\n🔧 Testing Express-like API:');
41
+
42
+ // Test 1: router.get()
43
+ console.log('\n Test 1: router.get()');
44
+ try {
45
+ router.get('/api/users', (req) => new Response('GET /api/users'));
46
+ console.log(' ✅ router.get() registered successfully');
47
+ console.log(' ✓ Route key:', 'GET /api/users');
48
+ console.log(' ✓ Handler registered:', router.routes.has('GET /api/users'));
49
+ } catch (e) {
50
+ console.log(' ❌ router.get() failed:', e.message);
51
+ }
52
+
53
+ // Test 2: router.post()
54
+ console.log('\n Test 2: router.post()');
55
+ try {
56
+ router.post('/api/users', (req) => new Response('POST /api/users', { status: 201 }));
57
+ console.log(' ✅ router.post() registered successfully');
58
+ console.log(' ✓ Route key:', 'POST /api/users');
59
+ console.log(' ✓ Handler registered:', router.routes.has('POST /api/users'));
60
+ } catch (e) {
61
+ console.log(' ❌ router.post() failed:', e.message);
62
+ }
63
+
64
+ // Test 3: router.get() with parameters
65
+ console.log('\n Test 3: router.get() with parameters');
66
+ try {
67
+ router.get('/api/users/:id', (req, id) => new Response(`GET /api/users/${id}`));
68
+ console.log(' ✅ router.get() with parameters registered successfully');
69
+ console.log(' ✓ Route key:', 'GET /api/users/:id');
70
+ console.log(' ✓ Handler registered:', router.routes.has('GET /api/users/:id'));
71
+ } catch (e) {
72
+ console.log(' ❌ router.get() with parameters failed:', e.message);
73
+ }
74
+
75
+ // Test 4: router.patch()
76
+ console.log('\n Test 4: router.patch()');
77
+ try {
78
+ router.patch('/api/users/:id', (req, id) => new Response(`PATCH /api/users/${id}`));
79
+ console.log(' ✅ router.patch() registered successfully');
80
+ console.log(' ✓ Route key:', 'PATCH /api/users/:id');
81
+ console.log(' ✓ Handler registered:', router.routes.has('PATCH /api/users/:id'));
82
+ } catch (e) {
83
+ console.log(' ❌ router.patch() failed:', e.message);
84
+ }
85
+
86
+ // Test 5: router.delete()
87
+ console.log('\n Test 5: router.delete()');
88
+ try {
89
+ router.delete('/api/users/:id', (req, id) => new Response(`DELETE /api/users/${id}`));
90
+ console.log(' ✅ router.delete() registered successfully');
91
+ console.log(' ✓ Route key:', 'DELETE /api/users/:id');
92
+ console.log(' ✓ Handler registered:', router.routes.has('DELETE /api/users/:id'));
93
+ } catch (e) {
94
+ console.log(' ❌ router.delete() failed:', e.message);
95
+ }
96
+
97
+ console.log('\n🔧 Testing Traditional registerRoute() API:');
98
+
99
+ // Test 6: registerRoute()
100
+ console.log('\n Test 6: registerRoute()');
101
+ try {
102
+ router.registerRoute('GET', '/health', (req) => new Response('OK'));
103
+ console.log(' ✅ registerRoute() works correctly');
104
+ console.log(' ✓ Route key:', 'GET /health');
105
+ console.log(' ✓ Handler registered:', router.routes.has('GET /health'));
106
+ } catch (e) {
107
+ console.log(' ❌ registerRoute() failed:', e.message);
108
+ }
109
+
110
+ console.log('\n📊 Route Summary:');
111
+ console.log(` Total routes registered: ${router.routes.size}`);
112
+ console.log(' Registered routes:');
113
+ for (const [key] of router.routes.entries()) {
114
+ console.log(` - ${key}`);
115
+ }
116
+
117
+ console.log('\n' + '='.repeat(60));
118
+ console.log('✨ All tests completed!');
119
+ console.log('\nConclusion:');
120
+ console.log(' ✅ Express-like methods (router.get, router.post, etc.) are now available');
121
+ console.log(' ✅ registerRoute() method still works');
122
+ console.log(' ✅ Both APIs are fully compatible and interchangeable');
123
+ console.log(' ✅ Framework is backward compatible with Express patterns');
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "{{SERVICE_NAME}}",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "wrangler dev",
8
+ "deploy": "wrangler deploy",
9
+ "test": "vitest"
10
+ },
11
+ "dependencies": {
12
+ "@tamyla/clodo-framework": "^4.4.1"
13
+ },
14
+ "devDependencies": {
15
+ "wrangler": "^3.0.0",
16
+ "vitest": "^2.0.0",
17
+ "@cloudflare/vitest-pool-workers": "^0.5.0"
18
+ }
19
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * AI Worker Template — @tamyla/clodo-framework
3
+ *
4
+ * A fully working Workers AI service with:
5
+ * - Text generation (chat/completion)
6
+ * - Streaming responses (SSE)
7
+ * - Embeddings generation
8
+ * - Prompt formatting
9
+ * - CORS + error handling + rate limiting
10
+ *
11
+ * Deploy with: npx wrangler deploy
12
+ */
13
+
14
+ import {
15
+ createCorsMiddleware,
16
+ createErrorHandler,
17
+ createLogger,
18
+ createRateLimitGuard,
19
+ composeMiddleware,
20
+ createEnvironmentGuard
21
+ } from '@tamyla/clodo-framework';
22
+
23
+ import {
24
+ AIClient,
25
+ Models,
26
+ createSSEStream,
27
+ streamResponse
28
+ } from '@tamyla/clodo-framework/utilities/ai';
29
+
30
+ import { formatAIPrompt } from '@tamyla/clodo-framework/utilities';
31
+
32
+ // ── Environment validation ────────────────────────────────────────────
33
+ const envGuard = createEnvironmentGuard({
34
+ required: ['AI'],
35
+ optional: ['KV_DATA', 'VECTORIZE_INDEX']
36
+ });
37
+
38
+ // ── Middleware stack ──────────────────────────────────────────────────
39
+ const middleware = composeMiddleware(
40
+ createCorsMiddleware({ origins: ['*'] }),
41
+ createLogger({ prefix: 'ai-worker', level: 'info' }),
42
+ createRateLimitGuard({ maxRequests: 50, windowMs: 60000 }),
43
+ createErrorHandler({ includeStack: false })
44
+ );
45
+
46
+ // ── Routes ────────────────────────────────────────────────────────────
47
+ const routes = {
48
+ // Health check
49
+ 'GET /health': async (req, env) => {
50
+ return jsonResponse({ status: 'healthy', model: Models.CHAT });
51
+ },
52
+
53
+ // Chat completion (non-streaming)
54
+ 'POST /api/chat': async (req, env) => {
55
+ const { messages, model, max_tokens } = await req.json();
56
+ const ai = new AIClient(env.AI);
57
+
58
+ const response = await ai.chat(messages || [], {
59
+ model: model || Models.CHAT,
60
+ max_tokens: max_tokens || 1024
61
+ });
62
+
63
+ return jsonResponse({ response: response.response, model: model || Models.CHAT });
64
+ },
65
+
66
+ // Chat completion (streaming SSE)
67
+ 'POST /api/chat/stream': async (req, env) => {
68
+ const { messages, model, max_tokens } = await req.json();
69
+ const ai = new AIClient(env.AI);
70
+
71
+ const stream = await ai.run(model || Models.CHAT, {
72
+ messages: messages || [],
73
+ max_tokens: max_tokens || 1024,
74
+ stream: true
75
+ });
76
+
77
+ return streamResponse(stream);
78
+ },
79
+
80
+ // Text generation from prompt template
81
+ 'POST /api/generate': async (req, env) => {
82
+ const { prompt, context, model, max_tokens } = await req.json();
83
+ const ai = new AIClient(env.AI);
84
+
85
+ // Format the prompt with context variables
86
+ const formattedPrompt = context ? formatAIPrompt(prompt, context) : prompt;
87
+
88
+ const response = await ai.generate(formattedPrompt, {
89
+ model: model || Models.TEXT_GENERATION,
90
+ max_tokens: max_tokens || 2048
91
+ });
92
+
93
+ return jsonResponse({ response: response.response, prompt: formattedPrompt });
94
+ },
95
+
96
+ // Generate embeddings
97
+ 'POST /api/embeddings': async (req, env) => {
98
+ const { text, texts, model } = await req.json();
99
+ const ai = new AIClient(env.AI);
100
+
101
+ const input = texts || [text];
102
+ const embeddings = await ai.embed(input, {
103
+ model: model || Models.EMBEDDINGS
104
+ });
105
+
106
+ return jsonResponse({
107
+ embeddings: embeddings.data,
108
+ model: model || Models.EMBEDDINGS,
109
+ dimensions: embeddings.data?.[0]?.length || 0
110
+ });
111
+ },
112
+
113
+ // Summarize text
114
+ 'POST /api/summarize': async (req, env) => {
115
+ const { text, max_length } = await req.json();
116
+ const ai = new AIClient(env.AI);
117
+
118
+ const result = await ai.summarize(text, {
119
+ max_length: max_length || 256
120
+ });
121
+
122
+ return jsonResponse({ summary: result.summary });
123
+ },
124
+
125
+ // List available models
126
+ 'GET /api/models': async (req, env) => {
127
+ return jsonResponse({
128
+ models: Object.entries(Models).map(([key, value]) => ({
129
+ name: key,
130
+ model: value
131
+ }))
132
+ });
133
+ }
134
+ };
135
+
136
+ // ── Helpers ───────────────────────────────────────────────────────────
137
+ function jsonResponse(data, status = 200) {
138
+ return new Response(JSON.stringify(data), {
139
+ status,
140
+ headers: { 'Content-Type': 'application/json' }
141
+ });
142
+ }
143
+
144
+ // ── Worker entry point ────────────────────────────────────────────────
145
+ export default {
146
+ async fetch(request, env, ctx) {
147
+ envGuard.check(env);
148
+
149
+ const url = new URL(request.url);
150
+ const routeKey = `${request.method} ${url.pathname}`;
151
+ const handler = routes[routeKey];
152
+
153
+ if (!handler) {
154
+ return jsonResponse({ error: 'Not Found', path: url.pathname }, 404);
155
+ }
156
+
157
+ // Execute through middleware chain
158
+ return middleware.execute(request, () => handler(request, env, ctx));
159
+ }
160
+ };
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "{{SERVICE_NAME}}",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "wrangler dev",
8
+ "deploy": "wrangler deploy",
9
+ "test": "vitest"
10
+ },
11
+ "dependencies": {
12
+ "@tamyla/clodo-framework": "^4.4.1"
13
+ },
14
+ "devDependencies": {
15
+ "wrangler": "^3.0.0",
16
+ "vitest": "^2.0.0",
17
+ "@cloudflare/vitest-pool-workers": "^0.5.0"
18
+ }
19
+ }