@tamyla/clodo-framework 4.0.15 → 4.1.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,15 @@
1
+ # [4.1.0](https://github.com/tamylaa/clodo-framework/compare/v4.0.15...v4.1.0) (2026-01-31)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **lint:** remove unnecessary escape in serviceName regex to satisfy no-useless-escape ([ac4dcda](https://github.com/tamylaa/clodo-framework/commit/ac4dcdabb39dda3dba3f4e06022f28936a53d645))
7
+
8
+
9
+ ### Features
10
+
11
+ * **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))
12
+
1
13
  ## [4.0.15](https://github.com/tamylaa/clodo-framework/compare/v4.0.14...v4.0.15) (2026-01-31)
2
14
 
3
15
 
@@ -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,54 @@ 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
+ return {
135
+ success: true,
136
+ serviceId: generationResult.serviceId || null,
137
+ servicePath: generationResult.servicePath || null,
138
+ serviceName: generationResult.serviceName || coreInputs.serviceName,
139
+ fileCount: generationResult.fileCount || (generationResult.generatedFiles || []).length,
140
+ generatedFiles: generationResult.generatedFiles || [],
141
+ warnings: validation.warnings || []
142
+ };
143
+ } catch (error) {
144
+ return {
145
+ success: false,
146
+ errors: [error.message]
147
+ };
148
+ }
149
+ }
102
150
  /**
103
151
  * Validate an existing service configuration
104
152
  */
@@ -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.1.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
+ }