@aifabrix/builder 2.32.3 → 2.33.1

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 (127) hide show
  1. package/.cursor/rules/project-rules.mdc +8 -0
  2. package/README.md +36 -8
  3. package/bin/aifabrix.js +6 -8
  4. package/integration/hubspot/README.md +12 -11
  5. package/integration/hubspot/companies.json +2048 -0
  6. package/integration/hubspot/create-hubspot.js +665 -0
  7. package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
  8. package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
  9. package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
  10. package/integration/hubspot/hubspot-deploy.json +832 -81
  11. package/integration/hubspot/hubspot-system.json +99 -0
  12. package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
  13. package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
  14. package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
  15. package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
  16. package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
  17. package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
  18. package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
  19. package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
  20. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
  22. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
  23. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
  24. package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
  25. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
  26. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
  29. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
  30. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
  31. package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
  32. package/integration/hubspot/test-dataplane-down-tests.js +419 -0
  33. package/integration/hubspot/test-dataplane-down.js +157 -0
  34. package/integration/hubspot/test.js +1517 -0
  35. package/integration/hubspot/variables.yaml +4 -4
  36. package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
  37. package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
  38. package/lib/api/applications.api.js +1 -0
  39. package/lib/api/index.js +6 -2
  40. package/lib/api/types/wizard.types.js +176 -38
  41. package/lib/api/wizard.api.js +161 -23
  42. package/lib/app/deploy.js +116 -54
  43. package/lib/app/display.js +6 -5
  44. package/lib/app/dockerfile.js +2 -1
  45. package/lib/app/list.js +17 -10
  46. package/lib/app/readme.js +41 -112
  47. package/lib/app/register.js +44 -9
  48. package/lib/app/rotate-secret.js +48 -31
  49. package/lib/cli.js +219 -70
  50. package/lib/commands/app.js +4 -9
  51. package/lib/commands/auth-config.js +125 -0
  52. package/lib/commands/auth-status.js +7 -8
  53. package/lib/commands/datasource.js +3 -6
  54. package/lib/commands/login-credentials.js +4 -4
  55. package/lib/commands/login-device.js +26 -17
  56. package/lib/commands/login.js +12 -10
  57. package/lib/commands/wizard-config-normalizer.js +92 -0
  58. package/lib/commands/wizard-core.js +515 -0
  59. package/lib/commands/wizard-dataplane.js +122 -0
  60. package/lib/commands/wizard-headless.js +115 -0
  61. package/lib/commands/wizard.js +110 -332
  62. package/lib/core/config.js +46 -0
  63. package/lib/core/secrets.js +3 -22
  64. package/lib/core/templates-env.js +1 -1
  65. package/lib/datasource/deploy.js +59 -23
  66. package/lib/datasource/list.js +108 -19
  67. package/lib/deployment/deployer.js +25 -0
  68. package/lib/deployment/environment.js +10 -13
  69. package/lib/external-system/delete.js +151 -0
  70. package/lib/external-system/deploy.js +53 -378
  71. package/lib/external-system/download-helpers.js +45 -65
  72. package/lib/external-system/download.js +33 -13
  73. package/lib/external-system/generator.js +11 -7
  74. package/lib/external-system/test-auth.js +4 -3
  75. package/lib/generator/builders.js +3 -1
  76. package/lib/generator/external-controller-manifest.js +157 -0
  77. package/lib/generator/external-schema-utils.js +236 -0
  78. package/lib/generator/external.js +55 -3
  79. package/lib/generator/index.js +22 -10
  80. package/lib/generator/wizard-prompts.js +33 -10
  81. package/lib/generator/wizard.js +69 -86
  82. package/lib/infrastructure/compose.js +100 -0
  83. package/lib/infrastructure/helpers.js +139 -0
  84. package/lib/infrastructure/index.js +52 -311
  85. package/lib/infrastructure/services.js +168 -0
  86. package/lib/schema/application-schema.json +23 -4
  87. package/lib/schema/external-datasource.schema.json +2 -2
  88. package/lib/schema/wizard-config.schema.json +234 -0
  89. package/lib/utils/api.js +102 -52
  90. package/lib/utils/app-existence.js +42 -0
  91. package/lib/utils/app-register-config.js +7 -2
  92. package/lib/utils/auth-config-validator.js +92 -0
  93. package/lib/utils/command-header.js +43 -0
  94. package/lib/utils/compose-generator.js +113 -70
  95. package/lib/utils/controller-url.js +65 -17
  96. package/lib/utils/dataplane-health.js +115 -0
  97. package/lib/utils/dataplane-resolver.js +29 -0
  98. package/lib/utils/dev-config.js +6 -2
  99. package/lib/utils/env-copy.js +2 -1
  100. package/lib/utils/env-ports.js +2 -1
  101. package/lib/utils/env-template.js +1 -1
  102. package/lib/utils/error-formatter.js +49 -0
  103. package/lib/utils/error-formatters/network-errors.js +13 -3
  104. package/lib/utils/external-readme.js +125 -0
  105. package/lib/utils/help-builder.js +190 -0
  106. package/lib/utils/infra-status.js +13 -3
  107. package/lib/utils/paths.js +17 -2
  108. package/lib/utils/port-resolver.js +111 -0
  109. package/lib/utils/secrets-helpers.js +3 -15
  110. package/lib/utils/secrets-utils.js +2 -2
  111. package/lib/utils/token-manager.js +9 -4
  112. package/lib/utils/variable-transformer.js +7 -2
  113. package/lib/validation/external-manifest-validator.js +202 -0
  114. package/lib/validation/validate-display.js +406 -0
  115. package/lib/validation/validate.js +159 -123
  116. package/lib/validation/validator.js +36 -3
  117. package/lib/validation/wizard-config-validator.js +267 -0
  118. package/package.json +4 -2
  119. package/templates/applications/README.md.hbs +18 -16
  120. package/templates/applications/miso-controller/env.template +1 -1
  121. package/templates/applications/miso-controller/rbac.yaml +7 -7
  122. package/templates/external-system/README.md.hbs +99 -0
  123. package/templates/github/ci.yaml.hbs +44 -1
  124. package/templates/github/release.yaml.hbs +44 -0
  125. package/templates/infra/compose.yaml.hbs +35 -0
  126. package/templates/python/docker-compose.hbs +26 -0
  127. package/templates/typescript/docker-compose.hbs +26 -0
