@aifabrix/builder 2.41.0 → 2.42.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 (138) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +1 -1
  3. package/integration/hubspot/README.md +8 -4
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/jest.config.manual.js +2 -1
  16. package/lib/api/external-test.api.js +111 -0
  17. package/lib/api/index.js +42 -19
  18. package/lib/api/pipeline.api.js +66 -120
  19. package/lib/api/types/pipeline.types.js +37 -0
  20. package/lib/api/wizard-platform.api.js +61 -0
  21. package/lib/api/wizard.api.js +34 -1
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +3 -1
  24. package/lib/app/prompts.js +44 -29
  25. package/lib/app/readme.js +8 -3
  26. package/lib/app/run-env-compose.js +64 -1
  27. package/lib/app/run-helpers.js +1 -1
  28. package/lib/app/show-display.js +1 -1
  29. package/lib/cli/setup-app.js +42 -11
  30. package/lib/cli/setup-credential-deployment.js +31 -6
  31. package/lib/cli/setup-dev.js +27 -0
  32. package/lib/cli/setup-environment.js +12 -4
  33. package/lib/cli/setup-external-system.js +19 -4
  34. package/lib/cli/setup-infra.js +54 -14
  35. package/lib/cli/setup-utility.js +117 -21
  36. package/lib/commands/credential-env.js +162 -0
  37. package/lib/commands/credential-list.js +17 -22
  38. package/lib/commands/credential-push.js +96 -0
  39. package/lib/commands/datasource.js +77 -6
  40. package/lib/commands/dev-init.js +39 -1
  41. package/lib/commands/repair-auth-config.js +99 -0
  42. package/lib/commands/repair-datasource-keys.js +208 -0
  43. package/lib/commands/repair-datasource.js +235 -0
  44. package/lib/commands/repair-env-template.js +348 -0
  45. package/lib/commands/repair-internal.js +85 -0
  46. package/lib/commands/repair-rbac.js +158 -0
  47. package/lib/commands/repair.js +507 -0
  48. package/lib/commands/test-e2e-external.js +165 -0
  49. package/lib/commands/upload.js +71 -40
  50. package/lib/commands/wizard-core-helpers.js +226 -4
  51. package/lib/commands/wizard-core.js +67 -29
  52. package/lib/commands/wizard-dataplane.js +1 -1
  53. package/lib/commands/wizard-entity-selection.js +43 -0
  54. package/lib/commands/wizard-headless.js +44 -5
  55. package/lib/commands/wizard-helpers.js +7 -3
  56. package/lib/commands/wizard.js +86 -64
  57. package/lib/core/config.js +7 -1
  58. package/lib/core/secrets.js +33 -12
  59. package/lib/datasource/deploy.js +12 -3
  60. package/lib/datasource/test-e2e.js +219 -0
  61. package/lib/datasource/test-integration.js +154 -0
  62. package/lib/deployment/deployer.js +7 -5
  63. package/lib/external-system/download.js +182 -204
  64. package/lib/external-system/generator.js +204 -56
  65. package/lib/external-system/test-execution.js +2 -1
  66. package/lib/external-system/test-system-level.js +73 -0
  67. package/lib/external-system/test.js +51 -18
  68. package/lib/generator/external-controller-manifest.js +29 -2
  69. package/lib/generator/external-schema-utils.js +1 -1
  70. package/lib/generator/external.js +10 -3
  71. package/lib/generator/index.js +4 -1
  72. package/lib/generator/split-readme.js +1 -0
  73. package/lib/generator/split-variables.js +7 -1
  74. package/lib/generator/split.js +194 -54
  75. package/lib/generator/wizard-prompts-secondary.js +294 -0
  76. package/lib/generator/wizard-prompts.js +105 -106
  77. package/lib/generator/wizard-readme.js +88 -0
  78. package/lib/generator/wizard.js +147 -158
  79. package/lib/infrastructure/compose.js +11 -1
  80. package/lib/infrastructure/index.js +11 -3
  81. package/lib/infrastructure/services.js +22 -11
  82. package/lib/schema/application-schema.json +8 -5
  83. package/lib/schema/external-datasource.schema.json +49 -26
  84. package/lib/schema/external-system.schema.json +82 -6
  85. package/lib/schema/wizard-config.schema.json +16 -0
  86. package/lib/utils/api.js +38 -10
  87. package/lib/utils/auth-headers.js +8 -7
  88. package/lib/utils/compose-generator.js +1 -1
  89. package/lib/utils/compose-handlebars-helpers.js +11 -0
  90. package/lib/utils/config-format-preference.js +51 -0
  91. package/lib/utils/config-format.js +36 -0
  92. package/lib/utils/configuration-env-resolver.js +179 -0
  93. package/lib/utils/credential-display.js +83 -0
  94. package/lib/utils/credential-secrets-env.js +115 -25
  95. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  96. package/lib/utils/deployment-validation-helpers.js +4 -4
  97. package/lib/utils/dev-ca-install.js +139 -0
  98. package/lib/utils/env-copy.js +23 -3
  99. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  100. package/lib/utils/error-formatters/permission-errors.js +0 -1
  101. package/lib/utils/error-formatters/validation-errors.js +0 -1
  102. package/lib/utils/external-readme.js +56 -29
  103. package/lib/utils/external-system-display.js +59 -1
  104. package/lib/utils/external-system-test-helpers.js +21 -8
  105. package/lib/utils/external-system-validators.js +3 -0
  106. package/lib/utils/file-upload.js +20 -50
  107. package/lib/utils/help-builder.js +1 -0
  108. package/lib/utils/infra-status.js +50 -44
  109. package/lib/utils/local-secrets.js +5 -5
  110. package/lib/utils/paths.js +85 -4
  111. package/lib/utils/secrets-canonical.js +93 -0
  112. package/lib/utils/secrets-generator.js +20 -0
  113. package/lib/utils/secrets-helpers.js +75 -89
  114. package/lib/utils/test-log-writer.js +56 -0
  115. package/lib/utils/token-manager.js +24 -32
  116. package/lib/validation/env-template-auth.js +157 -0
  117. package/lib/validation/env-template-kv.js +41 -0
  118. package/lib/validation/external-manifest-validator.js +25 -0
  119. package/lib/validation/external-system-auth-rules.js +86 -0
  120. package/lib/validation/validate-batch.js +149 -0
  121. package/lib/validation/validate-datasource-keys-api.js +33 -0
  122. package/lib/validation/validate-display.js +94 -16
  123. package/lib/validation/validate.js +25 -12
  124. package/lib/validation/validator.js +7 -9
  125. package/lib/validation/wizard-datasource-validation.js +50 -0
  126. package/package.json +7 -2
  127. package/templates/applications/dataplane/application.yaml +1 -1
  128. package/templates/applications/dataplane/env.template +5 -5
  129. package/templates/applications/dataplane/rbac.yaml +2 -2
  130. package/templates/applications/miso-controller/env.template +1 -1
  131. package/templates/external-system/README.md.hbs +65 -25
  132. package/templates/external-system/deploy.js.hbs +4 -2
  133. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  134. package/templates/external-system/external-system.json.hbs +1 -18
  135. package/templates/infra/compose.yaml.hbs +6 -0
  136. package/templates/python/docker-compose.hbs +4 -4
  137. package/templates/typescript/docker-compose.hbs +4 -4
  138. package/integration/hubspot/application.yaml +0 -37
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Run E2E tests for all datasources of an external system.
3
+ *
4
+ * @fileoverview test-e2e <external system> – run E2E for every datasource
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const chalk = require('chalk');
14
+ const logger = require('../utils/logger');
15
+ const { getIntegrationPath } = require('../utils/paths');
16
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
17
+ const { loadConfigFile } = require('../utils/config-format');
18
+ const { discoverIntegrationFiles, buildEffectiveDatasourceFiles } = require('./repair-internal');
19
+ const { runDatasourceTestE2E } = require('../datasource/test-e2e');
20
+
21
+ /**
22
+ * Derives datasource key from filename when file has no key (same logic as repair).
23
+ * @param {string} fileName - Datasource file name
24
+ * @param {string} systemKey - System key
25
+ * @returns {string}
26
+ */
27
+ function deriveDatasourceKeyFromFileName(fileName, systemKey) {
28
+ const base = path.basename(fileName, path.extname(fileName));
29
+ if (/^datasource-/.test(base)) {
30
+ const suffix = base.slice('datasource-'.length);
31
+ return systemKey && typeof systemKey === 'string' ? `${systemKey}-${suffix}` : base;
32
+ }
33
+ return base.replace(/-datasource-/, '-');
34
+ }
35
+
36
+ /* eslint-disable max-statements -- Key resolution from system or files */
37
+ /**
38
+ * Resolves the list of datasource keys for an external system (from system file or discovered files).
39
+ * @param {string} appPath - Integration app path
40
+ * @param {string} configPath - Application config path
41
+ * @param {Object} variables - Loaded application variables (externalIntegration.dataSources = filenames)
42
+ * @param {string} systemKey - System key from system file
43
+ * @param {Object} systemParsed - Parsed system config (may have dataSources array of keys)
44
+ * @param {string[]} datasourceFiles - Discovered datasource filenames
45
+ * @returns {string[]} Sorted list of datasource keys
46
+ */
47
+ function getDatasourceKeys(appPath, configPath, variables, systemKey, systemParsed, datasourceFiles) {
48
+ const fromSystem = Array.isArray(systemParsed.dataSources) && systemParsed.dataSources.length > 0
49
+ ? systemParsed.dataSources
50
+ : null;
51
+ const keys = [];
52
+ const seen = new Set();
53
+ if (fromSystem) {
54
+ fromSystem.forEach(k => {
55
+ if (k && typeof k === 'string' && !seen.has(k)) {
56
+ keys.push(k.trim());
57
+ seen.add(k.trim());
58
+ }
59
+ });
60
+ keys.sort();
61
+ return keys;
62
+ }
63
+ for (const fileName of datasourceFiles) {
64
+ const filePath = path.join(appPath, fileName);
65
+ if (!fs.existsSync(filePath)) continue;
66
+ try {
67
+ const parsed = loadConfigFile(filePath);
68
+ const key = parsed && typeof parsed.key === 'string' && parsed.key.trim()
69
+ ? parsed.key.trim()
70
+ : deriveDatasourceKeyFromFileName(fileName, systemKey);
71
+ if (key && !seen.has(key)) {
72
+ keys.push(key);
73
+ seen.add(key);
74
+ }
75
+ } catch {
76
+ const key = deriveDatasourceKeyFromFileName(fileName, systemKey);
77
+ if (key && !seen.has(key)) {
78
+ keys.push(key);
79
+ seen.add(key);
80
+ }
81
+ }
82
+ }
83
+ keys.sort();
84
+ return keys;
85
+ }
86
+
87
+ /* eslint-disable max-lines-per-function, max-statements -- Load context, then loop over keys */
88
+ /**
89
+ * Runs E2E for all datasources of an external system. Uses each datasource's payloadTemplate (no extra params required).
90
+ *
91
+ * @async
92
+ * @param {string} externalSystem - System key (e.g. hubspot-demo)
93
+ * @param {Object} options - Options passed to each runDatasourceTestE2E
94
+ * @param {string} [options.env] - Environment (dev, tst, pro)
95
+ * @param {boolean} [options.debug] - Include debug, write log
96
+ * @param {boolean} [options.verbose] - Verbose output
97
+ * @param {boolean} [options.async] - If false, sync mode (default true)
98
+ * @returns {Promise<{ success: boolean, results: Array<{ key: string, success: boolean, error?: string }> }>}
99
+ */
100
+ async function runTestE2EForExternalSystem(externalSystem, options = {}) {
101
+ if (!externalSystem || typeof externalSystem !== 'string') {
102
+ throw new Error('External system name is required');
103
+ }
104
+ const appPath = getIntegrationPath(externalSystem);
105
+ if (!fs.existsSync(appPath)) {
106
+ throw new Error(`Integration path not found: ${appPath}`);
107
+ }
108
+ const configPath = resolveApplicationConfigPath(appPath);
109
+ let variables = {};
110
+ if (fs.existsSync(configPath)) {
111
+ variables = loadConfigFile(configPath);
112
+ }
113
+ const { systemFiles, datasourceFiles: discovered } = discoverIntegrationFiles(appPath);
114
+ if (systemFiles.length === 0) {
115
+ throw new Error(`No system file found in ${appPath}. Expected *-system.yaml or *-system.json`);
116
+ }
117
+ const datasourceFiles = buildEffectiveDatasourceFiles(
118
+ appPath,
119
+ discovered,
120
+ variables.externalIntegration?.dataSources
121
+ );
122
+ const systemPath = path.join(appPath, systemFiles[0]);
123
+ const systemParsed = loadConfigFile(systemPath);
124
+ const systemKey = systemParsed.key ||
125
+ path.basename(systemFiles[0], path.extname(systemFiles[0])).replace(/-system$/, '');
126
+
127
+ const keys = getDatasourceKeys(
128
+ appPath,
129
+ configPath,
130
+ variables,
131
+ systemKey,
132
+ systemParsed,
133
+ datasourceFiles
134
+ );
135
+ if (keys.length === 0) {
136
+ logger.log(chalk.yellow(`No datasources found for ${externalSystem}. Add datasource files and run aifabrix repair.`));
137
+ return { success: true, results: [] };
138
+ }
139
+
140
+ const results = [];
141
+ const opts = {
142
+ app: externalSystem,
143
+ environment: options.env,
144
+ debug: options.debug,
145
+ verbose: options.verbose,
146
+ async: options.async !== false
147
+ };
148
+ for (const key of keys) {
149
+ try {
150
+ const data = await runDatasourceTestE2E(key, opts);
151
+ const steps = data.steps || data.completedActions || [];
152
+ const failed = data.success === false || steps.some(s => s.success === false || s.error);
153
+ results.push({ key, success: !failed });
154
+ } catch (err) {
155
+ results.push({ key, success: false, error: err.message });
156
+ }
157
+ }
158
+ const success = results.every(r => r.success);
159
+ return { success, results };
160
+ }
161
+
162
+ module.exports = {
163
+ runTestE2EForExternalSystem,
164
+ getDatasourceKeys
165
+ };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Upload external system to dataplane (upload → validate → publish).
2
+ * Upload external system to dataplane (single pipeline upload: upload → validate → publish).
3
3
  *
