@aifabrix/builder 2.44.5 → 2.44.6

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 (207) hide show
  1. package/.cursor/rules/cli-layout.mdc +1 -1
  2. package/.cursor/rules/project-rules.mdc +1 -1
  3. package/.npmrc.token +1 -1
  4. package/README.md +15 -23
  5. package/integration/hubspot-test/README.md +2 -0
  6. package/integration/hubspot-test/test.js +5 -3
  7. package/jest.projects.js +48 -2
  8. package/lib/api/controller-health.api.js +49 -0
  9. package/lib/api/dimension-values.api.js +82 -0
  10. package/lib/api/dimensions.api.js +114 -0
  11. package/lib/api/external-systems.api.js +1 -0
  12. package/lib/api/integration-clients.api.js +168 -0
  13. package/lib/api/types/dimension-values.types.js +28 -0
  14. package/lib/api/types/dimensions.types.js +31 -0
  15. package/lib/api/types/integration-clients.types.js +45 -0
  16. package/lib/api/validation-runner.js +46 -25
  17. package/lib/app/deploy-config.js +11 -1
  18. package/lib/app/deploy-status-display.js +3 -3
  19. package/lib/app/deploy.js +36 -14
  20. package/lib/app/display.js +15 -11
  21. package/lib/app/push.js +46 -23
  22. package/lib/app/register.js +1 -1
  23. package/lib/app/restart-display.js +95 -0
  24. package/lib/app/rotate-secret.js +1 -1
  25. package/lib/app/run-container-start.js +12 -6
  26. package/lib/app/run-env-compose.js +30 -1
  27. package/lib/app/run-helpers.js +44 -12
  28. package/lib/app/run-reload-sync.js +148 -0
  29. package/lib/app/run-resolve-image.js +51 -1
  30. package/lib/app/run.js +99 -73
  31. package/lib/build/index.js +75 -45
  32. package/lib/cli/doctor-check.js +117 -0
  33. package/lib/cli/index.js +8 -2
  34. package/lib/cli/infra-guided.js +445 -0
  35. package/lib/cli/setup-app.js +20 -2
  36. package/lib/cli/setup-auth.js +26 -0
  37. package/lib/cli/setup-dev-path-commands.js +50 -3
  38. package/lib/cli/setup-infra.js +134 -61
  39. package/lib/cli/setup-integration-client.js +182 -0
  40. package/lib/cli/setup-parameters.js +21 -2
  41. package/lib/cli/setup-platform.js +102 -0
  42. package/lib/cli/setup-secrets.js +18 -6
  43. package/lib/cli/setup-utility.js +78 -33
  44. package/lib/commands/datasource-capability-dimension-cli.js +128 -0
  45. package/lib/commands/datasource-capability-output.js +29 -0
  46. package/lib/commands/datasource-capability-relate-cli.js +140 -0
  47. package/lib/commands/datasource-capability.js +411 -0
  48. package/lib/commands/datasource-unified-test-cli.options.js +1 -1
  49. package/lib/commands/datasource.js +53 -13
  50. package/lib/commands/dev-down.js +3 -3
  51. package/lib/commands/dev-infra-gate.js +32 -0
  52. package/lib/commands/dev-init.js +13 -7
  53. package/lib/commands/dimension-value.js +179 -0
  54. package/lib/commands/dimension.js +330 -0
  55. package/lib/commands/integration-client.js +430 -0
  56. package/lib/commands/login-device.js +65 -30
  57. package/lib/commands/login.js +21 -10
  58. package/lib/commands/parameters-validate.js +78 -13
  59. package/lib/commands/repair-datasource-auto-rbac.js +166 -0
  60. package/lib/commands/repair-datasource-keys.js +10 -5
  61. package/lib/commands/repair-datasource.js +19 -7
  62. package/lib/commands/repair-env-template.js +4 -1
  63. package/lib/commands/repair-openapi-sync.js +172 -0
  64. package/lib/commands/repair-persist.js +102 -0
  65. package/lib/commands/repair-rbac-extract.js +27 -0
  66. package/lib/commands/repair-rbac-migrate.js +186 -0
  67. package/lib/commands/repair-rbac.js +214 -31
  68. package/lib/commands/repair-system-alignment.js +246 -0
  69. package/lib/commands/repair-system-permissions.js +168 -0
  70. package/lib/commands/repair.js +120 -338
  71. package/lib/commands/secure.js +1 -1
  72. package/lib/commands/setup-modes.js +455 -0
  73. package/lib/commands/setup-prompts.js +388 -0
  74. package/lib/commands/setup.js +149 -0
  75. package/lib/commands/teardown.js +228 -0
  76. package/lib/commands/up-common.js +79 -19
  77. package/lib/commands/up-dataplane.js +33 -11
  78. package/lib/commands/up-miso.js +7 -11
  79. package/lib/commands/upload.js +109 -23
  80. package/lib/commands/wizard-core-helpers.js +14 -11
  81. package/lib/commands/wizard-core.js +6 -5
  82. package/lib/commands/wizard-dataplane.js +2 -2
  83. package/lib/commands/wizard-entity-selection.js +4 -3
  84. package/lib/commands/wizard-headless.js +2 -1
  85. package/lib/commands/wizard.js +2 -1
  86. package/lib/constants/infra-compose-service-names.js +40 -0
  87. package/lib/core/env-reader.js +16 -3
  88. package/lib/core/secrets-admin-env.js +101 -0
  89. package/lib/core/secrets-ensure-infra.js +34 -1
  90. package/lib/core/secrets-ensure.js +88 -66
  91. package/lib/core/secrets-env-content.js +432 -0
  92. package/lib/core/secrets-env-write.js +27 -1
  93. package/lib/core/secrets-load.js +248 -0
  94. package/lib/core/secrets-names.js +32 -0
  95. package/lib/core/secrets.js +17 -757
  96. package/lib/datasource/capability/basic-exposure.js +76 -0
  97. package/lib/datasource/capability/capability-diff-slice.js +41 -0
  98. package/lib/datasource/capability/capability-key.js +34 -0
  99. package/lib/datasource/capability/capability-resolve.js +172 -0
  100. package/lib/datasource/capability/capability-storage-keys.js +22 -0
  101. package/lib/datasource/capability/copy-operations.js +348 -0
  102. package/lib/datasource/capability/copy-test-payload.js +139 -0
  103. package/lib/datasource/capability/create-operations.js +235 -0
  104. package/lib/datasource/capability/dimension-operations.js +151 -0
  105. package/lib/datasource/capability/dimension-validate.js +219 -0
  106. package/lib/datasource/capability/json-pointer.js +31 -0
  107. package/lib/datasource/capability/reference-rewrite.js +51 -0
  108. package/lib/datasource/capability/relate-operations.js +325 -0
  109. package/lib/datasource/capability/relate-validate.js +219 -0
  110. package/lib/datasource/capability/remove-operations.js +275 -0
  111. package/lib/datasource/capability/run-capability-copy.js +152 -0
  112. package/lib/datasource/capability/run-capability-diff.js +135 -0
  113. package/lib/datasource/capability/run-capability-dimension.js +291 -0
  114. package/lib/datasource/capability/run-capability-edit.js +377 -0
  115. package/lib/datasource/capability/run-capability-relate.js +193 -0
  116. package/lib/datasource/capability/run-capability-remove.js +105 -0
  117. package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
  118. package/lib/datasource/capability/validate-capability-slice.js +35 -0
  119. package/lib/datasource/list.js +136 -23
  120. package/lib/datasource/log-viewer.js +2 -4
  121. package/lib/datasource/unified-validation-run.js +51 -16
  122. package/lib/datasource/validate.js +53 -1
  123. package/lib/deployment/deploy-poll-ui.js +60 -0
  124. package/lib/deployment/deployer-status.js +29 -3
  125. package/lib/deployment/deployer.js +48 -30
  126. package/lib/deployment/environment.js +7 -2
  127. package/lib/deployment/poll-interval.js +72 -0
  128. package/lib/deployment/push.js +11 -9
  129. package/lib/external-system/deploy.js +4 -1
  130. package/lib/external-system/download.js +61 -32
  131. package/lib/external-system/sync-deploy-manifest.js +33 -0
  132. package/lib/infrastructure/index.js +49 -19
  133. package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
  134. package/lib/parameters/infra-kv-discovery.js +29 -4
  135. package/lib/parameters/infra-parameter-catalog.js +6 -3
  136. package/lib/parameters/infra-parameter-validate.js +67 -19
  137. package/lib/resolvers/datasource-resolver.js +53 -0
  138. package/lib/resolvers/dimension-file.js +52 -0
  139. package/lib/resolvers/manifest-resolver.js +133 -0
  140. package/lib/schema/external-datasource.schema.json +183 -53
  141. package/lib/schema/external-system.schema.json +23 -10
  142. package/lib/schema/infra.parameter.yaml +26 -11
  143. package/lib/schema/wizard-config.schema.json +1 -1
  144. package/lib/utils/aifabrix-config-dir-walk.js +40 -0
  145. package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
  146. package/lib/utils/app-run-containers.js +2 -2
  147. package/lib/utils/bash-secret-env.js +59 -0
  148. package/lib/utils/cli-secrets-error-format.js +78 -0
  149. package/lib/utils/cli-test-layout-chalk.js +31 -9
  150. package/lib/utils/cli-utils.js +4 -36
  151. package/lib/utils/datasource-test-run-display.js +8 -0
  152. package/lib/utils/dev-hosts-helper.js +3 -2
  153. package/lib/utils/dev-init-ssh-merge.js +2 -1
  154. package/lib/utils/docker-build.js +17 -9
  155. package/lib/utils/docker-reload-mount.js +127 -0
  156. package/lib/utils/external-readme.js +71 -2
  157. package/lib/utils/external-system-local-test-tty.js +3 -2
  158. package/lib/utils/external-system-readiness-core.js +45 -12
  159. package/lib/utils/external-system-readiness-deploy-display.js +3 -3
  160. package/lib/utils/external-system-readiness-display-internals.js +33 -3
  161. package/lib/utils/external-system-readiness-display.js +10 -1
  162. package/lib/utils/file-upload.js +40 -3
  163. package/lib/utils/health-check-db-init.js +107 -0
  164. package/lib/utils/health-check-public-warn.js +69 -0
  165. package/lib/utils/health-check-url.js +19 -4
  166. package/lib/utils/health-check.js +135 -105
  167. package/lib/utils/help-builder.js +5 -1
  168. package/lib/utils/image-name.js +34 -7
  169. package/lib/utils/integration-file-backup.js +74 -0
  170. package/lib/utils/mutagen-install.js +30 -3
  171. package/lib/utils/paths.js +108 -25
  172. package/lib/utils/postgres-wipe.js +212 -0
  173. package/lib/utils/register-aifabrix-shell-env.js +15 -0
  174. package/lib/utils/remote-dev-auth.js +21 -5
  175. package/lib/utils/remote-docker-env.js +9 -1
  176. package/lib/utils/remote-secrets-loader.js +42 -3
  177. package/lib/utils/resolve-docker-image-ref.js +9 -3
  178. package/lib/utils/secrets-ancestor-paths.js +47 -0
  179. package/lib/utils/secrets-helpers.js +17 -10
  180. package/lib/utils/secrets-kv-refs.js +42 -0
  181. package/lib/utils/secrets-kv-scope.js +19 -2
  182. package/lib/utils/secrets-materialize-local.js +134 -0
  183. package/lib/utils/secrets-path.js +24 -10
  184. package/lib/utils/secrets-utils.js +2 -2
  185. package/lib/utils/system-builder-root.js +34 -0
  186. package/lib/utils/url-declarative-resolve-build.js +6 -1
  187. package/lib/utils/url-declarative-runtime-base-path.js +32 -0
  188. package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
  189. package/lib/utils/urls-local-registry.js +23 -12
  190. package/lib/utils/validation-poll-ui.js +81 -0
  191. package/lib/utils/validation-run-poll.js +29 -5
  192. package/lib/utils/with-muted-logger.js +53 -0
  193. package/package.json +1 -1
  194. package/templates/applications/dataplane/application.yaml +1 -1
  195. package/templates/applications/dataplane/rbac.yaml +10 -10
  196. package/templates/applications/keycloak/env.template +8 -6
  197. package/templates/applications/miso-controller/application.yaml +7 -0
  198. package/templates/applications/miso-controller/env.template +1 -1
  199. package/templates/applications/miso-controller/rbac.yaml +9 -9
  200. package/templates/external-system/README.md.hbs +83 -123
  201. package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  202. package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
  203. package/.nyc_output/processinfo/index.json +0 -1
  204. package/lib/api/service-users.api.js +0 -150
  205. package/lib/api/types/service-users.types.js +0 -65
  206. package/lib/cli/setup-service-user.js +0 -187
  207. package/lib/commands/service-user.js +0 -429
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Optional capability-key checks after schema validation.
3
+ *
4
+ * @fileoverview capability validate command helpers
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const { normalizeCapabilityKey } = require('./capability-key');
10
+
11
+ /**
12
+ * Lists structural locations where a capability key should appear.
13
+ *
14
+ * @param {object} parsed - Datasource JSON
15
+ * @param {string} rawKey - Capability key
16
+ * @returns {{ key: string, missing: string[] }}
17
+ */
18
+ function checkCapabilitySlices(parsed, rawKey) {
19
+ const key = normalizeCapabilityKey(rawKey, 'capability');
20
+ const missing = [];
21
+ if (!Array.isArray(parsed.capabilities) || !parsed.capabilities.includes(key)) {
22
+ missing.push(`capabilities[] (missing "${key}")`);
23
+ }
24
+ if (!parsed.openapi?.operations?.[key]) {
25
+ missing.push(`openapi.operations.${key}`);
26
+ }
27
+ if (!parsed.execution?.cip?.operations?.[key]) {
28
+ missing.push(`execution.cip.operations.${key}`);
29
+ }
30
+ return { key, missing };
31
+ }
32
+
33
+ module.exports = {
34
+ checkCapabilitySlices
35
+ };
@@ -3,7 +3,10 @@ const { formatBlockingError } = require('../utils/cli-test-layout-chalk');
3
3
  * Datasource List Command