@@ -11,13 +11,14 @@
11
11
 
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
- const chalk = require('chalk');
15
14
  const validator = require('./validator');
16
15
  const { resolveExternalFiles } = require('../utils/schema-resolver');
17
16
  const { loadExternalSystemSchema, loadExternalDataSourceSchema, detectSchemaType } = require('../utils/schema-loader');
18
17
  const { formatValidationErrors } = require('../utils/error-formatter');
19
18
  const { detectAppType } = require('../utils/paths');
20
- const logger = require('../utils/logger');
19
+ const { displayValidationResults } = require('./validate-display');
20
+ const { generateControllerManifest } = require('../generator/external-controller-manifest');
21
+ const { validateControllerManifest } = require('./external-manifest-validator');
21
22
 
22
23
  /**
23
24
  * Validates a file path (detects type and validates)
@@ -204,6 +205,8 @@ async function validateExternalFile(filePath, type) {
204
205
  * @async
205
206
  * @function validateAppOrFile
206
207
  * @param {string} appOrFile - Application name or file path
208
+ * @param {Object} [options] - Validation options
209
+ * @param {string} [options.type] - Forced application type (external)
207
210
  * @returns {Promise<Object>} Validation result with aggregated results
208
211
  * @throws {Error} If validation fails
209
212
  *
@@ -277,158 +280,191 @@ function loadVariablesAndCheckExternalIntegration(variablesPath, appValidation)
277
280
  return null;
278
281
  }
279
282
 
280
- async function validateAppOrFile(appOrFile) {
281
- if (!appOrFile || typeof appOrFile !== 'string') {
282
- throw new Error('App name or file path is required');
283
- }
284
-
285
- const isFilePath = fs.existsSync(appOrFile) && fs.statSync(appOrFile).isFile();
286
- if (isFilePath) {
287
- return await validateFilePath(appOrFile);
288
- }
289
-
290
- const appName = appOrFile;
291
- const { appPath, isExternal } = await detectAppType(appName);
292
- const appValidation = await validator.validateApplication(appName);
293
- const rbacValidation = await validateRbacForExternalSystem(isExternal, appName);
294
-
295
- const variablesPath = path.join(appPath, 'variables.yaml');
296
- const earlyReturn = loadVariablesAndCheckExternalIntegration(variablesPath, appValidation);
297
- if (earlyReturn) {
298
- return earlyReturn;
283
+ /**
284
+ * Validates application configuration step
285
+ * @async
286
+ * @function validateApplicationStep
287
+ * @param {string} appName - Application name
288
+ * @returns {Promise<Object>} Application validation result
289
+ */
290
+ async function validateApplicationStep(appName) {
291
+ try {
292
+ const appValidation = await validator.validateApplication(appName);
293
+ return {
294
+ valid: appValidation.valid,
295
+ errors: appValidation.errors || [],
296
+ warnings: appValidation.warnings || []
297
+ };
298
+ } catch (error) {
299
+ return {
300
+ valid: false,
301
+ errors: [error.message],
302
+ warnings: []
303
+ };
299
304
  }
300
-
301
- const externalValidations = await validateExternalFilesForApp(appName);
302
- return aggregateValidationResults(appValidation, externalValidations, rbacValidation);
303
305
  }
