@tamyla/clodo-framework 4.0.13 → 4.0.14
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 +11 -0
- package/README.md +7 -0
- package/dist/cli/commands/create.js +2 -1
- package/dist/middleware/Composer.js +38 -0
- package/dist/middleware/Registry.js +14 -0
- package/dist/middleware/index.js +3 -0
- package/dist/middleware/shared/basicAuth.js +21 -0
- package/dist/middleware/shared/cors.js +28 -0
- package/dist/middleware/shared/index.js +3 -0
- package/dist/middleware/shared/logging.js +14 -0
- package/dist/service-management/GenerationEngine.js +13 -2
- package/dist/service-management/ServiceOrchestrator.js +6 -2
- package/dist/service-management/generators/code/ServiceMiddlewareGenerator.js +156 -10
- package/dist/service-management/generators/code/WorkerIndexGenerator.js +75 -9
- package/dist/simple-api.js +32 -1
- package/docs/MIDDLEWARE_MIGRATION_SUMMARY.md +121 -0
- package/package.json +4 -1
- package/scripts/DEPLOY_COMMAND_NEW.js +128 -0
- package/scripts/README-automated-testing-suite.md +356 -0
- package/scripts/README-test-clodo-deployment.md +157 -0
- package/scripts/README.md +50 -0
- package/scripts/analyze-imports.ps1 +104 -0
- package/scripts/analyze-mixed-code.js +163 -0
- package/scripts/analyze-mixed-rationale.js +149 -0
- package/scripts/automated-testing-suite.js +776 -0
- package/scripts/deployment/README.md +31 -0
- package/scripts/deployment/deploy-domain.ps1 +449 -0
- package/scripts/deployment/deploy-staging.js +120 -0
- package/scripts/deployment/validate-staging.js +166 -0
- package/scripts/diagnose-imports.js +362 -0
- package/scripts/framework-diagnostic.js +368 -0
- package/scripts/migration/migrate-middleware-legacy-to-contract.js +47 -0
- package/scripts/post-publish-test.js +663 -0
- package/scripts/scan-worker-issues.js +52 -0
- package/scripts/service-management/README.md +27 -0
- package/scripts/service-management/setup-interactive.ps1 +693 -0
- package/scripts/test-clodo-deployment.js +588 -0
- package/scripts/test-downstream-install.js +237 -0
- package/scripts/test-local-package.ps1 +126 -0
- package/scripts/test-local-package.sh +166 -0
- package/scripts/test-package.js +339 -0
- package/scripts/testing/README.md +49 -0
- package/scripts/testing/test-first.ps1 +0 -0
- package/scripts/testing/test-first50.ps1 +0 -0
- package/scripts/testing/test.ps1 +0 -0
- package/scripts/utilities/README.md +61 -0
- package/scripts/utilities/check-bin.js +8 -0
- package/scripts/utilities/check-bundle.js +23 -0
- package/scripts/utilities/check-dist-imports.js +65 -0
- package/scripts/utilities/check-import-paths.js +191 -0
- package/scripts/utilities/cleanup-cli.js +159 -0
- package/scripts/utilities/deployment-helpers.ps1 +199 -0
- package/scripts/utilities/fix-dist-imports.js +135 -0
- package/scripts/utilities/generate-secrets.js +159 -0
- package/scripts/utilities/safe-push.ps1 +51 -0
- package/scripts/utilities/setup-helpers.ps1 +206 -0
- package/scripts/utilities/test-packaged-artifact.js +92 -0
- package/scripts/utilities/validate-dist-imports.js +189 -0
- package/scripts/utilities/validate-schema.js +102 -0
- package/scripts/verify-exports.js +193 -0
- package/scripts/verify-worker-safety.js +73 -0
- package/types/middleware.d.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
## [4.0.14](https://github.com/tamylaa/clodo-framework/compare/v4.0.13...v4.0.14) (2026-01-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* middleware packaging, generators, tests, and packaged-artifact smoke test ([b001434](https://github.com/tamylaa/clodo-framework/commit/b001434697da2145e8778b5c0afd02275cd1604e))
|
|
7
|
+
* resolve Windows filesystem timing issues in tests ([7092ca6](https://github.com/tamylaa/clodo-framework/commit/7092ca6ffc230a19d25c96333c0313ead1739f15))
|
|
8
|
+
|
|
1
9
|
## [4.0.13](https://github.com/tamylaa/clodo-framework/compare/v4.0.12...v4.0.13) (2025-12-17)
|
|
2
10
|
|
|
3
11
|
|
|
@@ -173,6 +181,9 @@ Benefits:
|
|
|
173
181
|
- Moved 13 root-level documentation files to appropriate i-docs categories
|
|
174
182
|
- Root directory now contains only essential project files (config, package.json, README, etc.)
|
|
175
183
|
|
|
184
|
+
* **Documentation**: Added `docs/HOWTO_CONSUME_CLODO_FRAMEWORK.md` — a concise consumer guide covering CLI usage, public exports, packaging troubleshooting, and recommended consumption patterns.
|
|
185
|
+
* **Middleware Architecture**: Introduce contract-first middleware generation (v4.1 candidate) with `MiddlewareRegistry` and `MiddlewareComposer` to reduce duplication and improve testability. Added a migration tool `scripts/migration/migrate-middleware-legacy-to-contract.js` and a `--middleware-strategy` CLI flag to opt into `legacy` generator output.
|
|
186
|
+
|
|
176
187
|
* **Configuration Management**: Eliminated hard-coded values from source code
|
|
177
188
|
- Moved domain defaults from ServiceCreator to `validation-config.json`
|
|
178
189
|
- Added configuration hierarchy: CLI option → config file → fallback default
|
package/README.md
CHANGED
|
@@ -199,6 +199,13 @@ import { CloudflareAPI } from '@tamyla/clodo-framework/utils/cloudflare';
|
|
|
199
199
|
|
|
200
200
|
If you need functionality that's currently only in `bin/`, please open an issue - we'll consider adding it to the public API.
|
|
201
201
|
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## How to consume (quick)
|
|
205
|
+
A short guide and best practices for consuming `@tamyla/clodo-framework` are available in the docs: [docs/HOWTO_CONSUME_CLODO_FRAMEWORK.md](./docs/HOWTO_CONSUME_CLODO_FRAMEWORK.md). This file includes quickstart steps, public export guidance, CLI usage, and troubleshooting tips for packaged artifacts.
|
|
206
|
+
|
|
207
|
+
If you'd like this information added to `clodo.dev`, I can prepare a small website PR as well.
|
|
208
|
+
|
|
202
209
|
## Project Structure
|
|
203
210
|
|
|
204
211
|
The project is organized for maximum clarity and maintainability:
|
|
@@ -9,7 +9,7 @@ import chalk from 'chalk';
|
|
|
9
9
|
import { Clodo, ConfigLoader } from '@tamyla/clodo-framework';
|
|
10
10
|
import { StandardOptions } from '../../lib/shared/utils/cli-options.js';
|
|
11
11
|
export function registerCreateCommand(program) {
|
|
12
|
-
const command = program.command('create').description('Create a new Clodo service with conversational setup').option('-n, --non-interactive', 'Run in non-interactive mode with all required parameters').option('--service-name <name>', 'Service name (required in non-interactive mode)').option('--service-type <type>', 'Service type: data-service, auth-service, content-service, api-gateway, generic', 'generic').option('--domain-name <domain>', 'Domain name (required in non-interactive mode)').option('--cloudflare-token <token>', 'Cloudflare API token (required in non-interactive mode)').option('--cloudflare-account-id <id>', 'Cloudflare account ID (required in non-interactive mode)').option('--cloudflare-zone-id <id>', 'Cloudflare zone ID (required in non-interactive mode)').option('--environment <env>', 'Target environment: development, staging, production', 'development').option('--output-path <path>', 'Output directory for generated service', '.').option('--template-path <path>', 'Path to service templates', './templates').option('--force', 'Skip confirmation prompts').option('--validate', 'Validate service after creation');
|
|
12
|
+
const command = program.command('create').description('Create a new Clodo service with conversational setup').option('-n, --non-interactive', 'Run in non-interactive mode with all required parameters').option('--service-name <name>', 'Service name (required in non-interactive mode)').option('--service-type <type>', 'Service type: data-service, auth-service, content-service, api-gateway, generic', 'generic').option('--domain-name <domain>', 'Domain name (required in non-interactive mode)').option('--cloudflare-token <token>', 'Cloudflare API token (required in non-interactive mode)').option('--cloudflare-account-id <id>', 'Cloudflare account ID (required in non-interactive mode)').option('--cloudflare-zone-id <id>', 'Cloudflare zone ID (required in non-interactive mode)').option('--environment <env>', 'Target environment: development, staging, production', 'development').option('--output-path <path>', 'Output directory for generated service', '.').option('--template-path <path>', 'Path to service templates', './templates').option('--middleware-strategy <strategy>', 'Middleware generation strategy: contract|legacy', 'contract').option('--force', 'Skip confirmation prompts').option('--validate', 'Validate service after creation');
|
|
13
13
|
|
|
14
14
|
// Add standard options (--verbose, --quiet, --json, --no-color, --config-file)
|
|
15
15
|
StandardOptions.define(command).action(async options => {
|
|
@@ -40,6 +40,7 @@ export function registerCreateCommand(program) {
|
|
|
40
40
|
domain: mergedOptions.domainName,
|
|
41
41
|
environment: mergedOptions.environment,
|
|
42
42
|
outputPath: mergedOptions.outputPath,
|
|
43
|
+
middlewareStrategy: mergedOptions.middlewareStrategy,
|
|
43
44
|
interactive: !mergedOptions.nonInteractive,
|
|
44
45
|
credentials: {
|
|
45
46
|
token: mergedOptions.cloudflareToken,
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Middleware composer that runs middleware in sequence and supports short-circuiting
|
|
2
|
+
export class MiddlewareComposer {
|
|
3
|
+
static compose(...middlewares) {
|
|
4
|
+
const chain = middlewares.filter(Boolean);
|
|
5
|
+
return {
|
|
6
|
+
async execute(request, handler) {
|
|
7
|
+
// Preprocess/auth/validate phases
|
|
8
|
+
for (const m of chain) {
|
|
9
|
+
if (typeof m.preprocess === 'function') {
|
|
10
|
+
const res = await m.preprocess(request);
|
|
11
|
+
if (res) return res;
|
|
12
|
+
}
|
|
13
|
+
if (typeof m.authenticate === 'function') {
|
|
14
|
+
const res = await m.authenticate(request);
|
|
15
|
+
if (res) return res;
|
|
16
|
+
}
|
|
17
|
+
if (typeof m.validate === 'function') {
|
|
18
|
+
const res = await m.validate(request);
|
|
19
|
+
if (res) return res;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Call the final handler
|
|
24
|
+
let response = await handler(request);
|
|
25
|
+
|
|
26
|
+
// Postprocess in reverse order
|
|
27
|
+
for (const m of chain.slice().reverse()) {
|
|
28
|
+
if (typeof m.postprocess === 'function') {
|
|
29
|
+
const updated = await m.postprocess(response);
|
|
30
|
+
// Allow middleware to replace response
|
|
31
|
+
if (updated instanceof Response) response = updated;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return response;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Registry for service-specific middleware implementations
|
|
2
|
+
export class MiddlewareRegistry {
|
|
3
|
+
static implementations = new Map();
|
|
4
|
+
static register(serviceName, middleware) {
|
|
5
|
+
if (!serviceName) throw new Error('serviceName is required');
|
|
6
|
+
this.implementations.set(serviceName, middleware);
|
|
7
|
+
}
|
|
8
|
+
static get(serviceName) {
|
|
9
|
+
return this.implementations.get(serviceName) || null;
|
|
10
|
+
}
|
|
11
|
+
static clear() {
|
|
12
|
+
this.implementations.clear();
|
|
13
|
+
}
|
|
14
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function basicAuth({
|
|
2
|
+
realm = 'Restricted'
|
|
3
|
+
} = {}) {
|
|
4
|
+
return {
|
|
5
|
+
authenticate(request) {
|
|
6
|
+
const auth = request.headers.get('Authorization');
|
|
7
|
+
if (!auth) {
|
|
8
|
+
return new Response(JSON.stringify({
|
|
9
|
+
error: 'Unauthorized'
|
|
10
|
+
}), {
|
|
11
|
+
status: 401,
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
'WWW-Authenticate': `Basic realm="${realm}"`
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function cors(options = {}) {
|
|
2
|
+
const origin = options.origin || '*';
|
|
3
|
+
const allowMethods = options.methods || 'GET, POST, PUT, DELETE, OPTIONS';
|
|
4
|
+
const allowHeaders = options.headers || 'Content-Type, Authorization';
|
|
5
|
+
return {
|
|
6
|
+
preprocess(request) {
|
|
7
|
+
if (request.method === 'OPTIONS') {
|
|
8
|
+
const headers = new Headers();
|
|
9
|
+
headers.set('Access-Control-Allow-Origin', origin);
|
|
10
|
+
headers.set('Access-Control-Allow-Methods', allowMethods);
|
|
11
|
+
headers.set('Access-Control-Allow-Headers', allowHeaders);
|
|
12
|
+
return new Response(null, {
|
|
13
|
+
status: 204,
|
|
14
|
+
headers
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
},
|
|
19
|
+
postprocess(response) {
|
|
20
|
+
const headers = new Headers(response.headers);
|
|
21
|
+
headers.set('Access-Control-Allow-Origin', origin);
|
|
22
|
+
return new Response(response.body, {
|
|
23
|
+
...response,
|
|
24
|
+
headers
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function logging(options = {}) {
|
|
2
|
+
const level = options.level || 'info';
|
|
3
|
+
return {
|
|
4
|
+
preprocess(request) {
|
|
5
|
+
try {
|
|
6
|
+
const path = new URL(request.url).pathname;
|
|
7
|
+
console.log(`[${new Date().toISOString()}] ${level.toUpperCase()} ${request.method} ${path}`);
|
|
8
|
+
} catch (e) {
|
|
9
|
+
// Best-effort logging - do not throw
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
}
|
|
@@ -52,6 +52,8 @@ export class GenerationEngine {
|
|
|
52
52
|
this.templatesDir = options.templatesDir || join(__dirname, '..', '..', 'templates');
|
|
53
53
|
this.outputDir = options.outputDir || process.cwd();
|
|
54
54
|
this.force = options.force || false;
|
|
55
|
+
// Default middleware strategy: 'contract' (can be overridden per-generation)
|
|
56
|
+
this.middlewareStrategy = options.middlewareStrategy || 'contract';
|
|
55
57
|
|
|
56
58
|
// Initialize generator registry for centralized management
|
|
57
59
|
this.generatorRegistry = new GeneratorRegistry({
|
|
@@ -192,7 +194,7 @@ export class GenerationEngine {
|
|
|
192
194
|
},
|
|
193
195
|
generateServiceMiddleware: {
|
|
194
196
|
generator: 'serviceMiddlewareGenerator',
|
|
195
|
-
returnPath: 'src/middleware.js'
|
|
197
|
+
returnPath: 'src/middleware/service-middleware.js'
|
|
196
198
|
},
|
|
197
199
|
generateServiceUtils: {
|
|
198
200
|
generator: 'serviceUtilsGenerator',
|
|
@@ -283,6 +285,8 @@ export class GenerationEngine {
|
|
|
283
285
|
// Only create proxy method if it doesn't already exist
|
|
284
286
|
if (!this[methodName]) {
|
|
285
287
|
this[methodName] = async function (...args) {
|
|
288
|
+
// Allow per-call overrides (apply BEFORE building context so context picks it up)
|
|
289
|
+
if (args[0] && args[0].middlewareStrategy) this.middlewareStrategy = args[0].middlewareStrategy;
|
|
286
290
|
const context = this.buildContext(methodName, ...args);
|
|
287
291
|
const generator = this[config.generator];
|
|
288
292
|
const result = await generator.generate(context);
|
|
@@ -315,10 +319,12 @@ export class GenerationEngine {
|
|
|
315
319
|
}
|
|
316
320
|
|
|
317
321
|
// For most methods: (coreInputs, confirmedValues, servicePath)
|
|
322
|
+
// Include middlewareStrategy so generators can adapt their output
|
|
318
323
|
return {
|
|
319
324
|
coreInputs: args[0],
|
|
320
325
|
confirmedValues: args[1],
|
|
321
|
-
servicePath: args[2]
|
|
326
|
+
servicePath: args[2],
|
|
327
|
+
middlewareStrategy: this.middlewareStrategy
|
|
322
328
|
};
|
|
323
329
|
}
|
|
324
330
|
|
|
@@ -334,6 +340,11 @@ export class GenerationEngine {
|
|
|
334
340
|
outputPath: this.outputDir,
|
|
335
341
|
...options
|
|
336
342
|
};
|
|
343
|
+
|
|
344
|
+
// Allow per-generation override of middleware strategy
|
|
345
|
+
if (config.middlewareStrategy) {
|
|
346
|
+
this.middlewareStrategy = config.middlewareStrategy;
|
|
347
|
+
}
|
|
337
348
|
console.log('⚙️ Tier 3: Automated Generation');
|
|
338
349
|
console.log('Generating 67+ configuration files and service components...\n');
|
|
339
350
|
try {
|
|
@@ -25,6 +25,8 @@ export class ServiceOrchestrator {
|
|
|
25
25
|
this.interactive = options.interactive !== false;
|
|
26
26
|
this.outputPath = options.outputPath || '.';
|
|
27
27
|
this.templatePath = options.templatePath || './templates';
|
|
28
|
+
// Middleware strategy for generation: 'contract' (default) or 'legacy'
|
|
29
|
+
this.middlewareStrategy = options.middlewareStrategy || 'contract';
|
|
28
30
|
|
|
29
31
|
// Initialize modular handler components
|
|
30
32
|
this.inputHandler = new InputHandler({
|
|
@@ -63,7 +65,8 @@ export class ServiceOrchestrator {
|
|
|
63
65
|
console.log(chalk.yellow('⚙️ Tier 3: Automated Generation'));
|
|
64
66
|
console.log(chalk.white('Generating 67 configuration files and service components...\n'));
|
|
65
67
|
const generationResult = await this.generationHandler.generateService(coreInputs, confirmedValues, {
|
|
66
|
-
outputPath: this.outputPath
|
|
68
|
+
outputPath: this.outputPath,
|
|
69
|
+
middlewareStrategy: this.middlewareStrategy
|
|
67
70
|
});
|
|
68
71
|
|
|
69
72
|
// Display results
|
|
@@ -87,7 +90,8 @@ export class ServiceOrchestrator {
|
|
|
87
90
|
|
|
88
91
|
// Generate service using GenerationHandler
|
|
89
92
|
const generationResult = await this.generationHandler.generateService(coreInputs, confirmedValues, {
|
|
90
|
-
outputPath: this.outputPath
|
|
93
|
+
outputPath: this.outputPath,
|
|
94
|
+
middlewareStrategy: this.middlewareStrategy
|
|
91
95
|
});
|
|
92
96
|
console.log(chalk.green(`✓ Service "${coreInputs.serviceName}" created successfully`));
|
|
93
97
|
} catch (error) {
|
|
@@ -21,8 +21,13 @@ export class ServiceMiddlewareGenerator extends BaseGenerator {
|
|
|
21
21
|
if (!this.shouldGenerate(context)) {
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
|
|
25
|
+
// Allow legacy strategy if requested by context
|
|
26
|
+
const strategy = context && context.middlewareStrategy ? context.middlewareStrategy : 'contract';
|
|
27
|
+
let middlewareContent;
|
|
28
|
+
if (strategy === 'legacy') {
|
|
29
|
+
middlewareContent = `/**
|
|
30
|
+
* ${confirmedValues.displayName} - Service Middleware (LEGACY)
|
|
26
31
|
*
|
|
27
32
|
* Generated by Clodo Framework GenerationEngine
|
|
28
33
|
* Service Type: ${coreInputs.serviceType}
|
|
@@ -47,19 +52,19 @@ export function createServiceMiddleware(serviceConfig, env) {
|
|
|
47
52
|
headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
|
48
53
|
}
|
|
49
54
|
|
|
50
|
-
${confirmedValues.features.logging ? `
|
|
55
|
+
${confirmedValues.features && confirmedValues.features.logging ? `
|
|
51
56
|
// Request logging
|
|
52
|
-
console.log(
|
|
57
|
+
console.log('[' + new Date().toISOString() + '] ' + request.method + ' ' + request.url);` : ''}
|
|
53
58
|
|
|
54
|
-
${confirmedValues.features.rateLimiting ? `
|
|
59
|
+
${confirmedValues.features && confirmedValues.features.rateLimiting ? `
|
|
55
60
|
// Rate limiting (placeholder - implement based on requirements)
|
|
56
61
|
// This would typically check request frequency and block if over limit` : ''}
|
|
57
62
|
|
|
58
|
-
${confirmedValues.features.authentication ? `
|
|
63
|
+
${confirmedValues.features && confirmedValues.features.authentication ? `
|
|
59
64
|
// Authentication middleware (placeholder)
|
|
60
65
|
// This would validate JWT tokens, API keys, etc.` : ''}
|
|
61
66
|
|
|
62
|
-
${confirmedValues.features.authorization ? `
|
|
67
|
+
${confirmedValues.features && confirmedValues.features.authorization ? `
|
|
63
68
|
// Authorization middleware (placeholder)
|
|
64
69
|
// This would check user permissions and roles` : ''}
|
|
65
70
|
|
|
@@ -77,11 +82,11 @@ export function createServiceMiddleware(serviceConfig, env) {
|
|
|
77
82
|
headers.set('X-Version', '${confirmedValues.version}');
|
|
78
83
|
headers.set('X-Response-Time', Date.now().toString());
|
|
79
84
|
|
|
80
|
-
${confirmedValues.features.monitoring ? `
|
|
85
|
+
${confirmedValues.features && confirmedValues.features.monitoring ? `
|
|
81
86
|
// Response monitoring
|
|
82
|
-
console.log(
|
|
87
|
+
console.log('Response: ' + response.status + ' (' + Date.now() + 'ms)');` : ''}
|
|
83
88
|
|
|
84
|
-
${confirmedValues.features.caching ? `
|
|
89
|
+
${confirmedValues.features && confirmedValues.features.caching ? `
|
|
85
90
|
// Cache headers (placeholder - implement based on content type)
|
|
86
91
|
if (response.status === 200) {
|
|
87
92
|
headers.set('Cache-Control', 'public, max-age=300'); // 5 minutes
|
|
@@ -95,6 +100,35 @@ export function createServiceMiddleware(serviceConfig, env) {
|
|
|
95
100
|
};
|
|
96
101
|
}
|
|
97
102
|
`;
|
|
103
|
+
} else {
|
|
104
|
+
middlewareContent = `/**
|
|
105
|
+
* ${confirmedValues.displayName} - Service Middleware
|
|
106
|
+
*
|
|
107
|
+
* Generated by Clodo Framework GenerationEngine
|
|
108
|
+
* Service Type: ${coreInputs.serviceType}
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
// Service middleware contract skeleton - minimal and opt-in
|
|
112
|
+
export default class ${confirmedValues.packageName || confirmedValues.serviceName ? confirmedValues.serviceName : 'Service'}Middleware {
|
|
113
|
+
// Implement only the hooks you need
|
|
114
|
+
async preprocess(request) { return null; }
|
|
115
|
+
async authenticate(request) { return null; }
|
|
116
|
+
async validate(request) { return null; }
|
|
117
|
+
async postprocess(response) { return response; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Optional registration helper - Worker generator will call this to register
|
|
121
|
+
export function registerMiddleware(registry, serviceName) {
|
|
122
|
+
if (!registry || typeof registry.register !== 'function') return;
|
|
123
|
+
try {
|
|
124
|
+
registry.register(serviceName || '${coreInputs.serviceName}', new (exports.default || ${confirmedValues.packageName || confirmedValues.serviceName ? confirmedValues.serviceName : 'Service'}Middleware)());
|
|
125
|
+
} catch (e) {
|
|
126
|
+
// Non-fatal - allow services to manually register if needed
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
`;
|
|
131
|
+
}
|
|
98
132
|
const filePath = join(servicePath, 'src', 'middleware', 'service-middleware.js');
|
|
99
133
|
|
|
100
134
|
// Ensure directory exists
|
|
@@ -104,6 +138,118 @@ export function createServiceMiddleware(serviceConfig, env) {
|
|
|
104
138
|
});
|
|
105
139
|
writeFileSync(filePath, middlewareContent, 'utf8');
|
|
106
140
|
this.logger.info(`Generated: ${filePath}`);
|
|
141
|
+
|
|
142
|
+
// Also generate a lightweight runtime helper for middleware composition and registry
|
|
143
|
+
const runtimeContent = `// Lightweight middleware runtime for generated services
|
|
144
|
+
export const MiddlewareRegistry = (() => {
|
|
145
|
+
const map = new Map();
|
|
146
|
+
return {
|
|
147
|
+
register(serviceName, instance) { map.set(serviceName, instance); },
|
|
148
|
+
get(serviceName) { return map.get(serviceName) || null; },
|
|
149
|
+
clear() { map.clear(); }
|
|
150
|
+
};
|
|
151
|
+
})();
|
|
152
|
+
|
|
153
|
+
export const MiddlewareComposer = {
|
|
154
|
+
compose(...middlewares) {
|
|
155
|
+
const chain = middlewares.filter(Boolean);
|
|
156
|
+
return {
|
|
157
|
+
async execute(request, handler) {
|
|
158
|
+
let req = request;
|
|
159
|
+
for (const m of chain) {
|
|
160
|
+
if (typeof m.preprocess === 'function') {
|
|
161
|
+
const res = await m.preprocess(req);
|
|
162
|
+
if (res) return res;
|
|
163
|
+
}
|
|
164
|
+
if (typeof m.authenticate === 'function') {
|
|
165
|
+
const res = await m.authenticate(req);
|
|
166
|
+
if (res) return res;
|
|
167
|
+
}
|
|
168
|
+
if (typeof m.validate === 'function') {
|
|
169
|
+
const res = await m.validate(req);
|
|
170
|
+
if (res) return res;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
let response = await handler(req);
|
|
174
|
+
for (const m of chain.slice().reverse()) {
|
|
175
|
+
if (typeof m.postprocess === 'function') {
|
|
176
|
+
const updated = await m.postprocess(response);
|
|
177
|
+
if (updated instanceof Response) response = updated;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return response;
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
`;
|
|
186
|
+
const runtimePath = join(servicePath, 'src', 'middleware', 'runtime.js');
|
|
187
|
+
writeFileSync(runtimePath, runtimeContent, 'utf8');
|
|
188
|
+
this.logger.info(`Generated: ${runtimePath}`);
|
|
189
|
+
|
|
190
|
+
// Minimal shared implementations (copied into generated service for self-containment)
|
|
191
|
+
const sharedDir = join(servicePath, 'src', 'middleware', 'shared');
|
|
192
|
+
mkdirSync(sharedDir, {
|
|
193
|
+
recursive: true
|
|
194
|
+
});
|
|
195
|
+
const corsContent = `export function cors(options = {}) {
|
|
196
|
+
const origin = options.origin || '*';
|
|
197
|
+
const allowMethods = options.methods || 'GET, POST, PUT, DELETE, OPTIONS';
|
|
198
|
+
const allowHeaders = options.headers || 'Content-Type, Authorization';
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
preprocess(request) {
|
|
202
|
+
if (request.method === 'OPTIONS') {
|
|
203
|
+
const headers = new Headers();
|
|
204
|
+
headers.set('Access-Control-Allow-Origin', origin);
|
|
205
|
+
headers.set('Access-Control-Allow-Methods', allowMethods);
|
|
206
|
+
headers.set('Access-Control-Allow-Headers', allowHeaders);
|
|
207
|
+
return new Response(null, { status: 204, headers });
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
},
|
|
211
|
+
postprocess(response) {
|
|
212
|
+
const headers = new Headers(response.headers);
|
|
213
|
+
headers.set('Access-Control-Allow-Origin', origin);
|
|
214
|
+
return new Response(response.body, { ...response, headers });
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
`;
|
|
219
|
+
const loggingContent = `export function logging(options = {}) {
|
|
220
|
+
const level = options.level || 'info';
|
|
221
|
+
return {
|
|
222
|
+
preprocess(request) {
|
|
223
|
+
try {
|
|
224
|
+
const path = new URL(request.url).pathname;
|
|
225
|
+
console.log('[' + new Date().toISOString() + '] ' + level.toUpperCase() + ' ' + request.method + ' ' + path);
|
|
226
|
+
} catch (e) {}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
`;
|
|
232
|
+
const basicAuthContent = `export function basicAuth({ realm = 'Restricted' } = {}) {
|
|
233
|
+
return {
|
|
234
|
+
authenticate(request) {
|
|
235
|
+
const auth = request.headers.get('Authorization');
|
|
236
|
+
if (!auth) {
|
|
237
|
+
return new Response(JSON.stringify({ error: 'Unauthorized' }), {
|
|
238
|
+
status: 401,
|
|
239
|
+
headers: { 'Content-Type': 'application/json', 'WWW-Authenticate': 'Basic realm="' + realm + '"' }
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
`;
|
|
247
|
+
writeFileSync(join(sharedDir, 'cors.js'), corsContent, 'utf8');
|
|
248
|
+
writeFileSync(join(sharedDir, 'logging.js'), loggingContent, 'utf8');
|
|
249
|
+
writeFileSync(join(sharedDir, 'basicAuth.js'), basicAuthContent, 'utf8');
|
|
250
|
+
const indexContent = `export { cors } from './cors.js';\nexport { logging } from './logging.js';\nexport { basicAuth } from './basicAuth.js';\n`;
|
|
251
|
+
writeFileSync(join(sharedDir, 'index.js'), indexContent, 'utf8');
|
|
252
|
+
this.logger.info(`Generated shared middleware in: ${sharedDir}`);
|
|
107
253
|
return filePath;
|
|
108
254
|
}
|
|
109
255
|
|
|
@@ -70,7 +70,8 @@ export class WorkerIndexGenerator extends BaseGenerator {
|
|
|
70
70
|
|
|
71
71
|
import { domains } from '../config/domains.js';
|
|
72
72
|
import { createServiceHandlers } from '../handlers/service-handlers.js';
|
|
73
|
-
import {
|
|
73
|
+
import { MiddlewareRegistry, MiddlewareComposer } from '../middleware/runtime.js';
|
|
74
|
+
import * as Shared from '../middleware/shared/index.js';
|
|
74
75
|
|
|
75
76
|
export default {
|
|
76
77
|
async fetch(request, env, ctx) {
|
|
@@ -78,16 +79,81 @@ export default {
|
|
|
78
79
|
// Get service configuration
|
|
79
80
|
const serviceConfig = domains['${coreInputs.serviceName}'];
|
|
80
81
|
|
|
81
|
-
//
|
|
82
|
-
const
|
|
83
|
-
|
|
82
|
+
// Build shared middleware instances
|
|
83
|
+
const sharedMiddlewares = [
|
|
84
|
+
Shared.cors({ origin: serviceConfig.corsPolicy || '*' }),
|
|
85
|
+
Shared.logging({ level: serviceConfig.logLevel || 'info' })
|
|
86
|
+
];
|
|
84
87
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
// Lazy-load service middleware and support legacy factory compatibility
|
|
89
|
+
let serviceMiddlewareInstance = null;
|
|
90
|
+
let legacyFactory = null;
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
try {
|
|
93
|
+
const mod = await import('../middleware/service-middleware.js');
|
|
94
|
+
|
|
95
|
+
if (mod?.registerMiddleware) {
|
|
96
|
+
// New-style registration helper
|
|
97
|
+
mod.registerMiddleware(MiddlewareRegistry, serviceConfig.name);
|
|
98
|
+
serviceMiddlewareInstance = MiddlewareRegistry.get(serviceConfig.name);
|
|
99
|
+
} else if (mod?.default) {
|
|
100
|
+
const def = mod.default;
|
|
101
|
+
// If the default is a class (constructor), instantiate and register
|
|
102
|
+
if (typeof def === 'function' && def.prototype) {
|
|
103
|
+
try {
|
|
104
|
+
const instance = new def();
|
|
105
|
+
MiddlewareRegistry.register(serviceConfig.name, instance);
|
|
106
|
+
serviceMiddlewareInstance = instance;
|
|
107
|
+
} catch (e) {
|
|
108
|
+
// ignore instantiation errors
|
|
109
|
+
}
|
|
110
|
+
} else if (typeof def === 'function') {
|
|
111
|
+
// Legacy factory exported as default
|
|
112
|
+
legacyFactory = def;
|
|
113
|
+
}
|
|
114
|
+
} else if (mod?.createServiceMiddleware) {
|
|
115
|
+
legacyFactory = mod.createServiceMiddleware;
|
|
116
|
+
}
|
|
117
|
+
} catch (e) {
|
|
118
|
+
// No service-specific middleware found - continue with shared only
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Compose final middleware chain
|
|
122
|
+
let chain;
|
|
123
|
+
|
|
124
|
+
if (legacyFactory) {
|
|
125
|
+
const legacyInstance = legacyFactory(serviceConfig, env);
|
|
126
|
+
const adapter = {
|
|
127
|
+
preprocess: async (req) => {
|
|
128
|
+
if (legacyInstance && typeof legacyInstance.processRequest === 'function') {
|
|
129
|
+
const processed = await legacyInstance.processRequest(req);
|
|
130
|
+
if (processed instanceof Response) return processed; // short-circuit
|
|
131
|
+
return null; // continue (legacy returns a Request)
|
|
132
|
+
}
|
|
133
|
+
return null;
|
|
134
|
+
},
|
|
135
|
+
postprocess: async (res) => {
|
|
136
|
+
if (legacyInstance && typeof legacyInstance.processResponse === 'function') {
|
|
137
|
+
const r = await legacyInstance.processResponse(res);
|
|
138
|
+
return r instanceof Response ? r : res;
|
|
139
|
+
}
|
|
140
|
+
return res;
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
chain = MiddlewareComposer.compose(...sharedMiddlewares, adapter);
|
|
145
|
+
} else {
|
|
146
|
+
const svcMw = serviceMiddlewareInstance || MiddlewareRegistry.get(serviceConfig.name);
|
|
147
|
+
chain = MiddlewareComposer.compose(...sharedMiddlewares, svcMw);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Execute middleware chain with final handler
|
|
151
|
+
const response = await chain.execute(request, async (req) => {
|
|
152
|
+
const handlers = createServiceHandlers(serviceConfig, env);
|
|
153
|
+
return handlers.handleRequest(req, ctx);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
return response;
|
|
91
157
|
|
|
92
158
|
} catch (error) {
|
|
93
159
|
console.error('Worker error:', error);
|
package/dist/simple-api.js
CHANGED
|
@@ -25,7 +25,38 @@ export class Clodo {
|
|
|
25
25
|
* @returns {Promise<Object>} Service creation result
|
|
26
26
|
*/
|
|
27
27
|
static async createService(options = {}) {
|
|
28
|
-
|
|
28
|
+
// Instantiate orchestrator with provided middleware strategy and paths
|
|
29
|
+
const orchestrator = new ServiceOrchestrator({
|
|
30
|
+
interactive: options.interactive !== false,
|
|
31
|
+
outputPath: options.outputPath || '.',
|
|
32
|
+
templatePath: options.templatePath || './templates',
|
|
33
|
+
middlewareStrategy: options.middlewareStrategy || 'contract'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// If interactive mode requested (default), run interactive flow
|
|
37
|
+
if (options.interactive !== false) {
|
|
38
|
+
await orchestrator.runInteractive();
|
|
39
|
+
return {
|
|
40
|
+
success: true,
|
|
41
|
+
message: 'Service created (interactive)'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Non-interactive flow: build core inputs and run non-interactive
|
|
46
|
+
const coreInputs = {
|
|
47
|
+
serviceName: options.name,
|
|
48
|
+
serviceType: options.type,
|
|
49
|
+
domainName: options.domain,
|
|
50
|
+
environment: options.environment || 'development',
|
|
51
|
+
cloudflareToken: options.credentials?.token,
|
|
52
|
+
cloudflareAccountId: options.credentials?.accountId,
|
|
53
|
+
cloudflareZoneId: options.credentials?.zoneId
|
|
54
|
+
};
|
|
55
|
+
await orchestrator.runNonInteractive(coreInputs);
|
|
56
|
+
return {
|
|
57
|
+
success: true,
|
|
58
|
+
message: `Service ${options.name} created`
|
|
59
|
+
};
|
|
29
60
|
}
|
|
30
61
|
|
|
31
62
|
/**
|