4
4
  *
5
5
  * Lists datasources from the dataplane (GET /api/v1/external).
6
- * Resolves dataplane URL from the controller, then calls the dataplane list API.
6
+ * Resolves dataplane URL from the controller, then calls the dataplane list API with
7
+ * pagination (pageSize up to API max) so results are not capped at the first page.
8
+ * Optional key prefix uses API filter `key:like:<prefix>%` (SQL LIKE prefix) plus a
9
+ * client-side safety pass on the merged rows.
7
10
  *
8
11
  * @fileoverview Datasource listing for AI Fabrix Builder
9
12
  * @author AI Fabrix Team
@@ -18,6 +21,90 @@ const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
18
21
  const { formatApiError } = require('../utils/api-error-handler');
19
22
  const logger = require('../utils/logger');
20
23
 
24
+ /** Dataplane GET /api/v1/external allows pageSize up to 100 */
25
+ const LIST_DATASOURCE_PAGE_SIZE = 100;
26
+
27
+ /**
28
+ * Filter query fragment for datasource keys that start with a prefix (dataplane list API).
29
+ * @param {string} prefix - Trimmed non-empty prefix
30
+ * @returns {string}
31
+ */
32
+ function buildKeyPrefixFilterParam(prefix) {
33
+ return `key:like:${prefix}%`;
34
+ }
35
+
36
+ /**
37
+ * @param {Object|null|undefined} meta - Response `meta` object when present
38
+ * @returns {number|null}
39
+ */
40
+ function readTotalPagesFromMeta(meta) {
41
+ if (meta && typeof meta.totalPages === 'number' && meta.totalPages > 0) {
42
+ return meta.totalPages;
43
+ }
44
+ return null;
45
+ }
46
+
47
+ /**
48
+ * @param {number} batchLen
49
+ * @param {number} pageSize
50
+ * @param {number} page - 1-based page index
51
+ * @param {number|null} totalPages
52
+ * @returns {boolean}
53
+ */
54
+ function shouldStopDatasourcePaging(batchLen, pageSize, page, totalPages) {
55
+ if (batchLen < pageSize) return true;
56
+ if (typeof totalPages === 'number' && page >= totalPages) return true;
57
+ return false;
58
+ }
59
+
60
+ /**
61
+ * Loads every page of datasources from the dataplane for the current query.
62
+ * @async
63
+ * @param {string} dataplaneUrl
64
+ * @param {Object} authConfig
65
+ * @param {string|null} keyPrefix - Trimmed prefix or null for full list
66
+ * @returns {Promise<Array<Object>>}
67
+ */
68
+ async function fetchAllDatasourcePages(dataplaneUrl, authConfig, keyPrefix) {
69
+ const merged = [];
70
+ let page = 1;
71
+ for (;;) {
72
+ if (page > 10000) break;
73
+ const params = { page, pageSize: LIST_DATASOURCE_PAGE_SIZE };
74
+ if (keyPrefix) params.filter = buildKeyPrefixFilterParam(keyPrefix);
75
+ const response = await listDatasourcesFromDataplane(dataplaneUrl, authConfig, params);
76
+ if (!response.success || !response.data) {
77
+ const err = new Error('LIST_DATASOURCES_FAILED');
78
+ err.response = response;
79
+ throw err;
80
+ }
81
+ const batch = extractDatasources(response);
82
+ if (!batch.length) break;
83
+ merged.push(...batch);
84
+ const totalPages = readTotalPagesFromMeta(response.data.meta);
85
+ if (shouldStopDatasourcePaging(batch.length, LIST_DATASOURCE_PAGE_SIZE, page, totalPages)) {
86
+ break;
87
+ }
88
+ page += 1;
89
+ }
90
+ return merged;
91
+ }
92
+
93
+ /**
94
+ * @returns {Promise<Array<Object>|null>} Rows or null if API failure was handled (exit)
95
+ */
96
+ async function fetchDatasourcesForList(dataplaneUrl, authConfig, keyPrefix) {
97
+ try {
98
+ return await fetchAllDatasourcePages(dataplaneUrl, authConfig, keyPrefix);
99
+ } catch (err) {
100
+ if (err && err.response) {
101
+ handleDatasourceApiError(err.response, dataplaneUrl);
102
+ return null;
103
+ }
104
+ throw err;
105
+ }
106
+ }
107
+
21
108
  /**
22
109
  * Extracts datasources array from API response
23
110
  * Handles multiple response formats similar to applications list
@@ -100,21 +187,47 @@ function extractDatasources(response) {
100
187
  return datasources;
101
188
  }
102
189
 
190
+ /**
191
+ * Keeps datasources whose `key` starts with the given prefix (trimmed). No-op if prefix empty.
192
+ *
193
+ * @param {Array<Object>} datasources - Datasource objects from API
194
+ * @param {string} [prefix] - Match against datasource key (prefix match)
195
+ * @returns {Array<Object>}
196
+ */
197
+ function filterDatasourcesByKeyPrefix(datasources, prefix) {
198
+ if (!prefix || typeof prefix !== 'string') return datasources;
199
+ const p = prefix.trim();
200
+ if (!p) return datasources;
201
+ return datasources.filter((ds) => {
202
+ const k = String(ds?.key ?? '');
203
+ return k.startsWith(p);
204
+ });
205
+ }
206
+
103
207
  /**
104
208
  * Displays datasources in a formatted table
105
209
  *
106
210
  * @function displayDatasources
107
211
  * @param {Array} datasources - Array of datasource objects
108
212
  * @param {string} environment - Environment key
109
- * @param {string} dataplaneUrl - Dataplane URL for header display
213
+ * @param {string} [dataplaneUrl] - Dataplane URL for header display
214
+ * @param {string|null} [keyPrefix] - When set, header notes filter on datasource key
110
215
  */
