@tamyla/clodo-framework 4.4.1 → 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 -1851
- 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 +185 -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/validation/environmentGuard.js +172 -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
package/dist/utilities/index.js
CHANGED
|
@@ -62,4 +62,137 @@ export { ServiceBindingClient, RPCClient, ServiceRouter } from './bindings/index
|
|
|
62
62
|
// ============================================================
|
|
63
63
|
|
|
64
64
|
// Analytics Engine
|
|
65
|
-
export { AnalyticsWriter, EventTracker, MetricsCollector } from './analytics/index.js';
|
|
65
|
+
export { AnalyticsWriter, EventTracker, MetricsCollector } from './analytics/index.js';
|
|
66
|
+
|
|
67
|
+
// ============================================================
|
|
68
|
+
// STANDALONE UTILITY FUNCTIONS
|
|
69
|
+
// ============================================================
|
|
70
|
+
|
|
71
|
+
// AI Utilities
|
|
72
|
+
/**
|
|
73
|
+
* Run an AI model directly
|
|
74
|
+
* @param {Object} aiBinding - AI binding from env
|
|
75
|
+
* @param {string} model - Model name
|
|
76
|
+
* @param {Object} inputs - Model inputs
|
|
77
|
+
* @returns {Promise<Object>} Model response
|
|
78
|
+
*/
|
|
79
|
+
export async function runAIModel(aiBinding, model, inputs) {
|
|
80
|
+
const {
|
|
81
|
+
AIClient
|
|
82
|
+
} = await import('./ai/index.js');
|
|
83
|
+
const ai = new AIClient(aiBinding);
|
|
84
|
+
return ai.run(model, inputs);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Stream AI response
|
|
89
|
+
* @param {Object} aiBinding - AI binding from env
|
|
90
|
+
* @param {string} model - Model name
|
|
91
|
+
* @param {Object} inputs - Model inputs
|
|
92
|
+
* @returns {Promise<Response>} Streaming response
|
|
93
|
+
*/
|
|
94
|
+
export async function streamAIResponse(aiBinding, model, inputs) {
|
|
95
|
+
const {
|
|
96
|
+
AIClient,
|
|
97
|
+
streamResponse
|
|
98
|
+
} = await import('./ai/index.js');
|
|
99
|
+
const ai = new AIClient(aiBinding);
|
|
100
|
+
const stream = await ai.run(model, {
|
|
101
|
+
...inputs,
|
|
102
|
+
stream: true
|
|
103
|
+
});
|
|
104
|
+
return streamResponse(stream);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Format AI prompt with context
|
|
109
|
+
* @param {string} prompt - Base prompt
|
|
110
|
+
* @param {Object} context - Context data
|
|
111
|
+
* @returns {string} Formatted prompt
|
|
112
|
+
*/
|
|
113
|
+
export function formatAIPrompt(prompt, context = {}) {
|
|
114
|
+
let formatted = prompt;
|
|
115
|
+
|
|
116
|
+
// Replace placeholders like {{key}} with context values
|
|
117
|
+
for (const [key, value] of Object.entries(context)) {
|
|
118
|
+
const placeholder = new RegExp(`{{${key}}}`, 'g');
|
|
119
|
+
formatted = formatted.replace(placeholder, String(value));
|
|
120
|
+
}
|
|
121
|
+
return formatted;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Vectorize Utilities
|
|
125
|
+
/**
|
|
126
|
+
* Query vectors from Vectorize index
|
|
127
|
+
* @param {Object} vectorizeBinding - Vectorize binding from env
|
|
128
|
+
* @param {number[]} vector - Query vector
|
|
129
|
+
* @param {Object} options - Query options
|
|
130
|
+
* @returns {Promise<Object>} Query results
|
|
131
|
+
*/
|
|
132
|
+
export async function queryVectors(vectorizeBinding, vector, options = {}) {
|
|
133
|
+
const {
|
|
134
|
+
VectorStore
|
|
135
|
+
} = await import('./vectorize/index.js');
|
|
136
|
+
const store = new VectorStore(vectorizeBinding);
|
|
137
|
+
return store.query(vector, options);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Upsert vectors to Vectorize index
|
|
142
|
+
* @param {Object} vectorizeBinding - Vectorize binding from env
|
|
143
|
+
* @param {Array} vectors - Vectors to upsert
|
|
144
|
+
* @returns {Promise<Object>} Upsert results
|
|
145
|
+
*/
|
|
146
|
+
export async function upsertVectors(vectorizeBinding, vectors) {
|
|
147
|
+
const {
|
|
148
|
+
VectorStore
|
|
149
|
+
} = await import('./vectorize/index.js');
|
|
150
|
+
const store = new VectorStore(vectorizeBinding);
|
|
151
|
+
return store.upsert(vectors);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// KV Utilities
|
|
155
|
+
/**
|
|
156
|
+
* Get value from KV storage
|
|
157
|
+
* @param {Object} kvBinding - KV binding from env
|
|
158
|
+
* @param {string} key - Key to retrieve
|
|
159
|
+
* @param {Object} options - Get options
|
|
160
|
+
* @returns {Promise<*>} Retrieved value
|
|
161
|
+
*/
|
|
162
|
+
export async function getKV(kvBinding, key, options = {}) {
|
|
163
|
+
const {
|
|
164
|
+
KVStorage
|
|
165
|
+
} = await import('./kv/index.js');
|
|
166
|
+
const kv = new KVStorage(kvBinding);
|
|
167
|
+
return kv.get(key, options);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Put value in KV storage
|
|
172
|
+
* @param {Object} kvBinding - KV binding from env
|
|
173
|
+
* @param {string} key - Key to set
|
|
174
|
+
* @param {*} value - Value to store
|
|
175
|
+
* @param {Object} options - Put options
|
|
176
|
+
* @returns {Promise<void>}
|
|
177
|
+
*/
|
|
178
|
+
export async function putKV(kvBinding, key, value, options = {}) {
|
|
179
|
+
const {
|
|
180
|
+
KVStorage
|
|
181
|
+
} = await import('./kv/index.js');
|
|
182
|
+
const kv = new KVStorage(kvBinding);
|
|
183
|
+
return kv.set(key, value, options);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* List keys from KV storage
|
|
188
|
+
* @param {Object} kvBinding - KV binding from env
|
|
189
|
+
* @param {Object} options - List options
|
|
190
|
+
* @returns {Promise<Object>} List results
|
|
191
|
+
*/
|
|
192
|
+
export async function listKV(kvBinding, options = {}) {
|
|
193
|
+
const {
|
|
194
|
+
KVStorage
|
|
195
|
+
} = await import('./kv/index.js');
|
|
196
|
+
const kv = new KVStorage(kvBinding);
|
|
197
|
+
return kv.list(options);
|
|
198
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Guard — Validates Cloudflare Worker env bindings at startup
|
|
3
|
+
*
|
|
4
|
+
* Catches missing bindings early with descriptive errors instead of
|
|
5
|
+
* failing deep in handler logic with cryptic "Cannot read property of undefined".
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { createEnvironmentGuard } from '@tamyla/clodo-framework';
|
|
9
|
+
*
|
|
10
|
+
* const guard = createEnvironmentGuard({
|
|
11
|
+
* required: ['KV_DATA', 'AI', 'SECRET_KEY'],
|
|
12
|
+
* optional: ['DEBUG', 'ANTHROPIC_API_KEY'],
|
|
13
|
+
* validate: {
|
|
14
|
+
* SECRET_KEY: (v) => typeof v === 'string' && v.length >= 32
|
|
15
|
+
* }
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* // In your Worker fetch handler:
|
|
19
|
+
* export default {
|
|
20
|
+
* async fetch(request, env, ctx) {
|
|
21
|
+
* guard.check(env); // throws if bindings are missing
|
|
22
|
+
* // ... handle request
|
|
23
|
+
* }
|
|
24
|
+
* };
|
|
25
|
+
*
|
|
26
|
+
* @module @tamyla/clodo-framework/validation/environmentGuard
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} EnvironmentGuardConfig
|
|
31
|
+
* @property {string[]} required - Binding names that MUST be present
|
|
32
|
+
* @property {string[]} [optional=[]] - Binding names that MAY be present
|
|
33
|
+
* @property {Object<string, Function>} [validate={}] - Custom validators per binding
|
|
34
|
+
* @property {boolean} [throwOnMissing=true] - Throw on missing required binding (false = return report)
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
export class EnvironmentGuard {
|
|
38
|
+
/**
|
|
39
|
+
* @param {EnvironmentGuardConfig} config
|
|
40
|
+
*/
|
|
41
|
+
constructor(config = {}) {
|
|
42
|
+
this.required = config.required || [];
|
|
43
|
+
this.optional = config.optional || [];
|
|
44
|
+
this.validators = config.validate || {};
|
|
45
|
+
this.throwOnMissing = config.throwOnMissing !== false;
|
|
46
|
+
this._checked = false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate env bindings. Call once at startup or in the fetch handler.
|
|
51
|
+
* @param {Object} env - Cloudflare Worker environment
|
|
52
|
+
* @returns {{ valid: boolean, missing: string[], invalid: string[], present: string[], warnings: string[] }}
|
|
53
|
+
* @throws {Error} If throwOnMissing is true and required bindings are missing
|
|
54
|
+
*/
|
|
55
|
+
check(env) {
|
|
56
|
+
const result = {
|
|
57
|
+
valid: true,
|
|
58
|
+
missing: [],
|
|
59
|
+
invalid: [],
|
|
60
|
+
present: [],
|
|
61
|
+
warnings: []
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Check required bindings
|
|
65
|
+
for (const name of this.required) {
|
|
66
|
+
if (env[name] === undefined || env[name] === null) {
|
|
67
|
+
result.missing.push(name);
|
|
68
|
+
result.valid = false;
|
|
69
|
+
} else {
|
|
70
|
+
result.present.push(name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check optional bindings (warn if missing, don't fail)
|
|
75
|
+
for (const name of this.optional) {
|
|
76
|
+
if (env[name] === undefined || env[name] === null) {
|
|
77
|
+
result.warnings.push(`Optional binding '${name}' is not configured`);
|
|
78
|
+
} else {
|
|
79
|
+
result.present.push(name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Run custom validators
|
|
84
|
+
for (const [name, validator] of Object.entries(this.validators)) {
|
|
85
|
+
if (env[name] !== undefined && env[name] !== null) {
|
|
86
|
+
try {
|
|
87
|
+
const isValid = validator(env[name]);
|
|
88
|
+
if (!isValid) {
|
|
89
|
+
result.invalid.push(name);
|
|
90
|
+
result.valid = false;
|
|
91
|
+
}
|
|
92
|
+
} catch (err) {
|
|
93
|
+
result.invalid.push(name);
|
|
94
|
+
result.valid = false;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
this._checked = true;
|
|
99
|
+
if (!result.valid && this.throwOnMissing) {
|
|
100
|
+
const parts = [];
|
|
101
|
+
if (result.missing.length) {
|
|
102
|
+
parts.push(`Missing required bindings: ${result.missing.join(', ')}`);
|
|
103
|
+
}
|
|
104
|
+
if (result.invalid.length) {
|
|
105
|
+
parts.push(`Invalid bindings: ${result.invalid.join(', ')}`);
|
|
106
|
+
}
|
|
107
|
+
throw new Error(`[EnvironmentGuard] Environment validation failed.\n${parts.join('\n')}\n\n` + `Hint: Check your wrangler.toml bindings and .dev.vars for secrets.`);
|
|
108
|
+
}
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Create a middleware-compatible object that validates env on each request (with caching).
|
|
114
|
+
* Can be used with router.use() or composeMiddleware().
|
|
115
|
+
* @returns {Object} Middleware object with preprocess method
|
|
116
|
+
*/
|
|
117
|
+
asMiddleware() {
|
|
118
|
+
const guard = this;
|
|
119
|
+
return {
|
|
120
|
+
preprocess(request, env) {
|
|
121
|
+
if (!guard._checked) {
|
|
122
|
+
guard.check(env);
|
|
123
|
+
}
|
|
124
|
+
return null; // pass through
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Generate a TypeScript Env interface from the configured bindings.
|
|
131
|
+
* Useful for DX — generates type definitions from guard config.
|
|
132
|
+
* @param {Object} [bindingTypes={}] - Map of binding name → TypeScript type
|
|
133
|
+
* @returns {string} TypeScript interface definition
|
|
134
|
+
*/
|
|
135
|
+
generateEnvType(bindingTypes = {}) {
|
|
136
|
+
const lines = ['interface Env {'];
|
|
137
|
+
|
|
138
|
+
// Known Cloudflare binding types
|
|
139
|
+
const defaultTypes = {
|
|
140
|
+
KVNamespace: 'KVNamespace',
|
|
141
|
+
D1Database: 'D1Database',
|
|
142
|
+
R2Bucket: 'R2Bucket',
|
|
143
|
+
Ai: 'Ai',
|
|
144
|
+
VectorizeIndex: 'VectorizeIndex',
|
|
145
|
+
Queue: 'Queue',
|
|
146
|
+
DurableObjectNamespace: 'DurableObjectNamespace',
|
|
147
|
+
Fetcher: 'Fetcher',
|
|
148
|
+
AnalyticsEngineDataset: 'AnalyticsEngineDataset',
|
|
149
|
+
SendEmail: 'SendEmail',
|
|
150
|
+
Hyperdrive: 'Hyperdrive'
|
|
151
|
+
};
|
|
152
|
+
for (const name of this.required) {
|
|
153
|
+
const type = bindingTypes[name] || 'unknown';
|
|
154
|
+
lines.push(` ${name}: ${type};`);
|
|
155
|
+
}
|
|
156
|
+
for (const name of this.optional) {
|
|
157
|
+
const type = bindingTypes[name] || 'string';
|
|
158
|
+
lines.push(` ${name}?: ${type};`);
|
|
159
|
+
}
|
|
160
|
+
lines.push('}');
|
|
161
|
+
return lines.join('\n');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Create an environment guard — factory function
|
|
167
|
+
* @param {EnvironmentGuardConfig} config
|
|
168
|
+
* @returns {EnvironmentGuard}
|
|
169
|
+
*/
|
|
170
|
+
export function createEnvironmentGuard(config = {}) {
|
|
171
|
+
return new EnvironmentGuard(config);
|
|
172
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tamyla/clodo-framework",
|
|
3
|
-
"version": "4.
|
|
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
|
+
}
|