@aifabrix/builder 2.41.0 → 2.42.1
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/.cursor/rules/docs-rules.mdc +30 -0
- package/README.md +2 -2
- package/integration/hubspot/README.md +11 -5
- package/integration/hubspot/application.json +54 -0
- package/integration/hubspot/create-hubspot.js +9 -136
- package/integration/hubspot/env.template +3 -4
- package/integration/hubspot/hubspot-datasource-company.json +343 -5
- package/integration/hubspot/hubspot-datasource-contact.json +413 -5
- package/integration/hubspot/hubspot-datasource-deal.json +341 -4
- package/integration/hubspot/hubspot-datasource-users.json +116 -0
- package/integration/hubspot/hubspot-deploy.json +1250 -108
- package/integration/hubspot/hubspot-system.json +15 -32
- package/integration/hubspot/test-dataplane-down-tests.js +17 -16
- package/integration/hubspot/test-dataplane-down.js +2 -2
- package/jest.config.manual.js +2 -1
- package/lib/api/external-test.api.js +111 -0
- package/lib/api/index.js +42 -19
- package/lib/api/pipeline.api.js +66 -120
- package/lib/api/types/pipeline.types.js +37 -0
- package/lib/api/wizard-platform.api.js +61 -0
- package/lib/api/wizard.api.js +36 -2
- package/lib/app/config.js +23 -11
- package/lib/app/index.js +5 -3
- package/lib/app/prompts.js +46 -31
- package/lib/app/readme.js +11 -4
- package/lib/app/run-env-compose.js +64 -1
- package/lib/app/run-helpers.js +1 -1
- package/lib/app/show-display.js +1 -1
- package/lib/cli/setup-app.js +45 -14
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +27 -0
- package/lib/cli/setup-environment.js +12 -4
- package/lib/cli/setup-external-system.js +19 -4
- package/lib/cli/setup-infra.js +54 -14
- package/lib/cli/setup-utility.js +117 -21
- package/lib/commands/auth-config.js +22 -12
- package/lib/commands/credential-env.js +162 -0
- package/lib/commands/credential-list.js +17 -22
- package/lib/commands/credential-push.js +96 -0
- package/lib/commands/datasource.js +77 -6
- package/lib/commands/dev-init.js +39 -1
- package/lib/commands/repair-auth-config.js +99 -0
- package/lib/commands/repair-datasource-keys.js +208 -0
- package/lib/commands/repair-datasource.js +235 -0
- package/lib/commands/repair-env-template.js +348 -0
- package/lib/commands/repair-internal.js +85 -0
- package/lib/commands/repair-rbac.js +158 -0
- package/lib/commands/repair.js +518 -0
- package/lib/commands/secrets-set.js +6 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/up-dataplane.js +90 -6
- package/lib/commands/upload.js +71 -40
- package/lib/commands/wizard-core-helpers.js +230 -5
- package/lib/commands/wizard-core.js +68 -29
- package/lib/commands/wizard-dataplane.js +1 -1
- package/lib/commands/wizard-entity-selection.js +43 -0
- package/lib/commands/wizard-headless.js +49 -5
- package/lib/commands/wizard-helpers.js +7 -3
- package/lib/commands/wizard.js +93 -64
- package/lib/core/config.js +7 -1
- package/lib/core/secrets.js +33 -12
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/deployment/deployer.js +7 -5
- package/lib/external-system/download-helpers.js +3 -1
- package/lib/external-system/download.js +182 -204
- package/lib/external-system/generator.js +204 -56
- package/lib/external-system/test-execution.js +2 -1
- package/lib/external-system/test-system-level.js +73 -0
- package/lib/external-system/test.js +51 -18
- package/lib/generator/external-controller-manifest.js +29 -2
- package/lib/generator/external-schema-utils.js +4 -2
- package/lib/generator/external.js +10 -3
- package/lib/generator/index.js +4 -1
- package/lib/generator/split-readme.js +1 -0
- package/lib/generator/split-variables.js +7 -1
- package/lib/generator/split.js +194 -54
- package/lib/generator/wizard-prompts-secondary.js +326 -0
- package/lib/generator/wizard-prompts.js +105 -106
- package/lib/generator/wizard-readme.js +91 -0
- package/lib/generator/wizard.js +180 -179
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/index.js +11 -3
- package/lib/infrastructure/services.js +22 -11
- package/lib/schema/application-schema.json +8 -5
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +82 -6
- package/lib/schema/wizard-config.schema.json +23 -1
- package/lib/utils/api.js +38 -10
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/compose-generator.js +1 -1
- package/lib/utils/compose-handlebars-helpers.js +11 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +115 -25
- package/lib/utils/dataplane-pipeline-warning.js +28 -0
- package/lib/utils/deployment-validation-helpers.js +4 -4
- package/lib/utils/dev-ca-install.js +139 -0
- package/lib/utils/env-copy.js +23 -3
- package/lib/utils/error-formatters/http-status-errors.js +0 -1
- package/lib/utils/error-formatters/permission-errors.js +0 -1
- package/lib/utils/error-formatters/validation-errors.js +0 -1
- package/lib/utils/external-readme.js +89 -30
- package/lib/utils/external-system-display.js +59 -1
- package/lib/utils/external-system-test-helpers.js +21 -8
- package/lib/utils/external-system-validators.js +3 -0
- package/lib/utils/file-upload.js +20 -50
- package/lib/utils/help-builder.js +1 -0
- package/lib/utils/infra-status.js +50 -44
- package/lib/utils/local-secrets.js +5 -5
- package/lib/utils/paths.js +85 -4
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +20 -0
- package/lib/utils/secrets-helpers.js +75 -89
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager.js +24 -32
- package/lib/validation/env-template-auth.js +157 -0
- package/lib/validation/env-template-kv.js +41 -0
- package/lib/validation/external-manifest-validator.js +25 -0
- package/lib/validation/external-system-auth-rules.js +86 -0
- package/lib/validation/validate-batch.js +149 -0
- package/lib/validation/validate-datasource-keys-api.js +33 -0
- package/lib/validation/validate-display.js +94 -16
- package/lib/validation/validate.js +25 -12
- package/lib/validation/validator.js +7 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +7 -2
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +5 -5
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/external-system/README.md.hbs +75 -22
- package/templates/external-system/deploy.js.hbs +4 -2
- package/templates/external-system/external-datasource.yaml.hbs +217 -0
- package/templates/external-system/external-system.json.hbs +1 -18
- package/templates/infra/compose.yaml.hbs +6 -0
- package/templates/python/docker-compose.hbs +4 -4
- package/templates/typescript/docker-compose.hbs +4 -4
- package/integration/hubspot/application.yaml +0 -37
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repair external integration config: fix drift between config and files on disk.
|
|
3
|
+
*
|
|
4
|
+
* Aligns application.yaml with actual system/datasource files, fixes system key mismatch,
|
|
5
|
+
* creates missing externalIntegration block, extracts rbac.yaml from system when needed,
|
|
6
|
+
* and regenerates the deployment manifest.
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview Repair external integration drift
|
|
9
|
+
* @author AI Fabrix Team
|
|
10
|
+
* @version 2.0.0
|
|
11
|
+
*/
|
|
12
|
+
/* eslint-disable max-lines -- Repair flow with auth, normalization, and steps */
|
|
13
|
+
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
const path = require('path');
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const chalk = require('chalk');
|
|
19
|
+
const yaml = require('js-yaml');
|
|
20
|
+
const { detectAppType } = require('../utils/paths');
|
|
21
|
+
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
22
|
+
const { loadConfigFile, writeConfigFile, writeYamlPreservingComments, isYamlPath } = require('../utils/config-format');
|
|
23
|
+
const { systemKeyToKvPrefix, securityKeyToVar } = require('../utils/credential-secrets-env');
|
|
24
|
+
const logger = require('../utils/logger');
|
|
25
|
+
const generator = require('../generator');
|
|
26
|
+
const { repairEnvTemplate, normalizeSystemFileAuthAndConfig } = require('./repair-env-template');
|
|
27
|
+
const { repairDatasourceFile } = require('./repair-datasource');
|
|
28
|
+
const { mergeRbacFromDatasources } = require('./repair-rbac');
|
|
29
|
+
const { discoverIntegrationFiles, buildEffectiveDatasourceFiles } = require('./repair-internal');
|
|
30
|
+
const { normalizeDatasourceKeysAndFilenames } = require('./repair-datasource-keys');
|
|
31
|
+
|
|
32
|
+
/** Allowed authentication methods for repair --auth (matches external-system schema) */
|
|
33
|
+
const ALLOWED_AUTH = ['oauth2', 'aad', 'apikey', 'basic', 'queryParam', 'oidc', 'hmac', 'none'];
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Extracts roles and permissions from system object for rbac.yaml
|
|
37
|
+
* @param {Object} system - Parsed system config
|
|
38
|
+
* @returns {Object|null} RBAC object or null
|
|
39
|
+
*/
|
|
40
|
+
function extractRbacFromSystem(system) {
|
|
41
|
+
if (!system || typeof system !== 'object') return null;
|
|
42
|
+
const hasRoles = system.roles && Array.isArray(system.roles) && system.roles.length > 0;
|
|
43
|
+
const hasPermissions = system.permissions && Array.isArray(system.permissions) && system.permissions.length > 0;
|
|
44
|
+
if (!hasRoles && !hasPermissions) return null;
|
|
45
|
+
const rbac = {};
|
|
46
|
+
if (hasRoles) rbac.roles = system.roles;
|
|
47
|
+
if (hasPermissions) rbac.permissions = system.permissions;
|
|
48
|
+
return rbac;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Loads first system file and returns parsed object with key
|
|
53
|
+
* @param {string} appPath - Application path
|
|
54
|
+
* @param {string} systemFileName - System file name
|
|
55
|
+
* @returns {Object} Parsed system config
|
|
56
|
+
*/
|
|
57
|
+
function loadFirstSystemFile(appPath, systemFileName) {
|
|
58
|
+
const systemPath = path.join(appPath, systemFileName);
|
|
59
|
+
if (!fs.existsSync(systemPath)) {
|
|
60
|
+
throw new Error(`System file not found: ${systemPath}`);
|
|
61
|
+
}
|
|
62
|
+
return loadConfigFile(systemPath);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveSystemContext(appPath, systemFiles) {
|
|
66
|
+
const systemFilePath = path.join(appPath, systemFiles[0]);
|
|
67
|
+
const systemParsed = loadFirstSystemFile(appPath, systemFiles[0]);
|
|
68
|
+
const systemKey = systemParsed.key ||
|
|
69
|
+
path.basename(systemFiles[0], path.extname(systemFiles[0])).replace(/-system$/, '');
|
|
70
|
+
return { systemFilePath, systemParsed, systemKey };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ensureExternalIntegrationBlock(variables, systemFiles, datasourceFiles, changes) {
|
|
74
|
+
const extInt = variables.externalIntegration;
|
|
75
|
+
let updated = false;
|
|
76
|
+
if (!extInt) {
|
|
77
|
+
variables.externalIntegration = {
|
|
78
|
+
schemaBasePath: './',
|
|
79
|
+
systems: systemFiles,
|
|
80
|
+
dataSources: datasourceFiles,
|
|
81
|
+
autopublish: true,
|
|
82
|
+
version: '1.0.0'
|
|
83
|
+
};
|
|
84
|
+
changes.push('Created externalIntegration block from discovered files');
|
|
85
|
+
updated = true;
|
|
86
|
+
} else {
|
|
87
|
+
const prevSystems = extInt.systems || [];
|
|
88
|
+
const prevDataSources = extInt.dataSources || [];
|
|
89
|
+
if (JSON.stringify(prevSystems) !== JSON.stringify(systemFiles)) {
|
|
90
|
+
changes.push(`systems: [${prevSystems.join(', ')}] → [${systemFiles.join(', ')}]`);
|
|
91
|
+
extInt.systems = systemFiles;
|
|
92
|
+
updated = true;
|
|
93
|
+
}
|
|
94
|
+
if (JSON.stringify(prevDataSources) !== JSON.stringify(datasourceFiles)) {
|
|
95
|
+
changes.push(`dataSources: [${prevDataSources.join(', ')}] → [${datasourceFiles.join(', ')}]`);
|
|
96
|
+
extInt.dataSources = datasourceFiles;
|
|
97
|
+
updated = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return updated;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function alignAppKeyWithSystem(variables, systemKey, systemParsed, changes) {
|
|
104
|
+
const appKey = variables.app?.key;
|
|
105
|
+
if (!appKey || appKey === systemKey) return false;
|
|
106
|
+
if (!variables.app) variables.app = {};
|
|
107
|
+
variables.app.key = systemKey;
|
|
108
|
+
changes.push(`app.key: ${appKey} → ${systemKey}`);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Aligns datasource systemKey values to match the system key.
|
|
114
|
+
* Updates each datasource file whose systemKey differs from the system key.
|
|
115
|
+
*
|
|
116
|
+
* @param {string} appPath - Application directory path
|
|
117
|
+
* @param {string[]} datasourceFiles - Datasource file names
|
|
118
|
+
* @param {string} systemKey - Expected system key
|
|
119
|
+
* @param {boolean} dryRun - If true, report changes but do not write
|
|
120
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
121
|
+
* @returns {boolean} True if any file was updated (or would be in dry-run)
|
|
122
|
+
*/
|
|
123
|
+
function alignDatasourceSystemKeys(appPath, datasourceFiles, systemKey, dryRun, changes) {
|
|
124
|
+
if (!datasourceFiles || datasourceFiles.length === 0) return false;
|
|
125
|
+
let updated = false;
|
|
126
|
+
for (const datasourceFile of datasourceFiles) {
|
|
127
|
+
const datasourcePath = path.join(appPath, datasourceFile);
|
|
128
|
+
if (!fs.existsSync(datasourcePath)) continue;
|
|
129
|
+
const parsed = loadConfigFile(datasourcePath);
|
|
130
|
+
const old = parsed.systemKey;
|
|
131
|
+
if (old !== systemKey) {
|
|
132
|
+
parsed.systemKey = systemKey;
|
|
133
|
+
if (!dryRun) {
|
|
134
|
+
writeConfigFile(datasourcePath, parsed);
|
|
135
|
+
}
|
|
136
|
+
changes.push(`${datasourceFile}: systemKey ${old} → ${systemKey}`);
|
|
137
|
+
updated = true;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return updated;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Derives a datasource key from filename when the file has no key.
|
|
145
|
+
* - hubspot-datasource-company.json → hubspot-company
|
|
146
|
+
* - datasource-companies.json → {systemKey}-companies (e.g. test-hubspot-companies)
|
|
147
|
+
*
|
|
148
|
+
* @param {string} fileName - Datasource file name
|
|
149
|
+
* @param {string} systemKey - System key (e.g. test-hubspot), used for datasource-*.json style names
|
|
150
|
+
* @returns {string}
|
|
151
|
+
*/
|
|
152
|
+
function deriveDatasourceKeyFromFileName(fileName, systemKey) {
|
|
153
|
+
const base = path.basename(fileName, path.extname(fileName));
|
|
154
|
+
if (/^datasource-/.test(base)) {
|
|
155
|
+
const suffix = base.slice('datasource-'.length);
|
|
156
|
+
return systemKey && typeof systemKey === 'string' ? `${systemKey}-${suffix}` : base;
|
|
157
|
+
}
|
|
158
|
+
return base.replace(/-datasource-/, '-');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Aligns system file dataSources array to match datasource keys from discovered files.
|
|
163
|
+
* The system file holds logical keys (not filenames): each key comes from that datasource
|
|
164
|
+
* file's "key" property, or is derived from the filename when missing (e.g. datasource-companies.json → {systemKey}-companies).
|
|
165
|
+
*
|
|
166
|
+
* @param {string} appPath - Application directory path
|
|
167
|
+
* @param {Object} systemParsed - Parsed system config (mutated)
|
|
168
|
+
* @param {string[]} datasourceFiles - Datasource file names
|
|
169
|
+
* @param {string} systemKey - System key for deriving key when missing
|
|
170
|
+
* @param {boolean} dryRun - If true, report changes but do not write
|
|
171
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
172
|
+
* @returns {boolean} True if dataSources was updated (or would be in dry-run)
|
|
173
|
+
*/
|
|
174
|
+
function alignSystemFileDataSources(appPath, systemParsed, datasourceFiles, systemKey, dryRun, changes) {
|
|
175
|
+
const keys = [];
|
|
176
|
+
for (const fileName of datasourceFiles) {
|
|
177
|
+
const filePath = path.join(appPath, fileName);
|
|
178
|
+
if (!fs.existsSync(filePath)) continue;
|
|
179
|
+
try {
|
|
180
|
+
const parsed = loadConfigFile(filePath);
|
|
181
|
+
const key = parsed && typeof parsed.key === 'string' && parsed.key.trim()
|
|
182
|
+
? parsed.key.trim()
|
|
183
|
+
: deriveDatasourceKeyFromFileName(fileName, systemKey);
|
|
184
|
+
keys.push(key);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
logger.log(chalk.yellow(`⚠ Could not load datasource file ${fileName}: ${err.message}; using derived key`));
|
|
187
|
+
keys.push(deriveDatasourceKeyFromFileName(fileName, systemKey));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
keys.sort();
|
|
191
|
+
const prev = Array.isArray(systemParsed.dataSources) ? [...systemParsed.dataSources].sort() : [];
|
|
192
|
+
if (JSON.stringify(prev) === JSON.stringify(keys)) return false;
|
|
193
|
+
systemParsed.dataSources = keys;
|
|
194
|
+
changes.push(`dataSources: [${prev.join(', ') || '(none)'}] → [${keys.join(', ')}] (keys from each datasource file's "key" or filename)`);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Builds the set of auth variable names (UPPERCASE, no underscores) from authentication.variables and authentication.security.
|
|
200
|
+
* Used to detect configuration entries that belong only in the authentication section.
|
|
201
|
+
* Canonical keys per method are in lib/schema/external-system.schema.json $defs.authenticationVariablesByMethod
|
|
202
|
+
* (e.g. oauth2: variables baseUrl, tokenUrl, scope; security clientId, clientSecret).
|
|
203
|
+
*
|
|
204
|
+
* @param {Object} systemParsed - Parsed system config
|
|
205
|
+
* @param {string} _systemKey - System key (unused; for API consistency)
|
|
206
|
+
* @returns {Set<string>}
|
|
207
|
+
*/
|
|
208
|
+
function buildAuthVarNames(systemParsed, _systemKey) {
|
|
209
|
+
const names = new Set();
|
|
210
|
+
const auth = systemParsed.authentication;
|
|
211
|
+
if (auth && typeof auth.variables === 'object') {
|
|
212
|
+
for (const k of Object.keys(auth.variables)) {
|
|
213
|
+
names.add(String(k).toUpperCase().replace(/_/g, ''));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
if (auth && typeof auth.security === 'object') {
|
|
217
|
+
for (const k of Object.keys(auth.security)) {
|
|
218
|
+
names.add(securityKeyToVar(k));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return names;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Removes from system configuration any entry that represents standard auth variables
|
|
226
|
+
* (BASEURL, CLIENTID, CLIENTSECRET, TOKENURL, APIKEY, USERNAME, PASSWORD, etc.).
|
|
227
|
+
* These are supplied from the selected credential at runtime; the configuration array
|
|
228
|
+
* should contain only custom variables. Removes both plain and keyvault auth entries.
|
|
229
|
+
*
|
|
230
|
+
* @param {Object} systemParsed - Parsed system config (mutated)
|
|
231
|
+
* @param {string} systemKey - System key for naming consistency
|
|
232
|
+
* @param {boolean} dryRun - If true, report changes but do not write
|
|
233
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
234
|
+
* @returns {boolean} True if any entry was removed
|
|
235
|
+
*/
|
|
236
|
+
/**
|
|
237
|
+
* Derives the normalized auth variable part from a config entry name (for matching against authNames).
|
|
238
|
+
* E.g. KV_HUBSPOT_CLIENTID → CLIENTID, BASEURL → BASEURL.
|
|
239
|
+
* @param {string} name - Entry name
|
|
240
|
+
* @param {string} systemKey - System key for KV_ prefix
|
|
241
|
+
* @returns {string}
|
|
242
|
+
*/
|
|
243
|
+
function normalizedAuthPartFromConfigName(name, systemKey) {
|
|
244
|
+
const n = String(name).trim();
|
|
245
|
+
if (!n) return '';
|
|
246
|
+
const prefix = systemKeyToKvPrefix(systemKey);
|
|
247
|
+
const kvPrefix = `KV_${prefix}_`;
|
|
248
|
+
if (n.toUpperCase().startsWith(kvPrefix)) {
|
|
249
|
+
const rest = n.slice(kvPrefix.length);
|
|
250
|
+
return rest.toUpperCase().replace(/_/g, '');
|
|
251
|
+
}
|
|
252
|
+
return n.toUpperCase().replace(/_/g, '');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function removeAuthVarsFromConfiguration(systemParsed, systemKey, dryRun, changes) {
|
|
256
|
+
const config = systemParsed.configuration;
|
|
257
|
+
if (!Array.isArray(config)) return false;
|
|
258
|
+
const authNames = buildAuthVarNames(systemParsed, systemKey);
|
|
259
|
+
if (authNames.size === 0) return false;
|
|
260
|
+
const removed = [];
|
|
261
|
+
const filtered = config.filter((entry) => {
|
|
262
|
+
if (!entry || !entry.name) return true;
|
|
263
|
+
const authPart = normalizedAuthPartFromConfigName(entry.name, systemKey);
|
|
264
|
+
if (authNames.has(authPart)) {
|
|
265
|
+
removed.push(entry.name);
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
return true;
|
|
269
|
+
});
|
|
270
|
+
if (removed.length === 0) return false;
|
|
271
|
+
systemParsed.configuration = filtered;
|
|
272
|
+
changes.push(`Removed authentication variable(s) from configuration: ${removed.join(', ')}`);
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function createRbacFromSystemIfNeeded(appPath, systemFilePath, systemParsed, dryRun, changes) {
|
|
277
|
+
const rbacPath = path.join(appPath, 'rbac.yaml');
|
|
278
|
+
const rbacYmlPath = path.join(appPath, 'rbac.yml');
|
|
279
|
+
if (fs.existsSync(rbacPath) || fs.existsSync(rbacYmlPath)) return false;
|
|
280
|
+
const rbacFromSystem = extractRbacFromSystem(systemParsed);
|
|
281
|
+
if (!rbacFromSystem) return false;
|
|
282
|
+
if (!dryRun) {
|
|
283
|
+
const rbacYaml = yaml.dump(rbacFromSystem, { indent: 2, lineWidth: -1 });
|
|
284
|
+
fs.writeFileSync(rbacPath, rbacYaml, { mode: 0o644, encoding: 'utf8' });
|
|
285
|
+
delete systemParsed.roles;
|
|
286
|
+
delete systemParsed.permissions;
|
|
287
|
+
writeConfigFile(systemFilePath, systemParsed);
|
|
288
|
+
}
|
|
289
|
+
changes.push('Created rbac.yaml from system roles/permissions');
|
|
290
|
+
changes.push('Removed roles/permissions from system file (now in rbac.yaml)');
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Runs datasource repair for each file (dimensions, metadataSchema, optional expose/sync/test).
|
|
296
|
+
* @param {string} appPath - Application path
|
|
297
|
+
* @param {string[]} datasourceFiles - Datasource file names
|
|
298
|
+
* @param {Object} options - { expose?: boolean, sync?: boolean, test?: boolean }
|
|
299
|
+
* @param {boolean} dryRun - If true, do not write
|
|
300
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
301
|
+
* @returns {boolean} True if any datasource was updated
|
|
302
|
+
*/
|
|
303
|
+
function runDatasourceRepairs(appPath, datasourceFiles, options, dryRun, changes) {
|
|
304
|
+
if (!datasourceFiles || datasourceFiles.length === 0) return false;
|
|
305
|
+
let updated = false;
|
|
306
|
+
for (const fileName of datasourceFiles) {
|
|
307
|
+
const filePath = path.join(appPath, fileName);
|
|
308
|
+
if (!fs.existsSync(filePath)) continue;
|
|
309
|
+
try {
|
|
310
|
+
const parsed = loadConfigFile(filePath);
|
|
311
|
+
const { updated: fileUpdated, changes: fileChanges } = repairDatasourceFile(parsed, {
|
|
312
|
+
expose: options.expose,
|
|
313
|
+
sync: options.sync,
|
|
314
|
+
test: options.test
|
|
315
|
+
});
|
|
316
|
+
if (fileUpdated) {
|
|
317
|
+
updated = true;
|
|
318
|
+
fileChanges.forEach(c => changes.push(`${fileName}: ${c}`));
|
|
319
|
+
if (!dryRun) {
|
|
320
|
+
writeConfigFile(filePath, parsed);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
logger.log(chalk.yellow(`⚠ Could not repair datasource ${fileName}: ${err.message}`));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return updated;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function regenerateManifest(appName, appPath, changes) {
|
|
331
|
+
try {
|
|
332
|
+
const deployPath = await generator.generateDeployJson(appName, { appPath });
|
|
333
|
+
changes.push(`Regenerated ${path.basename(deployPath)}`);
|
|
334
|
+
return true;
|
|
335
|
+
} catch (err) {
|
|
336
|
+
logger.log(chalk.yellow(`⚠ Manifest regeneration skipped: ${err.message}`));
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function persistChangesAndRegenerate(configPath, variables, appName, appPath, changes, originalYamlContent) {
|
|
342
|
+
if (originalYamlContent !== null && originalYamlContent !== undefined && typeof originalYamlContent === 'string' && isYamlPath(configPath)) {
|
|
343
|
+
writeYamlPreservingComments(configPath, originalYamlContent, variables);
|
|
344
|
+
} else {
|
|
345
|
+
writeConfigFile(configPath, variables);
|
|
346
|
+
}
|
|
347
|
+
logger.log(chalk.green(`✓ Updated ${path.basename(configPath)}`));
|
|
348
|
+
changes.forEach(c => logger.log(chalk.gray(` ${c}`)));
|
|
349
|
+
return regenerateManifest(appName, appPath, changes);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Apply --auth: replace system file authentication with canonical block for the given method.
|
|
354
|
+
* Preserves existing authentication.variables (e.g. baseUrl, tokenUrl) from the current system file.
|
|
355
|
+
* @param {Object} ctx - Context with systemParsed, systemKey, auth, dryRun, changes
|
|
356
|
+
* @returns {boolean} True if auth was replaced
|
|
357
|
+
*/
|
|
358
|
+
function applyAuthMethod(ctx) {
|
|
359
|
+
if (!ctx.auth || typeof ctx.auth !== 'string') return false;
|
|
360
|
+
const method = ctx.auth.trim().toLowerCase();
|
|
361
|
+
if (!ALLOWED_AUTH.includes(method)) {
|
|
362
|
+
throw new Error(
|
|
363
|
+
`Invalid --auth "${ctx.auth}". Allowed methods: ${ALLOWED_AUTH.join(', ')}`
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
const existingAuth = ctx.systemParsed.authentication || ctx.systemParsed.auth || {};
|
|
367
|
+
const { buildAuthenticationFromMethod } = require('../external-system/generator');
|
|
368
|
+
const newAuth = buildAuthenticationFromMethod(ctx.systemKey, method);
|
|
369
|
+
const existingVars = existingAuth.variables && typeof existingAuth.variables === 'object' ? existingAuth.variables : {};
|
|
370
|
+
const mergedVariables = { ...newAuth.variables, ...existingVars };
|
|
371
|
+
ctx.systemParsed.authentication = {
|
|
372
|
+
...newAuth,
|
|
373
|
+
variables: Object.keys(mergedVariables).length ? mergedVariables : newAuth.variables
|
|
374
|
+
};
|
|
375
|
+
if (existingAuth.displayName !== undefined) {
|
|
376
|
+
ctx.systemParsed.authentication.displayName = existingAuth.displayName;
|
|
377
|
+
}
|
|
378
|
+
ctx.changes.push(`Set authentication method to ${method}`);
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Runs all repair steps (integration block, system dataSources, auth/config, app key, datasource keys, rbac, env.template).
|
|
384
|
+
* @param {Object} ctx - Context with appPath, configPath, variables, systemFilePath, systemParsed, systemKey, systemFiles, datasourceFiles, dryRun, changes, auth?
|
|
385
|
+
* @returns {{ updated: boolean, appKeyFixed: boolean, datasourceKeysFixed: boolean, rbacFileCreated: boolean, envTemplateRepaired: boolean }}
|
|
386
|
+
*/
|
|
387
|
+
function runRepairSteps(ctx) {
|
|
388
|
+
let updated = ensureExternalIntegrationBlock(
|
|
389
|
+
ctx.variables, ctx.systemFiles, ctx.datasourceFiles, ctx.changes
|
|
390
|
+
);
|
|
391
|
+
const authReplaced = applyAuthMethod(ctx);
|
|
392
|
+
updated = updated || authReplaced;
|
|
393
|
+
const systemAuthConfigNormalized = normalizeSystemFileAuthAndConfig(
|
|
394
|
+
ctx.systemParsed, ctx.systemKey, ctx.changes
|
|
395
|
+
);
|
|
396
|
+
const systemDataSourcesAligned = alignSystemFileDataSources(
|
|
397
|
+
ctx.appPath, ctx.systemParsed, ctx.datasourceFiles, ctx.systemKey, ctx.dryRun, ctx.changes
|
|
398
|
+
);
|
|
399
|
+
const authVarsRemoved = removeAuthVarsFromConfiguration(
|
|
400
|
+
ctx.systemParsed, ctx.systemKey, ctx.dryRun, ctx.changes
|
|
401
|
+
);
|
|
402
|
+
if ((authReplaced || systemAuthConfigNormalized || systemDataSourcesAligned || authVarsRemoved) && !ctx.dryRun) {
|
|
403
|
+
writeConfigFile(ctx.systemFilePath, ctx.systemParsed);
|
|
404
|
+
}
|
|
405
|
+
updated = updated || systemAuthConfigNormalized || systemDataSourcesAligned || authVarsRemoved;
|
|
406
|
+
const appKeyFixed = alignAppKeyWithSystem(
|
|
407
|
+
ctx.variables, ctx.systemKey, ctx.systemParsed, ctx.changes
|
|
408
|
+
);
|
|
409
|
+
const datasourceKeysFixed = alignDatasourceSystemKeys(
|
|
410
|
+
ctx.appPath, ctx.datasourceFiles, ctx.systemKey, ctx.dryRun, ctx.changes
|
|
411
|
+
);
|
|
412
|
+
const rbacFileCreated = createRbacFromSystemIfNeeded(
|
|
413
|
+
ctx.appPath, ctx.systemFilePath, ctx.systemParsed, ctx.dryRun, ctx.changes
|
|
414
|
+
);
|
|
415
|
+
const envTemplateRepaired = repairEnvTemplate(
|
|
416
|
+
ctx.appPath, ctx.systemParsed, ctx.systemKey, ctx.dryRun, ctx.changes
|
|
417
|
+
);
|
|
418
|
+
updated = updated || appKeyFixed || datasourceKeysFixed || rbacFileCreated || envTemplateRepaired;
|
|
419
|
+
return {
|
|
420
|
+
updated,
|
|
421
|
+
appKeyFixed,
|
|
422
|
+
datasourceKeysFixed,
|
|
423
|
+
rbacFileCreated,
|
|
424
|
+
envTemplateRepaired
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Repairs external integration config: syncs files, aligns keys, creates rbac, regenerates manifest
|
|
430
|
+
* @async
|
|
431
|
+
* @param {string} appName - Application/integration name
|
|
432
|
+
* @param {Object} [options] - Options
|
|
433
|
+
* @param {boolean} [options.dryRun] - If true, only report changes; do not write
|
|
434
|
+
* @returns {Promise<{ updated: boolean, changes: string[], systemFiles: string[], datasourceFiles: string[], appKeyFixed?: boolean, datasourceKeysFixed?: boolean, rbacFileCreated?: boolean, envTemplateRepaired?: boolean, manifestRegenerated?: boolean }>}
|
|
435
|
+
*/
|
|
436
|
+
/**
|
|
437
|
+
* Loads application config and discovers integration files; validates at least one system file exists.
|
|
438
|
+
* @param {string} appPath - Application path
|
|
439
|
+
* @param {string} configPath - Path to application config
|
|
440
|
+
* @returns {{ variables: Object, originalYamlContent: string|null, systemFiles: string[], datasourceFiles: string[] }}
|
|
441
|
+
*/
|
|
442
|
+
function loadConfigAndDiscover(appPath, configPath) {
|
|
443
|
+
const variables = loadConfigFile(configPath);
|
|
444
|
+
let originalYamlContent = null;
|
|
445
|
+
if (isYamlPath(configPath)) {
|
|
446
|
+
originalYamlContent = fs.readFileSync(configPath, 'utf8');
|
|
447
|
+
}
|
|
448
|
+
const { systemFiles, datasourceFiles: discoveredDatasourceFiles } = discoverIntegrationFiles(appPath);
|
|
449
|
+
if (systemFiles.length === 0) {
|
|
450
|
+
throw new Error(`No system file found in ${appPath}. Expected *-system.yaml or *-system.json`);
|
|
451
|
+
}
|
|
452
|
+
const datasourceFiles = buildEffectiveDatasourceFiles(appPath, discoveredDatasourceFiles, variables.externalIntegration?.dataSources);
|
|
453
|
+
return { variables, originalYamlContent, systemFiles, datasourceFiles };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function repairExternalIntegration(appName, options = {}) {
|
|
457
|
+
if (!appName || typeof appName !== 'string') throw new Error('App name is required');
|
|
458
|
+
const { dryRun = false, auth: authOption } = options;
|
|
459
|
+
if (authOption !== undefined && authOption !== null && typeof authOption !== 'string') {
|
|
460
|
+
throw new Error('Option --auth must be a string');
|
|
461
|
+
}
|
|
462
|
+
const { appPath, isExternal } = await detectAppType(appName);
|
|
463
|
+
if (!isExternal) throw new Error(`App '${appName}' is not an external integration`);
|
|
464
|
+
const configPath = resolveApplicationConfigPath(appPath);
|
|
465
|
+
if (!fs.existsSync(configPath)) throw new Error(`Application config not found: ${configPath}`);
|
|
466
|
+
|
|
467
|
+
const { variables, originalYamlContent, systemFiles, datasourceFiles: initialDatasourceFiles } = loadConfigAndDiscover(appPath, configPath);
|
|
468
|
+
const changes = [];
|
|
469
|
+
const { systemFilePath, systemParsed, systemKey } = resolveSystemContext(appPath, systemFiles);
|
|
470
|
+
const { updated: keysNormalized, datasourceFiles } = normalizeDatasourceKeysAndFilenames(
|
|
471
|
+
appPath,
|
|
472
|
+
initialDatasourceFiles,
|
|
473
|
+
systemKey,
|
|
474
|
+
variables,
|
|
475
|
+
dryRun,
|
|
476
|
+
changes
|
|
477
|
+
);
|
|
478
|
+
const steps = runRepairSteps({
|
|
479
|
+
appPath,
|
|
480
|
+
configPath,
|
|
481
|
+
variables,
|
|
482
|
+
systemFilePath,
|
|
483
|
+
systemParsed,
|
|
484
|
+
systemKey,
|
|
485
|
+
systemFiles,
|
|
486
|
+
datasourceFiles,
|
|
487
|
+
dryRun,
|
|
488
|
+
changes,
|
|
489
|
+
auth: authOption
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
const opts = { expose: Boolean(options.expose), sync: Boolean(options.sync), test: Boolean(options.test) };
|
|
493
|
+
const datasourceRepairUpdated = runDatasourceRepairs(appPath, datasourceFiles, opts, dryRun, changes);
|
|
494
|
+
const rbacMergeUpdated = options.rbac
|
|
495
|
+
? mergeRbacFromDatasources(appPath, systemParsed, datasourceFiles, extractRbacFromSystem, dryRun, changes)
|
|
496
|
+
: false;
|
|
497
|
+
const anyUpdated = keysNormalized || steps.updated || datasourceRepairUpdated || rbacMergeUpdated;
|
|
498
|
+
const manifestRegenerated = (anyUpdated && !dryRun)
|
|
499
|
+
? await persistChangesAndRegenerate(configPath, variables, appName, appPath, changes, originalYamlContent)
|
|
500
|
+
: false;
|
|
501
|
+
|
|
502
|
+
return {
|
|
503
|
+
updated: anyUpdated,
|
|
504
|
+
changes,
|
|
505
|
+
systemFiles,
|
|
506
|
+
datasourceFiles,
|
|
507
|
+
appKeyFixed: steps.appKeyFixed,
|
|
508
|
+
datasourceKeysFixed: steps.datasourceKeysFixed,
|
|
509
|
+
rbacFileCreated: steps.rbacFileCreated,
|
|
510
|
+
envTemplateRepaired: steps.envTemplateRepaired,
|
|
511
|
+
manifestRegenerated
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
module.exports = {
|
|
516
|
+
repairExternalIntegration,
|
|
517
|
+
discoverIntegrationFiles
|
|
518
|
+
};
|
|
@@ -60,6 +60,12 @@ async function handleSecretsSet(key, value, options) {
|
|
|
60
60
|
throw new Error('Secret key is required and must be a string');
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
if (key.startsWith('kv://')) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
'Secret key must not start with kv://. Use the key path without the prefix (e.g. my-app/clientSecret or hubspot/apiKey).'
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
63
69
|
if (value === undefined || value === null || value === '') {
|
|
64
70
|
throw new Error('Secret value is required');
|
|
65
71
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run E2E tests for all datasources of an external system.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview test-e2e <external system> – run E2E for every datasource
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const chalk = require('chalk');
|
|
14
|
+
const logger = require('../utils/logger');
|
|
15
|
+
const { getIntegrationPath } = require('../utils/paths');
|
|
16
|
+
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
17
|
+
const { loadConfigFile } = require('../utils/config-format');
|
|
18
|
+
const { discoverIntegrationFiles, buildEffectiveDatasourceFiles } = require('./repair-internal');
|
|
19
|
+
const { runDatasourceTestE2E } = require('../datasource/test-e2e');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Derives datasource key from filename when file has no key (same logic as repair).
|
|
23
|
+
* @param {string} fileName - Datasource file name
|
|
24
|
+
* @param {string} systemKey - System key
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
function deriveDatasourceKeyFromFileName(fileName, systemKey) {
|
|
28
|
+
const base = path.basename(fileName, path.extname(fileName));
|
|
29
|
+
if (/^datasource-/.test(base)) {
|
|
30
|
+
const suffix = base.slice('datasource-'.length);
|
|
31
|
+
return systemKey && typeof systemKey === 'string' ? `${systemKey}-${suffix}` : base;
|
|
32
|
+
}
|
|
33
|
+
return base.replace(/-datasource-/, '-');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/* eslint-disable max-statements -- Key resolution from system or files */
|
|
37
|
+
/**
|
|
38
|
+
* Resolves the list of datasource keys for an external system (from system file or discovered files).
|
|
39
|
+
* @param {string} appPath - Integration app path
|
|
40
|
+
* @param {string} configPath - Application config path
|
|
41
|
+
* @param {Object} variables - Loaded application variables (externalIntegration.dataSources = filenames)
|
|
42
|
+
* @param {string} systemKey - System key from system file
|
|
43
|
+
* @param {Object} systemParsed - Parsed system config (may have dataSources array of keys)
|
|
44
|
+
* @param {string[]} datasourceFiles - Discovered datasource filenames
|
|
45
|
+
* @returns {string[]} Sorted list of datasource keys
|
|
46
|
+
*/
|
|
47
|
+
function getDatasourceKeys(appPath, configPath, variables, systemKey, systemParsed, datasourceFiles) {
|
|
48
|
+
const fromSystem = Array.isArray(systemParsed.dataSources) && systemParsed.dataSources.length > 0
|
|
49
|
+
? systemParsed.dataSources
|
|
50
|
+
: null;
|
|
51
|
+
const keys = [];
|
|
52
|
+
const seen = new Set();
|
|
53
|
+
if (fromSystem) {
|
|
54
|
+
fromSystem.forEach(k => {
|
|
55
|
+
if (k && typeof k === 'string' && !seen.has(k)) {
|
|
56
|
+
keys.push(k.trim());
|
|
57
|
+
seen.add(k.trim());
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
keys.sort();
|
|
61
|
+
return keys;
|
|
62
|
+
}
|
|
63
|
+
for (const fileName of datasourceFiles) {
|
|
64
|
+
const filePath = path.join(appPath, fileName);
|
|
65
|
+
if (!fs.existsSync(filePath)) continue;
|
|
66
|
+
try {
|
|
67
|
+
const parsed = loadConfigFile(filePath);
|
|
68
|
+
const key = parsed && typeof parsed.key === 'string' && parsed.key.trim()
|
|
69
|
+
? parsed.key.trim()
|
|
70
|
+
: deriveDatasourceKeyFromFileName(fileName, systemKey);
|
|
71
|
+
if (key && !seen.has(key)) {
|
|
72
|
+
keys.push(key);
|
|
73
|
+
seen.add(key);
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
const key = deriveDatasourceKeyFromFileName(fileName, systemKey);
|
|
77
|
+
if (key && !seen.has(key)) {
|
|
78
|
+
keys.push(key);
|
|
79
|
+
seen.add(key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
keys.sort();
|
|
84
|
+
return keys;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* eslint-disable max-lines-per-function, max-statements -- Load context, then loop over keys */
|
|
88
|
+
/**
|
|
89
|
+
* Runs E2E for all datasources of an external system. Uses each datasource's payloadTemplate (no extra params required).
|
|
90
|
+
*
|
|
91
|
+
* @async
|
|
92
|
+
* @param {string} externalSystem - System key (e.g. hubspot-demo)
|
|
93
|
+
* @param {Object} options - Options passed to each runDatasourceTestE2E
|
|
94
|
+
* @param {string} [options.env] - Environment (dev, tst, pro)
|
|
95
|
+
* @param {boolean} [options.debug] - Include debug, write log
|
|
96
|
+
* @param {boolean} [options.verbose] - Verbose output
|
|
97
|
+
* @param {boolean} [options.async] - If false, sync mode (default true)
|
|
98
|
+
* @returns {Promise<{ success: boolean, results: Array<{ key: string, success: boolean, error?: string }> }>}
|
|
99
|
+
*/
|
|
100
|
+
async function runTestE2EForExternalSystem(externalSystem, options = {}) {
|
|
101
|
+
if (!externalSystem || typeof externalSystem !== 'string') {
|
|
102
|
+
throw new Error('External system name is required');
|
|
103
|
+
}
|
|
104
|
+
const appPath = getIntegrationPath(externalSystem);
|
|
105
|
+
if (!fs.existsSync(appPath)) {
|
|
106
|
+
throw new Error(`Integration path not found: ${appPath}`);
|
|
107
|
+
}
|
|
108
|
+
const configPath = resolveApplicationConfigPath(appPath);
|
|
109
|
+
let variables = {};
|
|
110
|
+
if (fs.existsSync(configPath)) {
|
|
111
|
+
variables = loadConfigFile(configPath);
|
|
112
|
+
}
|
|
113
|
+
const { systemFiles, datasourceFiles: discovered } = discoverIntegrationFiles(appPath);
|
|
114
|
+
if (systemFiles.length === 0) {
|
|
115
|
+
throw new Error(`No system file found in ${appPath}. Expected *-system.yaml or *-system.json`);
|
|
116
|
+
}
|
|
117
|
+
const datasourceFiles = buildEffectiveDatasourceFiles(
|
|
118
|
+
appPath,
|
|
119
|
+
discovered,
|
|
120
|
+
variables.externalIntegration?.dataSources
|
|
121
|
+
);
|
|
122
|
+
const systemPath = path.join(appPath, systemFiles[0]);
|
|
123
|
+
const systemParsed = loadConfigFile(systemPath);
|
|
124
|
+
const systemKey = systemParsed.key ||
|
|
125
|
+
path.basename(systemFiles[0], path.extname(systemFiles[0])).replace(/-system$/, '');
|
|
126
|
+
|
|
127
|
+
const keys = getDatasourceKeys(
|
|
128
|
+
appPath,
|
|
129
|
+
configPath,
|
|
130
|
+
variables,
|
|
131
|
+
systemKey,
|
|
132
|
+
systemParsed,
|
|
133
|
+
datasourceFiles
|
|
134
|
+
);
|
|
135
|
+
if (keys.length === 0) {
|
|
136
|
+
logger.log(chalk.yellow(`No datasources found for ${externalSystem}. Add datasource files and run aifabrix repair.`));
|
|
137
|
+
return { success: true, results: [] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const results = [];
|
|
141
|
+
const opts = {
|
|
142
|
+
app: externalSystem,
|
|
143
|
+
environment: options.env,
|
|
144
|
+
debug: options.debug,
|
|
145
|
+
verbose: options.verbose,
|
|
146
|
+
async: options.async !== false
|
|
147
|
+
};
|
|
148
|
+
for (const key of keys) {
|
|
149
|
+
try {
|
|
150
|
+
const data = await runDatasourceTestE2E(key, opts);
|
|
151
|
+
const steps = data.steps || data.completedActions || [];
|
|
152
|
+
const failed = data.success === false || steps.some(s => s.success === false || s.error);
|
|
153
|
+
results.push({ key, success: !failed });
|
|
154
|
+
} catch (err) {
|
|
155
|
+
results.push({ key, success: false, error: err.message });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const success = results.every(r => r.success);
|
|
159
|
+
return { success, results };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = {
|
|
163
|
+
runTestE2EForExternalSystem,
|
|
164
|
+
getDatasourceKeys
|
|
165
|
+
};
|