@aifabrix/builder 2.8.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/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 +4 -3
- package/lib/app-deploy.js +8 -20
- package/lib/app-dockerfile.js +7 -9
- package/lib/app-prompts.js +6 -5
- package/lib/app-push.js +9 -9
- package/lib/app-register.js +23 -5
- package/lib/app-rotate-secret.js +10 -0
- package/lib/app-run.js +5 -11
- package/lib/app.js +42 -14
- package/lib/build.js +20 -16
- package/lib/cli.js +61 -2
- package/lib/datasource-deploy.js +14 -20
- package/lib/external-system-deploy.js +123 -40
- package/lib/external-system-download.js +431 -0
- package/lib/external-system-generator.js +13 -10
- 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 +853 -852
- package/lib/schema/external-datasource.schema.json +823 -49
- package/lib/schema/external-system.schema.json +96 -78
- package/lib/templates.js +1 -1
- package/lib/utils/cli-utils.js +4 -4
- 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/validator.js +5 -2
- package/package.json +1 -1
package/lib/app.js
CHANGED
|
@@ -24,6 +24,7 @@ const { loadTemplateVariables, updateTemplateVariables, mergeTemplateVariables }
|
|
|
24
24
|
const logger = require('./utils/logger');
|
|
25
25
|
const auditLogger = require('./audit-logger');
|
|
26
26
|
const { downApp } = require('./app-down');
|
|
27
|
+
const { getAppPath } = require('./utils/paths');
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Displays success message after app creation
|
|
@@ -31,10 +32,15 @@ const { downApp } = require('./app-down');
|
|
|
31
32
|
* @param {Object} config - Final configuration
|
|
32
33
|
* @param {string} envConversionMessage - Environment conversion message
|
|
33
34
|
*/
|
|
34
|
-
function displaySuccessMessage(appName, config, envConversionMessage, hasAppFiles = false) {
|
|
35
|
+
function displaySuccessMessage(appName, config, envConversionMessage, hasAppFiles = false, appPath = null) {
|
|
35
36
|
logger.log(chalk.green('\n✓ Application created successfully!'));
|
|
36
37
|
logger.log(chalk.blue(`\nApplication: ${appName}`));
|
|
37
|
-
|
|
38
|
+
|
|
39
|
+
// Determine location based on app type
|
|
40
|
+
const baseDir = config.type === 'external' ? 'integration' : 'builder';
|
|
41
|
+
const location = appPath ? path.relative(process.cwd(), appPath) : `${baseDir}/${appName}/`;
|
|
42
|
+
logger.log(chalk.blue(`Location: ${location}`));
|
|
43
|
+
|
|
38
44
|
if (hasAppFiles) {
|
|
39
45
|
logger.log(chalk.blue(`Application files: apps/${appName}/`));
|
|
40
46
|
}
|
|
@@ -43,7 +49,7 @@ function displaySuccessMessage(appName, config, envConversionMessage, hasAppFile
|
|
|
43
49
|
logger.log(chalk.blue('Type: External System'));
|
|
44
50
|
logger.log(chalk.blue(`System Key: ${config.systemKey || appName}`));
|
|
45
51
|
logger.log(chalk.green('\nNext steps:'));
|
|
46
|
-
logger.log(chalk.white('1. Edit external system JSON files in
|
|
52
|
+
logger.log(chalk.white('1. Edit external system JSON files in ' + location));
|
|
47
53
|
logger.log(chalk.white('2. Run: aifabrix app register ' + appName + ' --environment dev'));
|
|
48
54
|
logger.log(chalk.white('3. Run: aifabrix build ' + appName + ' (deploys to dataplane)'));
|
|
49
55
|
logger.log(chalk.white('4. Run: aifabrix deploy ' + appName + ' (publishes to dataplane)'));
|
|
@@ -83,6 +89,16 @@ async function validateAppDirectoryNotExists(appPath, appName, baseDir = 'builde
|
|
|
83
89
|
}
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Gets the base directory path for an app based on its type
|
|
94
|
+
* @param {string} appName - Application name
|
|
95
|
+
* @param {string} appType - Application type ('external' or other)
|
|
96
|
+
* @returns {string} Base directory path ('integration' or 'builder')
|
|
97
|
+
*/
|
|
98
|
+
function getBaseDirForAppType(appType) {
|
|
99
|
+
return appType === 'external' ? 'integration' : 'builder';
|
|
100
|
+
}
|
|
101
|
+
|
|
86
102
|
/**
|
|
87
103
|
* Handles GitHub workflow generation if requested
|
|
88
104
|
* @async
|
|
@@ -124,9 +140,9 @@ async function handleGitHubWorkflows(options, config) {
|
|
|
124
140
|
* @param {string} appPath - Application directory path
|
|
125
141
|
* @throws {Error} If validation fails
|
|
126
142
|
*/
|
|
127
|
-
async function validateAppCreation(appName, options, appPath) {
|
|
143
|
+
async function validateAppCreation(appName, options, appPath, baseDir = 'builder') {
|
|
128
144
|
validateAppName(appName);
|
|
129
|
-
await validateAppDirectoryNotExists(appPath, appName,
|
|
145
|
+
await validateAppDirectoryNotExists(appPath, appName, baseDir);
|
|
130
146
|
|
|
131
147
|
if (!options.app) {
|
|
132
148
|
return;
|
|
@@ -272,10 +288,13 @@ async function createApp(appName, options = {}) {
|
|
|
272
288
|
throw new Error('Application name is required');
|
|
273
289
|
}
|
|
274
290
|
|
|
275
|
-
|
|
276
|
-
|
|
291
|
+
// Determine app type from options (will be confirmed during prompts)
|
|
292
|
+
// For now, check if type is explicitly set in options
|
|
293
|
+
const initialType = options.type || 'webapp';
|
|
294
|
+
const baseDir = getBaseDirForAppType(initialType);
|
|
295
|
+
const appPath = getAppPath(appName, initialType);
|
|
277
296
|
|
|
278
|
-
await validateAppCreation(appName, options, appPath);
|
|
297
|
+
await validateAppCreation(appName, options, appPath, baseDir);
|
|
279
298
|
|
|
280
299
|
if (options.template) {
|
|
281
300
|
await validateTemplate(options.template);
|
|
@@ -285,28 +304,37 @@ async function createApp(appName, options = {}) {
|
|
|
285
304
|
const mergedOptions = mergeTemplateVariables(options, templateVariables);
|
|
286
305
|
const config = await promptForOptions(appName, mergedOptions);
|
|
287
306
|
|
|
288
|
-
|
|
289
|
-
|
|
307
|
+
// Update appPath based on final config type (may have changed during prompts)
|
|
308
|
+
const finalBaseDir = getBaseDirForAppType(config.type);
|
|
309
|
+
const finalAppPath = getAppPath(appName, config.type);
|
|
310
|
+
|
|
311
|
+
// If path changed, validate the new path
|
|
312
|
+
if (finalAppPath !== appPath) {
|
|
313
|
+
await validateAppDirectoryNotExists(finalAppPath, appName, finalBaseDir);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await fs.mkdir(finalAppPath, { recursive: true });
|
|
317
|
+
await processTemplateFiles(options.template, finalAppPath, appName, options, config);
|
|
290
318
|
|
|
291
319
|
const existingEnv = await readExistingEnv(process.cwd());
|
|
292
320
|
const envConversionMessage = existingEnv
|
|
293
321
|
? '\n✓ Found existing .env file - sensitive values will be converted to kv:// references'
|
|
294
322
|
: '';
|
|
295
323
|
|
|
296
|
-
await generateConfigFiles(
|
|
324
|
+
await generateConfigFiles(finalAppPath, appName, config, existingEnv);
|
|
297
325
|
|
|
298
326
|
// Generate external system files if type is external
|
|
299
327
|
if (config.type === 'external') {
|
|
300
328
|
const externalGenerator = require('./external-system-generator');
|
|
301
|
-
await externalGenerator.generateExternalSystemFiles(
|
|
329
|
+
await externalGenerator.generateExternalSystemFiles(finalAppPath, appName, config);
|
|
302
330
|
}
|
|
303
331
|
|
|
304
332
|
if (options.app) {
|
|
305
|
-
await setupAppFiles(appName,
|
|
333
|
+
await setupAppFiles(appName, finalAppPath, config, options);
|
|
306
334
|
}
|
|
307
335
|
|
|
308
336
|
await handleGitHubWorkflows(options, config);
|
|
309
|
-
displaySuccessMessage(appName, config, envConversionMessage, options.app);
|
|
337
|
+
displaySuccessMessage(appName, config, envConversionMessage, options.app, finalAppPath);
|
|
310
338
|
|
|
311
339
|
// Log application creation for audit trail
|
|
312
340
|
await auditLogger.logApplicationCreation(appName, {
|
package/lib/build.js
CHANGED
|
@@ -14,6 +14,7 @@ const fs = require('fs').promises;
|
|
|
14
14
|
const fsSync = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const paths = require('./utils/paths');
|
|
17
|
+
const { detectAppType } = require('./utils/paths');
|
|
17
18
|
const { exec } = require('child_process');
|
|
18
19
|
const { promisify } = require('util');
|
|
19
20
|
const chalk = require('chalk');
|
|
@@ -68,9 +69,17 @@ async function copyTemplateFilesToDevDir(templatePath, devDir, _language) {
|
|
|
68
69
|
const sourcePath = path.join(templatePath, entry);
|
|
69
70
|
const targetPath = path.join(devDir, entry);
|
|
70
71
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
await fs.
|
|
72
|
+
// Skip if source file doesn't exist (e.g., .gitignore might not be in template)
|
|
73
|
+
try {
|
|
74
|
+
const entryStats = await fs.stat(sourcePath);
|
|
75
|
+
if (entryStats.isFile()) {
|
|
76
|
+
await fs.copyFile(sourcePath, targetPath);
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
// Skip files that don't exist (e.g., .gitignore might not be in template)
|
|
80
|
+
if (error.code !== 'ENOENT') {
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
74
83
|
}
|
|
75
84
|
}
|
|
76
85
|
}
|
|
@@ -82,7 +91,9 @@ async function copyTemplateFilesToDevDir(templatePath, devDir, _language) {
|
|
|
82
91
|
* @throws {Error} If file cannot be loaded or parsed
|
|
83
92
|
*/
|
|
84
93
|
async function loadVariablesYaml(appName) {
|
|
85
|
-
|
|
94
|
+
// Detect app type and get correct path (integration or builder)
|
|
95
|
+
const { appPath } = await detectAppType(appName);
|
|
96
|
+
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
86
97
|
|
|
87
98
|
if (!fsSync.existsSync(variablesPath)) {
|
|
88
99
|
throw new Error(`Configuration not found. Run 'aifabrix create ${appName}' first.`);
|
|
@@ -335,11 +346,12 @@ async function postBuildTasks(appName, buildConfig) {
|
|
|
335
346
|
* // Returns: 'myapp:latest'
|
|
336
347
|
*/
|
|
337
348
|
async function buildApp(appName, options = {}) {
|
|
338
|
-
// Check if app type is external -
|
|
349
|
+
// Check if app type is external - generate JSON files only (not deploy)
|
|
339
350
|
const variables = await loadVariablesYaml(appName);
|
|
340
351
|
if (variables.app && variables.app.type === 'external') {
|
|
341
|
-
const
|
|
342
|
-
await
|
|
352
|
+
const generator = require('./generator');
|
|
353
|
+
const jsonPath = await generator.generateDeployJson(appName);
|
|
354
|
+
logger.log(chalk.green(`✓ Generated deployment JSON: ${jsonPath}`));
|
|
343
355
|
return null;
|
|
344
356
|
}
|
|
345
357
|
|
|
@@ -482,12 +494,4 @@ async function buildApp(appName, options = {}) {
|
|
|
482
494
|
}
|
|
483
495
|
}
|
|
484
496
|
|
|
485
|
-
module.exports = {
|
|
486
|
-
loadVariablesYaml,
|
|
487
|
-
resolveContextPath,
|
|
488
|
-
executeDockerBuild: dockerBuild.executeDockerBuild,
|
|
489
|
-
detectLanguage,
|
|
490
|
-
generateDockerfile,
|
|
491
|
-
buildApp,
|
|
492
|
-
postBuildTasks
|
|
493
|
-
};
|
|
497
|
+
module.exports = { loadVariablesYaml, resolveContextPath, executeDockerBuild: dockerBuild.executeDockerBuild, detectLanguage, generateDockerfile, buildApp, postBuildTasks };
|
package/lib/cli.js
CHANGED
|
@@ -341,12 +341,13 @@ function setupCommands(program) {
|
|
|
341
341
|
});
|
|
342
342
|
|
|
343
343
|
program.command('json <app>')
|
|
344
|
-
.description('Generate deployment JSON')
|
|
344
|
+
.description('Generate deployment JSON (aifabrix-deploy.json for normal apps, application-schema.json for external systems)')
|
|
345
345
|
.action(async(appName) => {
|
|
346
346
|
try {
|
|
347
347
|
const result = await generator.generateDeployJsonWithValidation(appName);
|
|
348
348
|
if (result.success) {
|
|
349
|
-
|
|
349
|
+
const fileName = result.path.includes('application-schema.json') ? 'application-schema.json' : 'deployment JSON';
|
|
350
|
+
logger.log(`✓ Generated ${fileName}: ${result.path}`);
|
|
350
351
|
|
|
351
352
|
if (result.validation.warnings && result.validation.warnings.length > 0) {
|
|
352
353
|
logger.log('\n⚠️ Warnings:');
|
|
@@ -563,6 +564,64 @@ function setupCommands(program) {
|
|
|
563
564
|
process.exit(1);
|
|
564
565
|
}
|
|
565
566
|
});
|
|
567
|
+
|
|
568
|
+
// External system download command
|
|
569
|
+
program.command('download <system-key>')
|
|
570
|
+
.description('Download external system from dataplane to local development structure')
|
|
571
|
+
.option('-e, --environment <env>', 'Environment (dev, tst, pro)', 'dev')
|
|
572
|
+
.option('-c, --controller <url>', 'Controller URL')
|
|
573
|
+
.option('--dry-run', 'Show what would be downloaded without actually downloading')
|
|
574
|
+
.action(async(systemKey, options) => {
|
|
575
|
+
try {
|
|
576
|
+
const download = require('./external-system-download');
|
|
577
|
+
await download.downloadExternalSystem(systemKey, options);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
handleCommandError(error, 'download');
|
|
580
|
+
process.exit(1);
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
|
|
584
|
+
// Unit test command (local validation)
|
|
585
|
+
program.command('test <app>')
|
|
586
|
+
.description('Run unit tests for external system (local validation, no API calls)')
|
|
587
|
+
.option('-d, --datasource <key>', 'Test specific datasource only')
|
|
588
|
+
.option('-v, --verbose', 'Show detailed validation output')
|
|
589
|
+
.action(async(appName, options) => {
|
|
590
|
+
try {
|
|
591
|
+
const test = require('./external-system-test');
|
|
592
|
+
const results = await test.testExternalSystem(appName, options);
|
|
593
|
+
test.displayTestResults(results, options.verbose);
|
|
594
|
+
if (!results.valid) {
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
597
|
+
} catch (error) {
|
|
598
|
+
handleCommandError(error, 'test');
|
|
599
|
+
process.exit(1);
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
// Integration test command (via dataplane)
|
|
604
|
+
program.command('test-integration <app>')
|
|
605
|
+
.description('Run integration tests via dataplane pipeline API')
|
|
606
|
+
.option('-d, --datasource <key>', 'Test specific datasource only')
|
|
607
|
+
.option('-p, --payload <file>', 'Path to custom test payload file')
|
|
608
|
+
.option('-e, --environment <env>', 'Environment (dev, tst, pro)', 'dev')
|
|
609
|
+
.option('-c, --controller <url>', 'Controller URL')
|
|
610
|
+
.option('-v, --verbose', 'Show detailed test output')
|
|
611
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
|
|
612
|
+
.action(async(appName, options) => {
|
|
613
|
+
try {
|
|
614
|
+
const test = require('./external-system-test');
|
|
615
|
+
const results = await test.testExternalSystemIntegration(appName, options);
|
|
616
|
+
test.displayIntegrationTestResults(results, options.verbose);
|
|
617
|
+
if (!results.success) {
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
} catch (error) {
|
|
621
|
+
handleCommandError(error, 'test-integration');
|
|
622
|
+
process.exit(1);
|
|
623
|
+
}
|
|
624
|
+
});
|
|
566
625
|
}
|
|
567
626
|
|
|
568
627
|
module.exports = {
|
package/lib/datasource-deploy.js
CHANGED
|
@@ -129,39 +129,33 @@ async function deployDatasource(appKey, filePath, options) {
|
|
|
129
129
|
const dataplaneUrl = await getDataplaneUrl(options.controller, appKey, options.environment, authConfig);
|
|
130
130
|
logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
|
|
131
131
|
|
|
132
|
-
//
|
|
133
|
-
logger.log(chalk.blue('\n🚀
|
|
134
|
-
const
|
|
132
|
+
// Publish to dataplane (using publish endpoint)
|
|
133
|
+
logger.log(chalk.blue('\n🚀 Publishing datasource to dataplane...'));
|
|
134
|
+
const publishEndpoint = `${dataplaneUrl}/api/v1/pipeline/${systemKey}/publish`;
|
|
135
135
|
|
|
136
|
-
// Prepare
|
|
137
|
-
|
|
138
|
-
datasource: datasourceConfig
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
// Make API call to dataplane
|
|
142
|
-
// This is a placeholder - actual API structure may vary
|
|
143
|
-
let deployResponse;
|
|
136
|
+
// Prepare publish request - send datasource configuration directly
|
|
137
|
+
let publishResponse;
|
|
144
138
|
if (authConfig.type === 'bearer' && authConfig.token) {
|
|
145
|
-
|
|
146
|
-
|
|
139
|
+
publishResponse = await authenticatedApiCall(
|
|
140
|
+
publishEndpoint,
|
|
147
141
|
{
|
|
148
142
|
method: 'POST',
|
|
149
|
-
body: JSON.stringify(
|
|
143
|
+
body: JSON.stringify(datasourceConfig)
|
|
150
144
|
},
|
|
151
145
|
authConfig.token
|
|
152
146
|
);
|
|
153
147
|
} else {
|
|
154
|
-
throw new Error('Bearer token authentication required for dataplane
|
|
148
|
+
throw new Error('Bearer token authentication required for dataplane publish');
|
|
155
149
|
}
|
|
156
150
|
|
|
157
|
-
if (!
|
|
158
|
-
const formattedError =
|
|
159
|
-
logger.error(chalk.red('❌
|
|
151
|
+
if (!publishResponse.success) {
|
|
152
|
+
const formattedError = publishResponse.formattedError || formatApiError(publishResponse);
|
|
153
|
+
logger.error(chalk.red('❌ Publish failed:'));
|
|
160
154
|
logger.error(formattedError);
|
|
161
|
-
throw new Error(`Dataplane
|
|
155
|
+
throw new Error(`Dataplane publish failed: ${formattedError}`);
|
|
162
156
|
}
|
|
163
157
|
|
|
164
|
-
logger.log(chalk.green('\n✓ Datasource
|
|
158
|
+
logger.log(chalk.green('\n✓ Datasource published successfully!'));
|
|
165
159
|
logger.log(chalk.blue(`\nDatasource: ${datasourceConfig.key || datasourceConfig.displayName}`));
|
|
166
160
|
logger.log(chalk.blue(`System: ${systemKey}`));
|
|
167
161
|
logger.log(chalk.blue(`Environment: ${options.environment}`));
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const fs = require('fs').promises;
|
|
13
|
+
const fsSync = require('fs');
|
|
13
14
|
const path = require('path');
|
|
14
15
|
const yaml = require('js-yaml');
|
|
15
16
|
const chalk = require('chalk');
|
|
@@ -18,6 +19,8 @@ const { getDeploymentAuth } = require('./utils/token-manager');
|
|
|
18
19
|
const { getConfig } = require('./config');
|
|
19
20
|
const logger = require('./utils/logger');
|
|
20
21
|
const { getDataplaneUrl } = require('./datasource-deploy');
|
|
22
|
+
const { detectAppType, getDeployJsonPath } = require('./utils/paths');
|
|
23
|
+
const { generateExternalSystemApplicationSchema } = require('./generator');
|
|
21
24
|
|
|
22
25
|
/**
|
|
23
26
|
* Loads variables.yaml for an application
|
|
@@ -28,7 +31,9 @@ const { getDataplaneUrl } = require('./datasource-deploy');
|
|
|
28
31
|
* @throws {Error} If file cannot be loaded
|
|
29
32
|
*/
|
|
30
33
|
async function loadVariablesYaml(appName) {
|
|
31
|
-
|
|
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');
|
|
32
37
|
const content = await fs.readFile(variablesPath, 'utf8');
|
|
33
38
|
return yaml.load(content);
|
|
34
39
|
}
|
|
@@ -48,43 +53,69 @@ async function validateExternalSystemFiles(appName) {
|
|
|
48
53
|
throw new Error('externalIntegration block not found in variables.yaml');
|
|
49
54
|
}
|
|
50
55
|
|
|
51
|
-
|
|
52
|
-
const
|
|
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);
|
|
53
65
|
|
|
54
66
|
// Validate system files
|
|
55
67
|
const systemFiles = [];
|
|
56
68
|
if (variables.externalIntegration.systems && variables.externalIntegration.systems.length > 0) {
|
|
57
69
|
for (const systemFile of variables.externalIntegration.systems) {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
systemFiles.push(
|
|
62
|
-
}
|
|
63
|
-
|
|
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
|
+
}
|
|
64
83
|
}
|
|
65
84
|
}
|
|
66
85
|
} else {
|
|
67
86
|
throw new Error('No external system files specified in externalIntegration.systems');
|
|
68
87
|
}
|
|
69
88
|
|
|
70
|
-
// Validate datasource files
|
|
89
|
+
// Validate datasource files (naming: <app-name>-deploy-<datasource-key>.json)
|
|
71
90
|
const datasourceFiles = [];
|
|
72
91
|
if (variables.externalIntegration.dataSources && variables.externalIntegration.dataSources.length > 0) {
|
|
73
92
|
for (const datasourceFile of variables.externalIntegration.dataSources) {
|
|
74
|
-
|
|
93
|
+
// Try same folder first (new structure)
|
|
94
|
+
const datasourcePath = path.join(appPath, datasourceFile);
|
|
75
95
|
try {
|
|
76
96
|
await fs.access(datasourcePath);
|
|
77
97
|
datasourceFiles.push(datasourcePath);
|
|
78
98
|
} catch {
|
|
79
|
-
|
|
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
|
+
}
|
|
80
107
|
}
|
|
81
108
|
}
|
|
82
109
|
}
|
|
83
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
|
+
|
|
84
115
|
return {
|
|
85
116
|
systemFiles,
|
|
86
117
|
datasourceFiles,
|
|
87
|
-
systemKey
|
|
118
|
+
systemKey
|
|
88
119
|
};
|
|
89
120
|
}
|
|
90
121
|
|
|
@@ -172,11 +203,16 @@ async function buildExternalSystem(appName, options = {}) {
|
|
|
172
203
|
}
|
|
173
204
|
|
|
174
205
|
/**
|
|
175
|
-
* Publishes external system to dataplane
|
|
206
|
+
* Publishes external system to dataplane using application-level workflow
|
|
207
|
+
* Uses upload → validate → publish workflow for atomic deployment
|
|
176
208
|
* @async
|
|
177
209
|
* @function deployExternalSystem
|
|
178
210
|
* @param {string} appName - Application name
|
|
179
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)
|
|
180
216
|
* @returns {Promise<void>} Resolves when deployment completes
|
|
181
217
|
* @throws {Error} If deployment fails
|
|
182
218
|
*/
|
|
@@ -185,7 +221,12 @@ async function deployExternalSystem(appName, options = {}) {
|
|
|
185
221
|
logger.log(chalk.blue(`\n🚀 Publishing external system: ${appName}`));
|
|
186
222
|
|
|
187
223
|
// Validate files
|
|
188
|
-
const { systemFiles, datasourceFiles, systemKey } = await validateExternalSystemFiles(appName);
|
|
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'));
|
|
189
230
|
|
|
190
231
|
// Get authentication
|
|
191
232
|
const config = await getConfig();
|
|
@@ -202,53 +243,95 @@ async function deployExternalSystem(appName, options = {}) {
|
|
|
202
243
|
const dataplaneUrl = await getDataplaneUrl(controllerUrl, appName, environment, authConfig);
|
|
203
244
|
logger.log(chalk.green(`✓ Dataplane URL: ${dataplaneUrl}`));
|
|
204
245
|
|
|
205
|
-
//
|
|
206
|
-
logger.log(chalk.blue(
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const systemResponse = await authenticatedApiCall(
|
|
211
|
-
`${dataplaneUrl}/api/v1/pipeline/publish`,
|
|
246
|
+
// Step 1: Upload application
|
|
247
|
+
logger.log(chalk.blue('📤 Uploading application configuration...'));
|
|
248
|
+
const uploadResponse = await authenticatedApiCall(
|
|
249
|
+
`${dataplaneUrl}/api/v1/pipeline/upload`,
|
|
212
250
|
{
|
|
213
251
|
method: 'POST',
|
|
214
|
-
body: JSON.stringify(
|
|
252
|
+
body: JSON.stringify(applicationSchema)
|
|
215
253
|
},
|
|
216
254
|
authConfig.token
|
|
217
255
|
);
|
|
218
256
|
|
|
219
|
-
if (!
|
|
220
|
-
throw new Error(`Failed to
|
|
257
|
+
if (!uploadResponse.success || !uploadResponse.data) {
|
|
258
|
+
throw new Error(`Failed to upload application: ${uploadResponse.error || uploadResponse.formattedError || 'Unknown error'}`);
|
|
221
259
|
}
|
|
222
260
|
|
|
223
|
-
|
|
261
|
+
const uploadData = uploadResponse.data.data || uploadResponse.data;
|
|
262
|
+
const uploadId = uploadData.uploadId || uploadData.id;
|
|
224
263
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
logger.log(chalk.blue(`Publishing datasource: ${datasourceName}...`));
|
|
264
|
+
if (!uploadId) {
|
|
265
|
+
throw new Error('Upload ID not found in upload response');
|
|
266
|
+
}
|
|
229
267
|
|
|
230
|
-
|
|
231
|
-
const datasourceJson = JSON.parse(datasourceContent);
|
|
268
|
+
logger.log(chalk.green(`✓ Upload successful (ID: ${uploadId})`));
|
|
232
269
|
|
|
233
|
-
|
|
234
|
-
|
|
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`,
|
|
235
275
|
{
|
|
236
|
-
method: 'POST'
|
|
237
|
-
body: JSON.stringify(datasourceJson)
|
|
276
|
+
method: 'POST'
|
|
238
277
|
},
|
|
239
278
|
authConfig.token
|
|
240
279
|
);
|
|
241
280
|
|
|
242
|
-
if (!
|
|
243
|
-
throw new Error(`
|
|
281
|
+
if (!validateResponse.success || !validateResponse.data) {
|
|
282
|
+
throw new Error(`Validation failed: ${validateResponse.error || validateResponse.formattedError || 'Unknown error'}`);
|
|
244
283
|
}
|
|
245
284
|
|
|
246
|
-
|
|
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'}`);
|
|
247
321
|
}
|
|
248
322
|
|
|
323
|
+
const publishData = publishResponse.data.data || publishResponse.data;
|
|
324
|
+
|
|
249
325
|
logger.log(chalk.green('\n✅ External system published successfully!'));
|
|
250
326
|
logger.log(chalk.blue(`System: ${systemKey}`));
|
|
251
|
-
|
|
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
|
+
}
|
|
252
335
|
} catch (error) {
|
|
253
336
|
throw new Error(`Failed to deploy external system: ${error.message}`);
|
|
254
337
|
}
|