@aifabrix/builder 2.8.0 → 2.10.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.
Files changed (46) hide show
  1. package/integration/hubspot/README.md +136 -0
  2. package/integration/hubspot/env.template +9 -0
  3. package/integration/hubspot/hubspot-deploy-company.json +200 -0
  4. package/integration/hubspot/hubspot-deploy-contact.json +228 -0
  5. package/integration/hubspot/hubspot-deploy-deal.json +248 -0
  6. package/integration/hubspot/hubspot-deploy.json +91 -0
  7. package/integration/hubspot/variables.yaml +17 -0
  8. package/lib/app-config.js +4 -3
  9. package/lib/app-deploy.js +8 -20
  10. package/lib/app-dockerfile.js +7 -9
  11. package/lib/app-prompts.js +6 -5
  12. package/lib/app-push.js +9 -9
  13. package/lib/app-register.js +23 -5
  14. package/lib/app-rotate-secret.js +10 -0
  15. package/lib/app-run.js +5 -11
  16. package/lib/app.js +42 -14
  17. package/lib/build.js +20 -16
  18. package/lib/cli.js +61 -2
  19. package/lib/commands/login.js +7 -1
  20. package/lib/datasource-deploy.js +14 -20
  21. package/lib/external-system-deploy.js +123 -40
  22. package/lib/external-system-download.js +431 -0
  23. package/lib/external-system-generator.js +13 -10
  24. package/lib/external-system-test.js +446 -0
  25. package/lib/generator-builders.js +323 -0
  26. package/lib/generator.js +200 -292
  27. package/lib/schema/application-schema.json +853 -852
  28. package/lib/schema/env-config.yaml +9 -1
  29. package/lib/schema/external-datasource.schema.json +823 -49
  30. package/lib/schema/external-system.schema.json +96 -78
  31. package/lib/templates.js +36 -5
  32. package/lib/utils/api-error-handler.js +12 -12
  33. package/lib/utils/cli-utils.js +4 -4
  34. package/lib/utils/device-code.js +65 -2
  35. package/lib/utils/env-template.js +5 -4
  36. package/lib/utils/external-system-display.js +159 -0
  37. package/lib/utils/external-system-validators.js +245 -0
  38. package/lib/utils/paths.js +151 -1
  39. package/lib/utils/schema-resolver.js +7 -2
  40. package/lib/validator.js +5 -2
  41. package/package.json +1 -1
  42. package/templates/applications/keycloak/env.template +8 -2
  43. package/templates/applications/keycloak/variables.yaml +3 -3
  44. package/templates/applications/miso-controller/env.template +23 -10
  45. package/templates/applications/miso-controller/rbac.yaml +263 -213
  46. package/templates/applications/miso-controller/variables.yaml +3 -3
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
- logger.log(chalk.blue(`Location: builder/${appName}/`));
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 builder/' + appName + '/schemas/'));
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, 'builder');
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
- const builderPath = path.join(process.cwd(), 'builder');
276
- const appPath = path.join(builderPath, appName);
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
- await fs.mkdir(appPath, { recursive: true });
289
- await processTemplateFiles(options.template, appPath, appName, options, config);
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(appPath, appName, config, existingEnv);
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(appPath, appName, config);
329
+ await externalGenerator.generateExternalSystemFiles(finalAppPath, appName, config);
302
330
  }
303
331
 
304
332
  if (options.app) {
305
- await setupAppFiles(appName, appPath, config, options);
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
- const entryStats = await fs.stat(sourcePath);
72
- if (entryStats.isFile()) {
73
- await fs.copyFile(sourcePath, targetPath);
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
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
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 - deploy to dataplane instead of Docker build
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 externalDeploy = require('./external-system-deploy');
342
- await externalDeploy.buildExternalSystem(appName, options);
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
- logger.log(`✓ Generated deployment JSON: ${result.path}`);
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 = {
@@ -359,7 +359,13 @@ async function handleDeviceCodeLogin(controllerUrl, environment, offline, scope)
359
359
  );
360
360
 
361
361
  } catch (deviceError) {
362
- logger.error(chalk.red(`\n❌ Device code flow failed: ${deviceError.message}`));
362
+ // Display formatted error if available (includes detailed validation info)
363
+ if (deviceError.formattedError) {
364
+ logger.error(chalk.red('\n❌ Device code flow failed:'));
365
+ logger.log(deviceError.formattedError);
366
+ } else {
367
+ logger.error(chalk.red(`\n❌ Device code flow failed: ${deviceError.message}`));
368
+ }
363
369
  process.exit(1);
364
370
  }
365
371
  }
