@aifabrix/builder 2.44.3 → 2.44.4

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 (56) 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 +12 -1
  5. package/lib/api/certificates.api.js +21 -3
  6. package/lib/certification/post-unified-cert-sync.js +13 -2
  7. package/lib/certification/sync-after-external-command.js +6 -3
  8. package/lib/certification/sync-system-certification.js +60 -14
  9. package/lib/cli/setup-app.test-commands.js +67 -35
  10. package/lib/cli/setup-utility.js +1 -1
  11. package/lib/commands/datasource-unified-test-cli.js +81 -46
  12. package/lib/commands/datasource-unified-test-cli.options.js +4 -2
  13. package/lib/commands/datasource.js +3 -31
  14. package/lib/commands/repair-datasource-keys.js +1 -1
  15. package/lib/commands/repair-datasource-openapi.js +57 -0
  16. package/lib/commands/repair-datasource.js +5 -0
  17. package/lib/commands/repair-internal.js +2 -4
  18. package/lib/commands/repair.js +1 -2
  19. package/lib/commands/test-e2e-external.js +5 -6
  20. package/lib/commands/upload.js +18 -4
  21. package/lib/commands/wizard-dataplane.js +14 -6
  22. package/lib/datasource/datasource-validate-display.js +162 -0
  23. package/lib/datasource/datasource-validate-summary.js +194 -0
  24. package/lib/datasource/test-e2e.js +65 -37
  25. package/lib/datasource/unified-validation-run-body.js +1 -2
  26. package/lib/datasource/validate.js +14 -6
  27. package/lib/external-system/test.js +12 -8
  28. package/lib/generator/external-controller-manifest.js +12 -2
  29. package/lib/schema/cip-capacity-display.fallback.json +7 -0
  30. package/lib/schema/datasource-test-run.schema.json +79 -1
  31. package/lib/schema/external-datasource.schema.json +94 -2
  32. package/lib/schema/flag-map-validation-run.json +1 -2
  33. package/lib/schema/type/document-storage.json +83 -3
  34. package/lib/utils/configuration-env-resolver.js +38 -0
  35. package/lib/utils/dataplane-resolver.js +3 -2
  36. package/lib/utils/datasource-test-run-capacity-operations.js +149 -0
  37. package/lib/utils/datasource-test-run-debug-display.js +143 -1
  38. package/lib/utils/datasource-test-run-display.js +46 -33
  39. package/lib/utils/datasource-test-run-tty-log.js +6 -2
  40. package/lib/utils/datasource-test-run-tty-meta-lines.js +123 -0
  41. package/lib/utils/error-formatter.js +32 -2
  42. package/lib/utils/external-system-readiness-core.js +39 -0
  43. package/lib/utils/external-system-readiness-deploy-display.js +2 -3
  44. package/lib/utils/external-system-readiness-display-internals.js +3 -2
  45. package/lib/utils/external-system-system-test-tty.js +33 -9
  46. package/lib/utils/external-system-validators.js +62 -5
  47. package/lib/utils/load-cip-capacity-display-config.js +130 -0
  48. package/lib/utils/paths.js +10 -3
  49. package/lib/utils/schema-resolver.js +98 -2
  50. package/lib/utils/validation-run-poll.js +15 -4
  51. package/lib/utils/validation-run-request.js +4 -6
  52. package/lib/validation/dimension-display-helpers.js +60 -0
  53. package/lib/validation/validate-display-log-helpers.js +39 -0
  54. package/lib/validation/validate-display.js +89 -83
  55. package/package.json +1 -1
  56. package/templates/external-system/README.md.hbs +1 -2
@@ -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
 
@@ -204,8 +204,7 @@ function alignSystemFileDataSources(appPath, systemParsed, datasourceFiles, syst
204
204
  keys.push(deriveDatasourceKeyFromFileName(fileName, systemKey));
205
205
  }
206
206
  }
207
- keys.sort();
208
- const prev = Array.isArray(systemParsed.dataSources) ? [...systemParsed.dataSources].sort() : [];
207
+ const prev = Array.isArray(systemParsed.dataSources) ? [...systemParsed.dataSources] : [];
209
208
  if (JSON.stringify(prev) === JSON.stringify(keys)) return false;
210
209
  systemParsed.dataSources = keys;
211
210
  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,7 +81,6 @@ function getDatasourceKeys(appPath, configPath, variables, systemKey, systemPars
81
81
  }
82
82
  }
83
83
  }
84
- keys.sort();
85
84
  return keys;
86
85
  }
87
86
 
