@aifabrix/builder 2.51.1 → 2.52.1
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/jest.isolated-projects.js +25 -2
- package/jest.projects.js +4 -1
- package/lib/api/datasources-extended.api.js +8 -8
- package/lib/api/keycloak.api.js +92 -0
- package/lib/api/token-providers.api.js +77 -0
- package/lib/api/types/keycloak.types.js +23 -0
- package/lib/api/types/token-providers.types.js +65 -0
- package/lib/app/down.js +16 -1
- package/lib/app/run-container-start.js +4 -0
- package/lib/app/run-deploy-job.js +135 -27
- package/lib/app/run-env-compose.js +32 -0
- package/lib/app/run-env-recovery.js +35 -0
- package/lib/app/run-helpers.js +5 -6
- package/lib/app/run.js +11 -3
- package/lib/channels/add-channel.js +126 -19
- package/lib/channels/approval-guides/chatgpt.js +12 -9
- package/lib/channels/approval-guides/microsoft-copilot.js +16 -12
- package/lib/channels/channel-add-ensure.js +128 -0
- package/lib/channels/channel-artifact-generator.js +11 -1
- package/lib/channels/channel-branding.js +94 -0
- package/lib/channels/channel-credential.js +8 -0
- package/lib/channels/channel-discovery.js +330 -0
- package/lib/channels/channel-external-system.js +1 -1
- package/lib/channels/channel-runtime-resolver.js +103 -14
- package/lib/channels/channel-status-display.js +7 -1
- package/lib/channels/channel-targets.js +18 -1
- package/lib/channels/chatgpt-runtime-contract.js +1 -1
- package/lib/channels/microsoft-copilot-channel-add.js +138 -0
- package/lib/channels/microsoft-copilot-entra-sso-form.js +180 -0
- package/lib/channels/microsoft-copilot-entra-sso.js +264 -0
- package/lib/channels/microsoft-copilot-icons.js +74 -0
- package/lib/channels/microsoft-copilot-limits.js +84 -0
- package/lib/channels/microsoft-copilot-package.js +130 -49
- package/lib/channels/microsoft-copilot-validate.js +197 -0
- package/lib/cli/index.js +2 -0
- package/lib/cli/infra-guided-footers.js +1 -1
- package/lib/cli/infra-guided-public-url-warn.js +55 -0
- package/lib/cli/infra-guided.js +78 -62
- package/lib/cli/setup-app.help.js +4 -0
- package/lib/cli/setup-app.test-commands.js +59 -28
- package/lib/cli/setup-external-system.js +50 -12
- package/lib/cli/setup-infra.js +31 -4
- package/lib/commands/channel-add-setup.js +35 -10
- package/lib/commands/channel.js +27 -12
- package/lib/commands/deploy-gate-orchestrator.js +3 -2
- package/lib/commands/governance-verify-external.js +2 -5
- package/lib/commands/integration-definition.js +158 -0
- package/lib/commands/lifecycle-external.js +14 -7
- package/lib/commands/repair-auth-apply.js +19 -5
- package/lib/commands/repair-datasource-sync-operations.js +4 -1
- package/lib/commands/role-assistant-test-output.js +200 -0
- package/lib/commands/role-assistant.js +131 -114
- package/lib/commands/test-governance-external.js +2 -5
- package/lib/commands/up-miso.js +61 -24
- package/lib/commands/upload-data-sync-finalize.js +20 -3
- package/lib/commands/verify-governance-external.js +2 -5
- package/lib/commands/verify-operations-external.js +2 -5
- package/lib/commands/verify-operations-step-plan.js +31 -0
- package/lib/commands/verify-trust-command-action.js +2 -5
- package/lib/datasource/capability/capability-resolve.js +15 -5
- package/lib/datasource/capability/copy-operations.js +14 -4
- package/lib/datasource/datasource-identifier.js +4 -3
- package/lib/datasource/integration-context.js +3 -3
- package/lib/datasource/resolve-app.js +90 -8
- package/lib/datasource/test-e2e.js +2 -2
- package/lib/datasource/unified-validation-run-body.js +15 -1
- package/lib/datasource/unified-validation-run-resolve.js +2 -2
- package/lib/external-system/deploy-helpers.js +9 -5
- package/lib/external-system/deploy.js +2 -1
- package/lib/external-system/test-system-level.js +21 -3
- package/lib/external-system/test.js +4 -4
- package/lib/generator/builders.js +9 -15
- package/lib/infrastructure/index.js +19 -4
- package/lib/integration-definition/apply.js +478 -0
- package/lib/integration-definition/canonical-json.js +51 -0
- package/lib/integration-definition/emit-suite-bid.js +94 -0
- package/lib/integration-definition/enrich-crm-field-mappings.js +240 -0
- package/lib/integration-definition/enrich-openapi-operations.js +434 -0
- package/lib/integration-definition/from-discovery.js +339 -0
- package/lib/integration-definition/hash.js +37 -0
- package/lib/integration-definition/index.js +37 -0
- package/lib/integration-definition/openapi-enrichment/hubspot-crm-companies.json +205 -0
- package/lib/integration-definition/resource-type-catalog.js +59 -0
- package/lib/integration-definition/system-kind-enum.js +40 -0
- package/lib/integration-definition/validate.js +148 -0
- package/lib/programmatic/builder-api-request.js +5 -1
- package/lib/programmatic/builder-help-governance.js +3 -2
- package/lib/programmatic/capability-registry.js +19 -0
- package/lib/programmatic/openapi-component-schemas.js +6 -1
- package/lib/programmatic/openapi-descriptions.js +40 -0
- package/lib/programmatic/openapi-envelope-examples.js +269 -0
- package/lib/programmatic/openapi-operation-bindings.js +45 -0
- package/lib/programmatic/openapi-request-schemas.js +14 -0
- package/lib/programmatic/openapi-result-schemas-overflow.js +101 -0
- package/lib/programmatic/openapi-result-schemas.js +46 -99
- package/lib/programmatic/openapi-schema-descriptions-core.js +7 -4
- package/lib/programmatic/openapi-schema-descriptions-help.js +1 -1
- package/lib/programmatic/openapi-schema-descriptions-operations.js +24 -0
- package/lib/programmatic/openapi-schema-helpers.js +9 -9
- package/lib/programmatic/openapi-spec.js +11 -1
- package/lib/programmatic/orchestration-openapi-meta.js +10 -0
- package/lib/programmatic/route-handler-map.js +29 -1
- package/lib/programmatic/run-channel-package.js +236 -0
- package/lib/programmatic/run-integration-definition.js +69 -0
- package/lib/role-assistant/knowledge-markdown-frontmatter.js +75 -0
- package/lib/role-assistant/knowledge-sync-logging.js +58 -0
- package/lib/role-assistant/knowledge-sync.js +104 -89
- package/lib/role-assistant/test-cases-bid.js +155 -0
- package/lib/role-assistant/test-cases-evaluate.js +9 -1
- package/lib/role-assistant/test-cases-honesty.js +24 -10
- package/lib/role-assistant/test-cases.js +28 -0
- package/lib/role-assistant/test-runner-bid-apply.js +122 -0
- package/lib/role-assistant/test-runner-bid-document.js +278 -0
- package/lib/role-assistant/test-runner-workhub-answers.js +289 -46
- package/lib/role-assistant/test-runner-workhub.js +121 -25
- package/lib/role-assistant/test-suite.js +119 -0
- package/lib/schema/application-schema.json +64 -32
- package/lib/schema/external-datasource.schema.json +103 -17
- package/lib/schema/external-system.schema.json +54 -3
- package/lib/schema/infra.parameter.yaml +0 -20
- package/lib/schema/integration-definition.schema.json +195 -0
- package/lib/utils/authentication-shape.js +6 -0
- package/lib/utils/certify-external-workspace.js +40 -0
- package/lib/utils/docker-build.js +3 -3
- package/lib/utils/docker-run-ephemeral.js +77 -17
- package/lib/utils/health-check-public-warn.js +31 -13
- package/lib/utils/health-check.js +11 -1
- package/lib/utils/help-builder.js +1 -0
- package/lib/utils/paths.js +13 -15
- package/lib/utils/promote-dev-docker-image.js +97 -18
- package/lib/utils/role-assistant-paths.js +76 -14
- package/lib/utils/transient-developer-id.js +28 -0
- package/lib/utils/upload-sync-options.js +7 -4
- package/lib/validation/reserved-system-key-prefixes.js +56 -0
- package/lib/validation/validate.js +54 -19
- package/package.json +1 -1
- package/scripts/pnpm/af.mjs +24 -9
- package/scripts/pnpm/help.mjs +1 -1
- package/templates/applications/builder-api/application.yaml +3 -3
- package/templates/applications/dataplane/application.yaml +5 -5
- package/templates/applications/dataplane/env.template +8 -7
- package/templates/applications/dataplane/rbac.yaml +10 -10
- package/templates/applications/keycloak/application.yaml +2 -2
- package/templates/applications/keycloak/env.template +5 -2
- package/templates/applications/miso-controller/application.yaml +25 -11
- package/templates/applications/miso-controller/env.template +24 -5
- package/templates/channels/README.md +11 -1
- package/templates/channels/brand/color-192.png +0 -0
- package/templates/channels/brand/dark/128.png +0 -0
- package/templates/channels/brand/dark/192.png +0 -0
- package/templates/channels/brand/dark/256.png +0 -0
- package/templates/channels/brand/dark/32.png +0 -0
- package/templates/channels/brand/dark/512.png +0 -0
- package/templates/channels/brand/dark/64.png +0 -0
- package/templates/channels/brand/light/128.png +0 -0
- package/templates/channels/brand/light/192.png +0 -0
- package/templates/channels/brand/light/256.png +0 -0
- package/templates/channels/brand/light/32.png +0 -0
- package/templates/channels/brand/light/512.png +0 -0
- package/templates/channels/brand/light/64.png +0 -0
- package/templates/channels/brand/mono-white/128.png +0 -0
- package/templates/channels/brand/mono-white/192.png +0 -0
- package/templates/channels/brand/mono-white/256.png +0 -0
- package/templates/channels/brand/mono-white/32.png +0 -0
- package/templates/channels/brand/mono-white/512.png +0 -0
- package/templates/channels/brand/mono-white/64.png +0 -0
- package/templates/channels/brand/outline-32.png +0 -0
- package/templates/channels/chatgpt/install.template.md.hbs +76 -36
- package/templates/channels/chatgpt/integration/README.md +4 -4
- package/templates/channels/chatgpt/integration/chatgpt-system.json +1 -16
- package/templates/channels/chatgpt/manifest.template.json.hbs +3 -3
- package/templates/channels/microsoft-copilot/ai-plugin.template.json.hbs +10 -16
- package/templates/channels/microsoft-copilot/declarative-agent.template.json.hbs +5 -3
- package/templates/channels/microsoft-copilot/entra-sso-registration.template.md.hbs +27 -0
- package/templates/channels/microsoft-copilot/install.template.md.hbs +39 -20
- package/templates/channels/microsoft-copilot/integration/README.md +2 -3
- package/templates/channels/microsoft-copilot/integration/microsoft-copilot-system.json +1 -22
- package/templates/channels/microsoft-copilot/manifest.template.json.hbs +7 -7
- package/templates/channels/microsoft-copilot/oauth-plugin-vault-required.template.md.hbs +11 -0
- package/templates/channels/slack/install.template.md.hbs +6 -1
- package/templates/channels/slack/integration/README.md +2 -4
- package/templates/channels/teams-bot/install.template.md.hbs +4 -0
- package/templates/channels/teams-bot/integration/README.md +3 -5
- package/templates/channels/web-embed/integration/web-embed-system.json +1 -16
- package/templates/python/docker-compose.hbs +10 -0
- package/templates/typescript/docker-compose.hbs +10 -0
- /package/.cursor/plans/{Done → Archive}/157.1-security-quality-violations-by-type.csv +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CRM-style fieldMappings + properties query for OpenAPI object APIs.
|
|
3
|
+
* @fileoverview Keep BID propertyKeys durable across force-apply
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const RESERVED_ATTR = new Set(['id', 'externalId', 'associations']);
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Seed metadataSchema + fieldMappings.attributes from BID propertyKeys.
|
|
12
|
+
* Does not write dimensions[] (repair / enrich owns dimensions alignment).
|
|
13
|
+
* @param {Record<string, unknown>} body
|
|
14
|
+
* @param {unknown} propertyKeys
|
|
15
|
+
* @returns {void}
|
|
16
|
+
*/
|
|
17
|
+
function seedPropertyKeysOnDatasource(body, propertyKeys) {
|
|
18
|
+
const keys = Array.isArray(propertyKeys)
|
|
19
|
+
? propertyKeys.map((row) => String(row || '').trim()).filter(Boolean)
|
|
20
|
+
: [];
|
|
21
|
+
if (keys.length === 0) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (!body.metadataSchema || typeof body.metadataSchema !== 'object') {
|
|
25
|
+
body.metadataSchema = { type: 'object', properties: {} };
|
|
26
|
+
}
|
|
27
|
+
const schema = /** @type {any} */ (body.metadataSchema);
|
|
28
|
+
if (!schema.properties || typeof schema.properties !== 'object') {
|
|
29
|
+
schema.properties = {};
|
|
30
|
+
}
|
|
31
|
+
if (!body.fieldMappings || typeof body.fieldMappings !== 'object') {
|
|
32
|
+
body.fieldMappings = { attributes: {} };
|
|
33
|
+
}
|
|
34
|
+
const mappings = /** @type {any} */ (body.fieldMappings);
|
|
35
|
+
if (!mappings.attributes || typeof mappings.attributes !== 'object') {
|
|
36
|
+
mappings.attributes = {};
|
|
37
|
+
}
|
|
38
|
+
for (const key of keys) {
|
|
39
|
+
if (!schema.properties[key]) {
|
|
40
|
+
schema.properties[key] = {
|
|
41
|
+
type: 'string',
|
|
42
|
+
index: true,
|
|
43
|
+
filter: true,
|
|
44
|
+
description: `Synced property ${key}`
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (!mappings.attributes[key]) {
|
|
48
|
+
mappings.attributes[key] = {
|
|
49
|
+
expression: `{{raw.${key}}}`,
|
|
50
|
+
description: `Synced property ${key}`
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function mappingAttributes(body) {
|
|
57
|
+
const mappings = /** @type {any} */ (body.fieldMappings);
|
|
58
|
+
if (mappings && mappings.attributes && typeof mappings.attributes === 'object') {
|
|
59
|
+
return mappings.attributes;
|
|
60
|
+
}
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function propertyNameFromAttr(key, row) {
|
|
65
|
+
const expr =
|
|
66
|
+
row && typeof row === 'object' ? String(/** @type {any} */ (row).expression || '') : '';
|
|
67
|
+
const match = expr.match(/\{\{\s*raw\.properties\.([a-zA-Z0-9_]+)\s*\}\}/);
|
|
68
|
+
if (match) {
|
|
69
|
+
return match[1];
|
|
70
|
+
}
|
|
71
|
+
if (expr === `{{raw.${key}}}` || expr.includes(`raw.properties.${key}`)) {
|
|
72
|
+
return key;
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Collect vendor property names from CRM-style attribute expressions + labelKey.
|
|
79
|
+
* @param {Record<string, unknown>} body
|
|
80
|
+
* @returns {string[]}
|
|
81
|
+
*/
|
|
82
|
+
function collectCrmPropertyNames(body) {
|
|
83
|
+
const names = new Set();
|
|
84
|
+
const labelKeys = Array.isArray(body.labelKey) ? body.labelKey : [];
|
|
85
|
+
for (const label of labelKeys) {
|
|
86
|
+
const key = String(label || '').trim();
|
|
87
|
+
if (key) {
|
|
88
|
+
names.add(key);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const [key, row] of Object.entries(mappingAttributes(body))) {
|
|
92
|
+
if (RESERVED_ATTR.has(key)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const name = propertyNameFromAttr(key, row);
|
|
96
|
+
if (name) {
|
|
97
|
+
names.add(name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return [...names].sort();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function ensureCrmPropertiesBag(schema) {
|
|
104
|
+
if (!schema || !schema.properties || typeof schema.properties !== 'object') {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
schema.properties.properties = {
|
|
108
|
+
type: 'object',
|
|
109
|
+
additionalProperties: true,
|
|
110
|
+
description: 'Vendor nested properties bag (CRM object APIs)'
|
|
111
|
+
};
|
|
112
|
+
schema.properties.associations = {
|
|
113
|
+
type: 'object',
|
|
114
|
+
additionalProperties: true,
|
|
115
|
+
description: 'Associated records bag from vendor list/get'
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function rewriteCrmLabelExpressions(mappings, labelKeys) {
|
|
120
|
+
for (const label of labelKeys) {
|
|
121
|
+
if (mappings.attributes[label]) {
|
|
122
|
+
mappings.attributes[label].expression = `{{raw.properties.${label}}}`;
|
|
123
|
+
mappings.attributes[label].description =
|
|
124
|
+
'Display label from vendor properties object';
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function rewriteCrmAttributeExpressions(mappings, labelKeys) {
|
|
130
|
+
for (const [key, row] of Object.entries(mappings.attributes)) {
|
|
131
|
+
if (RESERVED_ATTR.has(key) || labelKeys.includes(key) || !row || typeof row !== 'object') {
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const expr = String(/** @type {any} */ (row).expression || '');
|
|
135
|
+
if (expr === `{{raw.${key}}}` || !expr.includes('raw.properties.')) {
|
|
136
|
+
/** @type {any} */ (row).expression = `{{raw.properties.${key}}}`;
|
|
137
|
+
if (!/** @type {any} */ (row).description) {
|
|
138
|
+
/** @type {any} */ (row).description = `Synced property ${key}`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function ensureCrmIdentityMappings(mappings) {
|
|
145
|
+
if (mappings.attributes.id) {
|
|
146
|
+
mappings.attributes.id.expression = '{{raw.id}}';
|
|
147
|
+
}
|
|
148
|
+
if (mappings.attributes.externalId) {
|
|
149
|
+
mappings.attributes.externalId.expression = '{{raw.id}}';
|
|
150
|
+
}
|
|
151
|
+
if (!mappings.attributes.associations) {
|
|
152
|
+
mappings.attributes.associations = {
|
|
153
|
+
expression: '{{raw.associations}}',
|
|
154
|
+
description: 'Associated records returned when OpenAPI associations query is set'
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* When collection paths look like CRM object APIs, prefer nested properties for labels
|
|
161
|
+
* and request those properties on list/get CIP queries.
|
|
162
|
+
* @param {Record<string, unknown>} body
|
|
163
|
+
* @param {string} samplePath
|
|
164
|
+
* @returns {void}
|
|
165
|
+
*/
|
|
166
|
+
function maybeEnrichCrmStyleFieldMappings(body, samplePath) {
|
|
167
|
+
if (!/\/objects\//i.test(samplePath)) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const mappings = /** @type {any} */ (body.fieldMappings);
|
|
171
|
+
if (!mappings || !mappings.attributes || typeof mappings.attributes !== 'object') {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const labelKeys = Array.isArray(body.labelKey) ? body.labelKey : ['name'];
|
|
175
|
+
ensureCrmPropertiesBag(/** @type {any} */ (body.metadataSchema));
|
|
176
|
+
rewriteCrmLabelExpressions(mappings, labelKeys);
|
|
177
|
+
rewriteCrmAttributeExpressions(mappings, labelKeys);
|
|
178
|
+
ensureCrmIdentityMappings(mappings);
|
|
179
|
+
applyCrmPropertiesQuery(body);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function resolveCipOperations(body) {
|
|
183
|
+
if (
|
|
184
|
+
body.execution &&
|
|
185
|
+
typeof body.execution === 'object' &&
|
|
186
|
+
/** @type {any} */ (body.execution).cip &&
|
|
187
|
+
typeof /** @type {any} */ (body.execution).cip === 'object'
|
|
188
|
+
) {
|
|
189
|
+
return /** @type {any} */ (body.execution).cip.operations;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function applyPropertiesToFetchSteps(op, joined) {
|
|
195
|
+
if (!op || typeof op !== 'object' || !Array.isArray(/** @type {any} */ (op).steps)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
for (const step of /** @type {any} */ (op).steps) {
|
|
199
|
+
if (!step || !step.fetch || typeof step.fetch !== 'object') {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (!step.fetch.query || typeof step.fetch.query !== 'object') {
|
|
203
|
+
step.fetch.query = {};
|
|
204
|
+
}
|
|
205
|
+
step.fetch.query.properties = joined;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Set CIP list/get query.properties from mapped CRM property names.
|
|
211
|
+
* @param {Record<string, unknown>} body
|
|
212
|
+
* @returns {void}
|
|
213
|
+
*/
|
|
214
|
+
function applyCrmPropertiesQuery(body) {
|
|
215
|
+
const propertyNames = collectCrmPropertyNames(body);
|
|
216
|
+
if (propertyNames.length === 0) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const cipOps = resolveCipOperations(body);
|
|
220
|
+
if (!cipOps || typeof cipOps !== 'object') {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const joined = propertyNames.join(',');
|
|
224
|
+
for (const [capKey, op] of Object.entries(cipOps)) {
|
|
225
|
+
const leaf = String(capKey || '')
|
|
226
|
+
.split('.')
|
|
227
|
+
.pop()
|
|
228
|
+
.toLowerCase();
|
|
229
|
+
if (leaf === 'list' || leaf === 'get') {
|
|
230
|
+
applyPropertiesToFetchSteps(op, joined);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
module.exports = {
|
|
236
|
+
seedPropertyKeysOnDatasource,
|
|
237
|
+
collectCrmPropertyNames,
|
|
238
|
+
maybeEnrichCrmStyleFieldMappings,
|
|
239
|
+
applyCrmPropertiesQuery
|
|
240
|
+
};
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bind BID capability keys to OpenAPI method/path (vendor-agnostic heuristics).
|
|
3
|
+
* @fileoverview BID apply OpenAPI enrichment
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const fs = require('../internal/fs-real-sync');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const {
|
|
11
|
+
maybeEnrichCrmStyleFieldMappings
|
|
12
|
+
} = require('./enrich-crm-field-mappings');
|
|
13
|
+
|
|
14
|
+
const PATH_PARAM_RE = /\{(\w+)\}/g;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} ref
|
|
18
|
+
* @param {string} [baseDir]
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
function resolveOpenApiRefPath(ref, baseDir) {
|
|
22
|
+
const trimmed = String(ref || '').trim();
|
|
23
|
+
if (!trimmed) {
|
|
24
|
+
throw new Error('openapiSourceRef is empty');
|
|
25
|
+
}
|
|
26
|
+
if (path.isAbsolute(trimmed)) {
|
|
27
|
+
return trimmed;
|
|
28
|
+
}
|
|
29
|
+
const moduleEnrichmentDir = path.join(__dirname, 'openapi-enrichment');
|
|
30
|
+
// Prefer caller baseDir, then bundled openapi-enrichment, then cwd.
|
|
31
|
+
// Cwd last avoids false misses when Jest mocks fs.existsSync but readFileSync
|
|
32
|
+
// remains real (fallback would otherwise point at <cwd>/<ref>).
|
|
33
|
+
const candidates = [
|
|
34
|
+
baseDir ? path.resolve(baseDir, trimmed) : null,
|
|
35
|
+
path.join(moduleEnrichmentDir, trimmed),
|
|
36
|
+
path.join(moduleEnrichmentDir, path.basename(trimmed)),
|
|
37
|
+
path.resolve(process.cwd(), trimmed)
|
|
38
|
+
].filter(Boolean);
|
|
39
|
+
for (const candidate of candidates) {
|
|
40
|
+
if (fs.existsSync(candidate)) {
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return candidates[0];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} ref
|
|
49
|
+
* @param {string} [baseDir]
|
|
50
|
+
* @returns {Record<string, unknown>}
|
|
51
|
+
*/
|
|
52
|
+
function loadOpenApiDocument(ref, baseDir) {
|
|
53
|
+
const abs = resolveOpenApiRefPath(ref, baseDir);
|
|
54
|
+
const raw = fs.readFileSync(abs, 'utf8');
|
|
55
|
+
const doc = JSON.parse(raw);
|
|
56
|
+
if (!doc || typeof doc !== 'object' || !doc.paths || typeof doc.paths !== 'object') {
|
|
57
|
+
throw new Error(`OpenAPI document missing paths: ${abs}`);
|
|
58
|
+
}
|
|
59
|
+
return doc;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Object-collection segment from paths like `/crm/v3/objects/companies`.
|
|
64
|
+
* @param {string} opPath
|
|
65
|
+
* @returns {string | null}
|
|
66
|
+
*/
|
|
67
|
+
function objectTypeFromOpenApiPath(opPath) {
|
|
68
|
+
const match = String(opPath || '').match(/\/objects\/([^/{?]+)/i);
|
|
69
|
+
const value = match && match[1] ? String(match[1]).trim() : '';
|
|
70
|
+
if (!value) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
// Skip opaque numeric type ids (e.g. HubSpot `0-3`); associations query
|
|
74
|
+
// expects named collection segments when available.
|
|
75
|
+
if (/^\d/.test(value)) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {Record<string, unknown>} doc
|
|
83
|
+
* @param {string} method
|
|
84
|
+
* @param {string} opPath
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
/**
|
|
88
|
+
* @param {Record<string, unknown>} doc
|
|
89
|
+
* @param {string} method
|
|
90
|
+
* @param {string} opPath
|
|
91
|
+
* @returns {any | null}
|
|
92
|
+
*/
|
|
93
|
+
function operationFromDoc(doc, method, opPath) {
|
|
94
|
+
const pathItem = /** @type {any} */ (doc && doc.paths ? doc.paths[opPath] : null);
|
|
95
|
+
if (!pathItem || typeof pathItem !== 'object') {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return (
|
|
99
|
+
pathItem[String(method || '').toLowerCase()] ||
|
|
100
|
+
pathItem[String(method || '').toUpperCase()] ||
|
|
101
|
+
null
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* @param {Record<string, unknown>} doc
|
|
107
|
+
* @param {string} method
|
|
108
|
+
* @param {string} opPath
|
|
109
|
+
* @returns {any[]}
|
|
110
|
+
*/
|
|
111
|
+
function operationParameters(doc, method, opPath) {
|
|
112
|
+
const pathItem = /** @type {any} */ (doc && doc.paths ? doc.paths[opPath] : null);
|
|
113
|
+
const op = operationFromDoc(doc, method, opPath);
|
|
114
|
+
return [
|
|
115
|
+
...(pathItem && Array.isArray(pathItem.parameters) ? pathItem.parameters : []),
|
|
116
|
+
...(op && Array.isArray(op.parameters) ? op.parameters : [])
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function operationDeclaresAssociationsQuery(doc, method, opPath) {
|
|
121
|
+
return operationParameters(doc, method, opPath).some(
|
|
122
|
+
(row) =>
|
|
123
|
+
row &&
|
|
124
|
+
typeof row === 'object' &&
|
|
125
|
+
row.in === 'query' &&
|
|
126
|
+
String(row.name || '').toLowerCase() === 'associations'
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Detect cursor list pagination from OpenAPI query params + response schema/docs.
|
|
132
|
+
* Vendor-agnostic: only emits when both cursor and page-size query params exist and
|
|
133
|
+
* response/docs mention a paging.next.after style cursor field.
|
|
134
|
+
* @param {Record<string, unknown>} doc
|
|
135
|
+
* @param {string} method
|
|
136
|
+
* @param {string} opPath
|
|
137
|
+
* @returns {{ cursorParam: string, pageSizeParam: string, cursorField: string } | null}
|
|
138
|
+
*/
|
|
139
|
+
function operationCursorPagination(doc, method, opPath) {
|
|
140
|
+
const params = operationParameters(doc, method, opPath).filter(
|
|
141
|
+
(row) => row && typeof row === 'object' && row.in === 'query'
|
|
142
|
+
);
|
|
143
|
+
const names = new Set(
|
|
144
|
+
params.map((row) => String(row.name || '').toLowerCase()).filter(Boolean)
|
|
145
|
+
);
|
|
146
|
+
if (!names.has('after') || !names.has('limit')) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
const blob = JSON.stringify({
|
|
150
|
+
params,
|
|
151
|
+
op: operationFromDoc(doc, method, opPath)
|
|
152
|
+
});
|
|
153
|
+
if (!/paging\.next\.after/i.test(blob)) {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
return {
|
|
157
|
+
cursorParam: 'after',
|
|
158
|
+
pageSizeParam: 'limit',
|
|
159
|
+
cursorField: '$.paging.next.after'
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* @param {string} method
|
|
165
|
+
* @param {string} opPath
|
|
166
|
+
* @param {Record<string, unknown>} op
|
|
167
|
+
* @returns {{ method: string, path: string, operationId: string, pathParamCount: number }}
|
|
168
|
+
*/
|
|
169
|
+
function normalizeOp(method, opPath, op) {
|
|
170
|
+
const params = [...String(opPath).matchAll(PATH_PARAM_RE)].map((m) => m[1]);
|
|
171
|
+
return {
|
|
172
|
+
method: String(method || '').toUpperCase(),
|
|
173
|
+
path: String(opPath),
|
|
174
|
+
operationId: typeof op.operationId === 'string' ? op.operationId : '',
|
|
175
|
+
pathParamCount: params.length
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* @param {Record<string, unknown>} doc
|
|
181
|
+
* @returns {Array<{ method: string, path: string, operationId: string, pathParamCount: number }>}
|
|
182
|
+
*/
|
|
183
|
+
function indexOpenApiOperations(doc) {
|
|
184
|
+
const out = [];
|
|
185
|
+
for (const [opPath, methods] of Object.entries(doc.paths || {})) {
|
|
186
|
+
if (!methods || typeof methods !== 'object') {
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
for (const [method, op] of Object.entries(methods)) {
|
|
190
|
+
if (!op || typeof op !== 'object' || method.startsWith('x-')) {
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (!['get', 'post', 'put', 'patch', 'delete'].includes(method.toLowerCase())) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
out.push(normalizeOp(method, opPath, /** @type {Record<string, unknown>} */ (op)));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* @param {string} text
|
|
204
|
+
* @returns {boolean}
|
|
205
|
+
*/
|
|
206
|
+
function isBatchOrSearch(text) {
|
|
207
|
+
// Keep DELETE archive ops; exclude batch/search helpers only.
|
|
208
|
+
return /\/batch\/|\bbatch\b|\/search|merge|upsert/i.test(text);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* @param {string} capabilityKey
|
|
213
|
+
* @param {Array<{ method: string, path: string, operationId: string, pathParamCount: number }>} ops
|
|
214
|
+
* @returns {{ method: string, path: string, operationId: string } | null}
|
|
215
|
+
*/
|
|
216
|
+
function pickFirstMatchingOp(candidates, predicate, idPattern) {
|
|
217
|
+
const hits = candidates.filter(predicate);
|
|
218
|
+
return hits.find((op) => idPattern.test(op.operationId)) || hits[0] || null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function pickOperationForCapability(capabilityKey, ops) {
|
|
222
|
+
const key = String(capabilityKey || '').toLowerCase();
|
|
223
|
+
const candidates = ops.filter((op) => !isBatchOrSearch(`${op.path} ${op.operationId}`));
|
|
224
|
+
const rules = {
|
|
225
|
+
list: {
|
|
226
|
+
predicate: (op) => op.method === 'GET' && op.pathParamCount === 0,
|
|
227
|
+
idPattern: /getpage|list|getall/i
|
|
228
|
+
},
|
|
229
|
+
get: {
|
|
230
|
+
predicate: (op) => op.method === 'GET' && op.pathParamCount >= 1,
|
|
231
|
+
idPattern: /getbyid|get.*id|read/i
|
|
232
|
+
},
|
|
233
|
+
create: {
|
|
234
|
+
predicate: (op) => op.method === 'POST' && op.pathParamCount === 0,
|
|
235
|
+
idPattern: /create$|_create$/i
|
|
236
|
+
},
|
|
237
|
+
createbasic: {
|
|
238
|
+
predicate: (op) => op.method === 'POST' && op.pathParamCount === 0,
|
|
239
|
+
idPattern: /create$|_create$/i
|
|
240
|
+
},
|
|
241
|
+
update: {
|
|
242
|
+
predicate: (op) =>
|
|
243
|
+
(op.method === 'PATCH' || op.method === 'PUT') && op.pathParamCount >= 1,
|
|
244
|
+
idPattern: /update$|_update$/i
|
|
245
|
+
},
|
|
246
|
+
updatebasic: {
|
|
247
|
+
predicate: (op) =>
|
|
248
|
+
(op.method === 'PATCH' || op.method === 'PUT') && op.pathParamCount >= 1,
|
|
249
|
+
idPattern: /update$|_update$/i
|
|
250
|
+
},
|
|
251
|
+
delete: {
|
|
252
|
+
predicate: (op) => op.method === 'DELETE' && op.pathParamCount >= 1,
|
|
253
|
+
idPattern: /archive|delete/i
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
const rule = rules[key];
|
|
257
|
+
if (!rule) {
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
return pickFirstMatchingOp(candidates, rule.predicate, rule.idPattern);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* @param {Record<string, unknown>} body
|
|
265
|
+
* @param {{ method: string, path: string, operationId: string }} binding
|
|
266
|
+
* @param {string} capabilityKey
|
|
267
|
+
* @param {{
|
|
268
|
+
* hasAssociationsQuery?: boolean,
|
|
269
|
+
* associationTargets?: string[],
|
|
270
|
+
* cursorPagination?: { cursorParam: string, pageSizeParam: string, cursorField: string } | null
|
|
271
|
+
* }} [bindOptions]
|
|
272
|
+
* @returns {void}
|
|
273
|
+
*/
|
|
274
|
+
function bindOpenApiOperation(body, binding, capabilityKey) {
|
|
275
|
+
if (!body.openapi || typeof body.openapi !== 'object') {
|
|
276
|
+
body.openapi = {};
|
|
277
|
+
}
|
|
278
|
+
const openapi = /** @type {Record<string, unknown>} */ (body.openapi);
|
|
279
|
+
openapi.enabled = true;
|
|
280
|
+
if (!openapi.operations || typeof openapi.operations !== 'object') {
|
|
281
|
+
openapi.operations = {};
|
|
282
|
+
}
|
|
283
|
+
const ops = /** @type {Record<string, unknown>} */ (openapi.operations);
|
|
284
|
+
const prev = ops[capabilityKey] && typeof ops[capabilityKey] === 'object'
|
|
285
|
+
? /** @type {Record<string, unknown>} */ (ops[capabilityKey])
|
|
286
|
+
: {};
|
|
287
|
+
ops[capabilityKey] = {
|
|
288
|
+
...prev,
|
|
289
|
+
operationId: binding.operationId || prev.operationId || capabilityKey,
|
|
290
|
+
method: binding.method,
|
|
291
|
+
path: binding.path
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function createCipQuery(capabilityKey, bindOptions) {
|
|
296
|
+
const cap = String(capabilityKey || '').toLowerCase();
|
|
297
|
+
const query = {};
|
|
298
|
+
const targets = Array.isArray(bindOptions.associationTargets)
|
|
299
|
+
? bindOptions.associationTargets.map((row) => String(row || '').trim()).filter(Boolean)
|
|
300
|
+
: [];
|
|
301
|
+
if (targets.length > 0 && bindOptions.hasAssociationsQuery && (cap === 'list' || cap === 'get')) {
|
|
302
|
+
query.associations = targets.join(',');
|
|
303
|
+
}
|
|
304
|
+
const cursorPagination =
|
|
305
|
+
bindOptions.cursorPagination && typeof bindOptions.cursorPagination === 'object'
|
|
306
|
+
? bindOptions.cursorPagination
|
|
307
|
+
: null;
|
|
308
|
+
if (cap === 'list' && cursorPagination) {
|
|
309
|
+
query[cursorPagination.pageSizeParam] = `{{${cursorPagination.pageSizeParam}}}`;
|
|
310
|
+
query[cursorPagination.cursorParam] = `{{${cursorPagination.cursorParam}}}`;
|
|
311
|
+
}
|
|
312
|
+
return { cap, query, cursorPagination };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function createCipSteps(capabilityKey, binding, cipQuery) {
|
|
316
|
+
const { cap, query, cursorPagination } = cipQuery;
|
|
317
|
+
const steps = [{ fetch: { source: 'openapi', openapiRef: capabilityKey, query } }];
|
|
318
|
+
if (cap === 'list' && /\/objects\//i.test(binding.path)) {
|
|
319
|
+
if (cursorPagination) {
|
|
320
|
+
steps.push({
|
|
321
|
+
paginate: {
|
|
322
|
+
strategy: 'cursor',
|
|
323
|
+
cursorField: cursorPagination.cursorField,
|
|
324
|
+
cursorParam: cursorPagination.cursorParam,
|
|
325
|
+
pageSizeParam: cursorPagination.pageSizeParam,
|
|
326
|
+
pageSize: 100,
|
|
327
|
+
maxPages: 100
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
steps.push({ map: { useFieldMappings: true, inputPath: '$.results[*]' } });
|
|
332
|
+
} else if (cap === 'get' || /^create/.test(cap) || /^update/.test(cap) || cap === 'delete' || cap === 'archive') {
|
|
333
|
+
steps.push({ map: { useFieldMappings: true, inputPath: '$' } });
|
|
334
|
+
}
|
|
335
|
+
steps.push({ output: { mode: 'records' } });
|
|
336
|
+
return steps;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function bindCipOperation(body, binding, capabilityKey, bindOptions) {
|
|
340
|
+
if (!body.execution || typeof body.execution !== 'object') {
|
|
341
|
+
body.execution = { engine: 'cip' };
|
|
342
|
+
}
|
|
343
|
+
const execution = /** @type {Record<string, unknown>} */ (body.execution);
|
|
344
|
+
if (!execution.cip || typeof execution.cip !== 'object') {
|
|
345
|
+
execution.cip = { version: '1.0', operations: {} };
|
|
346
|
+
}
|
|
347
|
+
const cip = /** @type {Record<string, unknown>} */ (execution.cip);
|
|
348
|
+
if (!cip.operations || typeof cip.operations !== 'object') {
|
|
349
|
+
cip.operations = {};
|
|
350
|
+
}
|
|
351
|
+
const cipOps = /** @type {Record<string, unknown>} */ (cip.operations);
|
|
352
|
+
const existing = cipOps[capabilityKey];
|
|
353
|
+
const steps = createCipSteps(capabilityKey, binding, createCipQuery(capabilityKey, bindOptions));
|
|
354
|
+
cipOps[capabilityKey] = {
|
|
355
|
+
...(existing && typeof existing === 'object' ? existing : {}),
|
|
356
|
+
enabled: true,
|
|
357
|
+
description:
|
|
358
|
+
(existing &&
|
|
359
|
+
typeof existing === 'object' &&
|
|
360
|
+
/** @type {any} */ (existing).description) ||
|
|
361
|
+
`Execute ${capabilityKey} via OpenAPI`,
|
|
362
|
+
steps
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function applyBindingToDatasource(body, binding, capabilityKey, bindOptions = {}) {
|
|
367
|
+
bindOpenApiOperation(body, binding, capabilityKey);
|
|
368
|
+
bindCipOperation(body, binding, capabilityKey, bindOptions);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Enrich datasource openapi.operations + CIP steps from an OpenAPI document.
|
|
373
|
+
* @param {Record<string, unknown>} body - datasource body (mutated)
|
|
374
|
+
* @param {string} openapiSourceRef
|
|
375
|
+
* @param {{ baseDir?: string, associationTargets?: string[] }} [options]
|
|
376
|
+
* @returns {string[]} capability keys that received method/path bindings
|
|
377
|
+
*/
|
|
378
|
+
function enrichDatasourceFromOpenApi(body, openapiSourceRef, options = {}) {
|
|
379
|
+
const doc = loadOpenApiDocument(openapiSourceRef, options.baseDir);
|
|
380
|
+
const indexed = indexOpenApiOperations(doc);
|
|
381
|
+
const caps = Array.isArray(body.capabilities)
|
|
382
|
+
? body.capabilities
|
|
383
|
+
.map((row) => (row && typeof row === 'object' ? row.key : row))
|
|
384
|
+
.filter((k) => typeof k === 'string' && k.trim())
|
|
385
|
+
: [];
|
|
386
|
+
const selfTypeCandidates = indexed
|
|
387
|
+
.map((op) => objectTypeFromOpenApiPath(op.path))
|
|
388
|
+
.filter(Boolean);
|
|
389
|
+
// Opaque OpenAPI paths (e.g. /objects/0-3) yield no segment — fall back to BID ds key.
|
|
390
|
+
const selfType =
|
|
391
|
+
selfTypeCandidates[0] ||
|
|
392
|
+
(typeof body.key === 'string' && body.key.trim() ? body.key.trim() : null);
|
|
393
|
+
const associationTargets = (
|
|
394
|
+
Array.isArray(options.associationTargets) ? options.associationTargets : []
|
|
395
|
+
)
|
|
396
|
+
.map((row) => String(row || '').trim())
|
|
397
|
+
.filter((row) => row && row !== selfType);
|
|
398
|
+
const bound = [];
|
|
399
|
+
let samplePath = '';
|
|
400
|
+
for (const capabilityKey of caps) {
|
|
401
|
+
const picked = pickOperationForCapability(capabilityKey, indexed);
|
|
402
|
+
if (!picked) {
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
applyBindingToDatasource(body, picked, capabilityKey, {
|
|
406
|
+
hasAssociationsQuery: operationDeclaresAssociationsQuery(
|
|
407
|
+
doc,
|
|
408
|
+
picked.method,
|
|
409
|
+
picked.path
|
|
410
|
+
),
|
|
411
|
+
associationTargets,
|
|
412
|
+
cursorPagination: operationCursorPagination(doc, picked.method, picked.path)
|
|
413
|
+
});
|
|
414
|
+
bound.push(capabilityKey);
|
|
415
|
+
if (!samplePath) {
|
|
416
|
+
samplePath = picked.path;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (samplePath) {
|
|
420
|
+
maybeEnrichCrmStyleFieldMappings(body, samplePath);
|
|
421
|
+
}
|
|
422
|
+
return bound;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
module.exports = {
|
|
426
|
+
resolveOpenApiRefPath,
|
|
427
|
+
loadOpenApiDocument,
|
|
428
|
+
indexOpenApiOperations,
|
|
429
|
+
pickOperationForCapability,
|
|
430
|
+
enrichDatasourceFromOpenApi,
|
|
431
|
+
objectTypeFromOpenApiPath,
|
|
432
|
+
operationDeclaresAssociationsQuery,
|
|
433
|
+
operationCursorPagination
|
|
434
|
+
};
|