@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,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
|
+
};
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Datasource repair helpers: align dimensions, metadataSchema, exposed, sync, testPayload
|
|
3
|
+
* with fieldMappings.attributes as source of truth.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Repair datasource files for external integration
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const DEFAULT_SYNC = {
|
|
13
|
+
mode: 'pull',
|
|
14
|
+
batchSize: 500,
|
|
15
|
+
maxParallelRequests: 5
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const MINIMAL_METADATA_SCHEMA = {
|
|
19
|
+
type: 'object',
|
|
20
|
+
additionalProperties: true
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Returns the set of attribute keys from fieldMappings.attributes.
|
|
25
|
+
* @param {Object} parsed - Parsed datasource object
|
|
26
|
+
* @returns {Set<string>}
|
|
27
|
+
*/
|
|
28
|
+
function getAttributeKeys(parsed) {
|
|
29
|
+
const attrs = parsed?.fieldMappings?.attributes;
|
|
30
|
+
if (!attrs || typeof attrs !== 'object') return new Set();
|
|
31
|
+
return new Set(Object.keys(attrs));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extracts paths from attribute expressions (e.g. {{ metadata.email }}).
|
|
36
|
+
* Skips record_ref: expressions.
|
|
37
|
+
* - topLevelKeys: first segment of each path (e.g. "metadata" from "metadata.id").
|
|
38
|
+
* - referencedSchemaPropertyNames: for paths like "metadata.xxx" or "metadata.xxx.yyy", the name "xxx"
|
|
39
|
+
* (the first property under metadata). Used to prune metadataSchema.properties so we only keep
|
|
40
|
+
* properties that are referenced; we do not compare against "metadata" or the schema would be wiped.
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} attributes - fieldMappings.attributes object
|
|
43
|
+
* @returns {{ paths: string[], topLevelKeys: Set<string>, referencedSchemaPropertyNames: Set<string> }}
|
|
44
|
+
*/
|
|
45
|
+
function parsePathsFromExpressions(attributes) {
|
|
46
|
+
const paths = [];
|
|
47
|
+
const topLevelKeys = new Set();
|
|
48
|
+
const referencedSchemaPropertyNames = new Set();
|
|
49
|
+
if (!attributes || typeof attributes !== 'object') return { paths, topLevelKeys, referencedSchemaPropertyNames };
|
|
50
|
+
for (const attr of Object.values(attributes)) {
|
|
51
|
+
const expr = attr?.expression;
|
|
52
|
+
if (typeof expr !== 'string') continue;
|
|
53
|
+
if (/^\s*record_ref:/i.test(expr.trim())) continue;
|
|
54
|
+
const match = expr.match(/\{\{\s*([^}]+)\s*\}\}/);
|
|
55
|
+
if (!match) continue;
|
|
56
|
+
const path = match[1].trim().split('|')[0].trim();
|
|
57
|
+
if (path) {
|
|
58
|
+
paths.push(path);
|
|
59
|
+
const segments = path.split('.');
|
|
60
|
+
const first = segments[0];
|
|
61
|
+
if (first) topLevelKeys.add(first);
|
|
62
|
+
if (first === 'metadata' && segments.length >= 2 && segments[1]) {
|
|
63
|
+
referencedSchemaPropertyNames.add(segments[1]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { paths, topLevelKeys, referencedSchemaPropertyNames };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Removes dimension entries whose value is metadata.<attr> and attr is not in fieldMappings.attributes.
|
|
72
|
+
* Mutates parsed.fieldMappings.dimensions.
|
|
73
|
+
*
|
|
74
|
+
* @param {Object} parsed - Parsed datasource (mutated)
|
|
75
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
76
|
+
* @returns {boolean} True if any dimension was removed
|
|
77
|
+
*/
|
|
78
|
+
function repairDimensionsFromAttributes(parsed, changes) {
|
|
79
|
+
const dims = parsed?.fieldMappings?.dimensions;
|
|
80
|
+
if (!dims || typeof dims !== 'object') return false;
|
|
81
|
+
const attributeKeys = getAttributeKeys(parsed);
|
|
82
|
+
let updated = false;
|
|
83
|
+
for (const [dimKey, value] of Object.entries(dims)) {
|
|
84
|
+
if (typeof value !== 'string') continue;
|
|
85
|
+
if (!value.startsWith('metadata.')) continue;
|
|
86
|
+
const attr = value.slice('metadata.'.length).trim();
|
|
87
|
+
if (!attr || attributeKeys.has(attr)) continue;
|
|
88
|
+
delete dims[dimKey];
|
|
89
|
+
changes.push(`Removed dimension '${dimKey}': ${value} not in fieldMappings.attributes`);
|
|
90
|
+
updated = true;
|
|
91
|
+
}
|
|
92
|
+
return updated;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Ensures metadataSchema exists (minimal stub if missing). If present, prunes top-level
|
|
97
|
+
* properties not referenced by any attribute expression. Uses the first property name under
|
|
98
|
+
* "metadata" in paths (e.g. metadata.id → "id") so we do not remove schema properties that
|
|
99
|
+
* are referenced. If no metadata.xxx paths exist, we do not prune (keep all properties).
|
|
100
|
+
*
|
|
101
|
+
* @param {Object} parsed - Parsed datasource (mutated)
|
|
102
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
103
|
+
* @returns {boolean} True if schema was added or pruned
|
|
104
|
+
*/
|
|
105
|
+
function repairMetadataSchemaFromAttributes(parsed, changes) {
|
|
106
|
+
const { referencedSchemaPropertyNames } = parsePathsFromExpressions(parsed?.fieldMappings?.attributes ?? {});
|
|
107
|
+
if (!parsed.metadataSchema || typeof parsed.metadataSchema !== 'object') {
|
|
108
|
+
parsed.metadataSchema = { ...MINIMAL_METADATA_SCHEMA };
|
|
109
|
+
changes.push('Added minimal metadataSchema (was missing)');
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
const props = parsed.metadataSchema.properties;
|
|
113
|
+
if (!props || typeof props !== 'object') return false;
|
|
114
|
+
if (referencedSchemaPropertyNames.size === 0) return false;
|
|
115
|
+
const toRemove = Object.keys(props).filter(k => !referencedSchemaPropertyNames.has(k));
|
|
116
|
+
if (toRemove.length === 0) return false;
|
|
117
|
+
toRemove.forEach(k => delete props[k]);
|
|
118
|
+
changes.push(`Pruned metadataSchema.properties: removed [${toRemove.join(', ')}] (not referenced by attributes)`);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Sets exposed.attributes to the list of fieldMappings.attributes keys (sorted).
|
|
124
|
+
* Only when options.expose is true; caller should gate.
|
|
125
|
+
*
|
|
126
|
+
* @param {Object} parsed - Parsed datasource (mutated)
|
|
127
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
128
|
+
* @returns {boolean} True if exposed was updated
|
|
129
|
+
*/
|
|
130
|
+
function repairExposeFromAttributes(parsed, changes) {
|
|
131
|
+
const keys = Array.from(getAttributeKeys(parsed)).filter(Boolean).sort();
|
|
132
|
+
if (keys.length === 0) return false;
|
|
133
|
+
if (!parsed.exposed) parsed.exposed = {};
|
|
134
|
+
const prev = parsed.exposed.attributes;
|
|
135
|
+
const same = Array.isArray(prev) && prev.length === keys.length && prev.every((v, i) => v === keys[i]);
|
|
136
|
+
if (same) return false;
|
|
137
|
+
parsed.exposed.attributes = keys;
|
|
138
|
+
changes.push(`Set exposed.attributes to [${keys.join(', ')}]`);
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Adds default sync section if missing or empty.
|
|
144
|
+
*
|
|
145
|
+
* @param {Object} parsed - Parsed datasource (mutated)
|
|
146
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
147
|
+
* @returns {boolean} True if sync was added
|
|
148
|
+
*/
|
|
149
|
+
function repairSyncSection(parsed, changes) {
|
|
150
|
+
const sync = parsed.sync;
|
|
151
|
+
if (sync && typeof sync === 'object' && Object.keys(sync).length > 0) return false;
|
|
152
|
+
parsed.sync = { ...DEFAULT_SYNC };
|
|
153
|
+
changes.push('Added default sync section (mode: pull, batchSize: 500, maxParallelRequests: 5)');
|
|
154
|
+
return true;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function placeholderForType(type) {
|
|
158
|
+
if (type === 'number' || type === 'integer') return 0;
|
|
159
|
+
if (type === 'boolean') return false;
|
|
160
|
+
if (type === 'array') return [];
|
|
161
|
+
if (type === 'object') return {};
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function setNested(obj, pathParts, value) {
|
|
166
|
+
let cur = obj;
|
|
167
|
+
for (let i = 0; i < pathParts.length - 1; i++) {
|
|
168
|
+
const p = pathParts[i];
|
|
169
|
+
if (!(p in cur) || typeof cur[p] !== 'object') cur[p] = {};
|
|
170
|
+
cur = cur[p];
|
|
171
|
+
}
|
|
172
|
+
const last = pathParts[pathParts.length - 1];
|
|
173
|
+
if (last) cur[last] = value;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Builds minimal payloadTemplate and expectedResult from attribute expression paths.
|
|
178
|
+
*
|
|
179
|
+
* @param {Object} parsed - Parsed datasource (mutated)
|
|
180
|
+
* @param {string[]} changes - Array to append change descriptions to
|
|
181
|
+
* @returns {boolean} True if testPayload was added or updated
|
|
182
|
+
*/
|
|
183
|
+
function repairTestPayload(parsed, changes) {
|
|
184
|
+
const attrs = parsed?.fieldMappings?.attributes;
|
|
185
|
+
if (!attrs || typeof attrs !== 'object') return false;
|
|
186
|
+
const payloadTemplate = {};
|
|
187
|
+
const expectedResult = {};
|
|
188
|
+
for (const [key, config] of Object.entries(attrs)) {
|
|
189
|
+
const type = config?.type || 'string';
|
|
190
|
+
const placeholder = placeholderForType(type);
|
|
191
|
+
expectedResult[key] = placeholder;
|
|
192
|
+
const match = config?.expression?.match(/\{\{\s*([^}|]+)/);
|
|
193
|
+
if (match) {
|
|
194
|
+
const path = match[1].trim();
|
|
195
|
+
setNested(payloadTemplate, path.split('.'), placeholder);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (!parsed.testPayload) parsed.testPayload = {};
|
|
199
|
+
parsed.testPayload.payloadTemplate = payloadTemplate;
|
|
200
|
+
parsed.testPayload.expectedResult = expectedResult;
|
|
201
|
+
changes.push('Generated testPayload.payloadTemplate and testPayload.expectedResult from attributes');
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Runs all requested datasource repairs. Core: dimensions + metadataSchema. Optional: expose, sync, testPayload.
|
|
207
|
+
*
|
|
208
|
+
* @param {Object} parsed - Parsed datasource object (mutated)
|
|
209
|
+
* @param {Object} options - { expose?: boolean, sync?: boolean, test?: boolean }
|
|
210
|
+
* @param {string[]} [changes] - Optional array to append change descriptions to
|
|
211
|
+
* @returns {{ updated: boolean, changes: string[] }}
|
|
212
|
+
*/
|
|
213
|
+
function repairDatasourceFile(parsed, options = {}, changes = []) {
|
|
214
|
+
const out = Array.isArray(changes) ? changes : [];
|
|
215
|
+
let updated = false;
|
|
216
|
+
updated = repairDimensionsFromAttributes(parsed, out) || updated;
|
|
217
|
+
updated = repairMetadataSchemaFromAttributes(parsed, out) || updated;
|
|
218
|
+
if (options.expose) updated = repairExposeFromAttributes(parsed, out) || updated;
|
|
219
|
+
if (options.sync) updated = repairSyncSection(parsed, out) || updated;
|
|
220
|
+
if (options.test) updated = repairTestPayload(parsed, out) || updated;
|
|
221
|
+
return { updated, changes: out };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
module.exports = {
|
|
225
|
+
getAttributeKeys,
|
|
226
|
+
parsePathsFromExpressions,
|
|
227
|
+
repairDimensionsFromAttributes,
|
|
228
|
+
repairMetadataSchemaFromAttributes,
|
|
229
|
+
repairExposeFromAttributes,
|
|
230
|
+
repairSyncSection,
|
|
231
|
+
repairTestPayload,
|
|
232
|
+
repairDatasourceFile,
|
|
233
|
+
DEFAULT_SYNC,
|
|
234
|
+
MINIMAL_METADATA_SCHEMA
|
|
235
|
+
};
|