@aifabrix/builder 2.41.0 → 2.42.0
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 +1 -1
- package/integration/hubspot/README.md +8 -4
- 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 +34 -1
- package/lib/app/config.js +23 -11
- package/lib/app/index.js +3 -1
- package/lib/app/prompts.js +44 -29
- package/lib/app/readme.js +8 -3
- 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 +42 -11
- 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/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 +507 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/upload.js +71 -40
- package/lib/commands/wizard-core-helpers.js +226 -4
- package/lib/commands/wizard-core.js +67 -29
- package/lib/commands/wizard-dataplane.js +1 -1
- package/lib/commands/wizard-entity-selection.js +43 -0
- package/lib/commands/wizard-headless.js +44 -5
- package/lib/commands/wizard-helpers.js +7 -3
- package/lib/commands/wizard.js +86 -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.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 +1 -1
- 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 +294 -0
- package/lib/generator/wizard-prompts.js +105 -106
- package/lib/generator/wizard-readme.js +88 -0
- package/lib/generator/wizard.js +147 -158
- 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 +16 -0
- 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 +56 -29
- 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 +65 -25
- 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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Credential list command – list credentials from Dataplane
|
|
3
3
|
* GET /api/v1/credential. Used by `aifabrix credential list`.
|
|
4
|
-
* The Controller does not expose this endpoint; credentials are listed from the Dataplane.
|
|
5
4
|
*
|
|
6
5
|
* @fileoverview Credential list command implementation
|
|
7
6
|
* @author AI Fabrix Team
|
|
@@ -10,6 +9,7 @@
|
|
|
10
9
|
|
|
11
10
|
const chalk = require('chalk');
|
|
12
11
|
const logger = require('../utils/logger');
|
|
12
|
+
const { formatCredentialWithStatus } = require('../utils/credential-display');
|
|
13
13
|
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
14
14
|
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
15
15
|
const { normalizeControllerUrl, resolveEnvironment } = require('../core/config');
|
|
@@ -37,12 +37,14 @@ async function getCredentialListAuth(controllerUrl) {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
* Extract credentials array from API response
|
|
41
|
-
*
|
|
40
|
+
* Extract credentials array from API response.
|
|
41
|
+
* Handles: { data: { meta, data: [...] } } (paginated), { data: [...] }, { credentials: [...] }, { items: [...] }.
|
|
42
|
+
* @param {Object} response - API response (e.g. { success, data } from API client, or raw body)
|
|
42
43
|
* @returns {Array}
|
|
43
44
|
*/
|
|
44
45
|
function extractCredentials(response) {
|
|
45
|
-
const
|
|
46
|
+
const body = response?.data ?? response;
|
|
47
|
+
const data = body?.data ?? body;
|
|
46
48
|
const items = data?.credentials ?? data?.items ?? (Array.isArray(data) ? data : []);
|
|
47
49
|
return Array.isArray(items) ? items : [];
|
|
48
50
|
}
|
|
@@ -59,9 +61,10 @@ function displayCredentialList(list, baseUrl) {
|
|
|
59
61
|
return;
|
|
60
62
|
}
|
|
61
63
|
list.forEach((c) => {
|
|
62
|
-
const key
|
|
63
|
-
const
|
|
64
|
-
|
|
64
|
+
const { key, name, statusFormatted, statusLabel } = formatCredentialWithStatus(c);
|
|
65
|
+
const prefix = statusFormatted ? `${statusFormatted} ` : ' ';
|
|
66
|
+
const line = `${prefix}${chalk.cyan(key)} - ${name}${statusLabel}`;
|
|
67
|
+
logger.log(line);
|
|
65
68
|
});
|
|
66
69
|
logger.log('');
|
|
67
70
|
}
|
|
@@ -69,10 +72,10 @@ function displayCredentialList(list, baseUrl) {
|
|
|
69
72
|
/**
|
|
70
73
|
* Ensure controller URL and auth; exit on failure. Returns { controllerUrl, authConfig } when valid.
|
|
71
74
|
* @async
|
|
72
|
-
* @param {Object} options - CLI options
|
|
75
|
+
* @param {Object} options - CLI options
|
|
73
76
|
* @returns {Promise<{controllerUrl: string, authConfig: Object}>}
|
|
74
77
|
*/
|
|
75
|
-
async function ensureControllerAndAuth(options) {
|
|
78
|
+
async function ensureControllerAndAuth(options = {}) {
|
|
76
79
|
const controllerUrl = options.controller || (await resolveControllerUrl());
|
|
77
80
|
if (!controllerUrl) {
|
|
78
81
|
logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
|
|
@@ -91,19 +94,14 @@ async function ensureControllerAndAuth(options) {
|
|
|
91
94
|
}
|
|
92
95
|
|
|
93
96
|
/**
|
|
94
|
-
* Resolve Dataplane URL for credential list (
|
|
97
|
+
* Resolve Dataplane URL for credential list (discover from controller + environment)
|
|
95
98
|
* @async
|
|
96
99
|
* @param {string} controllerUrl - Controller base URL
|
|
97
100
|
* @param {Object} authConfig - Auth config with token
|
|
98
|
-
* @param {Object} options - CLI options
|
|
99
|
-
* @param {string} [options.dataplane] - Optional Dataplane URL override
|
|
100
101
|
* @returns {Promise<string>} Dataplane base URL
|
|
101
102
|
* @throws {Error} When resolution fails (caller should exit)
|
|
102
103
|
*/
|
|
103
|
-
async function resolveCredentialListDataplaneUrl(controllerUrl, authConfig
|
|
104
|
-
if (options.dataplane) {
|
|
105
|
-
return options.dataplane.replace(/\/$/, '');
|
|
106
|
-
}
|
|
104
|
+
async function resolveCredentialListDataplaneUrl(controllerUrl, authConfig) {
|
|
107
105
|
const environment = await resolveEnvironment();
|
|
108
106
|
return await resolveDataplaneUrl(controllerUrl, environment, authConfig);
|
|
109
107
|
}
|
|
@@ -127,12 +125,11 @@ async function fetchAndDisplayCredentials(dataplaneUrl, authConfig, listOptions)
|
|
|
127
125
|
* @param {Object} options - CLI options
|
|
128
126
|
* @returns {Promise<string>} Dataplane URL (never returns on failure; process.exit(1))
|
|
129
127
|
*/
|
|
130
|
-
async function resolveDataplaneUrlOrExit(controllerUrl, authConfig
|
|
128
|
+
async function resolveDataplaneUrlOrExit(controllerUrl, authConfig) {
|
|
131
129
|
try {
|
|
132
|
-
return await resolveCredentialListDataplaneUrl(controllerUrl, authConfig
|
|
130
|
+
return await resolveCredentialListDataplaneUrl(controllerUrl, authConfig);
|
|
133
131
|
} catch (err) {
|
|
134
132
|
logger.error(chalk.red(`❌ Could not resolve Dataplane URL: ${err.message}`));
|
|
135
|
-
logger.error(chalk.gray('Use --dataplane <url> to specify the Dataplane URL directly.'));
|
|
136
133
|
process.exit(1);
|
|
137
134
|
}
|
|
138
135
|
}
|
|
@@ -141,15 +138,13 @@ async function resolveDataplaneUrlOrExit(controllerUrl, authConfig, options) {
|
|
|
141
138
|
* Run credential list command: call GET /api/v1/credential on Dataplane and display results
|
|
142
139
|
* @async
|
|
143
140
|
* @param {Object} options - CLI options
|
|
144
|
-
* @param {string} [options.controller] - Controller URL override
|
|
145
|
-
* @param {string} [options.dataplane] - Dataplane URL override (default: resolved from controller + environment)
|
|
146
141
|
* @param {boolean} [options.activeOnly] - List only active credentials
|
|
147
142
|
* @param {number} [options.pageSize] - Items per page
|
|
148
143
|
* @returns {Promise<void>}
|
|
149
144
|
*/
|
|
150
145
|
async function runCredentialList(options = {}) {
|
|
151
146
|
const { controllerUrl, authConfig } = await ensureControllerAndAuth(options);
|
|
152
|
-
const dataplaneUrl = await resolveDataplaneUrlOrExit(controllerUrl, authConfig
|
|
147
|
+
const dataplaneUrl = await resolveDataplaneUrlOrExit(controllerUrl, authConfig);
|
|
153
148
|
const listOptions = {
|
|
154
149
|
pageSize: options.pageSize || DEFAULT_PAGE_SIZE,
|
|
155
150
|
activeOnly: options.activeOnly
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential push command – pushes KV_* from .env to dataplane.
|
|
3
|
+
* Used by `aifabrix credential push <system-key>`.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Credential push command – push credential secrets to dataplane
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const logger = require('../utils/logger');
|
|
13
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
14
|
+
const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
|
|
15
|
+
const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
|
|
16
|
+
const { getIntegrationPath } = require('../utils/paths');
|
|
17
|
+
const { pushCredentialSecrets } = require('../utils/credential-secrets-env');
|
|
18
|
+
const { generateControllerManifest } = require('../generator/external-controller-manifest');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Validates system-key format.
|
|
22
|
+
* @param {string} systemKey - System key
|
|
23
|
+
* @throws {Error} If invalid
|
|
24
|
+
*/
|
|
25
|
+
function validateSystemKeyFormat(systemKey) {
|
|
26
|
+
if (!systemKey || typeof systemKey !== 'string') {
|
|
27
|
+
throw new Error('System key is required and must be a string');
|
|
28
|
+
}
|
|
29
|
+
if (!/^[a-z0-9-_]+$/.test(systemKey)) {
|
|
30
|
+
throw new Error('System key must contain only lowercase letters, numbers, hyphens, and underscores');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Builds upload payload for credential push (same shape as upload).
|
|
36
|
+
* @param {string} systemKey - System key
|
|
37
|
+
* @returns {Promise<Object>} { version, application, dataSources }
|
|
38
|
+
*/
|
|
39
|
+
async function buildPayload(systemKey) {
|
|
40
|
+
const manifest = await generateControllerManifest(systemKey, { type: 'external' });
|
|
41
|
+
return {
|
|
42
|
+
version: manifest.version || '1.0.0',
|
|
43
|
+
application: manifest.system,
|
|
44
|
+
dataSources: manifest.dataSources || []
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Runs credential push: push KV_* from .env to dataplane.
|
|
50
|
+
* @async
|
|
51
|
+
* @param {string} systemKey - External system key
|
|
52
|
+
* @returns {Promise<{ pushed: number }>} Count of secrets pushed
|
|
53
|
+
* @throws {Error} If auth or push fails
|
|
54
|
+
*/
|
|
55
|
+
function logPushResult(pushResult) {
|
|
56
|
+
if (pushResult.pushed > 0) {
|
|
57
|
+
const keyList = pushResult.keys?.length ? ` (${pushResult.keys.join(', ')})` : '';
|
|
58
|
+
logger.log(chalk.green(`✓ Pushed ${pushResult.pushed} credential secret(s) to dataplane${keyList}.`));
|
|
59
|
+
} else if (pushResult.skipped) {
|
|
60
|
+
logger.log(chalk.yellow('No credential secrets to push (empty .env or no KV_* vars with values).'));
|
|
61
|
+
} else {
|
|
62
|
+
logger.log(chalk.yellow('Secret push skipped'));
|
|
63
|
+
}
|
|
64
|
+
if (pushResult.warning) logger.log(chalk.yellow(`Warning: ${pushResult.warning}`));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function runCredentialPush(systemKey) {
|
|
68
|
+
validateSystemKeyFormat(systemKey);
|
|
69
|
+
const appPath = getIntegrationPath(systemKey);
|
|
70
|
+
const envFilePath = path.join(appPath, '.env');
|
|
71
|
+
|
|
72
|
+
const { resolveEnvironment } = require('../core/config');
|
|
73
|
+
const environment = await resolveEnvironment();
|
|
74
|
+
const controllerUrl = await resolveControllerUrl();
|
|
75
|
+
const authConfig = await getDeploymentAuth(controllerUrl, environment, systemKey);
|
|
76
|
+
|
|
77
|
+
if (!authConfig.token && !authConfig.clientId) {
|
|
78
|
+
throw new Error('Authentication required. Run "aifabrix login" or "aifabrix app register <system-key>" first.');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
requireBearerForDataplanePipeline(authConfig);
|
|
82
|
+
logger.log(chalk.blue('Resolving dataplane URL...'));
|
|
83
|
+
const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
|
|
84
|
+
|
|
85
|
+
const payload = await buildPayload(systemKey);
|
|
86
|
+
const pushResult = await pushCredentialSecrets(dataplaneUrl, authConfig, {
|
|
87
|
+
envFilePath,
|
|
88
|
+
appName: systemKey,
|
|
89
|
+
payload
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
logPushResult(pushResult);
|
|
93
|
+
return { pushed: pushResult.pushed || 0 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { runCredentialPush, validateSystemKeyFormat, buildPayload };
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* AI Fabrix Builder - Datasource Commands
|
|
3
3
|
*
|
|
4
4
|
* Handles datasource validation, listing, comparison, and deployment
|
|
5
|
-
* Commands: datasource validate, datasource list, datasource diff, datasource
|
|
5
|
+
* Commands: datasource validate, datasource list, datasource diff, datasource upload
|
|
6
6
|
*
|
|
7
7
|
* @fileoverview Datasource management commands for AI Fabrix Builder
|
|
8
8
|
* @author AI Fabrix Team
|
|
@@ -15,6 +15,9 @@ const { validateDatasourceFile } = require('../datasource/validate');
|
|
|
15
15
|
const { listDatasources } = require('../datasource/list');
|
|
16
16
|
const { compareDatasources } = require('../datasource/diff');
|
|
17
17
|
const { deployDatasource } = require('../datasource/deploy');
|
|
18
|
+
const { runDatasourceTestIntegration } = require('../datasource/test-integration');
|
|
19
|
+
const { runDatasourceTestE2E } = require('../datasource/test-e2e');
|
|
20
|
+
const { displayIntegrationTestResults, displayE2EResults } = require('../utils/external-system-display');
|
|
18
21
|
|
|
19
22
|
function setupDatasourceValidateCommand(datasource) {
|
|
20
23
|
datasource.command('validate <file>')
|
|
@@ -62,14 +65,80 @@ function setupDatasourceDiffCommand(datasource) {
|
|
|
62
65
|
});
|
|
63
66
|
}
|
|
64
67
|
|
|
65
|
-
function
|
|
66
|
-
datasource.command('
|
|
67
|
-
.description('
|
|
68
|
+
function setupDatasourceUploadCommand(datasource) {
|
|
69
|
+
datasource.command('upload <myapp> <file>')
|
|
70
|
+
.description('Upload datasource to dataplane')
|
|
68
71
|
.action(async(myapp, file, options) => {
|
|
69
72
|
try {
|
|
70
73
|
await deployDatasource(myapp, file, options);
|
|
71
74
|
} catch (error) {
|
|
72
|
-
logger.error(chalk.red('❌
|
|
75
|
+
logger.error(chalk.red('❌ Upload failed:'), error.message);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function setupDatasourceTestIntegrationCommand(datasource) {
|
|
82
|
+
datasource.command('test-integration <datasourceKey>')
|
|
83
|
+
.description('Run integration (config) test for one datasource via dataplane pipeline')
|
|
84
|
+
.option('-a, --app <appKey>', 'App key (default: resolve from cwd if inside integration/<appKey>/)')
|
|
85
|
+
.option('-p, --payload <file>', 'Path to custom test payload file')
|
|
86
|
+
.option('-e, --env <env>', 'Environment: dev, tst, or pro')
|
|
87
|
+
.option('--debug', 'Include debug output and write log to integration/<appKey>/logs/')
|
|
88
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
|
|
89
|
+
.action(async(datasourceKey, options) => {
|
|
90
|
+
try {
|
|
91
|
+
const result = await runDatasourceTestIntegration(datasourceKey, {
|
|
92
|
+
app: options.app,
|
|
93
|
+
payload: options.payload,
|
|
94
|
+
environment: options.env,
|
|
95
|
+
debug: options.debug,
|
|
96
|
+
timeout: options.timeout
|
|
97
|
+
});
|
|
98
|
+
displayIntegrationTestResults({
|
|
99
|
+
systemKey: result.systemKey || 'unknown',
|
|
100
|
+
datasourceResults: [result],
|
|
101
|
+
success: result.success
|
|
102
|
+
}, options.verbose);
|
|
103
|
+
if (!result.success) process.exit(1);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
logger.error(chalk.red('❌ Integration test failed:'), error.message);
|
|
106
|
+
process.exit(1);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function setupDatasourceTestE2ECommand(datasource) {
|
|
112
|
+
datasource.command('test-e2e <datasourceKey>')
|
|
113
|
+
.description('Run E2E test for one datasource (config, credential, sync, data, CIP) via dataplane')
|
|
114
|
+
.option('-a, --app <appKey>', 'App key (default: resolve from cwd if inside integration/<appKey>/)')
|
|
115
|
+
.option('-e, --env <env>', 'Environment: dev, tst, or pro')
|
|
116
|
+
.option('-v, --verbose', 'Show detailed step output and poll progress')
|
|
117
|
+
.option('--debug', 'Include debug output and write log to integration/<appKey>/logs/')
|
|
118
|
+
.option('--test-crud', 'Enable CRUD lifecycle test (body testCrud: true)')
|
|
119
|
+
.option('--record-id <id>', 'Record ID for test (body recordId)')
|
|
120
|
+
.option('--no-cleanup', 'Disable cleanup after test (body cleanup: false)')
|
|
121
|
+
.option('--primary-key-value <value|@path>', 'Primary key value or path to JSON file (e.g. @pk.json) for body primaryKeyValue')
|
|
122
|
+
.option('--no-async', 'Use sync mode (no polling); single POST, no asyncRun')
|
|
123
|
+
.action(async(datasourceKey, options) => {
|
|
124
|
+
try {
|
|
125
|
+
const data = await runDatasourceTestE2E(datasourceKey, {
|
|
126
|
+
app: options.app,
|
|
127
|
+
environment: options.env,
|
|
128
|
+
debug: options.debug,
|
|
129
|
+
verbose: options.verbose,
|
|
130
|
+
async: options.async !== false,
|
|
131
|
+
testCrud: options.testCrud,
|
|
132
|
+
recordId: options.recordId,
|
|
133
|
+
cleanup: options.cleanup,
|
|
134
|
+
primaryKeyValue: options.primaryKeyValue
|
|
135
|
+
});
|
|
136
|
+
displayE2EResults(data, options.verbose);
|
|
137
|
+
const steps = data.steps || data.completedActions || [];
|
|
138
|
+
const failed = data.success === false || steps.some(s => s.success === false || s.error);
|
|
139
|
+
if (failed) process.exit(1);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
logger.error(chalk.red('❌ E2E test failed:'), error.message);
|
|
73
142
|
process.exit(1);
|
|
74
143
|
}
|
|
75
144
|
});
|
|
@@ -84,7 +153,9 @@ function setupDatasourceCommands(program) {
|
|
|
84
153
|
setupDatasourceValidateCommand(datasource);
|
|
85
154
|
setupDatasourceListCommand(datasource);
|
|
86
155
|
setupDatasourceDiffCommand(datasource);
|
|
87
|
-
|
|
156
|
+
setupDatasourceUploadCommand(datasource);
|
|
157
|
+
setupDatasourceTestIntegrationCommand(datasource);
|
|
158
|
+
setupDatasourceTestE2ECommand(datasource);
|
|
88
159
|
}
|
|
89
160
|
|
|
90
161
|
module.exports = { setupDatasourceCommands };
|
package/lib/commands/dev-init.js
CHANGED
|
@@ -14,6 +14,44 @@ const { generateCSR, getCertDir, readClientCertPem, readClientKeyPem, getCertVal
|
|
|
14
14
|
const { getOrCreatePublicKeyContent } = require('../utils/ssh-key-helper');
|
|
15
15
|
const devApi = require('../api/dev.api');
|
|
16
16
|
const logger = require('../utils/logger');
|
|
17
|
+
const {
|
|
18
|
+
isSslUntrustedError,
|
|
19
|
+
fetchInstallCa,
|
|
20
|
+
installCaPlatform,
|
|
21
|
+
promptInstallCa
|
|
22
|
+
} = require('../utils/dev-ca-install');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Ensure the Builder Server is trusted: run health check; on SSL untrusted error,
|
|
26
|
+
* optionally fetch and install CA, then retry.
|
|
27
|
+
* @param {string} baseUrl - Builder Server base URL
|
|
28
|
+
* @param {Object} options - Commander options (yes, y, no-install-ca)
|
|
29
|
+
* @returns {Promise<void>}
|
|
30
|
+
*/
|
|
31
|
+
async function ensureServerTrusted(baseUrl, options) {
|
|
32
|
+
const skipInstall = options['no-install-ca'];
|
|
33
|
+
const autoInstall = options.yes || options.y;
|
|
34
|
+
try {
|
|
35
|
+
await devApi.getHealth(baseUrl);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (!isSslUntrustedError(err)) throw err;
|
|
38
|
+
const manualUrl = `${baseUrl.replace(/\/+$/, '')}/install-ca`;
|
|
39
|
+
if (skipInstall) {
|
|
40
|
+
throw new Error(`Server certificate not trusted. Install CA manually: ${manualUrl}`);
|
|
41
|
+
}
|
|
42
|
+
if (!autoInstall) {
|
|
43
|
+
const install = await promptInstallCa();
|
|
44
|
+
if (!install) {
|
|
45
|
+
throw new Error(`Server certificate not trusted. Install CA manually: ${manualUrl}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
logger.log(chalk.gray(' Downloading and installing CA...'));
|
|
49
|
+
const caPem = await fetchInstallCa(baseUrl);
|
|
50
|
+
await installCaPlatform(caPem, baseUrl);
|
|
51
|
+
logger.log(chalk.gray(' CA installed. Retrying...'));
|
|
52
|
+
await devApi.getHealth(baseUrl);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
17
55
|
|
|
18
56
|
/**
|
|
19
57
|
* Validate init options and return normalized baseUrl and devId.
|
|
@@ -213,7 +251,7 @@ async function runDevInit(options) {
|
|
|
213
251
|
logger.log(chalk.blue('\n🔐 Onboarding with Builder Server...\n'));
|
|
214
252
|
|
|
215
253
|
try {
|
|
216
|
-
await
|
|
254
|
+
await ensureServerTrusted(baseUrl, options);
|
|
217
255
|
} catch (err) {
|
|
218
256
|
throw new Error(`Cannot reach Builder Server at ${baseUrl}. Check URL and network. ${err.message}`);
|
|
219
257
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalizes system file authentication.security and configuration keyvault entries.
|
|
3
|
+
* @fileoverview Repair auth/config KV_* names and path-style kv:// values
|
|
4
|
+
* @author AI Fabrix Team
|
|
5
|
+
* @version 2.0.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const { systemKeyToKvPrefix, securityKeyToVar, kvEnvKeyToPath } = require('../utils/credential-secrets-env');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Returns true if a kv value looks like legacy format (KeyVault suffix or no path segments).
|
|
14
|
+
* @param {string} val - Value from authentication.security or configuration
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
function isLegacyKvValue(val) {
|
|
18
|
+
if (typeof val !== 'string' || !val.trim().toLowerCase().startsWith('kv://')) return false;
|
|
19
|
+
const after = val.trim().slice(5); // after 'kv://'
|
|
20
|
+
return after.includes('KeyVault') || !after.includes('/');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Normalizes authentication.security keyvault entries to path-style kv:// values (kv://systemKey/variable).
|
|
25
|
+
* @param {Object} security - authentication.security object (mutated)
|
|
26
|
+
* @param {string} prefix - KV prefix (e.g. 'HUBSPOT')
|
|
27
|
+
* @param {string} systemKey - System key for path namespace
|
|
28
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
29
|
+
* @returns {boolean} True if any change was made
|
|
30
|
+
*/
|
|
31
|
+
function normalizeSecuritySection(security, prefix, systemKey, changes) {
|
|
32
|
+
let updated = false;
|
|
33
|
+
for (const key of Object.keys(security)) {
|
|
34
|
+
const val = security[key];
|
|
35
|
+
if (typeof val !== 'string' || !isLegacyKvValue(val)) continue;
|
|
36
|
+
const envName = `KV_${prefix}_${securityKeyToVar(key)}`;
|
|
37
|
+
const pathVal = kvEnvKeyToPath(envName, systemKey);
|
|
38
|
+
if (pathVal) {
|
|
39
|
+
security[key] = pathVal;
|
|
40
|
+
changes.push(`authentication.security.${key}: normalized to path-style ${pathVal}`);
|
|
41
|
+
updated = true;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return updated;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalizes configuration array keyvault entries to canonical KV_* names and path-style values.
|
|
49
|
+
* @param {Object[]} config - configuration array (mutated)
|
|
50
|
+
* @param {string} prefix - KV prefix (e.g. 'HUBSPOT')
|
|
51
|
+
* @param {string} systemKey - System key for path namespace
|
|
52
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
53
|
+
* @returns {boolean} True if any change was made
|
|
54
|
+
*/
|
|
55
|
+
function normalizeConfigurationSection(config, prefix, systemKey, changes) {
|
|
56
|
+
let updated = false;
|
|
57
|
+
for (let i = 0; i < config.length; i++) {
|
|
58
|
+
const entry = config[i];
|
|
59
|
+
if (!entry || !entry.name || (entry.location !== 'keyvault' && !String(entry.name).startsWith('KV_'))) continue;
|
|
60
|
+
const afterPrefix = entry.name.startsWith(`KV_${prefix}_`)
|
|
61
|
+
? entry.name.slice(`KV_${prefix}_`.length)
|
|
62
|
+
: entry.name.replace(/^KV_[A-Z0-9]+_/, '');
|
|
63
|
+
const normalizedVar = afterPrefix.replace(/_/g, '').toUpperCase();
|
|
64
|
+
const canonicalName = `KV_${prefix}_${normalizedVar}`;
|
|
65
|
+
const pathVal = kvEnvKeyToPath(canonicalName, systemKey);
|
|
66
|
+
if (!pathVal) continue;
|
|
67
|
+
const pathValWithoutPrefix = pathVal.replace(/^kv:\/\//, '');
|
|
68
|
+
const valueLegacy = typeof entry.value === 'string' && (entry.value.includes('KeyVault') || !entry.value.includes('/'));
|
|
69
|
+
if (entry.name !== canonicalName || (valueLegacy && entry.value !== pathValWithoutPrefix)) {
|
|
70
|
+
config[i] = { ...entry, name: canonicalName, value: pathValWithoutPrefix, location: 'keyvault' };
|
|
71
|
+
changes.push(`configuration: normalized ${entry.name} → ${canonicalName}, value → path-style`);
|
|
72
|
+
updated = true;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return updated;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Normalizes system file authentication.security and configuration keyvault entries to canonical
|
|
80
|
+
* KV_* names and path-style kv:// values so upload validation and env.template align.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} systemParsed - Parsed system config (mutated)
|
|
83
|
+
* @param {string} systemKey - System key (e.g. 'hubspot')
|
|
84
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
85
|
+
* @returns {boolean} True if any change was made
|
|
86
|
+
*/
|
|
87
|
+
function normalizeSystemFileAuthAndConfig(systemParsed, systemKey, changes) {
|
|
88
|
+
const prefix = systemKeyToKvPrefix(systemKey);
|
|
89
|
+
if (!prefix) return false;
|
|
90
|
+
const security = systemParsed.authentication?.security;
|
|
91
|
+
let updated = (security && typeof security === 'object' && normalizeSecuritySection(security, prefix, systemKey, changes));
|
|
92
|
+
const config = systemParsed.configuration;
|
|
93
|
+
if (Array.isArray(config)) {
|
|
94
|
+
updated = normalizeConfigurationSection(config, prefix, systemKey, changes) || updated;
|
|
95
|
+
}
|
|
96
|
+
return updated;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = { normalizeSystemFileAuthAndConfig, isLegacyKvValue };
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize datasource keys and filenames to canonical form during repair.
|
|
3
|
+
*
|
|
4
|
+
* Key: <systemKey>-<resourceType> or <systemKey>-<resourceType>-2, -3 for duplicates.
|
|
5
|
+
* Filename: <systemKey>-datasource-<suffix>.<ext> where suffix = key without leading systemKey-.
|
|
6
|
+
* Skips keys/filenames that already match the valid pattern (e.g. customer-extra, customer-1).
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview Datasource key and filename normalization for repair
|
|
9
|
+
* @author AI Fabrix Team
|
|
10
|
+
* @version 2.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const { loadConfigFile, writeConfigFile } = require('../utils/config-format');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Returns suffix from a canonical-format filename: <systemKey>-datasource-<suffix>.<ext>.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} fileName - Filename
|
|
23
|
+
* @param {string} systemKey - System key
|
|
24
|
+
* @returns {string|null} Suffix or null if not in canonical format
|
|
25
|
+
*/
|
|
26
|
+
function suffixFromCanonicalFilename(fileName, systemKey) {
|
|
27
|
+
const base = path.basename(fileName);
|
|
28
|
+
const ext = path.extname(fileName);
|
|
29
|
+
const withoutExt = base.slice(0, -ext.length);
|
|
30
|
+
const prefix = `${systemKey}-datasource-`;
|
|
31
|
+
if (!withoutExt.startsWith(prefix)) return null;
|
|
32
|
+
return withoutExt.slice(prefix.length) || null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Returns true if the key already matches canonical form and should not be changed.
|
|
37
|
+
* Valid: <systemKey>-<resourceType> or <systemKey>-<resourceType>-<extra> (e.g. customer-extra, customer-1).
|
|
38
|
+
* When fileName is provided and is canonical, key may be just the suffix (e.g. record-storage).
|
|
39
|
+
* Invalid (will normalize): key ending with redundant -datasource (e.g. hubspot-demo-companies-datasource).
|
|
40
|
+
*
|
|
41
|
+
* @param {string} key - Datasource key
|
|
42
|
+
* @param {string} systemKey - System key
|
|
43
|
+
* @param {string} [fileName] - Optional filename; if canonical, key can be suffix-only
|
|
44
|
+
* @returns {boolean}
|
|
45
|
+
*/
|
|
46
|
+
function isKeyAlreadyCanonical(key, systemKey, fileName) {
|
|
47
|
+
if (!key || !systemKey) return false;
|
|
48
|
+
if (fileName && isFilenameAlreadyCanonical(fileName, systemKey)) {
|
|
49
|
+
const suffixFromFile = suffixFromCanonicalFilename(fileName, systemKey);
|
|
50
|
+
if (suffixFromFile && (key === suffixFromFile || key === `${systemKey}-${suffixFromFile}`)) {
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (!key.startsWith(systemKey + '-')) return false;
|
|
55
|
+
const suffix = key.slice(systemKey.length + 1);
|
|
56
|
+
if (!suffix) return false;
|
|
57
|
+
if (suffix.endsWith('-datasource')) return false;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Derives resourceType slug from key: strip systemKey prefix, then strip trailing -datasource if present.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} key - Current datasource key
|
|
65
|
+
* @param {string} systemKey - System key
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function slugFromKey(key, systemKey) {
|
|
69
|
+
if (!key || !systemKey || !key.startsWith(systemKey + '-')) return key || '';
|
|
70
|
+
let suffix = key.slice(systemKey.length + 1);
|
|
71
|
+
if (suffix.endsWith('-datasource')) suffix = suffix.slice(0, -'-datasource'.length);
|
|
72
|
+
return suffix || key;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Returns canonical filename for a datasource: <systemKey>-datasource-<suffix>.<ext>.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} canonicalKey - Canonical datasource key
|
|
79
|
+
* @param {string} systemKey - System key
|
|
80
|
+
* @param {string} ext - File extension including dot (e.g. .json)
|
|
81
|
+
* @returns {string}
|
|
82
|
+
*/
|
|
83
|
+
function canonicalDatasourceFilename(canonicalKey, systemKey, ext) {
|
|
84
|
+
const suffix = canonicalKey.startsWith(systemKey + '-')
|
|
85
|
+
? canonicalKey.slice(systemKey.length + 1)
|
|
86
|
+
: canonicalKey;
|
|
87
|
+
return `${systemKey}-datasource-${suffix}${ext}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Returns true if filename already matches canonical pattern <systemKey>-datasource-<suffix>.<ext>.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} fileName - Current filename
|
|
94
|
+
* @param {string} systemKey - System key
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
function isFilenameAlreadyCanonical(fileName, systemKey) {
|
|
98
|
+
const base = path.basename(fileName);
|
|
99
|
+
const ext = path.extname(fileName);
|
|
100
|
+
const withoutExt = base.slice(0, -ext.length);
|
|
101
|
+
const prefix = `${systemKey}-datasource-`;
|
|
102
|
+
if (!withoutExt.startsWith(prefix)) return false;
|
|
103
|
+
const suffix = withoutExt.slice(prefix.length);
|
|
104
|
+
if (!suffix || suffix.endsWith('-datasource')) return false;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Normalizes datasource keys and filenames to canonical form. Runs early in repair.
|
|
110
|
+
* Updates file contents (key property), renames files when needed, and updates variables.externalIntegration.dataSources.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} appPath - Application directory path
|
|
113
|
+
* @param {string[]} datasourceFiles - Current list of datasource filenames
|
|
114
|
+
* @param {string} systemKey - System key
|
|
115
|
+
* @param {Object} variables - Application variables (mutated: externalIntegration.dataSources updated)
|
|
116
|
+
* @param {boolean} dryRun - If true, do not write or rename
|
|
117
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
118
|
+
* @returns {{ updated: boolean, datasourceFiles: string[] }} Updated flag and new list of datasource filenames
|
|
119
|
+
*/
|
|
120
|
+
/* eslint-disable max-lines-per-function, max-statements, complexity -- Normalization loops and branching per file */
|
|
121
|
+
function normalizeDatasourceKeysAndFilenames(appPath, datasourceFiles, systemKey, variables, dryRun, changes) {
|
|
122
|
+
if (!datasourceFiles || datasourceFiles.length === 0) {
|
|
123
|
+
return { updated: false, datasourceFiles: datasourceFiles || [] };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const slugCounts = new Map();
|
|
127
|
+
const fileInfos = [];
|
|
128
|
+
|
|
129
|
+
for (const fileName of datasourceFiles) {
|
|
130
|
+
const filePath = path.join(appPath, fileName);
|
|
131
|
+
if (!fs.existsSync(filePath)) continue;
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = loadConfigFile(filePath);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
fileInfos.push({ fileName, key: null, skip: true });
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const key = (parsed && typeof parsed.key === 'string' && parsed.key.trim()) ? parsed.key.trim() : null;
|
|
140
|
+
if (isKeyAlreadyCanonical(key, systemKey, fileName) && isFilenameAlreadyCanonical(fileName, systemKey)) {
|
|
141
|
+
fileInfos.push({ fileName, key, skip: true });
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const slug = slugFromKey(key || fileName, systemKey);
|
|
145
|
+
fileInfos.push({
|
|
146
|
+
fileName,
|
|
147
|
+
parsed,
|
|
148
|
+
key,
|
|
149
|
+
slug,
|
|
150
|
+
canonicalKey: null,
|
|
151
|
+
skip: false
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
for (const info of fileInfos) {
|
|
156
|
+
if (info.skip) continue;
|
|
157
|
+
const slug = info.slug;
|
|
158
|
+
const n = (slugCounts.get(slug) || 0) + 1;
|
|
159
|
+
slugCounts.set(slug, n);
|
|
160
|
+
info.canonicalKey = n === 1 ? `${systemKey}-${slug}` : `${systemKey}-${slug}-${n}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let updated = false;
|
|
164
|
+
const newDatasourceFiles = [];
|
|
165
|
+
|
|
166
|
+
for (const info of fileInfos) {
|
|
167
|
+
if (info.skip) {
|
|
168
|
+
newDatasourceFiles.push(info.fileName);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const { fileName, parsed, canonicalKey } = info;
|
|
172
|
+
const ext = path.extname(fileName);
|
|
173
|
+
const canonicalFileName = canonicalDatasourceFilename(canonicalKey, systemKey, ext);
|
|
174
|
+
|
|
175
|
+
if (parsed.key !== canonicalKey) {
|
|
176
|
+
parsed.key = canonicalKey;
|
|
177
|
+
if (!dryRun) writeConfigFile(path.join(appPath, fileName), parsed);
|
|
178
|
+
changes.push(`${fileName}: key → ${canonicalKey}`);
|
|
179
|
+
updated = true;
|
|
180
|
+
}
|
|
181
|
+
if (fileName !== canonicalFileName) {
|
|
182
|
+
const oldPath = path.join(appPath, fileName);
|
|
183
|
+
const newPath = path.join(appPath, canonicalFileName);
|
|
184
|
+
if (!dryRun && fs.existsSync(oldPath) && !fs.existsSync(newPath)) {
|
|
185
|
+
fs.renameSync(oldPath, newPath);
|
|
186
|
+
}
|
|
187
|
+
changes.push(`Renamed ${fileName} → ${canonicalFileName}`);
|
|
188
|
+
updated = true;
|
|
189
|
+
newDatasourceFiles.push(canonicalFileName);
|
|
190
|
+
} else {
|
|
191
|
+
newDatasourceFiles.push(fileName);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (updated && variables.externalIntegration && Array.isArray(variables.externalIntegration.dataSources)) {
|
|
196
|
+
variables.externalIntegration.dataSources = [...newDatasourceFiles].sort();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return { updated, datasourceFiles: newDatasourceFiles };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
normalizeDatasourceKeysAndFilenames,
|
|
204
|
+
isKeyAlreadyCanonical,
|
|
205
|
+
slugFromKey,
|
|
206
|
+
canonicalDatasourceFilename,
|
|
207
|
+
isFilenameAlreadyCanonical
|
|
208
|
+
};
|