@aifabrix/builder 2.31.1 → 2.32.2

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 (118) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +10 -10
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +158 -136
  67. package/lib/schema/external-system.schema.json +43 -1
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +31 -2
  111. package/templates/external-system/deploy.ps1.hbs +34 -0
  112. package/templates/external-system/deploy.sh.hbs +34 -0
  113. package/templates/external-system/external-datasource.json.hbs +31 -12
  114. package/lib/app.js +0 -467
  115. package/lib/datasource-list.js +0 -141
  116. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  117. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  118. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -11,7 +11,7 @@
11
11
  const fs = require('fs').promises;
12
12
  const fsSync = require('fs');
13
13
  const path = require('path');
14
- const { getProjectRoot } = require('./utils/paths');
14
+ const { getProjectRoot } = require('../utils/paths');
15
15
 
16
16
  /**
17
17
  * Validates that a template exists and contains files
@@ -112,17 +112,14 @@ async function copyTemplateFiles(templateName, appPath) {
112
112
  * @returns {Promise<string[]>} Array of copied file paths
113
113
  * @throws {Error} If language template doesn't exist or copying fails
114
114
  */
115
- async function copyAppFiles(language, appPath) {
116
- if (!language || typeof language !== 'string') {
117
- throw new Error('Language is required and must be a string');
118
- }
119
-
120
- const normalizedLanguage = language.toLowerCase();
121
- // Use getProjectRoot to reliably find templates in all environments (including CI)
122
- const projectRoot = getProjectRoot();
123
- const languageTemplatePath = path.join(projectRoot, 'templates', normalizedLanguage);
124
-
125
- // Check if language template folder exists
115
+ /**
116
+ * Validates language template path
117
+ * @function validateLanguageTemplatePath
118
+ * @param {string} languageTemplatePath - Template path
119
+ * @param {string} normalizedLanguage - Normalized language name
120
+ * @throws {Error} If path is invalid
121
+ */
122
+ function validateLanguageTemplatePath(languageTemplatePath, normalizedLanguage) {
126
123
  if (!fsSync.existsSync(languageTemplatePath)) {
127
124
  throw new Error(`Language template '${normalizedLanguage}' not found. Expected folder: templates/${normalizedLanguage}/`);
128
125
  }
@@ -131,14 +128,17 @@ async function copyAppFiles(language, appPath) {
131
128
  if (!stats.isDirectory()) {
132
129
  throw new Error(`Language template '${normalizedLanguage}' exists but is not a directory`);
133
130
  }
131
+ }
134
132
 
135
- const copiedFiles = [];
136
- const entries = await fs.readdir(languageTemplatePath);
137
-
138
- // Copy only application files, skip Dockerfile and docker-compose templates
139
- const appFiles = entries.filter(entry => {
133
+ /**
134
+ * Filters application files from entries
135
+ * @function filterAppFiles
136
+ * @param {string[]} entries - Directory entries
137
+ * @returns {string[]} Filtered application files
138
+ */
139
+ function filterAppFiles(entries) {
140
+ return entries.filter(entry => {
140
141
  const lowerEntry = entry.toLowerCase();
141
- // Include .gitignore, exclude .hbs files and docker-related files
142
142
  if (entry === '.gitignore') {
143
143
  return true;
144
144
  }
@@ -153,15 +153,46 @@ async function copyAppFiles(language, appPath) {
153
153
  }
154
154
  return true;
155
155
  });
156
+ }
156
157
 
158
+ /**
159
+ * Copies a single file
160
+ * @async
161
+ * @function copySingleFile
162
+ * @param {string} sourcePath - Source file path
163
+ * @param {string} targetPath - Target file path
164
+ * @returns {Promise<string|null>} Target path if copied, null otherwise
165
+ */
166
+ async function copySingleFile(sourcePath, targetPath) {
167
+ const entryStats = await fs.stat(sourcePath);
168
+ if (entryStats.isFile()) {
169
+ await fs.copyFile(sourcePath, targetPath);
170
+ return targetPath;
171
+ }
172
+ return null;
173
+ }
174
+
175
+ async function copyAppFiles(language, appPath) {
176
+ if (!language || typeof language !== 'string') {
177
+ throw new Error('Language is required and must be a string');
178
+ }
179
+
180
+ const normalizedLanguage = language.toLowerCase();
181
+ const projectRoot = getProjectRoot();
182
+ const languageTemplatePath = path.join(projectRoot, 'templates', normalizedLanguage);
183
+
184
+ validateLanguageTemplatePath(languageTemplatePath, normalizedLanguage);
185
+
186
+ const entries = await fs.readdir(languageTemplatePath);
187
+ const appFiles = filterAppFiles(entries);
188
+
189
+ const copiedFiles = [];
157
190
  for (const entry of appFiles) {
158
191
  const sourcePath = path.join(languageTemplatePath, entry);
159
192
  const targetPath = path.join(appPath, entry);
160
-
161
- const entryStats = await fs.stat(sourcePath);
162
- if (entryStats.isFile()) {
163
- await fs.copyFile(sourcePath, targetPath);
164
- copiedFiles.push(targetPath);
193
+ const copied = await copySingleFile(sourcePath, targetPath);
194
+ if (copied) {
195
+ copiedFiles.push(copied);
165
196
  }
166
197
  }
167
198
 
@@ -13,11 +13,11 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const chalk = require('chalk');
15
15
  const validator = require('./validator');
16
- const { resolveExternalFiles } = require('./utils/schema-resolver');
17
- const { loadExternalSystemSchema, loadExternalDataSourceSchema, detectSchemaType } = require('./utils/schema-loader');
18
- const { formatValidationErrors } = require('./utils/error-formatter');
19
- const { detectAppType } = require('./utils/paths');
20
- const logger = require('./utils/logger');
16
+ const { resolveExternalFiles } = require('../utils/schema-resolver');
17
+ const { loadExternalSystemSchema, loadExternalDataSourceSchema, detectSchemaType } = require('../utils/schema-loader');
18
+ const { formatValidationErrors } = require('../utils/error-formatter');
19
+ const { detectAppType } = require('../utils/paths');
20
+ const logger = require('../utils/logger');
21
21
 
22
22
  /**
23
23
  * Validates a file path (detects type and validates)
@@ -49,10 +49,10 @@ async function validateExternalFilesForApp(appName) {
49
49
  for (const file of files) {
50
50
  const result = await validateExternalFile(file.path, file.type);
51
51
  validations.push({
52
+ ...result,
52
53
  file: file.fileName,
53
54
  path: file.path,
54
- type: file.type,
55
- ...result
55
+ type: file.type
56
56
  });
57
57
  }
58
58
 
@@ -103,54 +103,89 @@ function aggregateValidationResults(appValidation, externalValidations, rbacVali
103
103
  * @param {string} type - File type: 'system' | 'datasource'
104
104
  * @returns {Promise<Object>} Validation result
105
105
  */
106
- async function validateExternalFile(filePath, type) {
107
- if (!fs.existsSync(filePath)) {
108
- throw new Error(`File not found: ${filePath}`);
109
- }
110
-
106
+ /**
107
+ * Parses JSON file content
108
+ * @function parseJsonFileContent
109
+ * @param {string} filePath - File path
110
+ * @returns {Object} Parse result with parsed object or error
111
+ */
112
+ function parseJsonFileContent(filePath) {
111
113
  const content = fs.readFileSync(filePath, 'utf8');
112
- let parsed;
113
-
114
114
  try {
115
- parsed = JSON.parse(content);
115
+ return { parsed: JSON.parse(content), error: null };
116
116
  } catch (error) {
117
117
  return {
118
- valid: false,
119
- errors: [`Invalid JSON syntax: ${error.message}`],
120
- warnings: []
118
+ parsed: null,
119
+ error: {
120
+ valid: false,
121
+ errors: [`Invalid JSON syntax: ${error.message}`],
122
+ warnings: []
123
+ }
121
124
  };
122
125
  }
126
+ }
123
127
 
124
- let validate;
125
- // Normalize type: handle both 'external-system'/'external-datasource' and 'system'/'datasource'
126
- const normalizedType = type === 'external-system' ? 'system' : (type === 'external-datasource' ? 'datasource' : type);
128
+ /**
129
+ * Gets validator function for file type
130
+ * @function getValidatorForType
131
+ * @param {string} normalizedType - Normalized file type
132
+ * @returns {Function} Validator function
133
+ * @throws {Error} If type is unknown
134
+ */
135
+ function getValidatorForType(normalizedType) {
127
136
  if (normalizedType === 'system') {
128
- validate = loadExternalSystemSchema();
129
- } else if (normalizedType === 'datasource') {
130
- validate = loadExternalDataSourceSchema();
131
- } else {
132
- throw new Error(`Unknown file type: ${type}`);
137
+ return loadExternalSystemSchema();
138
+ }
139
+ if (normalizedType === 'datasource') {
140
+ return loadExternalDataSourceSchema();
133
141
  }
142
+ throw new Error(`Unknown file type: ${normalizedType}`);
143
+ }
134
144
 
135
- const valid = validate(parsed);
145
+ /**
146
+ * Validates role references in permissions
147
+ * @function validateRoleReferences
148
+ * @param {Object} parsed - Parsed JSON object
149
+ * @param {string[]} errors - Errors array to append to
150
+ */
151
+ function validateRoleReferences(parsed, errors) {
152
+ if (!parsed.permissions || !Array.isArray(parsed.permissions)) {
153
+ return;
154
+ }
155
+
156
+ const roles = parsed.roles || [];
157
+ const roleValues = new Set(roles.map(r => r.value));
158
+
159
+ parsed.permissions.forEach((permission, index) => {
160
+ if (permission.roles && Array.isArray(permission.roles)) {
161
+ permission.roles.forEach(roleValue => {
162
+ if (!roleValues.has(roleValue)) {
163
+ errors.push(`Permission "${permission.name}" (index ${index}) references role "${roleValue}" which does not exist in roles array`);
164
+ }
165
+ });
166
+ }
167
+ });
168
+ }
169
+
170
+ async function validateExternalFile(filePath, type) {
171
+ if (!fs.existsSync(filePath)) {
172
+ throw new Error(`File not found: ${filePath}`);
173
+ }
174
+
175
+ const parseResult = parseJsonFileContent(filePath);
176
+ if (parseResult.error) {
177
+ return parseResult.error;
178
+ }
179
+
180
+ const normalizedType = type === 'external-system' ? 'system' : (type === 'external-datasource' ? 'datasource' : type);
181
+ const validate = getValidatorForType(normalizedType);
182
+ const valid = validate(parseResult.parsed);
136
183
 
137
184
  const errors = valid ? [] : formatValidationErrors(validate.errors);
138
185
  const warnings = [];
139
186
 
140
- // Additional validation for external system files: check role references in permissions
141
- if (normalizedType === 'system' && parsed.permissions && Array.isArray(parsed.permissions)) {
142
- const roles = parsed.roles || [];
143
- const roleValues = new Set(roles.map(r => r.value));
144
-
145
- parsed.permissions.forEach((permission, index) => {
146
- if (permission.roles && Array.isArray(permission.roles)) {
147
- permission.roles.forEach(roleValue => {
148
- if (!roleValues.has(roleValue)) {
149
- errors.push(`Permission "${permission.name}" (index ${index}) references role "${roleValue}" which does not exist in roles array`);
150
- }
151
- });
152
- }
153
- });
187
+ if (normalizedType === 'system') {
188
+ validateRoleReferences(parseResult.parsed, errors);
154
189
  }
155
190
 
156
191
  return {
@@ -176,43 +211,38 @@ async function validateExternalFile(filePath, type) {
176
211
  * const result = await validateAppOrFile('myapp');
177
212
  * // Returns: { valid: true, application: {...}, externalFiles: [...] }
178
213
  */
179
- async function validateAppOrFile(appOrFile) {
180
- if (!appOrFile || typeof appOrFile !== 'string') {
181
- throw new Error('App name or file path is required');
182
- }
183
-
184
- // Check if it's a file path (exists and is a file)
185
- const isFilePath = fs.existsSync(appOrFile) && fs.statSync(appOrFile).isFile();
186
-
187
- if (isFilePath) {
188
- return await validateFilePath(appOrFile);
214
+ /**
215
+ * Validates RBAC for external systems
216
+ * @async
217
+ * @function validateRbacForExternalSystem
218
+ * @param {boolean} isExternal - Whether app is external system
219
+ * @param {string} appName - Application name
220
+ * @returns {Promise<Object|null>} RBAC validation result or null
221
+ */
222
+ async function validateRbacForExternalSystem(isExternal, appName) {
223
+ if (!isExternal) {
224
+ return null;
189
225
  }
190
226
 
191
- // Treat as app name
192
- const appName = appOrFile;
193
-
194
- // Detect app type to support both builder/ and integration/ directories
195
- const { appPath, isExternal } = await detectAppType(appName);
196
-
197
- // Validate application
198
- const appValidation = await validator.validateApplication(appName);
199
-
200
- // Validate rbac.yaml for external systems
201
- let rbacValidation = null;
202
- if (isExternal) {
203
- try {
204
- rbacValidation = await validator.validateRbac(appName);
205
- } catch (error) {
206
- rbacValidation = {
207
- valid: false,
208
- errors: [error.message],
209
- warnings: []
210
- };
211
- }
227
+ try {
228
+ return await validator.validateRbac(appName);
229
+ } catch (error) {
230
+ return {
231
+ valid: false,
232
+ errors: [error.message],
233
+ warnings: []
234
+ };
212
235
  }
236
+ }
213
237
 
214
- // Check for externalIntegration block
215
- const variablesPath = path.join(appPath, 'variables.yaml');
238
+ /**
239
+ * Loads and checks variables.yaml for externalIntegration
240
+ * @function loadVariablesAndCheckExternalIntegration
241
+ * @param {string} variablesPath - Path to variables.yaml
242
+ * @param {Object} appValidation - Application validation result
243
+ * @returns {Object|null} Variables object or null if no externalIntegration
244
+ */
245
+ function loadVariablesAndCheckExternalIntegration(variablesPath, appValidation) {
216
246
  if (!fs.existsSync(variablesPath)) {
217
247
  return {
218
248
  valid: appValidation.valid,
@@ -236,7 +266,6 @@ async function validateAppOrFile(appOrFile) {
236
266
  };
237
267
  }
238
268
 
239
- // If no externalIntegration block, return app validation only
240
269
  if (!variables.externalIntegration) {
241
270
  return {
242
271
  valid: appValidation.valid,
@@ -245,10 +274,31 @@ async function validateAppOrFile(appOrFile) {
245
274
  };
246
275
  }
247
276
 
248
- // Validate external files
249
- const externalValidations = await validateExternalFilesForApp(appName);
277
+ return null;
278
+ }
279
+
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;
299
+ }
250
300
 
251
- // Aggregate results
301
+ const externalValidations = await validateExternalFilesForApp(appName);
252
302
  return aggregateValidationResults(appValidation, externalValidations, rbacValidation);
253
303
  }
254
304
 
@@ -258,69 +308,97 @@ async function validateAppOrFile(appOrFile) {
258
308
  * @function displayValidationResults
259
309
  * @param {Object} result - Validation result from validateAppOrFile
260
310
  */
261
- function displayValidationResults(result) {
262
- if (result.valid) {
263
- logger.log(chalk.green('\n✓ Validation passed!'));
311
+ /**
312
+ * Displays application validation results
313
+ * @function displayApplicationValidation
314
+ * @param {Object} application - Application validation result
315
+ */
316
+ function displayApplicationValidation(application) {
317
+ if (!application) {
318
+ return;
319
+ }
320
+
321
+ logger.log(chalk.blue('\nApplication:'));
322
+ if (application.valid) {
323
+ logger.log(chalk.green(' ✓ Application configuration is valid'));
264
324
  } else {
265
- logger.log(chalk.red('\nValidation failed!'));
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}`));
333
+ });
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Displays external files validation results
339
+ * @function displayExternalFilesValidation
340
+ * @param {Array} externalFiles - External files validation results
341
+ */
342
+ function displayExternalFilesValidation(externalFiles) {
343
+ if (!externalFiles || externalFiles.length === 0) {
344
+ return;
266
345
  }
267
346
 
268
- // Display application validation
269
- if (result.application) {
270
- logger.log(chalk.blue('\nApplication:'));
271
- if (result.application.valid) {
272
- logger.log(chalk.green(' ✓ Application configuration is valid'));
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})`));
273
351
  } else {
274
- logger.log(chalk.red('Application configuration has errors:'));
275
- result.application.errors.forEach(error => {
352
+ logger.log(chalk.red(`${file.file} (${file.type}):`));
353
+ file.errors.forEach(error => {
276
354
  logger.log(chalk.red(` • ${error}`));
277
355
  });
278
356
  }
279
- if (result.application.warnings && result.application.warnings.length > 0) {
280
- result.application.warnings.forEach(warning => {
357
+ if (file.warnings && file.warnings.length > 0) {
358
+ file.warnings.forEach(warning => {
281
359
  logger.log(chalk.yellow(` ⚠ ${warning}`));
282
360
  });
283
361
  }
362
+ });
363
+ }
364
+
365
+ /**
366
+ * Displays RBAC validation results
367
+ * @function displayRbacValidation
368
+ * @param {Object} rbac - RBAC validation result
369
+ */
370
+ function displayRbacValidation(rbac) {
371
+ if (!rbac) {
372
+ return;
284
373
  }
285
374
 
286
- // Display external files validation
287
- if (result.externalFiles && result.externalFiles.length > 0) {
288
- logger.log(chalk.blue('\nExternal Integration Files:'));
289
- result.externalFiles.forEach(file => {
290
- if (file.valid) {
291
- logger.log(chalk.green(` ✓ ${file.file} (${file.type})`));
292
- } else {
293
- logger.log(chalk.red(` ✗ ${file.file} (${file.type}):`));
294
- file.errors.forEach(error => {
295
- logger.log(chalk.red(` • ${error}`));
296
- });
297
- }
298
- if (file.warnings && file.warnings.length > 0) {
299
- file.warnings.forEach(warning => {
300
- logger.log(chalk.yellow(` ⚠ ${warning}`));
301
- });
302
- }
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}`));
303
382
  });
304
383
  }
384
+ if (rbac.warnings && rbac.warnings.length > 0) {
385
+ rbac.warnings.forEach(warning => {
386
+ logger.log(chalk.yellow(` ⚠ ${warning}`));
387
+ });
388
+ }
389
+ }
305
390
 
306
- // Display rbac validation (for external systems)
307
- if (result.rbac) {
308
- logger.log(chalk.blue('\nRBAC Configuration:'));
309
- if (result.rbac.valid) {
310
- logger.log(chalk.green(' RBAC configuration is valid'));
311
- } else {
312
- logger.log(chalk.red(' ✗ RBAC configuration has errors:'));
313
- result.rbac.errors.forEach(error => {
314
- logger.log(chalk.red(` • ${error}`));
315
- });
316
- }
317
- if (result.rbac.warnings && result.rbac.warnings.length > 0) {
318
- result.rbac.warnings.forEach(warning => {
319
- logger.log(chalk.yellow(` ⚠ ${warning}`));
320
- });
321
- }
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!'));
322
396
  }
323
397
 
398
+ displayApplicationValidation(result.application);
399
+ displayExternalFilesValidation(result.externalFiles);
400
+ displayRbacValidation(result.rbac);
401
+
324
402
  // Display file validation (for direct file validation)
325
403
  if (result.file) {
326
404
  logger.log(chalk.blue(`\nFile: ${result.file}`));
@@ -352,6 +430,8 @@ function displayValidationResults(result) {
352
430
  module.exports = {
353
431
  validateAppOrFile,
354
432
  displayValidationResults,
355
- validateExternalFile
433
+ validateExternalFile,
434
+ validateExternalFilesForApp,
435
+ validateFilePath
356
436
  };
357
437
 
@@ -13,13 +13,13 @@ const fs = require('fs');
13
13
  const path = require('path');
14
14
  const yaml = require('js-yaml');
15
15
  const Ajv = require('ajv');
16
- const applicationSchema = require('./schema/application-schema.json');
17
- const externalSystemSchema = require('./schema/external-system.schema.json');
18
- const externalDataSourceSchema = require('./schema/external-datasource.schema.json');
19
- const { transformVariablesForValidation } = require('./utils/variable-transformer');
20
- const { checkEnvironment } = require('./utils/environment-checker');
21
- const { formatValidationErrors } = require('./utils/error-formatter');
22
- const { detectAppType } = require('./utils/paths');
16
+ const applicationSchema = require('../schema/application-schema.json');
17
+ const externalSystemSchema = require('../schema/external-system.schema.json');
18
+ const externalDataSourceSchema = require('../schema/external-datasource.schema.json');
19
+ const { transformVariablesForValidation } = require('../utils/variable-transformer');
20
+ const { checkEnvironment } = require('../utils/environment-checker');
21
+ const { formatValidationErrors } = require('../utils/error-formatter');
22
+ const { detectAppType } = require('../utils/paths');
23
23
 
24
24
  /**
25
25
  * Validates variables.yaml file against application schema
@@ -35,12 +35,15 @@ const { detectAppType } = require('./utils/paths');
35
35
  * const result = await validateVariables('myapp');
36
36
  * // Returns: { valid: true, errors: [], warnings: [] }
37
37
  */
38
- async function validateVariables(appName) {
39
- if (!appName || typeof appName !== 'string') {
40
- throw new Error('App name is required and must be a string');
41
- }
42
-
43
- // Detect app type and get correct path (integration or builder)
38
+ /**
39
+ * Loads and parses variables.yaml
40
+ * @async
41
+ * @function loadVariablesYaml
42
+ * @param {string} appName - Application name
43
+ * @returns {Promise<Object>} Variables object
44
+ * @throws {Error} If file not found or invalid
45
+ */
46
+ async function loadVariablesYaml(appName) {
44
47
  const { appPath } = await detectAppType(appName);
45
48
  const variablesPath = path.join(appPath, 'variables.yaml');
46
49
 
@@ -49,51 +52,67 @@ async function validateVariables(appName) {
49
52
  }
50
53
 
51
54
  const content = fs.readFileSync(variablesPath, 'utf8');
52
- let variables;
53
-
54
55
  try {
55
- variables = yaml.load(content);
56
+ return yaml.load(content);
56
57
  } catch (error) {
57
58
  throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
58
59
  }
60
+ }
59
61
 
60
- // Transform nested structure to flat schema format
61
- const transformed = transformVariablesForValidation(variables, appName);
62
-
62
+ /**
63
+ * Sets up AJV validator with external schemas
64
+ * @function setupAjvValidator
65
+ * @returns {Function} Compiled validator function
66
+ */
67
+ function setupAjvValidator() {
63
68
  const ajv = new Ajv({ allErrors: true, strict: false });
64
- // Register external schemas with their $id (GitHub raw URLs)
65
- // Create copies to avoid modifying the original schemas
66
69
  const externalSystemSchemaCopy = { ...externalSystemSchema };
67
70
  const externalDataSourceSchemaCopy = { ...externalDataSourceSchema };
68
- // Remove $schema for draft-2020-12 to avoid AJV issues
71
+
69
72
  if (externalDataSourceSchemaCopy.$schema && externalDataSourceSchemaCopy.$schema.includes('2020-12')) {
70
73
  delete externalDataSourceSchemaCopy.$schema;
71
74
  }
75
+
72
76
  ajv.addSchema(externalSystemSchemaCopy, externalSystemSchema.$id);
73
77
  ajv.addSchema(externalDataSourceSchemaCopy, externalDataSourceSchema.$id);
74
- const validate = ajv.compile(applicationSchema);
78
+ return ajv.compile(applicationSchema);
79
+ }
80
+
81
+ /**
82
+ * Validates external integration block
83
+ * @function validateExternalIntegrationBlock
84
+ * @param {Object} variables - Variables object
85
+ * @param {string[]} errors - Errors array to append to
86
+ */
87
+ function validateExternalIntegrationBlock(variables, errors) {
88
+ if (!variables.externalIntegration) {
89
+ errors.push('externalIntegration block is required when app.type is "external"');
90
+ return;
91
+ }
92
+
93
+ if (!variables.externalIntegration.schemaBasePath) {
94
+ errors.push('externalIntegration.schemaBasePath is required');
95
+ }
96
+ if (!variables.externalIntegration.systems || !Array.isArray(variables.externalIntegration.systems) || variables.externalIntegration.systems.length === 0) {
97
+ errors.push('externalIntegration.systems must be a non-empty array');
98
+ }
99
+ }
100
+
101
+ async function validateVariables(appName) {
102
+ if (!appName || typeof appName !== 'string') {
103
+ throw new Error('App name is required and must be a string');
104
+ }
105
+
106
+ const variables = await loadVariablesYaml(appName);
107
+ const transformed = transformVariablesForValidation(variables, appName);
108
+ const validate = setupAjvValidator();
75
109
  const valid = validate(transformed);
76
110
 
77
- // Additional explicit validation for external type
78
111
  const errors = valid ? [] : formatValidationErrors(validate.errors);
79
112
  const warnings = [];
80
113
 
81
- // If type is external, perform additional checks
82
114
  if (variables.app && variables.app.type === 'external') {
83
- // Check for externalIntegration block
84
- if (!variables.externalIntegration) {
85
- errors.push('externalIntegration block is required when app.type is "external"');
86
- } else {
87
- // Validate externalIntegration structure
88
- if (!variables.externalIntegration.schemaBasePath) {
89
- errors.push('externalIntegration.schemaBasePath is required');
90
- }
91
- if (!variables.externalIntegration.systems || !Array.isArray(variables.externalIntegration.systems) || variables.externalIntegration.systems.length === 0) {
92
- errors.push('externalIntegration.systems must be a non-empty array');
93
- }
94
- // Note: dataSources can be empty, so we don't validate that here
95
- // File existence is validated during build/deploy, not during schema validation
96
- }
115
+ validateExternalIntegrationBlock(variables, errors);
97
116
  }
98
117
 
99
118
  return {