@aifabrix/builder 2.40.2 → 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 +7 -5
- 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/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +2 -1
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- 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/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- 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 +44 -11
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +12 -1
- package/lib/app/prompts.js +44 -29
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +9 -6
- package/lib/app/run-env-compose.js +264 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show-display.js +1 -1
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +172 -15
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +206 -16
- package/lib/cli/setup-environment.js +16 -6
- package/lib/cli/setup-external-system.js +89 -24
- package/lib/cli/setup-infra.js +82 -15
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +129 -24
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- 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-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +347 -0
- 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/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +96 -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/admin-secrets.js +96 -0
- package/lib/core/config.js +7 -1
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +176 -89
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/deployer.js +7 -5
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +188 -203
- package/lib/external-system/generator.js +204 -56
- package/lib/external-system/test-auth.js +7 -3
- 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 +56 -19
- 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 +177 -25
- 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 +155 -158
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +98 -12
- package/lib/infrastructure/services.js +88 -22
- package/lib/schema/application-schema.json +32 -8
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +509 -411
- package/lib/schema/wizard-config.schema.json +16 -0
- package/lib/utils/api.js +41 -13
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +77 -76
- package/lib/utils/compose-handlebars-helpers.js +54 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +357 -0
- 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/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +103 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -2
- 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 +16 -2
- package/lib/utils/infra-status.js +80 -45
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +128 -37
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +114 -6
- package/lib/utils/secrets-helpers.js +108 -114
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +29 -36
- package/lib/utils/variable-transformer.js +3 -3
- 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 +72 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +8 -3
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +6 -5
- package/templates/applications/dataplane/env.template +15 -10
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +12 -10
- 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 +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
- package/integration/hubspot/application.yaml +0 -37
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Datasource E2E test - run full E2E test via dataplane external API
|
|
3
|
+
* @fileoverview Datasource E2E test logic (config, credential, sync, data, CIP)
|
|
4
|
+
* @author AI Fabrix Team
|
|
5
|
+
* @version 2.0.0
|
|
6
|
+
*/
|
|
7
|
+
/* eslint-disable max-statements -- Auth setup, API call, polling, debug log */
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const fs = require('fs').promises;
|
|
11
|
+
const chalk = require('chalk');
|
|
12
|
+
const logger = require('../utils/logger');
|
|
13
|
+
const { getIntegrationPath, resolveIntegrationAppKeyFromCwd } = require('../utils/paths');
|
|
14
|
+
const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
|
|
15
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
16
|
+
const { getDeviceOnlyAuth } = require('../utils/token-manager');
|
|
17
|
+
const { testDatasourceE2E, getE2ETestRun } = require('../api/external-test.api');
|
|
18
|
+
const { writeTestLog } = require('../utils/test-log-writer');
|
|
19
|
+
|
|
20
|
+
const DEFAULT_POLL_INTERVAL_MS = 2500;
|
|
21
|
+
const DEFAULT_POLL_TIMEOUT_MS = 15 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve appKey for datasource test-e2e
|
|
25
|
+
* @param {string} [appKey] - Explicit app key from --app
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function resolveAppKey(appKey) {
|
|
29
|
+
if (appKey) return appKey;
|
|
30
|
+
const fromCwd = resolveIntegrationAppKeyFromCwd();
|
|
31
|
+
if (fromCwd) return fromCwd;
|
|
32
|
+
throw new Error(
|
|
33
|
+
'Could not determine app context. Use --app <appKey> or run from integration/<appKey>/ directory.'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Resolve primaryKeyValue for request body: string as-is, or read and parse JSON from @path
|
|
39
|
+
* @param {string} [value] - Literal value or path prefixed with @ (e.g. @pk.json)
|
|
40
|
+
* @returns {Promise<string|Object|null>} Resolved value for body.primaryKeyValue, or null if absent
|
|
41
|
+
*/
|
|
42
|
+
async function resolvePrimaryKeyValue(value) {
|
|
43
|
+
if (value === null || value === undefined || value === '') return null;
|
|
44
|
+
const str = String(value).trim();
|
|
45
|
+
if (str.startsWith('@')) {
|
|
46
|
+
const filePath = path.resolve(str.slice(1).trim());
|
|
47
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
48
|
+
return JSON.parse(content);
|
|
49
|
+
}
|
|
50
|
+
return str;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Build E2E request body from options
|
|
55
|
+
* @param {Object} options - Command options
|
|
56
|
+
* @returns {Promise<Object>} Request body
|
|
57
|
+
*/
|
|
58
|
+
async function buildE2EBody(options) {
|
|
59
|
+
const body = {};
|
|
60
|
+
if (options.debug) body.includeDebug = true;
|
|
61
|
+
if (options.testCrud === true) body.testCrud = true;
|
|
62
|
+
if (options.recordId !== undefined && options.recordId !== null && options.recordId !== '') body.recordId = String(options.recordId);
|
|
63
|
+
if (options.cleanup === false) body.cleanup = false;
|
|
64
|
+
else if (options.cleanup === true) body.cleanup = true;
|
|
65
|
+
const pk = await resolvePrimaryKeyValue(options.primaryKeyValue);
|
|
66
|
+
if (pk !== null && pk !== undefined) body.primaryKeyValue = pk;
|
|
67
|
+
return body;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Poll E2E test run until completed or failed
|
|
72
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
73
|
+
* @param {string} sourceIdOrKey - Source ID or key
|
|
74
|
+
* @param {string} testRunId - Test run ID
|
|
75
|
+
* @param {Object} authConfig - Auth config
|
|
76
|
+
* @param {Object} opts - Poll options
|
|
77
|
+
* @param {number} [opts.intervalMs] - Poll interval (ms)
|
|
78
|
+
* @param {number} [opts.timeoutMs] - Max wait (ms)
|
|
79
|
+
* @param {boolean} [opts.verbose] - Log each poll
|
|
80
|
+
* @returns {Promise<Object>} Final poll result (status completed or failed)
|
|
81
|
+
*/
|
|
82
|
+
async function pollE2ETestRun(dataplaneUrl, sourceIdOrKey, testRunId, authConfig, opts = {}) {
|
|
83
|
+
const intervalMs = opts.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
84
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
|
|
85
|
+
const verbose = opts.verbose === true;
|
|
86
|
+
const deadline = Date.now() + timeoutMs;
|
|
87
|
+
let last;
|
|
88
|
+
while (Date.now() < deadline) {
|
|
89
|
+
last = await getE2ETestRun(dataplaneUrl, sourceIdOrKey, testRunId, authConfig);
|
|
90
|
+
if (last.status === 'completed' || last.status === 'failed') {
|
|
91
|
+
return last;
|
|
92
|
+
}
|
|
93
|
+
if (verbose) {
|
|
94
|
+
const steps = last.completedActions || [];
|
|
95
|
+
logger.log(chalk.gray(` Polling… status: ${last.status}, ${steps.length} step(s) completed`));
|
|
96
|
+
}
|
|
97
|
+
await new Promise(r => setTimeout(r, intervalMs));
|
|
98
|
+
}
|
|
99
|
+
throw new Error(
|
|
100
|
+
`E2E test run did not complete within ${timeoutMs / 1000}s (run ID: ${testRunId})`
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Run E2E test for one datasource (Bearer token or API key required; no client credentials).
|
|
106
|
+
* Default: async start + polling until completed/failed. Use options.async === false for sync.
|
|
107
|
+
*
|
|
108
|
+
* @async
|
|
109
|
+
* @param {string} datasourceKey - Datasource key (used as sourceIdOrKey)
|
|
110
|
+
* @param {Object} options - Options
|
|
111
|
+
* @param {string} [options.app] - App key (or resolve from cwd)
|
|
112
|
+
* @param {string} [options.environment] - Environment (dev, tst, pro)
|
|
113
|
+
* @param {boolean} [options.debug] - Include debug, write log file
|
|
114
|
+
* @param {boolean} [options.verbose] - Verbose output (e.g. poll progress)
|
|
115
|
+
* @param {boolean} [options.async] - If false, use sync mode (no polling). Default true.
|
|
116
|
+
* @param {boolean} [options.testCrud] - Set body testCrud true
|
|
117
|
+
* @param {string} [options.recordId] - Set body recordId
|
|
118
|
+
* @param {boolean} [options.cleanup] - Set body cleanup (default true)
|
|
119
|
+
* @param {string} [options.primaryKeyValue] - Set body primaryKeyValue (string or @path to JSON)
|
|
120
|
+
* @param {number} [options.pollIntervalMs] - Poll interval in ms (default 2500)
|
|
121
|
+
* @param {number} [options.pollTimeoutMs] - Poll timeout in ms (default 15 min)
|
|
122
|
+
* @returns {Promise<Object>} E2E test result (steps, success, error, etc.)
|
|
123
|
+
*/
|
|
124
|
+
async function runDatasourceTestE2E(datasourceKey, options = {}) {
|
|
125
|
+
if (!datasourceKey || typeof datasourceKey !== 'string') {
|
|
126
|
+
throw new Error('Datasource key is required');
|
|
127
|
+
}
|
|
128
|
+
const appKey = resolveAppKey(options.app);
|
|
129
|
+
const controllerUrl = await resolveControllerUrl();
|
|
130
|
+
const { resolveEnvironment } = require('../core/config');
|
|
131
|
+
const environment = options.environment || await resolveEnvironment();
|
|
132
|
+
const authConfig = await getDeviceOnlyAuth(controllerUrl);
|
|
133
|
+
const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
|
|
134
|
+
|
|
135
|
+
logger.log(chalk.blue(`\n🧪 Running E2E test for datasource: ${datasourceKey}`));
|
|
136
|
+
|
|
137
|
+
const body = await buildE2EBody(options);
|
|
138
|
+
const useAsync = options.async !== false;
|
|
139
|
+
const requestMeta = {
|
|
140
|
+
sourceIdOrKey: datasourceKey,
|
|
141
|
+
includeDebug: options.debug,
|
|
142
|
+
testCrud: options.testCrud,
|
|
143
|
+
recordId: options.recordId,
|
|
144
|
+
cleanup: options.cleanup,
|
|
145
|
+
primaryKeyValue: options.primaryKeyValue !== undefined && options.primaryKeyValue !== null
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const execOpts = {
|
|
149
|
+
dataplaneUrl,
|
|
150
|
+
datasourceKey,
|
|
151
|
+
authConfig,
|
|
152
|
+
body,
|
|
153
|
+
useAsync,
|
|
154
|
+
verbose: options.verbose,
|
|
155
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
156
|
+
pollTimeoutMs: options.pollTimeoutMs
|
|
157
|
+
};
|
|
158
|
+
let data;
|
|
159
|
+
try {
|
|
160
|
+
data = await executeE2EWithOptionalPoll(execOpts);
|
|
161
|
+
} catch (error) {
|
|
162
|
+
if (options.debug) {
|
|
163
|
+
const appPath = getIntegrationPath(appKey);
|
|
164
|
+
const integrationDir = path.dirname(appPath);
|
|
165
|
+
await writeTestLog(appKey, { request: requestMeta, error: error.message }, 'test-e2e', integrationDir);
|
|
166
|
+
}
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (options.debug) {
|
|
171
|
+
const appPath = getIntegrationPath(appKey);
|
|
172
|
+
const integrationDir = path.dirname(appPath);
|
|
173
|
+
const logPath = await writeTestLog(appKey, { request: requestMeta, response: data }, 'test-e2e', integrationDir);
|
|
174
|
+
logger.log(chalk.gray(` Debug log: ${logPath}`));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return data;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Call E2E API and optionally poll until completed. On throw, caller should log if debug.
|
|
182
|
+
* @param {Object} opts - Options
|
|
183
|
+
* @param {string} opts.dataplaneUrl - Dataplane URL
|
|
184
|
+
* @param {string} opts.datasourceKey - Source key
|
|
185
|
+
* @param {Object} opts.authConfig - Auth config
|
|
186
|
+
* @param {Object} opts.body - Request body
|
|
187
|
+
* @param {boolean} opts.useAsync - Whether to use async + poll
|
|
188
|
+
* @param {boolean} opts.verbose - Verbose poll progress
|
|
189
|
+
* @param {number} [opts.pollIntervalMs] - Override poll interval (ms)
|
|
190
|
+
* @param {number} [opts.pollTimeoutMs] - Override poll timeout (ms)
|
|
191
|
+
* @returns {Promise<Object>} Final result data
|
|
192
|
+
*/
|
|
193
|
+
/* eslint-disable-next-line max-params -- single opts object; destructuring in body */
|
|
194
|
+
async function executeE2EWithOptionalPoll(opts) {
|
|
195
|
+
const { dataplaneUrl, datasourceKey, authConfig, body, useAsync, verbose, pollIntervalMs, pollTimeoutMs } = opts;
|
|
196
|
+
const response = await testDatasourceE2E(dataplaneUrl, datasourceKey, authConfig, body, {
|
|
197
|
+
asyncRun: useAsync
|
|
198
|
+
});
|
|
199
|
+
let data = response.data || response;
|
|
200
|
+
if (useAsync && data !== null && data !== undefined && data.testRunId) {
|
|
201
|
+
data = await pollE2ETestRun(
|
|
202
|
+
dataplaneUrl,
|
|
203
|
+
datasourceKey,
|
|
204
|
+
data.testRunId,
|
|
205
|
+
authConfig,
|
|
206
|
+
{
|
|
207
|
+
intervalMs: pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
|
|
208
|
+
timeoutMs: pollTimeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
|
|
209
|
+
verbose
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
return data;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
runDatasourceTestE2E,
|
|
218
|
+
resolveAppKey
|
|
219
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Datasource integration test - run config test for one datasource via pipeline
|
|
3
|
+
* @fileoverview Datasource integration test logic
|
|
4
|
+
* @author AI Fabrix Team
|
|
5
|
+
* @version 2.0.0
|
|
6
|
+
*/
|
|
7
|
+
/* eslint-disable max-lines-per-function,max-statements,complexity -- Load config, resolve datasource, call pipeline test */
|
|
8
|
+
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
const { getIntegrationPath, resolveIntegrationAppKeyFromCwd } = require('../utils/paths');
|
|
13
|
+
const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
|
|
14
|
+
const { loadConfigFile } = require('../utils/config-format');
|
|
15
|
+
const { setupIntegrationTestAuth } = require('../external-system/test-auth');
|
|
16
|
+
const { getConfig } = require('../core/config');
|
|
17
|
+
const { testDatasourceViaPipeline } = require('../api/pipeline.api');
|
|
18
|
+
const { writeTestLog } = require('../utils/test-log-writer');
|
|
19
|
+
const testHelpers = require('../utils/external-system-test-helpers');
|
|
20
|
+
const fs = require('fs').promises;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Resolve systemKey and appKey for datasource test-integration
|
|
24
|
+
* @param {string} [appKey] - Explicit app key from --app
|
|
25
|
+
* @returns {Promise<{appKey: string, systemKey: string}>}
|
|
26
|
+
*/
|
|
27
|
+
async function resolveSystemKey(appKey) {
|
|
28
|
+
let resolvedAppKey = appKey;
|
|
29
|
+
if (!resolvedAppKey) {
|
|
30
|
+
resolvedAppKey = resolveIntegrationAppKeyFromCwd();
|
|
31
|
+
}
|
|
32
|
+
if (!resolvedAppKey) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'Could not determine app context. Use --app <appKey> or run from integration/<appKey>/ directory.'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const appPath = getIntegrationPath(resolvedAppKey);
|
|
38
|
+
const configPath = resolveApplicationConfigPath(appPath);
|
|
39
|
+
const config = loadConfigFile(configPath);
|
|
40
|
+
if (!config.externalIntegration || !config.externalIntegration.systems || config.externalIntegration.systems.length === 0) {
|
|
41
|
+
throw new Error(`No externalIntegration.systems found in ${configPath}`);
|
|
42
|
+
}
|
|
43
|
+
const systemFile = config.externalIntegration.systems[0];
|
|
44
|
+
const systemPath = path.isAbsolute(systemFile)
|
|
45
|
+
? systemFile
|
|
46
|
+
: path.join(appPath, systemFile);
|
|
47
|
+
const systemContent = await fs.readFile(systemPath, 'utf8');
|
|
48
|
+
const yaml = require('js-yaml');
|
|
49
|
+
const systemConfig = yaml.load(systemContent);
|
|
50
|
+
const systemKey = systemConfig?.key || path.basename(systemFile, '-system.yaml').replace('-system', '');
|
|
51
|
+
return { appKey: resolvedAppKey, systemKey };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run integration test for one datasource
|
|
56
|
+
* @async
|
|
57
|
+
* @param {string} datasourceKey - Datasource key
|
|
58
|
+
* @param {Object} options - Options
|
|
59
|
+
* @param {string} [options.app] - App key (or resolve from cwd)
|
|
60
|
+
* @param {string} [options.payload] - Path to custom payload file
|
|
61
|
+
* @param {string} [options.environment] - Environment (dev, tst, pro)
|
|
62
|
+
* @param {boolean} [options.debug] - Include debug, write log file
|
|
63
|
+
* @param {number} [options.timeout] - Request timeout ms
|
|
64
|
+
* @returns {Promise<Object>} Test result
|
|
65
|
+
*/
|
|
66
|
+
async function runDatasourceTestIntegration(datasourceKey, options = {}) {
|
|
67
|
+
if (!datasourceKey || typeof datasourceKey !== 'string') {
|
|
68
|
+
throw new Error('Datasource key is required');
|
|
69
|
+
}
|
|
70
|
+
const { appKey, systemKey } = await resolveSystemKey(options.app);
|
|
71
|
+
const appPath = getIntegrationPath(appKey);
|
|
72
|
+
const config = loadConfigFile(resolveApplicationConfigPath(appPath));
|
|
73
|
+
const schemaBasePath = config.externalIntegration?.schemaBasePath || './';
|
|
74
|
+
const datasourceFiles = config.externalIntegration?.dataSources || [];
|
|
75
|
+
const datasourceFile = datasourceFiles.find(f => {
|
|
76
|
+
const base = path.basename(f, path.extname(f));
|
|
77
|
+
return base === datasourceKey || base.includes(datasourceKey);
|
|
78
|
+
});
|
|
79
|
+
if (!datasourceFile) {
|
|
80
|
+
throw new Error(`Datasource '${datasourceKey}' not found in application config`);
|
|
81
|
+
}
|
|
82
|
+
const datasourcePath = path.isAbsolute(schemaBasePath)
|
|
83
|
+
? path.join(schemaBasePath, datasourceFile)
|
|
84
|
+
: path.join(appPath, schemaBasePath, datasourceFile);
|
|
85
|
+
const datasourceContent = await fs.readFile(datasourcePath, 'utf8');
|
|
86
|
+
const datasource = JSON.parse(datasourceContent);
|
|
87
|
+
if (datasource.key !== datasourceKey) {
|
|
88
|
+
throw new Error(`Datasource key mismatch: file has '${datasource.key}', expected '${datasourceKey}'`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const configObj = await getConfig();
|
|
92
|
+
const { authConfig, dataplaneUrl } = await setupIntegrationTestAuth(appKey, options, configObj);
|
|
93
|
+
const customPayload = await testHelpers.loadCustomPayload(options.payload);
|
|
94
|
+
const payloadTemplate = testHelpers.determinePayloadTemplate(datasource, datasourceKey, customPayload);
|
|
95
|
+
if (!payloadTemplate) {
|
|
96
|
+
throw new Error(`No test payload found for datasource '${datasourceKey}'`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
logger.log(chalk.blue(`\n📡 Testing datasource: ${datasourceKey} (system: ${systemKey})`));
|
|
100
|
+
|
|
101
|
+
const testData = { payloadTemplate };
|
|
102
|
+
if (options.debug) {
|
|
103
|
+
testData.includeDebug = true;
|
|
104
|
+
}
|
|
105
|
+
const timeout = parseInt(options.timeout, 10) || 30000;
|
|
106
|
+
|
|
107
|
+
let response;
|
|
108
|
+
try {
|
|
109
|
+
response = await testDatasourceViaPipeline({
|
|
110
|
+
dataplaneUrl,
|
|
111
|
+
systemKey,
|
|
112
|
+
datasourceKey,
|
|
113
|
+
authConfig,
|
|
114
|
+
testData,
|
|
115
|
+
options: { timeout }
|
|
116
|
+
});
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const result = { key: datasourceKey, success: false, error: error.message };
|
|
119
|
+
if (options.debug) {
|
|
120
|
+
await writeTestLog(appKey, { request: { systemKey, datasourceKey }, error: error.message }, 'test-integration');
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const data = response.data || response;
|
|
126
|
+
const success = data.success !== false;
|
|
127
|
+
const result = {
|
|
128
|
+
key: datasourceKey,
|
|
129
|
+
systemKey,
|
|
130
|
+
success,
|
|
131
|
+
skipped: false,
|
|
132
|
+
validationResults: data.validationResults || {},
|
|
133
|
+
fieldMappingResults: data.fieldMappingResults || {},
|
|
134
|
+
endpointTestResults: data.endpointTestResults || {}
|
|
135
|
+
};
|
|
136
|
+
if (data.error) {
|
|
137
|
+
result.error = data.error;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (options.debug) {
|
|
141
|
+
const logPath = await writeTestLog(appKey, {
|
|
142
|
+
request: { systemKey, datasourceKey, includeDebug: true },
|
|
143
|
+
response: data
|
|
144
|
+
}, 'test-integration');
|
|
145
|
+
logger.log(chalk.gray(` Debug log: ${logPath}`));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
runDatasourceTestIntegration,
|
|
153
|
+
resolveSystemKey
|
|
154
|
+
};
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
const fs = require('fs');
|
|
12
12
|
const { loadExternalDataSourceSchema } = require('../utils/schema-loader');
|
|
13
13
|
const { formatValidationErrors } = require('../utils/error-formatter');
|
|
14
|
+
const { validateFieldReferences } = require('./field-reference-validator');
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Validates a datasource file against external-datasource schema
|
|
@@ -48,11 +49,28 @@ async function validateDatasourceFile(filePath) {
|
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
const validate = loadExternalDataSourceSchema();
|
|
51
|
-
const
|
|
52
|
+
const schemaValid = validate(parsed);
|
|
53
|
+
|
|
54
|
+
if (!schemaValid) {
|
|
55
|
+
return {
|
|
56
|
+
valid: false,
|
|
57
|
+
errors: formatValidationErrors(validate.errors),
|
|
58
|
+
warnings: []
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fieldRefErrors = validateFieldReferences(parsed);
|
|
63
|
+
if (fieldRefErrors.length > 0) {
|
|
64
|
+
return {
|
|
65
|
+
valid: false,
|
|
66
|
+
errors: fieldRefErrors,
|
|
67
|
+
warnings: []
|
|
68
|
+
};
|
|
69
|
+
}
|
|
52
70
|
|
|
53
71
|
return {
|
|
54
|
-
valid,
|
|
55
|
-
errors:
|
|
72
|
+
valid: true,
|
|
73
|
+
errors: [],
|
|
56
74
|
warnings: []
|
|
57
75
|
};
|
|
58
76
|
}
|
|
@@ -193,14 +193,16 @@ function buildDeploymentData(validateToken, authConfig, options) {
|
|
|
193
193
|
* @throws {Error} If deployment failed
|
|
194
194
|
*/
|
|
195
195
|
function handleDeploymentResponse(response) {
|
|
196
|
-
|
|
196
|
+
const bodyFailed = response.data?.success === false;
|
|
197
|
+
if (response.success && !bodyFailed) {
|
|
197
198
|
return response.data.data || response.data;
|
|
198
199
|
}
|
|
199
200
|
|
|
200
|
-
// Handle deployment errors
|
|
201
|
-
if (response.status >= 400) {
|
|
202
|
-
const
|
|
203
|
-
error
|
|
201
|
+
// Handle deployment errors (HTTP error or 200 with success: false in body)
|
|
202
|
+
if (!response.success || bodyFailed || response.status >= 400) {
|
|
203
|
+
const msg = response.formattedError || response.error || response.data?.formattedError || response.data?.error || 'Unknown error';
|
|
204
|
+
const error = new Error(`Deployment request failed: ${msg}`);
|
|
205
|
+
error.status = response.status || (bodyFailed ? 400 : undefined);
|
|
204
206
|
error.data = response.data;
|
|
205
207
|
throw error;
|
|
206
208
|
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment deployment config and preset helpers.
|
|
3
|
+
* Parses/validates JSON config and builds preset-based config.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Environment config for AI Fabrix Builder
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const Ajv = require('ajv');
|
|
13
|
+
const { formatValidationErrors } = require('../utils/error-formatter');
|
|
14
|
+
const environmentDeployRequestSchema = require('../schema/environment-deploy-request.schema.json');
|
|
15
|
+
|
|
16
|
+
/** Allowed preset values for --preset (case-insensitive); API expects lowercase s, m, l, xl */
|
|
17
|
+
const PRESET_VALUES = ['s', 'm', 'l', 'xl'];
|
|
18
|
+
const DEFAULT_PRESET = 's';
|
|
19
|
+
const DEFAULT_SERVICE_NAME = 'aifabrix';
|
|
20
|
+
const DEFAULT_LOCATION = 'swedencentral';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Normalizes and validates preset from CLI (s, m, l, xl; case-insensitive)
|
|
24
|
+
* @param {string} [preset] - User-provided preset
|
|
25
|
+
* @returns {string} Normalized preset (lowercase)
|
|
26
|
+
* @throws {Error} If preset is not one of s, m, l, xl
|
|
27
|
+
*/
|
|
28
|
+
function normalizePreset(preset) {
|
|
29
|
+
const raw = (preset === null || preset === undefined || preset === '') ? DEFAULT_PRESET : String(preset).trim().toLowerCase();
|
|
30
|
+
if (!PRESET_VALUES.includes(raw)) {
|
|
31
|
+
throw new Error(
|
|
32
|
+
`Invalid preset "${preset}". Use one of: ${PRESET_VALUES.join(', ')}.\n` +
|
|
33
|
+
'Example: aifabrix env deploy dev --preset s'
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return raw;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Builds environmentConfig from env key and preset (no config file)
|
|
41
|
+
* @param {string} validatedEnvKey - Validated environment key
|
|
42
|
+
* @param {string} preset - Normalized preset (s, m, l, xl)
|
|
43
|
+
* @returns {Object} { environmentConfig, dryRun: false }
|
|
44
|
+
*/
|
|
45
|
+
function buildEnvironmentConfigFromPreset(validatedEnvKey, preset) {
|
|
46
|
+
return {
|
|
47
|
+
environmentConfig: {
|
|
48
|
+
key: validatedEnvKey,
|
|
49
|
+
environment: validatedEnvKey,
|
|
50
|
+
preset,
|
|
51
|
+
serviceName: DEFAULT_SERVICE_NAME,
|
|
52
|
+
location: DEFAULT_LOCATION
|
|
53
|
+
},
|
|
54
|
+
dryRun: false
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Reads and parses config file; throws if missing, unreadable, or invalid structure. */
|
|
59
|
+
function parseEnvironmentConfigFile(resolvedPath) {
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = fs.readFileSync(resolvedPath, 'utf8');
|
|
63
|
+
} catch (e) {
|
|
64
|
+
throw new Error(`Cannot read config file: ${resolvedPath}. ${e.message}`);
|
|
65
|
+
}
|
|
66
|
+
let parsed;
|
|
67
|
+
try {
|
|
68
|
+
parsed = JSON.parse(raw);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Invalid JSON in config file: ${resolvedPath}\n${e.message}\n` +
|
|
72
|
+
'Expected format: { "environmentConfig": { "key", "environment", "preset", "serviceName", "location" }, "dryRun": false }'
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
if (parsed === null || typeof parsed !== 'object') {
|
|
76
|
+
throw new Error(
|
|
77
|
+
`Config file must be a JSON object with "environmentConfig". File: ${resolvedPath}\n` +
|
|
78
|
+
'Example: { "environmentConfig": { "key": "dev", "environment": "dev", "preset": "s", "serviceName": "aifabrix", "location": "swedencentral" }, "dryRun": false }'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (parsed.environmentConfig === undefined) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Config file must contain "environmentConfig" (object). File: ${resolvedPath}\n` +
|
|
84
|
+
'Example: { "environmentConfig": { "key": "dev", "environment": "dev", "preset": "s", "serviceName": "aifabrix", "location": "swedencentral" } }'
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (typeof parsed.environmentConfig !== 'object' || parsed.environmentConfig === null) {
|
|
88
|
+
throw new Error(`"environmentConfig" must be an object. File: ${resolvedPath}`);
|
|
89
|
+
}
|
|
90
|
+
return parsed;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Validates parsed config against schema and returns deploy request.
|
|
95
|
+
* @param {Object} parsed - Parsed config object
|
|
96
|
+
* @param {string} resolvedPath - Path for error messages
|
|
97
|
+
* @returns {Object} { environmentConfig, dryRun? }
|
|
98
|
+
*/
|
|
99
|
+
function validateEnvironmentDeployParsed(parsed, resolvedPath) {
|
|
100
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
101
|
+
const validate = ajv.compile(environmentDeployRequestSchema);
|
|
102
|
+
if (!validate(parsed)) {
|
|
103
|
+
const messages = formatValidationErrors(validate.errors);
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Environment config validation failed (${resolvedPath}):\n • ${messages.join('\n • ')}\n` +
|
|
106
|
+
'Fix the config file and run the command again. See templates/infra/environment-dev.json for a valid example.'
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
environmentConfig: parsed.environmentConfig,
|
|
111
|
+
dryRun: Boolean(parsed.dryRun)
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Loads and validates environment deploy config from a JSON file
|
|
117
|
+
* @param {string} configPath - Absolute or relative path to config JSON
|
|
118
|
+
* @returns {Object} Valid deploy request { environmentConfig, dryRun? }
|
|
119
|
+
* @throws {Error} If file missing, invalid JSON, or validation fails
|
|
120
|
+
*/
|
|
121
|
+
function loadAndValidateEnvironmentDeployConfig(configPath) {
|
|
122
|
+
const resolvedPath = path.isAbsolute(configPath) ? configPath : path.resolve(process.cwd(), configPath);
|
|
123
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`Environment config file not found: ${resolvedPath}\n` +
|
|
126
|
+
'Use --config <file> with a JSON file containing "environmentConfig" (e.g. templates/infra/environment-dev.json).'
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
const parsed = parseEnvironmentConfigFile(resolvedPath);
|
|
130
|
+
return validateEnvironmentDeployParsed(parsed, resolvedPath);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
normalizePreset,
|
|
135
|
+
buildEnvironmentConfigFromPreset,
|
|
136
|
+
loadAndValidateEnvironmentDeployConfig
|
|
137
|
+
};
|