@@ -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
- // Deploy to dataplane
133
- logger.log(chalk.blue('\n🚀 Deploying to dataplane...'));
134
- const deployEndpoint = `${dataplaneUrl}/api/v1/pipeline/${systemKey}/deploy`;
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 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;
136
+ // Prepare publish request - send datasource configuration directly
137
+ let publishResponse;
144
138
  if (authConfig.type === 'bearer' && authConfig.token) {
145
- deployResponse = await authenticatedApiCall(
146
- deployEndpoint,
139
+ publishResponse = await authenticatedApiCall(
140
+ publishEndpoint,
147
141
  {
148
142
  method: 'POST',
149
- body: JSON.stringify(deployRequest)
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 deployment');
148
+ throw new Error('Bearer token authentication required for dataplane publish');
155
149
  }
156
150
 
157
- if (!deployResponse.success) {
158
- const formattedError = deployResponse.formattedError || formatApiError(deployResponse);
159
- logger.error(chalk.red('❌ Deployment failed:'));
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 deployment failed: ${formattedError}`);
155
+ throw new Error(`Dataplane publish failed: ${formattedError}`);
162
156
  }
163
157
 
164
- logger.log(chalk.green('\n✓ Datasource deployed successfully!'));
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
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
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
- const appPath = path.join(process.cwd(), 'builder', appName);
52
- const schemasPath = path.join(appPath, variables.externalIntegration.schemaBasePath || './schemas');
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
- const systemPath = path.join(schemasPath, systemFile);
59
- try {
60
- await fs.access(systemPath);
61
- systemFiles.push(systemPath);
62
- } catch {
63
- throw new Error(`External system file not found: ${systemPath}`);
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
- const datasourcePath = path.join(schemasPath, datasourceFile);
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
- throw new Error(`External datasource file not found: ${datasourcePath}`);
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: path.basename(systemFiles[0], '.json')
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 (deploy step - publish)
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
- // Publish external system
206
- logger.log(chalk.blue(`Publishing external system: ${systemKey}...`));
207
- const systemContent = await fs.readFile(systemFiles[0], 'utf8');
208
- const systemJson = JSON.parse(systemContent);
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(systemJson)
252
+ body: JSON.stringify(applicationSchema)
215
253
  },
216
254
  authConfig.token
217
255
  );
218
256
 
219
- if (!systemResponse.success) {
220
- throw new Error(`Failed to publish external system: ${systemResponse.error || systemResponse.formattedError}`);
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
- logger.log(chalk.green(`✓ External system published: ${systemKey}`));
261
+ const uploadData = uploadResponse.data.data || uploadResponse.data;
262
+ const uploadId = uploadData.uploadId || uploadData.id;
224
263
 
225
- // Publish datasources
226
- for (const datasourceFile of datasourceFiles) {
227
- const datasourceName = path.basename(datasourceFile, '.json');
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
- const datasourceContent = await fs.readFile(datasourceFile, 'utf8');
231
- const datasourceJson = JSON.parse(datasourceContent);
268
+ logger.log(chalk.green(`✓ Upload successful (ID: ${uploadId})`));
232
269
 
233
- const datasourceResponse = await authenticatedApiCall(
234
- `${dataplaneUrl}/api/v1/pipeline/${systemKey}/publish`,
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 (!datasourceResponse.success) {
243
- throw new Error(`Failed to publish datasource ${datasourceName}: ${datasourceResponse.error || datasourceResponse.formattedError}`);
281
+ if (!validateResponse.success || !validateResponse.data) {
282
+ throw new Error(`Validation failed: ${validateResponse.error || validateResponse.formattedError || 'Unknown error'}`);
244
283
  }
245
284
 
246
- logger.log(chalk.green(`✓ Datasource published: ${datasourceName}`));
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
- logger.log(chalk.blue(`Datasources: ${datasourceFiles.length}`));
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
  }