@aifabrix/builder 2.44.3 → 2.44.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +31 -15
- package/lib/api/certificates.api.js +21 -3
- package/lib/api/types/wizard.types.js +2 -1
- 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.help.js +1 -1
- package/lib/cli/setup-app.test-commands.js +75 -39
- package/lib/cli/setup-infra.js +6 -2
- package/lib/cli/setup-utility.js +20 -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-rbac.js +25 -2
- package/lib/commands/repair.js +2 -19
- package/lib/commands/test-e2e-external.js +9 -9
- package/lib/commands/up-common.js +25 -0
- package/lib/commands/upload.js +18 -4
- package/lib/commands/wizard-core.js +53 -11
- package/lib/commands/wizard-dataplane.js +14 -6
- package/lib/commands/wizard-entity-selection.js +71 -14
- package/lib/commands/wizard-headless.js +5 -2
- package/lib/commands/wizard-helpers.js +13 -1
- package/lib/commands/wizard.js +208 -60
- 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/generator/wizard-prompts.js +7 -1
- package/lib/generator/wizard.js +34 -0
- 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/schema/wizard-config.schema.json +1 -1
- 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-readme.js +47 -3
- 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/urls-local-registry.js +52 -10
- 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/applications/miso-controller/env.template +6 -6
- package/templates/external-system/README.md.hbs +58 -32
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview TTY output for `aifabrix datasource validate` (cli-test-layout-chalk).
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
const {
|
|
12
|
+
sectionTitle,
|
|
13
|
+
headerKeyValue,
|
|
14
|
+
metadata,
|
|
15
|
+
formatSuccessLine,
|
|
16
|
+
formatBlockingError,
|
|
17
|
+
successGlyph,
|
|
18
|
+
failureGlyph
|
|
19
|
+
} = require('../utils/cli-test-layout-chalk');
|
|
20
|
+
|
|
21
|
+
function logSubOk(whitePart, grayPart) {
|
|
22
|
+
if (grayPart) {
|
|
23
|
+
logger.log(` ${successGlyph()} ${chalk.white(whitePart)} ${metadata(grayPart)}`);
|
|
24
|
+
} else {
|
|
25
|
+
logger.log(` ${successGlyph()} ${chalk.white(whitePart)}`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function logHeaderAndIdentity(result, trimmed, showMapping) {
|
|
30
|
+
const { resolvedPath, summary } = result;
|
|
31
|
+
logger.log('');
|
|
32
|
+
logger.log(sectionTitle('Datasource validation'));
|
|
33
|
+
logger.log(metadata('Offline — JSON schema and integration wiring'));
|
|
34
|
+
logger.log('');
|
|
35
|
+
if (summary) {
|
|
36
|
+
logger.log(` ${headerKeyValue('Key:', summary.key)}`);
|
|
37
|
+
logger.log(` ${headerKeyValue('Resource type:', summary.resourceType)}`);
|
|
38
|
+
logger.log(` ${headerKeyValue('Entity type:', summary.entityType)}`);
|
|
39
|
+
logger.log(` ${headerKeyValue('File:', resolvedPath)}`);
|
|
40
|
+
if (showMapping && trimmed && String(trimmed) !== String(summary.key)) {
|
|
41
|
+
logger.log(` ${headerKeyValue('CLI input:', trimmed)}`);
|
|
42
|
+
}
|
|
43
|
+
logger.log('');
|
|
44
|
+
} else {
|
|
45
|
+
logger.log(` ${headerKeyValue('File:', resolvedPath)}`);
|
|
46
|
+
logger.log('');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function logInvalidBody(errors, warnings) {
|
|
51
|
+
logger.log(` ${formatBlockingError('Datasource file has errors')}`);
|
|
52
|
+
(errors || []).forEach(err => {
|
|
53
|
+
logger.log(` ${failureGlyph()} ${chalk.red(err)}`);
|
|
54
|
+
});
|
|
55
|
+
if (warnings && warnings.length > 0) {
|
|
56
|
+
logger.log('');
|
|
57
|
+
logger.log(sectionTitle('Warnings'));
|
|
58
|
+
warnings.forEach(w => {
|
|
59
|
+
logger.log(` ${chalk.yellow('⚠')} ${chalk.white(w)}`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function logValidMappingsAndRelations(summary) {
|
|
65
|
+
const mc = summary.metadataSchemaPropertyCount;
|
|
66
|
+
const mcLabel = `${mc} propert${mc === 1 ? 'y' : 'ies'}`;
|
|
67
|
+
logSubOk('Metadata schema', mcLabel);
|
|
68
|
+
logSubOk('Field mappings', `${summary.fieldMappingAttributeCount} attribute(s)`);
|
|
69
|
+
logger.log('');
|
|
70
|
+
logSubOk(`Primary key: ${summary.primaryKey}`);
|
|
71
|
+
logSubOk(`Label key: ${summary.labelKey}`);
|
|
72
|
+
logger.log('');
|
|
73
|
+
if (summary.foreignKeys.length > 0) {
|
|
74
|
+
logSubOk('Foreign keys', `${summary.foreignKeys.length} reference(s)`);
|
|
75
|
+
summary.foreignKeys.forEach(fk => {
|
|
76
|
+
logger.log(metadata(` • ${fk.name} → ${fk.target} (${fk.fields})`));
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
logSubOk('Foreign keys', 'none');
|
|
80
|
+
}
|
|
81
|
+
logger.log('');
|
|
82
|
+
if (summary.dimensionKeys.length > 0) {
|
|
83
|
+
logSubOk('Dimensions (ABAC)', '');
|
|
84
|
+
summary.dimensionKeys.forEach(k => {
|
|
85
|
+
logger.log(metadata(` ${k} → ${summary.dimensions[k]}`));
|
|
86
|
+
});
|
|
87
|
+
} else {
|
|
88
|
+
logSubOk('Dimensions (ABAC)', 'none configured');
|
|
89
|
+
}
|
|
90
|
+
logger.log('');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function logOpenapiSection(summary) {
|
|
94
|
+
if (summary.hasOpenapi) {
|
|
95
|
+
logSubOk('OpenAPI', summary.openapiLine);
|
|
96
|
+
if (summary.capabilityKeys.length > 0) {
|
|
97
|
+
const preview = summary.capabilityKeys.slice(0, 12).join(', ');
|
|
98
|
+
const more = summary.capabilityKeys.length > 12 ? ', …' : '';
|
|
99
|
+
logger.log(metadata(` Operations: ${preview}${more}`));
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
logSubOk('OpenAPI', 'not configured');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function logValidInterfaceAndFooter(summary) {
|
|
107
|
+
if (summary.exposedProfileNames.length > 0) {
|
|
108
|
+
logSubOk('Exposed profiles', summary.exposedProfileNames.join(', '));
|
|
109
|
+
} else {
|
|
110
|
+
logSubOk('Exposed profiles', 'none');
|
|
111
|
+
}
|
|
112
|
+
logger.log('');
|
|
113
|
+
logOpenapiSection(summary);
|
|
114
|
+
logger.log('');
|
|
115
|
+
logSubOk('Test payload', summary.testPayloadLine);
|
|
116
|
+
logger.log('');
|
|
117
|
+
logSubOk('Synchronization', summary.syncLine);
|
|
118
|
+
logger.log('');
|
|
119
|
+
const caps = summary.capabilityKeys;
|
|
120
|
+
const capHint =
|
|
121
|
+
caps.length > 0 ? `${caps.slice(0, 8).join(', ')}${caps.length > 8 ? ', …' : ''}` : 'none (no OpenAPI operations)';
|
|
122
|
+
logSubOk('Capabilities', capHint);
|
|
123
|
+
logger.log('');
|
|
124
|
+
logger.log(` ${formatSuccessLine('Datasource file is valid.')}`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function logWarningsOnly(warnings) {
|
|
128
|
+
if (!warnings || warnings.length === 0) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
logger.log('');
|
|
132
|
+
logger.log(sectionTitle('Warnings'));
|
|
133
|
+
warnings.forEach(w => {
|
|
134
|
+
logger.log(` ${chalk.yellow('⚠')} ${chalk.white(w)}`);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {object} result - validateDatasourceFile result (includes optional summary)
|
|
140
|
+
* @param {string} trimmed - CLI argument
|
|
141
|
+
* @param {boolean} showMapping - true when key resolved to a different path than arg
|
|
142
|
+
*/
|
|
143
|
+
function logDatasourceValidateOutcome(result, trimmed, showMapping) {
|
|
144
|
+
logHeaderAndIdentity(result, trimmed, showMapping);
|
|
145
|
+
const { valid, errors, warnings, summary } = result;
|
|
146
|
+
if (!valid) {
|
|
147
|
+
logInvalidBody(errors, warnings);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!summary) {
|
|
151
|
+
logger.log(` ${formatSuccessLine('Datasource file is valid.')}`);
|
|
152
|
+
logWarningsOnly(warnings);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
logValidMappingsAndRelations(summary);
|
|
156
|
+
logValidInterfaceAndFooter(summary);
|
|
157
|
+
logWarningsOnly(warnings);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
logDatasourceValidateOutcome
|
|
162
|
+
};
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Extract display summary from parsed external-datasource JSON (offline validate).
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { flattenRootDimensionsForDisplay } = require('../validation/dimension-display-helpers');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Count JSON Schema property nodes (recursive `properties` bags).
|
|
13
|
+
* @param {object|null|undefined} metadataSchema
|
|
14
|
+
* @returns {number}
|
|
15
|
+
*/
|
|
16
|
+
function countMetadataSchemaProperties(metadataSchema) {
|
|
17
|
+
if (!metadataSchema || typeof metadataSchema !== 'object') {
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
let n = 0;
|
|
21
|
+
function walk(node, depth) {
|
|
22
|
+
if (depth > 25 || !node || typeof node !== 'object') {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const props = node.properties;
|
|
26
|
+
if (props && typeof props === 'object' && !Array.isArray(props)) {
|
|
27
|
+
for (const key of Object.keys(props)) {
|
|
28
|
+
n += 1;
|
|
29
|
+
walk(props[key], depth + 1);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(node.allOf)) {
|
|
33
|
+
for (const sub of node.allOf) {
|
|
34
|
+
walk(sub, depth + 1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (node.items && typeof node.items === 'object') {
|
|
38
|
+
walk(node.items, depth + 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
walk(metadataSchema, 0);
|
|
42
|
+
return n;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @param {object} fk
|
|
47
|
+
* @returns {{ name: string, target: string, fields: string }}
|
|
48
|
+
*/
|
|
49
|
+
function normalizeForeignKeyRow(fk) {
|
|
50
|
+
return {
|
|
51
|
+
name: fk.name,
|
|
52
|
+
target: fk.targetDatasource,
|
|
53
|
+
fields: Array.isArray(fk.fields) ? fk.fields.join(', ') : ''
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @param {object} parsed
|
|
59
|
+
* @returns {{ dimensionKeys: string[], dimensions: Record<string, string> }}
|
|
60
|
+
*/
|
|
61
|
+
function summarizeDimensions(parsed) {
|
|
62
|
+
const dimFlat = flattenRootDimensionsForDisplay(parsed.dimensions);
|
|
63
|
+
const abacDims =
|
|
64
|
+
parsed.abac && parsed.abac.dimensions && typeof parsed.abac.dimensions === 'object' ? parsed.abac.dimensions : {};
|
|
65
|
+
const allDims = { ...dimFlat, ...abacDims };
|
|
66
|
+
return { dimensionKeys: Object.keys(allDims), dimensions: allDims };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {object|null} sync
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
function summarizeSyncLine(sync) {
|
|
74
|
+
if (!sync || typeof sync !== 'object') {
|
|
75
|
+
return 'Not configured';
|
|
76
|
+
}
|
|
77
|
+
const parts = [];
|
|
78
|
+
if (sync.mode) {
|
|
79
|
+
parts.push(String(sync.mode));
|
|
80
|
+
}
|
|
81
|
+
if (sync.batchSize !== undefined && sync.batchSize !== null) {
|
|
82
|
+
parts.push(`batch size ${sync.batchSize}`);
|
|
83
|
+
}
|
|
84
|
+
if (sync.schedule) {
|
|
85
|
+
parts.push(`schedule ${JSON.stringify(sync.schedule)}`);
|
|
86
|
+
}
|
|
87
|
+
return parts.length ? parts.join(', ') : 'Present';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* @param {object} parsed
|
|
92
|
+
* @returns {{ openapiLine: string, hasOpenapi: boolean, capabilityKeys: string[] }}
|
|
93
|
+
*/
|
|
94
|
+
function summarizeOpenapi(parsed) {
|
|
95
|
+
const openapi = parsed.openapi && typeof parsed.openapi === 'object' ? parsed.openapi : {};
|
|
96
|
+
const operations = openapi.operations && typeof openapi.operations === 'object' ? openapi.operations : {};
|
|
97
|
+
const capabilityKeys = Object.keys(operations);
|
|
98
|
+
let openapiLine = 'Not configured';
|
|
99
|
+
if (openapi.enabled === true) {
|
|
100
|
+
openapiLine = openapi.autoRbac ? 'enabled, auto RBAC' : 'enabled';
|
|
101
|
+
} else if (parsed.openapi && Object.prototype.hasOwnProperty.call(parsed.openapi, 'enabled')) {
|
|
102
|
+
openapiLine = 'disabled';
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
openapiLine,
|
|
106
|
+
hasOpenapi: Object.keys(openapi).length > 0,
|
|
107
|
+
capabilityKeys
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {object} parsed
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function summarizeTestPayloadLine(parsed) {
|
|
116
|
+
const tp = parsed.testPayload;
|
|
117
|
+
const hasTestPayload = !!tp && typeof tp === 'object';
|
|
118
|
+
if (!hasTestPayload) {
|
|
119
|
+
return 'Not configured';
|
|
120
|
+
}
|
|
121
|
+
if (Array.isArray(tp.scenarios)) {
|
|
122
|
+
return `${tp.scenarios.length} scenario(s)`;
|
|
123
|
+
}
|
|
124
|
+
return 'Present';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* @param {object} parsed
|
|
129
|
+
* @returns {string[]}
|
|
130
|
+
*/
|
|
131
|
+
function summarizeExposedProfileNames(parsed) {
|
|
132
|
+
if (parsed.exposed && parsed.exposed.profiles && typeof parsed.exposed.profiles === 'object') {
|
|
133
|
+
return Object.keys(parsed.exposed.profiles);
|
|
134
|
+
}
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* @param {string|undefined} v
|
|
140
|
+
* @returns {string}
|
|
141
|
+
*/
|
|
142
|
+
function dash(v) {
|
|
143
|
+
return v || '—';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* @param {object} parsed
|
|
148
|
+
* @returns {object}
|
|
149
|
+
*/
|
|
150
|
+
function assembleSummary(parsed) {
|
|
151
|
+
const attrs = parsed.fieldMappings && parsed.fieldMappings.attributes;
|
|
152
|
+
const attrCount = attrs && typeof attrs === 'object' ? Object.keys(attrs).length : 0;
|
|
153
|
+
const pk = Array.isArray(parsed.primaryKey) ? parsed.primaryKey.join(', ') : String(parsed.primaryKey || '');
|
|
154
|
+
const lk = Array.isArray(parsed.labelKey) ? parsed.labelKey.join(', ') : String(parsed.labelKey || '');
|
|
155
|
+
const fks = Array.isArray(parsed.foreignKeys) ? parsed.foreignKeys.map(normalizeForeignKeyRow) : [];
|
|
156
|
+
const { dimensionKeys, dimensions } = summarizeDimensions(parsed);
|
|
157
|
+
const oa = summarizeOpenapi(parsed);
|
|
158
|
+
const sync = parsed.sync && typeof parsed.sync === 'object' ? parsed.sync : null;
|
|
159
|
+
return {
|
|
160
|
+
key: dash(parsed.key),
|
|
161
|
+
resourceType: dash(parsed.resourceType),
|
|
162
|
+
entityType: dash(parsed.entityType),
|
|
163
|
+
metadataSchemaPropertyCount: countMetadataSchemaProperties(parsed.metadataSchema),
|
|
164
|
+
fieldMappingAttributeCount: attrCount,
|
|
165
|
+
primaryKey: pk || '—',
|
|
166
|
+
labelKey: lk || '—',
|
|
167
|
+
foreignKeys: fks,
|
|
168
|
+
dimensionKeys,
|
|
169
|
+
dimensions,
|
|
170
|
+
exposedProfileNames: summarizeExposedProfileNames(parsed),
|
|
171
|
+
openapiLine: oa.openapiLine,
|
|
172
|
+
hasOpenapi: oa.hasOpenapi,
|
|
173
|
+
capabilityKeys: oa.capabilityKeys,
|
|
174
|
+
testPayloadLine: summarizeTestPayloadLine(parsed),
|
|
175
|
+
hasTestPayload: !!parsed.testPayload && typeof parsed.testPayload === 'object',
|
|
176
|
+
syncLine: summarizeSyncLine(sync)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* @param {object} parsed - Parsed datasource JSON
|
|
182
|
+
* @returns {object|null}
|
|
183
|
+
*/
|
|
184
|
+
function buildDatasourceValidateSummary(parsed) {
|
|
185
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
return assembleSummary(parsed);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
buildDatasourceValidateSummary,
|
|
193
|
+
countMetadataSchemaProperties
|
|
194
|
+
};
|
|
@@ -33,9 +33,8 @@ function buildUnifiedE2eRunOptions(options, timeoutMs, pk) {
|
|
|
33
33
|
verbose: options.verbose,
|
|
34
34
|
async: options.async !== false,
|
|
35
35
|
noAsync: options.async === false,
|
|
36
|
-
testCrud: options.testCrud,
|
|
37
|
-
recordId: options.recordId,
|
|
38
36
|
cleanup: options.cleanup,
|
|
37
|
+
runScenarios: options.runScenarios,
|
|
39
38
|
primaryKeyValue: pk,
|
|
40
39
|
minVectorHits: options.minVectorHits,
|
|
41
40
|
minProcessed: options.minProcessed,
|
|
@@ -72,6 +71,59 @@ function e2eIntegrationLogDir(appKey) {
|
|
|
72
71
|
return path.dirname(getIntegrationPath(appKey));
|
|
73
72
|
}
|
|
74
73
|
|
|
74
|
+
/**
|
|
75
|
+
* @param {Object} options
|
|
76
|
+
* @returns {number}
|
|
77
|
+
*/
|
|
78
|
+
function resolveE2ePollTimeoutMs(options) {
|
|
79
|
+
const raw =
|
|
80
|
+
options.timeout !== undefined && options.timeout !== null && options.timeout !== ''
|
|
81
|
+
? parseInt(String(options.timeout), 10)
|
|
82
|
+
: options.pollTimeoutMs;
|
|
83
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_POLL_TIMEOUT_MS;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @param {string} datasourceKey
|
|
88
|
+
* @param {Object} options
|
|
89
|
+
* @param {string|Object|null} pk
|
|
90
|
+
* @returns {Object}
|
|
91
|
+
*/
|
|
92
|
+
function buildE2eRequestMeta(datasourceKey, options, pk) {
|
|
93
|
+
return {
|
|
94
|
+
datasourceKey,
|
|
95
|
+
runType: 'e2e',
|
|
96
|
+
includeDebug: includeDebugForRequest(options.debug),
|
|
97
|
+
cleanup: options.cleanup,
|
|
98
|
+
runScenarios: options.runScenarios,
|
|
99
|
+
primaryKeyValue: pk !== undefined && pk !== null,
|
|
100
|
+
minVectorHits: options.minVectorHits,
|
|
101
|
+
minProcessed: options.minProcessed,
|
|
102
|
+
minRecordCount: options.minRecordCount
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Write JSON debug log and print paths + CLI wall clock when --debug.
|
|
108
|
+
* @param {string} appKey
|
|
109
|
+
* @param {Object} requestMeta
|
|
110
|
+
* @param {Object} envelope
|
|
111
|
+
* @param {number} cliWallSeconds
|
|
112
|
+
* @returns {Promise<void>}
|
|
113
|
+
*/
|
|
114
|
+
async function logE2eDebugSuccess(appKey, requestMeta, envelope, cliWallSeconds) {
|
|
115
|
+
const logPath = await writeTestLog(
|
|
116
|
+
appKey,
|
|
117
|
+
{ request: { ...requestMeta, cliWallSeconds }, response: envelope },
|
|
118
|
+
'test-e2e',
|
|
119
|
+
e2eIntegrationLogDir(appKey)
|
|
120
|
+
);
|
|
121
|
+
logger.log(chalk.gray(` Debug log: ${logPath}`));
|
|
122
|
+
logger.log(
|
|
123
|
+
chalk.gray(` CLI wall time: ${cliWallSeconds}s (URL resolve, POST, poll, log write)`)
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
75
127
|
/**
|
|
76
128
|
* Throw when unified run failed, timed out, or needs async; optionally write debug log.
|
|
77
129
|
* @returns {Promise<void>}
|
|
@@ -112,42 +164,19 @@ async function throwIfUnifiedE2EBlocked(unifiedResult, appKey, options, requestM
|
|
|
112
164
|
}
|
|
113
165
|
|
|
114
166
|
/**
|
|
115
|
-
* Run E2E
|
|
116
|
-
* @
|
|
117
|
-
* @param {string} datasourceKey
|
|
118
|
-
* @param {Object} options
|
|
119
|
-
* @param {boolean} [options.sync] - Publish local datasource JSON before validation when true
|
|
120
|
-
* @returns {Promise<Object>} Shape compatible with displayE2EResults (steps, success, status)
|
|
167
|
+
* Run E2E for one datasource (unified validation API).
|
|
168
|
+
* @returns {Promise<Object>} displayE2EResults-compatible shape
|
|
121
169
|
*/
|
|
122
170
|
async function runDatasourceTestE2E(datasourceKey, options = {}) {
|
|
123
171
|
if (!datasourceKey || typeof datasourceKey !== 'string') {
|
|
124
172
|
throw new Error('Datasource key is required');
|
|
125
173
|
}
|
|
174
|
+
const cliWallStartedAt = Date.now();
|
|
126
175
|
const { appKey } = await resolveAppKeyForDatasource(datasourceKey, options.app);
|
|
127
|
-
|
|
128
176
|
logE2eDatasourceBanner(datasourceKey, options.verbose);
|
|
129
|
-
|
|
130
177
|
const pk = await resolvePrimaryKeyValue(options.primaryKeyValue);
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
? parseInt(String(options.timeout), 10)
|
|
134
|
-
: options.pollTimeoutMs;
|
|
135
|
-
const timeoutMs =
|
|
136
|
-
Number.isFinite(timeoutRaw) && timeoutRaw > 0 ? timeoutRaw : DEFAULT_POLL_TIMEOUT_MS;
|
|
137
|
-
|
|
138
|
-
const requestMeta = {
|
|
139
|
-
datasourceKey,
|
|
140
|
-
runType: 'e2e',
|
|
141
|
-
includeDebug: includeDebugForRequest(options.debug),
|
|
142
|
-
testCrud: options.testCrud,
|
|
143
|
-
recordId: options.recordId,
|
|
144
|
-
cleanup: options.cleanup,
|
|
145
|
-
primaryKeyValue: pk !== undefined && pk !== null,
|
|
146
|
-
minVectorHits: options.minVectorHits,
|
|
147
|
-
minProcessed: options.minProcessed,
|
|
148
|
-
minRecordCount: options.minRecordCount
|
|
149
|
-
};
|
|
150
|
-
|
|
178
|
+
const timeoutMs = resolveE2ePollTimeoutMs(options);
|
|
179
|
+
const requestMeta = buildE2eRequestMeta(datasourceKey, options, pk);
|
|
151
180
|
const unifiedResult = await runUnifiedDatasourceValidation(
|
|
152
181
|
datasourceKey,
|
|
153
182
|
buildUnifiedE2eRunOptions(options, timeoutMs, pk)
|
|
@@ -158,14 +187,13 @@ async function runDatasourceTestE2E(datasourceKey, options = {}) {
|
|
|
158
187
|
const display = e2eShapeFromEnvelope(unifiedResult.envelope);
|
|
159
188
|
Object.assign(display, { datasourceTestRun: unifiedResult.envelope });
|
|
160
189
|
|
|
190
|
+
const cliWallSeconds = Math.round((Date.now() - cliWallStartedAt) / 10) / 100;
|
|
191
|
+
if (unifiedResult.envelope && typeof unifiedResult.envelope === 'object') {
|
|
192
|
+
unifiedResult.envelope.cliWallSeconds = cliWallSeconds;
|
|
193
|
+
}
|
|
194
|
+
|
|
161
195
|
if (options.debug) {
|
|
162
|
-
|
|
163
|
-
appKey,
|
|
164
|
-
{ request: requestMeta, response: unifiedResult.envelope },
|
|
165
|
-
'test-e2e',
|
|
166
|
-
e2eIntegrationLogDir(appKey)
|
|
167
|
-
);
|
|
168
|
-
logger.log(chalk.gray(` Debug log: ${logPath}`));
|
|
196
|
+
await logE2eDebugSuccess(appKey, requestMeta, unifiedResult.envelope, cliWallSeconds);
|
|
169
197
|
}
|
|
170
198
|
|
|
171
199
|
return display;
|
|
@@ -42,10 +42,9 @@ async function buildUnifiedValidationBody(params) {
|
|
|
42
42
|
? buildE2eOptionsFromCli({
|
|
43
43
|
debug: options.debug,
|
|
44
44
|
verbose: options.verbose,
|
|
45
|
-
testCrud: options.testCrud,
|
|
46
|
-
recordId: options.recordId,
|
|
47
45
|
cleanup: options.cleanup,
|
|
48
46
|
primaryKeyValue: options.primaryKeyValue,
|
|
47
|
+
runScenarios: options.runScenarios,
|
|
49
48
|
minVectorHits: options.minVectorHits,
|
|
50
49
|
minProcessed: options.minProcessed,
|
|
51
50
|
minRecordCount: options.minRecordCount,
|
|
@@ -15,6 +15,7 @@ const { loadExternalDataSourceSchema } = require('../utils/schema-loader');
|
|
|
15
15
|
const { formatValidationErrors } = require('../utils/error-formatter');
|
|
16
16
|
const { validateFieldReferences } = require('./field-reference-validator');
|
|
17
17
|
const { validateAbac } = require('./abac-validator');
|
|
18
|
+
const { buildDatasourceValidateSummary } = require('./datasource-validate-summary');
|
|
18
19
|
|
|
19
20
|
const EXCLUDE_JSON_NAMES = new Set(['application.json', 'rbac.json', 'package.json', 'package-lock.json']);
|
|
20
21
|
|
|
@@ -161,7 +162,8 @@ function resolveValidateInputPath(identifier) {
|
|
|
161
162
|
* @async
|
|
162
163
|
* @function validateDatasourceFile
|
|
163
164
|
* @param {string} filePathOrKey - Path to the datasource JSON file, or datasource `key` resolved under integration/<app>/
|
|
164
|
-
* @returns {Promise<Object>} Validation result
|
|
165
|
+
* @returns {Promise<Object>} Validation result: `valid`, `errors`, `warnings`, `resolvedPath`, and `summary`
|
|
166
|
+
* (from `buildDatasourceValidateSummary` when JSON parses; `null` on JSON syntax errors).
|
|
165
167
|
* @throws {Error} If file cannot be read or parsed
|
|
166
168
|
*
|
|
167
169
|
* @example
|
|
@@ -185,19 +187,23 @@ async function validateDatasourceFile(filePathOrKey) {
|
|
|
185
187
|
valid: false,
|
|
186
188
|
errors: [`Invalid JSON syntax: ${error.message}`],
|
|
187
189
|
warnings: [],
|
|
188
|
-
resolvedPath: filePath
|
|
190
|
+
resolvedPath: filePath,
|
|
191
|
+
summary: null
|
|
189
192
|
};
|
|
190
193
|
}
|
|
191
194
|
|
|
195
|
+
const summary = buildDatasourceValidateSummary(parsed);
|
|
196
|
+
|
|
192
197
|
const validate = loadExternalDataSourceSchema();
|
|
193
198
|
const schemaValid = validate(parsed);
|
|
194
199
|
|
|
195
200
|
if (!schemaValid) {
|
|
196
201
|
return {
|
|
197
202
|
valid: false,
|
|
198
|
-
errors: formatValidationErrors(validate.errors),
|
|
203
|
+
errors: formatValidationErrors(validate.errors, { rootData: parsed, dedupe: true }),
|
|
199
204
|
warnings: [],
|
|
200
|
-
resolvedPath: filePath
|
|
205
|
+
resolvedPath: filePath,
|
|
206
|
+
summary
|
|
201
207
|
};
|
|
202
208
|
}
|
|
203
209
|
|
|
@@ -209,7 +215,8 @@ async function validateDatasourceFile(filePathOrKey) {
|
|
|
209
215
|
valid: false,
|
|
210
216
|
errors: postSchemaErrors,
|
|
211
217
|
warnings: [],
|
|
212
|
-
resolvedPath: filePath
|
|
218
|
+
resolvedPath: filePath,
|
|
219
|
+
summary
|
|
213
220
|
};
|
|
214
221
|
}
|
|
215
222
|
|
|
@@ -217,7 +224,8 @@ async function validateDatasourceFile(filePathOrKey) {
|
|
|
217
224
|
valid: true,
|
|
218
225
|
errors: [],
|
|
219
226
|
warnings: [],
|
|
220
|
-
resolvedPath: filePath
|
|
227
|
+
resolvedPath: filePath,
|
|
228
|
+
summary
|
|
221
229
|
};
|
|
222
230
|
}
|
|
223
231
|
|
|
@@ -154,12 +154,21 @@ async function loadDatasourceFiles(datasourceFiles, schemaBasePath, appPath) {
|
|
|
154
154
|
async function loadExternalSystemFiles(appName) {
|
|
155
155
|
const { appPath } = await detectAppType(appName);
|
|
156
156
|
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
157
|
+
const { orderDatasourceFileNamesBySystemKeys } = require('../utils/schema-resolver');
|
|
157
158
|
const configPath = resolveApplicationConfigPath(appPath);
|
|
158
159
|
const variables = await loadVariablesYamlFile(configPath);
|
|
159
160
|
|
|
160
161
|
const schemaBasePath = variables.externalIntegration.schemaBasePath || './';
|
|
161
162
|
const systemFiles = variables.externalIntegration.systems || [];
|
|
162
|
-
const
|
|
163
|
+
const rawDatasourceFiles = variables.externalIntegration.dataSources || [];
|
|
164
|
+
const schemaResolved = path.isAbsolute(schemaBasePath)
|
|
165
|
+
? schemaBasePath
|
|
166
|
+
: path.resolve(appPath, schemaBasePath);
|
|
167
|
+
const datasourceFiles = orderDatasourceFileNamesBySystemKeys(
|
|
168
|
+
schemaResolved,
|
|
169
|
+
systemFiles,
|
|
170
|
+
rawDatasourceFiles
|
|
171
|
+
);
|
|
163
172
|
|
|
164
173
|
const systemJsonFiles = await loadSystemFiles(systemFiles, schemaBasePath, appPath);
|
|
165
174
|
const datasourceJsonFiles = await loadDatasourceFiles(datasourceFiles, schemaBasePath, appPath);
|
|
@@ -198,10 +207,10 @@ function validateDatasourceSchema(datasource, systemKey, externalDataSourceSchem
|
|
|
198
207
|
/**
|
|
199
208
|
* Test datasource with payload template
|
|
200
209
|
* @param {Object} datasource - Datasource configuration
|
|
201
|
-
* @param {boolean}
|
|
210
|
+
* @param {boolean} _verbose - Show detailed output (reserved; not used in this path yet)
|
|
202
211
|
* @returns {Object} Test results
|
|
203
212
|
*/
|
|
204
|
-
function testDatasourceWithPayload(datasource,
|
|
213
|
+
function testDatasourceWithPayload(datasource, _verbose) {
|
|
205
214
|
const errors = [];
|
|
206
215
|
const warnings = [];
|
|
207
216
|
|
|
@@ -223,11 +232,6 @@ function testDatasourceWithPayload(datasource, verbose) {
|
|
|
223
232
|
warnings.push(...metadataSchemaResults.warnings);
|
|
224
233
|
}
|
|
225
234
|
|
|
226
|
-
// Compare with expectedResult if provided
|
|
227
|
-
if (datasource.testPayload.expectedResult && fieldMappingResults.mappedFields && verbose) {
|
|
228
|
-
warnings.push('expectedResult validation not yet implemented (requires transformation engine)');
|
|
229
|
-
}
|
|
230
|
-
|
|
231
235
|
return {
|
|
232
236
|
fieldMappingResults,
|
|
233
237
|
metadataSchemaResults,
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
const { detectAppType } = require('../utils/paths');
|
|
14
14
|
const { resolveApplicationConfigPath, resolveRbacPath } = require('../utils/app-config-resolver');
|
|
15
|
+
const { orderDatasourceFileNamesBySystemKeys } = require('../utils/schema-resolver');
|
|
15
16
|
const { loadSystemFile, loadDatasourceFiles } = require('./external');
|
|
16
17
|
const { loadVariables, loadRbac } = require('./helpers');
|
|
17
18
|
|
|
@@ -124,9 +125,18 @@ async function generateControllerManifest(appName, options = {}) {
|
|
|
124
125
|
if (systemFiles.length === 0) {
|
|
125
126
|
throw new Error('No system files specified in externalIntegration.systems');
|
|
126
127
|
}
|
|
128
|
+
const schemaResolved = path.isAbsolute(schemaBasePath)
|
|
129
|
+
? schemaBasePath
|
|
130
|
+
: path.resolve(appPath, schemaBasePath);
|
|
131
|
+
const rawDatasourceNames = variables.externalIntegration.dataSources || [];
|
|
132
|
+
const orderedDatasourceNames = orderDatasourceFileNamesBySystemKeys(
|
|
133
|
+
schemaResolved,
|
|
134
|
+
systemFiles,
|
|
135
|
+
rawDatasourceNames
|
|
136
|
+
);
|
|
127
137
|
const [systemJson, datasourceJsons] = await Promise.all([
|
|
128
138
|
loadSystemWithRbac(appPath, schemaBasePath, systemFiles[0]),
|
|
129
|
-
loadDatasourceFiles(appPath, schemaBasePath,
|
|
139
|
+
loadDatasourceFiles(appPath, schemaBasePath, orderedDatasourceNames, {
|
|
130
140
|
skipMissingDatasourceFiles: options.skipMissingDatasourceFiles
|
|
131
141
|
})
|
|
132
142
|
]);
|
|
@@ -134,7 +144,7 @@ async function generateControllerManifest(appName, options = {}) {
|
|
|
134
144
|
const externalIntegration = {
|
|
135
145
|
schemaBasePath,
|
|
136
146
|
systems: systemFiles,
|
|
137
|
-
dataSources:
|
|
147
|
+
dataSources: orderedDatasourceNames,
|
|
138
148
|
autopublish: variables.externalIntegration.autopublish !== false,
|
|
139
149
|
version: appVersion
|
|
140
150
|
};
|
|
@@ -324,17 +324,22 @@ async function promptForExistingCredentialInput() {
|
|
|
324
324
|
* @function promptForUserIntent
|
|
325
325
|
* @returns {Promise<string>} User intent
|
|
326
326
|
*/
|
|
327
|
+
const WIZARD_INTENT_MAX_LENGTH = 1000;
|
|
328
|
+
|
|
327
329
|
async function promptForUserIntent() {
|
|
328
330
|
const { intent } = await inquirer.prompt([
|
|
329
331
|
{
|
|
330
332
|
type: 'input',
|
|
331
333
|
name: 'intent',
|
|
332
|
-
message:
|
|
334
|
+
message: `Describe your primary use case (max ${WIZARD_INTENT_MAX_LENGTH} characters):`,
|
|
333
335
|
default: 'general integration',
|
|
334
336
|
validate: (input) => {
|
|
335
337
|
if (!input || typeof input !== 'string' || input.trim().length === 0) {
|
|
336
338
|
return 'Intent is required';
|
|
337
339
|
}
|
|
340
|
+
if (input.length > WIZARD_INTENT_MAX_LENGTH) {
|
|
341
|
+
return `Intent must be ${WIZARD_INTENT_MAX_LENGTH} characters or fewer`;
|
|
342
|
+
}
|
|
338
343
|
return true;
|
|
339
344
|
}
|
|
340
345
|
}
|
|
@@ -437,6 +442,7 @@ async function promptForRunWithSavedConfig() {
|
|
|
437
442
|
const secondary = require('./wizard-prompts-secondary');
|
|
438
443
|
|
|
439
444
|
module.exports = {
|
|
445
|
+
WIZARD_INTENT_MAX_LENGTH,
|
|
440
446
|
promptForMode,
|
|
441
447
|
promptForSystemIdOrKey,
|
|
442
448
|
promptForExistingSystem,
|