@@ -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
  }
@@ -73,10 +73,12 @@ function createDataplaneNotFoundError() {
73
73
  * @returns {Promise<string>} Dataplane URL
74
74
  * @throws {Error} If dataplane URL cannot be retrieved
75
75
  */
76
- async function tryFallbackDataplaneUrl(controllerUrl, environment, authConfig) {
76
+ async function tryFallbackDataplaneUrl(controllerUrl, environment, authConfig, silent) {
77
77
  try {
78
78
  const fallbackUrl = await getDataplaneUrl(controllerUrl, 'dataplane', environment, authConfig);
79
- logger.log(formatSuccessLine(`Dataplane URL: ${fallbackUrl}`));
79
+ if (!silent) {
80
+ logger.log(formatSuccessLine(`Dataplane URL: ${fallbackUrl}`));
81
+ }
80
82
  return fallbackUrl;
81
83
  } catch (fallbackError) {
82
84
  if (isNotFoundError(fallbackError)) {
@@ -93,19 +95,25 @@ async function tryFallbackDataplaneUrl(controllerUrl, environment, authConfig) {
93
95
  * @param {string} controllerUrl - Controller URL
94
96
  * @param {string} environment - Environment key
95
97
  * @param {Object} authConfig - Authentication configuration
98
+ * @param {{ silent?: boolean }} [opts] - When silent, skip progress/success lines (e.g. cert sync right after a run).
96
99
  * @returns {Promise<string>} Dataplane URL
97
100
  * @throws {Error} If dataplane URL cannot be discovered
98
101
  */
99
- async function discoverDataplaneUrl(controllerUrl, environment, authConfig) {
100
- logger.log(infoLine('🌐 Getting dataplane URL from controller...'));
102
+ async function discoverDataplaneUrl(controllerUrl, environment, authConfig, opts = {}) {
103
+ const silent = opts.silent === true;
104
+ if (!silent) {
105
+ logger.log(infoLine('🌐 Getting dataplane URL from controller...'));
106
+ }
101
107
  try {
102
108
  const dataplaneAppKey = await findDataplaneServiceAppKey(controllerUrl, environment, authConfig);
103
109
  if (dataplaneAppKey) {
104
110
  const dataplaneUrl = await getDataplaneUrl(controllerUrl, dataplaneAppKey, environment, authConfig);
105
- logger.log(formatSuccessLine(`Dataplane URL: ${dataplaneUrl}`));
111
+ if (!silent) {
112
+ logger.log(formatSuccessLine(`Dataplane URL: ${dataplaneUrl}`));
113
+ }
106
114
  return dataplaneUrl;
107
115
  }
108
- return await tryFallbackDataplaneUrl(controllerUrl, environment, authConfig);
116
+ return await tryFallbackDataplaneUrl(controllerUrl, environment, authConfig, silent);
109
117
  } catch (error) {
110
118
  if (error.message.includes('Could not discover dataplane URL')) {
111
119
  throw error;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * @fileoverview TTY output for `aifabrix datasource validate` (cli-test-layout-chalk).
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+ const logger = require('../utils/logger');
11
+ const {
12
+ sectionTitle,
13
+ headerKeyValue,
14
+ metadata,
15
+ formatSuccessLine,
16
+ formatBlockingError,
17
+ successGlyph,
18
+ failureGlyph
19
+ } = require('../utils/cli-test-layout-chalk');
20
+
21
+ function logSubOk(whitePart, grayPart) {
22
+ if (grayPart) {
23
+ logger.log(` ${successGlyph()} ${chalk.white(whitePart)} ${metadata(grayPart)}`);
24
+ } else {
25
+ logger.log(` ${successGlyph()} ${chalk.white(whitePart)}`);
26
+ }
27
+ }
28
+
29
+ function logHeaderAndIdentity(result, trimmed, showMapping) {
30
+ const { resolvedPath, summary } = result;
31
+ logger.log('');
32
+ logger.log(sectionTitle('Datasource validation'));
33
+ logger.log(metadata('Offline — JSON schema and integration wiring'));
34
+ logger.log('');
35
+ if (summary) {
36
+ logger.log(` ${headerKeyValue('Key:', summary.key)}`);
37
+ logger.log(` ${headerKeyValue('Resource type:', summary.resourceType)}`);
38
+ logger.log(` ${headerKeyValue('Entity type:', summary.entityType)}`);
39
+ logger.log(` ${headerKeyValue('File:', resolvedPath)}`);
40
+ if (showMapping && trimmed && String(trimmed) !== String(summary.key)) {
41
+ logger.log(` ${headerKeyValue('CLI input:', trimmed)}`);
42
+ }
43
+ logger.log('');
44
+ } else {
45
+ logger.log(` ${headerKeyValue('File:', resolvedPath)}`);
46
+ logger.log('');
47
+ }
48
+ }
49
+
50
+ function logInvalidBody(errors, warnings) {
51
+ logger.log(` ${formatBlockingError('Datasource file has errors')}`);
52
+ (errors || []).forEach(err => {
53
+ logger.log(` ${failureGlyph()} ${chalk.red(err)}`);
54
+ });
55
+ if (warnings && warnings.length > 0) {
56
+ logger.log('');
57
+ logger.log(sectionTitle('Warnings'));
58
+ warnings.forEach(w => {
59
+ logger.log(` ${chalk.yellow('⚠')} ${chalk.white(w)}`);
60
+ });
61
+ }
62
+ }
63
+
64
+ function logValidMappingsAndRelations(summary) {
65
+ const mc = summary.metadataSchemaPropertyCount;
66
+ const mcLabel = `${mc} propert${mc === 1 ? 'y' : 'ies'}`;
67
+ logSubOk('Metadata schema', mcLabel);
68
+ logSubOk('Field mappings', `${summary.fieldMappingAttributeCount} attribute(s)`);
69
+ logger.log('');
70
+ logSubOk(`Primary key: ${summary.primaryKey}`);
71
+ logSubOk(`Label key: ${summary.labelKey}`);
72
+ logger.log('');
73
+ if (summary.foreignKeys.length > 0) {
74
+ logSubOk('Foreign keys', `${summary.foreignKeys.length} reference(s)`);
75
+ summary.foreignKeys.forEach(fk => {
76
+ logger.log(metadata(` • ${fk.name} → ${fk.target} (${fk.fields})`));
77
+ });
78
+ } else {
79
+ logSubOk('Foreign keys', 'none');
80
+ }
81
+ logger.log('');
82
+ if (summary.dimensionKeys.length > 0) {
83
+ logSubOk('Dimensions (ABAC)', '');
84
+ summary.dimensionKeys.forEach(k => {
85
+ logger.log(metadata(` ${k} → ${summary.dimensions[k]}`));
86
+ });
87
+ } else {
88
+ logSubOk('Dimensions (ABAC)', 'none configured');
89
+ }
90
+ logger.log('');
91
+ }
92
+
93
+ function logOpenapiSection(summary) {
94
+ if (summary.hasOpenapi) {
95
+ logSubOk('OpenAPI', summary.openapiLine);
96
+ if (summary.capabilityKeys.length > 0) {
97
+ const preview = summary.capabilityKeys.slice(0, 12).join(', ');
98
+ const more = summary.capabilityKeys.length > 12 ? ', …' : '';
99
+ logger.log(metadata(` Operations: ${preview}${more}`));
100
+ }
101
+ return;
102
+ }
103
+ logSubOk('OpenAPI', 'not configured');
104
+ }
105
+
106
+ function logValidInterfaceAndFooter(summary) {
107
+ if (summary.exposedProfileNames.length > 0) {
108
+ logSubOk('Exposed profiles', summary.exposedProfileNames.join(', '));
109
+ } else {
110
+ logSubOk('Exposed profiles', 'none');
111
+ }
112
+ logger.log('');
113
+ logOpenapiSection(summary);
114
+ logger.log('');
115
+ logSubOk('Test payload', summary.testPayloadLine);
116
+ logger.log('');
117
+ logSubOk('Synchronization', summary.syncLine);
118
+ logger.log('');
119
+ const caps = summary.capabilityKeys;
120
+ const capHint =
121
+ caps.length > 0 ? `${caps.slice(0, 8).join(', ')}${caps.length > 8 ? ', …' : ''}` : 'none (no OpenAPI operations)';
122
+ logSubOk('Capabilities', capHint);
123
+ logger.log('');
124
+ logger.log(` ${formatSuccessLine('Datasource file is valid.')}`);
125
+ }
126
+
127
+ function logWarningsOnly(warnings) {
128
+ if (!warnings || warnings.length === 0) {
129
+ return;
130
+ }
131
+ logger.log('');
132
+ logger.log(sectionTitle('Warnings'));
133
+ warnings.forEach(w => {
134
+ logger.log(` ${chalk.yellow('⚠')} ${chalk.white(w)}`);
135
+ });
136
+ }
137
+
138
+ /**
139
+ * @param {object} result - validateDatasourceFile result (includes optional summary)
140
+ * @param {string} trimmed - CLI argument
141
+ * @param {boolean} showMapping - true when key resolved to a different path than arg
142
+ */
143
+ function logDatasourceValidateOutcome(result, trimmed, showMapping) {
144
+ logHeaderAndIdentity(result, trimmed, showMapping);
145
+ const { valid, errors, warnings, summary } = result;
146
+ if (!valid) {
147
+ logInvalidBody(errors, warnings);
148
+ return;
149
+ }
150
+ if (!summary) {
151
+ logger.log(` ${formatSuccessLine('Datasource file is valid.')}`);
152
+ logWarningsOnly(warnings);
153
+ return;
154
+ }
155
+ logValidMappingsAndRelations(summary);
156
+ logValidInterfaceAndFooter(summary);
157
+ logWarningsOnly(warnings);
158
+ }
159
+
160
+ module.exports = {
161
+ logDatasourceValidateOutcome
162
+ };
@@ -0,0 +1,194 @@
1
+ /**
2
+ * @fileoverview Extract display summary from parsed external-datasource JSON (offline validate).
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const { flattenRootDimensionsForDisplay } = require('../validation/dimension-display-helpers');
10
+
11
+ /**
12
+ * Count JSON Schema property nodes (recursive `properties` bags).
13
+ * @param {object|null|undefined} metadataSchema
14
+ * @returns {number}
15
+ */
16
+ function countMetadataSchemaProperties(metadataSchema) {
17
+ if (!metadataSchema || typeof metadataSchema !== 'object') {
18
+ return 0;
19
+ }
20
+ let n = 0;
21
+ function walk(node, depth) {
22
+ if (depth > 25 || !node || typeof node !== 'object') {
23
+ return;
24
+ }
25
+ const props = node.properties;
26
+ if (props && typeof props === 'object' && !Array.isArray(props)) {
27
+ for (const key of Object.keys(props)) {
28
+ n += 1;
29
+ walk(props[key], depth + 1);
30
+ }
31
+ }
32
+ if (Array.isArray(node.allOf)) {
33
+ for (const sub of node.allOf) {
34
+ walk(sub, depth + 1);
35
+ }
36
+ }
37
+ if (node.items && typeof node.items === 'object') {
38
+ walk(node.items, depth + 1);
39
+ }
40
+ }
41
+ walk(metadataSchema, 0);
42
+ return n;
43
+ }
44
+
45
+ /**
46
+ * @param {object} fk
47
+ * @returns {{ name: string, target: string, fields: string }}
48
+ */
49
+ function normalizeForeignKeyRow(fk) {
50
+ return {
51
+ name: fk.name,
52
+ target: fk.targetDatasource,
53
+ fields: Array.isArray(fk.fields) ? fk.fields.join(', ') : ''
54
+ };
55
+ }
56
+
57
+ /**
58
+ * @param {object} parsed
59
+ * @returns {{ dimensionKeys: string[], dimensions: Record<string, string> }}
60
+ */
61
+ function summarizeDimensions(parsed) {
62
+ const dimFlat = flattenRootDimensionsForDisplay(parsed.dimensions);
63
+ const abacDims =
64
+ parsed.abac && parsed.abac.dimensions && typeof parsed.abac.dimensions === 'object' ? parsed.abac.dimensions : {};
65
+ const allDims = { ...dimFlat, ...abacDims };
66
+ return { dimensionKeys: Object.keys(allDims), dimensions: allDims };
67
+ }
68
+
69
+ /**
70
+ * @param {object|null} sync
71
+ * @returns {string}
72
+ */
73
+ function summarizeSyncLine(sync) {
74
+ if (!sync || typeof sync !== 'object') {
75
+ return 'Not configured';
76
+ }
77
+ const parts = [];
78
+ if (sync.mode) {
79
+ parts.push(String(sync.mode));
80
+ }
81
+ if (sync.batchSize !== undefined && sync.batchSize !== null) {
82
+ parts.push(`batch size ${sync.batchSize}`);
83
+ }
84
+ if (sync.schedule) {
85
+ parts.push(`schedule ${JSON.stringify(sync.schedule)}`);
86
+ }
87
+ return parts.length ? parts.join(', ') : 'Present';
88
+ }
89
+
90
+ /**
91
+ * @param {object} parsed
92
+ * @returns {{ openapiLine: string, hasOpenapi: boolean, capabilityKeys: string[] }}
93
+ */
94
+ function summarizeOpenapi(parsed) {
95
+ const openapi = parsed.openapi && typeof parsed.openapi === 'object' ? parsed.openapi : {};
96
+ const operations = openapi.operations && typeof openapi.operations === 'object' ? openapi.operations : {};
97
+ const capabilityKeys = Object.keys(operations);
98
+ let openapiLine = 'Not configured';
99
+ if (openapi.enabled === true) {
100
+ openapiLine = openapi.autoRbac ? 'enabled, auto RBAC' : 'enabled';
101
+ } else if (parsed.openapi && Object.prototype.hasOwnProperty.call(parsed.openapi, 'enabled')) {
102
+ openapiLine = 'disabled';
103
+ }
104
+ return {
105
+ openapiLine,
106
+ hasOpenapi: Object.keys(openapi).length > 0,
107
+ capabilityKeys
108
+ };
109
+ }
110
+
111
+ /**
112
+ * @param {object} parsed
113
+ * @returns {string}
114
+ */
115
+ function summarizeTestPayloadLine(parsed) {
116
+ const tp = parsed.testPayload;
117
+ const hasTestPayload = !!tp && typeof tp === 'object';
118
+ if (!hasTestPayload) {
119
+ return 'Not configured';
120
+ }
121
+ if (Array.isArray(tp.scenarios)) {
122
+ return `${tp.scenarios.length} scenario(s)`;
123
+ }
124
+ return 'Present';
125
+ }
126
+
127
+ /**
128
+ * @param {object} parsed
129
+ * @returns {string[]}
130
+ */
131
+ function summarizeExposedProfileNames(parsed) {
132
+ if (parsed.exposed && parsed.exposed.profiles && typeof parsed.exposed.profiles === 'object') {
133
+ return Object.keys(parsed.exposed.profiles);
134
+ }
135
+ return [];
136
+ }
137
+
138
+ /**
139
+ * @param {string|undefined} v
140
+ * @returns {string}
141
+ */
142
+ function dash(v) {
143
+ return v || '—';
144
+ }
145
+
146
+ /**
147
+ * @param {object} parsed
148
+ * @returns {object}
149
+ */
150
+ function assembleSummary(parsed) {
151
+ const attrs = parsed.fieldMappings && parsed.fieldMappings.attributes;
152
+ const attrCount = attrs && typeof attrs === 'object' ? Object.keys(attrs).length : 0;
153
+ const pk = Array.isArray(parsed.primaryKey) ? parsed.primaryKey.join(', ') : String(parsed.primaryKey || '');
154
+ const lk = Array.isArray(parsed.labelKey) ? parsed.labelKey.join(', ') : String(parsed.labelKey || '');
155
+ const fks = Array.isArray(parsed.foreignKeys) ? parsed.foreignKeys.map(normalizeForeignKeyRow) : [];
156
+ const { dimensionKeys, dimensions } = summarizeDimensions(parsed);
157
+ const oa = summarizeOpenapi(parsed);
158
+ const sync = parsed.sync && typeof parsed.sync === 'object' ? parsed.sync : null;
159
+ return {
160
+ key: dash(parsed.key),
161
+ resourceType: dash(parsed.resourceType),
162
+ entityType: dash(parsed.entityType),
163
+ metadataSchemaPropertyCount: countMetadataSchemaProperties(parsed.metadataSchema),
164
+ fieldMappingAttributeCount: attrCount,
165
+ primaryKey: pk || '—',
166
+ labelKey: lk || '—',
167
+ foreignKeys: fks,
168
+ dimensionKeys,
169
+ dimensions,
170
+ exposedProfileNames: summarizeExposedProfileNames(parsed),
171
+ openapiLine: oa.openapiLine,
172
+ hasOpenapi: oa.hasOpenapi,
173
+ capabilityKeys: oa.capabilityKeys,
174
+ testPayloadLine: summarizeTestPayloadLine(parsed),
175
+ hasTestPayload: !!parsed.testPayload && typeof parsed.testPayload === 'object',
176
+ syncLine: summarizeSyncLine(sync)
177
+ };
178
+ }
179
+
180
+ /**
181
+ * @param {object} parsed - Parsed datasource JSON
182
+ * @returns {object|null}
183
+ */
184
+ function buildDatasourceValidateSummary(parsed) {
185
+ if (!parsed || typeof parsed !== 'object') {
186
+ return null;
187
+ }
188
+ return assembleSummary(parsed);
189
+ }
190
+
191
+ module.exports = {
192
+ buildDatasourceValidateSummary,
193
+ countMetadataSchemaProperties
194
+ };