@aifabrix/builder 2.6.3 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/aifabrix.js +4 -0
- package/lib/cli.js +51 -2
- package/lib/commands/datasource.js +94 -0
- package/lib/datasource-deploy.js +182 -0
- package/lib/datasource-diff.js +73 -0
- package/lib/datasource-list.js +138 -0
- package/lib/datasource-validate.js +63 -0
- package/lib/diff.js +266 -0
- package/lib/schema/application-schema.json +829 -687
- package/lib/schema/external-datasource.schema.json +464 -0
- package/lib/schema/external-system.schema.json +262 -0
- package/lib/secrets.js +20 -1
- package/lib/utils/env-copy.js +24 -0
- package/lib/utils/env-endpoints.js +50 -11
- package/lib/utils/schema-loader.js +220 -0
- package/lib/utils/schema-resolver.js +174 -0
- package/lib/utils/secrets-helpers.js +65 -17
- package/lib/validate.js +299 -0
- package/package.json +1 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Resolution Utilities
|
|
3
|
+
*
|
|
4
|
+
* Resolves paths for external integration schemas from application configuration.
|
|
5
|
+
* Handles schemaBasePath resolution and external file discovery.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Schema path resolution utilities for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const yaml = require('js-yaml');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolves schemaBasePath from application variables.yaml
|
|
18
|
+
* Supports both absolute and relative paths
|
|
19
|
+
*
|
|
20
|
+
* @async
|
|
21
|
+
* @function resolveSchemaBasePath
|
|
22
|
+
* @param {string} appName - Application name
|
|
23
|
+
* @returns {Promise<string>} Resolved absolute path to schema base directory
|
|
24
|
+
* @throws {Error} If variables.yaml not found, externalIntegration missing, or path invalid
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* const basePath = await resolveSchemaBasePath('myapp');
|
|
28
|
+
* // Returns: '/path/to/builder/myapp/schemas'
|
|
29
|
+
*/
|
|
30
|
+
async function resolveSchemaBasePath(appName) {
|
|
31
|
+
if (!appName || typeof appName !== 'string') {
|
|
32
|
+
throw new Error('App name is required and must be a string');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
36
|
+
|
|
37
|
+
if (!fs.existsSync(variablesPath)) {
|
|
38
|
+
throw new Error(`variables.yaml not found: ${variablesPath}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const content = fs.readFileSync(variablesPath, 'utf8');
|
|
42
|
+
let variables;
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
variables = yaml.load(content);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check if externalIntegration block exists
|
|
51
|
+
if (!variables.externalIntegration) {
|
|
52
|
+
throw new Error(`externalIntegration block not found in variables.yaml for app: ${appName}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!variables.externalIntegration.schemaBasePath) {
|
|
56
|
+
throw new Error(`schemaBasePath not found in externalIntegration block for app: ${appName}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const schemaBasePath = variables.externalIntegration.schemaBasePath;
|
|
60
|
+
const variablesDir = path.dirname(variablesPath);
|
|
61
|
+
|
|
62
|
+
// Resolve path (absolute or relative to variables.yaml location)
|
|
63
|
+
let resolvedPath;
|
|
64
|
+
if (path.isAbsolute(schemaBasePath)) {
|
|
65
|
+
resolvedPath = schemaBasePath;
|
|
66
|
+
} else {
|
|
67
|
+
resolvedPath = path.resolve(variablesDir, schemaBasePath);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Normalize path
|
|
71
|
+
resolvedPath = path.normalize(resolvedPath);
|
|
72
|
+
|
|
73
|
+
// Validate path exists
|
|
74
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
75
|
+
throw new Error(`Schema base path does not exist: ${resolvedPath}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (!fs.statSync(resolvedPath).isDirectory()) {
|
|
79
|
+
throw new Error(`Schema base path is not a directory: ${resolvedPath}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return resolvedPath;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Resolves all external system and datasource files from application configuration
|
|
87
|
+
* Returns array of file paths with metadata
|
|
88
|
+
*
|
|
89
|
+
* @async
|
|
90
|
+
* @function resolveExternalFiles
|
|
91
|
+
* @param {string} appName - Application name
|
|
92
|
+
* @returns {Promise<Array<{path: string, type: 'system'|'datasource', fileName: string}>>} Array of resolved file paths with metadata
|
|
93
|
+
* @throws {Error} If files cannot be resolved or do not exist
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* const files = await resolveExternalFiles('myapp');
|
|
97
|
+
* // Returns: [
|
|
98
|
+
* // { path: '/path/to/hubspot.json', type: 'system', fileName: 'hubspot.json' },
|
|
99
|
+
* // { path: '/path/to/hubspot-deal.json', type: 'datasource', fileName: 'hubspot-deal.json' }
|
|
100
|
+
* // ]
|
|
101
|
+
*/
|
|
102
|
+
async function resolveExternalFiles(appName) {
|
|
103
|
+
if (!appName || typeof appName !== 'string') {
|
|
104
|
+
throw new Error('App name is required and must be a string');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
108
|
+
|
|
109
|
+
if (!fs.existsSync(variablesPath)) {
|
|
110
|
+
throw new Error(`variables.yaml not found: ${variablesPath}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const content = fs.readFileSync(variablesPath, 'utf8');
|
|
114
|
+
let variables;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
variables = yaml.load(content);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check if externalIntegration block exists
|
|
123
|
+
if (!variables.externalIntegration) {
|
|
124
|
+
return []; // No external integration, return empty array
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Resolve schema base path
|
|
128
|
+
const schemaBasePath = await resolveSchemaBasePath(appName);
|
|
129
|
+
const resolvedFiles = [];
|
|
130
|
+
|
|
131
|
+
// Resolve systems files
|
|
132
|
+
if (variables.externalIntegration.systems && Array.isArray(variables.externalIntegration.systems)) {
|
|
133
|
+
for (const systemFile of variables.externalIntegration.systems) {
|
|
134
|
+
const systemPath = path.join(schemaBasePath, systemFile);
|
|
135
|
+
const normalizedPath = path.normalize(systemPath);
|
|
136
|
+
|
|
137
|
+
if (!fs.existsSync(normalizedPath)) {
|
|
138
|
+
throw new Error(`External system file not found: ${normalizedPath}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
resolvedFiles.push({
|
|
142
|
+
path: normalizedPath,
|
|
143
|
+
type: 'system',
|
|
144
|
+
fileName: systemFile
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Resolve datasources files
|
|
150
|
+
if (variables.externalIntegration.dataSources && Array.isArray(variables.externalIntegration.dataSources)) {
|
|
151
|
+
for (const datasourceFile of variables.externalIntegration.dataSources) {
|
|
152
|
+
const datasourcePath = path.join(schemaBasePath, datasourceFile);
|
|
153
|
+
const normalizedPath = path.normalize(datasourcePath);
|
|
154
|
+
|
|
155
|
+
if (!fs.existsSync(normalizedPath)) {
|
|
156
|
+
throw new Error(`External datasource file not found: ${normalizedPath}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
resolvedFiles.push({
|
|
160
|
+
path: normalizedPath,
|
|
161
|
+
type: 'datasource',
|
|
162
|
+
fileName: datasourceFile
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return resolvedFiles;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = {
|
|
171
|
+
resolveSchemaBasePath,
|
|
172
|
+
resolveExternalFiles
|
|
173
|
+
};
|
|
174
|
+
|
|
@@ -13,10 +13,10 @@ const path = require('path');
|
|
|
13
13
|
const yaml = require('js-yaml');
|
|
14
14
|
const config = require('../config');
|
|
15
15
|
const { buildHostnameToServiceMap, resolveUrlPort } = require('./secrets-utils');
|
|
16
|
-
const { rewriteInfraEndpoints, getEnvHosts } = require('./env-endpoints');
|
|
16
|
+
const { rewriteInfraEndpoints, getEnvHosts, getServicePort, getServiceHost, getLocalhostOverride } = require('./env-endpoints');
|
|
17
17
|
const { loadEnvConfig } = require('./env-config-loader');
|
|
18
|
-
const { processEnvVariables } = require('./env-copy');
|
|
19
18
|
const { updateContainerPortInEnvFile } = require('./env-ports');
|
|
19
|
+
const { buildEnvVarMap } = require('./env-map');
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
22
|
* Interpolate ${VAR} occurrences with values from envVars map
|
|
@@ -262,6 +262,24 @@ async function adjustLocalEnvPortsInContent(envContent, variablesPath) {
|
|
|
262
262
|
// Update infra endpoints with developer-id adjusted ports for local context
|
|
263
263
|
updated = await rewriteInfraEndpoints(updated, 'local');
|
|
264
264
|
|
|
265
|
+
// Interpolate ${VAR} references created by rewriteInfraEndpoints
|
|
266
|
+
// Get the ports that were just set by rewriteInfraEndpoints for interpolation
|
|
267
|
+
const hostsForPorts = await getEnvHosts('local');
|
|
268
|
+
const redisPort = await getServicePort('REDIS_PORT', 'redis', hostsForPorts, 'local');
|
|
269
|
+
const dbPort = await getServicePort('DB_PORT', 'postgres', hostsForPorts, 'local');
|
|
270
|
+
const localhostOverride = getLocalhostOverride('local');
|
|
271
|
+
const redisHost = getServiceHost(hostsForPorts.REDIS_HOST, 'local', 'localhost', localhostOverride);
|
|
272
|
+
const dbHost = getServiceHost(hostsForPorts.DB_HOST, 'local', 'localhost', localhostOverride);
|
|
273
|
+
|
|
274
|
+
// Build envVars map and ensure it has the correct values
|
|
275
|
+
const envVars = await buildEnvVarMap('local', null, devIdNum);
|
|
276
|
+
// Override with the actual values that were just set by rewriteInfraEndpoints
|
|
277
|
+
envVars.REDIS_HOST = redisHost;
|
|
278
|
+
envVars.REDIS_PORT = String(redisPort);
|
|
279
|
+
envVars.DB_HOST = dbHost;
|
|
280
|
+
envVars.DB_PORT = String(dbPort);
|
|
281
|
+
updated = interpolateEnvVars(updated, envVars);
|
|
282
|
+
|
|
265
283
|
return updated;
|
|
266
284
|
}
|
|
267
285
|
|
|
@@ -276,6 +294,29 @@ function readYamlAtPath(filePath) {
|
|
|
276
294
|
return yaml.load(content);
|
|
277
295
|
}
|
|
278
296
|
|
|
297
|
+
/**
|
|
298
|
+
* Merge a single secret value from canonical into result
|
|
299
|
+
* @function mergeSecretValue
|
|
300
|
+
* @param {Object} result - Result object to merge into
|
|
301
|
+
* @param {string} key - Secret key
|
|
302
|
+
* @param {*} canonicalValue - Value from canonical secrets
|
|
303
|
+
*/
|
|
304
|
+
function mergeSecretValue(result, key, canonicalValue) {
|
|
305
|
+
const currentValue = result[key];
|
|
306
|
+
// Fill missing, empty, or undefined values
|
|
307
|
+
if (!(key in result) || currentValue === undefined || currentValue === null || currentValue === '') {
|
|
308
|
+
result[key] = canonicalValue;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// Only replace values that are encrypted (have secure:// prefix)
|
|
312
|
+
// Plaintext values (no secure://) are used as-is
|
|
313
|
+
if (typeof currentValue === 'string' && typeof canonicalValue === 'string') {
|
|
314
|
+
if (currentValue.startsWith('secure://')) {
|
|
315
|
+
result[key] = canonicalValue;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
279
320
|
/**
|
|
280
321
|
* Apply canonical secrets path override if configured and file exists
|
|
281
322
|
* @async
|
|
@@ -287,21 +328,29 @@ async function applyCanonicalSecretsOverride(currentSecrets) {
|
|
|
287
328
|
let mergedSecrets = currentSecrets || {};
|
|
288
329
|
try {
|
|
289
330
|
const canonicalPath = await config.getSecretsPath();
|
|
290
|
-
if (canonicalPath) {
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
331
|
+
if (!canonicalPath) {
|
|
332
|
+
return mergedSecrets;
|
|
333
|
+
}
|
|
334
|
+
const resolvedCanonical = path.isAbsolute(canonicalPath)
|
|
335
|
+
? canonicalPath
|
|
336
|
+
: path.resolve(process.cwd(), canonicalPath);
|
|
337
|
+
if (!fs.existsSync(resolvedCanonical)) {
|
|
338
|
+
return mergedSecrets;
|
|
339
|
+
}
|
|
340
|
+
const configSecrets = readYamlAtPath(resolvedCanonical);
|
|
341
|
+
if (!configSecrets || typeof configSecrets !== 'object') {
|
|
342
|
+
return mergedSecrets;
|
|
343
|
+
}
|
|
344
|
+
// Apply canonical secrets as a fallback source:
|
|
345
|
+
// - Do NOT override any existing keys from user/build
|
|
346
|
+
// - Add only missing keys from canonical path
|
|
347
|
+
// - Also fill in empty/undefined values from canonical path
|
|
348
|
+
// - Replace encrypted values (secure://) with canonical plaintext
|
|
349
|
+
const result = { ...mergedSecrets };
|
|
350
|
+
for (const [key, canonicalValue] of Object.entries(configSecrets)) {
|
|
351
|
+
mergeSecretValue(result, key, canonicalValue);
|
|
304
352
|
}
|
|
353
|
+
mergedSecrets = result;
|
|
305
354
|
} catch {
|
|
306
355
|
// ignore and fall through
|
|
307
356
|
}
|
|
@@ -351,7 +400,6 @@ module.exports = {
|
|
|
351
400
|
replaceKvInContent,
|
|
352
401
|
resolveServicePortsInEnvContent,
|
|
353
402
|
loadEnvTemplate,
|
|
354
|
-
processEnvVariables,
|
|
355
403
|
updateContainerPortInEnvFile,
|
|
356
404
|
adjustLocalEnvPortsInContent,
|
|
357
405
|
readYamlAtPath,
|
package/lib/validate.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main Validation Command
|
|
3
|
+
*
|
|
4
|
+
* Validates applications or external integration files.
|
|
5
|
+
* Supports app name validation (including externalIntegration block) or direct file validation.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Main validation command for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const chalk = require('chalk');
|
|
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 logger = require('./utils/logger');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validates a single external file against its schema
|
|
23
|
+
*
|
|
24
|
+
* @async
|
|
25
|
+
* @function validateExternalFile
|
|
26
|
+
* @param {string} filePath - Path to the file
|
|
27
|
+
* @param {string} type - File type: 'system' | 'datasource'
|
|
28
|
+
* @returns {Promise<Object>} Validation result
|
|
29
|
+
*/
|
|
30
|
+
async function validateExternalFile(filePath, type) {
|
|
31
|
+
if (!fs.existsSync(filePath)) {
|
|
32
|
+
throw new Error(`File not found: ${filePath}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
36
|
+
let parsed;
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
parsed = JSON.parse(content);
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return {
|
|
42
|
+
valid: false,
|
|
43
|
+
errors: [`Invalid JSON syntax: ${error.message}`],
|
|
44
|
+
warnings: []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let validate;
|
|
49
|
+
if (type === 'system') {
|
|
50
|
+
validate = loadExternalSystemSchema();
|
|
51
|
+
} else if (type === 'datasource') {
|
|
52
|
+
validate = loadExternalDataSourceSchema();
|
|
53
|
+
} else {
|
|
54
|
+
throw new Error(`Unknown file type: ${type}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const valid = validate(parsed);
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
valid,
|
|
61
|
+
errors: valid ? [] : formatValidationErrors(validate.errors),
|
|
62
|
+
warnings: []
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Validates application or external integration file
|
|
68
|
+
* Detects if input is app name or file path and validates accordingly
|
|
69
|
+
*
|
|
70
|
+
* @async
|
|
71
|
+
* @function validateAppOrFile
|
|
72
|
+
* @param {string} appOrFile - Application name or file path
|
|
73
|
+
* @returns {Promise<Object>} Validation result with aggregated results
|
|
74
|
+
* @throws {Error} If validation fails
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* const result = await validateAppOrFile('myapp');
|
|
78
|
+
* // Returns: { valid: true, application: {...}, externalFiles: [...] }
|
|
79
|
+
*/
|
|
80
|
+
async function validateAppOrFile(appOrFile) {
|
|
81
|
+
if (!appOrFile || typeof appOrFile !== 'string') {
|
|
82
|
+
throw new Error('App name or file path is required');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check if it's a file path (exists and is a file)
|
|
86
|
+
const isFilePath = fs.existsSync(appOrFile) && fs.statSync(appOrFile).isFile();
|
|
87
|
+
|
|
88
|
+
if (isFilePath) {
|
|
89
|
+
// Validate single file
|
|
90
|
+
const schemaType = detectSchemaType(appOrFile);
|
|
91
|
+
let result;
|
|
92
|
+
|
|
93
|
+
if (schemaType === 'application') {
|
|
94
|
+
// For application files, we'd need to transform them first
|
|
95
|
+
// For now, just validate JSON structure
|
|
96
|
+
const content = fs.readFileSync(appOrFile, 'utf8');
|
|
97
|
+
try {
|
|
98
|
+
JSON.parse(content);
|
|
99
|
+
} catch (error) {
|
|
100
|
+
return {
|
|
101
|
+
valid: false,
|
|
102
|
+
errors: [`Invalid JSON syntax: ${error.message}`],
|
|
103
|
+
warnings: []
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
// Note: Full application validation requires variables.yaml transformation
|
|
107
|
+
// This is a simplified validation for direct JSON files
|
|
108
|
+
result = {
|
|
109
|
+
valid: true,
|
|
110
|
+
errors: [],
|
|
111
|
+
warnings: ['Application file validation is simplified. Use app name for full validation.']
|
|
112
|
+
};
|
|
113
|
+
} else if (schemaType === 'external-system') {
|
|
114
|
+
result = await validateExternalFile(appOrFile, 'system');
|
|
115
|
+
} else if (schemaType === 'external-datasource') {
|
|
116
|
+
result = await validateExternalFile(appOrFile, 'datasource');
|
|
117
|
+
} else {
|
|
118
|
+
throw new Error(`Unknown schema type: ${schemaType}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return {
|
|
122
|
+
valid: result.valid,
|
|
123
|
+
file: appOrFile,
|
|
124
|
+
type: schemaType,
|
|
125
|
+
errors: result.errors,
|
|
126
|
+
warnings: result.warnings
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Treat as app name
|
|
131
|
+
const appName = appOrFile;
|
|
132
|
+
|
|
133
|
+
// Validate application
|
|
134
|
+
const appValidation = await validator.validateApplication(appName);
|
|
135
|
+
|
|
136
|
+
// Check for externalIntegration block
|
|
137
|
+
const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
138
|
+
if (!fs.existsSync(variablesPath)) {
|
|
139
|
+
return {
|
|
140
|
+
valid: appValidation.valid,
|
|
141
|
+
application: appValidation,
|
|
142
|
+
externalFiles: []
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const yamlLib = require('js-yaml');
|
|
147
|
+
const content = fs.readFileSync(variablesPath, 'utf8');
|
|
148
|
+
let variables;
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
variables = yamlLib.load(content);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
return {
|
|
154
|
+
valid: appValidation.valid,
|
|
155
|
+
application: appValidation,
|
|
156
|
+
externalFiles: [],
|
|
157
|
+
warnings: [`Could not parse variables.yaml to check externalIntegration: ${error.message}`]
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// If no externalIntegration block, return app validation only
|
|
162
|
+
if (!variables.externalIntegration) {
|
|
163
|
+
return {
|
|
164
|
+
valid: appValidation.valid,
|
|
165
|
+
application: appValidation,
|
|
166
|
+
externalFiles: []
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Resolve and validate external files
|
|
171
|
+
const externalFiles = await resolveExternalFiles(appName);
|
|
172
|
+
const externalValidations = [];
|
|
173
|
+
|
|
174
|
+
for (const fileInfo of externalFiles) {
|
|
175
|
+
try {
|
|
176
|
+
const validation = await validateExternalFile(fileInfo.path, fileInfo.type);
|
|
177
|
+
externalValidations.push({
|
|
178
|
+
file: fileInfo.fileName,
|
|
179
|
+
path: fileInfo.path,
|
|
180
|
+
type: fileInfo.type,
|
|
181
|
+
...validation
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
externalValidations.push({
|
|
185
|
+
file: fileInfo.fileName,
|
|
186
|
+
path: fileInfo.path,
|
|
187
|
+
type: fileInfo.type,
|
|
188
|
+
valid: false,
|
|
189
|
+
errors: [error.message],
|
|
190
|
+
warnings: []
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Aggregate results
|
|
196
|
+
const allValid = appValidation.valid && externalValidations.every(v => v.valid);
|
|
197
|
+
const allErrors = [
|
|
198
|
+
...appValidation.errors,
|
|
199
|
+
...externalValidations.flatMap(v => v.errors.map(e => `${v.file}: ${e}`))
|
|
200
|
+
];
|
|
201
|
+
const allWarnings = [
|
|
202
|
+
...appValidation.warnings,
|
|
203
|
+
...externalValidations.flatMap(v => v.warnings)
|
|
204
|
+
];
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
valid: allValid,
|
|
208
|
+
application: appValidation,
|
|
209
|
+
externalFiles: externalValidations,
|
|
210
|
+
errors: allErrors,
|
|
211
|
+
warnings: allWarnings
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Displays validation results in a user-friendly format
|
|
217
|
+
*
|
|
218
|
+
* @function displayValidationResults
|
|
219
|
+
* @param {Object} result - Validation result from validateAppOrFile
|
|
220
|
+
*/
|
|
221
|
+
function displayValidationResults(result) {
|
|
222
|
+
if (result.valid) {
|
|
223
|
+
logger.log(chalk.green('\n✓ Validation passed!'));
|
|
224
|
+
} else {
|
|
225
|
+
logger.log(chalk.red('\n✗ Validation failed!'));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Display application validation
|
|
229
|
+
if (result.application) {
|
|
230
|
+
logger.log(chalk.blue('\nApplication:'));
|
|
231
|
+
if (result.application.valid) {
|
|
232
|
+
logger.log(chalk.green(' ✓ Application configuration is valid'));
|
|
233
|
+
} else {
|
|
234
|
+
logger.log(chalk.red(' ✗ Application configuration has errors:'));
|
|
235
|
+
result.application.errors.forEach(error => {
|
|
236
|
+
logger.log(chalk.red(` • ${error}`));
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
if (result.application.warnings && result.application.warnings.length > 0) {
|
|
240
|
+
result.application.warnings.forEach(warning => {
|
|
241
|
+
logger.log(chalk.yellow(` ⚠ ${warning}`));
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Display external files validation
|
|
247
|
+
if (result.externalFiles && result.externalFiles.length > 0) {
|
|
248
|
+
logger.log(chalk.blue('\nExternal Integration Files:'));
|
|
249
|
+
result.externalFiles.forEach(file => {
|
|
250
|
+
if (file.valid) {
|
|
251
|
+
logger.log(chalk.green(` ✓ ${file.file} (${file.type})`));
|
|
252
|
+
} else {
|
|
253
|
+
logger.log(chalk.red(` ✗ ${file.file} (${file.type}):`));
|
|
254
|
+
file.errors.forEach(error => {
|
|
255
|
+
logger.log(chalk.red(` • ${error}`));
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (file.warnings && file.warnings.length > 0) {
|
|
259
|
+
file.warnings.forEach(warning => {
|
|
260
|
+
logger.log(chalk.yellow(` ⚠ ${warning}`));
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Display file validation (for direct file validation)
|
|
267
|
+
if (result.file) {
|
|
268
|
+
logger.log(chalk.blue(`\nFile: ${result.file}`));
|
|
269
|
+
logger.log(chalk.blue(`Type: ${result.type}`));
|
|
270
|
+
if (result.valid) {
|
|
271
|
+
logger.log(chalk.green(' ✓ File is valid'));
|
|
272
|
+
} else {
|
|
273
|
+
logger.log(chalk.red(' ✗ File has errors:'));
|
|
274
|
+
result.errors.forEach(error => {
|
|
275
|
+
logger.log(chalk.red(` • ${error}`));
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
279
|
+
result.warnings.forEach(warning => {
|
|
280
|
+
logger.log(chalk.yellow(` ⚠ ${warning}`));
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Display aggregated warnings
|
|
286
|
+
if (result.warnings && result.warnings.length > 0) {
|
|
287
|
+
logger.log(chalk.yellow('\nWarnings:'));
|
|
288
|
+
result.warnings.forEach(warning => {
|
|
289
|
+
logger.log(chalk.yellow(` • ${warning}`));
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = {
|
|
295
|
+
validateAppOrFile,
|
|
296
|
+
displayValidationResults,
|
|
297
|
+
validateExternalFile
|
|
298
|
+
};
|
|
299
|
+
|