@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
@@ -49,6 +49,22 @@ function readDeveloperIdFromConfig(config) {
49
49
  return null;
50
50
  }
51
51
 
52
+ /**
53
+ * Substitute /mnt/data with local mount path for local .env and ensure mount dir exists on disk.
54
+ * Creates the mount folder on the local filesystem (next to the .env file) when it does not exist.
55
+ * @param {string} content - Env file content
56
+ * @param {string} outputPath - Resolved path of the .env file being written
57
+ * @returns {string} Content with /mnt/data replaced by path to mount directory
58
+ */
59
+ function substituteMntDataForLocal(content, outputPath) {
60
+ const outputDir = path.dirname(outputPath);
61
+ const localMountPath = path.resolve(outputDir, 'mount');
62
+ if (!fs.existsSync(localMountPath)) {
63
+ fs.mkdirSync(localMountPath, { recursive: true });
64
+ }
65
+ return content.replace(/\/mnt\/data/g, localMountPath);
66
+ }
67
+
52
68
  /**
53
69
  * Resolve output path for env file
54
70
  * @param {string} rawOutputPath - Raw output path from application config
@@ -100,7 +116,8 @@ async function writeEnvOutputForReload(outputPath, runEnvPath) {
100
116
  */
101
117
  async function writeEnvOutputForLocal(appName, outputPath) {
102
118
  const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
103
- const localContent = await generateEnvContent(appName, null, 'local', false);
119
+ let localContent = await generateEnvContent(appName, null, 'local', false);
120
+ localContent = substituteMntDataForLocal(localContent, outputPath);
104
121
  let toWrite = localContent;
105
122
  if (fs.existsSync(outputPath)) {
106
123
  const existingContent = await fsp.readFile(outputPath, 'utf8');
@@ -229,7 +246,8 @@ async function patchEnvContentForLocal(envContent, variables) {
229
246
  */
230
247
  async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOutputPathLabel) {
231
248
  const { generateEnvContent, parseEnvContentToMap, mergeEnvMapIntoContent } = require('../core/secrets');
232
- const localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false);
249
+ let localEnvContent = await generateEnvContent(appName, secretsPath, 'local', false);
250
+ localEnvContent = substituteMntDataForLocal(localEnvContent, outputPath);
233
251
  let toWrite = localEnvContent;
234
252
  if (fs.existsSync(outputPath)) {
235
253
  const existingContent = fs.readFileSync(outputPath, 'utf8');
@@ -250,7 +268,8 @@ async function writeLocalEnvToOutputPath(outputPath, appName, secretsPath, envOu
250
268
  */
251
269
  async function writePatchedEnvToOutputPath(envPath, outputPath, variables, envOutputPathLabel) {
252
270
  const envContent = fs.readFileSync(envPath, 'utf8');
253
- const patchedContent = await patchEnvContentForLocal(envContent, variables);
271
+ let patchedContent = await patchEnvContentForLocal(envContent, variables);
272
+ patchedContent = substituteMntDataForLocal(patchedContent, outputPath);
254
273
  fs.writeFileSync(outputPath, patchedContent, { mode: 0o600 });
255
274
  logger.log(chalk.green(`✓ Copied .env to: ${envOutputPathLabel}`));
256
275
  }
@@ -292,6 +311,7 @@ async function processEnvVariables(envPath, variablesPath, appName, secretsPath)
292
311
  module.exports = {
293
312
  processEnvVariables,
294
313
  resolveEnvOutputPath,
314
+ substituteMntDataForLocal,
295
315
  writeEnvOutputForReload,
296
316
  writeEnvOutputForLocal
297
317
  };
@@ -323,4 +323,3 @@ module.exports = {
323
323
  formatNotFoundError,
324
324
  formatGenericError
325
325
  };
326
-
@@ -134,4 +134,3 @@ module.exports = {
134
134
  extractMissingPermissions,
135
135
  extractRequiredPermissions
136
136
  };
137
-
@@ -130,4 +130,3 @@ function formatValidationError(errorData) {
130
130
  module.exports = {
131
131
  formatValidationError
132
132
  };
133
-
@@ -30,42 +30,65 @@ function formatDisplayName(key) {
30
30
  .join(' ');
31
31
  }
32
32
 
33
+ /**
34
+ * Derives suffix from datasource key for filename generation
35
+ * @param {string} key - Datasource key
36
+ * @param {string} systemKey - System key
37
+ * @param {string} entityType - Fallback entity type
38
+ * @returns {string} Suffix segment
39
+ */
40
+ function getDatasourceKeySuffix(key, systemKey, entityType) {
41
+ if (key.startsWith(`${systemKey}-deploy-`)) {
42
+ return key.slice(`${systemKey}-deploy-`.length);
43
+ }
44
+ if (systemKey && key.startsWith(`${systemKey}-`)) {
45
+ return key.slice(systemKey.length + 1);
46
+ }
47
+ if (key) {
48
+ return key;
49
+ }
50
+ return entityType;
51
+ }
52
+
53
+ /**
54
+ * Normalizes a single datasource entry for template use
55
+ * @param {Object} datasource - Datasource object
56
+ * @param {number} index - Index in array
57
+ * @param {string} systemKey - System key for filename generation
58
+ * @param {string} ext - File extension (e.g. '.json', '.yaml')
59
+ * @returns {{entityType: string, displayName: string, fileName: string, datasourceKey: string}} Normalized entry
60
+ */
61
+ function normalizeOneDatasource(datasource, index, systemKey, ext) {
62
+ const entityType = datasource.entityType ||
63
+ datasource.entityKey ||
64
+ datasource.key?.split('-').pop() ||
65
+ `entity${index + 1}`;
66
+ const displayName = datasource.displayName ||
67
+ datasource.name ||
68
+ `Datasource ${index + 1}`;
69
+ const key = datasource.key || '';
70
+ const suffix = getDatasourceKeySuffix(key, systemKey, entityType);
71
+ const datasourceKey = key || (systemKey ? `${systemKey}-${suffix}` : suffix);
72
+ const fileName = datasource.fileName || datasource.file ||
73
+ (systemKey ? `${systemKey}-datasource-${suffix}${ext}` : `${suffix}${ext}`);
74
+ return { entityType, displayName, fileName, datasourceKey };
75
+ }
76
+
33
77
  /**
34
78
  * Normalizes datasource entries for template use
35
79
  * @param {Array} datasources - Datasource objects
36
80
  * @param {string} systemKey - System key for filename generation
37
- * @returns {Array<{entityType: string, displayName: string, fileName: string}>} Normalized entries
81
+ * @param {string} [fileExt='.json'] - File extension for generated filenames (e.g. '.json', '.yaml')
82
+ * @returns {Array<{entityType: string, displayName: string, fileName: string, datasourceKey: string}>} Normalized entries
38
83
  */
39
- function normalizeDatasources(datasources, systemKey) {
84
+ function normalizeDatasources(datasources, systemKey, fileExt = '.json') {
40
85
  if (!Array.isArray(datasources)) {
41
86
  return [];
42
87
  }
43
- return datasources.map((datasource, index) => {
44
- const entityType = datasource.entityType ||
45
- datasource.entityKey ||
46
- datasource.key?.split('-').pop() ||
47
- `entity${index + 1}`;
48
- const displayName = datasource.displayName ||
49
- datasource.name ||
50
- `Datasource ${index + 1}`;
51
- let fileName = datasource.fileName || datasource.file;
52
- if (!fileName) {
53
- const key = datasource.key || '';
54
- // Suffix matches split getExternalDatasourceFileName for consistent README and file names
55
- let suffix;
56
- if (key.startsWith(`${systemKey}-deploy-`)) {
57
- suffix = key.slice(`${systemKey}-deploy-`.length);
58
- } else if (systemKey && key.startsWith(`${systemKey}-`)) {
59
- suffix = key.slice(systemKey.length + 1);
60
- } else if (key) {
61
- suffix = key;
62
- } else {
63
- suffix = entityType;
64
- }
65
- fileName = systemKey ? `${systemKey}-datasource-${suffix}.yaml` : `${suffix}.yaml`;
66
- }
67
- return { entityType, displayName, fileName };
68
- });
88
+ const ext = fileExt && fileExt.startsWith('.') ? fileExt : `.${fileExt || 'json'}`;
89
+ return datasources.map((datasource, index) =>
90
+ normalizeOneDatasource(datasource, index, systemKey, ext)
91
+ );
69
92
  }
70
93
 
71
94
  /**
@@ -78,6 +101,7 @@ function normalizeDatasources(datasources, systemKey) {
78
101
  * @param {string} [params.displayName] - Display name
79
102
  * @param {string} [params.description] - Description
80
103
  * @param {Array} [params.datasources] - Datasource objects
104
+ * @param {string} [params.fileExt] - File extension for config files (e.g. '.json', '.yaml'); default '.json'
81
105
  * @returns {Object} Template context
82
106
  */
83
107
  function buildExternalReadmeContext(params = {}) {
@@ -86,7 +110,8 @@ function buildExternalReadmeContext(params = {}) {
86
110
  const displayName = params.displayName || formatDisplayName(systemKey);
87
111
  const description = params.description || `External system integration for ${systemKey}`;
88
112
  const systemType = params.systemType || 'openapi';
89
- const datasources = normalizeDatasources(params.datasources, systemKey);
113
+ const fileExt = params.fileExt !== undefined ? params.fileExt : '.json';
114
+ const datasources = normalizeDatasources(params.datasources, systemKey, fileExt);
90
115
 
91
116
  return {
92
117
  appName,
@@ -94,7 +119,9 @@ function buildExternalReadmeContext(params = {}) {
94
119
  displayName,
95
120
  description,
96
121
  systemType,
122
+ fileExt: fileExt && fileExt.startsWith('.') ? fileExt : `.${fileExt || 'json'}`,
97
123
  datasourceCount: datasources.length,
124
+ hasDatasources: datasources.length > 0,
98
125
  datasources
99
126
  };
100
127
  }
@@ -241,8 +241,66 @@ function displayIntegrationTestResults(results, verbose = false) {
241
241
  }
242
242
  }
243
243
 
244
+ /**
245
+ * Displays E2E test results (steps: config, credential, sync, data, cip).
246
+ * Supports sync response (data.steps only), final poll (data.steps + data.success), and running poll
247
+ * (data.completedActions, no data.steps yet). When status is present (async flow), shows it.
248
+ *
249
+ * @param {Object} data - E2E response or poll data
250
+ * @param {string} [data.status] - Optional status: 'running' | 'completed' | 'failed' (async flow)
251
+ * @param {Object[]} [data.steps] - Per-step results (final state)
252
+ * @param {Object[]} [data.completedActions] - Steps completed so far (running state when steps absent)
253
+ * @param {boolean} [data.success] - Overall success (final state)
254
+ * @param {string} [data.error] - Error message when failed
255
+ * @param {boolean} [verbose] - Show detailed output
256
+ */
257
+ /* eslint-disable max-statements,complexity -- Step iteration and status display */
258
+ function displayE2EResults(data, verbose = false) {
259
+ logger.log(chalk.blue('\n📊 E2E Test Results\n'));
260
+ if (data.status) {
261
+ const statusLabel = data.status === 'running'
262
+ ? chalk.yellow('running')
263
+ : data.status === 'completed'
264
+ ? chalk.green('completed')
265
+ : data.status === 'failed'
266
+ ? chalk.red('failed')
267
+ : data.status;
268
+ logger.log(`Status: ${statusLabel}`);
269
+ }
270
+ const steps = data.steps || data.completedActions || [];
271
+ if (steps.length === 0) {
272
+ if (data.success === false) {
273
+ logger.log(chalk.red('✗ E2E test failed'));
274
+ if (data.error) logger.log(chalk.red(` Error: ${data.error}`));
275
+ } else if (data.status === 'running') {
276
+ logger.log(chalk.gray(' No steps completed yet'));
277
+ } else {
278
+ logger.log(chalk.yellow('No step results returned'));
279
+ }
280
+ return;
281
+ }
282
+ const isRunning = data.status === 'running' && !data.steps;
283
+ if (isRunning && verbose) {
284
+ logger.log(chalk.gray(` (${steps.length} step(s) completed so far)`));
285
+ }
286
+ for (const step of steps) {
287
+ const name = step.name || step.step || 'unknown';
288
+ const ok = step.success !== false && !step.error;
289
+ logger.log(` ${ok ? chalk.green('✓') : chalk.red('✗')} ${name}`);
290
+ if (!ok && (step.error || step.message)) logger.log(chalk.red(` ${step.error || step.message}`));
291
+ if (verbose && step.message && ok) logger.log(chalk.gray(` ${step.message}`));
292
+ }
293
+ if (isRunning) {
294
+ return;
295
+ }
296
+ const allPassed = steps.every(s => s.success !== false && !s.error);
297
+ logger.log(allPassed ? chalk.green('\n✅ E2E test passed!') : chalk.red('\n❌ E2E test failed'));
298
+ }
299
+
244
300
  module.exports = {
245
301
  displayTestResults,
246
- displayIntegrationTestResults
302
+ displayIntegrationTestResults,
303
+ displayE2EResults,
304
+ displayDatasourceIntegrationResult
247
305
  };
248
306
 
@@ -12,7 +12,8 @@
12
12
  const fs = require('fs').promises;
13
13
  const path = require('path');
14
14
  const { testDatasourceViaPipeline } = require('../api/pipeline.api');
15
- const { requireBearerForDataplanePipeline } = require('./token-manager');
15
+
16
+ /** Pipeline test endpoints accept client credentials; do not enforce Bearer-only */
16
17
 
17
18
  /**
18
19
  * Retry API call with exponential backoff
@@ -40,26 +41,31 @@ async function retryApiCall(fn, maxRetries = 3, backoffMs = 1000) {
40
41
  }
41
42
 
42
43
  /**
43
- * Calls pipeline test endpoint using centralized API client
44
+ * Calls pipeline test endpoint using centralized API client.
45
+ * Pipeline test accepts Bearer, API_KEY, or client credentials (x-client-id/x-client-secret) for CI/CD.
44
46
  * @async
45
47
  * @param {Object} params - Function parameters
46
48
  * @param {string} params.systemKey - System key
47
49
  * @param {string} params.datasourceKey - Datasource key
48
50
  * @param {Object} params.payloadTemplate - Test payload template
49
51
  * @param {string} params.dataplaneUrl - Dataplane URL
50
- * @param {Object} params.authConfig - Authentication configuration
52
+ * @param {Object} params.authConfig - Authentication configuration (token or client credentials)
51
53
  * @param {number} [params.timeout] - Request timeout in milliseconds (default: 30000)
54
+ * @param {boolean} [params.includeDebug] - Include debug output in response
52
55
  * @returns {Promise<Object>} Test response
53
56
  */
54
- async function callPipelineTestEndpoint({ systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout = 30000 }) {
55
- requireBearerForDataplanePipeline(authConfig);
57
+ async function callPipelineTestEndpoint({ systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout = 30000, includeDebug = false }) {
58
+ const testData = { payloadTemplate };
59
+ if (includeDebug) {
60
+ testData.includeDebug = true;
61
+ }
56
62
  const response = await retryApiCall(async() => {
57
63
  return await testDatasourceViaPipeline({
58
64
  dataplaneUrl,
59
65
  systemKey,
60
66
  datasourceKey,
61
67
  authConfig,
62
- testData: { payloadTemplate },
68
+ testData,
63
69
  options: { timeout }
64
70
  });
65
71
  });
@@ -67,6 +73,11 @@ async function callPipelineTestEndpoint({ systemKey, datasourceKey, payloadTempl
67
73
  if (!response.success || !response.data) {
68
74
  throw new Error(`Test endpoint failed: ${response.error || response.formattedError || 'Unknown error'}`);
69
75
  }
76
+ // When 200 with success: false in body, pass through; caller interprets via data.success
77
+ if (response.data?.success === false) {
78
+ const errMsg = response.data?.error || response.data?.formattedError || 'Test failed';
79
+ throw new Error(`Test endpoint failed: ${errMsg}`);
80
+ }
70
81
 
71
82
  return response.data.data || response.data;
72
83
  }
@@ -114,16 +125,18 @@ function determinePayloadTemplate(datasource, datasourceKey, customPayload) {
114
125
  * @param {string} params.dataplaneUrl - Dataplane URL
115
126
  * @param {Object} params.authConfig - Authentication configuration
116
127
  * @param {number} params.timeout - Request timeout
128
+ * @param {boolean} [params.includeDebug] - Include debug in response
117
129
  * @returns {Promise<Object>} Test result
118
130
  */
119
- async function testSingleDatasource({ systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout }) {
131
+ async function testSingleDatasource({ systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout, includeDebug = false }) {
120
132
  const testResponse = await callPipelineTestEndpoint({
121
133
  systemKey,
122
134
  datasourceKey,
123
135
  payloadTemplate,
124
136
  dataplaneUrl,
125
137
  authConfig,
126
- timeout
138
+ timeout,
139
+ includeDebug
127
140
  });
128
141
 
129
142
  return {
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const Ajv = require('ajv');
12
+ const addFormats = require('ajv-formats');
12
13
 
13
14
  /**
14
15
  * Validates field mapping expression syntax (pipe-based DSL)
@@ -263,6 +264,7 @@ function validateMetadataSchema(datasource, testPayload) {
263
264
 
264
265
  try {
265
266
  const ajv = new Ajv({ allErrors: true, strict: false });
267
+ addFormats(ajv);
266
268
  const validate = ajv.compile(datasource.metadataSchema);
267
269
  const valid = validate(payloadTemplate);
268
270
 
@@ -319,6 +321,7 @@ function validateAgainstSchema(data, schema) {
319
321
  allowUnionTypes: true,
320
322
  validateSchema: false
321
323
  });
324
+ addFormats(ajv);
322
325
  // Remove $schema for draft-2020-12 to avoid AJV issues
323
326
  const schemaCopy = { ...schema };
324
327
  if (schemaCopy.$schema && schemaCopy.$schema.includes('2020-12')) {
@@ -1,29 +1,14 @@
1
1
  /**
2
2
  * @fileoverview File upload utilities for multipart/form-data requests
3
+ * All API calls go via ApiClient (lib/api/index.js); no duplicate auth logic.
3
4
  * @author AI Fabrix Team
4
5
  * @version 2.0.0
5
6
  */
6
7
 
7
8
  const fs = require('fs').promises;
8
9
  const path = require('path');
9
- const { makeApiCall, authenticatedApiCall } = require('./api');
10
+ const { ApiClient } = require('../api');
10
11
 
11
- /**
12
- * Upload a file using multipart/form-data
13
- * @async
14
- * @function uploadFile
15
- * @param {string} url - API endpoint URL
16
- * @param {string} filePath - Path to file to upload
17
- * @param {string} fieldName - Form field name for the file (default: 'file')
18
- * @param {Object} [authConfig] - Authentication configuration
19
- * @param {string} [authConfig.type] - Auth type ('bearer' | 'client-credentials')
20
- * @param {string} [authConfig.token] - Bearer token
21
- * @param {string} [authConfig.clientId] - Client ID
22
- * @param {string} [authConfig.clientSecret] - Client secret
23
- * @param {Object} [additionalFields] - Additional form fields to include
24
- * @returns {Promise<Object>} API response
25
- * @throws {Error} If file upload fails
26
- */
27
12
  /**
28
13
  * Validates file exists
29
14
  * @async
@@ -63,47 +48,32 @@ async function buildFormData(filePath, fieldName, additionalFields) {
63
48
  }
64
49
 
65
50
  /**
66
- * Builds authentication headers
67
- * @function buildAuthHeaders
68
- * @param {Object} authConfig - Authentication configuration
69
- * @returns {Object} Headers object
51
+ * Upload a file using multipart/form-data via ApiClient (single place for auth and API calls).
52
+ * @async
53
+ * @function uploadFile
54
+ * @param {string} url - Full API endpoint URL (e.g. https://dataplane.example.com/api/v1/wizard/parse-openapi)
55
+ * @param {string} filePath - Path to file to upload
56
+ * @param {string} fieldName - Form field name for the file (default: 'file')
57
+ * @param {Object} [authConfig] - Authentication configuration (token-only for app endpoints)
58
+ * @param {string} [authConfig.type] - Auth type ('bearer' | 'client-token')
59
+ * @param {string} [authConfig.token] - Token (Bearer user token or x-client-token application token)
60
+ * @param {Object} [additionalFields] - Additional form fields to include
61
+ * @returns {Promise<Object>} API response
62
+ * @throws {Error} If file upload fails
70
63
  */
71
- function buildAuthHeaders(authConfig) {
72
- const headers = {};
73
- if (authConfig.type === 'bearer' && authConfig.token) {
74
- headers['Authorization'] = `Bearer ${authConfig.token}`;
75
- } else if (authConfig.type === 'client-credentials') {
76
- if (authConfig.clientId) {
77
- headers['x-client-id'] = authConfig.clientId;
78
- }
79
- if (authConfig.clientSecret) {
80
- headers['x-client-secret'] = authConfig.clientSecret;
81
- }
82
- }
83
- return headers;
84
- }
85
-
86
64
  async function uploadFile(url, filePath, fieldName = 'file', authConfig = {}, additionalFields = {}) {
87
65
  await validateFileExists(filePath);
88
66
 
89
- const formData = await buildFormData(filePath, fieldName, additionalFields);
90
- const headers = buildAuthHeaders(authConfig);
91
-
92
- const options = {
93
- method: 'POST',
94
- headers,
95
- body: formData
96
- };
67
+ const parsed = new URL(url);
68
+ const baseUrl = parsed.origin;
69
+ const endpointPath = parsed.pathname + parsed.search;
97
70
 
98
- // Use authenticatedApiCall if bearer token, otherwise makeApiCall
99
- if (authConfig.type === 'bearer' && authConfig.token) {
100
- return await authenticatedApiCall(url, options, authConfig.token);
101
- }
71
+ const formData = await buildFormData(filePath, fieldName, additionalFields);
72
+ const client = new ApiClient(baseUrl, authConfig);
102
73
 
103
- return await makeApiCall(url, options);
74
+ return await client.postFormData(endpointPath, formData);
104
75
  }
105
76
 
106
77
  module.exports = {
107
78
  uploadFile
108
79
  };
109
-
@@ -97,6 +97,7 @@ const CATEGORIES = [
97
97
  { name: 'download', term: 'download <system-key>' },
98
98
  { name: 'upload', term: 'upload <system-key>' },
99
99
  { name: 'delete', term: 'delete <system-key>' },
100
+ { name: 'repair', term: 'repair <app>' },
100
101
  { name: 'test', term: 'test <app>' },
101
102
  { name: 'test-integration', term: 'test-integration <app>' }
102
103
  ]
@@ -17,9 +17,53 @@ const containerUtils = require('./infra-containers');
17
17
 
18
18
  const execAsync = promisify(exec);
19
19
 
20
+ /**
21
+ * Builds services config map from ports and config flags.
22
+ * @param {Object} ports - Port configuration
23
+ * @param {Object} cfg - Config (pgadmin, redisCommander, traefik)
24
+ * @returns {Object} Map of serviceName -> { port, url }
25
+ */
26
+ function buildServicesConfig(ports, cfg) {
27
+ const services = {
28
+ postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
29
+ redis: { port: ports.redis, url: `localhost:${ports.redis}` }
30
+ };
31
+ if (cfg.pgadmin !== false) services.pgadmin = { port: ports.pgadmin, url: `http://localhost:${ports.pgadmin}` };
32
+ if (cfg.redisCommander !== false) {
33
+ services['redis-commander'] = { port: ports.redisCommander, url: `http://localhost:${ports.redisCommander}` };
34
+ }
35
+ if (cfg.traefik) {
36
+ services.traefik = {
37
+ port: `${ports.traefikHttp}, ${ports.traefikHttps}`,
38
+ url: `http://localhost:${ports.traefikHttp}, https://localhost:${ports.traefikHttps}`
39
+ };
40
+ }
41
+ return services;
42
+ }
43
+
44
+ /**
45
+ * Gets status for a single service.
46
+ * @param {string} serviceName - Service name
47
+ * @param {Object} serviceConfig - { port, url }
48
+ * @param {string} devId - Developer ID
49
+ * @returns {Promise<Object>} Status entry
50
+ */
51
+ async function getServiceStatus(serviceName, serviceConfig, devId) {
52
+ try {
53
+ const containerName = await containerUtils.findContainer(serviceName, devId, { strict: true });
54
+ const rawStatus = containerName
55
+ ? (await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`)).stdout.trim().replace(/['"]/g, '')
56
+ : 'not running';
57
+ return { status: rawStatus, port: serviceConfig.port, url: serviceConfig.url };
58
+ } catch {
59
+ return { status: 'not running', port: serviceConfig.port, url: serviceConfig.url };
60
+ }
61
+ }
62
+
20
63
  /**
21
64
  * Gets the status of infrastructure services
22
- * Returns detailed information about running containers
65
+ * Returns detailed information about running containers.
66
+ * Only includes pgAdmin, Redis Commander, and Traefik when enabled in config.
23
67
  *
24
68
  * @async
25
69
  * @function getInfraStatus
@@ -31,51 +75,13 @@ const execAsync = promisify(exec);
31
75
  */
32
76
  async function getInfraStatus() {
33
77
  const devId = await config.getDeveloperId();
34
- // Convert string developer ID to number for getDevPorts
35
- const devIdNum = parseInt(devId, 10);
36
- const ports = devConfig.getDevPorts(devIdNum);
37
- const services = {
38
- postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
39
- redis: { port: ports.redis, url: `localhost:${ports.redis}` },
40
- pgadmin: { port: ports.pgadmin, url: `http://localhost:${ports.pgadmin}` },
41
- 'redis-commander': { port: ports.redisCommander, url: `http://localhost:${ports.redisCommander}` },
42
- traefik: {
43
- port: `${ports.traefikHttp}, ${ports.traefikHttps}`,
44
- url: `http://localhost:${ports.traefikHttp}, https://localhost:${ports.traefikHttps}`
45
- }
46
- };
47
-
78
+ const cfg = await config.getConfig();
79
+ const ports = devConfig.getDevPorts(parseInt(devId, 10));
80
+ const services = buildServicesConfig(ports, cfg);
48
81
  const status = {};
49
-
50
- for (const [serviceName, serviceConfig] of Object.entries(services)) {
51
- try {
52
- // Strict: only this developer's infra (no fallback to dev 0), so status reflects reality
53
- const containerName = await containerUtils.findContainer(serviceName, devId, { strict: true });
54
- if (containerName) {
55
- const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
56
- // Normalize status value (trim whitespace and remove quotes)
57
- const normalizedStatus = stdout.trim().replace(/['"]/g, '');
58
- status[serviceName] = {
59
- status: normalizedStatus,
60
- port: serviceConfig.port,
61
- url: serviceConfig.url
62
- };
63
- } else {
64
- status[serviceName] = {
65
- status: 'not running',
66
- port: serviceConfig.port,
67
- url: serviceConfig.url
68
- };
69
- }
70
- } catch (error) {
71
- status[serviceName] = {
72
- status: 'not running',
73
- port: serviceConfig.port,
74
- url: serviceConfig.url
75
- };
76
- }
82
+ for (const [name, svc] of Object.entries(services)) {
83
+ status[name] = await getServiceStatus(name, svc, devId);
77
84
  }
78
-
79
85
  return status;
80
86
  }
81
87
 
@@ -13,12 +13,12 @@ const path = require('path');
13
13
  const yaml = require('js-yaml');
14
14
  const logger = require('../utils/logger');
15
15
  const pathsUtil = require('./paths');
16
- const { appendSecretsToFile } = require('./secrets-generator');
16
+ const { mergeSecretsIntoFile } = require('./secrets-generator');
17
17
 
18
18
  /**
19
19
  * Saves a secret to ~/.aifabrix/secrets.local.yaml
20
20
  * Uses paths.getAifabrixHome() to respect config.yaml aifabrix-home override
21
- * Appends the key to the end of the file without changing existing content (preserves comments and structure)
21
+ * Merges the key into the file (updates in place if key already exists, e.g. after rotate-secret)
22
22
  *
23
23
  * @async
24
24
  * @function saveLocalSecret
@@ -40,12 +40,12 @@ async function saveLocalSecret(key, value) {
40
40
  }
41
41
 
42
42
  const secretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
43
- appendSecretsToFile(secretsPath, { [key]: value });
43
+ mergeSecretsIntoFile(secretsPath, { [key]: value });
44
44
  }
45
45
 
46
46
  /**
47
47
  * Saves a secret to a specified secrets file path
48
- * Appends the key to the end of the file without changing existing content (preserves comments and structure)
48
+ * Merges the key into the file (updates in place if key already exists)
49
49
  *
50
50
  * @async
51
51
  * @function saveSecret
@@ -122,7 +122,7 @@ async function saveSecret(key, value, secretsPath) {
122
122
  validateSaveSecretParams(key, value, secretsPath);
123
123
 
124
124
  const resolvedPath = resolveAndPrepareSecretsPath(secretsPath);
125
- appendSecretsToFile(resolvedPath, { [key]: value });
125
+ mergeSecretsIntoFile(resolvedPath, { [key]: value });
126
126
  }
127
127
 
128
128
  /**