@aifabrix/builder 2.41.0 → 2.42.1

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 (142) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +2 -2
  3. package/integration/hubspot/README.md +11 -5
  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 +36 -2
  22. package/lib/app/config.js +23 -11
  23. package/lib/app/index.js +5 -3
  24. package/lib/app/prompts.js +46 -31
  25. package/lib/app/readme.js +11 -4
  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 +45 -14
  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/auth-config.js +22 -12
  37. package/lib/commands/credential-env.js +162 -0
  38. package/lib/commands/credential-list.js +17 -22
  39. package/lib/commands/credential-push.js +96 -0
  40. package/lib/commands/datasource.js +77 -6
  41. package/lib/commands/dev-init.js +39 -1
  42. package/lib/commands/repair-auth-config.js +99 -0
  43. package/lib/commands/repair-datasource-keys.js +208 -0
  44. package/lib/commands/repair-datasource.js +235 -0
  45. package/lib/commands/repair-env-template.js +348 -0
  46. package/lib/commands/repair-internal.js +85 -0
  47. package/lib/commands/repair-rbac.js +158 -0
  48. package/lib/commands/repair.js +518 -0
  49. package/lib/commands/secrets-set.js +6 -0
  50. package/lib/commands/test-e2e-external.js +165 -0
  51. package/lib/commands/up-dataplane.js +90 -6
  52. package/lib/commands/upload.js +71 -40
  53. package/lib/commands/wizard-core-helpers.js +230 -5
  54. package/lib/commands/wizard-core.js +68 -29
  55. package/lib/commands/wizard-dataplane.js +1 -1
  56. package/lib/commands/wizard-entity-selection.js +43 -0
  57. package/lib/commands/wizard-headless.js +49 -5
  58. package/lib/commands/wizard-helpers.js +7 -3
  59. package/lib/commands/wizard.js +93 -64
  60. package/lib/core/config.js +7 -1
  61. package/lib/core/secrets.js +33 -12
  62. package/lib/datasource/deploy.js +12 -3
  63. package/lib/datasource/test-e2e.js +219 -0
  64. package/lib/datasource/test-integration.js +154 -0
  65. package/lib/deployment/deployer.js +7 -5
  66. package/lib/external-system/download-helpers.js +3 -1
  67. package/lib/external-system/download.js +182 -204
  68. package/lib/external-system/generator.js +204 -56
  69. package/lib/external-system/test-execution.js +2 -1
  70. package/lib/external-system/test-system-level.js +73 -0
  71. package/lib/external-system/test.js +51 -18
  72. package/lib/generator/external-controller-manifest.js +29 -2
  73. package/lib/generator/external-schema-utils.js +4 -2
  74. package/lib/generator/external.js +10 -3
  75. package/lib/generator/index.js +4 -1
  76. package/lib/generator/split-readme.js +1 -0
  77. package/lib/generator/split-variables.js +7 -1
  78. package/lib/generator/split.js +194 -54
  79. package/lib/generator/wizard-prompts-secondary.js +326 -0
  80. package/lib/generator/wizard-prompts.js +105 -106
  81. package/lib/generator/wizard-readme.js +91 -0
  82. package/lib/generator/wizard.js +180 -179
  83. package/lib/infrastructure/compose.js +11 -1
  84. package/lib/infrastructure/index.js +11 -3
  85. package/lib/infrastructure/services.js +22 -11
  86. package/lib/schema/application-schema.json +8 -5
  87. package/lib/schema/external-datasource.schema.json +49 -26
  88. package/lib/schema/external-system.schema.json +82 -6
  89. package/lib/schema/wizard-config.schema.json +23 -1
  90. package/lib/utils/api.js +38 -10
  91. package/lib/utils/auth-headers.js +8 -7
  92. package/lib/utils/compose-generator.js +1 -1
  93. package/lib/utils/compose-handlebars-helpers.js +11 -0
  94. package/lib/utils/config-format-preference.js +51 -0
  95. package/lib/utils/config-format.js +36 -0
  96. package/lib/utils/configuration-env-resolver.js +179 -0
  97. package/lib/utils/credential-display.js +83 -0
  98. package/lib/utils/credential-secrets-env.js +115 -25
  99. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  100. package/lib/utils/deployment-validation-helpers.js +4 -4
  101. package/lib/utils/dev-ca-install.js +139 -0
  102. package/lib/utils/env-copy.js +23 -3
  103. package/lib/utils/error-formatters/http-status-errors.js +0 -1
  104. package/lib/utils/error-formatters/permission-errors.js +0 -1
  105. package/lib/utils/error-formatters/validation-errors.js +0 -1
  106. package/lib/utils/external-readme.js +89 -30
  107. package/lib/utils/external-system-display.js +59 -1
  108. package/lib/utils/external-system-test-helpers.js +21 -8
  109. package/lib/utils/external-system-validators.js +3 -0
  110. package/lib/utils/file-upload.js +20 -50
  111. package/lib/utils/help-builder.js +1 -0
  112. package/lib/utils/infra-status.js +50 -44
  113. package/lib/utils/local-secrets.js +5 -5
  114. package/lib/utils/paths.js +85 -4
  115. package/lib/utils/secrets-canonical.js +93 -0
  116. package/lib/utils/secrets-generator.js +20 -0
  117. package/lib/utils/secrets-helpers.js +75 -89
  118. package/lib/utils/test-log-writer.js +56 -0
  119. package/lib/utils/token-manager.js +24 -32
  120. package/lib/validation/env-template-auth.js +157 -0
  121. package/lib/validation/env-template-kv.js +41 -0
  122. package/lib/validation/external-manifest-validator.js +25 -0
  123. package/lib/validation/external-system-auth-rules.js +86 -0
  124. package/lib/validation/validate-batch.js +149 -0
  125. package/lib/validation/validate-datasource-keys-api.js +33 -0
  126. package/lib/validation/validate-display.js +94 -16
  127. package/lib/validation/validate.js +25 -12
  128. package/lib/validation/validator.js +7 -9
  129. package/lib/validation/wizard-datasource-validation.js +50 -0
  130. package/package.json +7 -2
  131. package/templates/applications/dataplane/application.yaml +1 -1
  132. package/templates/applications/dataplane/env.template +5 -5
  133. package/templates/applications/dataplane/rbac.yaml +2 -2
  134. package/templates/applications/miso-controller/env.template +1 -1
  135. package/templates/external-system/README.md.hbs +75 -22
  136. package/templates/external-system/deploy.js.hbs +4 -2
  137. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  138. package/templates/external-system/external-system.json.hbs +1 -18
  139. package/templates/infra/compose.yaml.hbs +6 -0
  140. package/templates/python/docker-compose.hbs +4 -4
  141. package/templates/typescript/docker-compose.hbs +4 -4
  142. package/integration/hubspot/application.yaml +0 -37
