@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
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @fileoverview TTY meta header lines for DatasourceTestRun (extracted for max-lines).
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ const chalk = require('chalk');
10
+ const { headerKeyValue } = require('./cli-test-layout-chalk');
11
+ const { pushCapacityOperationsSummaryLines } = require('./datasource-test-run-capacity-operations');
12
+
13
+ /** @type {number} */
14
+ const DEFAULT_MAX_REF_CHARS = 200;
15
+
16
+ // UI-only: round float seconds tokens like "1.1713749s" -> "1.171s".
17
+ function formatSecondsText(text) {
18
+ const txt = String(text);
19
+ return txt.replace(/(\d+\.\d+)(?=s\b)/g, m => {
20
+ const n = Number(m);
21
+ if (!Number.isFinite(n)) return m;
22
+ return n.toFixed(3);
23
+ });
24
+ }
25
+
26
+ /**
27
+ * @param {string} str
28
+ * @param {number} maxChars
29
+ * @returns {string}
30
+ */
31
+ function truncateRefLine(str, maxChars) {
32
+ const s = String(str);
33
+ if (s.length <= maxChars) return s;
34
+ return `${s.slice(0, Math.max(0, maxChars - 24))}… [+${s.length - maxChars + 24} chars]`;
35
+ }
36
+
37
+ /**
38
+ * @param {string[]} lines
39
+ * @param {Object} envelope
40
+ */
41
+ function pushTtyMetaRunWallLine(lines, envelope) {
42
+ if (envelope.cliWallSeconds === undefined || envelope.cliWallSeconds === null) return;
43
+ const n = Number(envelope.cliWallSeconds);
44
+ if (Number.isFinite(n) && n >= 0) {
45
+ lines.push(`${chalk.gray('Run wall:')} ${chalk.white(`~${n}s`)}`);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * @param {string[]} lines
51
+ * @param {Object} envelope
52
+ */
53
+ function pushTtyMetaCapabilitiesPreviewLine(lines, envelope) {
54
+ const caps = Array.isArray(envelope.capabilities) ? envelope.capabilities : [];
55
+ if (caps.length === 0) return;
56
+ const keys = caps
57
+ .map(c => (c && c.key !== undefined && c.key !== null ? String(c.key) : ''))
58
+ .filter(Boolean);
59
+ if (keys.length === 0) return;
60
+ const preview = keys.slice(0, 10).join(', ');
61
+ const more = keys.length > 10 ? ', …' : '';
62
+ lines.push(`${chalk.gray('Capabilities tested:')} ${chalk.white(`${preview}${more}`)}`);
63
+ }
64
+
65
+ /**
66
+ * @param {string[]} lines
67
+ * @param {Object} envelope
68
+ */
69
+ function pushTtyMetaDebugExecutionSummaryLine(lines, envelope) {
70
+ if (!envelope.debug || typeof envelope.debug !== 'object' || !envelope.debug.executionSummary) return;
71
+ lines.push(
72
+ `${chalk.blue.bold('debug.executionSummary:')} ${chalk.white(
73
+ truncateRefLine(formatSecondsText(envelope.debug.executionSummary), DEFAULT_MAX_REF_CHARS)
74
+ )}`
75
+ );
76
+ }
77
+
78
+ /**
79
+ * @param {string[]} lines
80
+ * @param {Object} envelope
81
+ */
82
+ function pushTtyMetaRunIdLine(lines, envelope) {
83
+ const rid = envelope.runId || envelope.testRunId;
84
+ if (rid) lines.push(`${chalk.gray('Run ID:')} ${chalk.cyan(String(rid))}`);
85
+ }
86
+
87
+ /**
88
+ * @param {string[]} lines
89
+ * @param {Object} envelope
90
+ */
91
+ function pushTtyMetaReportCompletenessLine(lines, envelope) {
92
+ if (!envelope.reportCompleteness || envelope.reportCompleteness === 'full') return;
93
+ lines.push(`${chalk.gray('Report:')} ${chalk.yellow(String(envelope.reportCompleteness))}`);
94
+ }
95
+
96
+ /**
97
+ * @param {Object} envelope
98
+ * @param {(e: Object) => string} formatEnvelopeStatusLine
99
+ * @param {{ includeDebugExecutionSummary?: boolean }} [options]
100
+ * @returns {string[]}
101
+ */
102
+ function buildTtyMetaLines(envelope, formatEnvelopeStatusLine, options = {}) {
103
+ const includeDebugExecutionSummary = options.includeDebugExecutionSummary === true;
104
+ const lines = [];
105
+ lines.push(
106
+ headerKeyValue('Datasource:', `${envelope.datasourceKey} (${envelope.systemKey})`)
107
+ );
108
+ lines.push(headerKeyValue('Run:', String(envelope.runType)));
109
+ pushTtyMetaRunWallLine(lines, envelope);
110
+ pushTtyMetaCapabilitiesPreviewLine(lines, envelope);
111
+ lines.push(formatEnvelopeStatusLine(envelope));
112
+ pushCapacityOperationsSummaryLines(lines, envelope);
113
+ if (includeDebugExecutionSummary) {
114
+ pushTtyMetaDebugExecutionSummaryLine(lines, envelope);
115
+ }
116
+ pushTtyMetaRunIdLine(lines, envelope);
117
+ pushTtyMetaReportCompletenessLine(lines, envelope);
118
+ return lines;
119
+ }
120
+
121
+ module.exports = {
122
+ buildTtyMetaLines
123
+ };
@@ -265,13 +265,38 @@ function formatSingleError(error, options) {
265
265
  return message || `${field}: ${error.message || 'Validation error'}`;
266
266
  }
267
267
 
268
+ /**
269
+ * Removes exact duplicate lines while preserving first-seen order (AJV composite schemas often repeat the same message).
270
+ * @param {string[]} strings
271
+ * @returns {string[]}
272
+ */
273
+ function dedupeFormattedMessagesPreserveOrder(strings) {
274
+ if (!Array.isArray(strings)) {
275
+ return strings;
276
+ }
277
+ const seen = new Set();
278
+ const out = [];
279
+ for (const s of strings) {
280
+ if (typeof s !== 'string') {
281
+ continue;
282
+ }
283
+ if (seen.has(s)) {
284
+ continue;
285
+ }
286
+ seen.add(s);
287
+ out.push(s);
288
+ }
289
+ return out;
290
+ }
291
+
268
292
  /**
269
293
  * Formats validation errors into developer-friendly messages
270
294
  * Converts technical schema errors into actionable advice
271
295
  *
272
296
  * @function formatValidationErrors
273
297
  * @param {Array} errors - Raw validation errors from Ajv
274
- * @param {Object} [options] - Optional; pass `{ deploymentManifest }` or `{ rootData }` for RBAC/pattern detail
298
+ * @param {Object} [options] - Optional; pass `{ deploymentManifest }` or `{ rootData }` for RBAC/pattern detail.
299
+ * Pass `{ dedupe: true }` to remove exact duplicate lines (AJV `allOf`/`if-then` often repeats the same message).
275
300
  * @returns {Array} Formatted error messages
276
301
  *
277
302
  * @example
@@ -283,7 +308,11 @@ function formatValidationErrors(errors, options) {
283
308
  return ['Unknown validation error'];
284
309
  }
285
310
 
286
- return errors.map((e) => formatSingleError(e, options));
311
+ const messages = errors.map(e => formatSingleError(e, options));
312
+ if (options && options.dedupe === true) {
313
+ return dedupeFormattedMessagesPreserveOrder(messages);
314
+ }
315
+ return messages;
287
316
  }
288
317
 
289
318
  /**
@@ -308,6 +337,7 @@ function formatMissingDbPasswordError(appKey, opts = {}) {
308
337
  module.exports = {
309
338
  formatSingleError,
310
339
  formatValidationErrors,
340
+ dedupeFormattedMessagesPreserveOrder,
311
341
  formatMissingDbPasswordError,
312
342
  getPatternDescription,
313
343
  getValueAtInstancePath,
@@ -214,6 +214,40 @@ function datasourceTestRunToLegacyRow(run) {
214
214
  };
215
215
  }
216
216
 
217
+ /**
218
+ * Map dataplane **datasourceSummaries** item into legacy probe row shape.
219
+ * @param {Object} summary
220
+ * @returns {Object}
221
+ */
222
+ function datasourceSummaryToLegacyRow(summary) {
223
+ const key = summary.datasourceKey || summary.datasource_key || 'unknown';
224
+ const root = String(summary.status || '').toLowerCase();
225
+ const vStat = String(summary.validationStatus || '').toLowerCase();
226
+ const issues = Array.isArray(summary.issues) ? summary.issues : [];
227
+ const { errors, warnings } = partitionDatasourceTestRunIssues(issues);
228
+ if (vStat === 'warn' && warnings.length === 0) {
229
+ warnings.push('validation warnings');
230
+ }
231
+ const isValid = vStat !== 'fail' && errors.length === 0;
232
+ const cert = String(summary.certificateStatus || '').toLowerCase();
233
+ if (cert === 'not_passed' && isValid) {
234
+ warnings.push('certification not passed');
235
+ }
236
+ return {
237
+ sourceKey: key,
238
+ key,
239
+ success: root !== 'fail',
240
+ skipped: root === 'skipped',
241
+ validationResults: { isValid, errors, warnings },
242
+ endpointTestResults: {
243
+ success: true,
244
+ endpointReachable: true,
245
+ message: null,
246
+ warning: false
247
+ }
248
+ };
249
+ }
250
+
217
251
  /**
218
252
  * Normalize runtime probe body for Tier B display.
219
253
  *
@@ -225,6 +259,7 @@ function datasourceTestRunToLegacyRow(run) {
225
259
  * - **Runtime probe (--probe)** uses validation run and returns either:
226
260
  * - legacy `{ results: ExternalDataSourceTestResponse[] }`, or
227
261
  * - canonical **DatasourceTestRun** envelope (single success shape) which we coerce into a legacy row
262
+ * - optional **datasourceSummaries** on the envelope for multi-datasource system runs
228
263
  *
229
264
  * This helper intentionally accepts multiple shapes so the CLI output stays truthful across dataplane versions.
230
265
  *
@@ -234,6 +269,10 @@ function datasourceTestRunToLegacyRow(run) {
234
269
  function coerceProbeRunToResultRows(probeRaw) {
235
270
  if (!probeRaw || typeof probeRaw !== 'object') return [];
236
271
  if (Array.isArray(probeRaw.results) && probeRaw.results.length > 0) return probeRaw.results;
272
+ const summaries = probeRaw.datasourceSummaries;
273
+ if (Array.isArray(summaries) && summaries.length > 0) {
274
+ return summaries.map(datasourceSummaryToLegacyRow);
275
+ }
237
276
  if (isDatasourceTestRunEnvelope(probeRaw)) return [datasourceTestRunToLegacyRow(probeRaw)];
238
277
  return [];
239
278
  }
@@ -54,7 +54,7 @@ function logDeployProbeDatasourceSection(probeData) {
54
54
  const results = coerceProbeRunToResultRows(probeData);
55
55
  const probeSummary = summarizeProbeResults(results);
56
56
  logSectionTitle('Runtime Readiness:');
57
- logDatasourceTable(probeSummary.rows, probeSummary);
57
+ logDatasourceTable(probeSummary.rows, probeSummary, 'Per datasource:');
58
58
  if (probeSummary.issues.length > 0) {
59
59
  logSeparator();
60
60
  logSectionTitle('Key Issues:');
@@ -253,10 +253,9 @@ function logDeployReadinessSummary(ctx) {
253
253
  logDeployConfigDeploymentRuntime(systemCfg, deploymentOk, probeData, summary, deploymentDetail || null);
254
254
 
255
255
  logSeparator();
256
+ logDatasourceTable(summary.rows, summary, probeData ? 'Configured datasources:' : undefined);
256
257
  if (probeData) {
257
258
  logDeployProbeDatasourceSection(probeData);
258
- } else {
259
- logDatasourceTable(summary.rows, summary);
260
259
  }
261
260
 
262
261
  logDeployIdentityAndCredentialBlocks(systemCfg, !!probeData);
@@ -50,9 +50,10 @@ function verdictLine(v) {
50
50
  /**
51
51
  * @param {Array<{ key: string, tier: string }>} rows
52
52
  * @param {{ ready: number, partial: number, failed: number }} counts
53
+ * @param {string} [title]
53
54
  */
54
- function logDatasourceTable(rows, counts) {
55
- logSectionTitle('Datasources:');
55
+ function logDatasourceTable(rows, counts, title) {
56
+ logSectionTitle(title && String(title).trim() ? String(title).trim() : 'Datasources:');
56
57
  for (const r of rows) {
57
58
  const statusLabel = r.tier === 'ready' ? 'Ready' : r.tier === 'failed' ? 'Failed' : 'Partial';
58
59
  logger.log(
@@ -133,8 +133,8 @@ function countByStatus(rows) {
133
133
  return counts;
134
134
  }
135
135
 
136
- function pickBlockingDatasourceKey(rows) {
137
- const keys = rows
136
+ function rollupRowsWithKeys(rows) {
137
+ return rows
138
138
  .map(r => {
139
139
  const env = r && r.datasourceTestRun;
140
140
  const st = rollupRowStatus(r);
@@ -146,15 +146,37 @@ function pickBlockingDatasourceKey(rows) {
146
146
  : '';
147
147
  return { key, st };
148
148
  })
149
- .filter(x => x.key);
150
- keys.sort((a, b) => {
151
- const d = statusRank(a.st) - statusRank(b.st);
152
- if (d !== 0) return d;
153
- return a.key.localeCompare(b.key);
154
- });
149
+ .filter(x => x.key)
150
+ .sort((a, b) => {
151
+ const d = statusRank(a.st) - statusRank(b.st);
152
+ if (d !== 0) return d;
153
+ return a.key.localeCompare(b.key);
154
+ });
155
+ }
156
+
157
+ /**
158
+ * Worst-first datasource key for suggested CLI drill-down (always returned when any row exists).
159
+ * @param {Array} rows
160
+ * @returns {string|null}
161
+ */
162
+ function pickDrillDownDatasourceKey(rows) {
163
+ const keys = rollupRowsWithKeys(rows);
155
164
  return keys.length ? keys[0].key : null;
156
165
  }
157
166
 
167
+ /**
168
+ * Datasource key to label as "blocking" only when at least one row is not fully OK (fail/warn/skipped).
169
+ * When every row is OK, returns null so we do not imply a blocker when the system is all-green.
170
+ * @param {Array} rows
171
+ * @returns {string|null}
172
+ */
173
+ function pickBlockingDatasourceKey(rows) {
174
+ const keys = rollupRowsWithKeys(rows);
175
+ if (keys.length === 0) return null;
176
+ if (keys.every(x => x.st === 'ok')) return null;
177
+ return keys[0].key;
178
+ }
179
+
158
180
  function issueKey(issue) {
159
181
  const code = issue && issue.code ? String(issue.code) : '';
160
182
  const msg = issue && issue.message ? String(issue.message) : '';
@@ -390,6 +412,7 @@ function displaySystemAggregateDatasourceTestRuns(results, opts) {
390
412
  const systemStatus = deriveSystemStatus(rows);
391
413
  const counts = countByStatus(rows);
392
414
  const blocking = pickBlockingDatasourceKey(rows);
415
+ const drillDownKey = pickDrillDownDatasourceKey(rows);
393
416
  logSystemHeader(results, runType, systemStatus);
394
417
  logVerdictAndSummary(runType, systemStatus, systemCertStatus(rows), counts);
395
418
  logDataQualityAndReadiness(rows);
@@ -400,7 +423,7 @@ function displaySystemAggregateDatasourceTestRuns(results, opts) {
400
423
  logCapabilitiesOverviewSection(rows, ttyIo);
401
424
  logIntegrationHealthSectionBlock(rows, runType, ttyIo);
402
425
  logCertificationSection(rows);
403
- logUseAndFooter(results, runType, systemStatus, blocking);
426
+ logUseAndFooter(results, runType, systemStatus, drillDownKey);
404
427
  }
405
428
 
406
429
  module.exports = {
@@ -410,6 +433,7 @@ module.exports = {
410
433
  deriveSystemReadiness,
411
434
  rollupRowStatus,
412
435
  pickBlockingDatasourceKey,
436
+ pickDrillDownDatasourceKey,
413
437
  collectKeyIssues,
414
438
  systemCertStatus,
415
439
  drillDownCommand
@@ -39,7 +39,21 @@ function validateFieldMappingExpression(expression) {
39
39
 
40
40
  // Validate transformations (optional)
41
41
  const transformations = expression.split('|').slice(1).map(t => t.trim());
42
- const validTransformations = ['toUpper', 'toLower', 'trim', 'default', 'toNumber', 'toString'];
42
+ // Keep in sync with dataplane field-mapping function registry (pipe DSL after {{path}})
43
+ const validTransformations = [
44
+ 'toUpper',
45
+ 'toLower',
46
+ 'trim',
47
+ 'default',
48
+ 'toNumber',
49
+ 'toString',
50
+ 'coalesce',
51
+ 'substring',
52
+ 'replace',
53
+ 'join',
54
+ 'split',
55
+ 'normalizeWhitespace'
56
+ ];
43
57
  for (const trans of transformations) {
44
58
  const transName = trans.split('(')[0].trim();
45
59
  if (!validTransformations.includes(transName)) {
@@ -67,6 +81,40 @@ function validateFieldMappingExpression(expression) {
67
81
  * @param {Object} payloadTemplate - Payload template
68
82
  * @param {Object} results - Results object to update
69
83
  */
84
+ /**
85
+ * Root object used to verify {{...}} paths exist in the test payload.
86
+ * HubSpot-style configs map from `{{raw.id}}` while `payloadTemplate` often holds the
87
+ * API record at the top level; wrap virtually so path checks match CIP input shape
88
+ * without duplicating the record under `raw` in JSON (metadataSchema validation still
89
+ * uses the unwrapped `payloadTemplate`).
90
+ * @param {Object} payloadTemplate - testPayload.payloadTemplate
91
+ * @param {Object} attributes - fieldMappings.attributes
92
+ * @returns {Object}
93
+ */
94
+ function fieldMappingPathRoot(payloadTemplate, attributes) {
95
+ if (!payloadTemplate || typeof payloadTemplate !== 'object') {
96
+ return payloadTemplate;
97
+ }
98
+ if ('raw' in payloadTemplate) {
99
+ return payloadTemplate;
100
+ }
101
+ const usesRawPrefix =
102
+ attributes &&
103
+ typeof attributes === 'object' &&
104
+ !Array.isArray(attributes) &&
105
+ Object.values(attributes).some(
106
+ cfg =>
107
+ cfg &&
108
+ typeof cfg === 'object' &&
109
+ typeof cfg.expression === 'string' &&
110
+ /\{\{\s*raw\./.test(cfg.expression)
111
+ );
112
+ if (usesRawPrefix) {
113
+ return { raw: payloadTemplate };
114
+ }
115
+ return payloadTemplate;
116
+ }
117
+
70
118
  function validateSingleFieldMapping(fieldName, fieldConfig, payloadTemplate, results) {
71
119
  if (!fieldConfig.expression) {
72
120
  results.errors.push(`Field '${fieldName}' missing expression`);
@@ -124,8 +172,13 @@ function checkPathExistsInPayload(fieldPath, payloadTemplate) {
124
172
  * @returns {Object} Map suitable for validateDimensions
125
173
  */
126
174
  function getDimensionsMapForValidation(datasource) {
127
- const root = datasource?.dimensions;
128
- if (!root || typeof root !== 'object' || Array.isArray(root)) return {};
175
+ if (!datasource || !Object.prototype.hasOwnProperty.call(datasource, 'dimensions')) {
176
+ return undefined;
177
+ }
178
+ const root = datasource.dimensions;
179
+ if (!root || typeof root !== 'object' || Array.isArray(root)) {
180
+ return {};
181
+ }
129
182
  const out = {};
130
183
  for (const [dimKey, binding] of Object.entries(root)) {
131
184
  if (binding && typeof binding === 'object' && typeof binding.field === 'string') {
@@ -238,7 +291,10 @@ function validateFieldMappings(datasource, testPayload) {
238
291
  return results;
239
292
  }
240
293
 
241
- validateDimensions(getDimensionsMapForValidation(datasource), results);
294
+ const dimensionsForValidation = getDimensionsMapForValidation(datasource);
295
+ if (dimensionsForValidation !== undefined) {
296
+ validateDimensions(dimensionsForValidation, results);
297
+ }
242
298
 
243
299
  // Validate attributes structure (required in new schema)
244
300
  const attributes = validateAttributesStructure(datasource.fieldMappings.attributes, results);
@@ -248,8 +304,9 @@ function validateFieldMappings(datasource, testPayload) {
248
304
 
249
305
  // Validate each attribute
250
306
  const payloadTemplate = testPayload.payloadTemplate || testPayload;
307
+ const pathCheckRoot = fieldMappingPathRoot(payloadTemplate, attributes);
251
308
  for (const [attributeName, attributeConfig] of Object.entries(attributes)) {
252
- validateSingleAttribute(attributeName, attributeConfig, payloadTemplate, results);
309
+ validateSingleAttribute(attributeName, attributeConfig, pathCheckRoot, results);
253
310
  }
254
311
 
255
312
  return results;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * @fileoverview Resolve CIP standard operation order for CLI display from the
3
+ * same source as `external-datasource.schema.json` ($defs.cipDefinition.properties.operations.properties).
4
+ * Falls back to `lib/schema/cip-capacity-display.fallback.json` when the schema file is unavailable.
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+
14
+ /**
15
+ * @param {object|null} schema - Parsed external-datasource.schema.json
16
+ * @returns {string[]|null}
17
+ */
18
+ function extractStandardOperationOrderFromSchema(schema) {
19
+ const props =
20
+ schema &&
21
+ schema.$defs &&
22
+ schema.$defs.cipDefinition &&
23
+ schema.$defs.cipDefinition.properties &&
24
+ schema.$defs.cipDefinition.properties.operations &&
25
+ schema.$defs.cipDefinition.properties.operations.properties;
26
+ if (!props || typeof props !== 'object') return null;
27
+ return Object.keys(props);
28
+ }
29
+
30
+ /**
31
+ * @param {string} p
32
+ * @returns {object|null}
33
+ */
34
+ function tryReadJsonFile(p) {
35
+ try {
36
+ if (!p || !fs.existsSync(p)) return null;
37
+ return JSON.parse(fs.readFileSync(p, 'utf8'));
38
+ } catch {
39
+ return null;
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Resolve path to dataplane external-datasource.schema.json (monorepo / env).
45
+ * Set `AIFABRIX_EXTERNAL_DATASOURCE_SCHEMA` to override.
46
+ * @returns {string|null}
47
+ */
48
+ function resolveExternalDatasourceSchemaPath() {
49
+ if (process.env.AIFABRIX_EXTERNAL_DATASOURCE_SCHEMA) {
50
+ const envPath = String(process.env.AIFABRIX_EXTERNAL_DATASOURCE_SCHEMA).trim();
51
+ if (envPath && fs.existsSync(envPath)) return envPath;
52
+ }
53
+ const candidates = [
54
+ path.join(process.cwd(), 'app/schemas/json/external-datasource.schema.json'),
55
+ path.join(process.cwd(), 'aifabrix-dataplane/app/schemas/json/external-datasource.schema.json'),
56
+ path.join(process.cwd(), '..', 'aifabrix-dataplane', 'app/schemas/json/external-datasource.schema.json'),
57
+ path.join(__dirname, '../../../aifabrix-dataplane/app/schemas/json/external-datasource.schema.json')
58
+ ];
59
+ for (const c of candidates) {
60
+ if (c && fs.existsSync(c)) return c;
61
+ }
62
+ return null;
63
+ }
64
+
65
+ const FALLBACK_PATH = path.join(__dirname, '../schema/cip-capacity-display.fallback.json');
66
+
67
+ /** @type {{ standardOrder: string[], aliases: Record<string, string> }|null} */
68
+ let _cache = null;
69
+
70
+ /**
71
+ * @returns {{ standardOrder: string[], aliases: Record<string, string> }}
72
+ */
73
+ function getCipCapacityDisplayConfig() {
74
+ if (_cache) return _cache;
75
+ const schemaPath = resolveExternalDatasourceSchemaPath();
76
+ const schema = schemaPath ? tryReadJsonFile(schemaPath) : null;
77
+ const fromSchema = extractStandardOperationOrderFromSchema(schema);
78
+ const fallback = tryReadJsonFile(FALLBACK_PATH) || {};
79
+ const fbOrder = Array.isArray(fallback.standardOperationOrder)
80
+ ? fallback.standardOperationOrder
81
+ : [];
82
+ const standardOrder = fromSchema && fromSchema.length ? fromSchema : fbOrder;
83
+ const aliases =
84
+ fallback.displayAliases && typeof fallback.displayAliases === 'object'
85
+ ? fallback.displayAliases
86
+ : { create: 'insert (create)' };
87
+ _cache = { standardOrder, aliases };
88
+ return _cache;
89
+ }
90
+
91
+ /**
92
+ * @param {string[]} standardOrder - from schema `cipDefinition.operations.properties` keys
93
+ * @param {string} op
94
+ * @returns {number}
95
+ */
96
+ function standardOperationRank(standardOrder, op) {
97
+ const idx = standardOrder.indexOf(op);
98
+ if (idx >= 0) return idx;
99
+ return 500;
100
+ }
101
+
102
+ /**
103
+ * @param {string} capacityKey e.g. capacity:myOp#3 or capacity:update#1
104
+ * @returns {{ op: string, index: number }|null}
105
+ */
106
+ function parseCapacityDetailKey(capacityKey) {
107
+ const s = String(capacityKey);
108
+ const m = s.match(/^capacity:([^#]+)#(\d+)$/i);
109
+ if (m) {
110
+ return { op: String(m[1]).toLowerCase(), index: parseInt(m[2], 10) };
111
+ }
112
+ const m2 = s.match(/^capacity:([^#]+)$/i);
113
+ if (m2) {
114
+ return { op: String(m2[1]).toLowerCase(), index: 0 };
115
+ }
116
+ return null;
117
+ }
118
+
119
+ function clearCipCapacityDisplayConfigCacheForTests() {
120
+ _cache = null;
121
+ }
122
+
123
+ module.exports = {
124
+ extractStandardOperationOrderFromSchema,
125
+ resolveExternalDatasourceSchemaPath,
126
+ getCipCapacityDisplayConfig,
127
+ standardOperationRank,
128
+ parseCapacityDetailKey,
129
+ clearCipCapacityDisplayConfigCacheForTests
130
+ };
@@ -615,8 +615,10 @@ async function detectAppType(appName, _options = {}) {
615
615
  }
616
616
 
617
617
  /**
618
- * Resolve-specific app path: prefer integration + env.template only (env-only mode).
619
- * If integration/<systemKey>/env.template exists, use that directory without requiring application.yaml.
618
+ * Resolve-specific app path: integration + env.template without application config → env-only mode.
619
+ * If integration/<systemKey>/env.template exists but there is no application.yaml, application.json,
620
+ * application.yml, or variables.yaml, use that directory with envOnly true (kv resolve only).
621
+ * When any application config file exists in that folder, use full resolve (envOnly false).
620
622
  * Otherwise fall back to detectAppType (integration or builder with full config).
621
623
  *
622
624
  * @param {string} appName - Application name
@@ -630,7 +632,12 @@ async function getResolveAppPath(appName) {
630
632
  const integrationPath = getIntegrationPath(appName);
631
633
  const envTemplatePath = path.join(integrationPath, 'env.template');
632
634
  if (fs.existsSync(integrationPath) && fs.existsSync(envTemplatePath)) {
633
- return { appPath: integrationPath, envOnly: true };
635
+ try {
636
+ resolveApplicationConfigPath(integrationPath);
637
+ return { appPath: integrationPath, envOnly: false };
638
+ } catch {
639
+ return { appPath: integrationPath, envOnly: true };
640
+ }
634
641
  }
635
642
  const result = await detectAppType(appName);
636
643
  return { appPath: result.appPath, envOnly: false };