@aifabrix/builder 2.44.3 → 2.44.5

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 (72) hide show
  1. package/.npmrc.token +1 -1
  2. package/integration/roundtrip-test-local/README.md +1 -2
  3. package/integration/roundtrip-test-local2/README.md +1 -2
  4. package/jest.projects.js +31 -15
  5. package/lib/api/certificates.api.js +21 -3
  6. package/lib/api/types/wizard.types.js +2 -1
  7. package/lib/certification/post-unified-cert-sync.js +13 -2
  8. package/lib/certification/sync-after-external-command.js +6 -3
  9. package/lib/certification/sync-system-certification.js +60 -14
  10. package/lib/cli/setup-app.help.js +1 -1
  11. package/lib/cli/setup-app.test-commands.js +75 -39
  12. package/lib/cli/setup-infra.js +6 -2
  13. package/lib/cli/setup-utility.js +20 -1
  14. package/lib/commands/datasource-unified-test-cli.js +81 -46
  15. package/lib/commands/datasource-unified-test-cli.options.js +4 -2
  16. package/lib/commands/datasource.js +3 -31
  17. package/lib/commands/repair-datasource-keys.js +1 -1
  18. package/lib/commands/repair-datasource-openapi.js +57 -0
  19. package/lib/commands/repair-datasource.js +5 -0
  20. package/lib/commands/repair-internal.js +2 -4
  21. package/lib/commands/repair-rbac.js +25 -2
  22. package/lib/commands/repair.js +2 -19
  23. package/lib/commands/test-e2e-external.js +9 -9
  24. package/lib/commands/up-common.js +25 -0
  25. package/lib/commands/upload.js +18 -4
  26. package/lib/commands/wizard-core.js +53 -11
  27. package/lib/commands/wizard-dataplane.js +14 -6
  28. package/lib/commands/wizard-entity-selection.js +71 -14
  29. package/lib/commands/wizard-headless.js +5 -2
  30. package/lib/commands/wizard-helpers.js +13 -1
  31. package/lib/commands/wizard.js +208 -60
  32. package/lib/datasource/datasource-validate-display.js +162 -0
  33. package/lib/datasource/datasource-validate-summary.js +194 -0
  34. package/lib/datasource/test-e2e.js +65 -37
  35. package/lib/datasource/unified-validation-run-body.js +1 -2
  36. package/lib/datasource/validate.js +14 -6
  37. package/lib/external-system/test.js +12 -8
  38. package/lib/generator/external-controller-manifest.js +12 -2
  39. package/lib/generator/wizard-prompts.js +7 -1
  40. package/lib/generator/wizard.js +34 -0
  41. package/lib/schema/cip-capacity-display.fallback.json +7 -0
  42. package/lib/schema/datasource-test-run.schema.json +79 -1
  43. package/lib/schema/external-datasource.schema.json +94 -2
  44. package/lib/schema/flag-map-validation-run.json +1 -2
  45. package/lib/schema/type/document-storage.json +83 -3
  46. package/lib/schema/wizard-config.schema.json +1 -1
  47. package/lib/utils/configuration-env-resolver.js +38 -0
  48. package/lib/utils/dataplane-resolver.js +3 -2
  49. package/lib/utils/datasource-test-run-capacity-operations.js +149 -0
  50. package/lib/utils/datasource-test-run-debug-display.js +143 -1
  51. package/lib/utils/datasource-test-run-display.js +46 -33
  52. package/lib/utils/datasource-test-run-tty-log.js +6 -2
  53. package/lib/utils/datasource-test-run-tty-meta-lines.js +123 -0
  54. package/lib/utils/error-formatter.js +32 -2
  55. package/lib/utils/external-readme.js +47 -3
  56. package/lib/utils/external-system-readiness-core.js +39 -0
  57. package/lib/utils/external-system-readiness-deploy-display.js +2 -3
  58. package/lib/utils/external-system-readiness-display-internals.js +3 -2
  59. package/lib/utils/external-system-system-test-tty.js +33 -9
  60. package/lib/utils/external-system-validators.js +62 -5
  61. package/lib/utils/load-cip-capacity-display-config.js +130 -0
  62. package/lib/utils/paths.js +10 -3
  63. package/lib/utils/schema-resolver.js +98 -2
  64. package/lib/utils/urls-local-registry.js +52 -10
  65. package/lib/utils/validation-run-poll.js +15 -4
  66. package/lib/utils/validation-run-request.js +4 -6
  67. package/lib/validation/dimension-display-helpers.js +60 -0
  68. package/lib/validation/validate-display-log-helpers.js +39 -0
  69. package/lib/validation/validate-display.js +89 -83
  70. package/package.json +1 -1
  71. package/templates/applications/miso-controller/env.template +6 -6
  72. package/templates/external-system/README.md.hbs +58 -32
