@aifabrix/builder 2.44.5 → 2.44.6
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 +1 -1
- package/.cursor/rules/project-rules.mdc +1 -1
- package/.npmrc.token +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 +48 -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/push.js +46 -23
- package/lib/app/register.js +1 -1
- package/lib/app/restart-display.js +95 -0
- package/lib/app/rotate-secret.js +1 -1
- package/lib/app/run-container-start.js +12 -6
- package/lib/app/run-env-compose.js +30 -1
- package/lib/app/run-helpers.js +44 -12
- package/lib/app/run-reload-sync.js +148 -0
- package/lib/app/run-resolve-image.js +51 -1
- package/lib/app/run.js +99 -73
- package/lib/build/index.js +75 -45
- package/lib/cli/doctor-check.js +117 -0
- package/lib/cli/index.js +8 -2
- package/lib/cli/infra-guided.js +445 -0
- package/lib/cli/setup-app.js +20 -2
- package/lib/cli/setup-auth.js +26 -0
- package/lib/cli/setup-dev-path-commands.js +50 -3
- package/lib/cli/setup-infra.js +134 -61
- 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.js +78 -33
- 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 +455 -0
- package/lib/commands/setup-prompts.js +388 -0
- package/lib/commands/setup.js +149 -0
- package/lib/commands/teardown.js +228 -0
- package/lib/commands/up-common.js +79 -19
- package/lib/commands/up-dataplane.js +33 -11
- package/lib/commands/up-miso.js +7 -11
- package/lib/commands/upload.js +109 -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/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 +432 -0
- package/lib/core/secrets-env-write.js +27 -1
- package/lib/core/secrets-load.js +248 -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 +4 -1
- 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/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/external-datasource.schema.json +183 -53
- package/lib/schema/external-system.schema.json +23 -10
- package/lib/schema/infra.parameter.yaml +26 -11
- 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-run-containers.js +2 -2
- 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/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/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 +19 -4
- package/lib/utils/health-check.js +135 -105
- package/lib/utils/help-builder.js +5 -1
- package/lib/utils/image-name.js +34 -7
- package/lib/utils/integration-file-backup.js +74 -0
- package/lib/utils/mutagen-install.js +30 -3
- package/lib/utils/paths.js +108 -25
- 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 +42 -3
- package/lib/utils/resolve-docker-image-ref.js +9 -3
- package/lib/utils/secrets-ancestor-paths.js +47 -0
- 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 +24 -10
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/system-builder-root.js +34 -0
- package/lib/utils/url-declarative-resolve-build.js +6 -1
- package/lib/utils/url-declarative-runtime-base-path.js +32 -0
- package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
- package/lib/utils/urls-local-registry.js +23 -12
- 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 +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/rbac.yaml +10 -10
- package/templates/applications/keycloak/env.template +8 -6
- package/templates/applications/miso-controller/application.yaml +7 -0
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/applications/miso-controller/rbac.yaml +9 -9
- package/templates/external-system/README.md.hbs +83 -123
- 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
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Semantic validation for `datasource capability relate`.
|
|
3
|
+
*
|
|
4
|
+
* Pure validation module: no CLI/auth logic.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview relate semantic validator
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {Object} RelateLocalContext
|
|
15
|
+
* @property {any} sourceDoc
|
|
16
|
+
* @property {any|null} targetDocLocal
|
|
17
|
+
* @property {string} targetDatasourceKey
|
|
18
|
+
* @property {string[]} fields
|
|
19
|
+
* @property {string[]|undefined} targetFields
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} RelateValidationInput
|
|
24
|
+
* @property {RelateLocalContext} localContext
|
|
25
|
+
* @property {any|null} remoteManifest - remote target datasource config (or null)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {Object} RelateValidationResult
|
|
30
|
+
* @property {boolean} ok
|
|
31
|
+
* @property {string[]} errors
|
|
32
|
+
* @property {string[]} warnings
|
|
33
|
+
* @property {{ targetDoc: any|null, resolvedTargetFields: string[] }} resolved
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
function _asObject(x) {
|
|
37
|
+
return x && typeof x === 'object' ? x : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _getMetadataProp(doc, fieldName) {
|
|
41
|
+
const props = doc?.metadataSchema?.properties;
|
|
42
|
+
if (!props || typeof props !== 'object') return null;
|
|
43
|
+
const node = props[fieldName];
|
|
44
|
+
return _asObject(node);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function _getFieldMappingsAttributes(doc) {
|
|
48
|
+
const attrs = doc?.fieldMappings?.attributes;
|
|
49
|
+
return attrs && typeof attrs === 'object' ? attrs : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function _normalizeType(node) {
|
|
53
|
+
const t = node?.type;
|
|
54
|
+
if (typeof t === 'string') return t;
|
|
55
|
+
if (Array.isArray(t)) {
|
|
56
|
+
// common JSONSchema pattern: ["string","null"]
|
|
57
|
+
const nonNull = t.filter((x) => x && x !== 'null');
|
|
58
|
+
if (nonNull.length === 1 && typeof nonNull[0] === 'string') return nonNull[0];
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function _isNullable(node) {
|
|
64
|
+
// dataplane uses `nullable: true` convention in config metadataSchema
|
|
65
|
+
return node?.nullable === true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* @param {RelateValidationInput} input
|
|
70
|
+
* @returns {RelateValidationResult}
|
|
71
|
+
*/
|
|
72
|
+
function validateRelateSemantics(input) {
|
|
73
|
+
const errors = [];
|
|
74
|
+
const warnings = [];
|
|
75
|
+
const local = input?.localContext;
|
|
76
|
+
const source = local?.sourceDoc;
|
|
77
|
+
const targetDatasourceKey = String(local?.targetDatasourceKey || '').trim();
|
|
78
|
+
const fields = Array.isArray(local?.fields) ? local.fields : [];
|
|
79
|
+
const resolvedTargetFields = _resolveTargetFields(local?.targetFields);
|
|
80
|
+
|
|
81
|
+
if (!_asObject(source)) {
|
|
82
|
+
errors.push('Local validation failed: sourceDoc missing or invalid.');
|
|
83
|
+
return { ok: false, errors, warnings, resolved: { targetDoc: null, resolvedTargetFields } };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const sourceAttrs = _getFieldMappingsAttributes(source);
|
|
87
|
+
const localFieldMeta = _validateSourceSide({ source, sourceAttrs, fields, errors });
|
|
88
|
+
|
|
89
|
+
const targetDoc = _resolveTargetDoc(local?.targetDocLocal, input?.remoteManifest);
|
|
90
|
+
if (!targetDoc) {
|
|
91
|
+
errors.push(
|
|
92
|
+
`Target datasource not found locally and remote validation did not provide a target config: ${targetDatasourceKey}`
|
|
93
|
+
);
|
|
94
|
+
return { ok: false, errors, warnings, resolved: { targetDoc: null, resolvedTargetFields } };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_validateJoinSemantics({
|
|
98
|
+
targetDoc,
|
|
99
|
+
targetDatasourceKey,
|
|
100
|
+
fields,
|
|
101
|
+
resolvedTargetFields,
|
|
102
|
+
localFieldMeta,
|
|
103
|
+
errors,
|
|
104
|
+
warnings
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return { ok: errors.length === 0, errors, warnings, resolved: { targetDoc, resolvedTargetFields } };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function _resolveTargetFields(targetFieldsRaw) {
|
|
111
|
+
return Array.isArray(targetFieldsRaw) && targetFieldsRaw.length > 0
|
|
112
|
+
? targetFieldsRaw.map((x) => String(x).trim()).filter(Boolean)
|
|
113
|
+
: ['externalId'];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _resolveTargetDoc(targetDocLocal, remoteManifest) {
|
|
117
|
+
if (_asObject(targetDocLocal)) return targetDocLocal;
|
|
118
|
+
if (_asObject(remoteManifest)) return remoteManifest;
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _validateSourceSide({ source, sourceAttrs, fields, errors }) {
|
|
123
|
+
if (!fields || fields.length === 0) {
|
|
124
|
+
errors.push('Local validation failed: at least one local field is required.');
|
|
125
|
+
return {};
|
|
126
|
+
}
|
|
127
|
+
if (!sourceAttrs) {
|
|
128
|
+
errors.push('Source must declare fieldMappings.attributes (object).');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** @type {Record<string, { type: string|null, nullable: boolean }>} */
|
|
132
|
+
const localFieldMeta = {};
|
|
133
|
+
for (const f of fields) {
|
|
134
|
+
const name = String(f || '').trim();
|
|
135
|
+
if (!name) {
|
|
136
|
+
errors.push('Local field names must be non-empty strings.');
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (sourceAttrs && !(name in sourceAttrs)) {
|
|
140
|
+
errors.push(`Source field not found in fieldMappings.attributes: ${name}`);
|
|
141
|
+
}
|
|
142
|
+
const node = _getMetadataProp(source, name);
|
|
143
|
+
if (!node) {
|
|
144
|
+
errors.push(`Source field not found in metadataSchema.properties: ${name}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
localFieldMeta[name] = { type: _normalizeType(node), nullable: _isNullable(node) };
|
|
148
|
+
}
|
|
149
|
+
return localFieldMeta;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _validateJoinSemantics({
|
|
153
|
+
targetDoc,
|
|
154
|
+
targetDatasourceKey,
|
|
155
|
+
fields,
|
|
156
|
+
resolvedTargetFields,
|
|
157
|
+
localFieldMeta,
|
|
158
|
+
errors,
|
|
159
|
+
warnings
|
|
160
|
+
}) {
|
|
161
|
+
const targetAttrs = _getFieldMappingsAttributes(targetDoc);
|
|
162
|
+
if (!targetAttrs) {
|
|
163
|
+
warnings.push(`Target datasource is missing fieldMappings.attributes: ${targetDatasourceKey}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (resolvedTargetFields.length !== fields.length) {
|
|
167
|
+
errors.push(
|
|
168
|
+
`Foreign key cardinality mismatch: ${fields.length} local field(s) vs ${resolvedTargetFields.length} target field(s).`
|
|
169
|
+
);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for (let i = 0; i < fields.length; i += 1) {
|
|
174
|
+
_validateJoinFieldPair({
|
|
175
|
+
targetDoc,
|
|
176
|
+
targetDatasourceKey,
|
|
177
|
+
localName: String(fields[i] || '').trim(),
|
|
178
|
+
targetName: String(resolvedTargetFields[i] || '').trim(),
|
|
179
|
+
localFieldMeta,
|
|
180
|
+
errors,
|
|
181
|
+
warnings
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function _validateJoinFieldPair({
|
|
187
|
+
targetDoc,
|
|
188
|
+
targetDatasourceKey,
|
|
189
|
+
localName,
|
|
190
|
+
targetName,
|
|
191
|
+
localFieldMeta,
|
|
192
|
+
errors,
|
|
193
|
+
warnings
|
|
194
|
+
}) {
|
|
195
|
+
if (!targetName) {
|
|
196
|
+
errors.push('Target field names must be non-empty strings.');
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const targetNode = _getMetadataProp(targetDoc, targetName);
|
|
200
|
+
if (!targetNode) {
|
|
201
|
+
errors.push(`Target field not found in metadataSchema.properties: ${targetDatasourceKey}.${targetName}`);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const lt = localFieldMeta[localName]?.type || null;
|
|
205
|
+
const tt = _normalizeType(targetNode);
|
|
206
|
+
if (lt && tt && lt !== tt) {
|
|
207
|
+
errors.push(`Type mismatch: ${localName} (${lt}) does not match ${targetDatasourceKey}.${targetName} (${tt}).`);
|
|
208
|
+
}
|
|
209
|
+
const ln = localFieldMeta[localName]?.nullable === true;
|
|
210
|
+
const tn = _isNullable(targetNode);
|
|
211
|
+
if (ln !== tn) {
|
|
212
|
+
warnings.push(`Nullable mismatch: ${localName} nullable=${ln} vs ${targetDatasourceKey}.${targetName} nullable=${tn}.`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
validateRelateSemantics
|
|
218
|
+
};
|
|
219
|
+
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remove a capability from one datasource JSON document (including profile + testPayload cleanup).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview capability remove — inverse of copy
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { jsonPointerPath } = require('./json-pointer');
|
|
10
|
+
const {
|
|
11
|
+
capabilityExists,
|
|
12
|
+
removeCapability,
|
|
13
|
+
deepClone
|
|
14
|
+
} = require('./copy-operations');
|
|
15
|
+
const {
|
|
16
|
+
resolveLogicalNameForRemove,
|
|
17
|
+
findMatchingOpsKeys,
|
|
18
|
+
resolveProfileKeyForLogical
|
|
19
|
+
} = require('./capability-resolve');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {unknown} capabilities
|
|
23
|
+
* @param {string} logicalName
|
|
24
|
+
* @returns {number} Index or -1
|
|
25
|
+
*/
|
|
26
|
+
function findCapabilityIndex(capabilities, logicalName) {
|
|
27
|
+
if (!Array.isArray(capabilities)) {
|
|
28
|
+
return -1;
|
|
29
|
+
}
|
|
30
|
+
return capabilities.findIndex(
|
|
31
|
+
(c) => String(c).toLowerCase() === String(logicalName).toLowerCase()
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Planned RFC 6902 remove ops against `originalDoc` (before mutation). Paths use actual
|
|
37
|
+
* openapi/CIP object keys and scenario array indices (descending order for stability).
|
|
38
|
+
*
|
|
39
|
+
* @param {object} originalDoc
|
|
40
|
+
* @param {string} logicalName
|
|
41
|
+
* @param {string|undefined} openapiKey
|
|
42
|
+
* @param {string|undefined} cipKey
|
|
43
|
+
* @param {string|null} profileKey
|
|
44
|
+
* @returns {object[]}
|
|
45
|
+
*/
|
|
46
|
+
function buildRemovePatchOperations(originalDoc, logicalName, openapiKey, cipKey, profileKey) {
|
|
47
|
+
const ops = [];
|
|
48
|
+
|
|
49
|
+
const capIdx = findCapabilityIndex(originalDoc.capabilities, logicalName);
|
|
50
|
+
if (capIdx >= 0) {
|
|
51
|
+
ops.push({ op: 'remove', path: `/capabilities/${capIdx}` });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const ooPath = openapiKey ?? logicalName;
|
|
55
|
+
if (originalDoc.openapi?.operations?.[ooPath] !== undefined) {
|
|
56
|
+
ops.push({ op: 'remove', path: jsonPointerPath('openapi', 'operations', ooPath) });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const cipPath = cipKey ?? logicalName;
|
|
60
|
+
if (originalDoc.execution?.cip?.operations?.[cipPath] !== undefined) {
|
|
61
|
+
ops.push({ op: 'remove', path: jsonPointerPath('execution', 'cip', 'operations', cipPath) });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
profileKey !== null &&
|
|
66
|
+
profileKey !== undefined &&
|
|
67
|
+
originalDoc.exposed?.profiles?.[profileKey] !== undefined
|
|
68
|
+
) {
|
|
69
|
+
ops.push({ op: 'remove', path: jsonPointerPath('exposed', 'profiles', profileKey) });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const aliases = new Set(
|
|
73
|
+
[logicalName, openapiKey, cipKey].filter((x) => x !== undefined && x !== null && x !== '')
|
|
74
|
+
);
|
|
75
|
+
pushScenarioRemovePatches(originalDoc, aliases, ops);
|
|
76
|
+
|
|
77
|
+
return ops;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* @param {object} originalDoc
|
|
82
|
+
* @param {Set<string>} aliases
|
|
83
|
+
* @param {object[]} ops
|
|
84
|
+
* @returns {void}
|
|
85
|
+
*/
|
|
86
|
+
function pushScenarioRemovePatches(originalDoc, aliases, ops) {
|
|
87
|
+
const scenarios = originalDoc.testPayload?.scenarios;
|
|
88
|
+
if (!Array.isArray(scenarios)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const idxs = [];
|
|
92
|
+
scenarios.forEach((s, i) => {
|
|
93
|
+
if (s && aliases.has(s.operation)) {
|
|
94
|
+
idxs.push(i);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
idxs.sort((a, b) => b - a);
|
|
98
|
+
for (const i of idxs) {
|
|
99
|
+
ops.push({ op: 'remove', path: jsonPointerPath('testPayload', 'scenarios', String(i)) });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Drop testPayload.scenarios entries whose `operation` matches any alias (logical + openapi/cip keys).
|
|
105
|
+
*
|
|
106
|
+
* @param {object} doc - Mutated datasource
|
|
107
|
+
* @param {string} logicalKey - Canonical capability name
|
|
108
|
+
* @param {string|undefined} openapiKey - openapi.operations key removed
|
|
109
|
+
* @param {string|undefined} cipKey - cip.operations key removed
|
|
110
|
+
* @returns {boolean} True if the scenarios array changed
|
|
111
|
+
*/
|
|
112
|
+
function pruneTestPayloadScenarios(doc, logicalKey, openapiKey, cipKey) {
|
|
113
|
+
const scenarios = doc.testPayload?.scenarios;
|
|
114
|
+
if (!Array.isArray(scenarios)) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const aliases = new Set(
|
|
118
|
+
[logicalKey, openapiKey, cipKey].filter((x) => x !== undefined && x !== null && x !== '')
|
|
119
|
+
);
|
|
120
|
+
const next = scenarios.filter((s) => !s || !aliases.has(s.operation));
|
|
121
|
+
if (next.length === scenarios.length) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
doc.testPayload.scenarios = next;
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Drop empty `testPayload.scenarios`, then remove `testPayload` when it becomes `{}`.
|
|
130
|
+
*
|
|
131
|
+
* @param {object} doc - Mutated datasource
|
|
132
|
+
* @returns {boolean} True if something was deleted from the tree
|
|
133
|
+
*/
|
|
134
|
+
function finalizeTestPayloadShape(doc) {
|
|
135
|
+
let changed = false;
|
|
136
|
+
const tp = doc.testPayload;
|
|
137
|
+
if (!tp || typeof tp !== 'object') {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
if (Array.isArray(tp.scenarios) && tp.scenarios.length === 0) {
|
|
141
|
+
delete tp.scenarios;
|
|
142
|
+
changed = true;
|
|
143
|
+
}
|
|
144
|
+
if (Object.keys(tp).length === 0) {
|
|
145
|
+
delete doc.testPayload;
|
|
146
|
+
changed = true;
|
|
147
|
+
}
|
|
148
|
+
return changed;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Remove `exposed.profiles` when it is an empty object after profile deletion.
|
|
153
|
+
*
|
|
154
|
+
* @param {object} doc - Mutated datasource
|
|
155
|
+
* @returns {boolean} True if `exposed.profiles` was removed
|
|
156
|
+
*/
|
|
157
|
+
function finalizeEmptyExposedProfiles(doc) {
|
|
158
|
+
if (!doc.exposed || typeof doc.exposed !== 'object') {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const prof = doc.exposed.profiles;
|
|
162
|
+
if (!prof || typeof prof !== 'object' || Object.keys(prof).length > 0) {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
delete doc.exposed.profiles;
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Mutate doc after capability slices removed: scenarios, empty containers, summary lines.
|
|
171
|
+
*
|
|
172
|
+
* @param {object} doc - Cloned datasource (mutated)
|
|
173
|
+
* @param {object} originalDoc - Original parsed JSON (for testPayload diff only)
|
|
174
|
+
* @param {string} logicalName
|
|
175
|
+
* @param {{ openapiKey?: string, cipKey?: string }} removedKeys - Actual openapi/CIP object keys removed
|
|
176
|
+
* @param {string[]} updatedSections - Human-readable sections changed
|
|
177
|
+
* @returns {void}
|
|
178
|
+
*/
|
|
179
|
+
function applyCapabilityRemoveSideEffects(
|
|
180
|
+
doc,
|
|
181
|
+
originalDoc,
|
|
182
|
+
logicalName,
|
|
183
|
+
removedKeys,
|
|
184
|
+
updatedSections
|
|
185
|
+
) {
|
|
186
|
+
const openapiKey = removedKeys?.openapiKey;
|
|
187
|
+
const cipKey = removedKeys?.cipKey;
|
|
188
|
+
const testPayloadBeforeJson = JSON.stringify(originalDoc.testPayload);
|
|
189
|
+
pruneTestPayloadScenarios(doc, logicalName, openapiKey, cipKey);
|
|
190
|
+
finalizeTestPayloadShape(doc);
|
|
191
|
+
|
|
192
|
+
const ooPath = openapiKey ?? logicalName;
|
|
193
|
+
const cipPath = cipKey ?? logicalName;
|
|
194
|
+
updatedSections.push(
|
|
195
|
+
'removed: capabilities / openapi.operations.' +
|
|
196
|
+
ooPath +
|
|
197
|
+
' / execution.cip.operations.' +
|
|
198
|
+
cipPath,
|
|
199
|
+
'exposed.profiles / testPayload.scenarios (see JSON Patch paths)'
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (finalizeEmptyExposedProfiles(doc)) {
|
|
203
|
+
updatedSections.push('exposed.profiles (removed empty object)');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (testPayloadBeforeJson !== JSON.stringify(doc.testPayload)) {
|
|
207
|
+
updatedSections.push('testPayload');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Apply capability removal on a deep clone of the document.
|
|
213
|
+
*
|
|
214
|
+
* @param {object} originalDoc - Parsed datasource JSON
|
|
215
|
+
* @param {object} opts - Options
|
|
216
|
+
* @param {string} opts.capability - Capability key to remove
|
|
217
|
+
* @param {boolean} [opts.force=false] - If true and capability absent, return unchanged doc
|
|
218
|
+
* @returns {{
|
|
219
|
+
* doc: object,
|
|
220
|
+
* removed: boolean,
|
|
221
|
+
* patchOperations: object[],
|
|
222
|
+
* updatedSections: string[]
|
|
223
|
+
* }}
|
|
224
|
+
*/
|
|
225
|
+
function applyCapabilityRemove(originalDoc, opts) {
|
|
226
|
+
const doc = deepClone(originalDoc);
|
|
227
|
+
const name = opts.capability;
|
|
228
|
+
const updatedSections = [];
|
|
229
|
+
|
|
230
|
+
if (!capabilityExists(doc, name)) {
|
|
231
|
+
if (!opts.force) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`Capability "${name}" not found in capabilities[], openapi.operations, or execution.cip.operations. ` +
|
|
234
|
+
'Use --force to succeed when the capability is already absent.'
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
doc,
|
|
239
|
+
removed: false,
|
|
240
|
+
patchOperations: [],
|
|
241
|
+
updatedSections: ['capability already absent (--force)']
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const logicalName = resolveLogicalNameForRemove(doc, name);
|
|
246
|
+
const openapiKey = findMatchingOpsKeys(originalDoc.openapi?.operations, logicalName)[0];
|
|
247
|
+
const cipKey = findMatchingOpsKeys(originalDoc.execution?.cip?.operations, logicalName)[0];
|
|
248
|
+
const profileKey = resolveProfileKeyForLogical(originalDoc, logicalName);
|
|
249
|
+
|
|
250
|
+
const patchOperations = buildRemovePatchOperations(
|
|
251
|
+
originalDoc,
|
|
252
|
+
logicalName,
|
|
253
|
+
openapiKey,
|
|
254
|
+
cipKey,
|
|
255
|
+
profileKey
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
removeCapability(doc, logicalName);
|
|
259
|
+
applyCapabilityRemoveSideEffects(doc, originalDoc, logicalName, { openapiKey, cipKey }, updatedSections);
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
doc,
|
|
263
|
+
removed: true,
|
|
264
|
+
patchOperations,
|
|
265
|
+
updatedSections
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = {
|
|
270
|
+
applyCapabilityRemove,
|
|
271
|
+
buildRemovePatchOperations,
|
|
272
|
+
pruneTestPayloadScenarios,
|
|
273
|
+
finalizeTestPayloadShape,
|
|
274
|
+
finalizeEmptyExposedProfiles
|
|
275
|
+
};
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backed capability copy with validation, backup, and atomic write.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Run datasource capability copy CLI operation
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { writeBackup } = require('../../utils/integration-file-backup');
|
|
12
|
+
const {
|
|
13
|
+
resolveValidateInputPath,
|
|
14
|
+
validateDatasourceParsed
|
|
15
|
+
} = require('../validate');
|
|
16
|
+
const { normalizeCapabilityKey } = require('./capability-key');
|
|
17
|
+
const { applyCapabilityCreate } = require('./create-operations');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve an existing JSON file path using real disk (`node:fs`). Other suites often
|
|
21
|
+
* `jest.mock('fs')`; `require('fs')` would see the mock and break existsSync/read here.
|
|
22
|
+
* @param {string} identifier - Trimmed path or datasource key
|
|
23
|
+
* @returns {string} Absolute path to file
|
|
24
|
+
*/
|
|
25
|
+
function resolveCapabilityCopyInputPath(identifier) {
|
|
26
|
+
const trimmed = String(identifier || '').trim();
|
|
27
|
+
if (!trimmed) {
|
|
28
|
+
throw new Error('File path is required and must be a string');
|
|
29
|
+
}
|
|
30
|
+
const candidates = [...new Set([trimmed, path.resolve(trimmed)])];
|
|
31
|
+
for (const p of candidates) {
|
|
32
|
+
try {
|
|
33
|
+
if (!fs.existsSync(p)) continue;
|
|
34
|
+
try {
|
|
35
|
+
const st = fs.statSync(p);
|
|
36
|
+
if (st && typeof st.isFile === 'function' && st.isFile()) {
|
|
37
|
+
return path.resolve(p);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
return path.resolve(p);
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
/* ignore */
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return resolveValidateInputPath(trimmed);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} filePath
|
|
51
|
+
* @param {object} obj
|
|
52
|
+
* @returns {void}
|
|
53
|
+
*/
|
|
54
|
+
function atomicWriteJson(filePath, obj) {
|
|
55
|
+
const dir = path.dirname(filePath);
|
|
56
|
+
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
|
|
57
|
+
const payload = `${JSON.stringify(obj, null, 2)}\n`;
|
|
58
|
+
fs.writeFileSync(tmp, payload, 'utf8');
|
|
59
|
+
fs.renameSync(tmp, filePath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @typedef {object} RunCapabilityCopyOpts
|
|
64
|
+
* @property {string} fileOrKey - Path or datasource key
|
|
65
|
+
* @property {string} from
|
|
66
|
+
* @property {string} as - Target capability key
|
|
67
|
+
* @property {boolean} [dryRun=false]
|
|
68
|
+
* @property {boolean} [overwrite=false]
|
|
69
|
+
* @property {boolean} [noBackup=false]
|
|
70
|
+
* @property {boolean} [basicExposure=false] - Programmatic only: minimal exposed.profiles from metadataSchema (CLI always passes false)
|
|
71
|
+
* @property {boolean} [includeTestPayload=false] - Also clone testPayload.scenarios rows matching the source operation (`--test` on CLI)
|
|
72
|
+
* @property {string} [openApiOperationId] - Create without --from: match openapi.operations[].operationId
|
|
73
|
+
* @property {string} [template] - Create without --from: template name under capability/templates/
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Execute capability copy on disk (or dry-run).
|
|
78
|
+
*
|
|
79
|
+
* @param {RunCapabilityCopyOpts} opts
|
|
80
|
+
* @returns {Promise<{
|
|
81
|
+
* dryRun: boolean,
|
|
82
|
+
* resolvedPath: string,
|
|
83
|
+
* resolvedAs: string,
|
|
84
|
+
* patchOperations: object[],
|
|
85
|
+
* updatedSections: string[],
|
|
86
|
+
* backupPath: string|null,
|
|
87
|
+
* validation: object
|
|
88
|
+
* }>}
|
|
89
|
+
*/
|
|
90
|
+
async function runCapabilityCopy(opts) {
|
|
91
|
+
const resolvedPath = resolveCapabilityCopyInputPath(opts.fileOrKey.trim());
|
|
92
|
+
const raw = fs.readFileSync(resolvedPath, 'utf8');
|
|
93
|
+
const parsed = JSON.parse(raw);
|
|
94
|
+
|
|
95
|
+
const to = normalizeCapabilityKey(opts.as, 'Target (--as)');
|
|
96
|
+
const fromRaw = opts.from !== undefined && opts.from !== null ? String(opts.from).trim() : '';
|
|
97
|
+
const from = fromRaw ? normalizeCapabilityKey(fromRaw, 'Source (--from)') : '';
|
|
98
|
+
const oidRaw =
|
|
99
|
+
opts.openApiOperationId !== undefined && opts.openApiOperationId !== null
|
|
100
|
+
? String(opts.openApiOperationId).trim()
|
|
101
|
+
: '';
|
|
102
|
+
const tplRaw =
|
|
103
|
+
opts.template !== undefined && opts.template !== null ? String(opts.template).trim() : '';
|
|
104
|
+
|
|
105
|
+
const result = applyCapabilityCreate(parsed, {
|
|
106
|
+
from: from || undefined,
|
|
107
|
+
to,
|
|
108
|
+
openApiOperationId: oidRaw || undefined,
|
|
109
|
+
template: tplRaw || undefined,
|
|
110
|
+
overwrite: Boolean(opts.overwrite),
|
|
111
|
+
basicExposure: Boolean(opts.basicExposure),
|
|
112
|
+
includeTestPayload: Boolean(opts.includeTestPayload)
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const validation = validateDatasourceParsed(result.doc);
|
|
116
|
+
if (!validation.valid) {
|
|
117
|
+
const err = new Error(validation.errors.join('\n'));
|
|
118
|
+
err.validationErrors = validation.errors;
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (opts.dryRun) {
|
|
123
|
+
return {
|
|
124
|
+
dryRun: true,
|
|
125
|
+
resolvedPath,
|
|
126
|
+
resolvedAs: result.resolvedAs,
|
|
127
|
+
patchOperations: result.patchOperations,
|
|
128
|
+
updatedSections: result.updatedSections,
|
|
129
|
+
backupPath: null,
|
|
130
|
+
validation
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const backupPath = writeBackup(resolvedPath, Boolean(opts.noBackup));
|
|
135
|
+
atomicWriteJson(resolvedPath, result.doc);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
dryRun: false,
|
|
139
|
+
resolvedPath,
|
|
140
|
+
resolvedAs: result.resolvedAs,
|
|
141
|
+
patchOperations: result.patchOperations,
|
|
142
|
+
updatedSections: result.updatedSections,
|
|
143
|
+
backupPath,
|
|
144
|
+
validation
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
runCapabilityCopy,
|
|
150
|
+
writeBackup,
|
|
151
|
+
atomicWriteJson
|
|
152
|
+
};
|