@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 dimension`.
|
|
3
|
+
*
|
|
4
|
+
* Pure validation module: no CLI/auth logic.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview dimension binding semantic validator
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
function _asObject(x) {
|
|
14
|
+
return x && typeof x === 'object' && !Array.isArray(x) ? x : null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function _getMetadataProp(doc, fieldName) {
|
|
18
|
+
const props = doc?.metadataSchema?.properties;
|
|
19
|
+
if (!props || typeof props !== 'object') return null;
|
|
20
|
+
const node = props[fieldName];
|
|
21
|
+
return _asObject(node);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function _collectForeignKeyNameSet(doc) {
|
|
25
|
+
const fks = Array.isArray(doc?.foreignKeys) ? doc.foreignKeys : [];
|
|
26
|
+
const out = new Set();
|
|
27
|
+
for (const fk of fks) {
|
|
28
|
+
const name = typeof fk?.name === 'string' ? fk.name.trim() : '';
|
|
29
|
+
if (name) out.add(name);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function _findForeignKeyRow(doc, name) {
|
|
35
|
+
const fks = Array.isArray(doc?.foreignKeys) ? doc.foreignKeys : [];
|
|
36
|
+
return fks.find((fk) => fk && fk.name === name) || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* @typedef {Object} DimensionVia
|
|
41
|
+
* @property {string} fk
|
|
42
|
+
* @property {string} dimension
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* @typedef {Object} DimensionLocalContext
|
|
47
|
+
* @property {any} sourceDoc
|
|
48
|
+
* @property {string} dimensionKey
|
|
49
|
+
* @property {'local'|'fk'} type
|
|
50
|
+
* @property {string|undefined} field
|
|
51
|
+
* @property {DimensionVia[]|undefined} via
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {Object} DimensionValidationInput
|
|
56
|
+
* @property {DimensionLocalContext} localContext
|
|
57
|
+
* @property {Record<string, any>|null} remoteTargetsByKey - map: datasourceKey -> datasource config
|
|
58
|
+
* @property {Set<string>|null} catalogDimensionKeys - dimension catalog keys when available
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef {Object} DimensionValidationResult
|
|
63
|
+
* @property {boolean} ok
|
|
64
|
+
* @property {string[]} errors
|
|
65
|
+
* @property {string[]} warnings
|
|
66
|
+
* @property {{ targetDatasourceKeys: string[] }} resolved
|
|
67
|
+
*/
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {DimensionValidationInput} input
|
|
71
|
+
* @returns {DimensionValidationResult}
|
|
72
|
+
*/
|
|
73
|
+
function validateDimensionSemantics(input) {
|
|
74
|
+
const errors = [];
|
|
75
|
+
const warnings = [];
|
|
76
|
+
const ctx = _normalizeInputOrReport(input, errors);
|
|
77
|
+
if (!ctx) return { ok: false, errors, warnings, resolved: { targetDatasourceKeys: [] } };
|
|
78
|
+
|
|
79
|
+
_validateCatalogKey({ input, dimensionKey: ctx.dimensionKey, errors });
|
|
80
|
+
|
|
81
|
+
if (ctx.type === 'local') {
|
|
82
|
+
_validateLocalBinding({ source: ctx.sourceDoc, field: ctx.field, errors });
|
|
83
|
+
return { ok: errors.length === 0, errors, warnings, resolved: { targetDatasourceKeys: [] } };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { targetDatasourceKeys } = _validateFkBinding({
|
|
87
|
+
source: ctx.sourceDoc,
|
|
88
|
+
via: ctx.via,
|
|
89
|
+
remoteTargetsByKey: input?.remoteTargetsByKey,
|
|
90
|
+
errors,
|
|
91
|
+
warnings
|
|
92
|
+
});
|
|
93
|
+
return { ok: errors.length === 0, errors, warnings, resolved: { targetDatasourceKeys } };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function _normalizeInputOrReport(input, errors) {
|
|
97
|
+
const local = input?.localContext;
|
|
98
|
+
const sourceDoc = local?.sourceDoc;
|
|
99
|
+
if (!_asObject(sourceDoc)) {
|
|
100
|
+
errors.push('Local validation failed: sourceDoc missing or invalid.');
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const dimensionKey = String(local?.dimensionKey || '').trim();
|
|
104
|
+
if (!dimensionKey) {
|
|
105
|
+
errors.push('Local validation failed: dimensionKey missing or invalid.');
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
const type = local?.type;
|
|
109
|
+
if (type !== 'local' && type !== 'fk') {
|
|
110
|
+
errors.push('Local validation failed: type must be "local" or "fk".');
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
sourceDoc,
|
|
115
|
+
dimensionKey,
|
|
116
|
+
type,
|
|
117
|
+
field: local?.field,
|
|
118
|
+
via: local?.via
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _validateCatalogKey({ input, dimensionKey, errors }) {
|
|
123
|
+
if (input?.catalogDimensionKeys instanceof Set) {
|
|
124
|
+
if (!input.catalogDimensionKeys.has(dimensionKey)) {
|
|
125
|
+
errors.push(`Unknown dimension key (not found in dimension catalog): ${dimensionKey}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function _validateLocalBinding({ source, field, errors }) {
|
|
131
|
+
const f = String(field || '').trim();
|
|
132
|
+
if (!f) {
|
|
133
|
+
errors.push('Local validation failed: --field is required for type=local.');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const node = _getMetadataProp(source, f);
|
|
137
|
+
if (!node) {
|
|
138
|
+
errors.push(`Source field not found in metadataSchema.properties: ${f}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function _validateFkBinding({ source, via, remoteTargetsByKey, errors, warnings }) {
|
|
143
|
+
const hops = Array.isArray(via) ? via : [];
|
|
144
|
+
if (hops.length === 0) {
|
|
145
|
+
errors.push('Local validation failed: at least one via entry is required for type=fk.');
|
|
146
|
+
return { targetDatasourceKeys: [] };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const fkNameSet = _collectForeignKeyNameSet(source);
|
|
150
|
+
const targets = _asObject(remoteTargetsByKey) ? remoteTargetsByKey : {};
|
|
151
|
+
const targetDatasourceKeys = [];
|
|
152
|
+
|
|
153
|
+
hops.forEach((hop) => {
|
|
154
|
+
const resolved = _validateViaHop({
|
|
155
|
+
source,
|
|
156
|
+
fkNameSet,
|
|
157
|
+
hop,
|
|
158
|
+
errors,
|
|
159
|
+
warnings
|
|
160
|
+
});
|
|
161
|
+
if (!resolved) return;
|
|
162
|
+
targetDatasourceKeys.push(resolved.targetDatasource);
|
|
163
|
+
_validateTargetDimensionIfAvailable({
|
|
164
|
+
targets,
|
|
165
|
+
targetDatasource: resolved.targetDatasource,
|
|
166
|
+
targetDimKey: resolved.targetDimKey,
|
|
167
|
+
errors
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return { targetDatasourceKeys };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function _validateViaHop({ source, fkNameSet, hop, errors, warnings }) {
|
|
175
|
+
const fkName = String(hop?.fk || '').trim();
|
|
176
|
+
const targetDimKey = String(hop?.dimension || '').trim();
|
|
177
|
+
if (!fkName) {
|
|
178
|
+
errors.push('via[].fk must be a non-empty string.');
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
if (!targetDimKey) {
|
|
182
|
+
errors.push('via[].dimension must be a non-empty string.');
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
if (!/^[a-zA-Z0-9_]+$/.test(targetDimKey)) {
|
|
186
|
+
errors.push(`via[].dimension "${targetDimKey}" must match ^[a-zA-Z0-9_]+$`);
|
|
187
|
+
}
|
|
188
|
+
if (!fkNameSet.has(fkName)) {
|
|
189
|
+
errors.push(`Foreign key not found in foreignKeys[].name: ${fkName}`);
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const targetDatasource = _resolveTargetDatasourceFromFk(source, fkName);
|
|
193
|
+
if (!targetDatasource) {
|
|
194
|
+
warnings.push(`Foreign key "${fkName}" has no targetDatasource; remote validation skipped for this hop.`);
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
return { targetDatasource, targetDimKey };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function _resolveTargetDatasourceFromFk(source, fkName) {
|
|
201
|
+
const fkRow = _findForeignKeyRow(source, fkName);
|
|
202
|
+
const targetDatasource = typeof fkRow?.targetDatasource === 'string' ? fkRow.targetDatasource.trim() : '';
|
|
203
|
+
return targetDatasource || '';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function _validateTargetDimensionIfAvailable({ targets, targetDatasource, targetDimKey, errors }) {
|
|
207
|
+
const targetCfg = targets[targetDatasource];
|
|
208
|
+
if (!targetCfg) return;
|
|
209
|
+
const dims = targetCfg?.dimensions;
|
|
210
|
+
const okDims = dims && typeof dims === 'object' && !Array.isArray(dims);
|
|
211
|
+
if (!okDims || !(targetDimKey in dims)) {
|
|
212
|
+
errors.push(`Target dimension not found: ${targetDatasource}.dimensions.${targetDimKey}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
validateDimensionSemantics
|
|
218
|
+
};
|
|
219
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSON Pointer segment escaping for RFC 6902 paths.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview JSON Pointer helpers
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {string} token - Unescaped segment
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
function escapeJsonPointerToken(token) {
|
|
14
|
+
return String(token).replace(/~/g, '~0').replace(/\//g, '~1');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {...string} segments
|
|
19
|
+
* @returns {string} Path starting with /
|
|
20
|
+
*/
|
|
21
|
+
function jsonPointerPath(...segments) {
|
|
22
|
+
if (segments.length === 0) {
|
|
23
|
+
return '';
|
|
24
|
+
}
|
|
25
|
+
return `/${segments.map(escapeJsonPointerToken).join('/')}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
escapeJsonPointerToken,
|
|
30
|
+
jsonPointerPath
|
|
31
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rewrite intra-document capability references when cloning an operation.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Structured reference rewrite (no blind string replace)
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Keys whose string values may reference another capability name */
|
|
10
|
+
const REFERENCE_KEYS = new Set([
|
|
11
|
+
'openapiRef',
|
|
12
|
+
'operation',
|
|
13
|
+
'operationRef',
|
|
14
|
+
'capability',
|
|
15
|
+
'dependsOn',
|
|
16
|
+
'sourceOperation'
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Deep-walk `node` and replace reference string values `from` → `to`.
|
|
21
|
+
*
|
|
22
|
+
* @param {unknown} node - JSON subtree (openapi operation or CIP operation)
|
|
23
|
+
* @param {string} from - Source capability key
|
|
24
|
+
* @param {string} to - Target capability key
|
|
25
|
+
* @returns {void}
|
|
26
|
+
*/
|
|
27
|
+
function rewriteCapabilityReferences(node, from, to) {
|
|
28
|
+
if (node === null || node === undefined) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
if (Array.isArray(node)) {
|
|
32
|
+
node.forEach((item) => rewriteCapabilityReferences(item, from, to));
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (typeof node !== 'object') {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
for (const key of Object.keys(node)) {
|
|
39
|
+
const val = node[key];
|
|
40
|
+
if (REFERENCE_KEYS.has(key) && val === from) {
|
|
41
|
+
node[key] = to;
|
|
42
|
+
} else if (typeof val === 'object') {
|
|
43
|
+
rewriteCapabilityReferences(val, from, to);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = {
|
|
49
|
+
rewriteCapabilityReferences,
|
|
50
|
+
REFERENCE_KEYS
|
|
51
|
+
};
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Metadata-only foreign key updates for datasource JSON (plan 132 Phase 4).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview capability relate — foreignKeys[] (+ optional metadataSchema property)
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { jsonPointerPath } = require('./json-pointer');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @param {unknown} o
|
|
13
|
+
* @returns {unknown}
|
|
14
|
+
*/
|
|
15
|
+
function deepClone(o) {
|
|
16
|
+
return JSON.parse(JSON.stringify(o));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function _asObject(x) {
|
|
20
|
+
return x && typeof x === 'object' ? x : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function _uniqStrings(list) {
|
|
24
|
+
const out = [];
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
for (const x of list || []) {
|
|
27
|
+
const s = String(x || '').trim();
|
|
28
|
+
if (!s || seen.has(s)) continue;
|
|
29
|
+
seen.add(s);
|
|
30
|
+
out.push(s);
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function _getTargetMetaProps(targetDoc) {
|
|
36
|
+
const props = targetDoc?.metadataSchema?.properties;
|
|
37
|
+
return props && typeof props === 'object' ? props : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function _buildRelationMetadataSchema({ relationName, targetDoc, resolvedTargetFields }) {
|
|
41
|
+
// Fallback to a minimal stub when we cannot derive target metadata.
|
|
42
|
+
const targetProps = _getTargetMetaProps(targetDoc);
|
|
43
|
+
if (!targetProps) {
|
|
44
|
+
return {
|
|
45
|
+
type: 'object',
|
|
46
|
+
nullable: true,
|
|
47
|
+
description: `Resolved relation via FK "${relationName}"`
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const include = _uniqStrings([
|
|
52
|
+
...(Array.isArray(resolvedTargetFields) ? resolvedTargetFields : []),
|
|
53
|
+
...(Array.isArray(targetDoc?.primaryKey) ? targetDoc.primaryKey : []),
|
|
54
|
+
...(Array.isArray(targetDoc?.labelKey) ? targetDoc.labelKey : [])
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/** @type {Record<string, any>} */
|
|
58
|
+
const properties = {};
|
|
59
|
+
for (const f of include) {
|
|
60
|
+
const node = targetProps[f];
|
|
61
|
+
if (_asObject(node)) {
|
|
62
|
+
properties[f] = deepClone(node);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const requiredFromTarget = Array.isArray(targetDoc?.metadataSchema?.required) ? targetDoc.metadataSchema.required : [];
|
|
67
|
+
const required = requiredFromTarget.filter((x) => include.includes(String(x)));
|
|
68
|
+
|
|
69
|
+
const out = {
|
|
70
|
+
type: 'object',
|
|
71
|
+
nullable: true,
|
|
72
|
+
description: `Resolved relation via FK "${relationName}"`,
|
|
73
|
+
properties
|
|
74
|
+
};
|
|
75
|
+
if (required.length > 0) {
|
|
76
|
+
out.required = required;
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* FK `name` must match schema: ^[a-z][a-zA-Z0-9]*$
|
|
83
|
+
*
|
|
84
|
+
* @param {string} raw
|
|
85
|
+
* @returns {string}
|
|
86
|
+
*/
|
|
87
|
+
function normalizeRelationName(raw) {
|
|
88
|
+
const s = String(raw || '').trim();
|
|
89
|
+
if (!s) {
|
|
90
|
+
throw new Error('--relation-name is required');
|
|
91
|
+
}
|
|
92
|
+
if (!/^[a-z][a-zA-Z0-9]*$/.test(s)) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`--relation-name "${s}" must match ^[a-z][a-zA-Z0-9]*$ (camelCase)`
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return s;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} raw
|
|
102
|
+
* @returns {string}
|
|
103
|
+
*/
|
|
104
|
+
function normalizeTargetDatasource(raw) {
|
|
105
|
+
const s = String(raw || '').trim();
|
|
106
|
+
if (!s) {
|
|
107
|
+
throw new Error('--to <targetDatasource> is required');
|
|
108
|
+
}
|
|
109
|
+
if (!/^[a-z0-9-]+$/.test(s)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`--to "${s}" must match targetDatasource pattern (lowercase letters, digits, hyphens)`
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
return s;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* @param {object} d - Mutated doc clone
|
|
119
|
+
* @param {object} fkRow
|
|
120
|
+
* @param {string} relationName
|
|
121
|
+
* @param {object[]} patchOperations
|
|
122
|
+
* @param {string[]} updatedSections
|
|
123
|
+
* @returns {boolean} replaced existing row
|
|
124
|
+
*/
|
|
125
|
+
function upsertForeignKeyRow(d, fkRow, relationName, patchOperations, updatedSections) {
|
|
126
|
+
const idx = d.foreignKeys.findIndex((fk) => fk && fk.name === relationName);
|
|
127
|
+
if (idx >= 0) {
|
|
128
|
+
d.foreignKeys[idx] = fkRow;
|
|
129
|
+
patchOperations.push({
|
|
130
|
+
op: 'replace',
|
|
131
|
+
path: `/foreignKeys/${idx}`,
|
|
132
|
+
value: fkRow
|
|
133
|
+
});
|
|
134
|
+
updatedSections.push(`foreignKeys.${relationName} → ${fkRow.targetDatasource}`);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
d.foreignKeys.push(fkRow);
|
|
138
|
+
patchOperations.push({
|
|
139
|
+
op: 'add',
|
|
140
|
+
path: '/foreignKeys/-',
|
|
141
|
+
value: fkRow
|
|
142
|
+
});
|
|
143
|
+
updatedSections.push(`foreignKeys.${relationName} → ${fkRow.targetDatasource}`);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _buildDefaultFkDescription({ fields, targetDatasource, resolvedTargetFields }) {
|
|
148
|
+
const left = Array.isArray(fields) ? fields.join(',') : String(fields || '');
|
|
149
|
+
const tf = Array.isArray(resolvedTargetFields) && resolvedTargetFields.length > 0 ? resolvedTargetFields : null;
|
|
150
|
+
const right = tf ? `${targetDatasource}.${tf.join(',')}` : targetDatasource;
|
|
151
|
+
return `Foreign key join: ${left} → ${right}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* @param {object} d
|
|
156
|
+
* @param {string} name
|
|
157
|
+
* @param {object[]} patchOperations
|
|
158
|
+
* @param {string[]} updatedSections
|
|
159
|
+
* @returns {void}
|
|
160
|
+
*/
|
|
161
|
+
function addMetadataPropertyStub(d, name, patchOperations, updatedSections, options = {}) {
|
|
162
|
+
if (!d.metadataSchema) {
|
|
163
|
+
d.metadataSchema = { type: 'object', properties: {} };
|
|
164
|
+
}
|
|
165
|
+
if (!d.metadataSchema.properties) {
|
|
166
|
+
d.metadataSchema.properties = {};
|
|
167
|
+
}
|
|
168
|
+
const exists = Boolean(d.metadataSchema.properties[name]);
|
|
169
|
+
|
|
170
|
+
const metaBlock = _buildRelationMetadataSchema({
|
|
171
|
+
relationName: name,
|
|
172
|
+
targetDoc: options.targetDoc || null,
|
|
173
|
+
resolvedTargetFields: options.resolvedTargetFields || null
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (exists) {
|
|
177
|
+
// On overwrite, keep any existing description to avoid wiping user-authored docs.
|
|
178
|
+
const existingDesc = d.metadataSchema.properties[name]?.description;
|
|
179
|
+
if (existingDesc) {
|
|
180
|
+
metaBlock.description = existingDesc;
|
|
181
|
+
}
|
|
182
|
+
d.metadataSchema.properties[name] = metaBlock;
|
|
183
|
+
patchOperations.push({
|
|
184
|
+
op: 'replace',
|
|
185
|
+
path: jsonPointerPath('metadataSchema', 'properties', name),
|
|
186
|
+
value: metaBlock
|
|
187
|
+
});
|
|
188
|
+
updatedSections.push(`metadataSchema.properties.${name}`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
d.metadataSchema.properties[name] = metaBlock;
|
|
193
|
+
patchOperations.push({
|
|
194
|
+
op: 'add',
|
|
195
|
+
path: jsonPointerPath('metadataSchema', 'properties', name),
|
|
196
|
+
value: metaBlock
|
|
197
|
+
});
|
|
198
|
+
updatedSections.push(`metadataSchema.properties.${name}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {object} opts
|
|
203
|
+
* @returns {{ fields: string[], targetFields: string[] | undefined }}
|
|
204
|
+
*/
|
|
205
|
+
function parseRelateFields(opts) {
|
|
206
|
+
const fields = Array.isArray(opts.fields)
|
|
207
|
+
? opts.fields.map((f) => String(f).trim()).filter(Boolean)
|
|
208
|
+
: [];
|
|
209
|
+
if (fields.length === 0) {
|
|
210
|
+
throw new Error('Provide at least one --field <name>');
|
|
211
|
+
}
|
|
212
|
+
const targetFields =
|
|
213
|
+
Array.isArray(opts.targetFields) && opts.targetFields.length > 0
|
|
214
|
+
? opts.targetFields.map((f) => String(f).trim()).filter(Boolean)
|
|
215
|
+
: undefined;
|
|
216
|
+
return { fields, targetFields };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* @param {object} d - mutated clone
|
|
221
|
+
* @param {string} name
|
|
222
|
+
* @param {boolean} overwrite
|
|
223
|
+
* @returns {boolean} whether an existing row was found
|
|
224
|
+
*/
|
|
225
|
+
function assertOverwriteAllowedAndGetExisting(d, name, overwrite) {
|
|
226
|
+
const idx0 = d.foreignKeys.findIndex((fk) => fk && fk.name === name);
|
|
227
|
+
if (idx0 >= 0 && !overwrite) {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`foreignKeys[].name "${name}" already exists; pass --overwrite to replace`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
return idx0 >= 0;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* @param {string} name
|
|
237
|
+
* @param {string[]} fields
|
|
238
|
+
* @param {string} targetDatasource
|
|
239
|
+
* @param {string[] | undefined} targetFields
|
|
240
|
+
* @param {object} [options]
|
|
241
|
+
* @param {object|null} [options.existingRow]
|
|
242
|
+
* @param {boolean|undefined} [options.required]
|
|
243
|
+
* @param {string|undefined} [options.description]
|
|
244
|
+
* @param {string[]|undefined|null} [options.resolvedTargetFields]
|
|
245
|
+
* @returns {object}
|
|
246
|
+
*/
|
|
247
|
+
function buildForeignKeyRow(name, fields, targetDatasource, targetFields, options = {}) {
|
|
248
|
+
const fkRow = { name, fields, targetDatasource };
|
|
249
|
+
if (targetFields && targetFields.length > 0) {
|
|
250
|
+
fkRow.targetFields = targetFields;
|
|
251
|
+
}
|
|
252
|
+
const existing = options.existingRow && typeof options.existingRow === 'object' ? options.existingRow : null;
|
|
253
|
+
const required =
|
|
254
|
+
options.required !== undefined && options.required !== null
|
|
255
|
+
? Boolean(options.required)
|
|
256
|
+
: existing?.required;
|
|
257
|
+
if (required !== undefined) {
|
|
258
|
+
fkRow.required = Boolean(required);
|
|
259
|
+
} else {
|
|
260
|
+
// Default for new FKs: explicit optional unless user marks required.
|
|
261
|
+
fkRow.required = false;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const description = typeof options.description === 'string' && options.description.trim()
|
|
265
|
+
? options.description.trim()
|
|
266
|
+
: existing?.description;
|
|
267
|
+
if (typeof description === 'string' && description.trim()) {
|
|
268
|
+
fkRow.description = description.trim();
|
|
269
|
+
} else {
|
|
270
|
+
fkRow.description = _buildDefaultFkDescription({
|
|
271
|
+
fields,
|
|
272
|
+
targetDatasource,
|
|
273
|
+
resolvedTargetFields: options.resolvedTargetFields || null
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return fkRow;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* @param {object} doc
|
|
281
|
+
* @param {object} opts
|
|
282
|
+
* @returns {{
|
|
283
|
+
* doc: object,
|
|
284
|
+
* patchOperations: object[],
|
|
285
|
+
* updatedSections: string[],
|
|
286
|
+
* replaced: boolean
|
|
287
|
+
* }}
|
|
288
|
+
*/
|
|
289
|
+
function applyCapabilityRelate(doc, opts) {
|
|
290
|
+
const name = normalizeRelationName(opts.relationName);
|
|
291
|
+
const targetDatasource = normalizeTargetDatasource(opts.targetDatasource);
|
|
292
|
+
const { fields, targetFields } = parseRelateFields(opts);
|
|
293
|
+
|
|
294
|
+
const d = deepClone(doc);
|
|
295
|
+
if (!Array.isArray(d.foreignKeys)) {
|
|
296
|
+
d.foreignKeys = [];
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const replaced = assertOverwriteAllowedAndGetExisting(d, name, Boolean(opts.overwrite));
|
|
300
|
+
const existingRow = replaced ? d.foreignKeys.find((fk) => fk && fk.name === name) : null;
|
|
301
|
+
const fkRow = buildForeignKeyRow(name, fields, targetDatasource, targetFields, {
|
|
302
|
+
existingRow,
|
|
303
|
+
required: opts.required,
|
|
304
|
+
description: opts.description,
|
|
305
|
+
resolvedTargetFields: opts.resolvedTargetFields
|
|
306
|
+
});
|
|
307
|
+
const patchOperations = [];
|
|
308
|
+
const updatedSections = [];
|
|
309
|
+
upsertForeignKeyRow(d, fkRow, name, patchOperations, updatedSections);
|
|
310
|
+
|
|
311
|
+
if (opts.addMetadataProperty !== false) {
|
|
312
|
+
addMetadataPropertyStub(d, name, patchOperations, updatedSections, {
|
|
313
|
+
targetDoc: opts.targetDoc,
|
|
314
|
+
resolvedTargetFields: opts.resolvedTargetFields
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
return { doc: d, patchOperations, updatedSections, replaced };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
module.exports = {
|
|
322
|
+
applyCapabilityRelate,
|
|
323
|
+
normalizeRelationName,
|
|
324
|
+
normalizeTargetDatasource
|
|
325
|
+
};
|