@@ -94,7 +94,7 @@ async function runDatasourceUnifiedTestOnceForWatch(datasourceKey, runOpts, disp
94
94
  try {
95
95
  const result = await runUnifiedDatasourceValidation(datasourceKey, runOpts);
96
96
  const exitCode = finalizeUnifiedValidationResult(result, displayOpts);
97
- await afterUnifiedValidationCertSync(exitCode, datasourceKey, certCliOptions, 'datasource test');
97
+ await afterUnifiedValidationCertSync(exitCode, datasourceKey, certCliOptions, 'datasource test', result.envelope);
98
98
  return {
99
99
  exitCode,
100
100
  envelope: result.envelope
@@ -121,7 +121,7 @@ async function datasourceTestCommandAction(datasourceKey, options) {
121
121
  const result = await runUnifiedDatasourceValidation(datasourceKey, runOpts);
122
122
  await writeDatasourceTestDebugLogIfRequested(appKey, datasourceKey, result, options);
123
123
  const exitCode = finalizeUnifiedValidationResult(result, displayOpts);
124
- await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test (watch)');
124
+ await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test (watch)', result.envelope);
125
125
  return {
126
126
  exitCode,
127
127
  envelope: result.envelope
@@ -140,7 +140,7 @@ async function datasourceTestCommandAction(datasourceKey, options) {
140
140
  }
141
141
  }
142
142
  const exitCode = finalizeUnifiedValidationResult(result, displayOpts);
143
- await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test');
143
+ await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test', result.envelope);
144
144
  process.exit(exitCode);
145
145
  } catch (error) {
146
146
  logger.error(formatBlockingError('Datasource test failed:'), error.message);
@@ -195,7 +195,13 @@ async function runIntegrationOnceForWatch(datasourceKey, integOpts, options, uni
195
195
  if (unifiedModes) {
196
196
  const uni = unifiedCliResultFromIntegrationReturn(result);
197
197
  const exitCode = finalizeUnifiedValidationResult(uni, unifiedDisplayOpts);
198
- await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test-integration (watch)');
198
+ await afterUnifiedValidationCertSync(
199
+ exitCode,
200
+ datasourceKey,
201
+ options,
202
+ 'datasource test-integration (watch)',
203
+ uni.envelope
204
+ );
199
205
  return { exitCode, envelope: uni.envelope };
200
206
  }
201
207
  displayIntegrationTestResults(
@@ -211,7 +217,13 @@ async function runIntegrationOnceForWatch(datasourceKey, integOpts, options, uni
211
217
  warningsAsErrors: unifiedDisplayOpts.warningsAsErrors === true,
212
218
  requireCert: unifiedDisplayOpts.requireCert === true
213
219
  });
214
- await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test-integration (watch)');
220
+ await afterUnifiedValidationCertSync(
221
+ exitCode,
222
+ datasourceKey,
223
+ options,
224
+ 'datasource test-integration (watch)',
225
+ result.datasourceTestRun || null
226
+ );
215
227
  return { exitCode, envelope: result.datasourceTestRun || null };
216
228
  } catch (err) {
217
229
  logger.error(formatBlockingError('Integration test failed:'), err.message);
@@ -219,49 +231,67 @@ async function runIntegrationOnceForWatch(datasourceKey, integOpts, options, uni
219
231
  }
220
232
  }
221
233
 
234
+ async function integrationTestCommandActionWatch(datasourceKey, options, integOpts, unifiedDisplayOpts) {
235
+ const { appKey } = await resolveAppKeyForDatasource(datasourceKey, options.app);
236
+ await runDatasourceValidationWatchLoop({
237
+ appKey,
238
+ extraPaths: options.watchPath || [],
239
+ includeApplicationYaml: options.watchApplicationYaml === true,
240
+ watchCi: options.watchCi === true,
241
+ watchFullDiff: options.watchFullDiff === true,
242
+ runOnce: () => runIntegrationOnceForWatch(datasourceKey, integOpts, options, unifiedDisplayOpts)
243
+ });
244
+ }
245
+
246
+ async function integrationTestCommandActionNonWatch(datasourceKey, integOpts, options, unifiedDisplayOpts) {
247
+ const result = await runDatasourceTestIntegration(datasourceKey, integOpts);
248
+ const unifiedModes =
249
+ options.json || options.summary || options.warningsAsErrors || options.requireCert;
250
+ if (unifiedModes) {
251
+ const uni = unifiedCliResultFromIntegrationReturn(result);
252
+ const exitCode = finalizeUnifiedValidationResult(uni, unifiedDisplayOpts);
253
+ await afterUnifiedValidationCertSync(
254
+ exitCode,
255
+ datasourceKey,
256
+ options,
257
+ 'datasource test-integration',
258
+ uni.envelope
259
+ );
260
+ process.exit(exitCode);
261
+ return;
262
+ }
263
+ displayIntegrationTestResults(
264
+ {
265
+ systemKey: result.systemKey || 'unknown',
266
+ datasourceResults: [result],
267
+ success: result.success
268
+ },
269
+ options.verbose,
270
+ { debug: options.debug, runType: 'integration' }
271
+ );
272
+ const integExit = finalizeAfterIntegrationDisplay(result, {
273
+ warningsAsErrors: unifiedDisplayOpts.warningsAsErrors === true,
274
+ requireCert: unifiedDisplayOpts.requireCert === true
275
+ });
276
+ await afterUnifiedValidationCertSync(
277
+ integExit,
278
+ datasourceKey,
279
+ options,
280
+ 'datasource test-integration',
281
+ result.datasourceTestRun || null
282
+ );
283
+ process.exit(integExit);
284
+ }
285
+
222
286
  async function integrationTestCommandAction(datasourceKey, options) {
223
287
  const integOpts = buildIntegrationTestOpts(options);
224
288
  const unifiedDisplayOpts = buildIntegrationUnifiedDisplayOpts(options);
225
289
  try {
226
290
  if (options.watch) {
227
- const { appKey } = await resolveAppKeyForDatasource(datasourceKey, options.app);
228
- await runDatasourceValidationWatchLoop({
229
- appKey,
230
- extraPaths: options.watchPath || [],
231
- includeApplicationYaml: options.watchApplicationYaml === true,
232
- watchCi: options.watchCi === true,
233
- watchFullDiff: options.watchFullDiff === true,
234
- runOnce: () => runIntegrationOnceForWatch(datasourceKey, integOpts, options, unifiedDisplayOpts)
235
- });
236
- return;
237
- }
238
- const result = await runDatasourceTestIntegration(datasourceKey, integOpts);
239
- const unifiedModes =
240
- options.json || options.summary || options.warningsAsErrors || options.requireCert;
241
- if (unifiedModes) {
242
- const exitCode = finalizeUnifiedValidationResult(
243
- unifiedCliResultFromIntegrationReturn(result),
244
- unifiedDisplayOpts
245
- );
246
- await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test-integration');
247
- process.exit(exitCode);
291
+ await integrationTestCommandActionWatch(datasourceKey, options, integOpts, unifiedDisplayOpts);
248
292
  return;
249
293
  }
250
- displayIntegrationTestResults(
251
- {
252
- systemKey: result.systemKey || 'unknown',
253
- datasourceResults: [result],
254
- success: result.success
255
- },
256
- options.verbose,
257
- { debug: options.debug, runType: 'integration' }
258
- );
259
- const integExit = finalizeAfterIntegrationDisplay(result, {
260
- warningsAsErrors: unifiedDisplayOpts.warningsAsErrors === true,
261
- requireCert: unifiedDisplayOpts.requireCert === true
262
- });
263
- await afterUnifiedValidationCertSync(integExit, datasourceKey, options, 'datasource test-integration');
264
- process.exit(integExit);
294
+ await integrationTestCommandActionNonWatch(datasourceKey, integOpts, options, unifiedDisplayOpts);
265
295
  } catch (error) {
266
296
  logger.error(formatBlockingError('Integration test failed:'), error.message);
267
297
  process.exit(4);
@@ -296,9 +326,8 @@ async function runDatasourceTestE2ECliOnce(datasourceKey, options) {
296
326
  debug: options.debug,
297
327
  verbose: options.verbose,
298
328
  async: options.async !== false,
299
- testCrud: options.testCrud,
300
- recordId: options.recordId,
301
329
  cleanup: options.cleanup,
330
+ runScenarios: options.runScenarios,
302
331
  primaryKeyValue: options.primaryKeyValue,
303
332
  minVectorHits: options.minVectorHits,
304
333
  minProcessed: options.minProcessed,
@@ -340,8 +369,8 @@ async function runDatasourceTestE2ECliOnce(datasourceKey, options) {
340
369
  }
341
370
 
342
371
  async function runDatasourceTestE2ECliAction(datasourceKey, options) {
343
- const { exitCode } = await runDatasourceTestE2ECliOnce(datasourceKey, options);
344
- await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test-e2e');
372
+ const { exitCode, envelope } = await runDatasourceTestE2ECliOnce(datasourceKey, options);
373
+ await afterUnifiedValidationCertSync(exitCode, datasourceKey, options, 'datasource test-e2e', envelope);
345
374
  process.exit(exitCode);
346
375
  }
347
376
 
@@ -371,7 +400,13 @@ async function e2eTestCommandAction(datasourceKey, capabilityKey, options) {
371
400
  runOnce: async() => {
372
401
  try {
373
402
  const r = await runDatasourceTestE2ECliOnce(datasourceKey, mergedOptions);
374
- await afterUnifiedValidationCertSync(r.exitCode, datasourceKey, mergedOptions, 'datasource test-e2e (watch)');
403
+ await afterUnifiedValidationCertSync(
404
+ r.exitCode,
405
+ datasourceKey,
406
+ mergedOptions,
407
+ 'datasource test-e2e (watch)',
408
+ r.envelope
409
+ );
375
410
  return r;
376
411
  } catch (err) {
377
412
  logger.error(formatBlockingError('E2E test failed:'), err.message);
@@ -166,8 +166,10 @@ function attachDatasourceTestE2eExclusiveOptions(cmd) {
166
166
  'Minimum record count assertion (e2eOptions.minRecordCount)',
167
167
  (v) => parseInt(String(v), 10)
168
168
  )
169
- .option('--test-crud', 'Enable CRUD lifecycle test (e2eOptions.testCrud)')
170
- .option('--record-id <id>', 'Record ID for test (e2eOptions.recordId)')
169
+ .option(
170
+ '--no-run-scenarios',
171
+ 'Do not expand testPayload.scenarios in capacity step (e2eOptions.runScenarios: false)'
172
+ )
171
173
  .option('--no-cleanup', 'Disable cleanup after test (e2eOptions.cleanup: false)')
172
174
  .option(
173
175
  '--primary-key-value <value|@path>',
@@ -10,10 +10,10 @@
10
10
  */
11
11
 
12
12
  const path = require('path');
13
- const chalk = require('chalk');
14
13
  const logger = require('../utils/logger');
15
- const { sectionTitle, headerKeyValue, metadata, formatSuccessLine, formatBlockingError } = require('../utils/cli-test-layout-chalk');
14
+ const { formatBlockingError } = require('../utils/cli-test-layout-chalk');
16
15
  const { validateDatasourceFile } = require('../datasource/validate');
16
+ const { logDatasourceValidateOutcome } = require('../datasource/datasource-validate-display');
17
17
  const { listDatasources } = require('../datasource/list');
18
18
  const { compareDatasources } = require('../datasource/diff');
19
19
  const { deployDatasource } = require('../datasource/deploy');
@@ -53,34 +53,6 @@ Examples:
53
53
  $ af ds upload ../integration/hubspot/hubspot-datasource-deals.json
54
54
  `;
55
55
 
56
- /**
57
- * TTY layout for local datasource JSON validation (aligned with cli-test-layout-chalk).
58
- * @param {{ valid: boolean, errors: string[], resolvedPath: string }} result
59
- * @param {string} trimmed - original CLI argument
60
- * @param {boolean} showMapping - show Key + File when key resolved to a path
61
- */
62
- function logDatasourceValidateOutcome(result, trimmed, showMapping) {
63
- logger.log('');
64
- logger.log(sectionTitle('Datasource validation'));
65
- logger.log(metadata('Offline — JSON schema and integration wiring'));
66
- logger.log('');
67
- if (!result.valid) {
68
- logger.log(headerKeyValue('File:', result.resolvedPath));
69
- logger.log('');
70
- logger.log(formatBlockingError('Datasource file has errors:'));
71
- result.errors.forEach(error => logger.log(chalk.red(` • ${error}`)));
72
- return;
73
- }
74
- if (showMapping) {
75
- logger.log(headerKeyValue('Key:', trimmed));
76
- logger.log(headerKeyValue('File:', result.resolvedPath));
77
- } else {
78
- logger.log(headerKeyValue('File:', result.resolvedPath));
79
- }
80
- logger.log('');
81
- logger.log(formatSuccessLine('Datasource file is valid.'));
82
- }
83
-
84
56
  function setupDatasourceValidateCommand(datasource) {
85
57
  datasource.command('validate <file-or-key>')
86
58
  .description('Validate datasource JSON (file path or datasource key under integration/<app>/)')
@@ -97,7 +69,7 @@ function setupDatasourceValidateCommand(datasource) {
97
69
  process.exit(1);
98
70
  }
99
71
  } catch (error) {
100
- logger.error(formatBlockingError('Validation failed:'), error.message);
72
+ logger.error(formatBlockingError(`Validation failed: ${error.message}`));
101
73
  process.exit(1);
102
74
  }
103
75
  });
@@ -193,7 +193,7 @@ function normalizeDatasourceKeysAndFilenames(appPath, datasourceFiles, systemKey
193
193
  }
194
194
 
195
195
  if (updated && variables.externalIntegration && Array.isArray(variables.externalIntegration.dataSources)) {
196
- variables.externalIntegration.dataSources = [...newDatasourceFiles].sort();
196
+ variables.externalIntegration.dataSources = [...newDatasourceFiles];
197
197
  }
198
198
 
199
199
  return { updated, datasourceFiles: newDatasourceFiles };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * @fileoverview Repair OpenAPI block on external datasource JSON.
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ function hasNonEmptyObject(v) {
10
+ return !!v && typeof v === 'object' && !Array.isArray(v) && Object.keys(v).length > 0;
11
+ }
12
+
13
+ /**
14
+ * Repair `openapi` section:
15
+ * - If operations exist, ensure `openapi.enabled=true`
16
+ * - If enabled/operations and missing documentKey/fileId, default documentKey to datasource key (common convention)
17
+ * - If enabled+operations and autoRbac missing, default to true (wizard/fixtures expectation)
18
+ *
19
+ * @param {Object} parsed - Parsed datasource (mutated)
20
+ * @param {string[]} changes - Change log
21
+ * @returns {boolean} True if updated
22
+ */
23
+ function repairOpenapiSection(parsed, changes) {
24
+ const openapi = parsed?.openapi;
25
+ if (!openapi || typeof openapi !== 'object') {
26
+ return false;
27
+ }
28
+
29
+ const hasOps = hasNonEmptyObject(openapi.operations);
30
+ const enabled = openapi.enabled === true;
31
+ let updated = false;
32
+
33
+ if (hasOps && openapi.enabled !== true) {
34
+ openapi.enabled = true;
35
+ changes.push('Set openapi.enabled=true (operations present)');
36
+ updated = true;
37
+ }
38
+
39
+ if ((enabled || hasOps) && !openapi.documentKey && !openapi.fileId) {
40
+ if (typeof parsed.key === 'string' && parsed.key.trim()) {
41
+ openapi.documentKey = parsed.key.trim();
42
+ changes.push(`Set openapi.documentKey=${openapi.documentKey}`);
43
+ updated = true;
44
+ }
45
+ }
46
+
47
+ if ((enabled || hasOps) && hasOps && openapi.autoRbac === undefined) {
48
+ openapi.autoRbac = true;
49
+ changes.push('Set openapi.autoRbac=true (enabled operations, default)');
50
+ updated = true;
51
+ }
52
+
53
+ return updated;
54
+ }
55
+
56
+ module.exports = { repairOpenapiSection };
57
+
@@ -9,6 +9,8 @@
9
9
 
10
10
  'use strict';
11
11
 
12
+ const { repairOpenapiSection } = require('./repair-datasource-openapi');
13
+
12
14
  const DEFAULT_SYNC = {
13
15
  mode: 'pull',
14
16
  batchSize: 500
@@ -446,6 +448,8 @@ function repairDatasourceFile(parsed, options = {}, changes = []) {
446
448
  updated = repairMetadataSchemaFromAttributes(parsed, out) || updated;
447
449
  }
448
450
 
451
+ updated = repairOpenapiSection(parsed, out) || updated;
452
+
449
453
  if (options.expose) {
450
454
  updated = repairExposeFromAttributes(parsed, out) || updated;
451
455
  }
@@ -472,6 +476,7 @@ module.exports = {
472
476
  repairExposeFromAttributes,
473
477
  repairSyncSection,
474
478
  repairTestPayload,
479
+ repairOpenapiSection,
475
480
  sanitizeTestPayloadTopLevel,
476
481
  repairDatasourceFile,
477
482
  DEFAULT_SYNC,
@@ -40,7 +40,6 @@ function discoverIntegrationFiles(appPath) {
40
40
  }
41
41
  }
42
42
  systemFiles.sort();
43
- datasourceFiles.sort();
44
43
  return { systemFiles, datasourceFiles };
45
44
  }
46
45
 
@@ -48,8 +47,8 @@ function discoverIntegrationFiles(appPath) {
48
47
  * Builds the effective datasource file list from application.yaml and discovered files.
49
48
  * @param {string} appPath - Application directory path
50
49
  * @param {string[]} discoveredDatasourceFiles - Filenames from discoverIntegrationFiles
51
- * @param {string[]} [existingDataSources] - externalIntegration.dataSources from application.yaml
52
- * @returns {string[]} Sorted, deduplicated list of datasource filenames
50
+ * @param {string[]} [existingDataSources] - externalIntegration.dataSources from application config
51
+ * @returns {string[]} Deduplicated list of datasource filenames (application order first, then remaining discovered files in directory order)
53
52
  */
54
53
  function buildEffectiveDatasourceFiles(appPath, discoveredDatasourceFiles, existingDataSources) {
55
54
  const existing = Array.isArray(existingDataSources) ? existingDataSources : [];
@@ -75,7 +74,6 @@ function buildEffectiveDatasourceFiles(appPath, discoveredDatasourceFiles, exist
75
74
  seen.add(name);
76
75
  }
77
76
  }
78
- result.sort();
79
77
  return result;
80
78
  }
81
79
 
@@ -17,6 +17,22 @@ const { resolveRbacPath } = require('../utils/app-config-resolver');
17
17
 
18
18
  const DEFAULT_CAPABILITIES = ['list', 'get', 'create', 'update', 'delete'];
19
19
 
20
+ /**
21
+ * Extracts roles and permissions from external system JSON for rbac.yaml (same rules as repair).
22
+ * @param {Object} system - Parsed system config
23
+ * @returns {Object|null} RBAC object or null
24
+ */
25
+ function extractRbacFromSystem(system) {
26
+ if (!system || typeof system !== 'object') return null;
27
+ const hasRoles = system.roles && Array.isArray(system.roles) && system.roles.length > 0;
28
+ const hasPermissions = system.permissions && Array.isArray(system.permissions) && system.permissions.length > 0;
29
+ if (!hasRoles && !hasPermissions) return null;
30
+ const rbac = {};
31
+ if (hasRoles) rbac.roles = system.roles;
32
+ if (hasPermissions) rbac.permissions = system.permissions;
33
+ return rbac;
34
+ }
35
+
20
36
  /**
21
37
  * Resolves capabilities from datasource (array or legacy object).
22
38
  * @param {Object} parsed - Parsed datasource
@@ -24,9 +40,15 @@ const DEFAULT_CAPABILITIES = ['list', 'get', 'create', 'update', 'delete'];
24
40
  */
25
41
  function getCapabilitiesFromDatasource(parsed) {
26
42
  const cap = parsed?.capabilities;
27
- if (Array.isArray(cap)) return cap.filter(c => typeof c === 'string');
43
+ if (Array.isArray(cap)) {
44
+ const arr = cap.filter(c => typeof c === 'string');
45
+ if (arr.length > 0) return arr;
46
+ return [...DEFAULT_CAPABILITIES];
47
+ }
28
48
  if (cap && typeof cap === 'object') {
29
- return Object.keys(cap).filter(k => cap[k] === true);
49
+ const keys = Object.keys(cap).filter(k => cap[k] === true);
50
+ if (keys.length > 0) return keys;
51
+ return [...DEFAULT_CAPABILITIES];
30
52
  }
31
53
  return [...DEFAULT_CAPABILITIES];
32
54
  }
@@ -159,6 +181,7 @@ function mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extrac
159
181
  }
160
182
 
161
183
  module.exports = {
184
+ extractRbacFromSystem,
162
185
  getCapabilitiesFromDatasource,
163
186
  mergeRbacFromDatasources
164
187
  };
@@ -26,7 +26,7 @@ const generator = require('../generator');
26
26
  const { repairEnvTemplate, normalizeSystemFileAuthAndConfig } = require('./repair-env-template');
27
27
  const { generateReadmeFromDeployJson } = require('../generator/split-readme');
28
28
  const { repairDatasourceFile } = require('./repair-datasource');
29
- const { mergeRbacFromDatasources } = require('./repair-rbac');
29
+ const { mergeRbacFromDatasources, extractRbacFromSystem } = require('./repair-rbac');
30
30
  const { discoverIntegrationFiles, buildEffectiveDatasourceFiles } = require('./repair-internal');
31
31
  const { normalizeDatasourceKeysAndFilenames } = require('./repair-datasource-keys');
32
32
 
@@ -49,22 +49,6 @@ function inferExternalReadmeFileExt(appPath) {
49
49
  return '.json';
50
50
  }
51
51
 
52
- /**
53
- * Extracts roles and permissions from system object for rbac.yaml
54
- * @param {Object} system - Parsed system config
55
- * @returns {Object|null} RBAC object or null
56
- */
57
- function extractRbacFromSystem(system) {
58
- if (!system || typeof system !== 'object') return null;
59
- const hasRoles = system.roles && Array.isArray(system.roles) && system.roles.length > 0;
60
- const hasPermissions = system.permissions && Array.isArray(system.permissions) && system.permissions.length > 0;
61
- if (!hasRoles && !hasPermissions) return null;
62
- const rbac = {};
63
- if (hasRoles) rbac.roles = system.roles;
64
- if (hasPermissions) rbac.permissions = system.permissions;
65
- return rbac;
66
- }
67
-
68
52
  /**
69
53
  * Loads first system file and returns parsed object with key
70
54
  * @param {string} appPath - Application path
@@ -204,8 +188,7 @@ function alignSystemFileDataSources(appPath, systemParsed, datasourceFiles, syst
204
188
  keys.push(deriveDatasourceKeyFromFileName(fileName, systemKey));
205
189
  }
206
190
  }
207
- keys.sort();
208
- const prev = Array.isArray(systemParsed.dataSources) ? [...systemParsed.dataSources].sort() : [];
191
+ const prev = Array.isArray(systemParsed.dataSources) ? [...systemParsed.dataSources] : [];
209
192
  if (JSON.stringify(prev) === JSON.stringify(keys)) return false;
210
193
  systemParsed.dataSources = keys;
211
194
  changes.push(`dataSources: [${prev.join(', ') || '(none)'}] → [${keys.join(', ')}] (keys from each datasource file's "key" or filename)`);
@@ -42,8 +42,8 @@ function deriveDatasourceKeyFromFileName(fileName, systemKey) {
42
42
  * @param {Object} variables - Loaded application variables (externalIntegration.dataSources = filenames)
43
43
  * @param {string} systemKey - System key from system file
44
44
  * @param {Object} systemParsed - Parsed system config (may have dataSources array of keys)
45
- * @param {string[]} datasourceFiles - Discovered datasource filenames
46
- * @returns {string[]} Sorted list of datasource keys
45
+ * @param {string[]} datasourceFiles - Discovered datasource filenames (application order when built via repair)
46
+ * @returns {string[]} Datasource keys in declaration order (system JSON or file list never sort; FK targets must run first)
47
47
  */
48
48
  function getDatasourceKeys(appPath, configPath, variables, systemKey, systemParsed, datasourceFiles) {
49
49
  const fromSystem = Array.isArray(systemParsed.dataSources) && systemParsed.dataSources.length > 0
@@ -54,11 +54,11 @@ function getDatasourceKeys(appPath, configPath, variables, systemKey, systemPars
54
54
  if (fromSystem) {
55
55
  fromSystem.forEach(k => {
56
56
  if (k && typeof k === 'string' && !seen.has(k)) {
57
- keys.push(k.trim());
58
- seen.add(k.trim());
57
+ const trimmed = k.trim();
58
+ keys.push(trimmed);
59
+ seen.add(trimmed);
59
60
  }
60
61
  });
61
- keys.sort();
62
62
  return keys;
63
63
  }
64
64
  for (const fileName of datasourceFiles) {
@@ -81,18 +81,18 @@ function getDatasourceKeys(appPath, configPath, variables, systemKey, systemPars
81
81
  }
82
82
  }
83
83
  }
84
- keys.sort();
85
84
  return keys;
86
85
  }
87
86
 
88
87
  /**
89
- * Full upload to dataplane when --sync (same path as `aifabrix upload <systemKey>`).
88
+ * Publish local integration files to the dataplane before E2E (same path as `aifabrix upload <systemKey>`),
89
+ * unless ``options.noSync`` is true.
90
90
  * @param {string} systemKey
91
91
  * @param {Object} options
92
92
  * @returns {Promise<void>}
93
93
  */
94
94
  async function syncLocalIfRequested(systemKey, options) {
95
- if (options.sync !== true) return;
95
+ if (options.noSync === true) return;
96
96
  logger.log(chalk.cyan('Syncing local config to dataplane…'));
97
97
  const { uploadExternalSystem } = require('./upload');
98
98
  await uploadExternalSystem(systemKey, {
@@ -113,7 +113,7 @@ async function syncLocalIfRequested(systemKey, options) {
113
113
  * @param {boolean} [options.debug] - Include debug, write log
114
114
  * @param {boolean} [options.verbose] - Verbose output
115
115
  * @param {boolean} [options.async] - If false, sync mode (default true)
116
- * @param {boolean} [options.sync] - When true, run full upload (`uploadExternalSystem`) before per-datasource E2E
116
+ * @param {boolean} [options.noSync] - When true, skip upload (E2E uses dataplane config already deployed)
117
117
  * @returns {Promise<{ success: boolean, results: Array<{ key: string, success: boolean, error?: string }> }>}
118
118
  */
119
119
  async function runTestE2EForExternalSystem(externalSystem, options = {}) {
@@ -13,6 +13,8 @@ const path = require('path');
13
13
  const fs = require('fs');
14
14
  const chalk = require('chalk');
15
15
  const logger = require('../utils/logger');
16
+ const config = require('../core/config');
17
+ const { getDefaultControllerUrl } = require('../utils/controller-url');
16
18
  const pathsUtil = require('../utils/paths');
17
19
  const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
18
20
  const { isYamlPath } = require('../utils/config-format');
@@ -166,6 +168,28 @@ function patchEnvOutputPathForDeployOnly(appName) {
166
168
  }
167
169
  }
168
170
 
171
+ /**
172
+ * For ``up-platform --force`` only: clear all stored auth tokens, set ``environment`` to ``dev``,
173
+ * set ``controller`` to the default URL for the current ``developer-id`` (``http://localhost`` +
174
+ * app port = 3000 + developerId × 100), and persist config. Does not change ``developer-id``.
175
+ * Builder dirs (keycloak, miso-controller, dataplane) are cleaned separately.
176
+ *
177
+ * @returns {Promise<void>}
178
+ */
179
+ async function applyUpPlatformForceConfig() {
180
+ const deviceCleared = await config.clearAllDeviceTokens();
181
+ const clientCleared = await config.clearAllClientTokens();
182
+ await config.setCurrentEnvironment('dev');
183
+ const defaultControllerUrl = await getDefaultControllerUrl();
184
+ await config.setControllerUrl(defaultControllerUrl);
185
+ logger.log(
186
+ chalk.blue(
187
+ `--force: cleared ${deviceCleared} device token(s) and ${clientCleared} client token(s); ` +
188
+ `environment set to dev; default controller set to ${defaultControllerUrl} (run aifabrix login after platform is up)`
189
+ )
190
+ );
191
+ }
192
+
169
193
  /**
170
194
  * Removes builder app directories for the given app names. Only removes paths under the builder root
171
195
  * to prevent path traversal. Uses getBuilderPath for each app and validates before removal.
@@ -231,6 +255,7 @@ async function ensureAppFromTemplate(appName) {
231
255
  }
232
256
 
233
257
  module.exports = {
258
+ applyUpPlatformForceConfig,
234
259
  cleanBuilderAppDirs,
235
260
  ensureAppFromTemplate,
236
261
  patchEnvOutputPathForDeployOnly,
@@ -17,7 +17,8 @@ const { getIntegrationPath } = require('../utils/paths');
17
17
  const { pushCredentialSecrets } = require('../utils/credential-secrets-env');
18
18
  const {
19
19
  buildResolvedEnvMapForIntegration,
20
- resolveConfigurationValues
20
+ resolveConfigurationValues,
21
+ collectResolvedVariableConfigurationNames
21
22
  } = require('../utils/configuration-env-resolver');
22
23
  const { validateExternalSystemComplete } = require('../validation/validate');
23
24
  const { displayValidationResults } = require('../validation/validate-display');
@@ -70,9 +71,11 @@ function buildUploadPayload(manifest) {
70
71
  /**
71
72
  * Resolves dataplane URL and auth (same pattern as download).
72
73
  * @param {string} systemKey - System key
74
+ * @param {{ silent?: boolean }} [opts] - silent: omit “Resolving dataplane URL…” and discovery success lines
73
75
  * @returns {Promise<{ dataplaneUrl: string, authConfig: Object, environment: string }>}
74
76
  */
75
- async function resolveDataplaneAndAuth(systemKey) {
77
+ async function resolveDataplaneAndAuth(systemKey, opts = {}) {
78
+ const silent = opts.silent === true;
76
79
  const { resolveEnvironment } = require('../core/config');
77
80
  const environment = await resolveEnvironment();
78
81
  const controllerUrl = await resolveControllerUrl();
@@ -82,8 +85,10 @@ async function resolveDataplaneAndAuth(systemKey) {
82
85
  throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register <systemKey>" first.');
83
86
  }
84
87
 
85
- logger.log(chalk.gray('Resolving dataplane URL...'));
86
- const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
88
+ if (!silent) {
89
+ logger.log(chalk.gray('Resolving dataplane URL...'));
90
+ }
91
+ const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig, { silent });
87
92
  return { dataplaneUrl, authConfig, environment };
88
93
  }
89
94
 
@@ -167,6 +172,15 @@ async function pushAndLogCredentialSecrets(dataplaneUrl, authConfig, systemKey,
167
172
  } else {
168
173
  logger.log(chalk.yellow('Secret push skipped'));
169
174
  }
175
+ const resolvedParamNames = collectResolvedVariableConfigurationNames(payload);
176
+ if (resolvedParamNames.length > 0) {
177
+ const nameList = ` (${resolvedParamNames.join(', ')})`;
178
+ logger.log(
179
+ formatSuccessLine(
180
+ `Resolved ${resolvedParamNames.length} integration parameter(s) from .env for publish${nameList}.`
181
+ )
182
+ );
183
+ }
170
184
  if (pushResult.warning) {
171
185
  logger.log(chalk.yellow(`Warning: ${pushResult.warning}`));
172
186
  }