@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 CHANGED
@@ -16,6 +16,7 @@
16
16
  const { Command } = require('commander');
17
17
  const cli = require('../lib/cli');
18
18
  const { setupAppCommands } = require('../lib/commands/app');
19
+ const { setupDatasourceCommands } = require('../lib/commands/datasource');
19
20
  const logger = require('../lib/utils/logger');
20
21
  const packageJson = require('../package.json');
21
22
 
@@ -36,6 +37,9 @@ function initializeCLI() {
36
37
  // Add application management commands
37
38
  setupAppCommands(program);
38
39
 
40
+ // Add datasource management commands
41
+ setupDatasourceCommands(program);
42
+
39
43
  // Parse command line arguments
40
44
  program.parse();
41
45
  }
package/lib/cli.js CHANGED
@@ -1,3 +1,4 @@
1
+ /* eslint-disable max-lines */
1
2
  /**
2
3
  * AI Fabrix Builder CLI Command Definitions
3
4
  *
@@ -265,12 +266,26 @@ function setupCommands(program) {
265
266
 
266
267
  // Utility commands
267
268
  program.command('resolve <app>')
268
- .description('Generate .env file from template')
269
+ .description('Generate .env file from template and validate application files')
269
270
  .option('-f, --force', 'Generate missing secret keys in secrets file')
271
+ .option('--skip-validation', 'Skip file validation after generating .env')
270
272
  .action(async(appName, options) => {
271
273
  try {
272
- const envPath = await secrets.generateEnvFile(appName, undefined, 'local', options.force);
274
+ // builder/.env should use docker context (postgres:5432)
275
+ // apps/.env (if envOutputPath is set) will be generated with local context by processEnvVariables
276
+ const envPath = await secrets.generateEnvFile(appName, undefined, 'docker', options.force);
273
277
  logger.log(`✓ Generated .env file: ${envPath}`);
278
+
279
+ // Validate application files after generating .env
280
+ if (!options.skipValidation) {
281
+ const validate = require('./validate');
282
+ const result = await validate.validateAppOrFile(appName);
283
+ validate.displayValidationResults(result);
284
+ if (!result.valid) {
285
+ logger.log(chalk.yellow('\n⚠️ Validation found errors. Fix them before deploying.'));
286
+ process.exit(1);
287
+ }
288
+ }
274
289
  } catch (error) {
275
290
  handleCommandError(error, 'resolve');
276
291
  process.exit(1);
@@ -330,6 +345,40 @@ function setupCommands(program) {
330
345
  }
331
346
  });
332
347
 
348
+ // Validation command
349
+ program.command('validate <appOrFile>')
350
+ .description('Validate application or external integration file')
351
+ .action(async(appOrFile) => {
352
+ try {
353
+ const validate = require('./validate');
354
+ const result = await validate.validateAppOrFile(appOrFile);
355
+ validate.displayValidationResults(result);
356
+ if (!result.valid) {
357
+ process.exit(1);
358
+ }
359
+ } catch (error) {
360
+ handleCommandError(error, 'validate');
361
+ process.exit(1);
362
+ }
363
+ });
364
+
365
+ // Diff command
366
+ program.command('diff <file1> <file2>')
367
+ .description('Compare two configuration files (for deployment pipeline)')
368
+ .action(async(file1, file2) => {
369
+ try {
370
+ const diff = require('./diff');
371
+ const result = await diff.compareFiles(file1, file2);
372
+ diff.formatDiffOutput(result);
373
+ if (!result.identical) {
374
+ process.exit(1);
375
+ }
376
+ } catch (error) {
377
+ handleCommandError(error, 'diff');
378
+ process.exit(1);
379
+ }
380
+ });
381
+
333
382
  program.command('dockerfile <app>')
334
383
  .description('Generate Dockerfile for an application')
335
384
  .option('-l, --language <lang>', 'Override language detection')
@@ -0,0 +1,94 @@
1
+ /**
2
+ * AI Fabrix Builder - Datasource Commands
3
+ *
4
+ * Handles datasource validation, listing, comparison, and deployment
5
+ * Commands: datasource validate, datasource list, datasource diff, datasource deploy
6
+ *
7
+ * @fileoverview Datasource management commands for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const chalk = require('chalk');
13
+ const logger = require('../utils/logger');
14
+ const { validateDatasourceFile } = require('../datasource-validate');
15
+ const { listDatasources } = require('../datasource-list');
16
+ const { compareDatasources } = require('../datasource-diff');
17
+ const { deployDatasource } = require('../datasource-deploy');
18
+
19
+ /**
20
+ * Setup datasource management commands
21
+ * @param {Command} program - Commander program instance
22
+ */
23
+ function setupDatasourceCommands(program) {
24
+ const datasource = program
25
+ .command('datasource')
26
+ .description('Manage external data sources');
27
+
28
+ // Validate command
29
+ datasource
30
+ .command('validate <file>')
31
+ .description('Validate external datasource JSON file')
32
+ .action(async(file) => {
33
+ try {
34
+ const result = await validateDatasourceFile(file);
35
+ if (result.valid) {
36
+ logger.log(chalk.green(`\n✓ Datasource file is valid: ${file}`));
37
+ } else {
38
+ logger.log(chalk.red(`\n✗ Datasource file has errors: ${file}`));
39
+ result.errors.forEach(error => {
40
+ logger.log(chalk.red(` • ${error}`));
41
+ });
42
+ process.exit(1);
43
+ }
44
+ } catch (error) {
45
+ logger.error(chalk.red('❌ Validation failed:'), error.message);
46
+ process.exit(1);
47
+ }
48
+ });
49
+
50
+ // List command
51
+ datasource
52
+ .command('list')
53
+ .description('List datasources from environment')
54
+ .requiredOption('-e, --environment <env>', 'Environment ID or key')
55
+ .action(async(options) => {
56
+ try {
57
+ await listDatasources(options);
58
+ } catch (error) {
59
+ logger.error(chalk.red('❌ Failed to list datasources:'), error.message);
60
+ process.exit(1);
61
+ }
62
+ });
63
+
64
+ // Diff command
65
+ datasource
66
+ .command('diff <file1> <file2>')
67
+ .description('Compare two datasource configuration files (for dataplane)')
68
+ .action(async(file1, file2) => {
69
+ try {
70
+ await compareDatasources(file1, file2);
71
+ } catch (error) {
72
+ logger.error(chalk.red('❌ Diff failed:'), error.message);
73
+ process.exit(1);
74
+ }
75
+ });
76
+
77
+ // Deploy command
78
+ datasource
79
+ .command('deploy <myapp> <file>')
80
+ .description('Deploy datasource to dataplane')
81
+ .requiredOption('--controller <url>', 'Controller URL')
82
+ .requiredOption('-e, --environment <env>', 'Environment (miso, dev, tst, pro)')
83
+ .action(async(myapp, file, options) => {
84
+ try {
85
+ await deployDatasource(myapp, file, options);
86
+ } catch (error) {
87
+ logger.error(chalk.red('❌ Deployment failed:'), error.message);
88
+ process.exit(1);
89
+ }
90
+ });
91
+ }
92
+
93
+ module.exports = { setupDatasourceCommands };
94
+
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Datasource Deployment
3
+ *
4
+ * Deploys datasource to dataplane via controller API.
5
+ * Gets dataplane URL from controller, then deploys to dataplane.
6
+ *
7
+ * @fileoverview Datasource deployment for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const chalk = require('chalk');
14
+ const { getDeploymentAuth } = require('./utils/token-manager');
15
+ const { authenticatedApiCall } = require('./utils/api');
16
+ const { formatApiError } = require('./utils/api-error-handler');
17
+ const logger = require('./utils/logger');
18
+ const { validateDatasourceFile } = require('./datasource-validate');
19
+
20
+ /**
21
+ * Gets dataplane URL from controller by fetching application details
22
+ *
23
+ * @async
24
+ * @function getDataplaneUrl
25
+ * @param {string} controllerUrl - Controller URL
26
+ * @param {string} appKey - Application key
27
+ * @param {string} environment - Environment key
28
+ * @param {Object} authConfig - Authentication configuration
29
+ * @returns {Promise<string>} Dataplane URL
30
+ * @throws {Error} If dataplane URL cannot be retrieved
31
+ */
32
+ async function getDataplaneUrl(controllerUrl, appKey, environment, authConfig) {
33
+ // Call controller API to get application details
34
+ // Expected: GET /api/v1/environments/{env}/applications/{appKey}
35
+ const endpoint = `${controllerUrl}/api/v1/environments/${environment}/applications/${appKey}`;
36
+
37
+ let response;
38
+ if (authConfig.type === 'bearer' && authConfig.token) {
39
+ response = await authenticatedApiCall(endpoint, {}, authConfig.token);
40
+ } else {
41
+ // For credentials, we'd need to use a different API call method
42
+ // For now, use bearer token approach
43
+ throw new Error('Bearer token authentication required for getting dataplane URL');
44
+ }
45
+
46
+ if (!response.success || !response.data) {
47
+ const formattedError = response.formattedError || formatApiError(response);
48
+ throw new Error(`Failed to get application from controller: ${formattedError}`);
49
+ }
50
+
51
+ // Extract dataplane URL from application response
52
+ // This is a placeholder - actual response structure may vary
53
+ const application = response.data.data || response.data;
54
+ const dataplaneUrl = application.dataplaneUrl || application.dataplane?.url || application.configuration?.dataplaneUrl;
55
+
56
+ if (!dataplaneUrl) {
57
+ logger.error(chalk.red('❌ Dataplane URL not found in application response'));
58
+ logger.error(chalk.gray('\nApplication response:'));
59
+ logger.error(chalk.gray(JSON.stringify(application, null, 2)));
60
+ throw new Error('Dataplane URL not found in application configuration');
61
+ }
62
+
63
+ return dataplaneUrl;
64
+ }
65
+
66
+ /**
67
+ * Deploys datasource to dataplane
68
+ *
69
+ * @async
70
+ * @function deployDatasource
71
+ * @param {string} appKey - Application key
72
+ * @param {string} filePath - Path to datasource JSON file
73
+ * @param {Object} options - Deployment options
74
+ * @param {string} options.controller - Controller URL
75
+ * @param {string} options.environment - Environment key
76
+ * @returns {Promise<Object>} Deployment result
77
+ * @throws {Error} If deployment fails
78
+ */
79
+ async function deployDatasource(appKey, filePath, options) {
80
+ if (!appKey || typeof appKey !== 'string') {
81
+ throw new Error('Application key is required');
82
+ }
83
+ if (!filePath || typeof filePath !== 'string') {
84
+ throw new Error('File path is required');
85
+ }
86
+ if (!options.controller) {
87
+ throw new Error('Controller URL is required (--controller)');
88
+ }
89
+ if (!options.environment) {
90
+ throw new Error('Environment is required (-e, --environment)');
91
+ }
92
+
93
+ logger.log(chalk.blue('📋 Deploying datasource...\n'));
94
+
95
+ // Validate datasource file
96
+ logger.log(chalk.blue('🔍 Validating datasource file...'));
97
+ const validation = await validateDatasourceFile(filePath);
98
+ if (!validation.valid) {
99
+ logger.error(chalk.red('❌ Datasource validation failed:'));
100
+ validation.errors.forEach(error => {
101
+ logger.error(chalk.red(` • ${error}`));
102
+ });
103
+ throw new Error('Datasource file validation failed');
104
+ }
105
+ logger.log(chalk.green('✓ Datasource file is valid'));
106
+
107
+ // Load datasource configuration
108
+ const content = fs.readFileSync(filePath, 'utf8');
109
+ let datasourceConfig;
110
+ try {
111
+ datasourceConfig = JSON.parse(content);
112
+ } catch (error) {
113
+ throw new Error(`Failed to parse datasource file: ${error.message}`);
114
+ }
115
+
116
+ // Extract systemKey
117
+ const systemKey = datasourceConfig.systemKey;
118
+ if (!systemKey) {
119
+ throw new Error('systemKey is required in datasource configuration');
120
+ }
121
+
122
+ // Get authentication
123
+ logger.log(chalk.blue('🔐 Getting authentication...'));
124
+ const authConfig = await getDeploymentAuth(options.controller, options.environment, appKey);
125
+ logger.log(chalk.green('✓ Authentication successful'));
126
+
127
+ // Get dataplane URL from controller
128
+ logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
129
+ const dataplaneUrl = await getDataplaneUrl(options.controller, appKey, options.environment, authConfig);
130
+ logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
131
+
132
+ // Deploy to dataplane
133
+ logger.log(chalk.blue('\n🚀 Deploying to dataplane...'));
134
+ const deployEndpoint = `${dataplaneUrl}/api/v1/pipeline/${systemKey}/deploy`;
135
+
136
+ // Prepare deployment request
137
+ const deployRequest = {
138
+ datasource: datasourceConfig
139
+ };
140
+
141
+ // Make API call to dataplane
142
+ // This is a placeholder - actual API structure may vary
143
+ let deployResponse;
144
+ if (authConfig.type === 'bearer' && authConfig.token) {
145
+ deployResponse = await authenticatedApiCall(
146
+ deployEndpoint,
147
+ {
148
+ method: 'POST',
149
+ body: JSON.stringify(deployRequest)
150
+ },
151
+ authConfig.token
152
+ );
153
+ } else {
154
+ throw new Error('Bearer token authentication required for dataplane deployment');
155
+ }
156
+
157
+ if (!deployResponse.success) {
158
+ const formattedError = deployResponse.formattedError || formatApiError(deployResponse);
159
+ logger.error(chalk.red('❌ Deployment failed:'));
160
+ logger.error(formattedError);
161
+ throw new Error(`Dataplane deployment failed: ${formattedError}`);
162
+ }
163
+
164
+ logger.log(chalk.green('\n✓ Datasource deployed successfully!'));
165
+ logger.log(chalk.blue(`\nDatasource: ${datasourceConfig.key || datasourceConfig.displayName}`));
166
+ logger.log(chalk.blue(`System: ${systemKey}`));
167
+ logger.log(chalk.blue(`Environment: ${options.environment}`));
168
+
169
+ return {
170
+ success: true,
171
+ datasourceKey: datasourceConfig.key,
172
+ systemKey: systemKey,
173
+ environment: options.environment,
174
+ dataplaneUrl: dataplaneUrl
175
+ };
176
+ }
177
+
178
+ module.exports = {
179
+ deployDatasource,
180
+ getDataplaneUrl
181
+ };
182
+
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Datasource Diff Command
3
+ *
4
+ * Compares two datasource configuration files.
5
+ * Specialized for dataplane deployment validation.
6
+ *
7
+ * @fileoverview Datasource comparison for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const chalk = require('chalk');
13
+ const { compareFiles, formatDiffOutput } = require('./diff');
14
+ const logger = require('./utils/logger');
15
+
16
+ /**
17
+ * Compares two datasource files with focus on dataplane-relevant fields
18
+ *
19
+ * @async
20
+ * @function compareDatasources
21
+ * @param {string} file1 - Path to first datasource file
22
+ * @param {string} file2 - Path to second datasource file
23
+ * @returns {Promise<void>}
24
+ * @throws {Error} If comparison fails
25
+ */
26
+ async function compareDatasources(file1, file2) {
27
+ const result = await compareFiles(file1, file2);
28
+
29
+ // Filter and highlight dataplane-relevant changes
30
+ const dataplaneRelevant = {
31
+ fieldMappings: result.changed.filter(c => c.path.includes('fieldMappings')),
32
+ exposed: result.changed.filter(c => c.path.includes('exposed')),
33
+ sync: result.changed.filter(c => c.path.includes('sync')),
34
+ openapi: result.changed.filter(c => c.path.includes('openapi')),
35
+ mcp: result.changed.filter(c => c.path.includes('mcp'))
36
+ };
37
+
38
+ // Display standard diff
39
+ formatDiffOutput(result);
40
+
41
+ // Display dataplane-specific highlights
42
+ const hasDataplaneChanges = Object.values(dataplaneRelevant).some(arr => arr.length > 0);
43
+
44
+ if (hasDataplaneChanges) {
45
+ logger.log(chalk.blue('\n📊 Dataplane-Relevant Changes:'));
46
+
47
+ if (dataplaneRelevant.fieldMappings.length > 0) {
48
+ logger.log(chalk.yellow(` • Field Mappings: ${dataplaneRelevant.fieldMappings.length} changes`));
49
+ }
50
+ if (dataplaneRelevant.exposed.length > 0) {
51
+ logger.log(chalk.yellow(` • Exposed Fields: ${dataplaneRelevant.exposed.length} changes`));
52
+ }
53
+ if (dataplaneRelevant.sync.length > 0) {
54
+ logger.log(chalk.yellow(` • Sync Configuration: ${dataplaneRelevant.sync.length} changes`));
55
+ }
56
+ if (dataplaneRelevant.openapi.length > 0) {
57
+ logger.log(chalk.yellow(` • OpenAPI Configuration: ${dataplaneRelevant.openapi.length} changes`));
58
+ }
59
+ if (dataplaneRelevant.mcp.length > 0) {
60
+ logger.log(chalk.yellow(` • MCP Configuration: ${dataplaneRelevant.mcp.length} changes`));
61
+ }
62
+ }
63
+
64
+ // Exit with appropriate code
65
+ if (!result.identical) {
66
+ process.exit(1);
67
+ }
68
+ }
69
+
70
+ module.exports = {
71
+ compareDatasources
72
+ };
73
+
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Datasource List Command
3
+ *
4
+ * Lists datasources from an environment via controller API.
5
+ *
6
+ * @fileoverview Datasource listing for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const chalk = require('chalk');
12
+ const { getConfig } = require('./config');
13
+ const { getOrRefreshDeviceToken } = require('./utils/token-manager');
14
+ const { authenticatedApiCall } = require('./utils/api');
15
+ const { formatApiError } = require('./utils/api-error-handler');
16
+ const logger = require('./utils/logger');
17
+
18
+ /**
19
+ * Extracts datasources array from API response
20
+ * Handles multiple response formats similar to applications list
21
+ *
22
+ * @function extractDatasources
23
+ * @param {Object} response - API response from authenticatedApiCall
24
+ * @returns {Array} Array of datasources
25
+ * @throws {Error} If response format is invalid
26
+ */
27
+ function extractDatasources(response) {
28
+ const apiResponse = response.data;
29
+ let datasources;
30
+
31
+ // Check if apiResponse.data is an array (wrapped format)
32
+ if (apiResponse && apiResponse.data && Array.isArray(apiResponse.data)) {
33
+ datasources = apiResponse.data;
34
+ } else if (Array.isArray(apiResponse)) {
35
+ // Check if apiResponse is directly an array
36
+ datasources = apiResponse;
37
+ } else if (apiResponse && Array.isArray(apiResponse.items)) {
38
+ // Check if apiResponse.items is an array (paginated format)
39
+ datasources = apiResponse.items;
40
+ } else if (apiResponse && apiResponse.data && apiResponse.data.items && Array.isArray(apiResponse.data.items)) {
41
+ // Check if apiResponse.data.items is an array (wrapped paginated format)
42
+ datasources = apiResponse.data.items;
43
+ } else {
44
+ logger.error(chalk.red('❌ Invalid response: expected data array or items array'));
45
+ logger.error(chalk.gray('\nAPI response type:'), typeof apiResponse);
46
+ logger.error(chalk.gray('API response:'), JSON.stringify(apiResponse, null, 2));
47
+ throw new Error('Invalid API response format: expected array of datasources');
48
+ }
49
+
50
+ return datasources;
51
+ }
52
+
53
+ /**
54
+ * Displays datasources in a formatted table
55
+ *
56
+ * @function displayDatasources
57
+ * @param {Array} datasources - Array of datasource objects
58
+ * @param {string} environment - Environment key
59
+ */
60
+ function displayDatasources(datasources, environment) {
61
+ if (datasources.length === 0) {
62
+ logger.log(chalk.yellow(`\nNo datasources found in environment: ${environment}`));
63
+ return;
64
+ }
65
+
66
+ logger.log(chalk.blue(`\n📋 Datasources in environment: ${environment}\n`));
67
+ logger.log(chalk.gray('Key'.padEnd(30) + 'Display Name'.padEnd(30) + 'System Key'.padEnd(20) + 'Version'.padEnd(15) + 'Status'));
68
+ logger.log(chalk.gray('-'.repeat(120)));
69
+
70
+ datasources.forEach((ds) => {
71
+ const key = (ds.key || 'N/A').padEnd(30);
72
+ const displayName = (ds.displayName || 'N/A').padEnd(30);
73
+ const systemKey = (ds.systemKey || 'N/A').padEnd(20);
74
+ const version = (ds.version || 'N/A').padEnd(15);
75
+ const status = ds.enabled !== false ? chalk.green('enabled') : chalk.red('disabled');
76
+ logger.log(`${key}${displayName}${systemKey}${version}${status}`);
77
+ });
78
+ logger.log('');
79
+ }
80
+
81
+ /**
82
+ * Lists datasources from an environment
83
+ *
84
+ * @async
85
+ * @function listDatasources
86
+ * @param {Object} options - Command options
87
+ * @param {string} options.environment - Environment ID or key
88
+ * @throws {Error} If listing fails
89
+ */
90
+ async function listDatasources(options) {
91
+ const config = await getConfig();
92
+
93
+ // Try to get device token
94
+ let controllerUrl = null;
95
+ let token = null;
96
+
97
+ if (config.device) {
98
+ const deviceUrls = Object.keys(config.device);
99
+ if (deviceUrls.length > 0) {
100
+ controllerUrl = deviceUrls[0];
101
+ const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
102
+ if (deviceToken && deviceToken.token) {
103
+ token = deviceToken.token;
104
+ controllerUrl = deviceToken.controller;
105
+ }
106
+ }
107
+ }
108
+
109
+ if (!token || !controllerUrl) {
110
+ logger.error(chalk.red('❌ Not logged in. Run: aifabrix login'));
111
+ logger.error(chalk.gray(' Use device code flow: aifabrix login --method device --controller <url>'));
112
+ process.exit(1);
113
+ }
114
+
115
+ // Call controller API - using placeholder endpoint until full specs available
116
+ // Expected: GET /api/v1/environments/{env}/datasources
117
+ const endpoint = `${controllerUrl}/api/v1/environments/${options.environment}/datasources`;
118
+ const response = await authenticatedApiCall(endpoint, {}, token);
119
+
120
+ if (!response.success || !response.data) {
121
+ const formattedError = response.formattedError || formatApiError(response);
122
+ logger.error(formattedError);
123
+ logger.error(chalk.gray('\nFull response for debugging:'));
124
+ logger.error(chalk.gray(JSON.stringify(response, null, 2)));
125
+ process.exit(1);
126
+ return; // Ensure we don't continue after exit
127
+ }
128
+
129
+ const datasources = extractDatasources(response);
130
+ displayDatasources(datasources, options.environment);
131
+ }
132
+
133
+ module.exports = {
134
+ listDatasources,
135
+ displayDatasources,
136
+ extractDatasources
137
+ };
138
+
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Datasource Validation
3
+ *
4
+ * Validates external datasource JSON files against schema.
5
+ *
6
+ * @fileoverview Datasource validation for AI Fabrix Builder
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const { loadExternalDataSourceSchema } = require('./utils/schema-loader');
13
+ const { formatValidationErrors } = require('./utils/error-formatter');
14
+
15
+ /**
16
+ * Validates a datasource file against external-datasource schema
17
+ *
18
+ * @async
19
+ * @function validateDatasourceFile
20
+ * @param {string} filePath - Path to the datasource JSON file
21
+ * @returns {Promise<Object>} Validation result with errors and warnings
22
+ * @throws {Error} If file cannot be read or parsed
23
+ *
24
+ * @example
25
+ * const result = await validateDatasourceFile('./hubspot-deal.json');
26
+ * // Returns: { valid: true, errors: [], warnings: [] }
27
+ */
28
+ async function validateDatasourceFile(filePath) {
29
+ if (!filePath || typeof filePath !== 'string') {
30
+ throw new Error('File path is required and must be a string');
31
+ }
32
+
33
+ if (!fs.existsSync(filePath)) {
34
+ throw new Error(`File not found: ${filePath}`);
35
+ }
36
+
37
+ const content = fs.readFileSync(filePath, 'utf8');
38
+ let parsed;
39
+
40
+ try {
41
+ parsed = JSON.parse(content);
42
+ } catch (error) {
43
+ return {
44
+ valid: false,
45
+ errors: [`Invalid JSON syntax: ${error.message}`],
46
+ warnings: []
47
+ };
48
+ }
49
+
50
+ const validate = loadExternalDataSourceSchema();
51
+ const valid = validate(parsed);
52
+
53
+ return {
54
+ valid,
55
+ errors: valid ? [] : formatValidationErrors(validate.errors),
56
+ warnings: []
57
+ };
58
+ }
59
+
60
+ module.exports = {
61
+ validateDatasourceFile
62
+ };
63
+