@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.
@@ -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
- const resolvedCanonical = path.isAbsolute(canonicalPath)
292
- ? canonicalPath
293
- : path.resolve(process.cwd(), canonicalPath);
294
- if (fs.existsSync(resolvedCanonical)) {
295
- const configSecrets = readYamlAtPath(resolvedCanonical);
296
- // Apply canonical secrets as a fallback source:
297
- // - Do NOT override any existing keys from user/build
298
- // - Add only missing keys from canonical path
299
- if (configSecrets && typeof configSecrets === 'object') {
300
- const result = { ...configSecrets, ...mergedSecrets };
301
- mergedSecrets = result;
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,
@@ -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
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.6.3",
3
+ "version": "2.7.0",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {