@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.
- package/.cursor/rules/project-rules.mdc +8 -0
- package/README.md +36 -8
- package/bin/aifabrix.js +6 -8
- package/integration/hubspot/README.md +12 -11
- package/integration/hubspot/companies.json +2048 -0
- package/integration/hubspot/create-hubspot.js +665 -0
- package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
- package/integration/hubspot/hubspot-deploy.json +832 -81
- package/integration/hubspot/hubspot-system.json +99 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
- package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
- package/integration/hubspot/test-dataplane-down-tests.js +419 -0
- package/integration/hubspot/test-dataplane-down.js +157 -0
- package/integration/hubspot/test.js +1517 -0
- package/integration/hubspot/variables.yaml +4 -4
- package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
- package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
- package/lib/api/applications.api.js +1 -0
- package/lib/api/index.js +6 -2
- package/lib/api/types/wizard.types.js +176 -38
- package/lib/api/wizard.api.js +161 -23
- package/lib/app/deploy.js +116 -54
- package/lib/app/display.js +6 -5
- package/lib/app/dockerfile.js +2 -1
- package/lib/app/list.js +17 -10
- package/lib/app/readme.js +41 -112
- package/lib/app/register.js +44 -9
- package/lib/app/rotate-secret.js +48 -31
- package/lib/cli.js +219 -70
- package/lib/commands/app.js +4 -9
- package/lib/commands/auth-config.js +125 -0
- package/lib/commands/auth-status.js +7 -8
- package/lib/commands/datasource.js +3 -6
- package/lib/commands/login-credentials.js +4 -4
- package/lib/commands/login-device.js +26 -17
- package/lib/commands/login.js +12 -10
- package/lib/commands/wizard-config-normalizer.js +92 -0
- package/lib/commands/wizard-core.js +515 -0
- package/lib/commands/wizard-dataplane.js +122 -0
- package/lib/commands/wizard-headless.js +115 -0
- package/lib/commands/wizard.js +110 -332
- package/lib/core/config.js +46 -0
- package/lib/core/secrets.js +3 -22
- package/lib/core/templates-env.js +1 -1
- package/lib/datasource/deploy.js +59 -23
- package/lib/datasource/list.js +108 -19
- package/lib/deployment/deployer.js +25 -0
- package/lib/deployment/environment.js +10 -13
- package/lib/external-system/delete.js +151 -0
- package/lib/external-system/deploy.js +53 -378
- package/lib/external-system/download-helpers.js +45 -65
- package/lib/external-system/download.js +33 -13
- package/lib/external-system/generator.js +11 -7
- package/lib/external-system/test-auth.js +4 -3
- package/lib/generator/builders.js +3 -1
- package/lib/generator/external-controller-manifest.js +157 -0
- package/lib/generator/external-schema-utils.js +236 -0
- package/lib/generator/external.js +55 -3
- package/lib/generator/index.js +22 -10
- package/lib/generator/wizard-prompts.js +33 -10
- package/lib/generator/wizard.js +69 -86
- package/lib/infrastructure/compose.js +100 -0
- package/lib/infrastructure/helpers.js +139 -0
- package/lib/infrastructure/index.js +52 -311
- package/lib/infrastructure/services.js +168 -0
- package/lib/schema/application-schema.json +23 -4
- package/lib/schema/external-datasource.schema.json +2 -2
- package/lib/schema/wizard-config.schema.json +234 -0
- package/lib/utils/api.js +102 -52
- package/lib/utils/app-existence.js +42 -0
- package/lib/utils/app-register-config.js +7 -2
- package/lib/utils/auth-config-validator.js +92 -0
- package/lib/utils/command-header.js +43 -0
- package/lib/utils/compose-generator.js +113 -70
- package/lib/utils/controller-url.js +65 -17
- package/lib/utils/dataplane-health.js +115 -0
- package/lib/utils/dataplane-resolver.js +29 -0
- package/lib/utils/dev-config.js +6 -2
- package/lib/utils/env-copy.js +2 -1
- package/lib/utils/env-ports.js +2 -1
- package/lib/utils/env-template.js +1 -1
- package/lib/utils/error-formatter.js +49 -0
- package/lib/utils/error-formatters/network-errors.js +13 -3
- package/lib/utils/external-readme.js +125 -0
- package/lib/utils/help-builder.js +190 -0
- package/lib/utils/infra-status.js +13 -3
- package/lib/utils/paths.js +17 -2
- package/lib/utils/port-resolver.js +111 -0
- package/lib/utils/secrets-helpers.js +3 -15
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/token-manager.js +9 -4
- package/lib/utils/variable-transformer.js +7 -2
- package/lib/validation/external-manifest-validator.js +202 -0
- package/lib/validation/validate-display.js +406 -0
- package/lib/validation/validate.js +159 -123
- package/lib/validation/validator.js +36 -3
- package/lib/validation/wizard-config-validator.js +267 -0
- package/package.json +4 -2
- package/templates/applications/README.md.hbs +18 -16
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/applications/miso-controller/rbac.yaml +7 -7
- package/templates/external-system/README.md.hbs +99 -0
- package/templates/github/ci.yaml.hbs +44 -1
- package/templates/github/release.yaml.hbs +44 -0
- package/templates/infra/compose.yaml.hbs +35 -0
- package/templates/python/docker-compose.hbs +26 -0
- 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
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
*
|
|
307
|
-
*
|
|
308
|
-
* @function
|
|
309
|
-
* @param {
|
|
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
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
*
|
|
339
|
-
* @
|
|
340
|
-
* @
|
|
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
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
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
|
|
371
|
-
if (!
|
|
372
|
-
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
392
|
-
if (
|
|
393
|
-
|
|
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
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
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
|
-
|
|
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:
|
|
352
|
-
totalWarnings:
|
|
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
|
+
};
|