@aifabrix/builder 2.60.0 → 2.62.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/README.md +14 -11
- package/docs/README.md +80 -0
- package/docs/builder-help/evidence-patterns.json +155 -0
- package/docs/builder-help/golden-examples/crm-company.json +30 -0
- package/docs/builder-help/golden-examples/crm-deal.json +29 -0
- package/docs/builder-help/golden-examples/document-storage-keyed-get.json +85 -0
- package/docs/builder-help/golden-examples/document-storage.json +30 -0
- package/docs/builder-help/golden-examples/meeting-transcript.json +29 -0
- package/docs/builder-help/golden-examples/repository-template.json +29 -0
- package/docs/builder-help/golden-examples/service-ticket.json +29 -0
- package/docs/builder-help/platform-roles.json +98 -0
- package/docs/builder-help/resource-type-catalog.json +402 -0
- package/lib/api/applications.api.js +22 -2
- package/lib/api/bootstrap-snapshot.api.js +115 -0
- package/lib/api/configuration.api.js +131 -0
- package/lib/api/system-secrets.api.js +72 -0
- package/lib/app/deploy.js +14 -5
- package/lib/app/run-docker-fallback.js +6 -1
- package/lib/app/run-env-compose.js +5 -6
- package/lib/app/run-helpers.js +2 -1
- package/lib/app/run-parameter-sync.js +142 -0
- package/lib/app/run.js +6 -4
- package/lib/build/docker-build-args.js +5 -2
- package/lib/build/index.js +3 -2
- package/lib/build/standard-docker-build.js +6 -2
- package/lib/channels/channel-artifact-generator.js +13 -3
- package/lib/cli/infra-guided-footers.js +10 -8
- package/lib/cli/infra-guided.js +30 -10
- package/lib/cli/setup-app.js +5 -0
- package/lib/cli/setup-environment.js +156 -0
- package/lib/cli/setup-infra-up-platform-action.js +14 -1
- package/lib/cli/setup-platform.js +2 -0
- package/lib/commands/auth-config.js +2 -0
- package/lib/commands/datasource-capability-upsert-cli.js +112 -0
- package/lib/commands/datasource-capability.js +4 -2
- package/lib/commands/env-secret-context.js +113 -0
- package/lib/commands/env-secret-list.js +200 -0
- package/lib/commands/env-secret-push-confirm.js +54 -0
- package/lib/commands/env-secret-push-run.js +227 -0
- package/lib/commands/env-secret-push.js +168 -0
- package/lib/commands/login.js +4 -0
- package/lib/commands/repair-datasource-apply.js +5 -1
- package/lib/commands/repair-datasource-keyed-document.js +122 -0
- package/lib/commands/repair-datasource-legacy.js +94 -0
- package/lib/commands/repair-datasource-run.js +1 -0
- package/lib/commands/setup-image-refresh.js +26 -0
- package/lib/commands/setup-modes.js +8 -9
- package/lib/commands/setup-prompts-platform-mode.js +34 -0
- package/lib/commands/setup-prompts.js +2 -180
- package/lib/commands/setup.js +3 -0
- package/lib/commands/up-builder-api.js +24 -8
- package/lib/commands/up-common.js +16 -1
- package/lib/commands/up-dataplane-bootstrap.js +70 -14
- package/lib/commands/up-dataplane-credentials.js +21 -0
- package/lib/commands/up-dataplane.js +37 -18
- package/lib/commands/up-integration-server.js +112 -0
- package/lib/commands/verify-operations-skip-e2e.js +25 -1
- package/lib/commands/verify-operations-steps.js +13 -1
- package/lib/commands/wizard-config-normalizer.js +12 -5
- package/lib/commands/wizard-core.js +5 -157
- package/lib/commands/wizard-file-saving.js +163 -0
- package/lib/core/admin-secrets-env-overlay.js +8 -3
- package/lib/core/env-platform-expand.js +5 -1
- package/lib/core/environment-access-policy.js +121 -0
- package/lib/core/environment-mode-policy.js +177 -0
- package/lib/core/local-env-overrides.js +149 -0
- package/lib/core/secrets-ensure.js +4 -2
- package/lib/core/secrets-env-content.js +17 -10
- package/lib/core/secrets-env-write.js +11 -4
- package/lib/core/secrets-load.js +4 -2
- package/lib/datasource/binary-documents-validator.js +190 -0
- package/lib/datasource/capability/capability-manifest.js +1 -0
- package/lib/datasource/capability/run-capability-upsert.js +202 -0
- package/lib/datasource/capability/upsert-ingredients.js +291 -0
- package/lib/datasource/capability/upsert-operations.js +138 -0
- package/lib/datasource/capability/upsert-test-scaffold.js +189 -0
- package/lib/datasource/validate.js +12 -5
- package/lib/deployment/installation/index.js +14 -1
- package/lib/deployment/installation/infra-catalog.js +3 -0
- package/lib/deployment/installation/local-environment-stage.js +285 -0
- package/lib/deployment/installation/local-installation.js +76 -0
- package/lib/deployment/installation/stage-input.js +5 -1
- package/lib/generator/builders.js +3 -1
- package/lib/generator/index.js +3 -0
- package/lib/generator/split-variables.js +2 -2
- package/lib/lifecycle/product-model.js +4 -3
- package/lib/lifecycle/report-display.js +3 -2
- package/lib/parameters/database-adoption.js +125 -0
- package/lib/parameters/infra-parameter-catalog.js +1 -1
- package/lib/parameters/physical-database-naming.js +136 -0
- package/lib/parameters/physical-secret-name.js +194 -0
- package/lib/parameters/rsa-secret-values.js +165 -0
- package/lib/programmatic/builder-help-enterprise-sync-fabrix.js +1 -1
- package/lib/programmatic/builder-help-governance.js +1 -1
- package/lib/programmatic/builder-help.js +1 -1
- package/lib/programmatic/run-channel-install-artifacts.js +28 -2
- package/lib/programmatic/run-channel-package.js +3 -1
- package/lib/programmatic/workspace-context.js +37 -2
- package/lib/role-assistant/test-runner-workhub-answers.js +4 -1
- package/lib/role-assistant/test-runner-workhub-missing-fields.js +42 -0
- package/lib/role-assistant/test-runner-workhub-wait-stop.js +94 -0
- package/lib/role-assistant/test-runner-workhub.js +28 -19
- package/lib/schema/application-schema.json +14 -2
- package/lib/schema/external-datasource.schema.json +23 -3
- package/lib/schema/infra.parameter.yaml +440 -56
- package/lib/schema/infrastructure-schema.json +213 -120
- package/lib/utils/compose-generate-docker-compose.js +9 -14
- package/lib/utils/compose-generator.js +4 -7
- package/lib/utils/compose-miso-env.js +2 -2
- package/lib/utils/compose-traefik-ingress-base.js +3 -4
- package/lib/utils/datasource-binary-evidence.js +92 -0
- package/lib/utils/datasource-test-run-capability-scope.js +44 -1
- package/lib/utils/datasource-test-run-debug-display.js +2 -0
- package/lib/utils/datasource-test-run-display.js +8 -2
- package/lib/utils/datasource-test-run-issue-guidance.js +176 -0
- package/lib/utils/datasource-test-run-tty-log.js +2 -0
- package/lib/utils/declarative-url-ports.js +3 -0
- package/lib/utils/env-copy.js +11 -10
- package/lib/utils/env-environment-file-paths.js +105 -0
- package/lib/utils/environment-scoped-resources.js +48 -14
- package/lib/utils/external-system-system-test-tty.js +3 -2
- package/lib/utils/image-tags.js +2 -2
- package/lib/utils/paths-system-builder-keys.js +7 -1
- package/lib/utils/paths.js +7 -2
- package/lib/utils/platform-kv-ref.js +1 -1
- package/lib/utils/postgres-platform-bootstrap.js +35 -1
- package/lib/utils/prepare-local-data-mount.js +58 -0
- package/lib/utils/redis-env-scope.js +21 -1
- package/lib/utils/registry-auth-sources.js +154 -0
- package/lib/utils/registry-credentials.js +131 -14
- package/lib/utils/secrets-generator.js +30 -21
- package/lib/utils/secrets-helpers.js +5 -8
- package/lib/utils/secrets-kv-scope.js +127 -28
- package/lib/utils/secrets-materialize-local.js +9 -0
- package/lib/utils/secrets-missing-error.js +35 -1
- package/lib/utils/system-secret-mapping.js +125 -0
- package/lib/utils/test-log-writer.js +2 -1
- package/lib/utils/token-manager.js +14 -8
- package/lib/utils/url-public-path-prefix.js +6 -10
- package/lib/validation/external-manifest-validator.js +5 -0
- package/lib/validation/openapi-contract-surface-validator.js +3 -1
- package/lib/validation/validate-external-file.js +5 -1
- package/package.json +6 -4
- package/templates/agent-kit/agent-kit.yaml +1 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
- package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +15 -0
- package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +3 -2
- package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +6 -2
- package/templates/agent-kit/skills/aifabrix-prove/references/evidence-lifecycle.md +22 -0
- package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +7 -1
- package/templates/agent-kit/skills/aifabrix-role-assistant/references/testing-playbook.md +117 -0
- package/templates/agent-kit/skills/shared/interaction.md +83 -0
- package/templates/agent-kit/skills/shared/status.md +3 -12
- package/templates/applications/builder-api/application.yaml +1 -1
- package/templates/applications/builder-api/env.template +5 -1
- package/templates/applications/dataplane/application.yaml +2 -2
- package/templates/applications/dataplane/env.template +10 -12
- package/templates/applications/integration-server/application.yaml +91 -0
- package/templates/applications/integration-server/env.template +215 -0
- package/templates/applications/keycloak/application.yaml +6 -1
- package/templates/applications/mirrored-templates.json +52 -0
- package/templates/applications/miso-controller/application.yaml +116 -2
- package/templates/applications/miso-controller/env.template +62 -17
- package/templates/applications/miso-controller/rbac.yaml +17 -0
- package/templates/external-system/external-datasource.yaml.hbs +7 -1
- package/templates/marketplace/createUiDefinition.json +52 -4
- package/templates/marketplace/main.json +88 -10
- package/templates/python/docker-compose.hbs +1 -1
- package/templates/agent-kit/skills/aifabrix-plan/references/interaction.md +0 -41
- /package/{lib/programmatic/help-content → docs/builder-help/content}/channel-onboarding.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/cip-overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/connected-system-ui.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/dimensions-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/enterprise-sync-fabrix.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/overview.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/subscription-guide.md +0 -0
- /package/{lib/programmatic/help-content → docs/builder-help/content}/workflow.md +0 -0
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File-backed upsert capability authoring with validation and atomic write.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Run datasource capability upsert CLI operation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const {
|
|
10
|
+
readDatasourceConfigFile,
|
|
11
|
+
writeBackup,
|
|
12
|
+
atomicWriteJson
|
|
13
|
+
} = require('./run-capability-copy');
|
|
14
|
+
const { validateDatasourceParsed, resolveValidateInputPath } = require('../validate');
|
|
15
|
+
const {
|
|
16
|
+
resolveVendorOpenApiSpec,
|
|
17
|
+
inferIntegrationContextFromPath
|
|
18
|
+
} = require('../../validation/openapi-contract-surface-validator');
|
|
19
|
+
const { analyzeUpsertIngredients } = require('./upsert-ingredients');
|
|
20
|
+
const { applyUpsertCapability, existingUpsertMatches } = require('./upsert-operations');
|
|
21
|
+
const { scaffoldUpsertTests } = require('./upsert-test-scaffold');
|
|
22
|
+
|
|
23
|
+
function parseIdentityFields(raw) {
|
|
24
|
+
if (raw === undefined || raw === null || String(raw).trim() === '') return [];
|
|
25
|
+
return [...new Set(String(raw).split(',').map((field) => field.trim()).filter(Boolean))];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function applyExplicitIdentity(doc, fields) {
|
|
29
|
+
if (fields.length === 0) return [];
|
|
30
|
+
const mapped = new Set(Object.keys(doc.fieldMappings?.attributes || {}));
|
|
31
|
+
const primary = new Set(Array.isArray(doc.primaryKey) ? doc.primaryKey : []);
|
|
32
|
+
const invalid = fields.filter((field) => !mapped.has(field) || primary.has(field));
|
|
33
|
+
if (invalid.length > 0) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`--identity must name mapped non-primary-key fields; invalid: ${invalid.join(', ')}`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (!doc.validation) doc.validation = {};
|
|
39
|
+
if (!Array.isArray(doc.validation.repeatingValues)) {
|
|
40
|
+
doc.validation.repeatingValues = [];
|
|
41
|
+
}
|
|
42
|
+
const added = [];
|
|
43
|
+
fields.forEach((field) => {
|
|
44
|
+
const existing = doc.validation.repeatingValues.find((row) => row?.field === field);
|
|
45
|
+
if (existing && existing.strategy !== 'reject') {
|
|
46
|
+
throw new Error(`validation.repeatingValues for "${field}" already uses ${existing.strategy}`);
|
|
47
|
+
}
|
|
48
|
+
if (!existing) {
|
|
49
|
+
doc.validation.repeatingValues.push({ field, scope: ['global'], strategy: 'reject' });
|
|
50
|
+
added.push(field);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return added;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveVendorSpec(doc, resolvedPath, providedSpec) {
|
|
57
|
+
if (providedSpec) return { spec: providedSpec, localPath: null };
|
|
58
|
+
const context = inferIntegrationContextFromPath(resolvedPath);
|
|
59
|
+
const documentKey = typeof doc.openapi?.documentKey === 'string'
|
|
60
|
+
? doc.openapi.documentKey.trim()
|
|
61
|
+
: '';
|
|
62
|
+
if (!context || !documentKey) return { spec: null, localPath: null };
|
|
63
|
+
return resolveVendorOpenApiSpec(context.appPath, context.systemKey, documentKey);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ineligibleError(analysis, vendor) {
|
|
67
|
+
const detail = analysis.issues.map((row) => `${row.code}: ${row.message}`).join('\n');
|
|
68
|
+
const error = new Error(detail || 'Upsert ingredients are not eligible');
|
|
69
|
+
error.analysis = analysis;
|
|
70
|
+
error.vendorOpenApiPath = vendor.localPath;
|
|
71
|
+
return error;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function applyTestScaffold(result, analysis, targetKey) {
|
|
75
|
+
const scaffold = scaffoldUpsertTests(result.doc, analysis, targetKey);
|
|
76
|
+
if (scaffold.issue) {
|
|
77
|
+
throw ineligibleError({
|
|
78
|
+
eligible: false,
|
|
79
|
+
status: 'author-fixable',
|
|
80
|
+
issues: [scaffold.issue]
|
|
81
|
+
}, { localPath: null });
|
|
82
|
+
}
|
|
83
|
+
if (!scaffold.changed) return false;
|
|
84
|
+
result.doc = scaffold.doc;
|
|
85
|
+
result.patchOperations.push(...scaffold.patchOperations);
|
|
86
|
+
result.updatedSections.push(...scaffold.updatedSections);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildEnvelope(opts, parsed, analysis, result) {
|
|
91
|
+
return {
|
|
92
|
+
changed: true,
|
|
93
|
+
eligible: true,
|
|
94
|
+
capabilityKey: result.resolvedAs,
|
|
95
|
+
identityFields: analysis.identityFields,
|
|
96
|
+
createKey: analysis.halves.create.logical,
|
|
97
|
+
updateKey: analysis.halves.update.logical,
|
|
98
|
+
lookupCandidates: analysis.lookupCandidates,
|
|
99
|
+
issues: analysis.issues,
|
|
100
|
+
patchOperations: result.patchOperations,
|
|
101
|
+
nextActions: [
|
|
102
|
+
`aifabrix datasource capability validate ${opts.fileOrKey} --capability ${result.resolvedAs}`,
|
|
103
|
+
`aifabrix validate ${parsed.systemKey || '<system-key>'}`,
|
|
104
|
+
`aifabrix datasource test-e2e ${parsed.key || '<datasource-key>'} --app ${parsed.systemKey || '<system-key>'} --json`
|
|
105
|
+
]
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function prependIdentityPatches(result, doc, added, state) {
|
|
110
|
+
if (added.length === 0) return;
|
|
111
|
+
let patches;
|
|
112
|
+
if (!state.hadValidation) {
|
|
113
|
+
patches = [{ op: 'add', path: '/validation', value: doc.validation }];
|
|
114
|
+
} else if (!state.hadRepeatingValues) {
|
|
115
|
+
patches = [{
|
|
116
|
+
op: 'add',
|
|
117
|
+
path: '/validation/repeatingValues',
|
|
118
|
+
value: doc.validation.repeatingValues
|
|
119
|
+
}];
|
|
120
|
+
} else {
|
|
121
|
+
patches = added.map((field) => ({
|
|
122
|
+
op: 'add',
|
|
123
|
+
path: '/validation/repeatingValues/-',
|
|
124
|
+
value: { field, scope: ['global'], strategy: 'reject' }
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
result.patchOperations.unshift(...patches);
|
|
128
|
+
added.forEach((field) => result.updatedSections.push(`validation.repeatingValues: ${field}`));
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function unchangedUpsertResult(opts, parsed, analysis, resolvedPath) {
|
|
132
|
+
const result = { resolvedAs: opts.as || 'upsert', patchOperations: [], updatedSections: [] };
|
|
133
|
+
const validation = validateDatasourceParsed(parsed);
|
|
134
|
+
const envelope = buildEnvelope(opts, parsed, analysis, result);
|
|
135
|
+
return {
|
|
136
|
+
...envelope,
|
|
137
|
+
changed: false,
|
|
138
|
+
dryRun: Boolean(opts.dryRun),
|
|
139
|
+
resolvedPath,
|
|
140
|
+
backupPath: null,
|
|
141
|
+
updatedSections: [],
|
|
142
|
+
validation
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function validateGeneratedDoc(doc) {
|
|
147
|
+
const validation = validateDatasourceParsed(doc);
|
|
148
|
+
if (validation.valid) return validation;
|
|
149
|
+
const error = new Error(validation.errors.join('\n'));
|
|
150
|
+
error.validationErrors = validation.errors;
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function finishUpsertRun(opts, resolvedPath, result, envelope, validation) {
|
|
155
|
+
const common = { resolvedPath, updatedSections: result.updatedSections, validation };
|
|
156
|
+
if (opts.dryRun) return { ...envelope, ...common, dryRun: true, backupPath: null };
|
|
157
|
+
const backupPath = writeBackup(resolvedPath, Boolean(opts.noBackup));
|
|
158
|
+
atomicWriteJson(resolvedPath, result.doc);
|
|
159
|
+
return { ...envelope, ...common, dryRun: false, backupPath };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function runCapabilityUpsert(opts) {
|
|
163
|
+
const resolvedPath = resolveValidateInputPath(String(opts.fileOrKey || '').trim());
|
|
164
|
+
const parsed = readDatasourceConfigFile(resolvedPath);
|
|
165
|
+
const identityState = {
|
|
166
|
+
hadValidation: Boolean(parsed.validation),
|
|
167
|
+
hadRepeatingValues: Array.isArray(parsed.validation?.repeatingValues)
|
|
168
|
+
};
|
|
169
|
+
const identityAdded = applyExplicitIdentity(parsed, parseIdentityFields(opts.identity));
|
|
170
|
+
const vendor = resolveVendorSpec(parsed, resolvedPath, opts.vendorSpec);
|
|
171
|
+
const analysis = analyzeUpsertIngredients(parsed, {
|
|
172
|
+
createKey: opts.create || 'create',
|
|
173
|
+
updateKey: opts.update || 'update',
|
|
174
|
+
targetKey: opts.as || 'upsert',
|
|
175
|
+
vendorSpec: vendor.spec
|
|
176
|
+
});
|
|
177
|
+
if (!analysis.eligible) throw ineligibleError(analysis, vendor);
|
|
178
|
+
const targetKey = opts.as || 'upsert';
|
|
179
|
+
const targetMatches = !opts.overwrite && existingUpsertMatches(parsed, analysis, targetKey);
|
|
180
|
+
const result = targetMatches
|
|
181
|
+
? { doc: parsed, resolvedAs: targetKey, patchOperations: [], updatedSections: [] }
|
|
182
|
+
: applyUpsertCapability(parsed, analysis, {
|
|
183
|
+
targetKey,
|
|
184
|
+
overwrite: Boolean(opts.overwrite)
|
|
185
|
+
});
|
|
186
|
+
prependIdentityPatches(result, parsed, identityAdded, identityState);
|
|
187
|
+
const testChanged = opts.test ? applyTestScaffold(result, analysis, targetKey) : false;
|
|
188
|
+
if (targetMatches && identityAdded.length === 0 && !testChanged) {
|
|
189
|
+
return unchangedUpsertResult(opts, parsed, analysis, resolvedPath);
|
|
190
|
+
}
|
|
191
|
+
const validation = validateGeneratedDoc(result.doc);
|
|
192
|
+
const envelope = buildEnvelope(opts, parsed, analysis, result);
|
|
193
|
+
return finishUpsertRun(opts, resolvedPath, result, envelope, validation);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
module.exports = {
|
|
197
|
+
runCapabilityUpsert,
|
|
198
|
+
parseIdentityFields,
|
|
199
|
+
applyExplicitIdentity,
|
|
200
|
+
resolveVendorSpec,
|
|
201
|
+
applyTestScaffold
|
|
202
|
+
};
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static authoring eligibility for manifest-declared upsert capabilities.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Upsert ingredient analysis without runtime readiness claims
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { findVendorOperation } = require('../../validation/openapi-contract-vendor-validation');
|
|
10
|
+
const {
|
|
11
|
+
resolveSourceCapabilityForCopy,
|
|
12
|
+
resolveOpsKeysForCapability
|
|
13
|
+
} = require('./capability-resolve');
|
|
14
|
+
|
|
15
|
+
const FUZZY_TOKENS = new Set([
|
|
16
|
+
'contains', 'like', 'ilike', 'token', 'tokenized', 'prefix', 'suffix',
|
|
17
|
+
'substring', 'fuzzy', 'partial', 'startswith', 'endswith'
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
const ISSUE_GUIDANCE = {
|
|
21
|
+
'UPSERT-HALF-INVALID': {
|
|
22
|
+
consequence: 'The upsert would not compose one valid create half and one valid update half.',
|
|
23
|
+
remediation: ['Repair or select existing create-shaped and update-shaped capabilities.'],
|
|
24
|
+
prevention: 'Validate each write capability before composing upsert.',
|
|
25
|
+
residualRisk: 'Authoring remains blocked until both halves are valid.'
|
|
26
|
+
},
|
|
27
|
+
'UPSERT-IDENTITY-MISSING': {
|
|
28
|
+
consequence: 'Record existence cannot be resolved, so a retry may create a duplicate.',
|
|
29
|
+
remediation: ['Declare mapped business identity fields explicitly with --identity.'],
|
|
30
|
+
prevention: 'Model business identity before adding write orchestration.',
|
|
31
|
+
residualRisk: 'Do not publish or claim upsert while identity is absent.'
|
|
32
|
+
},
|
|
33
|
+
'UPSERT-IDENTITY-UNMAPPED': {
|
|
34
|
+
consequence: 'The runtime cannot bind every declared identity field to source data.',
|
|
35
|
+
remediation: ['Add valid field mappings or select mapped non-primary-key identity fields.'],
|
|
36
|
+
prevention: 'Keep identity declarations and field mappings in the same review.',
|
|
37
|
+
residualRisk: 'An incomplete identity can select or update the wrong record.'
|
|
38
|
+
},
|
|
39
|
+
'UPSERT-OPENAPI-UNRESOLVED': {
|
|
40
|
+
consequence: 'Builder cannot establish static authoring eligibility.',
|
|
41
|
+
remediation: ['Materialize the registered vendor OpenAPI document, then rerun the command.'],
|
|
42
|
+
prevention: 'Keep the vendor OpenAPI document synchronized before capability authoring.',
|
|
43
|
+
residualRisk: 'Source support is unknown; no capability was written.'
|
|
44
|
+
},
|
|
45
|
+
'UPSERT-EXACT-LOOKUP-MISSING': {
|
|
46
|
+
consequence: 'A write may create a duplicate or update the wrong business record.',
|
|
47
|
+
remediation: [
|
|
48
|
+
'Bind the complete identity to a real equality lookup when the source supports it.',
|
|
49
|
+
'If the source cannot support exact lookup, keep separate create and update capabilities.'
|
|
50
|
+
],
|
|
51
|
+
prevention: 'Require complete equality binding and a two-write no-duplicate E2E case.',
|
|
52
|
+
residualRisk: 'AI cannot repair an external API that lacks exact lookup or uniqueness.'
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
function uniqueStrings(values) {
|
|
57
|
+
return [...new Set(values.map((value) => String(value).trim()).filter(Boolean))];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function declaredIdentityFields(doc) {
|
|
61
|
+
const primary = new Set(uniqueStrings(Array.isArray(doc.primaryKey) ? doc.primaryKey : []));
|
|
62
|
+
const rows = Array.isArray(doc.validation?.repeatingValues)
|
|
63
|
+
? doc.validation.repeatingValues
|
|
64
|
+
: [];
|
|
65
|
+
return uniqueStrings(
|
|
66
|
+
rows
|
|
67
|
+
.filter((row) => row?.strategy === 'reject' && !primary.has(row.field))
|
|
68
|
+
.map((row) => row.field)
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function mappedFields(doc) {
|
|
73
|
+
const attrs = doc.fieldMappings?.attributes;
|
|
74
|
+
return new Set(attrs && typeof attrs === 'object' ? Object.keys(attrs) : []);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function assertSourceHalf(doc, logicalKey, expectedShape) {
|
|
78
|
+
const logical = resolveSourceCapabilityForCopy(doc, logicalKey);
|
|
79
|
+
const keys = resolveOpsKeysForCapability(doc, logical);
|
|
80
|
+
const operation = doc.execution.cip.operations[keys.cipKey];
|
|
81
|
+
if (operation?.enabled === false) {
|
|
82
|
+
throw new Error(`${expectedShape} capability "${logicalKey}" is disabled`);
|
|
83
|
+
}
|
|
84
|
+
if (operation?.shape && operation.shape !== expectedShape) {
|
|
85
|
+
throw new Error(
|
|
86
|
+
`${expectedShape} capability "${logicalKey}" declares shape "${operation.shape}"`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return { logical, ...keys };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function collectText(node, out = []) {
|
|
93
|
+
if (typeof node === 'string') {
|
|
94
|
+
out.push(node.toLowerCase());
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
if (Array.isArray(node)) {
|
|
98
|
+
node.forEach((item) => collectText(item, out));
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
if (node && typeof node === 'object') {
|
|
102
|
+
Object.entries(node).forEach(([key, value]) => {
|
|
103
|
+
out.push(String(key).toLowerCase());
|
|
104
|
+
collectText(value, out);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function resolveLocalSchema(schema, vendorSpec) {
|
|
111
|
+
const ref = schema?.$ref;
|
|
112
|
+
if (typeof ref !== 'string' || !ref.startsWith('#/')) return schema;
|
|
113
|
+
return ref.slice(2).split('/').reduce((node, token) => node?.[token], vendorSpec) || schema;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function collectVendorParameterNames(vendorOp, vendorSpec) {
|
|
117
|
+
const names = [];
|
|
118
|
+
const parameters = Array.isArray(vendorOp?.parameters) ? vendorOp.parameters : [];
|
|
119
|
+
parameters
|
|
120
|
+
.filter((parameter) => !parameter?.in || parameter.in === 'query')
|
|
121
|
+
.forEach((parameter) => names.push(parameter?.name));
|
|
122
|
+
const rawSchema = vendorOp?.requestBody?.content?.['application/json']?.schema;
|
|
123
|
+
const schema = resolveLocalSchema(rawSchema, vendorSpec);
|
|
124
|
+
if (schema?.properties && typeof schema.properties === 'object') {
|
|
125
|
+
names.push(...Object.keys(schema.properties));
|
|
126
|
+
}
|
|
127
|
+
return uniqueStrings(names).map((name) => name.toLowerCase());
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function operationBinding(doc, operationKey) {
|
|
131
|
+
const cip = doc.execution?.cip?.operations?.[operationKey];
|
|
132
|
+
const steps = Array.isArray(cip?.steps) ? cip.steps : [];
|
|
133
|
+
const fetch = steps.map((step) => step?.fetch).find(Boolean);
|
|
134
|
+
return fetch || {};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function templateReferencesField(value, field) {
|
|
138
|
+
if (typeof value === 'string') {
|
|
139
|
+
const templates = [...value.matchAll(/\{\{([^}]+)\}\}/g)];
|
|
140
|
+
return templates.some((match) =>
|
|
141
|
+
match[1].toLowerCase().split(/[^a-z0-9_]+/).includes(field.toLowerCase())
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
if (Array.isArray(value)) {
|
|
145
|
+
return value.some((item) => templateReferencesField(item, field));
|
|
146
|
+
}
|
|
147
|
+
if (!value || typeof value !== 'object') return false;
|
|
148
|
+
return Object.values(value).some((item) => templateReferencesField(item, field));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function bindingCarriesField(binding, vendorOp, vendorSpec, field) {
|
|
152
|
+
const accepted = new Set(collectVendorParameterNames(vendorOp, vendorSpec));
|
|
153
|
+
const containers = [binding.query, binding.bodyTemplate, binding.parameters];
|
|
154
|
+
return containers.some((container) =>
|
|
155
|
+
container && typeof container === 'object' && !Array.isArray(container) &&
|
|
156
|
+
Object.entries(container).some(([key, value]) =>
|
|
157
|
+
accepted.has(key.toLowerCase()) && templateReferencesField(value, field)
|
|
158
|
+
)
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function candidateCoverage(doc, opKey, opCfg, vendorOp, identityFields, vendorSpec) {
|
|
163
|
+
const binding = operationBinding(doc, opKey);
|
|
164
|
+
const text = collectText({
|
|
165
|
+
query: binding.query,
|
|
166
|
+
bodyTemplate: binding.bodyTemplate,
|
|
167
|
+
parameters: binding.parameters
|
|
168
|
+
});
|
|
169
|
+
const coveredFields = identityFields.filter((field) =>
|
|
170
|
+
bindingCarriesField(binding, vendorOp, vendorSpec, field)
|
|
171
|
+
);
|
|
172
|
+
const fuzzy = text.some((item) =>
|
|
173
|
+
[...FUZZY_TOKENS].some((token) => item === token || item.includes(`"${token}"`))
|
|
174
|
+
);
|
|
175
|
+
return {
|
|
176
|
+
operationKey: opKey,
|
|
177
|
+
operationId: opCfg.operationId || null,
|
|
178
|
+
method: String(opCfg.method || 'get').toUpperCase(),
|
|
179
|
+
coveredFields,
|
|
180
|
+
missingFields: identityFields.filter((field) => !coveredFields.includes(field)),
|
|
181
|
+
fuzzy
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function analyzeLookupCandidates(doc, vendorSpec, identityFields, excludedKeys) {
|
|
186
|
+
const operations = doc.openapi?.operations || {};
|
|
187
|
+
return Object.entries(operations)
|
|
188
|
+
.filter(([key]) => !excludedKeys.has(key.toLowerCase()))
|
|
189
|
+
.map(([key, config]) => {
|
|
190
|
+
const vendorOp = findVendorOperation(vendorSpec, config);
|
|
191
|
+
if (!vendorOp) return null;
|
|
192
|
+
return candidateCoverage(doc, key, config, vendorOp, identityFields, vendorSpec);
|
|
193
|
+
})
|
|
194
|
+
.filter(Boolean);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function issue(code, message, ownership, missingFields = []) {
|
|
198
|
+
return {
|
|
199
|
+
code,
|
|
200
|
+
severity: 'blocking',
|
|
201
|
+
message,
|
|
202
|
+
observed: message,
|
|
203
|
+
ownership,
|
|
204
|
+
aiFixable: ownership === 'manifest',
|
|
205
|
+
missingFields,
|
|
206
|
+
...(ISSUE_GUIDANCE[code] || {})
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function blocked(status, context, finding) {
|
|
211
|
+
return { eligible: false, status, ...context, issues: [finding] };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function analyzeIdentity(doc, halves) {
|
|
215
|
+
const identityFields = declaredIdentityFields(doc);
|
|
216
|
+
const context = { halves, identityFields, lookupCandidates: [] };
|
|
217
|
+
if (identityFields.length === 0) {
|
|
218
|
+
return blocked('author-fixable', context, issue(
|
|
219
|
+
'UPSERT-IDENTITY-MISSING',
|
|
220
|
+
'No mapped business identity is declared with validation.repeatingValues strategy reject',
|
|
221
|
+
'manifest'
|
|
222
|
+
));
|
|
223
|
+
}
|
|
224
|
+
const unmapped = identityFields.filter((field) => !mappedFields(doc).has(field));
|
|
225
|
+
if (unmapped.length === 0) return context;
|
|
226
|
+
return blocked('author-fixable', context, issue(
|
|
227
|
+
'UPSERT-IDENTITY-UNMAPPED',
|
|
228
|
+
`Business identity fields are not mapped: ${unmapped.join(', ')}`,
|
|
229
|
+
'manifest',
|
|
230
|
+
unmapped
|
|
231
|
+
));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function noExactLookup(halves, identityFields, lookupCandidates) {
|
|
235
|
+
const covered = new Set(lookupCandidates.flatMap((candidate) => candidate.coveredFields));
|
|
236
|
+
const missing = identityFields.filter((field) => !covered.has(field));
|
|
237
|
+
const suffix = missing.length ? `; uncovered: ${missing.join(', ')}` : '';
|
|
238
|
+
return blocked(
|
|
239
|
+
'source-limited',
|
|
240
|
+
{ halves, identityFields, lookupCandidates },
|
|
241
|
+
issue(
|
|
242
|
+
'UPSERT-EXACT-LOOKUP-MISSING',
|
|
243
|
+
`No declared non-fuzzy read covers the full business identity${suffix}`,
|
|
244
|
+
'manifest-or-source',
|
|
245
|
+
missing
|
|
246
|
+
)
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function analyzeUpsertIngredients(doc, options = {}) {
|
|
251
|
+
const createKey = options.createKey || 'create';
|
|
252
|
+
const updateKey = options.updateKey || 'update';
|
|
253
|
+
let halves;
|
|
254
|
+
try {
|
|
255
|
+
halves = {
|
|
256
|
+
create: assertSourceHalf(doc, createKey, 'create'),
|
|
257
|
+
update: assertSourceHalf(doc, updateKey, 'update')
|
|
258
|
+
};
|
|
259
|
+
} catch (error) {
|
|
260
|
+
return blocked(
|
|
261
|
+
'author-fixable',
|
|
262
|
+
{ identityFields: [], lookupCandidates: [] },
|
|
263
|
+
issue('UPSERT-HALF-INVALID', error.message, 'manifest')
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
const identity = analyzeIdentity(doc, halves);
|
|
267
|
+
if (identity.eligible === false) return identity;
|
|
268
|
+
const { identityFields } = identity;
|
|
269
|
+
if (!options.vendorSpec) {
|
|
270
|
+
return blocked('indeterminate', identity, issue(
|
|
271
|
+
'UPSERT-OPENAPI-UNRESOLVED',
|
|
272
|
+
'The materialized vendor OpenAPI document could not be resolved',
|
|
273
|
+
'local-openapi'
|
|
274
|
+
));
|
|
275
|
+
}
|
|
276
|
+
const excluded = new Set([
|
|
277
|
+
halves.create.openapiKey.toLowerCase(), halves.update.openapiKey.toLowerCase(),
|
|
278
|
+
String(options.targetKey || 'upsert').toLowerCase()
|
|
279
|
+
]);
|
|
280
|
+
const lookupCandidates = analyzeLookupCandidates(doc, options.vendorSpec, identityFields, excluded);
|
|
281
|
+
const exact = lookupCandidates.find((candidate) => !candidate.fuzzy && candidate.missingFields.length === 0);
|
|
282
|
+
if (!exact) return noExactLookup(halves, identityFields, lookupCandidates);
|
|
283
|
+
return { eligible: true, status: 'eligible', halves, identityFields, lookupCandidates, selectedLookup: exact, issues: [] };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
module.exports = {
|
|
287
|
+
analyzeUpsertIngredients,
|
|
288
|
+
declaredIdentityFields,
|
|
289
|
+
collectVendorParameterNames,
|
|
290
|
+
candidateCoverage
|
|
291
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create the canonical manifest-declared upsert slice after eligibility passes.
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Pure upsert manifest mutation
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { jsonPointerPath } = require('./json-pointer');
|
|
10
|
+
const { storageOpsKey } = require('./capability-storage-keys');
|
|
11
|
+
const {
|
|
12
|
+
deepClone,
|
|
13
|
+
removeCapability,
|
|
14
|
+
resolveTargetKey
|
|
15
|
+
} = require('./copy-operations');
|
|
16
|
+
const { resolveProfileKeyForLogical, capabilityLogicalExists } = require('./capability-resolve');
|
|
17
|
+
const { buildCapabilityObject } = require('./capability-manifest');
|
|
18
|
+
|
|
19
|
+
function profileFields(profile) {
|
|
20
|
+
if (Array.isArray(profile)) return profile;
|
|
21
|
+
if (profile && Array.isArray(profile.attributes)) return profile.attributes;
|
|
22
|
+
return [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function buildUpsertProfile(doc, createLogical, updateLogical, identityFields) {
|
|
26
|
+
const createKey = resolveProfileKeyForLogical(doc, createLogical);
|
|
27
|
+
const updateKey = resolveProfileKeyForLogical(doc, updateLogical);
|
|
28
|
+
const createFields = profileFields(createKey ? doc.exposed.profiles[createKey] : null);
|
|
29
|
+
const updateSet = new Set(profileFields(updateKey ? doc.exposed.profiles[updateKey] : null));
|
|
30
|
+
const common = createFields.filter((field) => updateSet.has(field));
|
|
31
|
+
const fields = [...new Set([...common, ...identityFields])];
|
|
32
|
+
if (fields.length === 0) {
|
|
33
|
+
throw new Error('Create and update profiles have no shared input fields');
|
|
34
|
+
}
|
|
35
|
+
return fields;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function buildOpenApiOperation(createOperation, targetKey) {
|
|
39
|
+
const operation = deepClone(createOperation);
|
|
40
|
+
operation.operationId = targetKey;
|
|
41
|
+
operation.method = 'POST';
|
|
42
|
+
operation.restRuntime = {
|
|
43
|
+
pathKind: 'collectionSubresource',
|
|
44
|
+
subpathSegments: [targetKey]
|
|
45
|
+
};
|
|
46
|
+
return operation;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildCipOperation(doc, targetKey, createKey, updateKey, identityFields) {
|
|
50
|
+
return {
|
|
51
|
+
enabled: true,
|
|
52
|
+
description: `Create or update ${doc.displayName || doc.key || 'record'} by ${identityFields.join(', ')}`,
|
|
53
|
+
shape: 'upsert',
|
|
54
|
+
steps: [
|
|
55
|
+
{ fetch: { source: 'datasource', datasource: doc.key, operation: createKey } },
|
|
56
|
+
{ fetch: { source: 'datasource', datasource: doc.key, operation: updateKey } },
|
|
57
|
+
{ output: { mode: 'records' } }
|
|
58
|
+
]
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function addCapability(doc, targetKey) {
|
|
63
|
+
if (!Array.isArray(doc.capabilities)) doc.capabilities = [];
|
|
64
|
+
const usesObjects = doc.capabilities.some((row) => row && typeof row === 'object');
|
|
65
|
+
const value = usesObjects
|
|
66
|
+
? { ...buildCapabilityObject(targetKey, doc.displayName || doc.key), description: `Create or update ${doc.displayName || doc.key || 'record'}` }
|
|
67
|
+
: targetKey;
|
|
68
|
+
doc.capabilities.push(value);
|
|
69
|
+
return value;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function applyUpsertCapability(originalDoc, analysis, options = {}) {
|
|
73
|
+
if (!analysis?.eligible) throw new Error('Upsert ingredients are not eligible');
|
|
74
|
+
const doc = deepClone(originalDoc);
|
|
75
|
+
const targetKey = resolveTargetKey(doc, options.targetKey || 'upsert', Boolean(options.overwrite));
|
|
76
|
+
if (options.overwrite && capabilityLogicalExists(doc, targetKey)) removeCapability(doc, targetKey);
|
|
77
|
+
const storageKey = storageOpsKey(targetKey);
|
|
78
|
+
const createOperation = doc.openapi.operations[analysis.halves.create.openapiKey];
|
|
79
|
+
const openapiOperation = buildOpenApiOperation(createOperation, targetKey);
|
|
80
|
+
const cipOperation = buildCipOperation(
|
|
81
|
+
doc, targetKey, analysis.halves.create.logical, analysis.halves.update.logical,
|
|
82
|
+
analysis.identityFields
|
|
83
|
+
);
|
|
84
|
+
const profile = buildUpsertProfile(
|
|
85
|
+
doc, analysis.halves.create.logical, analysis.halves.update.logical,
|
|
86
|
+
analysis.identityFields
|
|
87
|
+
);
|
|
88
|
+
doc.openapi.operations[storageKey] = openapiOperation;
|
|
89
|
+
doc.execution.cip.operations[storageKey] = cipOperation;
|
|
90
|
+
if (!doc.exposed) doc.exposed = {};
|
|
91
|
+
if (!doc.exposed.profiles) doc.exposed.profiles = {};
|
|
92
|
+
doc.exposed.profiles[targetKey] = profile;
|
|
93
|
+
const capability = addCapability(doc, targetKey);
|
|
94
|
+
const patchOperations = [
|
|
95
|
+
{ op: 'add', path: jsonPointerPath('openapi', 'operations', storageKey), value: openapiOperation },
|
|
96
|
+
{ op: 'add', path: jsonPointerPath('execution', 'cip', 'operations', storageKey), value: cipOperation },
|
|
97
|
+
{ op: 'add', path: jsonPointerPath('exposed', 'profiles', targetKey), value: profile },
|
|
98
|
+
{ op: 'add', path: '/capabilities/-', value: capability }
|
|
99
|
+
];
|
|
100
|
+
return {
|
|
101
|
+
doc,
|
|
102
|
+
resolvedAs: targetKey,
|
|
103
|
+
patchOperations,
|
|
104
|
+
updatedSections: [
|
|
105
|
+
`capabilities[]: ${targetKey}`,
|
|
106
|
+
`openapi.operations.${storageKey}`,
|
|
107
|
+
`execution.cip.operations.${storageKey}`,
|
|
108
|
+
`exposed.profiles.${targetKey}`
|
|
109
|
+
]
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function existingUpsertMatches(originalDoc, analysis, targetKey = 'upsert') {
|
|
114
|
+
if (!capabilityLogicalExists(originalDoc, targetKey)) return false;
|
|
115
|
+
const expected = applyUpsertCapability(originalDoc, analysis, {
|
|
116
|
+
targetKey,
|
|
117
|
+
overwrite: true
|
|
118
|
+
}).doc;
|
|
119
|
+
const storageKey = storageOpsKey(targetKey);
|
|
120
|
+
const actualSlices = {
|
|
121
|
+
openapi: originalDoc.openapi?.operations?.[storageKey],
|
|
122
|
+
cip: originalDoc.execution?.cip?.operations?.[storageKey],
|
|
123
|
+
profile: originalDoc.exposed?.profiles?.[targetKey]
|
|
124
|
+
};
|
|
125
|
+
const expectedSlices = {
|
|
126
|
+
openapi: expected.openapi?.operations?.[storageKey],
|
|
127
|
+
cip: expected.execution?.cip?.operations?.[storageKey],
|
|
128
|
+
profile: expected.exposed?.profiles?.[targetKey]
|
|
129
|
+
};
|
|
130
|
+
return JSON.stringify(actualSlices) === JSON.stringify(expectedSlices);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = {
|
|
134
|
+
applyUpsertCapability,
|
|
135
|
+
existingUpsertMatches,
|
|
136
|
+
buildUpsertProfile,
|
|
137
|
+
buildCipOperation
|
|
138
|
+
};
|