sap-ai-dev-toolkit 0.9.9922 → 0.9.9924
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/.github/agents/sap-btp-infrastructure-admin.agent.md +23 -0
- package/.github/agents/sap-bw-datasphere-migration-specialist.agent.md +22 -0
- package/.github/agents/sap-datasphere-developer.agent.md +22 -0
- package/.github/agents/sap-devops-observability-specialist.agent.md +54 -0
- package/.github/agents/sap-process-automation-developer.agent.md +50 -0
- package/.github/skills/btp-platform-administration/SKILL.md +53 -0
- package/.github/skills/bw-datasphere-migration/SKILL.md +66 -0
- package/.github/skills/cloud-alm-operations/SKILL.md +135 -0
- package/.github/skills/datasphere-administration/SKILL.md +40 -0
- package/.github/skills/datasphere-development/SKILL.md +55 -0
- package/.github/skills/process-automation-development/SKILL.md +113 -0
- package/NOTICE +2 -1
- package/README.md +178 -12
- package/dist/checksums.txt +5 -5
- package/dist/vsp-darwin-arm64 +0 -0
- package/dist/vsp-darwin-x64 +0 -0
- package/dist/vsp-linux-arm64 +0 -0
- package/dist/vsp-linux-x64 +0 -0
- package/dist/vsp-win32-x64.exe +0 -0
- package/inventory.md +60 -0
- package/package.json +8 -2
- package/patches/vsp-single-logon.patch +258 -0
- package/scripts/build-vsp.mjs +1 -1
- package/src/btp-cli.mjs +199 -0
- package/src/btp-server.mjs +125 -0
- package/src/btp-tools.mjs +776 -0
- package/src/cloud-alm-client.mjs +44 -0
- package/src/cloud-alm-config.mjs +92 -0
- package/src/cloud-alm-credentials.mjs +29 -0
- package/src/cloud-alm-data.mjs +129 -0
- package/src/cloud-alm-server.mjs +115 -0
- package/src/cloud-alm-tools.mjs +682 -0
- package/src/datasphere-auth.mjs +279 -0
- package/src/datasphere-cli.mjs +220 -0
- package/src/datasphere-command.mjs +236 -0
- package/src/datasphere-config.mjs +154 -0
- package/src/datasphere-credentials.mjs +37 -0
- package/src/datasphere-csn.mjs +73 -0
- package/src/datasphere-discovery.mjs +151 -0
- package/src/datasphere-server.mjs +249 -0
- package/src/datasphere-tools.mjs +809 -0
- package/src/hana-database.mjs +3 -3
- package/src/hana-inspector.mjs +2 -2
- package/src/harnesses.mjs +31 -1
- package/src/install-user-assets.mjs +1 -1
- package/src/integration-common.mjs +7 -7
- package/src/integration-config.mjs +5 -0
- package/src/integration-credentials.mjs +25 -16
- package/src/integration-http.mjs +24 -7
- package/src/launcher.mjs +56 -4
- package/src/mcp-config.mjs +37 -1
- package/src/process-automation-automation-tools.mjs +125 -0
- package/src/process-automation-client.mjs +36 -0
- package/src/process-automation-common.mjs +69 -0
- package/src/process-automation-config.mjs +142 -0
- package/src/process-automation-credentials.mjs +37 -0
- package/src/process-automation-inbox-tools.mjs +246 -0
- package/src/process-automation-server.mjs +139 -0
- package/src/process-automation-tools.mjs +727 -0
- package/src/process-automation-visibility-tools.mjs +182 -0
- package/src/setup.mjs +623 -11
- package/tools.md +138 -0
|
@@ -0,0 +1,727 @@
|
|
|
1
|
+
// MCP tools for SAP Build Process Automation. This module owns the Workflow
|
|
2
|
+
// Runtime API (definitions, instances, user tasks, admin jobs), decisions and
|
|
3
|
+
// business events, and registers the Inbox, Automation/Unified trigger and
|
|
4
|
+
// Visibility modules. Every write checks an exact confirmation string before any
|
|
5
|
+
// tenant call, and every result passes through secret redaction.
|
|
6
|
+
import { randomUUID } from 'node:crypto';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import {
|
|
9
|
+
CREATE_ANNOTATIONS,
|
|
10
|
+
OVERWRITE_ANNOTATIONS,
|
|
11
|
+
READ_ONLY_ANNOTATIONS,
|
|
12
|
+
confirmation,
|
|
13
|
+
pick,
|
|
14
|
+
redactSecrets,
|
|
15
|
+
requireConfirmation,
|
|
16
|
+
resultOf
|
|
17
|
+
} from './integration-common.mjs';
|
|
18
|
+
import { IntegrationError } from './integration-http.mjs';
|
|
19
|
+
import {
|
|
20
|
+
EVENTS,
|
|
21
|
+
RULES,
|
|
22
|
+
WORKFLOW,
|
|
23
|
+
apiKeyHeaders,
|
|
24
|
+
capContext,
|
|
25
|
+
enc,
|
|
26
|
+
environmentId,
|
|
27
|
+
environmentQuery,
|
|
28
|
+
guid,
|
|
29
|
+
identifier,
|
|
30
|
+
outputFile,
|
|
31
|
+
payload,
|
|
32
|
+
rows,
|
|
33
|
+
skip,
|
|
34
|
+
timestamp,
|
|
35
|
+
top,
|
|
36
|
+
writeExportFile
|
|
37
|
+
} from './process-automation-common.mjs';
|
|
38
|
+
import { INBOX_TOOL_NAMES, registerInboxTools } from './process-automation-inbox-tools.mjs';
|
|
39
|
+
import { AUTOMATION_TOOL_NAMES, registerAutomationTools } from './process-automation-automation-tools.mjs';
|
|
40
|
+
import { VISIBILITY_TOOL_NAMES, registerVisibilityTools } from './process-automation-visibility-tools.mjs';
|
|
41
|
+
|
|
42
|
+
const ERROR_DETAIL_LIMIT = 10;
|
|
43
|
+
const TRACE_LOG_LIMIT = 20;
|
|
44
|
+
const TRACE_TASK_LIMIT = 50;
|
|
45
|
+
|
|
46
|
+
const definitionId = identifier('process definition id');
|
|
47
|
+
const ruleServiceId = identifier('decision rule service id');
|
|
48
|
+
const INSTANCE_STATUS = z.enum(['RUNNING', 'ERRONEOUS', 'SUSPENDED', 'CANCELED', 'COMPLETED']);
|
|
49
|
+
const TASK_STATUS = z.enum(['READY', 'RESERVED', 'COMPLETED', 'CANCELED']);
|
|
50
|
+
const PRIORITY = z.enum(['LOW', 'MEDIUM', 'HIGH', 'VERY_HIGH']);
|
|
51
|
+
const OPEN_TASK_STATUSES = new Set(['READY', 'RESERVED']);
|
|
52
|
+
const userList = z.array(z.string().min(1).max(255)).max(100);
|
|
53
|
+
|
|
54
|
+
const INSTANCE_FIELDS = 'id,definitionId,definitionVersion,subject,status,businessKey,startedAt,startedBy,completedAt,parentInstanceId,rootInstanceId,projectId,projectVersion,applicationScope,attributes'.split(',');
|
|
55
|
+
const TASK_FIELDS = 'id,activityId,subject,description,status,priority,dueDate,createdAt,createdBy,claimedAt,completedAt,lastChangedAt,processor,recipientUsers,recipientGroups,workflowInstanceId,workflowDefinitionId,definitionId,applicationScope,userInterfaceUri,attributes'.split(',');
|
|
56
|
+
const DEFINITION_FIELDS = 'id,name,version,createdAt,createdBy,applicationScope,jobs'.split(',');
|
|
57
|
+
|
|
58
|
+
const TRANSITIONS = Object.freeze({
|
|
59
|
+
cancel: Object.freeze({ from: ['RUNNING', 'ERRONEOUS', 'SUSPENDED'], to: 'CANCELED' }),
|
|
60
|
+
suspend: Object.freeze({ from: ['RUNNING', 'ERRONEOUS'], to: 'SUSPENDED' }),
|
|
61
|
+
resume: Object.freeze({ from: ['SUSPENDED'], to: 'RUNNING' }),
|
|
62
|
+
retry: Object.freeze({ from: ['ERRONEOUS'], to: 'RUNNING' })
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const INSTANCE_ORDER = z.enum(['startedAt desc', 'startedAt asc', 'completedAt desc', 'completedAt asc', 'definitionId asc', 'definitionId desc', 'subject asc', 'subject desc', 'businessKey asc', 'businessKey desc', 'id asc', 'id desc']);
|
|
66
|
+
const TASK_ORDER = z.enum(['createdAt desc', 'createdAt asc', 'dueDate asc', 'dueDate desc', 'priority desc', 'priority asc', 'claimedAt desc', 'claimedAt asc', 'completedAt desc', 'completedAt asc', 'lastChangedAt desc', 'lastChangedAt asc', 'subject asc', 'subject desc']);
|
|
67
|
+
const DEFINITION_ORDER = z.enum(['id asc', 'id desc', 'name asc', 'name desc', 'version asc', 'version desc', 'createdAt asc', 'createdAt desc']);
|
|
68
|
+
|
|
69
|
+
export const WORKFLOW_TOOL_NAMES = Object.freeze([
|
|
70
|
+
'bpa_connection_info',
|
|
71
|
+
'bpa_list_process_definitions',
|
|
72
|
+
'bpa_get_process_definition',
|
|
73
|
+
'bpa_delete_process_definition',
|
|
74
|
+
'bpa_list_task_definitions',
|
|
75
|
+
'bpa_delete_form',
|
|
76
|
+
'bpa_list_instances',
|
|
77
|
+
'bpa_get_instance',
|
|
78
|
+
'bpa_find_stuck_instances',
|
|
79
|
+
'bpa_trace_process',
|
|
80
|
+
'bpa_start_instance',
|
|
81
|
+
'bpa_change_instance_status',
|
|
82
|
+
'bpa_update_instance_context',
|
|
83
|
+
'bpa_update_instance_roles',
|
|
84
|
+
'bpa_delete_instances',
|
|
85
|
+
'bpa_send_message',
|
|
86
|
+
'bpa_list_tasks',
|
|
87
|
+
'bpa_get_task',
|
|
88
|
+
'bpa_complete_task',
|
|
89
|
+
'bpa_update_tasks',
|
|
90
|
+
'bpa_update_task_context',
|
|
91
|
+
'bpa_export_workflow_data',
|
|
92
|
+
'bpa_purge_workflow_data',
|
|
93
|
+
'bpa_get_job',
|
|
94
|
+
'bpa_invoke_decision',
|
|
95
|
+
'bpa_test_decision',
|
|
96
|
+
'bpa_send_business_event'
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
export const PROCESS_AUTOMATION_TOOL_NAMES = Object.freeze([
|
|
100
|
+
...WORKFLOW_TOOL_NAMES,
|
|
101
|
+
...INBOX_TOOL_NAMES,
|
|
102
|
+
...AUTOMATION_TOOL_NAMES,
|
|
103
|
+
...VISIBILITY_TOOL_NAMES
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
function isPlainObject(value) {
|
|
107
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Expected values are a subset: extra actual keys/elements are allowed. */
|
|
111
|
+
function subsetMismatches(expected, actual, path = '$') {
|
|
112
|
+
if (Array.isArray(expected)) {
|
|
113
|
+
if (!Array.isArray(actual) || actual.length < expected.length) return [{ path, expected, actual }];
|
|
114
|
+
return expected.flatMap((item, index) => subsetMismatches(item, actual[index], `${path}[${index}]`));
|
|
115
|
+
}
|
|
116
|
+
if (isPlainObject(expected)) {
|
|
117
|
+
if (!isPlainObject(actual)) return [{ path, expected, actual }];
|
|
118
|
+
return Object.keys(expected).flatMap(name => (Object.hasOwn(actual, name)
|
|
119
|
+
? subsetMismatches(expected[name], actual[name], `${path}.${name}`)
|
|
120
|
+
: [{ path: `${path}.${name}`, expected: expected[name], actual: undefined }]));
|
|
121
|
+
}
|
|
122
|
+
return Object.is(expected, actual) ? [] : [{ path, expected, actual }];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function transition(action, instanceId, status) {
|
|
126
|
+
const { from, to } = TRANSITIONS[action];
|
|
127
|
+
if (!from.includes(status)) throw new IntegrationError(`instance ${instanceId} is ${status}; ${action} requires ${from.join(' or ')}`);
|
|
128
|
+
return to;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** 202/303 responses of asynchronous workflow operations point at /v1/jobs/{id}. */
|
|
132
|
+
function asyncOutcome({ status, responseHeaders }) {
|
|
133
|
+
const jobId = /\/jobs\/([^/?#]+)/.exec(responseHeaders?.location ?? '')?.[1];
|
|
134
|
+
return {
|
|
135
|
+
status,
|
|
136
|
+
...(status === 303 ? { already_running: true } : {}),
|
|
137
|
+
...(jobId ? { job_id: decodeURIComponent(jobId) } : {})
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Register all SAP Build Process Automation tools; returns the registered names. */
|
|
142
|
+
export function registerProcessAutomationTools(server, { rest, summary, verify, access = {}, now = Date.now } = {}) {
|
|
143
|
+
const registeredNames = [];
|
|
144
|
+
const register = (name, title, description, inputSchema, annotations, handler) => {
|
|
145
|
+
registeredNames.push(name);
|
|
146
|
+
server.registerTool(name, { title, description, inputSchema, annotations }, async args => resultOf(
|
|
147
|
+
async () => redactSecrets(await handler(args)),
|
|
148
|
+
{ product: 'SAP Build Process Automation', fallback: 'SAP Build Process Automation request failed. Check the service key, the user\'s role collections, the API key scopes and API reachability.' }
|
|
149
|
+
));
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const get = (path, query) => rest.request('GET', `${WORKFLOW}${path}`, query ? { query } : {});
|
|
153
|
+
const getInstance = id => get(`/workflow-instances/${enc(id)}`);
|
|
154
|
+
const getTask = id => get(`/task-instances/${enc(id)}`);
|
|
155
|
+
const errorMessages = async id => rows(await get(`/workflow-instances/${enc(id)}/error-messages`));
|
|
156
|
+
const invokeDecision = (id, vocabulary, revision, version) => rest.request('POST', RULES, {
|
|
157
|
+
json: {
|
|
158
|
+
RuleServiceId: id,
|
|
159
|
+
Vocabulary: vocabulary,
|
|
160
|
+
...(revision ? { RuleServiceRevision: revision } : {}),
|
|
161
|
+
...(version ? { RuleServiceVersion: version } : {})
|
|
162
|
+
},
|
|
163
|
+
expect: [200]
|
|
164
|
+
});
|
|
165
|
+
const asyncCall = async (method, path, options = {}) => asyncOutcome(await rest.request(method, `${WORKFLOW}${path}`, {
|
|
166
|
+
...options,
|
|
167
|
+
responseType: 'none',
|
|
168
|
+
withStatus: true,
|
|
169
|
+
responseHeaders: ['location']
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
// ------------------------------------------------------------ connection
|
|
173
|
+
|
|
174
|
+
register('bpa_connection_info', 'Process Automation connection info',
|
|
175
|
+
'Report the connected API host, token host, auth mode, credential source, task attribution, and whether an environment API key and default environment are configured. Verification is read-only and never returns credentials.',
|
|
176
|
+
{ verify: z.boolean().default(false) },
|
|
177
|
+
READ_ONLY_ANNOTATIONS,
|
|
178
|
+
async ({ verify: online }) => {
|
|
179
|
+
const verification = online && verify ? await verify() : undefined;
|
|
180
|
+
return { ...summary, ...(verification ? { verification } : {}) };
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ------------------------------------------------------------ definitions
|
|
184
|
+
|
|
185
|
+
register('bpa_list_process_definitions', 'List process definitions',
|
|
186
|
+
'List the latest version of each deployed process (workflow) definition. Roles: ProcessAutomationAdmin or ProcessAutomationDeveloper.',
|
|
187
|
+
{ top: top(200, 100), skip, orderby: DEFINITION_ORDER.optional() },
|
|
188
|
+
READ_ONLY_ANNOTATIONS,
|
|
189
|
+
async ({ top: limit, skip: offset, orderby }) => rows(await get('/workflow-definitions', { $top: limit, $skip: offset, $orderby: orderby }))
|
|
190
|
+
.map(row => pick(row, DEFINITION_FIELDS)));
|
|
191
|
+
|
|
192
|
+
register('bpa_get_process_definition', 'Get process definition',
|
|
193
|
+
'Read a deployed process definition (latest or one version), optionally with its model, its default start context and the list of deployed versions.',
|
|
194
|
+
{
|
|
195
|
+
definition_id: definitionId,
|
|
196
|
+
version: z.string().regex(/^\d{1,9}$/, 'expected a version number').optional(),
|
|
197
|
+
include: z.array(z.enum(['model', 'sample_start_context', 'versions'])).max(3).default([])
|
|
198
|
+
},
|
|
199
|
+
READ_ONLY_ANNOTATIONS,
|
|
200
|
+
async ({ definition_id: id, version, include }) => {
|
|
201
|
+
const base = `/workflow-definitions/${enc(id)}${version ? `/versions/${enc(version)}` : ''}`;
|
|
202
|
+
const wanted = new Set(include);
|
|
203
|
+
const result = { definition: await get(base) };
|
|
204
|
+
if (wanted.has('model')) result.model = await get(`${base}/model`);
|
|
205
|
+
if (wanted.has('sample_start_context')) result.sample_start_context = await get(`${base}/sample-contexts/default-start-context`);
|
|
206
|
+
if (wanted.has('versions')) result.versions = rows(await get(`/workflow-definitions/${enc(id)}/versions`, { $orderby: 'version desc' })).map(row => pick(row, DEFINITION_FIELDS));
|
|
207
|
+
return result;
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
register('bpa_delete_process_definition', 'Undeploy process definition',
|
|
211
|
+
'Undeploy all versions of a classic workflow definition (SAP Business Application Studio) and delete its instances; asynchronous, returns the job id for bpa_get_job. Processes built in SAP Build are deleted through their project instead. Role: ProcessAutomationAdmin. Requires confirmation "DELETE BPA DEFINITION <definition_id>".',
|
|
212
|
+
{ definition_id: definitionId, cascade: z.boolean().default(false), confirmation },
|
|
213
|
+
OVERWRITE_ANNOTATIONS,
|
|
214
|
+
async ({ definition_id: id, cascade, confirmation: given }) => {
|
|
215
|
+
requireConfirmation(given, `DELETE BPA DEFINITION ${id}`);
|
|
216
|
+
return asyncCall('DELETE', `/workflow-definitions/${enc(id)}`, { query: { cascade }, expect: [202, 303] });
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
register('bpa_list_task_definitions', 'List task definitions',
|
|
220
|
+
'List user-task definitions (activity id + workflow definition id), optionally with their custom attribute definitions.',
|
|
221
|
+
{ top: top(200, 100), skip, include_attribute_definitions: z.boolean().default(false) },
|
|
222
|
+
READ_ONLY_ANNOTATIONS,
|
|
223
|
+
async ({ top: limit, skip: offset, include_attribute_definitions: expand }) => rows(await get('/task-definitions', {
|
|
224
|
+
$top: limit,
|
|
225
|
+
$skip: offset,
|
|
226
|
+
...(expand ? { $expand: 'attributeDefinitions' } : {})
|
|
227
|
+
})));
|
|
228
|
+
|
|
229
|
+
register('bpa_delete_form', 'Undeploy classic form',
|
|
230
|
+
'Undeploy all versions of a classic workflow form (SAP Business Application Studio). Role: ProcessAutomationAdmin. Requires confirmation "DELETE BPA FORM <form_id>".',
|
|
231
|
+
{ form_id: identifier('form id'), confirmation },
|
|
232
|
+
OVERWRITE_ANNOTATIONS,
|
|
233
|
+
async ({ form_id: id, confirmation: given }) => {
|
|
234
|
+
requireConfirmation(given, `DELETE BPA FORM ${id}`);
|
|
235
|
+
await rest.request('DELETE', `${WORKFLOW}/forms/${enc(id)}`, { expect: [204], responseType: 'none' });
|
|
236
|
+
return { deleted: id };
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ------------------------------------------------------------ instances
|
|
240
|
+
|
|
241
|
+
register('bpa_list_instances', 'List workflow instances',
|
|
242
|
+
'List workflow instances (default newest first). Multiple statuses are OR-combined; other filters are AND-combined. Without a status filter BPA returns RUNNING and ERRONEOUS instances. Role: ProcessAutomationAdmin.',
|
|
243
|
+
{
|
|
244
|
+
definition_id: definitionId.optional(),
|
|
245
|
+
definition_version: z.string().regex(/^\d{1,9}$/).optional(),
|
|
246
|
+
status: z.array(INSTANCE_STATUS).min(1).max(5).optional(),
|
|
247
|
+
business_key: z.string().min(1).max(255).optional(),
|
|
248
|
+
contains_text: z.string().min(1).max(255).optional(),
|
|
249
|
+
subject: z.string().min(1).max(255).optional(),
|
|
250
|
+
started_by: z.string().min(1).max(255).optional(),
|
|
251
|
+
started_from: timestamp.optional(),
|
|
252
|
+
started_up_to: timestamp.optional(),
|
|
253
|
+
completed_from: timestamp.optional(),
|
|
254
|
+
completed_up_to: timestamp.optional(),
|
|
255
|
+
root_instance_id: guid.optional(),
|
|
256
|
+
parent_instance_id: guid.optional(),
|
|
257
|
+
project_id: z.string().min(1).max(255).optional(),
|
|
258
|
+
project_version: z.string().min(1).max(64).optional(),
|
|
259
|
+
include_attributes: z.boolean().default(false),
|
|
260
|
+
orderby: INSTANCE_ORDER.default('startedAt desc'),
|
|
261
|
+
top: top(200, 50),
|
|
262
|
+
skip
|
|
263
|
+
},
|
|
264
|
+
READ_ONLY_ANNOTATIONS,
|
|
265
|
+
async args => rows(await get('/workflow-instances', {
|
|
266
|
+
definitionId: args.definition_id,
|
|
267
|
+
definitionVersion: args.definition_version,
|
|
268
|
+
status: args.status,
|
|
269
|
+
businessKey: args.business_key,
|
|
270
|
+
containsText: args.contains_text,
|
|
271
|
+
subject: args.subject,
|
|
272
|
+
startedBy: args.started_by,
|
|
273
|
+
startedFrom: args.started_from,
|
|
274
|
+
startedUpTo: args.started_up_to,
|
|
275
|
+
completedFrom: args.completed_from,
|
|
276
|
+
completedUpTo: args.completed_up_to,
|
|
277
|
+
rootInstanceId: args.root_instance_id,
|
|
278
|
+
parentInstanceId: args.parent_instance_id,
|
|
279
|
+
projectId: args.project_id,
|
|
280
|
+
projectVersion: args.project_version,
|
|
281
|
+
...(args.include_attributes ? { $expand: 'attributes' } : {}),
|
|
282
|
+
$orderby: args.orderby,
|
|
283
|
+
$top: args.top,
|
|
284
|
+
$skip: args.skip
|
|
285
|
+
})).map(row => pick(row, INSTANCE_FIELDS)));
|
|
286
|
+
|
|
287
|
+
register('bpa_get_instance', 'Get workflow instance',
|
|
288
|
+
'Read one workflow instance with optional context (capped at 48 KiB, or one context fragment by path), outputs (COMPLETED only), execution logs, error messages, custom attributes and instance role assignments.',
|
|
289
|
+
{
|
|
290
|
+
instance_id: guid,
|
|
291
|
+
include: z.array(z.enum(['context', 'outputs', 'execution_logs', 'error_messages', 'attributes', 'roles'])).max(6).default(['error_messages']),
|
|
292
|
+
context_path: z.string().regex(/^[A-Za-z0-9_$][A-Za-z0-9_$./-]{0,499}$/, 'expected a context path such as order/items').optional()
|
|
293
|
+
.describe('Read only this context fragment (e.g. order/items) instead of the whole context.'),
|
|
294
|
+
log_limit: z.number().int().min(1).max(200).default(50)
|
|
295
|
+
},
|
|
296
|
+
READ_ONLY_ANNOTATIONS,
|
|
297
|
+
async ({ instance_id: id, include, context_path: contextPath, log_limit: logLimit }) => {
|
|
298
|
+
const base = `/workflow-instances/${enc(id)}`;
|
|
299
|
+
const result = { instance: await get(base) };
|
|
300
|
+
const wanted = new Set(include);
|
|
301
|
+
if (contextPath) {
|
|
302
|
+
const path = contextPath.split('/').map(enc).join('/');
|
|
303
|
+
Object.assign(result, capContext(await get(`${base}/context/${path}`)));
|
|
304
|
+
} else if (wanted.has('context')) {
|
|
305
|
+
Object.assign(result, capContext(await get(`${base}/context`)));
|
|
306
|
+
}
|
|
307
|
+
if (wanted.has('outputs')) Object.assign(result, capContext(await get(`${base}/outputs`), 'outputs'));
|
|
308
|
+
if (wanted.has('execution_logs')) result.execution_logs = rows(await get(`${base}/execution-logs`)).slice(-logLimit);
|
|
309
|
+
if (wanted.has('error_messages')) result.error_messages = await errorMessages(id);
|
|
310
|
+
if (wanted.has('attributes')) result.attributes = await get(`${base}/attributes`);
|
|
311
|
+
if (wanted.has('roles')) result.roles = await get(`${base}/roles`);
|
|
312
|
+
return result;
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
register('bpa_find_stuck_instances', 'Find stuck workflow instances',
|
|
316
|
+
'Find erroneous instances (with their first error messages), suspended instances, and running instances older than a threshold.',
|
|
317
|
+
{
|
|
318
|
+
definition_id: definitionId.optional(),
|
|
319
|
+
running_longer_than_minutes: z.number().int().min(1).max(43_200).default(60),
|
|
320
|
+
top: top(100, 25)
|
|
321
|
+
},
|
|
322
|
+
READ_ONLY_ANNOTATIONS,
|
|
323
|
+
async ({ definition_id, running_longer_than_minutes: minutes, top: limit }) => {
|
|
324
|
+
const byStatus = async status => rows(await get('/workflow-instances', {
|
|
325
|
+
definitionId: definition_id,
|
|
326
|
+
status,
|
|
327
|
+
$orderby: 'startedAt asc',
|
|
328
|
+
$top: limit
|
|
329
|
+
})).map(row => pick(row, INSTANCE_FIELDS));
|
|
330
|
+
const [erroneous, suspended, running] = [await byStatus('ERRONEOUS'), await byStatus('SUSPENDED'), await byStatus('RUNNING')];
|
|
331
|
+
const cutoff = now() - minutes * 60_000;
|
|
332
|
+
for (const row of erroneous.slice(0, ERROR_DETAIL_LIMIT)) row.error_messages = await errorMessages(row.id);
|
|
333
|
+
return {
|
|
334
|
+
erroneous,
|
|
335
|
+
suspended,
|
|
336
|
+
long_running: running.filter(row => Date.parse(row.startedAt) < cutoff),
|
|
337
|
+
cutoff: new Date(cutoff).toISOString()
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
register('bpa_trace_process', 'Trace process end to end',
|
|
342
|
+
'Resolve workflow instances by id, business key or contained text (e.g. the correlation value from the ABAP/RAP event or CPI message) and return per instance its status, error messages, user tasks (all statuses; open ones appear in My Inbox / Fiori Task Center) and recent execution logs.',
|
|
343
|
+
z.object({
|
|
344
|
+
instance_id: guid.optional(),
|
|
345
|
+
business_key: z.string().min(1).max(255).optional(),
|
|
346
|
+
contains_text: z.string().min(1).max(255).optional(),
|
|
347
|
+
max_instances: z.number().int().min(1).max(10).default(5)
|
|
348
|
+
}).refine(
|
|
349
|
+
args => [args.instance_id, args.business_key, args.contains_text].filter(value => value !== undefined).length === 1,
|
|
350
|
+
'provide exactly one of instance_id, business_key, contains_text'
|
|
351
|
+
),
|
|
352
|
+
READ_ONLY_ANNOTATIONS,
|
|
353
|
+
async ({ instance_id, business_key, contains_text, max_instances: maxInstances }) => {
|
|
354
|
+
const instances = instance_id
|
|
355
|
+
? [await getInstance(instance_id)]
|
|
356
|
+
: rows(await get('/workflow-instances', {
|
|
357
|
+
businessKey: business_key,
|
|
358
|
+
containsText: contains_text,
|
|
359
|
+
status: INSTANCE_STATUS.options,
|
|
360
|
+
$orderby: 'startedAt desc',
|
|
361
|
+
$top: maxInstances
|
|
362
|
+
}));
|
|
363
|
+
const traced = [];
|
|
364
|
+
for (const raw of instances) {
|
|
365
|
+
const instance = pick(raw, INSTANCE_FIELDS);
|
|
366
|
+
const base = `/workflow-instances/${enc(instance.id)}`;
|
|
367
|
+
traced.push({
|
|
368
|
+
instance,
|
|
369
|
+
...(instance.status === 'ERRONEOUS' ? { error_messages: await errorMessages(instance.id) } : {}),
|
|
370
|
+
tasks: rows(await get('/task-instances', { workflowInstanceId: instance.id, $top: TRACE_TASK_LIMIT })).map(row => pick(row, TASK_FIELDS)),
|
|
371
|
+
recent_logs: rows(await get(`${base}/execution-logs`)).slice(-TRACE_LOG_LIMIT)
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
return { instances: traced };
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
register('bpa_start_instance', 'Start process instance',
|
|
378
|
+
'Start a workflow instance of the latest deployed version of a definition with the given start context (see bpa_get_process_definition include sample_start_context). With an environment API key configured it is sent as api-key for shared environments. Role: ProcessAutomationParticipant. Requires confirmation "START BPA PROCESS <definition_id>".',
|
|
379
|
+
{ definition_id: definitionId, context: payload, environment_id: environmentId, confirmation },
|
|
380
|
+
CREATE_ANNOTATIONS,
|
|
381
|
+
async ({ definition_id: id, context, environment_id: environment, confirmation: given }) => {
|
|
382
|
+
requireConfirmation(given, `START BPA PROCESS ${id}`);
|
|
383
|
+
const instance = await rest.request('POST', `${WORKFLOW}/workflow-instances`, {
|
|
384
|
+
query: environmentQuery(access, environment),
|
|
385
|
+
headers: apiKeyHeaders(access, 'api-key'),
|
|
386
|
+
json: { definitionId: id, context },
|
|
387
|
+
expect: [200, 201]
|
|
388
|
+
});
|
|
389
|
+
return pick(instance, INSTANCE_FIELDS);
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
register('bpa_change_instance_status', 'Change workflow instance status',
|
|
393
|
+
'Cancel, suspend, resume or retry a workflow instance after checking its current status; cascade also applies the change to subflow instances. Status changes may take effect asynchronously. Role: ProcessAutomationAdmin. Requires confirmation "<ACTION> BPA INSTANCE <instance_id>" (e.g. "RETRY BPA INSTANCE <id>").',
|
|
394
|
+
{ instance_id: guid, action: z.enum(['cancel', 'suspend', 'resume', 'retry']), cascade: z.boolean().default(false), confirmation },
|
|
395
|
+
OVERWRITE_ANNOTATIONS,
|
|
396
|
+
async ({ instance_id: id, action, cascade, confirmation: given }) => {
|
|
397
|
+
requireConfirmation(given, `${action.toUpperCase()} BPA INSTANCE ${id}`);
|
|
398
|
+
const before = await getInstance(id);
|
|
399
|
+
const status = transition(action, id, before?.status);
|
|
400
|
+
await rest.request('PATCH', `${WORKFLOW}/workflow-instances/${enc(id)}`, {
|
|
401
|
+
json: { status, ...(cascade ? { cascade: true } : {}) },
|
|
402
|
+
expect: [200, 202, 204],
|
|
403
|
+
responseType: 'none'
|
|
404
|
+
});
|
|
405
|
+
const after = await getInstance(id);
|
|
406
|
+
return { before: before?.status, after: after?.status, instance: pick(after, INSTANCE_FIELDS) };
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
register('bpa_update_instance_context', 'Update workflow instance context',
|
|
410
|
+
'Merge into (PATCH) or replace (PUT) the context of a workflow instance, e.g. to correct data before retry or to remove sensitive data. Suspend the instance first. Role: ProcessAutomationAdmin. Requires confirmation "MERGE BPA CONTEXT <instance_id>" or "REPLACE BPA CONTEXT <instance_id>".',
|
|
411
|
+
{ instance_id: guid, mode: z.enum(['merge', 'replace']), context: payload, confirmation },
|
|
412
|
+
OVERWRITE_ANNOTATIONS,
|
|
413
|
+
async ({ instance_id: id, mode, context, confirmation: given }) => {
|
|
414
|
+
requireConfirmation(given, `${mode.toUpperCase()} BPA CONTEXT ${id}`);
|
|
415
|
+
const path = `${WORKFLOW}/workflow-instances/${enc(id)}/context`;
|
|
416
|
+
await rest.request(mode === 'merge' ? 'PATCH' : 'PUT', path, { json: context, expect: [200, 204], responseType: 'none' });
|
|
417
|
+
return { instance_id: id, mode, ...capContext(await rest.request('GET', path)) };
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
register('bpa_update_instance_roles', 'Update workflow instance roles',
|
|
421
|
+
'Overwrite instance-specific role assignments (viewer, admin, context viewer, context admin users/groups; max 100 each); omitted roles stay unchanged. Role: ProcessAutomationAdmin. Requires confirmation "UPDATE BPA ROLES <instance_id>".',
|
|
422
|
+
{
|
|
423
|
+
instance_id: guid,
|
|
424
|
+
viewer_users: userList.optional(),
|
|
425
|
+
viewer_groups: userList.optional(),
|
|
426
|
+
admin_users: userList.optional(),
|
|
427
|
+
admin_groups: userList.optional(),
|
|
428
|
+
context_viewer_users: userList.optional(),
|
|
429
|
+
context_viewer_groups: userList.optional(),
|
|
430
|
+
context_admin_users: userList.optional(),
|
|
431
|
+
context_admin_groups: userList.optional(),
|
|
432
|
+
confirmation
|
|
433
|
+
},
|
|
434
|
+
OVERWRITE_ANNOTATIONS,
|
|
435
|
+
async ({ instance_id: id, confirmation: given, ...roles }) => {
|
|
436
|
+
requireConfirmation(given, `UPDATE BPA ROLES ${id}`);
|
|
437
|
+
const body = Object.fromEntries(Object.entries(roles)
|
|
438
|
+
.filter(([, value]) => value !== undefined)
|
|
439
|
+
.map(([name, value]) => [name.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()), value]));
|
|
440
|
+
if (!Object.keys(body).length) throw new IntegrationError('provide at least one role assignment to update');
|
|
441
|
+
const path = `${WORKFLOW}/workflow-instances/${enc(id)}/roles`;
|
|
442
|
+
await rest.request('PATCH', path, { json: body, expect: [200, 204], responseType: 'none' });
|
|
443
|
+
return { instance_id: id, roles: await rest.request('GET', path) };
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
register('bpa_delete_instances', 'Delete workflow instances',
|
|
447
|
+
'Permanently delete workflow instances (up to 1000 per call); asynchronous for larger sets, returns the job id for bpa_get_job. Role: ProcessAutomationAdmin. Requires confirmation "DELETE BPA INSTANCES <count>".',
|
|
448
|
+
{ instance_ids: z.array(guid).min(1).max(1000), confirmation },
|
|
449
|
+
OVERWRITE_ANNOTATIONS,
|
|
450
|
+
async ({ instance_ids: ids, confirmation: given }) => {
|
|
451
|
+
const unique = [...new Set(ids)];
|
|
452
|
+
requireConfirmation(given, `DELETE BPA INSTANCES ${unique.length}`);
|
|
453
|
+
return { requested: unique.length, ...await asyncCall('PATCH', '/workflow-instances', { json: unique.map(id => ({ id, deleted: true })), expect: [200, 202] }) };
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
register('bpa_send_message', 'Send workflow message',
|
|
457
|
+
'Deliver a message to workflow instances waiting in an intermediate message event (classic workflows only; processes built in SAP Build resume through bpa_execute_api_trigger with execution_id). Target by instance id, or by workflow definition and/or business key. Requires confirmation "SEND BPA MESSAGE <message_definition_id>".',
|
|
458
|
+
z.object({
|
|
459
|
+
message_definition_id: identifier('message definition id'),
|
|
460
|
+
workflow_instance_id: guid.optional(),
|
|
461
|
+
workflow_definition_id: definitionId.optional(),
|
|
462
|
+
business_key: z.string().min(1).max(255).optional(),
|
|
463
|
+
context: payload.optional(),
|
|
464
|
+
confirmation
|
|
465
|
+
}).refine(args => args.workflow_instance_id || args.workflow_definition_id || args.business_key,
|
|
466
|
+
'provide workflow_instance_id, workflow_definition_id or business_key'),
|
|
467
|
+
CREATE_ANNOTATIONS,
|
|
468
|
+
async ({ message_definition_id: messageId, workflow_instance_id, workflow_definition_id, business_key, context, confirmation: given }) => {
|
|
469
|
+
requireConfirmation(given, `SEND BPA MESSAGE ${messageId}`);
|
|
470
|
+
const consumers = await rest.request('POST', `${WORKFLOW}/messages`, {
|
|
471
|
+
json: {
|
|
472
|
+
definitionId: messageId,
|
|
473
|
+
...(workflow_instance_id ? { workflowInstanceId: workflow_instance_id } : {}),
|
|
474
|
+
...(workflow_definition_id ? { workflowDefinitionId: workflow_definition_id } : {}),
|
|
475
|
+
...(business_key ? { businessKey: business_key } : {}),
|
|
476
|
+
...(context ? { context } : {})
|
|
477
|
+
},
|
|
478
|
+
expect: [200]
|
|
479
|
+
});
|
|
480
|
+
return { consumed_by: rows(consumers) };
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
// ------------------------------------------------------------ user tasks
|
|
484
|
+
|
|
485
|
+
register('bpa_list_tasks', 'List user tasks',
|
|
486
|
+
'List user-task instances (the queue behind My Inbox / Fiori Task Center), newest first; open tasks by default. Multiple statuses or priorities are OR-combined.',
|
|
487
|
+
{
|
|
488
|
+
status: z.array(TASK_STATUS).min(1).max(4).default(['READY', 'RESERVED']),
|
|
489
|
+
priority: z.array(PRIORITY).min(1).max(4).optional(),
|
|
490
|
+
workflow_instance_id: guid.optional(),
|
|
491
|
+
workflow_definition_id: definitionId.optional(),
|
|
492
|
+
definition_id: z.string().min(1).max(255).optional().describe('Task definition id, e.g. usertask1@orderapproval'),
|
|
493
|
+
activity_id: z.string().min(1).max(255).optional(),
|
|
494
|
+
recipient_user: z.string().min(1).max(255).optional(),
|
|
495
|
+
recipient_group: z.string().min(1).max(255).optional(),
|
|
496
|
+
processor: z.string().min(1).max(255).optional(),
|
|
497
|
+
contains_text: z.string().min(1).max(255).optional(),
|
|
498
|
+
created_from: timestamp.optional(),
|
|
499
|
+
created_up_to: timestamp.optional(),
|
|
500
|
+
due_date_from: timestamp.optional(),
|
|
501
|
+
due_date_up_to: timestamp.optional(),
|
|
502
|
+
completed_from: timestamp.optional(),
|
|
503
|
+
completed_up_to: timestamp.optional(),
|
|
504
|
+
include_attributes: z.boolean().default(false),
|
|
505
|
+
orderby: TASK_ORDER.default('createdAt desc'),
|
|
506
|
+
top: top(200, 50),
|
|
507
|
+
skip
|
|
508
|
+
},
|
|
509
|
+
READ_ONLY_ANNOTATIONS,
|
|
510
|
+
async args => rows(await get('/task-instances', {
|
|
511
|
+
status: args.status,
|
|
512
|
+
priority: args.priority,
|
|
513
|
+
workflowInstanceId: args.workflow_instance_id,
|
|
514
|
+
workflowDefinitionId: args.workflow_definition_id,
|
|
515
|
+
definitionId: args.definition_id,
|
|
516
|
+
activityId: args.activity_id,
|
|
517
|
+
recipientUsers: args.recipient_user,
|
|
518
|
+
recipientGroups: args.recipient_group,
|
|
519
|
+
processor: args.processor,
|
|
520
|
+
containsText: args.contains_text,
|
|
521
|
+
createdFrom: args.created_from,
|
|
522
|
+
createdUpTo: args.created_up_to,
|
|
523
|
+
dueDateFrom: args.due_date_from,
|
|
524
|
+
dueDateUpTo: args.due_date_up_to,
|
|
525
|
+
completedFrom: args.completed_from,
|
|
526
|
+
completedUpTo: args.completed_up_to,
|
|
527
|
+
...(args.include_attributes ? { $expand: 'attributes' } : {}),
|
|
528
|
+
$orderby: args.orderby,
|
|
529
|
+
$top: args.top,
|
|
530
|
+
$skip: args.skip
|
|
531
|
+
})).map(row => pick(row, TASK_FIELDS)));
|
|
532
|
+
|
|
533
|
+
register('bpa_get_task', 'Get user task',
|
|
534
|
+
'Read one user-task instance, by default with its context (capped at 48 KiB) and optionally its custom attributes.',
|
|
535
|
+
{ task_id: guid, include_context: z.boolean().default(true), include_attributes: z.boolean().default(false) },
|
|
536
|
+
READ_ONLY_ANNOTATIONS,
|
|
537
|
+
async ({ task_id: id, include_context: includeContext, include_attributes: includeAttributes }) => {
|
|
538
|
+
const result = { task: await getTask(id) };
|
|
539
|
+
if (includeContext) Object.assign(result, capContext(await get(`/task-instances/${enc(id)}/context`)));
|
|
540
|
+
if (includeAttributes) result.attributes = await get(`/task-instances/${enc(id)}/attributes`);
|
|
541
|
+
return result;
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
register('bpa_complete_task', 'Complete user task',
|
|
545
|
+
'Complete an open user task, optionally with a decision (the form button id) and output context. Requires confirmation "COMPLETE BPA TASK <task_id>".',
|
|
546
|
+
{ task_id: guid, decision: z.string().min(1).max(255).optional(), context: payload.optional(), confirmation },
|
|
547
|
+
OVERWRITE_ANNOTATIONS,
|
|
548
|
+
async ({ task_id: id, decision, context, confirmation: given }) => {
|
|
549
|
+
requireConfirmation(given, `COMPLETE BPA TASK ${id}`);
|
|
550
|
+
const before = await getTask(id);
|
|
551
|
+
if (!OPEN_TASK_STATUSES.has(before?.status)) throw new IntegrationError(`task ${id} is ${before?.status}; only open tasks can be completed`);
|
|
552
|
+
await rest.request('PATCH', `${WORKFLOW}/task-instances/${enc(id)}`, {
|
|
553
|
+
json: { status: 'COMPLETED', ...(decision ? { decision } : {}), ...(context ? { context } : {}) },
|
|
554
|
+
expect: [200, 204],
|
|
555
|
+
responseType: 'none'
|
|
556
|
+
});
|
|
557
|
+
const after = await getTask(id);
|
|
558
|
+
return { before: before?.status, after: after?.status, ...(decision ? { decision } : {}) };
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
register('bpa_update_tasks', 'Update user tasks',
|
|
562
|
+
'Administratively change properties of up to 100 user tasks: subject, description, recipients, processor (claim/release by admin: set or clear), due date, priority, confidence level, UI URI. Asynchronous; returns the job id. Role: ProcessAutomationAdmin. Requires confirmation "UPDATE BPA TASKS <count>".',
|
|
563
|
+
{
|
|
564
|
+
updates: z.array(z.object({
|
|
565
|
+
id: guid,
|
|
566
|
+
subject: z.string().min(1).max(255).optional(),
|
|
567
|
+
description: z.string().max(1024).optional(),
|
|
568
|
+
recipient_users: z.string().max(5000).optional().describe('Comma-separated user ids'),
|
|
569
|
+
recipient_groups: z.string().max(5000).optional().describe('Comma-separated group ids'),
|
|
570
|
+
processor: z.string().max(255).optional().describe('User id; empty string releases the task'),
|
|
571
|
+
due_date: timestamp.optional(),
|
|
572
|
+
priority: PRIORITY.optional(),
|
|
573
|
+
confidence_level: z.number().min(0).max(1).optional(),
|
|
574
|
+
user_interface_uri: z.string().max(2048).optional()
|
|
575
|
+
})).min(1).max(100),
|
|
576
|
+
confirmation
|
|
577
|
+
},
|
|
578
|
+
OVERWRITE_ANNOTATIONS,
|
|
579
|
+
async ({ updates, confirmation: given }) => {
|
|
580
|
+
requireConfirmation(given, `UPDATE BPA TASKS ${updates.length}`);
|
|
581
|
+
const body = updates.map(({ id, recipient_users, recipient_groups, due_date, confidence_level, user_interface_uri, ...rest }) => ({
|
|
582
|
+
id,
|
|
583
|
+
...rest,
|
|
584
|
+
...(recipient_users !== undefined ? { recipientUsers: recipient_users } : {}),
|
|
585
|
+
...(recipient_groups !== undefined ? { recipientGroups: recipient_groups } : {}),
|
|
586
|
+
...(due_date !== undefined ? { dueDate: due_date } : {}),
|
|
587
|
+
...(confidence_level !== undefined ? { confidenceLevel: confidence_level } : {}),
|
|
588
|
+
...(user_interface_uri !== undefined ? { userInterfaceUri: user_interface_uri } : {})
|
|
589
|
+
}));
|
|
590
|
+
if (body.some(update => Object.keys(update).length === 1)) throw new IntegrationError('every update needs at least one property besides id');
|
|
591
|
+
return { requested: body.length, ...await asyncCall('PATCH', '/task-instances', { json: body, expect: [200, 202, 204] }) };
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
register('bpa_update_task_context', 'Update user task context',
|
|
595
|
+
'Merge a payload into the stored context of a user task (exceptional corrections only; prefer the task input mapping). Requires confirmation "UPDATE BPA TASK CONTEXT <task_id>".',
|
|
596
|
+
{ task_id: guid, context: payload, confirmation },
|
|
597
|
+
OVERWRITE_ANNOTATIONS,
|
|
598
|
+
async ({ task_id: id, context, confirmation: given }) => {
|
|
599
|
+
requireConfirmation(given, `UPDATE BPA TASK CONTEXT ${id}`);
|
|
600
|
+
const path = `${WORKFLOW}/task-instances/${enc(id)}/context`;
|
|
601
|
+
await rest.request('PATCH', path, { json: context, expect: [200, 204], responseType: 'none' });
|
|
602
|
+
return { task_id: id, ...capContext(await rest.request('GET', path)) };
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
// ------------------------------------------------------------ administration
|
|
606
|
+
|
|
607
|
+
register('bpa_export_workflow_data', 'Export workflow data',
|
|
608
|
+
'Download the workflow data export (ZIP of definitions/forms metadata, workflow instances and/or task instances) to a new local file. Role: ProcessAutomationAdmin.',
|
|
609
|
+
{
|
|
610
|
+
type: z.enum(['ALL', 'TASK_INSTANCES', 'WORKFLOW_INSTANCES', 'ALL_DEFINITIONS']).default('ALL'),
|
|
611
|
+
created_from: timestamp.optional(),
|
|
612
|
+
created_up_to: timestamp.optional(),
|
|
613
|
+
collection_query: z.enum(['own', 'shared']).optional(),
|
|
614
|
+
output_file: outputFile
|
|
615
|
+
},
|
|
616
|
+
READ_ONLY_ANNOTATIONS,
|
|
617
|
+
async ({ type, created_from, created_up_to, collection_query, output_file: path }) => {
|
|
618
|
+
const bytes = await rest.request('GET', `${WORKFLOW}/export`, {
|
|
619
|
+
query: { type, createdFrom: created_from, createdUpTo: created_up_to, collectionQuery: collection_query },
|
|
620
|
+
accept: 'application/zip',
|
|
621
|
+
responseType: 'bytes'
|
|
622
|
+
});
|
|
623
|
+
return { type, ...await writeExportFile(path, bytes) };
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
register('bpa_purge_workflow_data', 'Purge all workflow data',
|
|
627
|
+
'Irreversibly erase ALL workflow definitions, workflow instances and form definitions of the tenant; asynchronous, returns the job id. Role: ProcessAutomationAdmin. Requires confirmation "PURGE ALL BPA WORKFLOW DATA".',
|
|
628
|
+
{ confirmation },
|
|
629
|
+
OVERWRITE_ANNOTATIONS,
|
|
630
|
+
async ({ confirmation: given }) => {
|
|
631
|
+
requireConfirmation(given, 'PURGE ALL BPA WORKFLOW DATA');
|
|
632
|
+
return asyncCall('POST', '/purge', { expect: [202, 303] });
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
register('bpa_get_job', 'Get workflow job status',
|
|
636
|
+
'Track an asynchronous workflow operation (definition undeployment, purge, bulk instance deletion, bulk task update) by the job id those tools return. Role: ProcessAutomationAdmin.',
|
|
637
|
+
{ job_id: identifier('job id') },
|
|
638
|
+
READ_ONLY_ANNOTATIONS,
|
|
639
|
+
async ({ job_id: id }) => get(`/jobs/${enc(id)}`));
|
|
640
|
+
|
|
641
|
+
// ------------------------------------------------------------ decisions
|
|
642
|
+
|
|
643
|
+
register('bpa_invoke_decision', 'Invoke decision',
|
|
644
|
+
'Evaluate a deployed decision (rule service) with input vocabulary objects such as [{ "<InputTypeName>": { ... } }]; latest deployment unless a revision/version is given. Rule-service ids come from the deployed project; there is no list API. Evaluation has no side effects.',
|
|
645
|
+
{
|
|
646
|
+
rule_service_id: ruleServiceId,
|
|
647
|
+
vocabulary: z.array(payload).min(1).max(50),
|
|
648
|
+
revision: z.string().min(1).max(64).optional(),
|
|
649
|
+
version: z.string().min(1).max(64).optional()
|
|
650
|
+
},
|
|
651
|
+
READ_ONLY_ANNOTATIONS,
|
|
652
|
+
async ({ rule_service_id: id, vocabulary, revision, version }) => invokeDecision(id, vocabulary, revision, version));
|
|
653
|
+
|
|
654
|
+
register('bpa_test_decision', 'Test decision',
|
|
655
|
+
'Run decision test cases sequentially and compare each Result with the expected output (subset match: an object is compared with Result[0], an array with Result).',
|
|
656
|
+
{
|
|
657
|
+
rule_service_id: ruleServiceId,
|
|
658
|
+
revision: z.string().min(1).max(64).optional(),
|
|
659
|
+
version: z.string().min(1).max(64).optional(),
|
|
660
|
+
cases: z.array(z.object({
|
|
661
|
+
name: z.string().min(1).max(120),
|
|
662
|
+
vocabulary: z.array(payload).min(1).max(50),
|
|
663
|
+
expected: z.union([payload, z.array(payload)])
|
|
664
|
+
})).min(1).max(50)
|
|
665
|
+
},
|
|
666
|
+
READ_ONLY_ANNOTATIONS,
|
|
667
|
+
async ({ rule_service_id: id, revision, version, cases }) => {
|
|
668
|
+
const results = [];
|
|
669
|
+
for (const testCase of cases) {
|
|
670
|
+
try {
|
|
671
|
+
const response = await invokeDecision(id, testCase.vocabulary, revision, version);
|
|
672
|
+
const actual = rows(response?.Result);
|
|
673
|
+
const mismatches = Array.isArray(testCase.expected)
|
|
674
|
+
? subsetMismatches(testCase.expected, actual)
|
|
675
|
+
: subsetMismatches(testCase.expected, actual[0], '$[0]');
|
|
676
|
+
results.push({ name: testCase.name, pass: mismatches.length === 0, mismatches, result: actual });
|
|
677
|
+
} catch (error) {
|
|
678
|
+
if (!(error instanceof IntegrationError)) throw error;
|
|
679
|
+
results.push({ name: testCase.name, pass: false, error: error.message });
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const passed = results.filter(result => result.pass).length;
|
|
683
|
+
return { passed, failed: results.length - passed, cases: results };
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
// ------------------------------------------------------------ business events
|
|
687
|
+
|
|
688
|
+
register('bpa_send_business_event', 'Send business event',
|
|
689
|
+
'Send a CloudEvents business event that triggers event-based processes (the endpoint SAP documents for SAP Integration Suite, advanced event mesh webhooks). Requires confirmation "SEND BPA EVENT <type>".',
|
|
690
|
+
{
|
|
691
|
+
type: z.string().min(1).max(255),
|
|
692
|
+
source: z.string().min(1).max(255),
|
|
693
|
+
subject: z.string().min(1).max(255).optional(),
|
|
694
|
+
data: payload,
|
|
695
|
+
id: z.string().min(1).max(128).optional(),
|
|
696
|
+
confirmation
|
|
697
|
+
},
|
|
698
|
+
CREATE_ANNOTATIONS,
|
|
699
|
+
async ({ type, source, subject, data, id, confirmation: given }) => {
|
|
700
|
+
requireConfirmation(given, `SEND BPA EVENT ${type}`);
|
|
701
|
+
const event = {
|
|
702
|
+
specversion: '1.0',
|
|
703
|
+
id: id ?? randomUUID(),
|
|
704
|
+
type,
|
|
705
|
+
source,
|
|
706
|
+
...(subject ? { subject } : {}),
|
|
707
|
+
time: new Date(now()).toISOString(),
|
|
708
|
+
datacontenttype: 'application/json',
|
|
709
|
+
data
|
|
710
|
+
};
|
|
711
|
+
const { status } = await rest.request('POST', EVENTS, {
|
|
712
|
+
body: JSON.stringify(event),
|
|
713
|
+
contentType: 'application/cloudevents+json',
|
|
714
|
+
expect: [200, 201, 202, 204],
|
|
715
|
+
responseType: 'text',
|
|
716
|
+
withStatus: true
|
|
717
|
+
});
|
|
718
|
+
return { accepted: true, status, event_id: event.id };
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const context = { rest, access, now };
|
|
722
|
+
registerInboxTools(register, context);
|
|
723
|
+
registerAutomationTools(register, context);
|
|
724
|
+
registerVisibilityTools(register, context);
|
|
725
|
+
|
|
726
|
+
return registeredNames;
|
|
727
|
+
}
|