4
4
  * @fileoverview Upload command handler for aifabrix upload <system-key>
5
5
  * @author AI Fabrix Team
@@ -14,15 +14,16 @@ const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../uti
14
14
  const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
15
15
  const { getIntegrationPath } = require('../utils/paths');
16
16
  const { pushCredentialSecrets } = require('../utils/credential-secrets-env');
17
+ const {
18
+ buildResolvedEnvMapForIntegration,
19
+ resolveConfigurationValues
20
+ } = require('../utils/configuration-env-resolver');
17
21
  const { validateExternalSystemComplete } = require('../validation/validate');
18
22
  const { displayValidationResults } = require('../validation/validate-display');
19
23
  const { generateControllerManifest } = require('../generator/external-controller-manifest');
20
- const {
21
- uploadApplicationViaPipeline,
22
- validateUploadViaPipeline,
23
- publishUploadViaPipeline
24
- } = require('../api/pipeline.api');
24
+ const { uploadApplicationViaPipeline } = require('../api/pipeline.api');
25
25
  const { formatApiError } = require('../utils/api-error-handler');
26
+ const { logDataplanePipelineWarning } = require('../utils/dataplane-pipeline-warning');
26
27
 
27
28
  /**
28
29
  * Validates system-key format (same as download).
@@ -40,25 +41,25 @@ function validateSystemKeyFormat(systemKey) {
40
41
 
41
42
  /**
42
43
  * Builds pipeline upload payload from controller manifest.
43
- * Payload: { version, application, dataSources }; application = system with RBAC.
44
+ * Payload: { version, application, dataSources, status }; Builder always uses status "draft".
44
45
  * @param {Object} manifest - Controller manifest from generateControllerManifest
45
- * @returns {Object} { version, application, dataSources }
46
+ * @returns {Object} { version, application, dataSources, status: "draft" }
46
47
  */
