@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.
Files changed (72) 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 +31 -15
  5. package/lib/api/certificates.api.js +21 -3
  6. package/lib/api/types/wizard.types.js +2 -1
  7. package/lib/certification/post-unified-cert-sync.js +13 -2
  8. package/lib/certification/sync-after-external-command.js +6 -3
  9. package/lib/certification/sync-system-certification.js +60 -14
  10. package/lib/cli/setup-app.help.js +1 -1
  11. package/lib/cli/setup-app.test-commands.js +75 -39
  12. package/lib/cli/setup-infra.js +6 -2
  13. package/lib/cli/setup-utility.js +20 -1
  14. package/lib/commands/datasource-unified-test-cli.js +81 -46
  15. package/lib/commands/datasource-unified-test-cli.options.js +4 -2
  16. package/lib/commands/datasource.js +3 -31
  17. package/lib/commands/repair-datasource-keys.js +1 -1
  18. package/lib/commands/repair-datasource-openapi.js +57 -0
  19. package/lib/commands/repair-datasource.js +5 -0
  20. package/lib/commands/repair-internal.js +2 -4
  21. package/lib/commands/repair-rbac.js +25 -2
  22. package/lib/commands/repair.js +2 -19
  23. package/lib/commands/test-e2e-external.js +9 -9
  24. package/lib/commands/up-common.js +25 -0
  25. package/lib/commands/upload.js +18 -4
  26. package/lib/commands/wizard-core.js +53 -11
  27. package/lib/commands/wizard-dataplane.js +14 -6
  28. package/lib/commands/wizard-entity-selection.js +71 -14
  29. package/lib/commands/wizard-headless.js +5 -2
  30. package/lib/commands/wizard-helpers.js +13 -1
  31. package/lib/commands/wizard.js +208 -60
  32. package/lib/datasource/datasource-validate-display.js +162 -0
  33. package/lib/datasource/datasource-validate-summary.js +194 -0
  34. package/lib/datasource/test-e2e.js +65 -37
  35. package/lib/datasource/unified-validation-run-body.js +1 -2
  36. package/lib/datasource/validate.js +14 -6
  37. package/lib/external-system/test.js +12 -8
  38. package/lib/generator/external-controller-manifest.js +12 -2
  39. package/lib/generator/wizard-prompts.js +7 -1
  40. package/lib/generator/wizard.js +34 -0
  41. package/lib/schema/cip-capacity-display.fallback.json +7 -0
  42. package/lib/schema/datasource-test-run.schema.json +79 -1
  43. package/lib/schema/external-datasource.schema.json +94 -2
  44. package/lib/schema/flag-map-validation-run.json +1 -2
  45. package/lib/schema/type/document-storage.json +83 -3
  46. package/lib/schema/wizard-config.schema.json +1 -1
  47. package/lib/utils/configuration-env-resolver.js +38 -0
  48. package/lib/utils/dataplane-resolver.js +3 -2
  49. package/lib/utils/datasource-test-run-capacity-operations.js +149 -0
  50. package/lib/utils/datasource-test-run-debug-display.js +143 -1
  51. package/lib/utils/datasource-test-run-display.js +46 -33
  52. package/lib/utils/datasource-test-run-tty-log.js +6 -2
  53. package/lib/utils/datasource-test-run-tty-meta-lines.js +123 -0
  54. package/lib/utils/error-formatter.js +32 -2
  55. package/lib/utils/external-readme.js +47 -3
  56. package/lib/utils/external-system-readiness-core.js +39 -0
  57. package/lib/utils/external-system-readiness-deploy-display.js +2 -3
  58. package/lib/utils/external-system-readiness-display-internals.js +3 -2
  59. package/lib/utils/external-system-system-test-tty.js +33 -9
  60. package/lib/utils/external-system-validators.js +62 -5
  61. package/lib/utils/load-cip-capacity-display-config.js +130 -0
  62. package/lib/utils/paths.js +10 -3
  63. package/lib/utils/schema-resolver.js +98 -2
  64. package/lib/utils/urls-local-registry.js +52 -10
  65. package/lib/utils/validation-run-poll.js +15 -4
  66. package/lib/utils/validation-run-request.js +4 -6
  67. package/lib/validation/dimension-display-helpers.js +60 -0
  68. package/lib/validation/validate-display-log-helpers.js +39 -0
  69. package/lib/validation/validate-display.js +89 -83
  70. package/package.json +1 -1
  71. package/templates/applications/miso-controller/env.template +6 -6
  72. 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 test for one datasource (unified validation API; deployment auth like test-integration).
116
- * @async
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 timeoutRaw =
132
- options.timeout !== undefined && options.timeout !== null && options.timeout !== ''
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
- const logPath = await writeTestLog(
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 with errors, warnings, and `resolvedPath` (absolute JSON path)
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 datasourceFiles = variables.externalIntegration.dataSources || [];
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} verbose - Show detailed output
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, verbose) {
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, variables.externalIntegration.dataSources || [], {
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: variables.externalIntegration.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: 'Describe your primary use case (any text):',
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,