111
- function displayDatasources(datasources, environment, dataplaneUrl) {
216
+ function displayDatasources(datasources, environment, dataplaneUrl, keyPrefix = null) {
112
217
  const environmentName = environment || 'dev';
113
- const header = `Datasources in ${environmentName} environment${dataplaneUrl ? ` (${dataplaneUrl})` : ''}`;
218
+ const prefixNote =
219
+ keyPrefix && String(keyPrefix).trim()
220
+ ? ` — datasource keys starting with "${String(keyPrefix).trim()}"`
221
+ : '';
222
+ const header = `Datasources in ${environmentName} environment${dataplaneUrl ? ` (${dataplaneUrl})` : ''}${prefixNote}`;
114
223
 
115
224
  if (datasources.length === 0) {
116
225
  logger.log(chalk.bold(`\n📋 ${header}:\n`));
117
- logger.log(chalk.gray(' No datasources found in this environment.\n'));
226
+ const emptyDetail =
227
+ keyPrefix && String(keyPrefix).trim()
228
+ ? ` No datasources with keys starting with "${String(keyPrefix).trim()}".\n`
229
+ : ' No datasources found in this environment.\n';
230
+ logger.log(chalk.gray(emptyDetail));
118
231
  return;
119
232
  }
120
233
 
@@ -133,14 +246,6 @@ function displayDatasources(datasources, environment, dataplaneUrl) {
133
246
  logger.log('');
134
247
  }
135
248
 
136
- /**
137
- * Lists datasources from an environment
138
- *
139
- * @async
140
- * @function listDatasources
141
- * @param {Object} _options - Command options (unused, kept for compatibility)
142
- * @throws {Error} If listing fails
143
- */
144
249
  /**
145
250
  * Gets device token from config
146
251
  * @async
@@ -275,7 +380,11 @@ function setupAuthConfig(token, controllerUrl) {
275
380
  };
276
381
  }
277
382
 
278
- async function listDatasources(_options) {
383
+ /**
384
+ * @param {Object} [options]
385
+ * @param {string} [options.keyPrefix] - If set, only datasources whose `key` starts with this string (after trim)
386
+ */
387
+ async function listDatasources(options = {}) {
279
388
  const config = await getConfig();
280
389
 
281
390
  // Resolve environment from config.yaml (no flags)
@@ -296,21 +405,25 @@ async function listDatasources(_options) {
296
405
  // Resolve dataplane URL first (required for list call)
297
406
  const dataplaneUrl = await resolveAndValidateDataplaneUrl(controllerUrl, environment, authConfig);
298
407
 
299
- // List datasources from dataplane (GET /api/v1/external)
300
- const response = await listDatasourcesFromDataplane(dataplaneUrl, authConfig);
408
+ const rawPrefix = options.keyPrefix;
409
+ const keyPrefix =
410
+ typeof rawPrefix === 'string' && rawPrefix.trim() ? rawPrefix.trim() : null;
301
411
 
302
- if (!response.success || !response.data) {
303
- handleDatasourceApiError(response, dataplaneUrl);
304
- return; // Ensure we don't continue after exit
305
- }
412
+ let datasources = await fetchDatasourcesForList(dataplaneUrl, authConfig, keyPrefix);
413
+ if (datasources === null) return;
306
414
 
307
- const datasources = extractDatasources(response);
308
- displayDatasources(datasources, environment, dataplaneUrl);
415
+ if (keyPrefix) {
416
+ datasources = filterDatasourcesByKeyPrefix(datasources, keyPrefix);
417
+ }
418
+ displayDatasources(datasources, environment, dataplaneUrl, keyPrefix);
309
419
  }
310
420
 
311
421
  module.exports = {
312
422
  listDatasources,
313
423
  displayDatasources,
314
- extractDatasources
424
+ extractDatasources,
425
+ fetchAllDatasourcePages,
426
+ buildKeyPrefixFilterParam,
427
+ filterDatasourcesByKeyPrefix
315
428
  };
316
429
 
@@ -320,10 +320,8 @@ function parseDatasourceLogJson(content, logPath) {
320
320
  async function runLogViewer(datasourceKey, options) {
321
321
  const { app, file, logType } = options;
322
322
  let logPath;
323
- let fileName;
324
323
  if (file && typeof file === 'string' && file.trim()) {
325
324
  logPath = path.isAbsolute(file) ? file : path.resolve(process.cwd(), file.trim());
326
- fileName = path.basename(logPath);
327
325
  } else {
328
326
  if (!datasourceKey || typeof datasourceKey !== 'string') {
329
327
  throw new Error('Datasource key is required when --file is not provided');
@@ -347,11 +345,11 @@ async function runLogViewer(datasourceKey, options) {
347
345
  );
348
346
  }
349
347
  }
350
- fileName = path.basename(logPath);
351
348
  }
349
+ const resolvedLogPath = path.resolve(logPath);
352
350
  const content = await fsp.readFile(logPath, 'utf8');
353
351
  const parsed = parseDatasourceLogJson(content, logPath);
354
- formatLogContent(parsed, logType, fileName);
352
+ formatLogContent(parsed, logType, resolvedLogPath);
355
353
  }
356
354
 
357
355
  module.exports = {
@@ -16,11 +16,53 @@ const { getSystemKeyFromAppKey, findDatasourceFileByKey } = require('./integrati
16
16
  const { loadDatasourceForApp } = require('./unified-validation-run-resolve');
17
17
  const { buildUnifiedValidationBody } = require('./unified-validation-run-body');
18
18
  const { postValidationRunAndOptionalPoll } = require('./unified-validation-run-post');
19
- const { publishDatasourceViaPipeline } = require('../api/pipeline.api');
20
19
  const { requireBearerForDataplanePipeline } = require('../utils/token-manager');
21
- const { formatApiError } = require('../utils/api-error-handler');
22
20
  const logger = require('../utils/logger');
23
21
 
22
+ function _resourceTypeFromDatasource(datasource) {
23
+ return datasource && typeof datasource === 'object' && typeof datasource.resourceType === 'string'
24
+ ? datasource.resourceType
25
+ : null;
26
+ }
27
+
28
+ function _extractOpKeyFromDpSec013Message(msg, datasourceKey) {
29
+ if (typeof msg !== 'string' || !msg.trim()) return '';
30
+ const m = msg.match(new RegExp(`^${datasourceKey}:([^\\s-]+)\\s*-`));
31
+ return m && m[1] ? String(m[1]).trim() : '';
32
+ }
33
+
34
+ function annotateMissingAutoRbacPermissions(envelope, datasourceKey, datasource) {
35
+ const issues =
36
+ envelope && envelope.validation && Array.isArray(envelope.validation.issues)
37
+ ? envelope.validation.issues
38
+ : [];
39
+ if (!issues.length) return;
40
+
41
+ const resourceType = _resourceTypeFromDatasource(datasource);
42
+ if (!resourceType) return;
43
+
44
+ for (const iss of issues) {
45
+ if (!iss || iss.code !== 'DP-SEC-013' || typeof iss.message !== 'string') continue;
46
+ const operationKey = _extractOpKeyFromDpSec013Message(iss.message, datasourceKey);
47
+ if (!operationKey) continue;
48
+
49
+ iss.details = iss.details && typeof iss.details === 'object' ? iss.details : {};
50
+ iss.details.resolvedPermission = `${resourceType}:${operationKey}`;
51
+ }
52
+ }
53
+
54
+ async function maybeSyncDatasourceToDataplane({ options, authConfig, systemKey }) {
55
+ if (options.sync !== true) return;
56
+ requireBearerForDataplanePipeline(authConfig);
57
+ logger.log(chalk.cyan('Syncing local config to dataplane…'));
58
+ // Publish full external system (system + all datasources) to ensure system-level RBAC/permissions
59
+ // are updated before validation runs. Datasource-only publish can still fail DP-SEC-013 when the
60
+ // dataplane system permissions[] are stale.
61
+ const { uploadExternalSystem } = require('../commands/upload');
62
+ await uploadExternalSystem(systemKey, { minimal: true });
63
+ logger.log(formatSuccessLine('Sync complete'));
64
+ }
65
+
24
66
  /**
25
67
  * Resolve datasource JSON path and loaded object (same rules as test-integration).
26
68
  * Re-export for tests.
@@ -49,19 +91,7 @@ async function runUnifiedDatasourceValidation(datasourceKey, options) {
49
91
  const configObj = await getConfig();
50
92
  const { authConfig, dataplaneUrl } = await setupIntegrationTestAuth(appKey, options, configObj);
51
93
 
52
- if (options.sync === true) {
53
- requireBearerForDataplanePipeline(authConfig);
54
- logger.log(chalk.cyan('Syncing local config to dataplane…'));
55
- const publishResponse = await publishDatasourceViaPipeline(dataplaneUrl, systemKey, authConfig, datasource);
56
- if (!publishResponse || publishResponse.success === false) {
57
- const msg =
58
- (publishResponse && (publishResponse.formattedError || publishResponse.error)) ||
59
- formatApiError(publishResponse, dataplaneUrl) ||
60
- 'Publish failed';
61
- throw new Error(`Sync failed: ${msg}`);
62
- }
63
- logger.log(formatSuccessLine('Sync complete'));
64
- }
94
+ await maybeSyncDatasourceToDataplane({ options, authConfig, systemKey });
65
95
 
66
96
  const useAsync = options.noAsync ? false : options.async !== false;
67
97
  const timeoutMs = parseInt(String(options.timeout || '30000'), 10) || 30000;
@@ -76,7 +106,7 @@ async function runUnifiedDatasourceValidation(datasourceKey, options) {
76
106
  options
77
107
  });
78
108
 
79
- return postValidationRunAndOptionalPoll({
109
+ const result = await postValidationRunAndOptionalPoll({
80
110
  dataplaneUrl,
81
111
  authConfig,
82
112
  body,
@@ -85,6 +115,11 @@ async function runUnifiedDatasourceValidation(datasourceKey, options) {
85
115
  noAsync: options.noAsync === true || options.async === false,
86
116
  verbosePoll: options.verbose === true
87
117
  });
118
+
119
+ if (result && result.envelope) {
120
+ annotateMissingAutoRbacPermissions(result.envelope, datasourceKey, datasource);
121
+ }
122
+ return result;
88
123
  }
89
124
 
90
125
  module.exports = {
@@ -229,8 +229,60 @@ async function validateDatasourceFile(filePathOrKey) {
229
229
  };
230
230
  }
231
231
 
232
+ /**
233
+ * Validates parsed datasource JSON (same rules as {@link validateDatasourceFile}, without reading a file).
234
+ *
235
+ * @param {Object} parsed - Parsed datasource object
236
+ * @returns {{ valid: boolean, errors: string[], warnings: string[], summary: Object|null }}
237
+ */
238
+ function validateDatasourceParsed(parsed) {
239
+ if (!parsed || typeof parsed !== 'object') {
240
+ return {
241
+ valid: false,
242
+ errors: ['Datasource JSON must be an object'],
243
+ warnings: [],
244
+ summary: null
245
+ };
246
+ }
247
+
248
+ const summary = buildDatasourceValidateSummary(parsed);
249
+
250
+ const validate = loadExternalDataSourceSchema();
251
+ const schemaValid = validate(parsed);
252
+
253
+ if (!schemaValid) {
254
+ return {
255
+ valid: false,
256
+ errors: formatValidationErrors(validate.errors, { rootData: parsed, dedupe: true }),
257
+ warnings: [],
258
+ summary
259
+ };
260
+ }
261
+
262
+ const fieldRefErrors = validateFieldReferences(parsed);
263
+ const abacErrors = validateAbac(parsed);
264
+ const postSchemaErrors = [...fieldRefErrors, ...abacErrors];
265
+ if (postSchemaErrors.length > 0) {
266
+ return {
267
+ valid: false,
268
+ errors: postSchemaErrors,
269
+ warnings: [],
270
+ summary
271
+ };
272
+ }
273
+
274
+ return {
275
+ valid: true,
276
+ errors: [],
277
+ warnings: [],
278
+ summary
279
+ };
280
+ }
281
+
232
282
  module.exports = {
233
283
  validateDatasourceFile,
234
- resolveValidateInputPath
284
+ validateDatasourceParsed,
285
+ resolveValidateInputPath,
286
+ resolvePathFromIntegrationDatasourceKey
235
287
  };
236
288
 
@@ -0,0 +1,60 @@
1
+ /**
2
+ * TTY ora spinner + non-TTY lines for deploy pipeline polling (aligned with guided infra commands).
3
+ *
4
+ * @fileoverview Deploy poll presentation helpers
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const chalk = require('chalk');
12
+ const ora = require('ora');
13
+ const logger = require('../utils/logger');
14
+ const { buildDeployPollSpinnerText } = require('./deployer-status');
15
+
16
+ function shouldUseDeployPollSpinner() {
17
+ return Boolean(process && process.stdout && process.stdout.isTTY);
18
+ }
19
+
20
+ /**
21
+ * @param {number} maxAttempts - Max polling attempts (shown in UI)
22
+ * @param {{ silent?: boolean }} [options]
23
+ * @returns {{ onPollProgress: Function, finish: () => void }}
24
+ */
25
+ function createDeployPollHandlers(maxAttempts, options = {}) {
26
+ const silent = options && options.silent === true;
27
+ if (silent) {
28
+ return {
29
+ onPollProgress: () => {},
30
+ finish: () => {}
31
+ };
32
+ }
33
+ if (shouldUseDeployPollSpinner()) {
34
+ const spinner = ora({
35
+ text: buildDeployPollSpinnerText(null, 0, maxAttempts),
36
+ spinner: 'dots'
37
+ }).start();
38
+ return {
39
+ onPollProgress: (deploymentData, attempt, maxA) => {
40
+ spinner.text = buildDeployPollSpinnerText(deploymentData, attempt, maxA);
41
+ },
42
+ finish: () => {
43
+ spinner.stop();
44
+ }
45
+ };
46
+ }
47
+ return {
48
+ onPollProgress: (deploymentData, attempt, maxA) => {
49
+ const status = deploymentData.status ?? 'pending';
50
+ const progress = deploymentData.progress ?? 0;
51
+ logger.log(chalk.gray(`Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxA})`));
52
+ },
53
+ finish: () => {}
54
+ };
55
+ }
56
+
57
+ module.exports = {
58
+ createDeployPollHandlers,
59
+ shouldUseDeployPollSpinner
60
+ };
@@ -53,7 +53,7 @@ function extractDeploymentData(response) {
53
53
  }
54
54
 
55
55
  /**
56
- * Logs deployment progress
56
+ * Logs deployment progress (non-spinner / programmatic polling)
57
57
  * @param {Object} deploymentData - Deployment data
58
58
  * @param {number} attempt - Current attempt
59
59
  * @param {number} maxAttempts - Maximum attempts
@@ -64,6 +64,19 @@ function logDeploymentProgress(deploymentData, attempt, maxAttempts) {
64
64
  logger.log(chalk.blue(` Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`));
65
65
  }
66
66
 
67
+ /**
68
+ * Single-line ora text for deploy polling (updates in place; same style as guided infra spinners).
69
+ * @param {Object|null|undefined} deploymentData - Latest deployment payload (optional before first response)
70
+ * @param {number} attempt - Zero-based attempt index
71
+ * @param {number} maxAttempts - Max polling attempts
72
+ * @returns {string}
73
+ */
74
+ function buildDeployPollSpinnerText(deploymentData, attempt, maxAttempts) {
75
+ const status = deploymentData?.status ?? 'pending';
76
+ const progress = deploymentData && Number.isFinite(Number(deploymentData.progress)) ? Number(deploymentData.progress) : 0;
77
+ return `Deploying application... Status: ${status} (${progress}%) (attempt ${attempt + 1}/${maxAttempts})`;
78
+ }
79
+
67
80
  /**
68
81
  * Process deployment status response
69
82
  * @param {Object} response - API response
@@ -71,9 +84,17 @@ function logDeploymentProgress(deploymentData, attempt, maxAttempts) {
71
84
  * @param {number} maxAttempts - Maximum attempts
72
85
  * @param {number} interval - Polling interval
73
86
  * @param {string} deploymentId - Deployment ID for error messages
87
+ * @param {Function|null} [onProgress] - If set, called instead of logDeploymentProgress with (deploymentData, attempt, maxAttempts)
74
88
  * @returns {Promise<Object|null>} Deployment data if terminal, null if needs to continue polling
75
89
  */
76
- async function processDeploymentStatusResponse(response, attempt, maxAttempts, interval, deploymentId) {
90
+ async function processDeploymentStatusResponse(
91
+ response,
92
+ attempt,
93
+ maxAttempts,
94
+ interval,
95
+ deploymentId,
96
+ onProgress = null
97
+ ) {
77
98
  if (!response.success || !response.data) {
78
99
  handleDeploymentStatusError(response, deploymentId);
79
100
  }
@@ -83,7 +104,11 @@ async function processDeploymentStatusResponse(response, attempt, maxAttempts, i
83
104
  return deploymentData;
84
105
  }
85
106
 
86
- logDeploymentProgress(deploymentData, attempt, maxAttempts);
107
+ if (typeof onProgress === 'function') {
108
+ onProgress(deploymentData, attempt, maxAttempts);
109
+ } else {
110
+ logDeploymentProgress(deploymentData, attempt, maxAttempts);
111
+ }
87
112
  if (attempt < maxAttempts - 1) {
88
113
  await new Promise(resolve => setTimeout(resolve, interval));
89
114
  }
@@ -97,5 +122,6 @@ module.exports = {
97
122
  handleDeploymentStatusError,
98
123
  extractDeploymentData,
99
124
  logDeploymentProgress,
125
+ buildDeployPollSpinnerText,
100
126
  processDeploymentStatusResponse
101
127
  };