47
48
  function buildUploadPayload(manifest) {
48
49
  return {
49
50
  version: manifest.version || '1.0.0',
50
51
  application: manifest.system,
51
- dataSources: manifest.dataSources || []
52
+ dataSources: manifest.dataSources || [],
53
+ status: 'draft'
52
54
  };
53
55
  }
54
56
 
55
57
  /**
56
58
  * Resolves dataplane URL and auth (same pattern as download).
57
59
  * @param {string} systemKey - System key
58
- * @param {Object} options - Options with optional dataplane override
59
60
  * @returns {Promise<{ dataplaneUrl: string, authConfig: Object, environment: string }>}
60
61
  */
61
- async function resolveDataplaneAndAuth(systemKey, options) {
62
+ async function resolveDataplaneAndAuth(systemKey) {
62
63
  const { resolveEnvironment } = require('../core/config');
63
64
  const environment = await resolveEnvironment();
64
65
  const controllerUrl = await resolveControllerUrl();
@@ -68,42 +69,42 @@ async function resolveDataplaneAndAuth(systemKey, options) {
68
69
  throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register <system-key>" first.');
69
70
  }
70
71
 
71
- let dataplaneUrl;
72
- if (options.dataplane) {
73
- dataplaneUrl = options.dataplane.replace(/\/$/, '');
74
- } else {
75
- logger.log(chalk.blue('Resolving dataplane URL...'));
76
- dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
77
- }
78
-
72
+ logger.log(chalk.blue('Resolving dataplane URL...'));
73
+ const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
79
74
  return { dataplaneUrl, authConfig, environment };
80
75
  }
81
76
 
82
77
  /**
83
- * Runs upload → validate → publish on the dataplane.
78
+ * Runs single pipeline upload (upload → validate → publish) on the dataplane.
84
79
  * @param {string} dataplaneUrl - Dataplane base URL
85
80
  * @param {Object} authConfig - Auth config
86
- * @param {Object} payload - { version, application, dataSources }
87
- * @returns {Promise<{ uploadId: string }>}
81
+ * @param {Object} payload - { version, application, dataSources, status: "draft" }
82
+ * @returns {Promise<Object>} Publication result from Dataplane
88
83
  */
89
84
  async function runUploadValidatePublish(dataplaneUrl, authConfig, payload) {
90
- const uploadRes = await uploadApplicationViaPipeline(dataplaneUrl, authConfig, payload);
91
- const uploadId = uploadRes?.data?.uploadId ?? uploadRes?.data?.id ?? uploadRes?.uploadId;
92
- if (!uploadId) {
93
- const msg = uploadRes?.success === false
94
- ? formatApiError(uploadRes, dataplaneUrl)
95
- : 'Upload did not return an upload ID';
85
+ const res = await uploadApplicationViaPipeline(dataplaneUrl, authConfig, payload);
86
+ if (res?.success === false) {
87
+ const msg = formatApiError(res, dataplaneUrl);
96
88
  throw new Error(msg);
97
89
  }
90
+ return res;
91
+ }
98
92
 
99
- const validateRes = await validateUploadViaPipeline(dataplaneUrl, uploadId, authConfig);
100
- if (validateRes?.success === false) {
101
- const msg = formatApiError(validateRes, dataplaneUrl);
102
- throw new Error(`Upload validation failed: ${msg}`);
93
+ /**
94
+ * Builds a short summary of validation errors for the thrown message.
95
+ * @param {Object} validationResult - Result from validateExternalSystemComplete
96
+ * @returns {string} First few errors joined for the error message
97
+ */
98
+ function formatValidationErrorSummary(validationResult) {
99
+ const errors = validationResult.errors || [];
100
+ if (errors.length === 0) {
101
+ return 'Validation failed. Fix errors before uploading.';
103
102
  }
104
-
105
- await publishUploadViaPipeline(dataplaneUrl, uploadId, authConfig);
106
- return { uploadId };
103
+ const maxShow = 3;
104
+ const shown = errors.slice(0, maxShow).map(e => (typeof e === 'string' ? e : String(e)));
105
+ const summary = shown.join('; ');
106
+ const more = errors.length > maxShow ? ` (and ${errors.length - maxShow} more)` : '';
107
+ return `Validation failed: ${summary}${more}. Fix errors above and run the command again.`;
107
108
  }
108
109
 
109
110
  /**
@@ -114,7 +115,24 @@ async function runUploadValidatePublish(dataplaneUrl, authConfig, payload) {
114
115
  function throwIfValidationFailed(validationResult) {
115
116
  if (!validationResult.valid) {
116
117
  displayValidationResults(validationResult);
117
- throw new Error('Validation failed. Fix errors before uploading.');
118
+ throw new Error(formatValidationErrorSummary(validationResult));
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Resolves configuration arrays in the upload payload (application + dataSources) from env and secrets.
124
+ * @param {string} systemKey - System key
125
+ * @param {Object} payload - Upload payload (mutated)
126
+ */
127
+ async function resolvePayloadConfiguration(systemKey, payload) {
128
+ const { envMap, secrets } = await buildResolvedEnvMapForIntegration(systemKey);
129
+ if (Array.isArray(payload.application?.configuration) && payload.application.configuration.length > 0) {
130
+ resolveConfigurationValues(payload.application.configuration, envMap, secrets, systemKey);
131
+ }
132
+ for (const ds of payload.dataSources || []) {
133
+ if (Array.isArray(ds?.configuration) && ds.configuration.length > 0) {
134
+ resolveConfigurationValues(ds.configuration, envMap, secrets, systemKey);
135
+ }
118
136
  }
119
137
  }
120
138
 
@@ -133,7 +151,10 @@ async function pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey,
133
151
  payload
134
152
  });
135
153
  if (pushResult.pushed > 0) {
136
- logger.log(chalk.green(`Pushed ${pushResult.pushed} credential secret(s) to dataplane.`));
154
+ const keyList = pushResult.keys?.length ? ` (${pushResult.keys.join(', ')})` : '';
155
+ logger.log(chalk.green(`Pushed ${pushResult.pushed} credential secret(s) to dataplane${keyList}.`));
156
+ } else {
157
+ logger.log(chalk.yellow('Secret push skipped'));
137
158
  }
138
159
  if (pushResult.warning) {
139
160
  logger.log(chalk.yellow(`Warning: ${pushResult.warning}`));
@@ -141,11 +162,10 @@ async function pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey,
141
162
  }
142
163
 
143
164
  /**
144
- * Uploads external system to dataplane (upload validate → publish). No controller deploy.
165
+ * Uploads external system to dataplane (single pipeline upload). No controller deploy.
145
166
  * @param {string} systemKey - External system key (integration/<system-key>/)
146
167
  * @param {Object} [options] - Options
147
168
  * @param {boolean} [options.dryRun] - Validate and build payload only; no API calls
148
- * @param {string} [options.dataplane] - Override dataplane URL
149
169
  * @returns {Promise<void>}
150
170
  * @throws {Error} If validation or API calls fail
151
171
  */
@@ -159,6 +179,7 @@ async function uploadExternalSystem(systemKey, options = {}) {
159
179
 
160
180
  const manifest = await generateControllerManifest(systemKey, { type: 'external' });
161
181
  const payload = buildUploadPayload(manifest);
182
+ await resolvePayloadConfiguration(systemKey, payload);
162
183
 
163
184
  if (options.dryRun) {
164
185
  logger.log(chalk.yellow('Dry run: would upload payload (no API calls).'));
@@ -166,13 +187,23 @@ async function uploadExternalSystem(systemKey, options = {}) {
166
187
  return;
167
188
  }
168
189
 
169
- const { dataplaneUrl, authConfig, environment } = await resolveDataplaneAndAuth(systemKey, options);
190
+ const { dataplaneUrl, authConfig, environment } = await resolveDataplaneAndAuth(systemKey);
170
191
  requireBearerForDataplanePipeline(authConfig);
171
192
  logger.log(chalk.blue(`Dataplane: ${dataplaneUrl}`));
193
+ logDataplanePipelineWarning();
172
194
 
173
195
  await pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey, payload);
174
196
  await runUploadValidatePublish(dataplaneUrl, authConfig, payload);
197
+ logUploadSuccess(environment, systemKey, dataplaneUrl);
198
+ }
175
199
 
200
+ /**
201
+ * Logs upload success summary.
202
+ * @param {string} environment - Environment key
203
+ * @param {string} systemKey - System key
204
+ * @param {string} dataplaneUrl - Dataplane URL
205
+ */
206
+ function logUploadSuccess(environment, systemKey, dataplaneUrl) {
176
207
  logger.log(chalk.green('\nUpload validated and published to dataplane.'));
177
208
  logger.log(chalk.blue(`Environment: ${environment}`));
178
209
  logger.log(chalk.blue(`System: ${systemKey}`));
@@ -6,8 +6,14 @@
6
6
 
7
7
  const chalk = require('chalk');
8
8
  const ora = require('ora');
9
+ const path = require('path');
10
+ const fs = require('fs').promises;
11
+ const yaml = require('js-yaml');
9
12
  const logger = require('../utils/logger');
13
+ const { getIntegrationPath } = require('../utils/paths');
10
14
  const { parseOpenApi, testMcpConnection, credentialSelection } = require('../api/wizard.api');
15
+ const { listCredentials } = require('../api/credentials.api');
16
+ const { listExternalSystems, getExternalSystem } = require('../api/external-systems.api');
11
17
 
12
18
  /**
13
19
  * Parse OpenAPI file or URL
@@ -158,6 +164,7 @@ function buildConfigPreferences(configPrefs) {
158
164
  intent: configPrefs?.intent || 'general integration',
159
165
  fieldOnboardingLevel: configPrefs?.fieldOnboardingLevel || 'full',
160
166
  enableOpenAPIGeneration: configPrefs?.enableOpenAPIGeneration !== false,
167
+ debug: configPrefs?.debug === true,
161
168
  userPreferences: {
162
169
  enableMCP: configPrefs?.enableMCP || false,
163
170
  enableABAC: configPrefs?.enableABAC || false,
@@ -175,9 +182,10 @@ function buildConfigPreferences(configPrefs) {
175
182
  * @param {Object} params.prefs - Configuration preferences
176
183
  * @param {string} [params.credentialIdOrKey] - Credential ID or key
177
184
  * @param {string} [params.systemIdOrKey] - System ID or key
185
+ * @param {string} [params.entityName] - Entity name for multi-entity OpenAPI (from discover-entities)
178
186
  * @returns {Object} Configuration payload
179
187
  */
180
- function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credentialIdOrKey, systemIdOrKey }) {
188
+ function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credentialIdOrKey, systemIdOrKey, entityName }) {
181
189
  const detectedTypeValue = detectedType?.recommendedType || detectedType?.apiType || detectedType?.selectedType || 'record-based';
182
190
  const payload = {
183
191
  openapiSpec,
@@ -190,6 +198,32 @@ function buildConfigPayload({ openapiSpec, detectedType, mode, prefs, credential
190
198
  };
191
199
  if (credentialIdOrKey) payload.credentialIdOrKey = credentialIdOrKey;
192
200
  if (systemIdOrKey) payload.systemIdOrKey = systemIdOrKey;
201
+ if (entityName) payload.entityName = entityName;
202
+ if (prefs.debug) payload.debug = true;
203
+ return payload;
204
+ }
205
+
206
+ /**
207
+ * Build payload for platform config endpoint.
208
+ * Schema allows only: datasourceKeys?, credentialIdOrKey?, configurationValues?, debug?
209
+ * (additionalProperties: false - do NOT send intent, mode, fieldOnboardingLevel, etc.)
210
+ * @param {Object} params - Parameters object
211
+ * @param {string} [params.credentialIdOrKey] - Credential ID or key
212
+ * @param {string[]} [params.datasourceKeys] - Datasource keys to include (defaults to all)
213
+ * @param {Object} [params.configurationValues] - Configuration value overrides
214
+ * @param {boolean} [params.debug] - When true, capture debug log
215
+ * @returns {Object} Platform config payload
216
+ */
217
+ function buildPlatformConfigPayload({ credentialIdOrKey, datasourceKeys, configurationValues, debug }) {
218
+ const payload = {};
219
+ if (credentialIdOrKey) payload.credentialIdOrKey = credentialIdOrKey;
220
+ if (datasourceKeys && Array.isArray(datasourceKeys) && datasourceKeys.length > 0) {
221
+ payload.datasourceKeys = datasourceKeys;
222
+ }
223
+ if (configurationValues && typeof configurationValues === 'object' && Object.keys(configurationValues).length > 0) {
224
+ payload.configurationValues = configurationValues;
225
+ }
226
+ if (debug) payload.debug = true;
193
227
  return payload;
194
228
  }
195
229
 
@@ -250,14 +284,19 @@ function formatValidationDetailsPlain(errorData) {
250
284
  /**
251
285
  * Create and throw config generation error with optional formatted message
252
286
  * @param {Object} generateResponse - API response (error)
287
+ * @param {Object} [options] - Optional options
288
+ * @param {string} [options.debugManifestHint] - Hint to append when debug manifest was saved (e.g. review debug.log and fix manually)
253
289
  * @throws {Error}
254
290
  */
255
- function throwConfigGenerationError(generateResponse) {
291
+ function throwConfigGenerationError(generateResponse, options = {}) {
256
292
  const summary = generateResponse.error || generateResponse.formattedError || 'Unknown error';
257
293
  const detailsPlain = formatValidationDetailsPlain(generateResponse.errorData);
258
- const message = detailsPlain
294
+ let message = detailsPlain
259
295
  ? `Configuration generation failed: ${summary}\n${detailsPlain}`
260
296
  : `Configuration generation failed: ${summary}`;
297
+ if (options.debugManifestHint) {
298
+ message += `\n\n${options.debugManifestHint}`;
299
+ }
261
300
  const err = new Error(message);
262
301
  if (generateResponse.formattedError) {
263
302
  err.formatted = generateResponse.formattedError;
@@ -265,6 +304,181 @@ function throwConfigGenerationError(generateResponse) {
265
304
  throw err;
266
305
  }
267
306
 
307
+ /**
308
+ * Write debug log to integration/<appName>/debug.log
309
+ * @async
310
+ * @param {string} appName - Application name
311
+ * @param {string} content - Debug log content
312
+ */
313
+ async function writeDebugLog(appName, content) {
314
+ try {
315
+ const dir = getIntegrationPath(appName);
316
+ await fs.mkdir(dir, { recursive: true });
317
+ const debugPath = path.join(dir, 'debug.log');
318
+ await fs.writeFile(debugPath, content, 'utf8');
319
+ logger.log(chalk.gray(` Debug log saved to integration/${appName}/debug.log`));
320
+ } catch (e) {
321
+ logger.warn(`Could not save debug.log: ${e.message}`);
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Write systemConfig and datasourceConfig from error response for manual fix
327
+ * @async
328
+ * @param {string} appName - Application name
329
+ * @param {Object} [systemConfig] - System config from errorData
330
+ * @param {Object|Object[]} [datasourceConfig] - Datasource config(s) from errorData
331
+ * @returns {Promise<string[]>} Names of saved files
332
+ */
333
+ async function writeDebugManifest(appName, systemConfig, datasourceConfig) {
334
+ const saved = [];
335
+ try {
336
+ const dir = getIntegrationPath(appName);
337
+ await fs.mkdir(dir, { recursive: true });
338
+ if (systemConfig && typeof systemConfig === 'object') {
339
+ const systemPath = path.join(dir, 'debug-system.yaml');
340
+ await fs.writeFile(systemPath, yaml.dump(systemConfig, { lineWidth: -1 }), 'utf8');
341
+ saved.push('debug-system.yaml');
342
+ logger.log(chalk.gray(` Debug manifest saved to integration/${appName}/debug-system.yaml`));
343
+ }
344
+ if (datasourceConfig !== undefined && datasourceConfig !== null) {
345
+ const configs = Array.isArray(datasourceConfig) ? datasourceConfig : [datasourceConfig];
346
+ if (configs.length > 0 && configs.every(c => c && typeof c === 'object')) {
347
+ const datasourcePath = path.join(dir, 'debug-datasource.yaml');
348
+ const toWrite = configs.length === 1 ? configs[0] : configs;
349
+ await fs.writeFile(datasourcePath, yaml.dump(toWrite, { lineWidth: -1 }), 'utf8');
350
+ saved.push('debug-datasource.yaml');
351
+ logger.log(chalk.gray(` Debug manifest saved to integration/${appName}/debug-datasource.yaml`));
352
+ }
353
+ }
354
+ } catch (e) {
355
+ logger.warn(`Could not save debug manifest: ${e.message}`);
356
+ }
357
+ return saved;
358
+ }
359
+
360
+ /**
361
+ * Save debug manifest on error and throw
362
+ * @param {Object} generateResponse - API error response
363
+ * @param {Object} opts - Options
364
+ * @param {boolean} opts.enableDebug - Whether debug was enabled
365
+ * @param {string} [opts.appName] - App name for writing files
366
+ */
367
+ async function saveDebugManifestOnErrorAndThrow(generateResponse, opts) {
368
+ const { enableDebug, appName } = opts;
369
+ let debugManifestHint = null;
370
+ if (enableDebug && appName) {
371
+ const errorData = generateResponse.errorData || {};
372
+ const debugLog = errorData.debugLog;
373
+ if (debugLog && typeof debugLog === 'string') {
374
+ await writeDebugLog(appName, debugLog);
375
+ }
376
+ const systemConfig = errorData.systemConfig;
377
+ const datasourceConfig = errorData.datasourceConfig || errorData.datasourceConfigs;
378
+ const savedManifest = await writeDebugManifest(appName, systemConfig, datasourceConfig);
379
+ if (debugLog || savedManifest.length > 0) {
380
+ const files = [debugLog && 'debug.log', ...savedManifest].filter(Boolean).join(', ');
381
+ debugManifestHint = `Debug manifest saved to integration/${appName}/ (${files}). Review the log and fix the manifest manually, then run: aifabrix wizard ${appName}`;
382
+ }
383
+ }
384
+ throwConfigGenerationError(generateResponse, { debugManifestHint });
385
+ }
386
+
387
+ /** Throws with validation error; saves debug manifest when options.debug and options.appName are set. */
388
+ async function throwValidationFailureWithDebug(validateResponse, systemConfig, configs, errorMsg, options) {
389
+ if (!options.debug || !options.appName) throw new Error(`Configuration validation failed: ${errorMsg}`);
390
+ const errorData = validateResponse.errorData || validateResponse.data || {};
391
+ const debugLog = errorData.debugLog;
392
+ if (debugLog && typeof debugLog === 'string') await writeDebugLog(options.appName, debugLog);
393
+ const savedManifest = await writeDebugManifest(
394
+ options.appName, errorData.systemConfig || systemConfig, errorData.datasourceConfig || errorData.datasourceConfigs || configs
395
+ );
396
+ if (!debugLog && savedManifest.length === 0) throw new Error(`Configuration validation failed: ${errorMsg}`);
397
+ const files = [debugLog && 'debug.log', ...savedManifest].filter(Boolean).join(', ');
398
+ throw new Error(`Configuration validation failed: ${errorMsg}\n\nDebug manifest saved to integration/${options.appName}/ (${files}). Review the log and fix the manifest manually, then run: aifabrix wizard ${options.appName}`);
399
+ }
400
+
401
+ /**
402
+ * Resolve credential config from user action (select/skip/create)
403
+ * @async
404
+ * @param {string} dataplaneUrl - Dataplane URL
405
+ * @param {Object} authConfig - Auth config
406
+ * @param {Object} credentialAction - Result from promptForCredentialAction
407
+ * @returns {Promise<Object>} configCredential for handleCredentialSelection
408
+ */
409
+ async function resolveCredentialConfig(dataplaneUrl, authConfig, credentialAction) {
410
+ const { promptForExistingCredential } = require('../generator/wizard-prompts');
411
+ if (credentialAction.action !== 'select') {
412
+ return credentialAction.action === 'skip' ? { action: 'skip' } : { action: credentialAction.action };
413
+ }
414
+ let credentialsList = [];
415
+ try {
416
+ const listResponse = await listCredentials(dataplaneUrl, authConfig, {
417
+ activeOnly: true,
418
+ pageSize: 100
419
+ });
420
+ const body = listResponse?.data ?? listResponse;
421
+ const inner = Array.isArray(body) ? body : (body?.data ?? body);
422
+ credentialsList = Array.isArray(inner) ? inner : (inner?.credentials ?? inner?.items ?? []) || [];
423
+ } catch (_) {
424
+ credentialsList = [];
425
+ }
426
+ const selected = await promptForExistingCredential(Array.isArray(credentialsList) ? credentialsList : []);
427
+ return { action: 'select', credentialIdOrKey: selected.credentialIdOrKey };
428
+ }
429
+
430
+ /**
431
+ * Fetch external systems list from dataplane
432
+ * @async
433
+ * @param {string} dataplaneUrl - Dataplane URL
434
+ * @param {Object} authConfig - Auth config
435
+ * @returns {Promise<Object[]>} Systems list
436
+ */
437
+ async function fetchSystemsListForAddDatasource(dataplaneUrl, authConfig) {
438
+ try {
439
+ const listResponse = await listExternalSystems(dataplaneUrl, authConfig, { pageSize: 100 });
440
+ const body = listResponse?.data ?? listResponse;
441
+ const inner = Array.isArray(body) ? body : (body?.data ?? body);
442
+ const list = Array.isArray(inner) ? inner : (inner?.items ?? inner?.systems ?? []) || [];
443
+ return Array.isArray(list) ? list : [];
444
+ } catch (_) {
445
+ return [];
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Resolve and validate external system for add-datasource (prompt until valid)
451
+ * @async
452
+ * @param {string} dataplaneUrl - Dataplane URL
453
+ * @param {Object} authConfig - Auth config
454
+ * @param {Object[]} systemsList - Systems list
455
+ * @param {string} initialSystemIdOrKey - Initial system ID or key
456
+ * @returns {Promise<{systemResponse: Object, systemIdOrKey: string}>}
457
+ */
458
+ async function resolveExternalSystemForAddDatasource(dataplaneUrl, authConfig, systemsList, initialSystemIdOrKey) {
459
+ const { promptForExistingSystem } = require('../generator/wizard-prompts');
460
+ const { isExternalSystemForAddDatasource } = require('./wizard-helpers');
461
+ let systemIdOrKey = initialSystemIdOrKey;
462
+ let systemResponse;
463
+ for (;;) {
464
+ try {
465
+ systemResponse = await getExternalSystem(dataplaneUrl, systemIdOrKey, authConfig);
466
+ const sys = systemResponse?.data || systemResponse;
467
+ if (sys && (systemResponse?.data || systemResponse?.success)) {
468
+ if (!isExternalSystemForAddDatasource(sys)) {
469
+ logger.log(chalk.red('Cannot add datasource to a webapp. Please select an external system.'));
470
+ } else {
471
+ break;
472
+ }
473
+ }
474
+ } catch (err) {
475
+ logger.log(chalk.red(`System not found or error: ${err.message}`));
476
+ }
477
+ systemIdOrKey = await promptForExistingSystem(systemsList, systemIdOrKey);
478
+ }
479
+ return { systemResponse, systemIdOrKey };
480
+ }
481
+
268
482
  module.exports = {
269
483
  parseOpenApiSource,
270
484
  testMcpServerConnection,
@@ -272,7 +486,15 @@ module.exports = {
272
486
  runCredentialSelectionLoop,
273
487
  buildConfigPreferences,
274
488
  buildConfigPayload,
489
+ buildPlatformConfigPayload,
275
490
  extractConfigurationFromResponse,
276
491
  formatValidationDetailsPlain,
277
- throwConfigGenerationError
492
+ throwConfigGenerationError,
493
+ writeDebugLog,
494
+ writeDebugManifest,
495
+ saveDebugManifestOnErrorAndThrow,
496
+ throwValidationFailureWithDebug,
497
+ resolveCredentialConfig,
498
+ fetchSystemsListForAddDatasource,
499
+ resolveExternalSystemForAddDatasource
278
500
  };