@tamyla/clodo-framework 4.0.15 → 4.2.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 CHANGED
@@ -1,3 +1,22 @@
1
+ # [4.2.0](https://github.com/tamylaa/clodo-framework/compare/v4.1.0...v4.2.0) (2026-01-31)
2
+
3
+
4
+ ### Features
5
+
6
+ * **validation:** integrate ConfigurationValidator into ValidationHandler; add post-generation validation in ServiceOrchestrator; tests ([#5](https://github.com/tamylaa/clodo-framework/issues/5)) ([2889864](https://github.com/tamylaa/clodo-framework/commit/28898641bac4c2d6d206e12dbba8216e1de386a3))
7
+
8
+ # [4.1.0](https://github.com/tamylaa/clodo-framework/compare/v4.0.15...v4.1.0) (2026-01-31)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * **lint:** remove unnecessary escape in serviceName regex to satisfy no-useless-escape ([ac4dcda](https://github.com/tamylaa/clodo-framework/commit/ac4dcdabb39dda3dba3f4e06022f28936a53d645))
14
+
15
+
16
+ ### Features
17
+
18
+ * **programmatic-service-api:** programmatic createService + configurable validation ([#4](https://github.com/tamylaa/clodo-framework/issues/4)) ([040f68b](https://github.com/tamylaa/clodo-framework/commit/040f68b9115b604b3f27d7baa14c63122494c5f3))
19
+
1
20
  ## [4.0.15](https://github.com/tamylaa/clodo-framework/compare/v4.0.14...v4.0.15) (2026-01-31)
2
21
 
3
22
 
@@ -0,0 +1,22 @@
1
+ // Centralized configuration for service payload schema values
2
+ // Allows runtime overrides for enums and features to keep validation flexible and configurable
3
+
4
+ let config = {
5
+ serviceTypes: ['api-service', 'data-service', 'worker', 'pages', 'gateway', 'generic'],
6
+ features: ['d1', 'upstash', 'r2', 'pages', 'ws', 'durableObject', 'cron', 'metrics']
7
+ };
8
+ export function getConfig() {
9
+ return {
10
+ ...config
11
+ };
12
+ }
13
+ export function setConfig(updates = {}) {
14
+ if (updates.serviceTypes) config.serviceTypes = [...updates.serviceTypes];
15
+ if (updates.features) config.features = [...updates.features];
16
+ }
17
+ export function resetConfig() {
18
+ config = {
19
+ serviceTypes: ['api-service', 'data-service', 'worker', 'pages', 'gateway', 'generic'],
20
+ features: ['d1', 'upstash', 'r2', 'pages', 'ws', 'durableObject', 'cron', 'metrics']
21
+ };
22
+ }
@@ -0,0 +1,9 @@
1
+ import { ServiceOrchestrator } from '../service-management/ServiceOrchestrator.js';
2
+ export async function createServiceProgrammatic(payload, options = {}) {
3
+ const orchestrator = new ServiceOrchestrator({
4
+ interactive: false,
5
+ outputPath: options.outputDir || '.'
6
+ });
7
+ return await orchestrator.createService(payload, options);
8
+ }
9
+ export const createService = createServiceProgrammatic; // Backwards-compatible alias
@@ -99,6 +99,91 @@ export class ServiceOrchestrator {
99
99
  }
100
100
  }
101
101
 
102
+ /**
103
+ * Programmatic service creation API
104
+ * Accepts a ServicePayload and performs validation, generation and returns structured result
105
+ */
106
+ async createService(payload, options = {}) {
107
+ // Validate payload using new validator
108
+ const {
109
+ validateServicePayload
110
+ } = await import('../validation/payloadValidation.js');
111
+ const validation = validateServicePayload(payload);
112
+ if (!validation.valid && !options.force) {
113
+ return {
114
+ success: false,
115
+ errors: validation.errors,
116
+ warnings: validation.warnings || []
117
+ };
118
+ }
119
+
120
+ // Map payload to coreInputs expected by generation handlers
121
+ const coreInputs = {
122
+ serviceName: payload.serviceName,
123
+ serviceType: payload.serviceType,
124
+ domainName: payload.domain,
125
+ environment: options.environment || 'development'
126
+ };
127
+ try {
128
+ // Generate derived confirmations and run generation
129
+ const confirmedValues = await this.confirmationHandler.generateAndConfirm(coreInputs);
130
+ const generationResult = await this.generationHandler.generateService(coreInputs, confirmedValues, {
131
+ outputPath: options.outputDir || this.outputPath,
132
+ middlewareStrategy: this.middlewareStrategy
133
+ });
134
+
135
+ // Post-generation validation using ValidationHandler
136
+ const validationHandler = options.customConfig ? new ValidationHandler({
137
+ customConfig: options.customConfig
138
+ }) : this.validationHandler;
139
+ try {
140
+ const postValidation = await validationHandler.validateService(generationResult.servicePath);
141
+ if (!postValidation.valid && !options.force) {
142
+ return {
143
+ success: false,
144
+ errors: postValidation.issues || [],
145
+ warnings: validation.warnings || [],
146
+ validationReport: postValidation
147
+ };
148
+ }
149
+ return {
150
+ success: true,
151
+ serviceId: generationResult.serviceId || null,
152
+ servicePath: generationResult.servicePath || null,
153
+ serviceName: generationResult.serviceName || coreInputs.serviceName,
154
+ fileCount: generationResult.fileCount || (generationResult.generatedFiles || []).length,
155
+ generatedFiles: generationResult.generatedFiles || [],
156
+ warnings: validation.warnings || [],
157
+ validationReport: postValidation
158
+ };
159
+ } catch (valErr) {
160
+ // If validation step throws, surface error unless forced
161
+ if (!options.force) {
162
+ return {
163
+ success: false,
164
+ errors: [valErr.message]
165
+ };
166
+ }
167
+ return {
168
+ success: true,
169
+ serviceId: generationResult.serviceId || null,
170
+ servicePath: generationResult.servicePath || null,
171
+ serviceName: generationResult.serviceName || coreInputs.serviceName,
172
+ fileCount: generationResult.fileCount || (generationResult.generatedFiles || []).length,
173
+ generatedFiles: generationResult.generatedFiles || [],
174
+ warnings: validation.warnings || [],
175
+ validationReport: {
176
+ error: valErr.message
177
+ }
178
+ };
179
+ }
180
+ } catch (error) {
181
+ return {
182
+ success: false,
183
+ errors: [error.message]
184
+ };
185
+ }
186
+ }
102
187
  /**
103
188
  * Validate an existing service configuration
104
189
  */
@@ -6,8 +6,7 @@
6
6
  import fs from 'fs/promises';
7
7
  import path from 'path';
8
8
  import { FrameworkConfig } from '../../utils/framework-config.js';
9
- // import { ConfigurationValidator } from '../../lib/shared/utils/configuration-validator.js';
10
-
9
+ import { ConfigurationValidator } from '../../security/ConfigurationValidator.js';
11
10
  export class ValidationHandler {
12
11
  constructor(options = {}) {
13
12
  this.strict = options.strict || false;
@@ -94,17 +93,33 @@ export class ValidationHandler {
94
93
  issues.push(...wranglerValidation.issues);
95
94
 
96
95
  // Run comprehensive configuration validation using ConfigurationValidator
97
- // Temporarily disabled due to import issues
98
- // try {
99
- // const configValidation = await ConfigurationValidator.validateServiceConfig(servicePath);
100
- // if (!configValidation.isValid) {
101
- // issues.push(...configValidation.errors);
102
- // issues.push(...configValidation.warnings.map(w => `Warning: ${w}`));
103
- // }
104
- // } catch (error) {
105
- // issues.push(`Configuration validation failed: ${error.message}`);
106
- // }
107
-
96
+ try {
97
+ // Determine manifest path candidates and select first that exists
98
+ const manifestCandidates = ['clodo-service-manifest.json', 'service-manifest.json', 'manifest.json'];
99
+ let manifestPath = null;
100
+ for (const candidate of manifestCandidates) {
101
+ const candidatePath = path.join(servicePath, candidate);
102
+ try {
103
+ await fs.access(candidatePath);
104
+ manifestPath = candidatePath;
105
+ break;
106
+ } catch {
107
+ // Not found, continue
108
+ }
109
+ }
110
+ const wranglerPath = path.join(servicePath, 'wrangler.toml');
111
+ if (manifestPath) {
112
+ const configValidation = ConfigurationValidator.validateServiceConfig(manifestPath, wranglerPath);
113
+ if (!configValidation.valid) {
114
+ issues.push(...(configValidation.issues || []).map(i => `Configuration mismatch: ${i.message || JSON.stringify(i)}`));
115
+ }
116
+ } else {
117
+ // No manifest found — warn but do not block validation
118
+ issues.push('Warning: No service manifest found (clodo-service-manifest.json) — skipping manifest↔wrangler validation');
119
+ }
120
+ } catch (error) {
121
+ issues.push(`Configuration validation step failed: ${error.message}`);
122
+ }
108
123
  return {
109
124
  valid: issues.length === 0,
110
125
  issues,
@@ -0,0 +1,159 @@
1
+ import { z } from 'zod';
2
+
3
+ // Allowed enums
4
+ import { getConfig } from '../config/service-schema-config.js';
5
+ export const VALID_SERVICE_TYPES = () => getConfig().serviceTypes;
6
+ export const VALID_FEATURES = () => getConfig().features;
7
+
8
+ // Zod schema with refined validations
9
+ export const ServicePayloadSchema = z.object({
10
+ serviceName: z.string().min(3).max(50).regex(/^[a-z0-9-]+$/, 'serviceName must be lowercase letters, numbers and hyphens only'),
11
+ serviceType: z.string().refine(v => VALID_SERVICE_TYPES().includes(v), {
12
+ message: `Invalid serviceType. Expected one of: ${VALID_SERVICE_TYPES().join(', ')}`
13
+ }),
14
+ domain: z.string().min(3).regex(/^([a-z0-9]+(-[a-z0-9]+)*\.)+[a-z]{2,}$/, 'domain must be a valid domain name'),
15
+ description: z.string().optional(),
16
+ template: z.string().optional(),
17
+ features: z.array(z.string().refine(v => VALID_FEATURES().includes(v), {
18
+ message: `Invalid feature. Expected one of: ${VALID_FEATURES().join(', ')}`
19
+ })).optional(),
20
+ bindings: z.array(z.string()).optional(),
21
+ resources: z.record(z.any()).optional(),
22
+ specs: z.record(z.any()).optional(),
23
+ clodo: z.record(z.any()).optional()
24
+ });
25
+ export function validateServicePayload(payload) {
26
+ const result = {
27
+ valid: true,
28
+ errors: [],
29
+ warnings: []
30
+ };
31
+ try {
32
+ ServicePayloadSchema.parse(payload);
33
+
34
+ // Additional semantic checks
35
+ if (payload.features && Array.isArray(payload.features)) {
36
+ const duplicates = payload.features.filter((v, i, a) => a.indexOf(v) !== i);
37
+ if (duplicates.length) {
38
+ result.warnings.push({
39
+ field: 'features',
40
+ code: 'DUPLICATE_FEATURES',
41
+ message: `Duplicate features: ${[...new Set(duplicates)].join(', ')}`
42
+ });
43
+ }
44
+
45
+ // Check for unknown features if config changed at runtime
46
+ const configured = VALID_FEATURES();
47
+ const unknown = payload.features.filter(f => !configured.includes(f));
48
+ if (unknown.length) {
49
+ result.warnings.push({
50
+ field: 'features',
51
+ code: 'UNKNOWN_FEATURES',
52
+ message: `Unknown features: ${[...new Set(unknown)].join(', ')}`
53
+ });
54
+ }
55
+ }
56
+ } catch (err) {
57
+ result.valid = false;
58
+
59
+ // Handle Zod-like errors with an errors array
60
+ if (err && err.errors && Array.isArray(err.errors)) {
61
+ for (const e of err.errors) {
62
+ result.errors.push({
63
+ field: (e.path || []).join('.'),
64
+ code: 'SCHEMA_VALIDATION',
65
+ message: e.message
66
+ });
67
+ }
68
+ }
69
+ // Some environments stringify Zod errors into err.message (JSON array) - try to parse
70
+ else if (typeof err?.message === 'string' && err.message.trim().startsWith('[')) {
71
+ try {
72
+ const parsed = JSON.parse(err.message);
73
+ if (Array.isArray(parsed)) {
74
+ for (const e of parsed) {
75
+ const field = (e.path || []).join('.');
76
+ const msg = e.message || JSON.stringify(e);
77
+ result.errors.push({
78
+ field,
79
+ code: 'SCHEMA_VALIDATION',
80
+ message: msg
81
+ });
82
+ }
83
+ } else {
84
+ result.errors.push({
85
+ code: 'UNKNOWN',
86
+ message: err.message
87
+ });
88
+ }
89
+ } catch (parseErr) {
90
+ result.errors.push({
91
+ code: 'UNKNOWN',
92
+ message: err.message
93
+ });
94
+ }
95
+ } else {
96
+ result.errors.push({
97
+ code: 'UNKNOWN',
98
+ message: err.message
99
+ });
100
+ }
101
+ }
102
+ return result;
103
+ }
104
+
105
+ // Parameter metadata for discovery (derived from schema)
106
+ export function getParameterDefinitions() {
107
+ return {
108
+ serviceName: {
109
+ name: 'serviceName',
110
+ type: 'string',
111
+ required: true,
112
+ description: 'Unique identifier for the service',
113
+ validationRules: [{
114
+ rule: 'pattern',
115
+ value: '^[a-z0-9-]+$',
116
+ message: 'Lowercase letters, numbers and hyphens only'
117
+ }, {
118
+ rule: 'minLength',
119
+ value: 3,
120
+ message: 'At least 3 characters'
121
+ }, {
122
+ rule: 'maxLength',
123
+ value: 50,
124
+ message: 'At most 50 characters'
125
+ }]
126
+ },
127
+ serviceType: {
128
+ name: 'serviceType',
129
+ type: 'string',
130
+ required: true,
131
+ description: 'Type of service to create',
132
+ enum: VALID_SERVICE_TYPES()
133
+ },
134
+ domain: {
135
+ name: 'domain',
136
+ type: 'string',
137
+ required: true,
138
+ description: 'Domain for the service',
139
+ validationRules: [{
140
+ rule: 'pattern',
141
+ value: '^([a-z0-9]+(-[a-z0-9]+)*\\.)+[a-z]{2,}$',
142
+ message: 'Must be a valid domain'
143
+ }]
144
+ },
145
+ features: {
146
+ name: 'features',
147
+ type: 'array',
148
+ required: false,
149
+ description: 'Features to enable',
150
+ enum: VALID_FEATURES
151
+ },
152
+ clodo: {
153
+ name: 'clodo',
154
+ type: 'object',
155
+ required: false,
156
+ description: 'Passthrough data for clodo-application specific configuration'
157
+ }
158
+ };
159
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tamyla/clodo-framework",
3
- "version": "4.0.15",
3
+ "version": "4.2.0",
4
4
  "description": "Reusable framework for Clodo-style software architecture on Cloudflare Workers + D1",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -153,7 +153,8 @@
153
153
  "commander": "^11.0.0",
154
154
  "inquirer": "^12.10.0",
155
155
  "uuid": "^13.0.0",
156
- "wrangler": ">=3.0.0"
156
+ "wrangler": ">=3.0.0",
157
+ "zod": "^4.3.6"
157
158
  },
158
159
  "devDependencies": {
159
160
  "@babel/cli": "^7.23.0",
@@ -0,0 +1,12 @@
1
+ import { validateServicePayload } from '../src/validation/payloadValidation.js';
2
+
3
+ const cases = [
4
+ { payload: { serviceName: 'Invalid Name', serviceType: 'generic', domain: 'example.com' }, name: 'invalid name' },
5
+ { payload: { serviceName: 'svc2', serviceType: 'unknown-type', domain: 'example.com' }, name: 'invalid type' },
6
+ { payload: { serviceName: 'svc3', serviceType: 'generic', domain: 'example.com', features: ['d1','nonsense'] }, name: 'invalid feature' }
7
+ ];
8
+
9
+ for (const c of cases) {
10
+ const res = validateServicePayload(c.payload);
11
+ console.log('CASE:', c.name, JSON.stringify(res, null, 2));
12
+ }