304
306
 
305
307
  /**
306
- * Displays validation results in a user-friendly format
307
- *
308
- * @function displayValidationResults
309
- * @param {Object} result - Validation result from validateAppOrFile
310
- */
311
- /**
312
- * Displays application validation results
313
- * @function displayApplicationValidation
314
- * @param {Object} application - Application validation result
308
+ * Validates individual component files step
309
+ * @async
310
+ * @function validateComponentsStep
311
+ * @param {string} appName - Application name
312
+ * @returns {Promise<Object>} Components validation result
315
313
  */
316
- function displayApplicationValidation(application) {
317
- if (!application) {
318
- return;
319
- }
314
+ async function validateComponentsStep(appName) {
315
+ try {
316
+ const externalValidations = await validateExternalFilesForApp(appName);
317
+ const componentErrors = [];
318
+ const componentWarnings = [];
319
+ const componentFiles = [];
320
+
321
+ externalValidations.forEach(validation => {
322
+ componentFiles.push({
323
+ file: validation.file,
324
+ type: validation.type,
325
+ valid: validation.valid,
326
+ path: validation.path
327
+ });
320
328
 
321
- logger.log(chalk.blue('\nApplication:'));
322
- if (application.valid) {
323
- logger.log(chalk.green(' ✓ Application configuration is valid'));
324
- } else {
325
- logger.log(chalk.red(' Application configuration has errors:'));
326
- application.errors.forEach(error => {
327
- logger.log(chalk.red(` • ${error}`));
328
- });
329
- }
330
- if (application.warnings && application.warnings.length > 0) {
331
- application.warnings.forEach(warning => {
332
- logger.log(chalk.yellow(` ⚠ ${warning}`));
329
+ if (!validation.valid) {
330
+ componentErrors.push(`${validation.file} (${validation.type}): ${validation.errors.join(', ')}`);
331
+ }
332
+ if (validation.warnings && validation.warnings.length > 0) {
333
+ componentWarnings.push(`${validation.file}: ${validation.warnings.join(', ')}`);
334
+ }
333
335
  });
336
+
337
+ return {
338
+ valid: componentErrors.length === 0,
339
+ errors: componentErrors,
340
+ warnings: componentWarnings,
341
+ files: componentFiles
342
+ };
343
+ } catch (error) {
344
+ return {
345
+ valid: false,
346
+ errors: [error.message],
347
+ warnings: [],
348
+ files: []
349
+ };
334
350
  }
335
351
  }
336
352
 
337
353
  /**
338
- * Displays external files validation results
339
- * @function displayExternalFilesValidation
340
- * @param {Array} externalFiles - External files validation results
354
+ * Validates full deployment manifest step
355
+ * @async
356
+ * @function validateManifestStep
357
+ * @param {string} appName - Application name
358
+ * @returns {Promise<Object>} Manifest validation result
341
359
  */
342
- function displayExternalFilesValidation(externalFiles) {
343
- if (!externalFiles || externalFiles.length === 0) {
344
- return;
360
+ async function validateManifestStep(appName) {
361
+ try {
362
+ const manifest = await generateControllerManifest(appName);
363
+ const manifestValidation = await validateControllerManifest(manifest);
364
+ return {
365
+ valid: manifestValidation.valid,
366
+ errors: manifestValidation.errors || [],
367
+ warnings: manifestValidation.warnings || []
368
+ };
369
+ } catch (error) {
370
+ return {
371
+ valid: false,
372
+ errors: [`Failed to validate manifest: ${error.message}`],
373
+ warnings: []
374
+ };
345
375
  }
346
-
347
- logger.log(chalk.blue('\nExternal Integration Files:'));
348
- externalFiles.forEach(file => {
349
- if (file.valid) {
350
- logger.log(chalk.green(` ✓ ${file.file} (${file.type})`));
351
- } else {
352
- logger.log(chalk.red(` ✗ ${file.file} (${file.type}):`));
353
- file.errors.forEach(error => {
354
- logger.log(chalk.red(` • ${error}`));
355
- });
356
- }
357
- if (file.warnings && file.warnings.length > 0) {
358
- file.warnings.forEach(warning => {
359
- logger.log(chalk.yellow(` ⚠ ${warning}`));
360
- });
361
- }
362
- });
363
376
  }
364
377
 
365
378
  /**
366
- * Displays RBAC validation results
367
- * @function displayRbacValidation
368
- * @param {Object} rbac - RBAC validation result
379
+ * Validates external system completely (components + full manifest)
380
+ * Performs step-by-step validation: application config → components → full manifest
381
+ *
382
+ * @async
383
+ * @function validateExternalSystemComplete
384
+ * @param {string} appName - Application name
385
+ * @returns {Promise<Object>} Complete validation result with step-by-step results
386
+ * @throws {Error} If validation fails critically
387
+ *
388
+ * @example
389
+ * const result = await validateExternalSystemComplete('my-hubspot');
390
+ * // Returns: { valid: true, errors: [], warnings: [], steps: {...} }
369
391
  */
370
- function displayRbacValidation(rbac) {
371
- if (!rbac) {
372
- return;
392
+ async function validateExternalSystemComplete(appName) {
393
+ if (!appName || typeof appName !== 'string') {
394
+ throw new Error('App name is required and must be a string');
373
395
  }
374
396
 
375
- logger.log(chalk.blue('\nRBAC Configuration:'));
376
- if (rbac.valid) {
377
- logger.log(chalk.green(' ✓ RBAC configuration is valid'));
378
- } else {
379
- logger.log(chalk.red(' ✗ RBAC configuration has errors:'));
380
- rbac.errors.forEach(error => {
381
- logger.log(chalk.red(` • ${error}`));
382
- });
383
- }
384
- if (rbac.warnings && rbac.warnings.length > 0) {
385
- rbac.warnings.forEach(warning => {
386
- logger.log(chalk.yellow(` ⚠ ${warning}`));
387
- });
397
+ const steps = {
398
+ application: { valid: false, errors: [], warnings: [] },
399
+ components: { valid: false, errors: [], warnings: [], files: [] },
400
+ manifest: { valid: false, errors: [], warnings: [] }
401
+ };
402
+
403
+ // Step 1: Validate Application Config
404
+ steps.application = await validateApplicationStep(appName);
405
+
406
+ // Step 2: Validate Individual Components
407
+ steps.components = await validateComponentsStep(appName);
408
+
409
+ // If components have errors, return early (don't validate manifest)
410
+ if (!steps.components.valid) {
411
+ return {
412
+ valid: false,
413
+ errors: [...steps.application.errors, ...steps.components.errors],
414
+ warnings: [...steps.application.warnings, ...steps.components.warnings],
415
+ steps
416
+ };
388
417
  }
418
+
419
+ // Step 3 & 4: Generate and Validate Full Manifest (only if Step 2 passes)
420
+ steps.manifest = await validateManifestStep(appName);
421
+
422
+ // Aggregate Results
423
+ const allErrors = [...steps.application.errors, ...steps.components.errors, ...steps.manifest.errors];
424
+ const allWarnings = [...steps.application.warnings, ...steps.components.warnings, ...steps.manifest.warnings];
425
+
426
+ return {
427
+ valid: allErrors.length === 0,
428
+ errors: allErrors,
429
+ warnings: allWarnings,
430
+ steps
431
+ };
389
432
  }
390
433
 
391
- function displayValidationResults(result) {
392
- if (result.valid) {
393
- logger.log(chalk.green('\n✓ Validation passed!'));
394
- } else {
395
- logger.log(chalk.red('\n✗ Validation failed!'));
434
+ async function validateAppOrFile(appOrFile, options = {}) {
435
+ if (!appOrFile || typeof appOrFile !== 'string') {
436
+ throw new Error('App name or file path is required');
396
437
  }
397
438
 
398
- displayApplicationValidation(result.application);
399
- displayExternalFilesValidation(result.externalFiles);
400
- displayRbacValidation(result.rbac);
401
-
402
- // Display file validation (for direct file validation)
403
- if (result.file) {
404
- logger.log(chalk.blue(`\nFile: ${result.file}`));
405
- logger.log(chalk.blue(`Type: ${result.type}`));
406
- if (result.valid) {
407
- logger.log(chalk.green(' ✓ File is valid'));
408
- } else {
409
- logger.log(chalk.red(' ✗ File has errors:'));
410
- result.errors.forEach(error => {
411
- logger.log(chalk.red(` • ${error}`));
412
- });
413
- }
414
- if (result.warnings && result.warnings.length > 0) {
415
- result.warnings.forEach(warning => {
416
- logger.log(chalk.yellow(` ⚠ ${warning}`));
417
- });
418
- }
439
+ const isFilePath = fs.existsSync(appOrFile) && fs.statSync(appOrFile).isFile();
440
+ if (isFilePath) {
441
+ return await validateFilePath(appOrFile);
419
442
  }
420
443
 
421
- // Display aggregated warnings
422
- if (result.warnings && result.warnings.length > 0) {
423
- logger.log(chalk.yellow('\nWarnings:'));
424
- result.warnings.forEach(warning => {
425
- logger.log(chalk.yellow(` • ${warning}`));
426
- });
444
+ const appName = appOrFile;
445
+ const { appPath, isExternal } = await detectAppType(appName, options);
446
+
447
+ // For external systems with --type external flag, use new unified validation
448
+ if (isExternal && options.type === 'external') {
449
+ return await validateExternalSystemComplete(appName);
450
+ }
451
+
452
+ const appValidation = await validator.validateApplication(appName);
453
+ const rbacValidation = await validateRbacForExternalSystem(isExternal, appName);
454
+
455
+ const variablesPath = path.join(appPath, 'variables.yaml');
456
+ const earlyReturn = loadVariablesAndCheckExternalIntegration(variablesPath, appValidation);
457
+ if (earlyReturn) {
458
+ return earlyReturn;
427
459
  }
460
+
461
+ const externalValidations = await validateExternalFilesForApp(appName);
462
+ return aggregateValidationResults(appValidation, externalValidations, rbacValidation);
428
463
  }
429
464
 
430
465
  module.exports = {
431
466
  validateAppOrFile,
467
+ validateExternalSystemComplete,
432
468
  displayValidationResults,
433
469
  validateExternalFile,
434
470
  validateExternalFilesForApp,
@@ -98,6 +98,27 @@ function validateExternalIntegrationBlock(variables, errors) {
98
98
  }
99
99
  }
100
100
 
101
+ /**
102
+ * Validates frontDoorRouting configuration
103
+ * @function validateFrontDoorRouting
104
+ * @param {Object} variables - Variables object
105
+ * @param {string[]} errors - Errors array to append to
106
+ */
107
+ function validateFrontDoorRouting(variables, errors) {
108
+ const frontDoor = variables.frontDoorRouting;
109
+ if (!frontDoor) {
110
+ return;
111
+ }
112
+
113
+ if (frontDoor.enabled === true && (!frontDoor.host || typeof frontDoor.host !== 'string')) {
114
+ errors.push('frontDoorRouting.host is required when frontDoorRouting.enabled is true');
115
+ }
116
+
117
+ if (frontDoor.pattern && !String(frontDoor.pattern).startsWith('/')) {
118
+ errors.push('frontDoorRouting.pattern must start with "/"');
119
+ }
120
+ }
121
+
101
122
  async function validateVariables(appName) {
102
123
  if (!appName || typeof appName !== 'string') {
103
124
  throw new Error('App name is required and must be a string');
@@ -115,6 +136,8 @@ async function validateVariables(appName) {
115
136
  validateExternalIntegrationBlock(variables, errors);
116
137
  }
117
138
 
139
+ validateFrontDoorRouting(variables, errors);
140
+
118
141
  return {
119
142
  valid: valid && errors.length === 0,
120
143
  errors,
@@ -152,6 +175,10 @@ function validateRoles(roles) {
152
175
  } else {
153
176
  roleNames.add(role.value);
154
177
  }
178
+ // Reject Groups (capital G) - must use groups (lowercase)
179
+ if (role.Groups !== undefined) {
180
+ errors.push(`Role at index ${index} uses 'Groups' (capital G) but must use 'groups' (lowercase) for schema compatibility`);
181
+ }
155
182
  });
156
183
  return errors;
157
184
  }
@@ -229,7 +256,9 @@ async function validateEnvTemplate(appName) {
229
256
  throw new Error('App name is required and must be a string');
230
257
  }
231
258
 
232
- const templatePath = path.join(process.cwd(), 'builder', appName, 'env.template');
259
+ // Support both builder/ and integration/ directories using detectAppType
260
+ const { appPath } = await detectAppType(appName);
261
+ const templatePath = path.join(appPath, 'env.template');
233
262
 
234
263
  if (!fs.existsSync(templatePath)) {
235
264
  throw new Error(`env.template not found: ${templatePath}`);
@@ -341,15 +370,19 @@ async function validateApplication(appName) {
341
370
  const env = await validateEnvTemplate(appName);
342
371
 
343
372
  const valid = variables.valid && rbac.valid && env.valid;
373
+ const errors = [...(variables.errors || []), ...(rbac.errors || []), ...(env.errors || [])];
374
+ const warnings = [...(variables.warnings || []), ...(rbac.warnings || []), ...(env.warnings || [])];
344
375
 
345
376
  return {
346
377
  valid,
347
378
  variables,
348
379
  rbac,
349
380
  env,
381
+ errors,
382
+ warnings,
350
383
  summary: {
351
- totalErrors: variables.errors.length + rbac.errors.length + env.errors.length,
352
- totalWarnings: variables.warnings.length + rbac.warnings.length + env.warnings.length
384
+ totalErrors: errors.length,
385
+ totalWarnings: warnings.length
353
386
  }
354
387
  };
355
388
  }
@@ -0,0 +1,267 @@
1
+ /**
2
+ * @fileoverview Wizard configuration validator for wizard.yaml files
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const fs = require('fs').promises;
8
+ const path = require('path');
9
+ const yaml = require('js-yaml');
10
+ const Ajv = require('ajv');
11
+ const wizardConfigSchema = require('../schema/wizard-config.schema.json');
12
+
13
+ /**
14
+ * Resolve environment variable references in a value
15
+ * Supports ${VAR_NAME} syntax
16
+ * @function resolveEnvVar
17
+ * @param {string} value - Value that may contain env var references
18
+ * @returns {string} Resolved value
19
+ */
20
+ function resolveEnvVar(value) {
21
+ if (typeof value !== 'string') {
22
+ return value;
23
+ }
24
+ return value.replace(/\$\{([^}]+)\}/g, (match, varName) => {
25
+ const envValue = process.env[varName];
26
+ if (envValue === undefined) {
27
+ throw new Error(`Environment variable '${varName}' is not defined`);
28
+ }
29
+ return envValue;
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Recursively resolve environment variables in an object
35
+ * @function resolveEnvVarsInObject
36
+ * @param {Object} obj - Object to process
37
+ * @returns {Object} Object with resolved env vars
38
+ */
39
+ function resolveEnvVarsInObject(obj) {
40
+ if (obj === null || obj === undefined) {
41
+ return obj;
42
+ }
43
+ if (typeof obj === 'string') {
44
+ return resolveEnvVar(obj);
45
+ }
46
+ if (Array.isArray(obj)) {
47
+ return obj.map(item => resolveEnvVarsInObject(item));
48
+ }
49
+ if (typeof obj === 'object') {
50
+ const result = {};
51
+ for (const [key, value] of Object.entries(obj)) {
52
+ result[key] = resolveEnvVarsInObject(value);
53
+ }
54
+ return result;
55
+ }
56
+ return obj;
57
+ }
58
+
59
+ /**
60
+ * Format AJV validation errors into user-friendly messages
61
+ * @function formatValidationErrors
62
+ * @param {Object[]} errors - AJV validation errors
63
+ * @returns {string[]} Formatted error messages
64
+ */
65
+ function formatValidationErrors(errors) {
66
+ if (!errors || errors.length === 0) {
67
+ return [];
68
+ }
69
+ return errors.map(error => {
70
+ const path = error.instancePath || '/';
71
+ const message = error.message || 'Unknown validation error';
72
+ if (error.keyword === 'required') {
73
+ return `Missing required field: ${error.params.missingProperty}`;
74
+ }
75
+ if (error.keyword === 'enum') {
76
+ return `${path}: ${message}. Allowed values: ${error.params.allowedValues.join(', ')}`;
77
+ }
78
+ if (error.keyword === 'pattern') {
79
+ return `${path}: ${message}`;
80
+ }
81
+ if (error.keyword === 'additionalProperties') {
82
+ return `${path}: Unknown property '${error.params.additionalProperty}'`;
83
+ }
84
+ return `${path}: ${message}`;
85
+ });
86
+ }
87
+
88
+ /**
89
+ * Load and parse wizard.yaml file
90
+ * @async
91
+ * @function loadWizardConfig
92
+ * @param {string} configPath - Path to wizard.yaml file
93
+ * @returns {Promise<Object>} Parsed configuration object
94
+ * @throws {Error} If file cannot be read or parsed
95
+ */
96
+ async function loadWizardConfig(configPath) {
97
+ const resolvedPath = path.resolve(configPath);
98
+ try {
99
+ await fs.access(resolvedPath);
100
+ const content = await fs.readFile(resolvedPath, 'utf8');
101
+ const config = yaml.load(content);
102
+ if (!config || typeof config !== 'object') {
103
+ throw new Error('Configuration file is empty or invalid');
104
+ }
105
+ return config;
106
+ } catch (error) {
107
+ if (error.code === 'ENOENT') {
108
+ throw new Error(`Configuration file not found: ${resolvedPath}`);
109
+ }
110
+ if (error.name === 'YAMLException') {
111
+ throw new Error(`Invalid YAML syntax: ${error.message}`);
112
+ }
113
+ throw error;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Validate wizard configuration against schema
119
+ * @function validateWizardConfigSchema
120
+ * @param {Object} config - Configuration object to validate
121
+ * @returns {Object} Validation result with valid flag and errors
122
+ */
123
+ function validateWizardConfigSchema(config) {
124
+ const ajv = new Ajv({ allErrors: true, strict: false });
125
+ const validate = ajv.compile(wizardConfigSchema);
126
+ const valid = validate(config);
127
+ return {
128
+ valid,
129
+ errors: valid ? [] : formatValidationErrors(validate.errors)
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Validate file path exists (for openapi-file source type)
135
+ * @function validateFilePath
136
+ * @param {string} filePath - Path to validate
137
+ * @param {string} basePath - Base path for relative paths
138
+ * @returns {Promise<Object>} Validation result with valid flag and errors
139
+ */
140
+ async function validateFilePath(filePath, basePath) {
141
+ const baseDir = path.resolve(basePath);
142
+ const resolvedPath = path.isAbsolute(filePath)
143
+ ? path.resolve(filePath)
144
+ : path.resolve(baseDir, filePath);
145
+ if (!path.isAbsolute(filePath)) {
146
+ const baseWithSep = baseDir.endsWith(path.sep) ? baseDir : `${baseDir}${path.sep}`;
147
+ if (!resolvedPath.startsWith(baseWithSep)) {
148
+ return {
149
+ valid: false,
150
+ errors: [`OpenAPI file path must be within: ${baseDir}`]
151
+ };
152
+ }
153
+ }
154
+ try {
155
+ await fs.access(resolvedPath);
156
+ } catch (error) {
157
+ return {
158
+ valid: false,
159
+ errors: [`OpenAPI file not found: ${resolvedPath}`]
160
+ };
161
+ }
162
+ return { valid: true, errors: [] };
163
+ }
164
+
165
+ /**
166
+ * Validate schema and resolve environment variables
167
+ * @function validateSchemaAndResolveEnvVars
168
+ * @param {Object} config - Configuration object
169
+ * @param {boolean} shouldResolveEnvVars - Whether to resolve env vars
170
+ * @returns {Object} Result with valid flag, errors, and config
171
+ */
172
+ function validateSchemaAndResolveEnvVars(config, shouldResolveEnvVars) {
173
+ if (shouldResolveEnvVars) {
174
+ try {
175
+ config = resolveEnvVarsInObject(config);
176
+ } catch (error) {
177
+ return { valid: false, errors: [error.message], config };
178
+ }
179
+ }
180
+ const schemaResult = validateWizardConfigSchema(config);
181
+ if (!schemaResult.valid) {
182
+ return { valid: false, errors: schemaResult.errors, config };
183
+ }
184
+ return { valid: true, errors: [], config };
185
+ }
186
+
187
+ /**
188
+ * Perform additional semantic validations
189
+ * @function performSemanticValidations
190
+ * @param {Object} config - Configuration object
191
+ * @param {string} configPath - Path to config file
192
+ * @param {boolean} validateFilePaths - Whether to validate file paths
193
+ * @returns {Promise<Object>} Validation result with errors array
194
+ */
195
+ async function performSemanticValidations(config, configPath, validateFilePaths) {
196
+ const errors = [];
197
+ if (validateFilePaths && config.source?.type === 'openapi-file' && config.source?.filePath) {
198
+ const basePath = path.dirname(path.resolve(configPath));
199
+ const fileResult = await validateFilePath(config.source.filePath, basePath);
200
+ if (!fileResult.valid) {
201
+ errors.push(...fileResult.errors);
202
+ }
203
+ }
204
+ if (config.mode === 'add-datasource' && !config.systemIdOrKey) {
205
+ errors.push('\'systemIdOrKey\' is required when mode is \'add-datasource\'');
206
+ }
207
+ return errors;
208
+ }
209
+
210
+ /**
211
+ * Validate wizard configuration with all checks
212
+ * @async
213
+ * @function validateWizardConfig
214
+ * @param {string} configPath - Path to wizard.yaml file
215
+ * @param {Object} [options] - Validation options
216
+ * @param {boolean} [options.resolveEnvVars=true] - Whether to resolve env vars
217
+ * @param {boolean} [options.validateFilePaths=true] - Whether to validate file paths
218
+ * @returns {Promise<Object>} Validation result with valid flag, errors, and config
219
+ */
220
+ async function validateWizardConfig(configPath, options = {}) {
221
+ const { resolveEnvVars: shouldResolveEnvVars = true, validateFilePaths = true } = options;
222
+ let config;
223
+ try {
224
+ config = await loadWizardConfig(configPath);
225
+ } catch (error) {
226
+ return { valid: false, errors: [error.message], config: null };
227
+ }
228
+ const schemaResult = validateSchemaAndResolveEnvVars(config, shouldResolveEnvVars);
229
+ if (!schemaResult.valid) {
230
+ return schemaResult;
231
+ }
232
+ config = schemaResult.config;
233
+ const errors = await performSemanticValidations(config, configPath, validateFilePaths);
234
+ return { valid: errors.length === 0, errors, config };
235
+ }
236
+
237
+ /**
238
+ * Display validation results to console
239
+ * @function displayValidationResults
240
+ * @param {Object} result - Validation result
241
+ * @param {boolean} result.valid - Whether validation passed
242
+ * @param {string[]} result.errors - Array of error messages
243
+ */
244
+ function displayValidationResults(result) {
245
+ const chalk = require('chalk');
246
+ if (result.valid) {
247
+ // eslint-disable-next-line no-console
248
+ console.log(chalk.green('Wizard configuration is valid'));
249
+ } else {
250
+ // eslint-disable-next-line no-console
251
+ console.log(chalk.red('✗ Wizard configuration validation failed:'));
252
+ result.errors.forEach(error => {
253
+ // eslint-disable-next-line no-console
254
+ console.log(chalk.red(` - ${error}`));
255
+ });
256
+ }
257
+ }
258
+
259
+ module.exports = {
260
+ loadWizardConfig,
261
+ validateWizardConfig,
262
+ validateWizardConfigSchema,
263
+ resolveEnvVar,
264
+ resolveEnvVarsInObject,
265
+ formatValidationErrors,
266
+ displayValidationResults
267
+ };