@aifabrix/builder 2.7.0 → 2.9.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/.cursor/rules/project-rules.mdc +680 -0
- package/integration/hubspot/README.md +136 -0
- package/integration/hubspot/env.template +9 -0
- package/integration/hubspot/hubspot-deploy-company.json +200 -0
- package/integration/hubspot/hubspot-deploy-contact.json +228 -0
- package/integration/hubspot/hubspot-deploy-deal.json +248 -0
- package/integration/hubspot/hubspot-deploy.json +91 -0
- package/integration/hubspot/variables.yaml +17 -0
- package/lib/app-config.js +13 -2
- package/lib/app-deploy.js +9 -3
- package/lib/app-dockerfile.js +14 -1
- package/lib/app-prompts.js +177 -13
- package/lib/app-push.js +16 -1
- package/lib/app-register.js +37 -5
- package/lib/app-rotate-secret.js +10 -0
- package/lib/app-run.js +19 -0
- package/lib/app.js +70 -25
- package/lib/audit-logger.js +9 -4
- package/lib/build.js +25 -13
- package/lib/cli.js +109 -2
- package/lib/commands/login.js +40 -3
- package/lib/config.js +121 -114
- package/lib/datasource-deploy.js +14 -20
- package/lib/environment-deploy.js +305 -0
- package/lib/external-system-deploy.js +345 -0
- package/lib/external-system-download.js +431 -0
- package/lib/external-system-generator.js +190 -0
- package/lib/external-system-test.js +446 -0
- package/lib/generator-builders.js +323 -0
- package/lib/generator.js +200 -292
- package/lib/schema/application-schema.json +830 -800
- package/lib/schema/external-datasource.schema.json +868 -46
- package/lib/schema/external-system.schema.json +98 -80
- package/lib/schema/infrastructure-schema.json +1 -1
- package/lib/templates.js +32 -1
- package/lib/utils/cli-utils.js +4 -4
- package/lib/utils/device-code.js +10 -2
- package/lib/utils/external-system-display.js +159 -0
- package/lib/utils/external-system-validators.js +245 -0
- package/lib/utils/paths.js +151 -1
- package/lib/utils/schema-resolver.js +7 -2
- package/lib/utils/token-encryption.js +68 -0
- package/lib/validator.js +52 -5
- package/package.json +1 -1
- package/tatus +181 -0
- package/templates/external-system/external-datasource.json.hbs +55 -0
- package/templates/external-system/external-system.json.hbs +37 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Environment Deployment Module
|
|
3
|
+
*
|
|
4
|
+
* Handles environment deployment/setup in Miso Controller.
|
|
5
|
+
* Sets up environment infrastructure before applications can be deployed.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Environment deployment 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 config = require('./config');
|
|
15
|
+
const { validateControllerUrl, validateEnvironmentKey } = require('./utils/deployment-validation');
|
|
16
|
+
const { getOrRefreshDeviceToken } = require('./utils/token-manager');
|
|
17
|
+
const { authenticatedApiCall } = require('./utils/api');
|
|
18
|
+
const { handleDeploymentErrors } = require('./utils/deployment-errors');
|
|
19
|
+
const auditLogger = require('./audit-logger');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Validates environment deployment prerequisites
|
|
23
|
+
* @param {string} envKey - Environment key
|
|
24
|
+
* @param {string} controllerUrl - Controller URL
|
|
25
|
+
* @throws {Error} If prerequisites are not met
|
|
26
|
+
*/
|
|
27
|
+
function validateEnvironmentPrerequisites(envKey, controllerUrl) {
|
|
28
|
+
if (!envKey || typeof envKey !== 'string') {
|
|
29
|
+
throw new Error('Environment key is required');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!controllerUrl || typeof controllerUrl !== 'string') {
|
|
33
|
+
throw new Error('Controller URL is required');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Validate environment key format
|
|
37
|
+
validateEnvironmentKey(envKey);
|
|
38
|
+
|
|
39
|
+
// Validate controller URL
|
|
40
|
+
validateControllerUrl(controllerUrl);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Gets authentication for environment deployment
|
|
45
|
+
* Uses device token (not app-specific client credentials)
|
|
46
|
+
* @async
|
|
47
|
+
* @param {string} controllerUrl - Controller URL
|
|
48
|
+
* @returns {Promise<Object>} Authentication configuration
|
|
49
|
+
* @throws {Error} If authentication is not available
|
|
50
|
+
*/
|
|
51
|
+
async function getEnvironmentAuth(controllerUrl) {
|
|
52
|
+
const validatedUrl = validateControllerUrl(controllerUrl);
|
|
53
|
+
|
|
54
|
+
// Get or refresh device token
|
|
55
|
+
const deviceToken = await getOrRefreshDeviceToken(validatedUrl);
|
|
56
|
+
|
|
57
|
+
if (!deviceToken || !deviceToken.token) {
|
|
58
|
+
throw new Error('Device token is required for environment deployment. Run "aifabrix login" first to authenticate.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
type: 'device',
|
|
63
|
+
token: deviceToken.token,
|
|
64
|
+
controller: deviceToken.controller || validatedUrl
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Sends environment deployment request to controller
|
|
70
|
+
* @async
|
|
71
|
+
* @param {string} controllerUrl - Controller URL
|
|
72
|
+
* @param {string} envKey - Environment key
|
|
73
|
+
* @param {Object} authConfig - Authentication configuration
|
|
74
|
+
* @param {Object} options - Deployment options
|
|
75
|
+
* @returns {Promise<Object>} Deployment result
|
|
76
|
+
* @throws {Error} If deployment fails
|
|
77
|
+
*/
|
|
78
|
+
async function sendEnvironmentDeployment(controllerUrl, envKey, authConfig, options = {}) {
|
|
79
|
+
const validatedUrl = validateControllerUrl(controllerUrl);
|
|
80
|
+
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
81
|
+
|
|
82
|
+
// Build environment deployment request
|
|
83
|
+
const deploymentRequest = {
|
|
84
|
+
key: validatedEnvKey,
|
|
85
|
+
displayName: `${validatedEnvKey.charAt(0).toUpperCase() + validatedEnvKey.slice(1)} Environment`,
|
|
86
|
+
description: `${validatedEnvKey.charAt(0).toUpperCase() + validatedEnvKey.slice(1)} environment for application deployments`
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// Add configuration if provided
|
|
90
|
+
if (options.config) {
|
|
91
|
+
// TODO: Load and parse config file if provided
|
|
92
|
+
// For now, just include the config path in description
|
|
93
|
+
deploymentRequest.description += ` (config: ${options.config})`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// API endpoint: POST /api/v1/environments/{env}/deploy
|
|
97
|
+
// Alternative: POST /api/v1/environments/deploy with environment in body
|
|
98
|
+
const endpoint = `${validatedUrl}/api/v1/environments/${validatedEnvKey}/deploy`;
|
|
99
|
+
|
|
100
|
+
// Log deployment attempt for audit
|
|
101
|
+
await auditLogger.logDeploymentAttempt(validatedEnvKey, validatedUrl, options);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const response = await authenticatedApiCall(
|
|
105
|
+
endpoint,
|
|
106
|
+
{
|
|
107
|
+
method: 'POST',
|
|
108
|
+
body: JSON.stringify(deploymentRequest)
|
|
109
|
+
},
|
|
110
|
+
authConfig.token
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (!response.success) {
|
|
114
|
+
const error = new Error(response.formattedError || response.error || 'Environment deployment failed');
|
|
115
|
+
error.status = response.status;
|
|
116
|
+
error.data = response.errorData;
|
|
117
|
+
throw error;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Handle response structure
|
|
121
|
+
const responseData = response.data || {};
|
|
122
|
+
return {
|
|
123
|
+
success: true,
|
|
124
|
+
environment: validatedEnvKey,
|
|
125
|
+
deploymentId: responseData.deploymentId || responseData.id,
|
|
126
|
+
status: responseData.status || 'initiated',
|
|
127
|
+
url: responseData.url || `${validatedUrl}/environments/${validatedEnvKey}`,
|
|
128
|
+
message: responseData.message
|
|
129
|
+
};
|
|
130
|
+
} catch (error) {
|
|
131
|
+
// Use unified error handler
|
|
132
|
+
await handleDeploymentErrors(error, validatedEnvKey, validatedUrl, false);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Polls environment deployment status
|
|
139
|
+
* @async
|
|
140
|
+
* @param {string} deploymentId - Deployment ID
|
|
141
|
+
* @param {string} controllerUrl - Controller URL
|
|
142
|
+
* @param {string} envKey - Environment key
|
|
143
|
+
* @param {Object} authConfig - Authentication configuration
|
|
144
|
+
* @param {Object} options - Polling options
|
|
145
|
+
* @returns {Promise<Object>} Final deployment status
|
|
146
|
+
*/
|
|
147
|
+
async function pollEnvironmentStatus(deploymentId, controllerUrl, envKey, authConfig, options = {}) {
|
|
148
|
+
const validatedUrl = validateControllerUrl(controllerUrl);
|
|
149
|
+
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
150
|
+
|
|
151
|
+
const pollInterval = options.pollInterval || 5000;
|
|
152
|
+
const maxAttempts = options.maxAttempts || 60;
|
|
153
|
+
const statusEndpoint = `${validatedUrl}/api/v1/environments/${validatedEnvKey}/status`;
|
|
154
|
+
|
|
155
|
+
logger.log(chalk.blue(`⏳ Polling environment status (${pollInterval}ms intervals)...`));
|
|
156
|
+
|
|
157
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
158
|
+
try {
|
|
159
|
+
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
160
|
+
|
|
161
|
+
const response = await authenticatedApiCall(
|
|
162
|
+
statusEndpoint,
|
|
163
|
+
{
|
|
164
|
+
method: 'GET'
|
|
165
|
+
},
|
|
166
|
+
authConfig.token
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (response.success && response.data) {
|
|
170
|
+
const status = response.data.status || response.data.ready;
|
|
171
|
+
const isReady = status === 'ready' || status === 'completed' || response.data.ready === true;
|
|
172
|
+
|
|
173
|
+
if (isReady) {
|
|
174
|
+
return {
|
|
175
|
+
success: true,
|
|
176
|
+
environment: validatedEnvKey,
|
|
177
|
+
status: 'ready',
|
|
178
|
+
message: 'Environment is ready for application deployments'
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check for terminal failure states
|
|
183
|
+
if (status === 'failed' || status === 'error') {
|
|
184
|
+
throw new Error(`Environment deployment failed: ${response.data.message || 'Unknown error'}`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
// If it's a terminal error (not a timeout), throw it
|
|
189
|
+
if (error.message && error.message.includes('failed')) {
|
|
190
|
+
throw error;
|
|
191
|
+
}
|
|
192
|
+
// Otherwise, continue polling
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (attempt < maxAttempts) {
|
|
196
|
+
logger.log(chalk.gray(` Attempt ${attempt}/${maxAttempts}...`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Timeout
|
|
201
|
+
throw new Error(`Environment deployment timeout after ${maxAttempts} attempts. Check controller logs for status.`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Displays environment deployment results
|
|
206
|
+
* @param {Object} result - Deployment result
|
|
207
|
+
*/
|
|
208
|
+
function displayDeploymentResults(result) {
|
|
209
|
+
logger.log(chalk.green('\n✅ Environment deployed successfully'));
|
|
210
|
+
logger.log(chalk.blue(` Environment: ${result.environment}`));
|
|
211
|
+
logger.log(chalk.blue(` Status: ${result.status === 'ready' ? '✅ ready' : result.status}`));
|
|
212
|
+
if (result.url) {
|
|
213
|
+
logger.log(chalk.blue(` URL: ${result.url}`));
|
|
214
|
+
}
|
|
215
|
+
if (result.deploymentId) {
|
|
216
|
+
logger.log(chalk.blue(` Deployment ID: ${result.deploymentId}`));
|
|
217
|
+
}
|
|
218
|
+
logger.log(chalk.green('\n✓ Environment is ready for application deployments'));
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Deploys/setups an environment in the controller
|
|
223
|
+
* @async
|
|
224
|
+
* @function deployEnvironment
|
|
225
|
+
* @param {string} envKey - Environment key (miso, dev, tst, pro)
|
|
226
|
+
* @param {Object} options - Deployment options
|
|
227
|
+
* @param {string} options.controller - Controller URL (required)
|
|
228
|
+
* @param {string} [options.config] - Environment configuration file (optional)
|
|
229
|
+
* @param {boolean} [options.skipValidation] - Skip validation checks
|
|
230
|
+
* @param {boolean} [options.poll] - Poll for deployment status (default: true)
|
|
231
|
+
* @param {boolean} [options.noPoll] - Do not poll for status
|
|
232
|
+
* @returns {Promise<Object>} Deployment result
|
|
233
|
+
* @throws {Error} If deployment fails
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* await deployEnvironment('dev', { controller: 'https://controller.aifabrix.ai' });
|
|
237
|
+
*/
|
|
238
|
+
async function deployEnvironment(envKey, options = {}) {
|
|
239
|
+
try {
|
|
240
|
+
// 1. Input validation
|
|
241
|
+
if (!envKey || typeof envKey !== 'string' || envKey.trim().length === 0) {
|
|
242
|
+
throw new Error('Environment key is required');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const controllerUrl = options.controller || options['controller-url'];
|
|
246
|
+
if (!controllerUrl) {
|
|
247
|
+
throw new Error('Controller URL is required. Use --controller flag');
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 2. Validate prerequisites
|
|
251
|
+
if (!options.skipValidation) {
|
|
252
|
+
validateEnvironmentPrerequisites(envKey, controllerUrl);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 3. Update root-level environment in config.yaml
|
|
256
|
+
await config.setCurrentEnvironment(envKey);
|
|
257
|
+
|
|
258
|
+
// 4. Get authentication (device token)
|
|
259
|
+
logger.log(chalk.blue(`\n📋 Deploying environment '${envKey}' to ${controllerUrl}...`));
|
|
260
|
+
const authConfig = await getEnvironmentAuth(controllerUrl);
|
|
261
|
+
logger.log(chalk.green('✓ Environment validated'));
|
|
262
|
+
logger.log(chalk.green('✓ Authentication successful'));
|
|
263
|
+
|
|
264
|
+
// 5. Send environment deployment request
|
|
265
|
+
logger.log(chalk.blue('\n🚀 Deploying environment infrastructure...'));
|
|
266
|
+
const validatedControllerUrl = validateControllerUrl(authConfig.controller);
|
|
267
|
+
const result = await sendEnvironmentDeployment(validatedControllerUrl, envKey, authConfig, options);
|
|
268
|
+
|
|
269
|
+
logger.log(chalk.blue(`📤 Sending deployment request to ${validatedControllerUrl}/api/v1/environments/${envKey}/deploy...`));
|
|
270
|
+
|
|
271
|
+
// 6. Poll for status if enabled
|
|
272
|
+
const shouldPoll = options.poll !== false && !options.noPoll;
|
|
273
|
+
if (shouldPoll && result.deploymentId) {
|
|
274
|
+
const pollResult = await pollEnvironmentStatus(
|
|
275
|
+
result.deploymentId,
|
|
276
|
+
validatedControllerUrl,
|
|
277
|
+
envKey,
|
|
278
|
+
authConfig,
|
|
279
|
+
{
|
|
280
|
+
pollInterval: 5000,
|
|
281
|
+
maxAttempts: 60
|
|
282
|
+
}
|
|
283
|
+
);
|
|
284
|
+
result.status = pollResult.status;
|
|
285
|
+
result.message = pollResult.message;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// 7. Display results
|
|
289
|
+
displayDeploymentResults(result);
|
|
290
|
+
|
|
291
|
+
return result;
|
|
292
|
+
} catch (error) {
|
|
293
|
+
// Error handling is done in sendEnvironmentDeployment and pollEnvironmentStatus
|
|
294
|
+
// Re-throw with context
|
|
295
|
+
if (error._logged !== true) {
|
|
296
|
+
logger.error(chalk.red(`\n❌ Environment deployment failed: ${error.message}`));
|
|
297
|
+
}
|
|
298
|
+
throw error;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
module.exports = {
|
|
303
|
+
deployEnvironment
|
|
304
|
+
};
|
|
305
|
+
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External System Deployment Module
|
|
3
|
+
*
|
|
4
|
+
* Handles deployment of external systems and datasources via pipeline API
|
|
5
|
+
* for external type applications.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview External system deployment for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs').promises;
|
|
13
|
+
const fsSync = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const yaml = require('js-yaml');
|
|
16
|
+
const chalk = require('chalk');
|
|
17
|
+
const { authenticatedApiCall } = require('./utils/api');
|
|
18
|
+
const { getDeploymentAuth } = require('./utils/token-manager');
|
|
19
|
+
const { getConfig } = require('./config');
|
|
20
|
+
const logger = require('./utils/logger');
|
|
21
|
+
const { getDataplaneUrl } = require('./datasource-deploy');
|
|
22
|
+
const { detectAppType, getDeployJsonPath } = require('./utils/paths');
|
|
23
|
+
const { generateExternalSystemApplicationSchema } = require('./generator');
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Loads variables.yaml for an application
|
|
27
|
+
* @async
|
|
28
|
+
* @function loadVariablesYaml
|
|
29
|
+
* @param {string} appName - Application name
|
|
30
|
+
* @returns {Promise<Object>} Variables configuration
|
|
31
|
+
* @throws {Error} If file cannot be loaded
|
|
32
|
+
*/
|
|
33
|
+
async function loadVariablesYaml(appName) {
|
|
34
|
+
// Detect app type and get correct path (integration or builder)
|
|
35
|
+
const { appPath } = await detectAppType(appName);
|
|
36
|
+
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
37
|
+
const content = await fs.readFile(variablesPath, 'utf8');
|
|
38
|
+
return yaml.load(content);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Validates external system files exist
|
|
43
|
+
* @async
|
|
44
|
+
* @function validateExternalSystemFiles
|
|
45
|
+
* @param {string} appName - Application name
|
|
46
|
+
* @returns {Promise<Object>} Validation result with file paths
|
|
47
|
+
* @throws {Error} If validation fails
|
|
48
|
+
*/
|
|
49
|
+
async function validateExternalSystemFiles(appName) {
|
|
50
|
+
const variables = await loadVariablesYaml(appName);
|
|
51
|
+
|
|
52
|
+
if (!variables.externalIntegration) {
|
|
53
|
+
throw new Error('externalIntegration block not found in variables.yaml');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Detect app type and get correct path (integration or builder)
|
|
57
|
+
const { appPath } = await detectAppType(appName);
|
|
58
|
+
|
|
59
|
+
// For new structure, files are in same folder (schemaBasePath is usually './')
|
|
60
|
+
// For backward compatibility, support old schemas/ subfolder
|
|
61
|
+
const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
|
|
62
|
+
const schemasPath = path.isAbsolute(schemaBasePath)
|
|
63
|
+
? schemaBasePath
|
|
64
|
+
: path.join(appPath, schemaBasePath);
|
|
65
|
+
|
|
66
|
+
// Validate system files
|
|
67
|
+
const systemFiles = [];
|
|
68
|
+
if (variables.externalIntegration.systems && variables.externalIntegration.systems.length > 0) {
|
|
69
|
+
for (const systemFile of variables.externalIntegration.systems) {
|
|
70
|
+
// Try new naming first: <app-name>-deploy.json in same folder
|
|
71
|
+
const newSystemPath = getDeployJsonPath(appName, 'external', true);
|
|
72
|
+
if (fsSync.existsSync(newSystemPath)) {
|
|
73
|
+
systemFiles.push(newSystemPath);
|
|
74
|
+
} else {
|
|
75
|
+
// Fall back to specified path
|
|
76
|
+
const systemPath = path.join(schemasPath, systemFile);
|
|
77
|
+
try {
|
|
78
|
+
await fs.access(systemPath);
|
|
79
|
+
systemFiles.push(systemPath);
|
|
80
|
+
} catch {
|
|
81
|
+
throw new Error(`External system file not found: ${systemPath} (also checked: ${newSystemPath})`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
throw new Error('No external system files specified in externalIntegration.systems');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Validate datasource files (naming: <app-name>-deploy-<datasource-key>.json)
|
|
90
|
+
const datasourceFiles = [];
|
|
91
|
+
if (variables.externalIntegration.dataSources && variables.externalIntegration.dataSources.length > 0) {
|
|
92
|
+
for (const datasourceFile of variables.externalIntegration.dataSources) {
|
|
93
|
+
// Try same folder first (new structure)
|
|
94
|
+
const datasourcePath = path.join(appPath, datasourceFile);
|
|
95
|
+
try {
|
|
96
|
+
await fs.access(datasourcePath);
|
|
97
|
+
datasourceFiles.push(datasourcePath);
|
|
98
|
+
} catch {
|
|
99
|
+
// Fall back to schemaBasePath
|
|
100
|
+
const fallbackPath = path.join(schemasPath, datasourceFile);
|
|
101
|
+
try {
|
|
102
|
+
await fs.access(fallbackPath);
|
|
103
|
+
datasourceFiles.push(fallbackPath);
|
|
104
|
+
} catch {
|
|
105
|
+
throw new Error(`External datasource file not found: ${datasourcePath} or ${fallbackPath}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Extract systemKey from system file (remove -deploy.json suffix if present)
|
|
112
|
+
const systemFileName = path.basename(systemFiles[0], '.json');
|
|
113
|
+
const systemKey = systemFileName.replace(/-deploy$/, '');
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
systemFiles,
|
|
117
|
+
datasourceFiles,
|
|
118
|
+
systemKey
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Deploys external system to dataplane (build step - deploy, not publish)
|
|
124
|
+
* @async
|
|
125
|
+
* @function buildExternalSystem
|
|
126
|
+
* @param {string} appName - Application name
|
|
127
|
+
* @param {Object} options - Deployment options
|
|
128
|
+
* @returns {Promise<void>} Resolves when deployment completes
|
|
129
|
+
* @throws {Error} If deployment fails
|
|
130
|
+
*/
|
|
131
|
+
async function buildExternalSystem(appName, options = {}) {
|
|
132
|
+
try {
|
|
133
|
+
logger.log(chalk.blue(`\n🔨 Building external system: ${appName}`));
|
|
134
|
+
|
|
135
|
+
// Validate files
|
|
136
|
+
const { systemFiles, datasourceFiles, systemKey } = await validateExternalSystemFiles(appName);
|
|
137
|
+
|
|
138
|
+
// Get authentication
|
|
139
|
+
const config = await getConfig();
|
|
140
|
+
const environment = options.environment || 'dev';
|
|
141
|
+
const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
|
|
142
|
+
const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
|
|
143
|
+
|
|
144
|
+
if (!authConfig.token && !authConfig.clientId) {
|
|
145
|
+
throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Get dataplane URL from controller
|
|
149
|
+
logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
|
|
150
|
+
const dataplaneUrl = await getDataplaneUrl(controllerUrl, appName, environment, authConfig);
|
|
151
|
+
logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
|
|
152
|
+
|
|
153
|
+
// Deploy external system
|
|
154
|
+
logger.log(chalk.blue(`Deploying external system: ${systemKey}...`));
|
|
155
|
+
const systemContent = await fs.readFile(systemFiles[0], 'utf8');
|
|
156
|
+
const systemJson = JSON.parse(systemContent);
|
|
157
|
+
|
|
158
|
+
const systemResponse = await authenticatedApiCall(
|
|
159
|
+
`${dataplaneUrl}/api/v1/pipeline/deploy`,
|
|
160
|
+
{
|
|
161
|
+
method: 'POST',
|
|
162
|
+
body: JSON.stringify(systemJson)
|
|
163
|
+
},
|
|
164
|
+
authConfig.token
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
if (!systemResponse.success) {
|
|
168
|
+
throw new Error(`Failed to deploy external system: ${systemResponse.error || systemResponse.formattedError}`);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
logger.log(chalk.green(`✓ External system deployed: ${systemKey}`));
|
|
172
|
+
|
|
173
|
+
// Deploy datasources
|
|
174
|
+
for (const datasourceFile of datasourceFiles) {
|
|
175
|
+
const datasourceName = path.basename(datasourceFile, '.json');
|
|
176
|
+
logger.log(chalk.blue(`Deploying datasource: ${datasourceName}...`));
|
|
177
|
+
|
|
178
|
+
const datasourceContent = await fs.readFile(datasourceFile, 'utf8');
|
|
179
|
+
const datasourceJson = JSON.parse(datasourceContent);
|
|
180
|
+
|
|
181
|
+
const datasourceResponse = await authenticatedApiCall(
|
|
182
|
+
`${dataplaneUrl}/api/v1/pipeline/${systemKey}/deploy`,
|
|
183
|
+
{
|
|
184
|
+
method: 'POST',
|
|
185
|
+
body: JSON.stringify(datasourceJson)
|
|
186
|
+
},
|
|
187
|
+
authConfig.token
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (!datasourceResponse.success) {
|
|
191
|
+
throw new Error(`Failed to deploy datasource ${datasourceName}: ${datasourceResponse.error || datasourceResponse.formattedError}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
logger.log(chalk.green(`✓ Datasource deployed: ${datasourceName}`));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
logger.log(chalk.green('\n✅ External system built successfully!'));
|
|
198
|
+
logger.log(chalk.blue(`System: ${systemKey}`));
|
|
199
|
+
logger.log(chalk.blue(`Datasources: ${datasourceFiles.length}`));
|
|
200
|
+
} catch (error) {
|
|
201
|
+
throw new Error(`Failed to build external system: ${error.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Publishes external system to dataplane using application-level workflow
|
|
207
|
+
* Uses upload → validate → publish workflow for atomic deployment
|
|
208
|
+
* @async
|
|
209
|
+
* @function deployExternalSystem
|
|
210
|
+
* @param {string} appName - Application name
|
|
211
|
+
* @param {Object} options - Deployment options
|
|
212
|
+
* @param {string} [options.environment] - Environment (dev, tst, pro)
|
|
213
|
+
* @param {string} [options.controller] - Controller URL
|
|
214
|
+
* @param {boolean} [options.skipValidation] - Skip validation step and go straight to publish
|
|
215
|
+
* @param {boolean} [options.generateMcpContract] - Generate MCP contract (default: true)
|
|
216
|
+
* @returns {Promise<void>} Resolves when deployment completes
|
|
217
|
+
* @throws {Error} If deployment fails
|
|
218
|
+
*/
|
|
219
|
+
async function deployExternalSystem(appName, options = {}) {
|
|
220
|
+
try {
|
|
221
|
+
logger.log(chalk.blue(`\n🚀 Publishing external system: ${appName}`));
|
|
222
|
+
|
|
223
|
+
// Validate files
|
|
224
|
+
const { systemFiles: _systemFiles, datasourceFiles, systemKey } = await validateExternalSystemFiles(appName);
|
|
225
|
+
|
|
226
|
+
// Generate application-schema.json structure
|
|
227
|
+
logger.log(chalk.blue('📋 Generating application schema...'));
|
|
228
|
+
const applicationSchema = await generateExternalSystemApplicationSchema(appName);
|
|
229
|
+
logger.log(chalk.green('✓ Application schema generated'));
|
|
230
|
+
|
|
231
|
+
// Get authentication
|
|
232
|
+
const config = await getConfig();
|
|
233
|
+
const environment = options.environment || 'dev';
|
|
234
|
+
const controllerUrl = options.controller || config.deployment?.controllerUrl || 'http://localhost:3000';
|
|
235
|
+
const authConfig = await getDeploymentAuth(controllerUrl, environment, appName);
|
|
236
|
+
|
|
237
|
+
if (!authConfig.token && !authConfig.clientId) {
|
|
238
|
+
throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register" first.');
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Get dataplane URL from controller
|
|
242
|
+
logger.log(chalk.blue('🌐 Getting dataplane URL from controller...'));
|
|
243
|
+
const dataplaneUrl = await getDataplaneUrl(controllerUrl, appName, environment, authConfig);
|
|
244
|
+
logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
|
|
245
|
+
|
|
246
|
+
// Step 1: Upload application
|
|
247
|
+
logger.log(chalk.blue('📤 Uploading application configuration...'));
|
|
248
|
+
const uploadResponse = await authenticatedApiCall(
|
|
249
|
+
`${dataplaneUrl}/api/v1/pipeline/upload`,
|
|
250
|
+
{
|
|
251
|
+
method: 'POST',
|
|
252
|
+
body: JSON.stringify(applicationSchema)
|
|
253
|
+
},
|
|
254
|
+
authConfig.token
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
if (!uploadResponse.success || !uploadResponse.data) {
|
|
258
|
+
throw new Error(`Failed to upload application: ${uploadResponse.error || uploadResponse.formattedError || 'Unknown error'}`);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const uploadData = uploadResponse.data.data || uploadResponse.data;
|
|
262
|
+
const uploadId = uploadData.uploadId || uploadData.id;
|
|
263
|
+
|
|
264
|
+
if (!uploadId) {
|
|
265
|
+
throw new Error('Upload ID not found in upload response');
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
logger.log(chalk.green(`✓ Upload successful (ID: ${uploadId})`));
|
|
269
|
+
|
|
270
|
+
// Step 2: Validate upload (optional, can be skipped)
|
|
271
|
+
if (!options.skipValidation) {
|
|
272
|
+
logger.log(chalk.blue('🔍 Validating upload...'));
|
|
273
|
+
const validateResponse = await authenticatedApiCall(
|
|
274
|
+
`${dataplaneUrl}/api/v1/pipeline/upload/${uploadId}/validate`,
|
|
275
|
+
{
|
|
276
|
+
method: 'POST'
|
|
277
|
+
},
|
|
278
|
+
authConfig.token
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
if (!validateResponse.success || !validateResponse.data) {
|
|
282
|
+
throw new Error(`Validation failed: ${validateResponse.error || validateResponse.formattedError || 'Unknown error'}`);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const validateData = validateResponse.data.data || validateResponse.data;
|
|
286
|
+
|
|
287
|
+
// Display changes
|
|
288
|
+
if (validateData.changes && validateData.changes.length > 0) {
|
|
289
|
+
logger.log(chalk.blue('\n📋 Changes to be published:'));
|
|
290
|
+
for (const change of validateData.changes) {
|
|
291
|
+
const changeType = change.type || 'unknown';
|
|
292
|
+
const changeEntity = change.entity || change.key || 'unknown';
|
|
293
|
+
const emoji = changeType === 'new' ? '➕' : changeType === 'modified' ? '✏️' : '🗑️';
|
|
294
|
+
logger.log(chalk.gray(` ${emoji} ${changeType}: ${changeEntity}`));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (validateData.summary) {
|
|
299
|
+
logger.log(chalk.blue(`\n📊 Summary: ${validateData.summary}`));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
logger.log(chalk.green('✓ Validation successful'));
|
|
303
|
+
} else {
|
|
304
|
+
logger.log(chalk.yellow('⚠ Skipping validation step'));
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Step 3: Publish application
|
|
308
|
+
const generateMcpContract = options.generateMcpContract !== false; // Default to true
|
|
309
|
+
logger.log(chalk.blue(`📢 Publishing application (MCP contract: ${generateMcpContract ? 'enabled' : 'disabled'})...`));
|
|
310
|
+
|
|
311
|
+
const publishResponse = await authenticatedApiCall(
|
|
312
|
+
`${dataplaneUrl}/api/v1/pipeline/upload/${uploadId}/publish?generateMcpContract=${generateMcpContract}`,
|
|
313
|
+
{
|
|
314
|
+
method: 'POST'
|
|
315
|
+
},
|
|
316
|
+
authConfig.token
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (!publishResponse.success || !publishResponse.data) {
|
|
320
|
+
throw new Error(`Failed to publish application: ${publishResponse.error || publishResponse.formattedError || 'Unknown error'}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const publishData = publishResponse.data.data || publishResponse.data;
|
|
324
|
+
|
|
325
|
+
logger.log(chalk.green('\n✅ External system published successfully!'));
|
|
326
|
+
logger.log(chalk.blue(`System: ${systemKey}`));
|
|
327
|
+
if (publishData.systems && publishData.systems.length > 0) {
|
|
328
|
+
logger.log(chalk.blue(`Published systems: ${publishData.systems.length}`));
|
|
329
|
+
}
|
|
330
|
+
if (publishData.dataSources && publishData.dataSources.length > 0) {
|
|
331
|
+
logger.log(chalk.blue(`Published datasources: ${publishData.dataSources.length}`));
|
|
332
|
+
} else {
|
|
333
|
+
logger.log(chalk.blue(`Datasources: ${datasourceFiles.length}`));
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
throw new Error(`Failed to deploy external system: ${error.message}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
module.exports = {
|
|
341
|
+
buildExternalSystem,
|
|
342
|
+
deployExternalSystem,
|
|
343
|
+
validateExternalSystemFiles
|
|
344
|
+
};
|
|
345
|
+
|