@aifabrix/builder 2.6.2 → 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/config.js +25 -36
- 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/api.js +19 -3
- 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/utils/token-manager.js +57 -21
- package/lib/validate.js +299 -0
- package/package.json +1 -1
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
|
-
|
|
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
|
+
|
package/lib/config.js
CHANGED
|
@@ -226,13 +226,23 @@ async function setCurrentEnvironment(environment) {
|
|
|
226
226
|
* @returns {boolean} True if token is expired
|
|
227
227
|
*/
|
|
228
228
|
function isTokenExpired(expiresAt) {
|
|
229
|
-
if (!expiresAt)
|
|
230
|
-
return true;
|
|
231
|
-
}
|
|
229
|
+
if (!expiresAt) return true;
|
|
232
230
|
const expirationTime = new Date(expiresAt).getTime();
|
|
233
231
|
const now = Date.now();
|
|
234
|
-
|
|
235
|
-
|
|
232
|
+
return now >= (expirationTime - 5 * 60 * 1000); // 5 minute buffer
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check if token should be refreshed proactively (within 15 minutes of expiry)
|
|
237
|
+
* Helps keep Keycloak sessions alive by refreshing before SSO Session Idle timeout (30 minutes)
|
|
238
|
+
* @param {string} expiresAt - ISO timestamp string
|
|
239
|
+
* @returns {boolean} True if token should be refreshed proactively
|
|
240
|
+
*/
|
|
241
|
+
function shouldRefreshToken(expiresAt) {
|
|
242
|
+
if (!expiresAt) return true;
|
|
243
|
+
const expirationTime = new Date(expiresAt).getTime();
|
|
244
|
+
const now = Date.now();
|
|
245
|
+
return now >= (expirationTime - 15 * 60 * 1000); // 15 minutes buffer
|
|
236
246
|
}
|
|
237
247
|
|
|
238
248
|
/**
|
|
@@ -242,9 +252,7 @@ function isTokenExpired(expiresAt) {
|
|
|
242
252
|
*/
|
|
243
253
|
async function getDeviceToken(controllerUrl) {
|
|
244
254
|
const config = await getConfig();
|
|
245
|
-
if (!config.device || !config.device[controllerUrl])
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
255
|
+
if (!config.device || !config.device[controllerUrl]) return null;
|
|
248
256
|
const deviceToken = config.device[controllerUrl];
|
|
249
257
|
return {
|
|
250
258
|
controller: controllerUrl,
|
|
@@ -262,12 +270,8 @@ async function getDeviceToken(controllerUrl) {
|
|
|
262
270
|
*/
|
|
263
271
|
async function getClientToken(environment, appName) {
|
|
264
272
|
const config = await getConfig();
|
|
265
|
-
if (!config.environments || !config.environments[environment])
|
|
266
|
-
|
|
267
|
-
}
|
|
268
|
-
if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) {
|
|
269
|
-
return null;
|
|
270
|
-
}
|
|
273
|
+
if (!config.environments || !config.environments[environment]) return null;
|
|
274
|
+
if (!config.environments[environment].clients || !config.environments[environment].clients[appName]) return null;
|
|
271
275
|
return config.environments[environment].clients[appName];
|
|
272
276
|
}
|
|
273
277
|
|
|
@@ -281,14 +285,8 @@ async function getClientToken(environment, appName) {
|
|
|
281
285
|
*/
|
|
282
286
|
async function saveDeviceToken(controllerUrl, token, refreshToken, expiresAt) {
|
|
283
287
|
const config = await getConfig();
|
|
284
|
-
if (!config.device) {
|
|
285
|
-
|
|
286
|
-
}
|
|
287
|
-
config.device[controllerUrl] = {
|
|
288
|
-
token: token,
|
|
289
|
-
refreshToken: refreshToken,
|
|
290
|
-
expiresAt: expiresAt
|
|
291
|
-
};
|
|
288
|
+
if (!config.device) config.device = {};
|
|
289
|
+
config.device[controllerUrl] = { token, refreshToken, expiresAt };
|
|
292
290
|
await saveConfig(config);
|
|
293
291
|
}
|
|
294
292
|
|
|
@@ -303,20 +301,10 @@ async function saveDeviceToken(controllerUrl, token, refreshToken, expiresAt) {
|
|
|
303
301
|
*/
|
|
304
302
|
async function saveClientToken(environment, appName, controllerUrl, token, expiresAt) {
|
|
305
303
|
const config = await getConfig();
|
|
306
|
-
if (!config.environments) {
|
|
307
|
-
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
config.environments[environment] = { clients: {} };
|
|
311
|
-
}
|
|
312
|
-
if (!config.environments[environment].clients) {
|
|
313
|
-
config.environments[environment].clients = {};
|
|
314
|
-
}
|
|
315
|
-
config.environments[environment].clients[appName] = {
|
|
316
|
-
controller: controllerUrl,
|
|
317
|
-
token: token,
|
|
318
|
-
expiresAt: expiresAt
|
|
319
|
-
};
|
|
304
|
+
if (!config.environments) config.environments = {};
|
|
305
|
+
if (!config.environments[environment]) config.environments[environment] = { clients: {} };
|
|
306
|
+
if (!config.environments[environment].clients) config.environments[environment].clients = {};
|
|
307
|
+
config.environments[environment].clients[appName] = { controller: controllerUrl, token, expiresAt };
|
|
320
308
|
await saveConfig(config);
|
|
321
309
|
}
|
|
322
310
|
|
|
@@ -468,6 +456,7 @@ const exportsObj = {
|
|
|
468
456
|
getCurrentEnvironment,
|
|
469
457
|
setCurrentEnvironment,
|
|
470
458
|
isTokenExpired,
|
|
459
|
+
shouldRefreshToken,
|
|
471
460
|
getDeviceToken,
|
|
472
461
|
getClientToken,
|
|
473
462
|
saveDeviceToken,
|
|
@@ -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
|
+
|