@@ -3,12 +3,13 @@
3
3
  * @author AI Fabrix Team
4
4
  * @version 2.0.0
5
5
  */
6
+ /* eslint-disable max-lines */
6
7
 
7
8
  const chalk = require('chalk');
8
9
  const logger = require('../utils/logger');
9
10
  const {
10
11
  promptForMode,
11
- promptForSystemIdOrKey,
12
+ promptForExistingSystem,
12
13
  promptForSourceType,
13
14
  promptForOpenApiFile,
14
15
  promptForOpenApiUrl,
@@ -24,45 +25,30 @@ const {
24
25
  const {
25
26
  validateAndCheckAppDirectory,
26
27
  formatDataplaneRejectedTokenMessage,
28
+ extractSessionId,
27
29
  handleOpenApiParsing,
28
30
  handleCredentialSelection,
29
31
  handleTypeDetection,
32
+ handleEntitySelection,
30
33
  handleConfigurationGeneration,
31
34
  validateWizardConfiguration,
32
35
  handleFileSaving,
33
- setupDataplaneAndAuth
36
+ setupDataplaneAndAuth,
37
+ resolveCredentialConfig,
38
+ fetchSystemsListForAddDatasource,
39
+ resolveExternalSystemForAddDatasource
34
40
  } = require('./wizard-core');
35
41
  const { handleWizardHeadless } = require('./wizard-headless');
36
- const { createWizardSession, updateWizardSession, getWizardPlatforms } = require('../api/wizard.api');
37
- const { getExternalSystem } = require('../api/external-systems.api');
42
+ const { createWizardSession, updateWizardSession, getWizardPlatforms, getPreview } = require('../api/wizard.api');
38
43
  const { writeWizardConfig, wizardConfigExists, validateWizardConfig } = require('../validation/wizard-config-validator');
39
44
  const { appendWizardError } = require('../utils/cli-utils');
40
45
  const {
41
46
  buildPreferencesForSave,
42
47
  buildWizardStateForSave,
43
48
  showWizardConfigSummary,
44
- ensureIntegrationDir,
45
- isExternalSystemForAddDatasource
49
+ ensureIntegrationDir
46
50
  } = require('./wizard-helpers');
47
-
48
- /**
49
- * Extract session ID from response data
50
- * @function extractSessionId
51
- * @param {Object} responseData - Response data from API
52
- * @returns {string} Session ID
53
- * @throws {Error} If session ID not found or invalid
54
- */
55
- function extractSessionId(responseData) {
56
- let sessionId = responseData?.data?.sessionId || responseData?.sessionId ||
57
- responseData?.data?.session_id || responseData?.session_id;
58
- if (sessionId && typeof sessionId === 'object') {
59
- sessionId = sessionId.id || sessionId.sessionId || sessionId.session_id;
60
- }
61
- if (!sessionId || typeof sessionId !== 'string') {
62
- throw new Error(`Session ID not found: ${JSON.stringify(responseData, null, 2)}`);
63
- }
64
- return sessionId;
65
- }
51
+ const { humanizeAppKey } = require('../generator/wizard-prompts-secondary');
66
52
 
67
53
  /**
68
54
  * Create wizard session with given mode and optional systemIdOrKey (no prompts)
@@ -151,7 +137,8 @@ async function handleInteractiveConfigGeneration(options) {
151
137
  fieldOnboardingLevel: preferences.fieldOnboardingLevel || 'full',
152
138
  enableMCP: preferences.mcp,
153
139
  enableABAC: preferences.abac,
154
- enableRBAC: preferences.rbac
140
+ enableRBAC: preferences.rbac,
141
+ debug: options.debug === true
155
142
  };
156
143
 
157
144
  const result = await handleConfigurationGeneration(options.dataplaneUrl, options.authConfig, {
@@ -160,28 +147,52 @@ async function handleInteractiveConfigGeneration(options) {
160
147
  detectedType: options.detectedType,
161
148
  configPrefs,
162
149
  credentialIdOrKey: options.credentialIdOrKey,
163
- systemIdOrKey: options.systemIdOrKey
150
+ systemIdOrKey: options.systemIdOrKey,
151
+ sourceType: options.sourceType,
152
+ platformKey: options.sourceType === 'known-platform' ? options.sourceData : undefined,
153
+ entityName: options.entityName,
154
+ appName: options.appName,
155
+ systemDisplayName: options.systemDisplayName
164
156
  });
165
157
 
166
158
  return {
167
159
  ...result,
168
- preferences: buildPreferencesForSave(userIntent, preferences)
160
+ preferences: buildPreferencesForSave(userIntent, preferences, { debug: options.debug })
169
161
  };
170
162
  }
171
163
 
172
164
  /**
173
165
  * Handle configuration review and validation step (interactive mode only)
166
+ * Fetches preview summary from dataplane; falls back to YAML dump if preview unavailable.
174
167
  * @async
175
168
  * @function handleConfigurationReview
176
169
  * @param {string} dataplaneUrl - Dataplane URL
177
170
  * @param {Object} authConfig - Authentication configuration
171
+ * @param {string} sessionId - Wizard session ID
178
172
  * @param {Object} systemConfig - System configuration
179
173
  * @param {Object[]} datasourceConfigs - Datasource configurations
180
- * @returns {Promise<Object>} Final configurations
174
+ * @param {Object} [opts] - Optional options
175
+ * @param {string} [opts.appKey] - App key for debug manifest path
176
+ * @param {boolean} [opts.debug] - When true, save debug manifest on validation failure
177
+ * @returns {Promise<Object|null>} Final configurations or null if cancelled
181
178
  */
182
- async function handleConfigurationReview(dataplaneUrl, authConfig, systemConfig, datasourceConfigs) {
179
+ async function handleConfigurationReview(dataplaneUrl, authConfig, sessionId, systemConfig, datasourceConfigs, opts = {}) {
183
180
  logger.log(chalk.blue('\n\uD83D\uDCCB Step 6-7: Review & Validate'));
184
- const reviewResult = await promptForConfigReview(systemConfig, datasourceConfigs);
181
+
182
+ let preview = null;
183
+ try {
184
+ const previewResponse = await getPreview(dataplaneUrl, sessionId, authConfig);
185
+ if (previewResponse?.success && previewResponse?.data) {
186
+ preview = previewResponse.data;
187
+ }
188
+ } catch {
189
+ // Fall back to YAML display
190
+ }
191
+ if (!preview) {
192
+ logger.warn('Preview unavailable, showing full configuration.');
193
+ }
194
+
195
+ const reviewResult = await promptForConfigReview({ preview, systemConfig, datasourceConfigs, appKey: opts.appKey });
185
196
 
186
197
  if (reviewResult.action === 'cancel') {
187
198
  logger.log(chalk.yellow('Wizard cancelled.'));
@@ -191,7 +202,10 @@ async function handleConfigurationReview(dataplaneUrl, authConfig, systemConfig,
191
202
  const finalSystemConfig = reviewResult.systemConfig || systemConfig;
192
203
  const finalDatasourceConfigs = reviewResult.datasourceConfigs || datasourceConfigs;
193
204
 
194
- await validateWizardConfiguration(dataplaneUrl, authConfig, finalSystemConfig, finalDatasourceConfigs);
205
+ await validateWizardConfiguration(dataplaneUrl, authConfig, finalSystemConfig, finalDatasourceConfigs, {
206
+ debug: opts.debug,
207
+ appName: opts.appKey
208
+ });
195
209
 
196
210
  return { systemConfig: finalSystemConfig, datasourceConfigs: finalDatasourceConfigs };
197
211
  }
@@ -208,7 +222,7 @@ async function handleConfigurationReview(dataplaneUrl, authConfig, systemConfig,
208
222
  * @returns {Promise<Object>} Collected state (source, credential, preferences) for wizard.yaml save
209
223
  */
210
224
  async function doWizardSteps(appKey, dataplaneUrl, authConfig, sessionId, flowOpts, state) {
211
- const { mode, systemIdOrKey, platforms } = flowOpts;
225
+ const { mode, systemIdOrKey, platforms, debug } = flowOpts;
212
226
  const { sourceType, sourceData } = await handleInteractiveSourceSelection(
213
227
  dataplaneUrl, sessionId, authConfig, platforms
214
228
  );
@@ -220,13 +234,13 @@ async function doWizardSteps(appKey, dataplaneUrl, authConfig, sessionId, flowOp
220
234
 
221
235
  const openapiSpec = await handleOpenApiParsing(dataplaneUrl, authConfig, sourceType, sourceData);
222
236
  const credentialAction = await promptForCredentialAction();
223
- const configCredential = credentialAction.action === 'skip'
224
- ? { action: 'skip' }
225
- : { action: credentialAction.action, credentialIdOrKey: credentialAction.credentialIdOrKey };
237
+ const configCredential = await resolveCredentialConfig(dataplaneUrl, authConfig, credentialAction);
226
238
  state.credential = configCredential;
227
239
  const credentialIdOrKey = await handleCredentialSelection(dataplaneUrl, authConfig, configCredential);
228
240
 
229
241
  const detectedType = await handleTypeDetection(dataplaneUrl, authConfig, openapiSpec);
242
+ const entityName = openapiSpec && sourceType !== 'known-platform'
243
+ ? await handleEntitySelection(dataplaneUrl, authConfig, openapiSpec) : null;
230
244
  const genResult = await handleInteractiveConfigGeneration({
231
245
  dataplaneUrl,
232
246
  authConfig,
@@ -234,12 +248,25 @@ async function doWizardSteps(appKey, dataplaneUrl, authConfig, sessionId, flowOp
234
248
  openapiSpec,
235
249
  detectedType,
236
250
  credentialIdOrKey,
237
- systemIdOrKey
251
+ systemIdOrKey,
252
+ sourceType,
253
+ sourceData,
254
+ entityName: entityName || undefined,
255
+ appName: appKey,
256
+ debug,
257
+ systemDisplayName: flowOpts.systemDisplayName
238
258
  });
239
259
  const { systemConfig, datasourceConfigs, systemKey, preferences: savedPrefs } = genResult;
240
260
  state.preferences = savedPrefs || {};
241
261
 
242
- const finalConfigs = await handleConfigurationReview(dataplaneUrl, authConfig, systemConfig, datasourceConfigs);
262
+ const finalConfigs = await handleConfigurationReview(
263
+ dataplaneUrl,
264
+ authConfig,
265
+ sessionId,
266
+ systemConfig,
267
+ datasourceConfigs,
268
+ { appKey, debug }
269
+ );
243
270
  if (!finalConfigs) return null;
244
271
 
245
272
  await handleFileSaving(
@@ -290,18 +317,23 @@ async function runWizardStepsAfterSession(appKey, dataplaneUrl, authConfig, sess
290
317
  * @returns {Promise<void>} Resolves when wizard flow completes
291
318
  */
292
319
  async function executeWizardFlow(appKey, dataplaneUrl, authConfig, flowOpts = {}) {
293
- const { mode, systemIdOrKey, configPath } = flowOpts;
320
+ const { mode, systemIdOrKey, configPath, debug, systemDisplayName } = flowOpts;
294
321
 
322
+ if (debug) {
323
+ logger.log(chalk.gray(`[DEBUG] Wizard debug mode enabled for app: ${appKey}`));
324
+ }
295
325
  logger.log(chalk.blue('\n\uD83D\uDCCB Step 1: Create Session'));
296
326
  const sessionId = await createSessionFromParams(dataplaneUrl, authConfig, mode, systemIdOrKey, appKey);
297
327
  logger.log(chalk.green('\u2713 Session created'));
298
328
 
299
- const platforms = await getWizardPlatforms(dataplaneUrl, authConfig);
329
+ const platforms = mode === 'add-datasource' ? [] : await getWizardPlatforms(dataplaneUrl, authConfig);
300
330
  const state = await runWizardStepsAfterSession(appKey, dataplaneUrl, authConfig, sessionId, {
301
331
  mode,
302
332
  systemIdOrKey,
303
333
  platforms,
304
- configPath
334
+ configPath,
335
+ debug: flowOpts.debug,
336
+ systemDisplayName
305
337
  });
306
338
  if (!state) return;
307
339
 
@@ -362,26 +394,15 @@ async function resolveCreateNewPath(options, loadedConfig) {
362
394
  * @returns {Promise<Object>} { appKey, configPath, dataplaneUrl, authConfig, systemIdOrKey }
363
395
  */
364
396
  async function resolveAddDatasourcePath(options, loadedConfig) {
365
- let systemIdOrKey = loadedConfig?.systemIdOrKey || (await promptForSystemIdOrKey(loadedConfig?.systemIdOrKey));
366
- const { dataplaneUrl, authConfig } = await setupDataplaneAndAuth(options, systemIdOrKey || 'wizard');
367
- let systemResponse;
368
- for (;;) {
369
- try {
370
- systemResponse = await getExternalSystem(dataplaneUrl, systemIdOrKey, authConfig);
371
- const sys = systemResponse?.data || systemResponse;
372
- if (sys && (systemResponse?.data || systemResponse?.success)) {
373
- if (!isExternalSystemForAddDatasource(sys)) {
374
- logger.log(chalk.red('Cannot add datasource to a webapp. Please enter an external system ID or key.'));
375
- systemIdOrKey = await promptForSystemIdOrKey(systemIdOrKey);
376
- continue;
377
- }
378
- break;
379
- }
380
- } catch (err) {
381
- logger.log(chalk.red(`System not found or error: ${err.message}`));
382
- }
383
- systemIdOrKey = await promptForSystemIdOrKey(systemIdOrKey);
384
- }
397
+ const { dataplaneUrl, authConfig } = await setupDataplaneAndAuth(
398
+ options,
399
+ loadedConfig?.systemIdOrKey || loadedConfig?.appKey || 'wizard'
400
+ );
401
+ const systemsList = await fetchSystemsListForAddDatasource(dataplaneUrl, authConfig);
402
+ const initialSystemIdOrKey = loadedConfig?.systemIdOrKey || (await promptForExistingSystem(systemsList, loadedConfig?.systemIdOrKey));
403
+ const { systemResponse, systemIdOrKey } = await resolveExternalSystemForAddDatasource(
404
+ dataplaneUrl, authConfig, systemsList, initialSystemIdOrKey
405
+ );
385
406
  const sys = systemResponse?.data || systemResponse;
386
407
  const appKey = sys?.key || sys?.systemKey || systemIdOrKey;
387
408
  const configPath = await ensureIntegrationDir(appKey);
@@ -449,15 +470,24 @@ async function handleWizardWithSavedConfig(options, loadedConfig, displayPath) {
449
470
  }
450
471
 
451
472
  async function handleWizardInteractive(options) {
452
- const mode = await promptForMode();
473
+ const allowAddDatasource = !options.app;
474
+ const mode = allowAddDatasource ? await promptForMode(undefined, true) : 'create-system';
453
475
  const resolved = mode === 'create-system'
454
476
  ? await resolveCreateNewPath(options, null)
455
477
  : await resolveAddDatasourcePath(options, null);
456
478
  if (!resolved) return;
457
479
  const { appKey, configPath, dataplaneUrl, authConfig } = resolved;
458
480
  const systemIdOrKey = mode === 'add-datasource' ? resolved.systemIdOrKey : undefined;
481
+ const systemDisplayName = options.systemDisplayName || options.displayName ||
482
+ (mode === 'create-system' ? humanizeAppKey(appKey) : undefined);
459
483
  try {
460
- await executeWizardFlow(appKey, dataplaneUrl, authConfig, { mode, systemIdOrKey, configPath });
484
+ await executeWizardFlow(appKey, dataplaneUrl, authConfig, {
485
+ mode,
486
+ systemIdOrKey,
487
+ configPath,
488
+ debug: options.debug,
489
+ systemDisplayName
490
+ });
461
491
  logger.log(chalk.gray(`To change settings, edit integration/${appKey}/wizard.yaml and run: aifabrix wizard ${appKey}`));
462
492
  } catch (error) {
463
493
  await handleWizardError(appKey, configPath, mode, systemIdOrKey, error);
@@ -480,5 +510,4 @@ async function handleWizard(options = {}) {
480
510
  }
481
511
  return await handleWizardInteractive(options);
482
512
  }
483
-
484
513
  module.exports = { handleWizard, handleWizardHeadless };
@@ -126,7 +126,8 @@ function getDefaultConfig() {
126
126
  environment: 'dev',
127
127
  controller: undefined,
128
128
  environments: {},
129
- device: {}
129
+ device: {},
130
+ format: undefined
130
131
  };
131
132
  }
132
133
 
@@ -489,4 +490,9 @@ const { createPathConfigFunctions } = require('../utils/config-paths');
489
490
  const pathConfigFunctions = createPathConfigFunctions(getConfig, saveConfig);
490
491
  Object.assign(exportsObj, pathConfigFunctions);
491
492
 
493
+ // Format preference functions
494
+ const { createFormatFunctions } = require('../utils/config-format-preference');
495
+ const formatFunctions = createFormatFunctions(getConfig, saveConfig);
496
+ Object.assign(exportsObj, formatFunctions);
497
+
492
498
  module.exports = exportsObj;
@@ -206,6 +206,38 @@ async function loadMergedConfigAndUserSecrets() {
206
206
  }
207
207
  }
208
208
 
209
+ /**
210
+ * Loads merged secrets using config/user cascade, builder file merge, and default fallback.
211
+ * @async
212
+ * @returns {Promise<Object>} Merged secrets object (not decrypted)
213
+ */
214
+ async function loadSecretsWithFallbacks() {
215
+ let merged = await loadMergedConfigAndUserSecrets();
216
+ if (!merged || Object.keys(merged).length === 0) {
217
+ merged = loadPrimaryUserSecrets();
218
+ if (Object.keys(merged).length === 0) {
219
+ merged = loadUserSecrets();
220
+ }
221
+ merged = await applyCanonicalSecretsOverride(merged);
222
+ }
223
+ try {
224
+ const projectRoot = pathsUtil.getProjectRoot();
225
+ if (projectRoot) {
226
+ const builderPath = path.join(projectRoot, 'builder', 'secrets.local.yaml');
227
+ if (fs.existsSync(builderPath)) {
228
+ const builderSecrets = mergeUserWithConfigFile(merged || {}, builderPath);
229
+ if (builderSecrets) merged = builderSecrets;
230
+ }
231
+ }
232
+ } catch {
233
+ // Ignore (e.g. no project root or read error)
234
+ }
235
+ if (Object.keys(merged).length === 0) {
236
+ merged = loadDefaultSecrets();
237
+ }
238
+ return merged;
239
+ }
240
+
209
241
  async function loadSecrets(secretsPath, _appName) {
210
242
  if (secretsPath) {
211
243
  const resolvedPath = resolveSecretsPath(secretsPath);
@@ -218,18 +250,7 @@ async function loadSecrets(secretsPath, _appName) {
218
250
  }
219
251
  return await decryptSecretsObject(explicitSecrets);
220
252
  }
221
-
222
- let mergedSecrets = await loadMergedConfigAndUserSecrets();
223
- if (!mergedSecrets || Object.keys(mergedSecrets).length === 0) {
224
- mergedSecrets = loadPrimaryUserSecrets();
225
- if (Object.keys(mergedSecrets).length === 0) {
226
- mergedSecrets = loadUserSecrets();
227
- }
228
- mergedSecrets = await applyCanonicalSecretsOverride(mergedSecrets);
229
- }
230
- if (Object.keys(mergedSecrets).length === 0) {
231
- mergedSecrets = loadDefaultSecrets();
232
- }
253
+ const mergedSecrets = await loadSecretsWithFallbacks();
233
254
  ensureNonEmptySecrets(mergedSecrets);
234
255
  return await decryptSecretsObject(mergedSecrets);
235
256
  }
@@ -16,6 +16,11 @@ const { getEnvironmentApplication } = require('../api/environments.api');
16
16
  const { publishDatasourceViaPipeline } = require('../api/pipeline.api');
17
17
  const { formatApiError } = require('../utils/api-error-handler');
18
18
  const logger = require('../utils/logger');
19
+ const { logDataplanePipelineWarning } = require('../utils/dataplane-pipeline-warning');
20
+ const {
21
+ buildResolvedEnvMapForIntegration,
22
+ resolveConfigurationValues
23
+ } = require('../utils/configuration-env-resolver');
19
24
  const { validateDatasourceFile } = require('./validate');
20
25
 
21
26
  /**
@@ -50,7 +55,7 @@ async function getDataplaneUrl(controllerUrl, appKey, environment, authConfig) {
50
55
  if (!dataplaneUrl) {
51
56
  const appType = application.configuration?.type || application.type;
52
57
  if (appType === 'external') {
53
- throw new Error('Dataplane URL not found for external system. Provide --dataplane <url>.');
58
+ throw new Error('Dataplane URL not found for external system in application configuration.');
54
59
  }
55
60
  throw new Error('Dataplane URL not found in application configuration');
56
61
  }
@@ -107,8 +112,6 @@ async function validateAndLoadDatasourceFile(filePath) {
107
112
  * @param {string} controllerUrl - Controller URL
108
113
  * @param {string} environment - Environment key
109
114
  * @param {string} appKey - Application key
110
- * @param {Object} [options] - Options
111
- * @param {string} [options.dataplane] - Dataplane URL override
112
115
  * @returns {Promise<Object>} Object with authConfig and dataplaneUrl
113
116
  */
114
117
  async function setupDeploymentAuth(controllerUrl, environment, appKey) {
@@ -154,6 +157,7 @@ async function setupDeploymentAuth(controllerUrl, environment, appKey) {
154
157
  async function publishDatasourceToDataplane(dataplaneUrl, systemKey, authConfig, datasourceConfig) {
155
158
  requireBearerForDataplanePipeline(authConfig);
156
159
  logger.log(chalk.blue('\n🚀 Publishing datasource to dataplane...'));
160
+ logDataplanePipelineWarning();
157
161
 
158
162
  const publishResponse = await publishDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig, datasourceConfig);
159
163
 
@@ -228,6 +232,11 @@ async function deployDatasource(appKey, filePath, _options) {
228
232
  throw new Error('systemKey is required in datasource configuration');
229
233
  }
230
234
 
235
+ if (Array.isArray(datasourceConfig.configuration) && datasourceConfig.configuration.length > 0) {
236
+ const { envMap, secrets } = await buildResolvedEnvMapForIntegration(systemKey);
237
+ resolveConfigurationValues(datasourceConfig.configuration, envMap, secrets, systemKey);
238
+ }
239
+
231
240
  // Setup authentication and get dataplane URL
232
241
  const { authConfig, dataplaneUrl } = await setupDeploymentAuth(controllerUrl, environment, appKey);
233
242
 
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Datasource E2E test - run full E2E test via dataplane external API
3
+ * @fileoverview Datasource E2E test logic (config, credential, sync, data, CIP)
4
+ * @author AI Fabrix Team
5
+ * @version 2.0.0
6
+ */
7
+ /* eslint-disable max-statements -- Auth setup, API call, polling, debug log */
8
+
9
+ const path = require('path');
10
+ const fs = require('fs').promises;
11
+ const chalk = require('chalk');
12
+ const logger = require('../utils/logger');
13
+ const { getIntegrationPath, resolveIntegrationAppKeyFromCwd } = require('../utils/paths');
14
+ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
15
+ const { resolveControllerUrl } = require('../utils/controller-url');
16
+ const { getDeviceOnlyAuth } = require('../utils/token-manager');
17
+ const { testDatasourceE2E, getE2ETestRun } = require('../api/external-test.api');
18
+ const { writeTestLog } = require('../utils/test-log-writer');
19
+
20
+ const DEFAULT_POLL_INTERVAL_MS = 2500;
21
+ const DEFAULT_POLL_TIMEOUT_MS = 15 * 60 * 1000;
22
+
23
+ /**
24
+ * Resolve appKey for datasource test-e2e
25
+ * @param {string} [appKey] - Explicit app key from --app
26
+ * @returns {string}
27
+ */
28
+ function resolveAppKey(appKey) {
29
+ if (appKey) return appKey;
30
+ const fromCwd = resolveIntegrationAppKeyFromCwd();
31
+ if (fromCwd) return fromCwd;
32
+ throw new Error(
33
+ 'Could not determine app context. Use --app <appKey> or run from integration/<appKey>/ directory.'
34
+ );
35
+ }
36
+
37
+ /**
38
+ * Resolve primaryKeyValue for request body: string as-is, or read and parse JSON from @path
39
+ * @param {string} [value] - Literal value or path prefixed with @ (e.g. @pk.json)
40
+ * @returns {Promise<string|Object|null>} Resolved value for body.primaryKeyValue, or null if absent
41
+ */
42
+ async function resolvePrimaryKeyValue(value) {
43
+ if (value === null || value === undefined || value === '') return null;
44
+ const str = String(value).trim();
45
+ if (str.startsWith('@')) {
46
+ const filePath = path.resolve(str.slice(1).trim());
47
+ const content = await fs.readFile(filePath, 'utf8');
48
+ return JSON.parse(content);
49
+ }
50
+ return str;
51
+ }
52
+
53
+ /**
54
+ * Build E2E request body from options
55
+ * @param {Object} options - Command options
56
+ * @returns {Promise<Object>} Request body
57
+ */
58
+ async function buildE2EBody(options) {
59
+ const body = {};
60
+ if (options.debug) body.includeDebug = true;
61
+ if (options.testCrud === true) body.testCrud = true;
62
+ if (options.recordId !== undefined && options.recordId !== null && options.recordId !== '') body.recordId = String(options.recordId);
63
+ if (options.cleanup === false) body.cleanup = false;
64
+ else if (options.cleanup === true) body.cleanup = true;
65
+ const pk = await resolvePrimaryKeyValue(options.primaryKeyValue);
66
+ if (pk !== null && pk !== undefined) body.primaryKeyValue = pk;
67
+ return body;
68
+ }
69
+
70
+ /**
71
+ * Poll E2E test run until completed or failed
72
+ * @param {string} dataplaneUrl - Dataplane URL
73
+ * @param {string} sourceIdOrKey - Source ID or key
74
+ * @param {string} testRunId - Test run ID
75
+ * @param {Object} authConfig - Auth config
76
+ * @param {Object} opts - Poll options
77
+ * @param {number} [opts.intervalMs] - Poll interval (ms)
78
+ * @param {number} [opts.timeoutMs] - Max wait (ms)
79
+ * @param {boolean} [opts.verbose] - Log each poll
80
+ * @returns {Promise<Object>} Final poll result (status completed or failed)
81
+ */
82
+ async function pollE2ETestRun(dataplaneUrl, sourceIdOrKey, testRunId, authConfig, opts = {}) {
83
+ const intervalMs = opts.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
84
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
85
+ const verbose = opts.verbose === true;
86
+ const deadline = Date.now() + timeoutMs;
87
+ let last;
88
+ while (Date.now() < deadline) {
89
+ last = await getE2ETestRun(dataplaneUrl, sourceIdOrKey, testRunId, authConfig);
90
+ if (last.status === 'completed' || last.status === 'failed') {
91
+ return last;
92
+ }
93
+ if (verbose) {
94
+ const steps = last.completedActions || [];
95
+ logger.log(chalk.gray(` Polling… status: ${last.status}, ${steps.length} step(s) completed`));
96
+ }
97
+ await new Promise(r => setTimeout(r, intervalMs));
98
+ }
99
+ throw new Error(
100
+ `E2E test run did not complete within ${timeoutMs / 1000}s (run ID: ${testRunId})`
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Run E2E test for one datasource (Bearer token or API key required; no client credentials).
106
+ * Default: async start + polling until completed/failed. Use options.async === false for sync.
107
+ *
108
+ * @async
109
+ * @param {string} datasourceKey - Datasource key (used as sourceIdOrKey)
110
+ * @param {Object} options - Options
111
+ * @param {string} [options.app] - App key (or resolve from cwd)
112
+ * @param {string} [options.environment] - Environment (dev, tst, pro)
113
+ * @param {boolean} [options.debug] - Include debug, write log file
114
+ * @param {boolean} [options.verbose] - Verbose output (e.g. poll progress)
115
+ * @param {boolean} [options.async] - If false, use sync mode (no polling). Default true.
116
+ * @param {boolean} [options.testCrud] - Set body testCrud true
117
+ * @param {string} [options.recordId] - Set body recordId
118
+ * @param {boolean} [options.cleanup] - Set body cleanup (default true)
119
+ * @param {string} [options.primaryKeyValue] - Set body primaryKeyValue (string or @path to JSON)
120
+ * @param {number} [options.pollIntervalMs] - Poll interval in ms (default 2500)
121
+ * @param {number} [options.pollTimeoutMs] - Poll timeout in ms (default 15 min)
122
+ * @returns {Promise<Object>} E2E test result (steps, success, error, etc.)
123
+ */
124
+ async function runDatasourceTestE2E(datasourceKey, options = {}) {
125
+ if (!datasourceKey || typeof datasourceKey !== 'string') {
126
+ throw new Error('Datasource key is required');
127
+ }
128
+ const appKey = resolveAppKey(options.app);
129
+ const controllerUrl = await resolveControllerUrl();
130
+ const { resolveEnvironment } = require('../core/config');
131
+ const environment = options.environment || await resolveEnvironment();
132
+ const authConfig = await getDeviceOnlyAuth(controllerUrl);
133
+ const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
134
+
135
+ logger.log(chalk.blue(`\n🧪 Running E2E test for datasource: ${datasourceKey}`));
136
+
137
+ const body = await buildE2EBody(options);
138
+ const useAsync = options.async !== false;
139
+ const requestMeta = {
140
+ sourceIdOrKey: datasourceKey,
141
+ includeDebug: options.debug,
142
+ testCrud: options.testCrud,
143
+ recordId: options.recordId,
144
+ cleanup: options.cleanup,
145
+ primaryKeyValue: options.primaryKeyValue !== undefined && options.primaryKeyValue !== null
146
+ };
147
+
148
+ const execOpts = {
149
+ dataplaneUrl,
150
+ datasourceKey,
151
+ authConfig,
152
+ body,
153
+ useAsync,
154
+ verbose: options.verbose,
155
+ pollIntervalMs: options.pollIntervalMs,
156
+ pollTimeoutMs: options.pollTimeoutMs
157
+ };
158
+ let data;
159
+ try {
160
+ data = await executeE2EWithOptionalPoll(execOpts);
161
+ } catch (error) {
162
+ if (options.debug) {
163
+ const appPath = getIntegrationPath(appKey);
164
+ const integrationDir = path.dirname(appPath);
165
+ await writeTestLog(appKey, { request: requestMeta, error: error.message }, 'test-e2e', integrationDir);
166
+ }
167
+ throw error;
168
+ }
169
+
170
+ if (options.debug) {
171
+ const appPath = getIntegrationPath(appKey);
172
+ const integrationDir = path.dirname(appPath);
173
+ const logPath = await writeTestLog(appKey, { request: requestMeta, response: data }, 'test-e2e', integrationDir);
174
+ logger.log(chalk.gray(` Debug log: ${logPath}`));
175
+ }
176
+
177
+ return data;
178
+ }
179
+
180
+ /**
181
+ * Call E2E API and optionally poll until completed. On throw, caller should log if debug.
182
+ * @param {Object} opts - Options
183
+ * @param {string} opts.dataplaneUrl - Dataplane URL
184
+ * @param {string} opts.datasourceKey - Source key
185
+ * @param {Object} opts.authConfig - Auth config
186
+ * @param {Object} opts.body - Request body
187
+ * @param {boolean} opts.useAsync - Whether to use async + poll
188
+ * @param {boolean} opts.verbose - Verbose poll progress
189
+ * @param {number} [opts.pollIntervalMs] - Override poll interval (ms)
190
+ * @param {number} [opts.pollTimeoutMs] - Override poll timeout (ms)
191
+ * @returns {Promise<Object>} Final result data
192
+ */
193
+ /* eslint-disable-next-line max-params -- single opts object; destructuring in body */
194
+ async function executeE2EWithOptionalPoll(opts) {
195
+ const { dataplaneUrl, datasourceKey, authConfig, body, useAsync, verbose, pollIntervalMs, pollTimeoutMs } = opts;
196
+ const response = await testDatasourceE2E(dataplaneUrl, datasourceKey, authConfig, body, {
197
+ asyncRun: useAsync
198
+ });
199
+ let data = response.data || response;
200
+ if (useAsync && data !== null && data !== undefined && data.testRunId) {
201
+ data = await pollE2ETestRun(
202
+ dataplaneUrl,
203
+ datasourceKey,
204
+ data.testRunId,
205
+ authConfig,
206
+ {
207
+ intervalMs: pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
208
+ timeoutMs: pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
209
+ verbose
210
+ }
211
+ );
212
+ }
213
+ return data;
214
+ }
215
+
216
+ module.exports = {
217
+ runDatasourceTestE2E,
218
+ resolveAppKey
219
+ };