@aifabrix/builder 2.44.5 → 2.45.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/cli-layout.mdc +8 -4
- package/.cursor/rules/project-rules.mdc +1 -1
- package/README.md +15 -23
- package/integration/hubspot-test/README.md +2 -0
- package/integration/hubspot-test/test.js +5 -3
- package/jest.projects.js +104 -2
- package/lib/api/controller-health.api.js +49 -0
- package/lib/api/dimension-values.api.js +82 -0
- package/lib/api/dimensions.api.js +114 -0
- package/lib/api/external-systems.api.js +1 -0
- package/lib/api/integration-clients.api.js +168 -0
- package/lib/api/types/dimension-values.types.js +28 -0
- package/lib/api/types/dimensions.types.js +31 -0
- package/lib/api/types/integration-clients.types.js +45 -0
- package/lib/api/validation-runner.js +46 -25
- package/lib/app/deploy-config.js +11 -1
- package/lib/app/deploy-status-display.js +3 -3
- package/lib/app/deploy.js +36 -14
- package/lib/app/display.js +15 -11
- package/lib/app/helpers.js +3 -3
- package/lib/app/index.js +3 -3
- package/lib/app/push.js +46 -23
- package/lib/app/register.js +7 -6
- package/lib/app/restart-display.js +126 -0
- package/lib/app/rotate-secret.js +7 -6
- package/lib/app/run-container-start.js +12 -6
- package/lib/app/run-env-compose.js +30 -1
- package/lib/app/run-helpers.js +58 -19
- package/lib/app/run-reload-sync.js +148 -0
- package/lib/app/run-resolve-image.js +51 -1
- package/lib/app/run.js +148 -74
- package/lib/app/show-display.js +7 -0
- package/lib/app/show.js +87 -5
- package/lib/build/index.js +83 -49
- package/lib/cli/doctor-check.js +117 -0
- package/lib/cli/index.js +8 -2
- package/lib/cli/infra-guided.js +460 -0
- package/lib/cli/installation-log-command.js +73 -0
- package/lib/cli/setup-app.js +31 -3
- package/lib/cli/setup-auth.js +98 -27
- package/lib/cli/setup-dev-path-commands.js +50 -3
- package/lib/cli/setup-infra-up-dataplane-action.js +111 -0
- package/lib/cli/setup-infra-up-platform-action.js +131 -0
- package/lib/cli/setup-infra.js +132 -118
- package/lib/cli/setup-integration-client.js +182 -0
- package/lib/cli/setup-parameters.js +21 -2
- package/lib/cli/setup-platform.js +102 -0
- package/lib/cli/setup-secrets.js +18 -6
- package/lib/cli/setup-utility-resolve.js +132 -0
- package/lib/cli/setup-utility.js +143 -84
- package/lib/commands/app-logs.js +81 -33
- package/lib/commands/auth-config.js +116 -18
- package/lib/commands/datasource-capability-dimension-cli.js +128 -0
- package/lib/commands/datasource-capability-output.js +29 -0
- package/lib/commands/datasource-capability-relate-cli.js +140 -0
- package/lib/commands/datasource-capability.js +411 -0
- package/lib/commands/datasource-unified-test-cli.options.js +1 -1
- package/lib/commands/datasource.js +53 -13
- package/lib/commands/dev-down.js +3 -3
- package/lib/commands/dev-infra-gate.js +32 -0
- package/lib/commands/dev-init.js +13 -7
- package/lib/commands/dimension-value.js +179 -0
- package/lib/commands/dimension.js +330 -0
- package/lib/commands/integration-client.js +430 -0
- package/lib/commands/login-device.js +65 -30
- package/lib/commands/login.js +21 -10
- package/lib/commands/parameters-validate.js +78 -13
- package/lib/commands/repair-datasource-auto-rbac.js +166 -0
- package/lib/commands/repair-datasource-keys.js +10 -5
- package/lib/commands/repair-datasource.js +19 -7
- package/lib/commands/repair-env-template.js +4 -1
- package/lib/commands/repair-openapi-sync.js +172 -0
- package/lib/commands/repair-persist.js +102 -0
- package/lib/commands/repair-rbac-extract.js +27 -0
- package/lib/commands/repair-rbac-migrate.js +186 -0
- package/lib/commands/repair-rbac.js +214 -31
- package/lib/commands/repair-system-alignment.js +246 -0
- package/lib/commands/repair-system-permissions.js +168 -0
- package/lib/commands/repair.js +120 -338
- package/lib/commands/secure.js +1 -1
- package/lib/commands/setup-modes.js +468 -0
- package/lib/commands/setup-prompts.js +421 -0
- package/lib/commands/setup.js +254 -0
- package/lib/commands/teardown.js +277 -0
- package/lib/commands/up-common.js +113 -19
- package/lib/commands/up-dataplane.js +44 -19
- package/lib/commands/up-miso.js +18 -18
- package/lib/commands/upload.js +111 -23
- package/lib/commands/wizard-core-helpers.js +14 -11
- package/lib/commands/wizard-core.js +6 -5
- package/lib/commands/wizard-dataplane.js +2 -2
- package/lib/commands/wizard-entity-selection.js +4 -3
- package/lib/commands/wizard-headless.js +2 -1
- package/lib/commands/wizard.js +2 -1
- package/lib/constants/infra-compose-service-names.js +40 -0
- package/lib/core/audit-logger.js +1 -34
- package/lib/core/config-admin-email.js +56 -0
- package/lib/core/config-normalize.js +60 -0
- package/lib/core/config-registered-controller-urls.js +54 -0
- package/lib/core/config.js +33 -50
- package/lib/core/env-reader.js +16 -3
- package/lib/core/secrets-admin-env.js +101 -0
- package/lib/core/secrets-ensure-infra.js +34 -1
- package/lib/core/secrets-ensure.js +88 -66
- package/lib/core/secrets-env-content.js +428 -0
- package/lib/core/secrets-env-declarative-expand.js +170 -0
- package/lib/core/secrets-env-write.js +29 -1
- package/lib/core/secrets-load.js +252 -0
- package/lib/core/secrets-names.js +32 -0
- package/lib/core/secrets.js +17 -757
- package/lib/datasource/capability/basic-exposure.js +76 -0
- package/lib/datasource/capability/capability-diff-slice.js +41 -0
- package/lib/datasource/capability/capability-key.js +34 -0
- package/lib/datasource/capability/capability-resolve.js +172 -0
- package/lib/datasource/capability/capability-storage-keys.js +22 -0
- package/lib/datasource/capability/copy-operations.js +348 -0
- package/lib/datasource/capability/copy-test-payload.js +139 -0
- package/lib/datasource/capability/create-operations.js +235 -0
- package/lib/datasource/capability/dimension-operations.js +151 -0
- package/lib/datasource/capability/dimension-validate.js +219 -0
- package/lib/datasource/capability/json-pointer.js +31 -0
- package/lib/datasource/capability/reference-rewrite.js +51 -0
- package/lib/datasource/capability/relate-operations.js +325 -0
- package/lib/datasource/capability/relate-validate.js +219 -0
- package/lib/datasource/capability/remove-operations.js +275 -0
- package/lib/datasource/capability/run-capability-copy.js +152 -0
- package/lib/datasource/capability/run-capability-diff.js +135 -0
- package/lib/datasource/capability/run-capability-dimension.js +291 -0
- package/lib/datasource/capability/run-capability-edit.js +377 -0
- package/lib/datasource/capability/run-capability-relate.js +193 -0
- package/lib/datasource/capability/run-capability-remove.js +105 -0
- package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
- package/lib/datasource/capability/validate-capability-slice.js +35 -0
- package/lib/datasource/list.js +136 -23
- package/lib/datasource/log-viewer.js +2 -4
- package/lib/datasource/unified-validation-run.js +51 -16
- package/lib/datasource/validate.js +53 -1
- package/lib/deployment/deploy-poll-ui.js +60 -0
- package/lib/deployment/deployer-status.js +29 -3
- package/lib/deployment/deployer.js +48 -30
- package/lib/deployment/environment.js +7 -2
- package/lib/deployment/poll-interval.js +72 -0
- package/lib/deployment/push.js +11 -9
- package/lib/external-system/deploy.js +9 -2
- package/lib/external-system/download.js +61 -32
- package/lib/external-system/sync-deploy-manifest.js +33 -0
- package/lib/infrastructure/index.js +49 -19
- package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
- package/lib/internal/node-fs.js +2 -0
- package/lib/parameters/infra-kv-discovery.js +29 -4
- package/lib/parameters/infra-parameter-catalog.js +6 -3
- package/lib/parameters/infra-parameter-validate.js +67 -19
- package/lib/resolvers/datasource-resolver.js +53 -0
- package/lib/resolvers/dimension-file.js +52 -0
- package/lib/resolvers/manifest-resolver.js +133 -0
- package/lib/schema/application-schema.json +4 -0
- package/lib/schema/external-datasource.schema.json +183 -53
- package/lib/schema/external-system.schema.json +23 -10
- package/lib/schema/infra.parameter.yaml +26 -1
- package/lib/schema/wizard-config.schema.json +1 -1
- package/lib/utils/aifabrix-config-dir-walk.js +40 -0
- package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
- package/lib/utils/app-config-resolver.js +24 -1
- package/lib/utils/app-run-containers.js +2 -2
- package/lib/utils/applications-config-defaults.js +206 -0
- package/lib/utils/auth-config-validator.js +2 -12
- package/lib/utils/bash-secret-env.js +59 -0
- package/lib/utils/cli-secrets-error-format.js +78 -0
- package/lib/utils/cli-test-layout-chalk.js +31 -9
- package/lib/utils/cli-utils.js +4 -36
- package/lib/utils/compose-generate-docker-compose.js +111 -6
- package/lib/utils/compose-generator.js +17 -8
- package/lib/utils/controller-url.js +50 -7
- package/lib/utils/datasource-test-run-display.js +8 -0
- package/lib/utils/dev-hosts-helper.js +3 -2
- package/lib/utils/dev-init-ssh-merge.js +2 -1
- package/lib/utils/docker-build.js +17 -9
- package/lib/utils/docker-reload-mount.js +127 -0
- package/lib/utils/env-copy.js +99 -14
- package/lib/utils/env-template.js +5 -1
- package/lib/utils/external-readme.js +71 -2
- package/lib/utils/external-system-local-test-tty.js +3 -2
- package/lib/utils/external-system-readiness-core.js +45 -12
- package/lib/utils/external-system-readiness-deploy-display.js +3 -3
- package/lib/utils/external-system-readiness-display-internals.js +33 -3
- package/lib/utils/external-system-readiness-display.js +10 -1
- package/lib/utils/file-upload.js +40 -3
- package/lib/utils/health-check-db-init.js +107 -0
- package/lib/utils/health-check-public-warn.js +69 -0
- package/lib/utils/health-check-url.js +28 -10
- package/lib/utils/health-check.js +139 -107
- package/lib/utils/help-builder.js +5 -1
- package/lib/utils/image-name.js +34 -7
- package/lib/utils/infra-optional-service-flags.js +69 -0
- package/lib/utils/installation-log-core.js +282 -0
- package/lib/utils/installation-log-record.js +237 -0
- package/lib/utils/installation-log.js +123 -0
- package/lib/utils/integration-file-backup.js +74 -0
- package/lib/utils/log-redaction.js +105 -0
- package/lib/utils/manifest-location.js +164 -0
- package/lib/utils/manifest-source-emit.js +162 -0
- package/lib/utils/mutagen-install.js +30 -3
- package/lib/utils/paths.js +308 -76
- package/lib/utils/postgres-wipe.js +212 -0
- package/lib/utils/register-aifabrix-shell-env.js +15 -0
- package/lib/utils/remote-dev-auth.js +21 -5
- package/lib/utils/remote-docker-env.js +9 -1
- package/lib/utils/remote-secrets-loader.js +49 -4
- package/lib/utils/resolve-docker-image-ref.js +9 -3
- package/lib/utils/run-cli-flags.js +29 -0
- package/lib/utils/secrets-ancestor-paths.js +47 -0
- package/lib/utils/secrets-canonical.js +10 -3
- package/lib/utils/secrets-helpers.js +17 -10
- package/lib/utils/secrets-kv-refs.js +42 -0
- package/lib/utils/secrets-kv-scope.js +19 -2
- package/lib/utils/secrets-materialize-local.js +134 -0
- package/lib/utils/secrets-path.js +26 -13
- package/lib/utils/secrets-utils.js +20 -10
- package/lib/utils/system-builder-root.js +42 -0
- package/lib/utils/url-declarative-public-base.js +80 -12
- package/lib/utils/url-declarative-resolve-build-urls.js +238 -0
- package/lib/utils/url-declarative-resolve-build.js +24 -388
- package/lib/utils/url-declarative-resolve-expand-token.js +189 -0
- package/lib/utils/url-declarative-resolve-load-doc.js +12 -3
- package/lib/utils/url-declarative-resolve-surface-state.js +102 -0
- package/lib/utils/url-declarative-resolve.js +47 -7
- package/lib/utils/url-declarative-runtime-base-path.js +52 -0
- package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
- package/lib/utils/urls-local-registry-scan.js +103 -0
- package/lib/utils/urls-local-registry.js +158 -76
- package/lib/utils/validation-poll-ui.js +81 -0
- package/lib/utils/validation-run-poll.js +29 -5
- package/lib/utils/with-muted-logger.js +53 -0
- package/package.json +3 -1
- package/templates/applications/dataplane/application.yaml +5 -1
- package/templates/applications/dataplane/rbac.yaml +10 -10
- package/templates/applications/keycloak/env.template +8 -6
- package/templates/applications/miso-controller/application.yaml +9 -0
- package/templates/applications/miso-controller/env.template +27 -29
- package/templates/applications/miso-controller/rbac.yaml +9 -9
- package/templates/external-system/README.md.hbs +83 -123
- package/.npmrc.token +0 -1
- package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
- package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
- package/.nyc_output/processinfo/index.json +0 -1
- package/lib/api/service-users.api.js +0 -150
- package/lib/api/types/service-users.types.js +0 -65
- package/lib/cli/setup-service-user.js +0 -187
- package/lib/commands/service-user.js +0 -429
package/lib/commands/app-logs.js
CHANGED
|
@@ -15,6 +15,7 @@ const containerHelpers = require('../utils/app-run-containers');
|
|
|
15
15
|
const { validateAppName } = require('../app/push');
|
|
16
16
|
|
|
17
17
|
const { execWithDockerEnv } = require('../utils/docker-exec');
|
|
18
|
+
const { maskEnvLine } = require('../utils/log-redaction');
|
|
18
19
|
|
|
19
20
|
/** Default number of log lines */
|
|
20
21
|
const DEFAULT_TAIL_LINES = 100;
|
|
@@ -43,36 +44,11 @@ const LEVEL_JSON_NUMERIC_REGEX = /"level"\s*:\s*(\d+)/;
|
|
|
43
44
|
/** Fallback: line contains whole-word "error" or "Error" when no other level detected (catches stack traces, "Error: msg", etc.) */
|
|
44
45
|
const ERROR_WORD_FALLBACK_REGEX = /\berror\b/i;
|
|
45
46
|
|
|
46
|
-
/**
|
|
47
|
-
const
|
|
47
|
+
/** Keycloak / Java style: validation failure without the word "error" (would be dropped by `-l error` otherwise) */
|
|
48
|
+
const VALIDATION_FAILURE_LINE_REGEX = /\bvalidation\s+failed\b/i;
|
|
48
49
|
|
|
49
|
-
/**
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
/** URL with embedded credentials: scheme://user:password@host → scheme://user:***@host */
|
|
53
|
-
const URL_CREDENTIAL_PATTERN = /(\w+:\/\/)([^:@]*):([^@]+)@/g;
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Masks a single env line if the key looks like a secret or value contains URL credentials
|
|
57
|
-
* @param {string} line - Line in form KEY=value
|
|
58
|
-
* @returns {string} Same line or KEY=*** or value with masked URL credentials
|
|
59
|
-
*/
|
|
60
|
-
function maskEnvLine(line) {
|
|
61
|
-
const eq = line.indexOf('=');
|
|
62
|
-
if (eq <= 0) return line;
|
|
63
|
-
const key = line.slice(0, eq);
|
|
64
|
-
const value = line.slice(eq + 1);
|
|
65
|
-
|
|
66
|
-
const keyForCheck = key.replace(KEY_PREFIXES_TO_STRIP, '');
|
|
67
|
-
const isSecretKey = SECRET_KEY_PATTERN.test(keyForCheck);
|
|
68
|
-
|
|
69
|
-
const maskedValue = value.replace(URL_CREDENTIAL_PATTERN, '$1$2:***@');
|
|
70
|
-
const hasUrlCredentials = maskedValue !== value;
|
|
71
|
-
|
|
72
|
-
if (isSecretKey) return `${key}=***`;
|
|
73
|
-
if (hasUrlCredentials) return `${key}=${maskedValue}`;
|
|
74
|
-
return line;
|
|
75
|
-
}
|
|
50
|
+
/** Other failure phrases treated as error severity for level filtering */
|
|
51
|
+
const FAILURE_PHRASE_AS_ERROR_REGEX = /\b(fatal|failed to run|unable to (start|run)|exception in thread)\b/i;
|
|
76
52
|
|
|
77
53
|
/** Normalize level string to canonical 'debug'|'info'|'warn'|'error'. */
|
|
78
54
|
function normalizeLevel(raw) {
|
|
@@ -104,9 +80,66 @@ function getLogLevel(line) {
|
|
|
104
80
|
const jsonNum = line.match(LEVEL_JSON_NUMERIC_REGEX);
|
|
105
81
|
if (jsonNum) return numericLevelToName(parseInt(jsonNum[1], 10));
|
|
106
82
|
if (ERROR_WORD_FALLBACK_REGEX.test(line)) return 'error';
|
|
83
|
+
if (VALIDATION_FAILURE_LINE_REGEX.test(line)) return 'error';
|
|
84
|
+
if (FAILURE_PHRASE_AS_ERROR_REGEX.test(line)) return 'error';
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Lines after an `error`-level line often have no level prefix (e.g. Keycloak config validation details). */
|
|
89
|
+
const ERROR_FILTER_CONTEXT_UNCLASSIFIED_MAX = 50;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Whether to show a log line when `--level` filtering is on. After an emitted error line, includes
|
|
93
|
+
* up to {@link ERROR_FILTER_CONTEXT_UNCLASSIFIED_MAX} following lines with no parseable level so
|
|
94
|
+
* stack traces and multi-line validation output are not dropped.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} line
|
|
97
|
+
* @param {string|null} minLevel - normalized LOG_LEVELS value or null
|
|
98
|
+
* @param {{ pendingAfterError: number }} state - mutable; only used when minLevel is `error`
|
|
99
|
+
* @returns {boolean}
|
|
100
|
+
*/
|
|
101
|
+
/**
|
|
102
|
+
* @param {string|null} level
|
|
103
|
+
* @param {string} minLevel
|
|
104
|
+
* @param {{ pendingAfterError: number }} state
|
|
105
|
+
* @returns {boolean|null} true if line shown by sticky rule; null to continue
|
|
106
|
+
*/
|
|
107
|
+
function tryStickyErrorContinuation(level, minLevel, state) {
|
|
108
|
+
if (minLevel !== 'error' || state.pendingAfterError <= 0) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
if (level === null || level === undefined) {
|
|
112
|
+
state.pendingAfterError -= 1;
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
if (level !== 'error') {
|
|
116
|
+
state.pendingAfterError = 0;
|
|
117
|
+
}
|
|
107
118
|
return null;
|
|
108
119
|
}
|
|
109
120
|
|
|
121
|
+
function shouldShowFilteredLogLine(line, minLevel, state) {
|
|
122
|
+
if (minLevel === null || minLevel === undefined || minLevel === '' || !LOG_LEVELS.includes(minLevel)) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
if (line.trim() === '' && minLevel === 'error' && state.pendingAfterError > 0) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
const level = getLogLevel(line);
|
|
129
|
+
const sticky = tryStickyErrorContinuation(level, minLevel, state);
|
|
130
|
+
if (sticky === true) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (passesLevelFilter(level, minLevel)) {
|
|
135
|
+
if (minLevel === 'error' && level === 'error') {
|
|
136
|
+
state.pendingAfterError = ERROR_FILTER_CONTEXT_UNCLASSIFIED_MAX;
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
|
|
110
143
|
/**
|
|
111
144
|
* Whether a line's level passes the minimum level filter (show this level and above).
|
|
112
145
|
* @param {string|null} lineLevel - Level from getLogLevel (null treated as 'info')
|
|
@@ -143,7 +176,12 @@ async function dumpMaskedEnv(containerName) {
|
|
|
143
176
|
lines.forEach((line) => logger.log(maskEnvLine(line)));
|
|
144
177
|
logger.log(chalk.gray('\n--- Logs ---\n'));
|
|
145
178
|
} catch (err) {
|
|
146
|
-
logger.log(
|
|
179
|
+
logger.log(
|
|
180
|
+
chalk.gray(
|
|
181
|
+
'(Could not read container env — the container may be stopped, restarting, or not ready yet. ' +
|
|
182
|
+
'Docker log output below is still shown.)\n'
|
|
183
|
+
)
|
|
184
|
+
);
|
|
147
185
|
}
|
|
148
186
|
}
|
|
149
187
|
|
|
@@ -175,8 +213,9 @@ async function runDockerLogs(containerName, options) {
|
|
|
175
213
|
const proc = spawn('docker', args, { stdio: ['inherit', 'pipe', 'pipe'], env: dockerEnv });
|
|
176
214
|
proc.on('error', reject);
|
|
177
215
|
|
|
216
|
+
const filterState = { pendingAfterError: 0 };
|
|
178
217
|
function onLine(line) {
|
|
179
|
-
if (
|
|
218
|
+
if (shouldShowFilteredLogLine(line, minLevel, filterState)) {
|
|
180
219
|
process.stdout.write(line + '\n');
|
|
181
220
|
}
|
|
182
221
|
}
|
|
@@ -241,8 +280,11 @@ async function runDockerLogsFollow(containerName, tail, minLevel) {
|
|
|
241
280
|
logger.log(chalk.red(`Error: ${err.message}`));
|
|
242
281
|
process.exit(1);
|
|
243
282
|
});
|
|
283
|
+
const filterState = { pendingAfterError: 0 };
|
|
244
284
|
function onLine(line) {
|
|
245
|
-
if (
|
|
285
|
+
if (shouldShowFilteredLogLine(line, level, filterState)) {
|
|
286
|
+
process.stdout.write(line + '\n');
|
|
287
|
+
}
|
|
246
288
|
}
|
|
247
289
|
const rlOut = readline.createInterface({ input: proc.stdout, crlfDelay: Infinity });
|
|
248
290
|
rlOut.on('line', onLine);
|
|
@@ -301,4 +343,10 @@ async function runAppLogs(appKey, options = {}) {
|
|
|
301
343
|
}
|
|
302
344
|
}
|
|
303
345
|
|
|
304
|
-
module.exports = {
|
|
346
|
+
module.exports = {
|
|
347
|
+
runAppLogs,
|
|
348
|
+
maskEnvLine,
|
|
349
|
+
getLogLevel,
|
|
350
|
+
passesLevelFilter,
|
|
351
|
+
shouldShowFilteredLogLine
|
|
352
|
+
};
|
|
@@ -8,53 +8,144 @@
|
|
|
8
8
|
* @version 2.0.0
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
const chalk = require('chalk');
|
|
11
12
|
const { formatBlockingError, formatSuccessLine } = require('../utils/cli-test-layout-chalk');
|
|
12
13
|
const {
|
|
13
14
|
setControllerUrl,
|
|
14
15
|
setCurrentEnvironment,
|
|
15
|
-
getControllerUrl
|
|
16
|
+
getControllerUrl,
|
|
17
|
+
getRegisteredControllerUrls,
|
|
18
|
+
getConfig
|
|
16
19
|
} = require('../core/config');
|
|
17
20
|
const {
|
|
18
21
|
validateControllerUrl,
|
|
19
22
|
validateEnvironment,
|
|
20
23
|
checkUserLoggedIn
|
|
21
24
|
} = require('../utils/auth-config-validator');
|
|
22
|
-
const {
|
|
25
|
+
const { hasStoredDeviceTokenForController } = require('../utils/controller-url');
|
|
23
26
|
const logger = require('../utils/logger');
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* True when user ran --set-controller with no URL (pick from config.yaml).
|
|
30
|
+
* @param {unknown} setController - Commander option value
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
function isInteractiveControllerPick(setController) {
|
|
34
|
+
return (
|
|
35
|
+
setController === true ||
|
|
36
|
+
(typeof setController === 'string' && setController.trim() === '')
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* True when a non-empty controller URL string was passed.
|
|
42
|
+
* @param {unknown} setController - Commander option value
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
function hasExplicitControllerUrl(setController) {
|
|
46
|
+
return typeof setController === 'string' && setController.trim() !== '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Normalize controller URL for equality checks (trailing slashes).
|
|
51
|
+
* @param {string|null|undefined} url - URL or empty
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
function normalizeForCompare(url) {
|
|
55
|
+
if (!url || typeof url !== 'string') return '';
|
|
56
|
+
return url.trim().replace(/\/+$/, '');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function throwNoRegisteredControllers() {
|
|
60
|
+
const msg =
|
|
61
|
+
'No controllers are registered in config. Run "aifabrix login" first, or set a controller with "aifabrix auth --set-controller <url>".';
|
|
62
|
+
logger.error(formatBlockingError(msg));
|
|
63
|
+
throw new Error(msg);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function throwNonInteractiveControllerPick() {
|
|
67
|
+
const msg =
|
|
68
|
+
'Cannot choose a controller without a URL in non-interactive mode. Run: aifabrix auth --set-controller <url>';
|
|
69
|
+
logger.error(formatBlockingError(msg));
|
|
70
|
+
throw new Error(msg);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Pick default controller from URLs in config (`controller` + `device` keys), or set the only one.
|
|
75
|
+
* @async
|
|
76
|
+
* @returns {Promise<void>}
|
|
77
|
+
*/
|
|
78
|
+
async function handleSelectRegisteredController() {
|
|
79
|
+
const urls = await getRegisteredControllerUrls();
|
|
80
|
+
|
|
81
|
+
if (urls.length === 0) {
|
|
82
|
+
throwNoRegisteredControllers();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (!process.stdin.isTTY) {
|
|
86
|
+
throwNonInteractiveControllerPick();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (urls.length === 1) {
|
|
90
|
+
const sole = urls[0];
|
|
91
|
+
const current = await getControllerUrl();
|
|
92
|
+
if (normalizeForCompare(current) === normalizeForCompare(sole)) {
|
|
93
|
+
logger.log(formatSuccessLine(`Default controller is already set to ${sole}.`));
|
|
94
|
+
logger.log(
|
|
95
|
+
chalk.white('To add another controller, run: aifabrix auth --set-controller <url>')
|
|
96
|
+
);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
await handleSetController(sole);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const inquirer = require('inquirer');
|
|
104
|
+
const { controllerUrl } = await inquirer.prompt([
|
|
105
|
+
{
|
|
106
|
+
type: 'list',
|
|
107
|
+
name: 'controllerUrl',
|
|
108
|
+
message: 'Select default controller:',
|
|
109
|
+
choices: urls
|
|
110
|
+
}
|
|
111
|
+
]);
|
|
112
|
+
await handleSetController(controllerUrl);
|
|
113
|
+
}
|
|
114
|
+
|
|
25
115
|
/**
|
|
26
116
|
* Handle set-controller command
|
|
27
|
-
* Allows setting the default controller when no
|
|
28
|
-
*
|
|
117
|
+
* Allows setting the default controller when there are no device tokens, or when a token exists
|
|
118
|
+
* for the target URL (including switching among multiple logged-in controllers). If device tokens
|
|
119
|
+
* exist only for other controller URLs, throws with a clear message.
|
|
29
120
|
*
|
|
30
121
|
* @async
|
|
31
122
|
* @function handleSetController
|
|
32
123
|
* @param {string} url - Controller URL to set
|
|
33
124
|
* @returns {Promise<void>}
|
|
34
|
-
* @throws {Error} If validation fails or credentials exist for
|
|
125
|
+
* @throws {Error} If validation fails or credentials exist only for other controllers
|
|
35
126
|
*/
|
|
36
127
|
async function handleSetController(url) {
|
|
37
128
|
try {
|
|
38
129
|
validateControllerUrl(url);
|
|
39
130
|
const normalizedUrl = url.trim().replace(/\/+$/, '');
|
|
40
131
|
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return;
|
|
47
|
-
}
|
|
132
|
+
const userConfig = await getConfig();
|
|
133
|
+
const device =
|
|
134
|
+
userConfig.device && typeof userConfig.device === 'object' ? userConfig.device : {};
|
|
135
|
+
const deviceKeys = Object.keys(device);
|
|
136
|
+
const hasTokenForTarget = await hasStoredDeviceTokenForController(normalizedUrl);
|
|
48
137
|
|
|
49
|
-
|
|
50
|
-
if (normalizedLoggedIn === normalizedUrl) {
|
|
138
|
+
if (deviceKeys.length === 0 || hasTokenForTarget) {
|
|
51
139
|
await setControllerUrl(url);
|
|
52
140
|
logger.log(formatSuccessLine(`Controller URL set to: ${url}`));
|
|
53
141
|
return;
|
|
54
142
|
}
|
|
55
143
|
|
|
144
|
+
const otherKey =
|
|
145
|
+
deviceKeys.find((k) => normalizeForCompare(k) !== normalizeForCompare(normalizedUrl)) ||
|
|
146
|
+
deviceKeys[0];
|
|
56
147
|
throw new Error(
|
|
57
|
-
`You have credentials for another controller (${
|
|
148
|
+
`You have credentials for another controller (${otherKey.trim().replace(/\/+$/, '')}).\n` +
|
|
58
149
|
'To use a different controller either run "aifabrix login" with that controller, or run "aifabrix logout" first to clear credentials, then set the new controller with "aifabrix auth --set-controller <url>".'
|
|
59
150
|
);
|
|
60
151
|
} catch (error) {
|
|
@@ -107,20 +198,27 @@ async function handleSetEnvironment(environment) {
|
|
|
107
198
|
* @async
|
|
108
199
|
* @function handleAuthConfig
|
|
109
200
|
* @param {Object} options - Command options
|
|
110
|
-
* @param {string} [options.setController] - Controller URL
|
|
201
|
+
* @param {string|boolean} [options.setController] - Controller URL, or true when flag has no value
|
|
111
202
|
* @param {string} [options.setEnvironment] - Environment to set
|
|
112
203
|
* @returns {Promise<void>}
|
|
113
204
|
* @throws {Error} If command fails
|
|
114
205
|
*/
|
|
115
206
|
async function handleAuthConfig(options) {
|
|
116
|
-
|
|
207
|
+
const pick = isInteractiveControllerPick(options.setController);
|
|
208
|
+
const hasUrl = hasExplicitControllerUrl(options.setController);
|
|
209
|
+
|
|
210
|
+
if (!pick && !hasUrl && !options.setEnvironment) {
|
|
117
211
|
throw new Error(
|
|
118
212
|
'No action specified. Use "aifabrix auth --set-controller <url>" or "aifabrix auth --set-environment <env>".'
|
|
119
213
|
);
|
|
120
214
|
}
|
|
121
|
-
|
|
215
|
+
|
|
216
|
+
if (pick) {
|
|
217
|
+
await handleSelectRegisteredController();
|
|
218
|
+
} else if (hasUrl) {
|
|
122
219
|
await handleSetController(options.setController);
|
|
123
220
|
}
|
|
221
|
+
|
|
124
222
|
if (options.setEnvironment) {
|
|
125
223
|
await handleSetEnvironment(options.setEnvironment);
|
|
126
224
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `datasource capability dimension` command registration.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview dimension binding CLI
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
const {
|
|
13
|
+
formatBlockingError,
|
|
14
|
+
headerKeyValue,
|
|
15
|
+
infoLine,
|
|
16
|
+
formatSuccessLine,
|
|
17
|
+
colorRollupPrefixedLine
|
|
18
|
+
} = require('../utils/cli-test-layout-chalk');
|
|
19
|
+
const { runCapabilityDimension } = require('../datasource/capability/run-capability-dimension');
|
|
20
|
+
const { printCapabilitySuccessFooter } = require('./datasource-capability-output');
|
|
21
|
+
|
|
22
|
+
const CAP_DIMENSION_HELP = `
|
|
23
|
+
Adds or replaces one root **dimensions.<key>** binding (metadata-only; no pipeline).
|
|
24
|
+
|
|
25
|
+
Examples:
|
|
26
|
+
$ aifabrix datasource capability dimension test-e2e-hubspot-companies --dimension market --type local --field country
|
|
27
|
+
$ aifabrix datasource capability dimension test-e2e-hubspot-companies --dimension owner --type fk --via hubspotOwner:owner --actor email
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
function parseVia(raw) {
|
|
31
|
+
const list = Array.isArray(raw) ? raw : [];
|
|
32
|
+
return list.map((x) => String(x).trim()).filter(Boolean);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function logDimensionValidationOutcome(result) {
|
|
36
|
+
logger.log(formatSuccessLine('Local validation passed'));
|
|
37
|
+
if (result.remoteValidation?.ok) {
|
|
38
|
+
logger.log(formatSuccessLine('Remote validation passed'));
|
|
39
|
+
} else {
|
|
40
|
+
logger.log(colorRollupPrefixedLine('⚠ Remote validation skipped (not authenticated)'));
|
|
41
|
+
}
|
|
42
|
+
if (Array.isArray(result.semanticWarnings) && result.semanticWarnings.length > 0) {
|
|
43
|
+
result.semanticWarnings.forEach((w) => logger.log(colorRollupPrefixedLine(`⚠ ${w}`)));
|
|
44
|
+
}
|
|
45
|
+
logger.log(formatSuccessLine('Dimension binding updated'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} fileOrKey
|
|
50
|
+
* @param {object} options - Commander options
|
|
51
|
+
* @returns {Promise<void>}
|
|
52
|
+
*/
|
|
53
|
+
async function runDimensionAction(fileOrKey, options) {
|
|
54
|
+
const via = parseVia(options.via);
|
|
55
|
+
|
|
56
|
+
const result = await runCapabilityDimension({
|
|
57
|
+
fileOrKey,
|
|
58
|
+
dimension: options.dimension,
|
|
59
|
+
type: options.type,
|
|
60
|
+
field: options.field,
|
|
61
|
+
via,
|
|
62
|
+
actor: options.actor,
|
|
63
|
+
operator: options.operator,
|
|
64
|
+
required: options.required,
|
|
65
|
+
dryRun: Boolean(options.dryRun),
|
|
66
|
+
noBackup: Boolean(options.noBackup),
|
|
67
|
+
overwrite: Boolean(options.overwrite)
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
if (result.dryRun) {
|
|
71
|
+
logger.log(infoLine('Dry run — planned JSON Patch operations:'));
|
|
72
|
+
logger.log('');
|
|
73
|
+
logger.log(JSON.stringify(result.patchOperations, null, 2));
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
logDimensionValidationOutcome(result);
|
|
78
|
+
|
|
79
|
+
if (result.backupPath) {
|
|
80
|
+
logger.log(headerKeyValue('Backup:', result.backupPath));
|
|
81
|
+
}
|
|
82
|
+
printCapabilitySuccessFooter(result.resolvedPath, result.updatedSections, 'Updated');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {import('commander').Command} cap
|
|
87
|
+
* @returns {void}
|
|
88
|
+
*/
|
|
89
|
+
function setupCapabilityDimensionCommand(cap) {
|
|
90
|
+
cap
|
|
91
|
+
.command('dimension <file-or-key>')
|
|
92
|
+
.description('Add or replace one root dimensions binding (local or FK-backed)')
|
|
93
|
+
.requiredOption('--dimension <key>', 'Dimension key (ABAC-facing key; underscores allowed)')
|
|
94
|
+
.requiredOption('--type <type>', 'local | fk')
|
|
95
|
+
.option('--field <name>', 'For type=local: normalized attribute name in metadataSchema.properties')
|
|
96
|
+
.option(
|
|
97
|
+
'--via <fk:dimension>',
|
|
98
|
+
'For type=fk: hop as <fkName>:<dimensionKey>; repeat for multi-hop traversal',
|
|
99
|
+
(value, prev) => {
|
|
100
|
+
const list = prev || [];
|
|
101
|
+
list.push(value);
|
|
102
|
+
return list;
|
|
103
|
+
},
|
|
104
|
+
[]
|
|
105
|
+
)
|
|
106
|
+
.option('--actor <actor>', 'For type=fk: displayName | email | userId | groups | roles')
|
|
107
|
+
.option('--operator <op>', 'For type=fk: eq | in (default depends on actor)')
|
|
108
|
+
.option('--required', 'Set dimensions.<key>.required=true')
|
|
109
|
+
.option('--no-required', 'Set dimensions.<key>.required=false')
|
|
110
|
+
.option('--dry-run', 'Print JSON Patch operations; do not write')
|
|
111
|
+
.option('--overwrite', 'Replace existing dimensions.<key> binding')
|
|
112
|
+
.option('--no-backup', 'Skip backup copy under integration/<app>/backup/')
|
|
113
|
+
.addHelpText('after', CAP_DIMENSION_HELP)
|
|
114
|
+
.action(async(fileOrKey, options) => {
|
|
115
|
+
try {
|
|
116
|
+
await runDimensionAction(fileOrKey, options);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger.error(formatBlockingError(`capability dimension failed: ${error.message}`));
|
|
119
|
+
process.exit(1);
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
setupCapabilityDimensionCommand,
|
|
126
|
+
runDimensionAction
|
|
127
|
+
};
|
|
128
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared success footer for datasource capability mutating commands.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview capability CLI output footer
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const logger = require('../utils/logger');
|
|
10
|
+
const { formatBulletSection, formatNextActions } = require('../utils/cli-test-layout-chalk');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @param {string} resolvedPath
|
|
14
|
+
* @param {string[]} updatedSections
|
|
15
|
+
* @param {string} [heading='Updated']
|
|
16
|
+
* @returns {void}
|
|
17
|
+
*/
|
|
18
|
+
function printCapabilitySuccessFooter(resolvedPath, updatedSections, heading = 'Updated') {
|
|
19
|
+
const display =
|
|
20
|
+
resolvedPath.includes(' ') ? `"${resolvedPath}"` : resolvedPath;
|
|
21
|
+
logger.log('');
|
|
22
|
+
logger.log(formatBulletSection(`${heading}:`, updatedSections));
|
|
23
|
+
logger.log('');
|
|
24
|
+
logger.log(formatNextActions([`aifabrix datasource validate ${display}`]));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
printCapabilitySuccessFooter
|
|
29
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `datasource capability relate` command registration.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview relate CLI
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const logger = require('../utils/logger');
|
|
10
|
+
const {
|
|
11
|
+
formatBlockingError,
|
|
12
|
+
headerKeyValue,
|
|
13
|
+
infoLine,
|
|
14
|
+
formatSuccessLine,
|
|
15
|
+
colorRollupPrefixedLine
|
|
16
|
+
} = require('../utils/cli-test-layout-chalk');
|
|
17
|
+
const { runCapabilityRelate } = require('../datasource/capability/run-capability-relate');
|
|
18
|
+
const { printCapabilitySuccessFooter } = require('./datasource-capability-output');
|
|
19
|
+
|
|
20
|
+
const CAP_RELATE_HELP = `
|
|
21
|
+
Adds or replaces one **foreignKeys[]** row (metadata-only; no pipeline). Optional **metadataSchema.properties** stub unless **--skip-metadata-property**.
|
|
22
|
+
|
|
23
|
+
$ aifabrix datasource capability relate hubspot-deals --relation-name company --to hubspot-companies --field companyId --target-field externalId
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
function parseTargetFields(raw) {
|
|
27
|
+
const tf = raw;
|
|
28
|
+
if (Array.isArray(tf) && tf.length > 0) {
|
|
29
|
+
return tf.map((x) => String(x).trim()).filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
if (tf !== undefined && tf !== null && String(tf).trim()) {
|
|
32
|
+
return [String(tf).trim()];
|
|
33
|
+
}
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function logRelateValidationOutcome(result, joinLeft, joinRight) {
|
|
38
|
+
logger.log(formatSuccessLine('Local validation passed'));
|
|
39
|
+
if (result.remoteValidation?.ok) {
|
|
40
|
+
logger.log(formatSuccessLine('Remote validation passed'));
|
|
41
|
+
} else {
|
|
42
|
+
logger.log(colorRollupPrefixedLine('⚠ Remote validation skipped (not authenticated)'));
|
|
43
|
+
}
|
|
44
|
+
if (Array.isArray(result.semanticWarnings) && result.semanticWarnings.length > 0) {
|
|
45
|
+
result.semanticWarnings.forEach((w) => logger.log(colorRollupPrefixedLine(`⚠ ${w}`)));
|
|
46
|
+
}
|
|
47
|
+
logger.log(formatSuccessLine(`Relation created: ${joinLeft} → ${joinRight}`));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* @param {string} fileOrKey
|
|
52
|
+
* @param {object} options - Commander options
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
*/
|
|
55
|
+
async function runRelateAction(fileOrKey, options) {
|
|
56
|
+
const fields = [];
|
|
57
|
+
if (options.field) {
|
|
58
|
+
fields.push(String(options.field).trim());
|
|
59
|
+
}
|
|
60
|
+
const targetFields = parseTargetFields(options.targetField);
|
|
61
|
+
|
|
62
|
+
const result = await runCapabilityRelate({
|
|
63
|
+
fileOrKey,
|
|
64
|
+
relationName: options.relationName,
|
|
65
|
+
targetDatasource: options.to,
|
|
66
|
+
fields,
|
|
67
|
+
targetFields,
|
|
68
|
+
required: options.required,
|
|
69
|
+
description: options.description,
|
|
70
|
+
dryRun: Boolean(options.dryRun),
|
|
71
|
+
noBackup: Boolean(options.noBackup),
|
|
72
|
+
overwrite: Boolean(options.overwrite),
|
|
73
|
+
addMetadataProperty: !options.skipMetadataProperty
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
if (result.dryRun) {
|
|
77
|
+
logger.log(infoLine('Dry run — planned JSON Patch operations:'));
|
|
78
|
+
logger.log('');
|
|
79
|
+
logger.log(JSON.stringify(result.patchOperations, null, 2));
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const joinLeft = fields.length === 1 ? fields[0] : 'fields';
|
|
84
|
+
const joinRight =
|
|
85
|
+
Array.isArray(targetFields) && targetFields.length === 1
|
|
86
|
+
? `${options.to}.${targetFields[0]}`
|
|
87
|
+
: options.to;
|
|
88
|
+
logRelateValidationOutcome(result, joinLeft, joinRight);
|
|
89
|
+
|
|
90
|
+
if (result.backupPath) {
|
|
91
|
+
logger.log(headerKeyValue('Backup:', result.backupPath));
|
|
92
|
+
}
|
|
93
|
+
printCapabilitySuccessFooter(result.resolvedPath, result.updatedSections, 'Updated');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* @param {import('commander').Command} cap
|
|
98
|
+
* @returns {void}
|
|
99
|
+
*/
|
|
100
|
+
function setupCapabilityRelateCommand(cap) {
|
|
101
|
+
cap
|
|
102
|
+
.command('relate <file-or-key>')
|
|
103
|
+
.description(
|
|
104
|
+
'Add or replace foreignKeys[] metadata (+ optional metadataSchema property for relation name)'
|
|
105
|
+
)
|
|
106
|
+
.requiredOption('--relation-name <name>', 'FK name (camelCase; unique per datasource)')
|
|
107
|
+
.requiredOption('--to <targetDatasource>', 'Target datasource key (cross-system JSON key)')
|
|
108
|
+
.requiredOption('--field <name>', 'Local normalized attribute (foreignKeys.fields[])')
|
|
109
|
+
.option('--description <text>', 'FK description (defaults to a generated description)')
|
|
110
|
+
.option('--required', 'Mark FK required (foreignKeys[].required=true)')
|
|
111
|
+
.option('--no-required', 'Mark FK optional (foreignKeys[].required=false)')
|
|
112
|
+
.option(
|
|
113
|
+
'--target-field <name>',
|
|
114
|
+
'Target join field(s); repeat flag for composite; omit to use runtime default (externalId)',
|
|
115
|
+
(value, prev) => {
|
|
116
|
+
const list = prev || [];
|
|
117
|
+
list.push(value);
|
|
118
|
+
return list;
|
|
119
|
+
},
|
|
120
|
+
[]
|
|
121
|
+
)
|
|
122
|
+
.option('--dry-run', 'Print JSON Patch operations; do not write')
|
|
123
|
+
.option('--overwrite', 'Replace existing foreignKeys row with the same name')
|
|
124
|
+
.option('--skip-metadata-property', 'Do not add metadataSchema.properties.<relationName>')
|
|
125
|
+
.option('--no-backup', 'Skip backup copy under integration/<app>/backup/')
|
|
126
|
+
.addHelpText('after', CAP_RELATE_HELP)
|
|
127
|
+
.action(async(fileOrKey, options) => {
|
|
128
|
+
try {
|
|
129
|
+
await runRelateAction(fileOrKey, options);
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logger.error(formatBlockingError(`capability relate failed: ${error.message}`));
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
module.exports = {
|
|
138
|
+
setupCapabilityRelateCommand,
|
|
139
|
+
runRelateAction
|
|
140
|
+
};
|