@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.
- package/.npmrc.token +1 -1
- package/integration/roundtrip-test-local/README.md +1 -2
- package/integration/roundtrip-test-local2/README.md +1 -2
- package/jest.projects.js +12 -1
- package/lib/api/certificates.api.js +21 -3
- package/lib/certification/post-unified-cert-sync.js +13 -2
- package/lib/certification/sync-after-external-command.js +6 -3
- package/lib/certification/sync-system-certification.js +60 -14
- package/lib/cli/setup-app.test-commands.js +67 -35
- package/lib/cli/setup-utility.js +1 -1
- package/lib/commands/datasource-unified-test-cli.js +81 -46
- package/lib/commands/datasource-unified-test-cli.options.js +4 -2
- package/lib/commands/datasource.js +3 -31
- package/lib/commands/repair-datasource-keys.js +1 -1
- package/lib/commands/repair-datasource-openapi.js +57 -0
- package/lib/commands/repair-datasource.js +5 -0
- package/lib/commands/repair-internal.js +2 -4
- package/lib/commands/repair.js +1 -2
- package/lib/commands/test-e2e-external.js +5 -6
- package/lib/commands/upload.js +18 -4
- package/lib/commands/wizard-dataplane.js +14 -6
- package/lib/datasource/datasource-validate-display.js +162 -0
- package/lib/datasource/datasource-validate-summary.js +194 -0
- package/lib/datasource/test-e2e.js +65 -37
- package/lib/datasource/unified-validation-run-body.js +1 -2
- package/lib/datasource/validate.js +14 -6
- package/lib/external-system/test.js +12 -8
- package/lib/generator/external-controller-manifest.js +12 -2
- package/lib/schema/cip-capacity-display.fallback.json +7 -0
- package/lib/schema/datasource-test-run.schema.json +79 -1
- package/lib/schema/external-datasource.schema.json +94 -2
- package/lib/schema/flag-map-validation-run.json +1 -2
- package/lib/schema/type/document-storage.json +83 -3
- package/lib/utils/configuration-env-resolver.js +38 -0
- package/lib/utils/dataplane-resolver.js +3 -2
- package/lib/utils/datasource-test-run-capacity-operations.js +149 -0
- package/lib/utils/datasource-test-run-debug-display.js +143 -1
- package/lib/utils/datasource-test-run-display.js +46 -33
- package/lib/utils/datasource-test-run-tty-log.js +6 -2
- package/lib/utils/datasource-test-run-tty-meta-lines.js +123 -0
- package/lib/utils/error-formatter.js +32 -2
- package/lib/utils/external-system-readiness-core.js +39 -0
- package/lib/utils/external-system-readiness-deploy-display.js +2 -3
- package/lib/utils/external-system-readiness-display-internals.js +3 -2
- package/lib/utils/external-system-system-test-tty.js +33 -9
- package/lib/utils/external-system-validators.js +62 -5
- package/lib/utils/load-cip-capacity-display-config.js +130 -0
- package/lib/utils/paths.js +10 -3
- package/lib/utils/schema-resolver.js +98 -2
- package/lib/utils/validation-run-poll.js +15 -4
- package/lib/utils/validation-run-request.js +4 -6
- package/lib/validation/dimension-display-helpers.js +60 -0
- package/lib/validation/validate-display-log-helpers.js +39 -0
- package/lib/validation/validate-display.js +89 -83
- package/package.json +1 -1
- 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
|
-
|
|
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
|
|
137
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
+
};
|
package/lib/utils/paths.js
CHANGED
|
@@ -615,8 +615,10 @@ async function detectAppType(appName, _options = {}) {
|
|
|
615
615
|
}
|
|
616
616
|
|
|
617
617
|
/**
|
|
618
|
-
* Resolve-specific app path:
|
|
619
|
-
* If integration/<systemKey>/env.template exists
|
|
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
|
-
|
|
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 };
|