@mseep/bw-modeling-mcp 0.8.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/CHANGELOG.md +140 -0
- package/LICENSE +21 -0
- package/README.md +598 -0
- package/dist/bw-client.js +774 -0
- package/dist/index.js +2199 -0
- package/dist/tools/activation.js +171 -0
- package/dist/tools/adso.js +895 -0
- package/dist/tools/composite_provider.js +169 -0
- package/dist/tools/cp_components.js +347 -0
- package/dist/tools/dataflow.js +148 -0
- package/dist/tools/datasource.js +536 -0
- package/dist/tools/delete.js +22 -0
- package/dist/tools/dtp.js +602 -0
- package/dist/tools/infoarea.js +117 -0
- package/dist/tools/infoobject.js +447 -0
- package/dist/tools/infosource.js +225 -0
- package/dist/tools/processchain.js +154 -0
- package/dist/tools/processvariant.js +49 -0
- package/dist/tools/push.js +100 -0
- package/dist/tools/query.js +631 -0
- package/dist/tools/reporting.js +558 -0
- package/dist/tools/repository.js +84 -0
- package/dist/tools/request_monitor.js +174 -0
- package/dist/tools/roles.js +503 -0
- package/dist/tools/search.js +107 -0
- package/dist/tools/transformation.js +1392 -0
- package/package.json +51 -0
|
@@ -0,0 +1,602 @@
|
|
|
1
|
+
import { MEDIA_TYPES, createClientFromEnv } from '../bw-client.js';
|
|
2
|
+
import { bwActivate } from './activation.js';
|
|
3
|
+
/**
|
|
4
|
+
* Parse <atom:entry> elements from a BW Atom feed (xref / search responses).
|
|
5
|
+
* Each entry contains a <bwModel:object> with objectName, objectType, objectStatus.
|
|
6
|
+
*/
|
|
7
|
+
function parseAtomEntries(xml) {
|
|
8
|
+
const entries = [];
|
|
9
|
+
const entryRegex = /<atom:entry>([\s\S]*?)<\/atom:entry>/g;
|
|
10
|
+
let match;
|
|
11
|
+
while ((match = entryRegex.exec(xml)) !== null) {
|
|
12
|
+
const body = match[1];
|
|
13
|
+
const nameMatch = body.match(/objectName="([^"]+)"/);
|
|
14
|
+
const typeMatch = body.match(/objectType="([^"]+)"/);
|
|
15
|
+
const statusMatch = body.match(/objectStatus="([^"]+)"/);
|
|
16
|
+
const titleMatch = body.match(/<atom:title>([^<]+)<\/atom:title>/);
|
|
17
|
+
const hrefMatch = body.match(/href="([^"]+)"/);
|
|
18
|
+
if (nameMatch && typeMatch) {
|
|
19
|
+
entries.push({
|
|
20
|
+
objectName: nameMatch[1],
|
|
21
|
+
objectType: typeMatch[1],
|
|
22
|
+
objectStatus: statusMatch?.[1] ?? 'unknown',
|
|
23
|
+
title: titleMatch?.[1] ?? '',
|
|
24
|
+
href: hrefMatch?.[1] ?? '',
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return entries;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* bw_get_dtps — list DTPs that depend on an object (via xref).
|
|
32
|
+
*
|
|
33
|
+
* Uses the cross-reference endpoint to find all DTPA objects that use the given object.
|
|
34
|
+
* Filters xref results to objectType=DTPA only.
|
|
35
|
+
*
|
|
36
|
+
* After activating a Transformation, the activation response lists deactivated DTPs.
|
|
37
|
+
* This tool can be used independently to find dependent DTPs before activation.
|
|
38
|
+
*/
|
|
39
|
+
export async function bwGetDtps(client, objectType, objectName) {
|
|
40
|
+
const path = `/sap/bw/modeling/repo/is/xref?objectType=${encodeURIComponent(objectType.toUpperCase())}&objectName=${encodeURIComponent(objectName.toUpperCase())}`;
|
|
41
|
+
const result = await client.get(path, 'application/atom+xml;type=feed');
|
|
42
|
+
const allEntries = parseAtomEntries(result.body);
|
|
43
|
+
const dtps = allEntries.filter((e) => e.objectType === 'DTPA');
|
|
44
|
+
if (dtps.length === 0) {
|
|
45
|
+
return `No dependent DTPs found for ${objectType.toUpperCase()} ${objectName.toUpperCase()}.`;
|
|
46
|
+
}
|
|
47
|
+
const lines = [
|
|
48
|
+
`Found ${dtps.length} DTP(s) dependent on ${objectType.toUpperCase()} ${objectName.toUpperCase()}:`,
|
|
49
|
+
'',
|
|
50
|
+
...dtps.map((d, i) => `${i + 1}. ${d.objectName} — status: ${d.objectStatus}` +
|
|
51
|
+
(d.title ? ` — "${d.title}"` : '')),
|
|
52
|
+
'',
|
|
53
|
+
'To activate all inactive DTPs: call bw_activate for each with object_type="dtpa" and lock_handle="".',
|
|
54
|
+
];
|
|
55
|
+
return lines.join('\n');
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* bw_get_dtp_details — read a single DTP definition.
|
|
59
|
+
* (Used internally; exposed via bw_get_dtps in index.ts if needed.)
|
|
60
|
+
*/
|
|
61
|
+
export async function bwGetDtpDetails(client, dtpName) {
|
|
62
|
+
const path = `/sap/bw/modeling/dtpa/${dtpName.toLowerCase()}/m`;
|
|
63
|
+
const result = await client.get(path, MEDIA_TYPES['dtpa']);
|
|
64
|
+
const status = result.headers['object_status'] ?? result.headers['OBJECT_STATUS'] ?? 'unknown';
|
|
65
|
+
return `DTP: ${dtpName.toUpperCase()}\nStatus: ${status}\n\n${result.body}`;
|
|
66
|
+
}
|
|
67
|
+
function parseDtpXml(xml, status) {
|
|
68
|
+
const attr = (tag, name) => {
|
|
69
|
+
const m = tag.match(new RegExp(`\\b${name}="([^"]*)"`));
|
|
70
|
+
return m ? m[1] : '';
|
|
71
|
+
};
|
|
72
|
+
// Root attributes
|
|
73
|
+
const rootMatch = xml.match(/<dtpa:dataTransferProcess([^>]*)>/);
|
|
74
|
+
const rootAttrs = rootMatch?.[1] ?? '';
|
|
75
|
+
const name = attr(rootAttrs, 'name');
|
|
76
|
+
const description = attr(rootAttrs, 'description');
|
|
77
|
+
// Extraction settings
|
|
78
|
+
const extrMatch = xml.match(/<extractionSettings([^>]*)\/?>/);
|
|
79
|
+
const extractionMode = attr(extrMatch?.[1] ?? '', 'extractionMode');
|
|
80
|
+
const packageSize = attr(extrMatch?.[1] ?? '', 'packageSize');
|
|
81
|
+
// Source / target
|
|
82
|
+
const srcMatch = xml.match(/<source([^>]*)\/?>/);
|
|
83
|
+
const tgtMatch = xml.match(/<target([^>]*)\/?>/);
|
|
84
|
+
const source = {
|
|
85
|
+
type: attr(srcMatch?.[1] ?? '', 'type'),
|
|
86
|
+
name: attr(srcMatch?.[1] ?? '', 'name'),
|
|
87
|
+
description: attr(srcMatch?.[1] ?? '', 'description'),
|
|
88
|
+
};
|
|
89
|
+
const target = {
|
|
90
|
+
type: attr(tgtMatch?.[1] ?? '', 'type'),
|
|
91
|
+
name: attr(tgtMatch?.[1] ?? '', 'name'),
|
|
92
|
+
description: attr(tgtMatch?.[1] ?? '', 'description'),
|
|
93
|
+
};
|
|
94
|
+
// Transformation (overview/object)
|
|
95
|
+
const ovMatch = xml.match(/<overview>[\s\S]*?<object([^>]*)\/?>[\s\S]*?<\/overview>/);
|
|
96
|
+
const transformation = {
|
|
97
|
+
name: attr(ovMatch?.[1] ?? '', 'name'),
|
|
98
|
+
description: attr(ovMatch?.[1] ?? '', 'description'),
|
|
99
|
+
};
|
|
100
|
+
// Filter fields
|
|
101
|
+
const filterFields = [];
|
|
102
|
+
const fieldsRegex = /<fields([^>]*)>([\s\S]*?)<\/fields>/g;
|
|
103
|
+
let fm;
|
|
104
|
+
while ((fm = fieldsRegex.exec(xml)) !== null) {
|
|
105
|
+
const fieldAttrs = fm[1];
|
|
106
|
+
const fieldBody = fm[2];
|
|
107
|
+
const selections = [];
|
|
108
|
+
const selRegex = /<selection\b([^>]*)(?:\/>|>([\s\S]*?)<\/selection>)/g;
|
|
109
|
+
let sm;
|
|
110
|
+
while ((sm = selRegex.exec(fieldBody)) !== null) {
|
|
111
|
+
const selAttrs = sm[1];
|
|
112
|
+
const selBody = sm[2] ?? '';
|
|
113
|
+
const operator = selAttrs.match(/\boperator="([^"]*)"/)?.[1] ?? '';
|
|
114
|
+
const excluding = selAttrs.includes('excluding="true"');
|
|
115
|
+
const low = selBody.match(/<low[^>]*\bvalue="([^"]*)"/)?.[1] ?? '';
|
|
116
|
+
selections.push({ operator, excluding, low });
|
|
117
|
+
}
|
|
118
|
+
const hasRoutine = /<routine[\s>]/.test(fieldBody) && !/<routine\s*\/>/.test(fieldBody);
|
|
119
|
+
const routineCode = [];
|
|
120
|
+
if (hasRoutine) {
|
|
121
|
+
const codeRegex = /<code(?:\s[^>]*)?>([^<]*)<\/code>/g;
|
|
122
|
+
let cm;
|
|
123
|
+
while ((cm = codeRegex.exec(fieldBody)) !== null) {
|
|
124
|
+
routineCode.push(cm[1]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
filterFields.push({
|
|
128
|
+
name: attr(fieldAttrs, 'name'),
|
|
129
|
+
dtaName: attr(fieldAttrs, 'dtaName'),
|
|
130
|
+
description: attr(fieldAttrs, 'description'),
|
|
131
|
+
selected: fieldAttrs.includes('selected="true"'),
|
|
132
|
+
selections,
|
|
133
|
+
hasRoutine,
|
|
134
|
+
routineCode,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
// Global routine code
|
|
138
|
+
const globalRoutineCode = [];
|
|
139
|
+
const globalRegex = /<globalRoutineCode>([^<]*)<\/globalRoutineCode>/g;
|
|
140
|
+
let gm;
|
|
141
|
+
while ((gm = globalRegex.exec(xml)) !== null) {
|
|
142
|
+
globalRoutineCode.push(gm[1]);
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
name,
|
|
146
|
+
description,
|
|
147
|
+
status,
|
|
148
|
+
source,
|
|
149
|
+
target,
|
|
150
|
+
transformation,
|
|
151
|
+
extractionMode,
|
|
152
|
+
packageSize,
|
|
153
|
+
filterFields,
|
|
154
|
+
globalRoutineCode,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* bw_get_dtp — read a DTP definition and return a structured summary + raw XML.
|
|
159
|
+
*
|
|
160
|
+
* Flow:
|
|
161
|
+
* GET /sap/bw/modeling/dtpa/{dtpName}/m?forceCacheUpdate=true
|
|
162
|
+
* Parse key fields and return readable summary.
|
|
163
|
+
*/
|
|
164
|
+
export async function bwGetDtp(client, dtpName) {
|
|
165
|
+
const path = `/sap/bw/modeling/dtpa/${dtpName.toLowerCase()}/m?forceCacheUpdate=true`;
|
|
166
|
+
const result = await client.get(path, MEDIA_TYPES['dtpa']);
|
|
167
|
+
const status = result.headers['object_status'] ?? result.headers['OBJECT_STATUS'] ?? 'unknown';
|
|
168
|
+
const info = parseDtpXml(result.body, status);
|
|
169
|
+
const modeLabel = info.extractionMode === 'F' ? 'Full' : info.extractionMode === 'D' ? 'Delta' : info.extractionMode;
|
|
170
|
+
const lines = [
|
|
171
|
+
`DTP: ${info.name}`,
|
|
172
|
+
`Status: ${info.status}`,
|
|
173
|
+
`Description: ${info.description}`,
|
|
174
|
+
'',
|
|
175
|
+
`Source: ${info.source.type} ${info.source.name}` + (info.source.description ? ` — ${info.source.description}` : ''),
|
|
176
|
+
`Target: ${info.target.type} ${info.target.name}` + (info.target.description ? ` — ${info.target.description}` : ''),
|
|
177
|
+
`Transformation: ${info.transformation.name}` + (info.transformation.description ? ` — ${info.transformation.description}` : ''),
|
|
178
|
+
'',
|
|
179
|
+
`── Extraction Settings ──`,
|
|
180
|
+
` Mode: ${modeLabel} (${info.extractionMode})`,
|
|
181
|
+
` Package Size: ${info.packageSize}`,
|
|
182
|
+
'',
|
|
183
|
+
`── Filter Fields (${info.filterFields.length}) ──`,
|
|
184
|
+
];
|
|
185
|
+
if (info.filterFields.length === 0) {
|
|
186
|
+
lines.push(' (no filter fields)');
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
for (const f of info.filterFields) {
|
|
190
|
+
lines.push(` [${f.selected ? 'selected' : 'inactive'}] ${f.name} (${f.dtaName})`);
|
|
191
|
+
if (f.selections.length > 0) {
|
|
192
|
+
for (const s of f.selections) {
|
|
193
|
+
const sign = s.excluding ? '≠' : '=';
|
|
194
|
+
const val = s.low === '' ? "''" : `"${s.low}"`;
|
|
195
|
+
lines.push(` → ${s.operator} ${sign} ${val}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (f.hasRoutine) {
|
|
199
|
+
lines.push(` → Routine (${f.routineCode.length} lines):`);
|
|
200
|
+
for (const codeLine of f.routineCode) {
|
|
201
|
+
lines.push(` ${codeLine}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!f.hasRoutine && f.selections.length === 0) {
|
|
205
|
+
lines.push(' → (no selection / no routine)');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
if (info.globalRoutineCode.length > 0) {
|
|
210
|
+
lines.push('', '── Global Routine Code ──');
|
|
211
|
+
for (const line of info.globalRoutineCode) {
|
|
212
|
+
lines.push(` ${line}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
lines.push('', `── Raw XML ──`, result.body);
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* bw_create_dtp — create a new DTP for an existing Transformation, then activate it.
|
|
220
|
+
*
|
|
221
|
+
* Flow:
|
|
222
|
+
* 1. POST generateDtpId → DTP name from Location header
|
|
223
|
+
* 2. Lock with activity_context: CREA (rawPost on lockClient = passed-in client)
|
|
224
|
+
* 3. POST minimal XML with fresh createClientFromEnv() (session isolation)
|
|
225
|
+
* 4. Explicit unlock (rawPost on lockClient)
|
|
226
|
+
* 5a. If filter_field: Lock (new client) → GET (fresh) → PUT (fresh) → bwActivate
|
|
227
|
+
* 5b. If no filter: bwActivate with empty lockHandle
|
|
228
|
+
*/
|
|
229
|
+
export async function bwCreateDtp(client, args) {
|
|
230
|
+
const trfnName = args.trfn_name.toUpperCase();
|
|
231
|
+
const srcName = args.source_name.toUpperCase();
|
|
232
|
+
const srcType = args.source_type.toUpperCase();
|
|
233
|
+
const tgtName = args.target_name.toUpperCase();
|
|
234
|
+
const tgtType = args.target_type.toUpperCase();
|
|
235
|
+
const desc = args.description ?? '';
|
|
236
|
+
const pkg = args.package ?? '$TMP';
|
|
237
|
+
// Source element attributes. For ADSO/TRCS sources tlogo and type coincide and the name is
|
|
238
|
+
// passed through. For a DataSource source (source_type "RSDS") tlogo and type differ
|
|
239
|
+
// (tlogo "RSDS", type "DTASRC") and the name is the RSDS compound key.
|
|
240
|
+
let sourceTlogo;
|
|
241
|
+
let sourceTypeAttr;
|
|
242
|
+
let sourceNameAttr;
|
|
243
|
+
if (srcType === 'RSDS') {
|
|
244
|
+
if (!args.source_system) {
|
|
245
|
+
throw new Error('source_type "RSDS" (DataSource source) requires source_system to build the ' +
|
|
246
|
+
'RSDS compound source name.');
|
|
247
|
+
}
|
|
248
|
+
sourceTlogo = 'RSDS';
|
|
249
|
+
sourceTypeAttr = 'DTASRC';
|
|
250
|
+
// RSDS compound key: DataSource name left-justified in a 30-char field, then the
|
|
251
|
+
// source system name appended with no trailing padding (total length 40).
|
|
252
|
+
sourceNameAttr = srcName.padEnd(30, ' ') + args.source_system.toUpperCase();
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
sourceTlogo = srcType;
|
|
256
|
+
sourceTypeAttr = srcType;
|
|
257
|
+
sourceNameAttr = srcName;
|
|
258
|
+
}
|
|
259
|
+
const language = process.env.BW_LANGUAGE ?? 'DE';
|
|
260
|
+
const masterSystem = new URL(process.env.BW_URL ?? 'http://localhost').hostname.split('.')[0].toUpperCase();
|
|
261
|
+
const responsible = (process.env.BW_USER ?? '').toUpperCase();
|
|
262
|
+
// Step 1: Generate DTP name via POST generateDtpId — DTP name is in Location header
|
|
263
|
+
const csrfToken = await client.getCsrfToken();
|
|
264
|
+
const genResponse = await client.rawPost('/sap/bw/modeling/dtpa/generateDtpId', '', {
|
|
265
|
+
'Accept': MEDIA_TYPES['dtpa'],
|
|
266
|
+
'Content-Type': MEDIA_TYPES['dtpa'],
|
|
267
|
+
'x-csrf-token': csrfToken,
|
|
268
|
+
});
|
|
269
|
+
const location = genResponse.headers['location'] ?? genResponse.headers['Location'] ?? '';
|
|
270
|
+
if (!location) {
|
|
271
|
+
throw new Error(`generateDtpId returned no Location header. Response: ${JSON.stringify(genResponse.headers)}`);
|
|
272
|
+
}
|
|
273
|
+
const dtpName = location.split('/').pop().toUpperCase();
|
|
274
|
+
const dtpLower = dtpName.toLowerCase();
|
|
275
|
+
// Step 2: Lock with CREA
|
|
276
|
+
const csrfToken2 = await client.getCsrfToken();
|
|
277
|
+
const lockResponse = await client.rawPost(`/sap/bw/modeling/dtpa/${dtpLower}?action=lock`, '', {
|
|
278
|
+
'activity_context': 'CREA',
|
|
279
|
+
'Accept': MEDIA_TYPES['dtpa'],
|
|
280
|
+
'x-csrf-token': csrfToken2,
|
|
281
|
+
});
|
|
282
|
+
const lockHandle = lockResponse.body.match(/<LOCK_HANDLE>([^<]+)<\/LOCK_HANDLE>/)?.[1] ?? '';
|
|
283
|
+
if (!lockHandle) {
|
|
284
|
+
throw new Error(`No <LOCK_HANDLE> in CREA lock response:\n${lockResponse.body}`);
|
|
285
|
+
}
|
|
286
|
+
// Step 3: POST minimal XML — fresh session (same isolation as bwCreateTransformation)
|
|
287
|
+
const postBody = `<?xml version="1.0" encoding="UTF-8"?>
|
|
288
|
+
<Dtpa:dataTransferProcess
|
|
289
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
290
|
+
xmlns:Dtpa="http://www.sap.com/bw/modeling/DataTransferProcess.ecore"
|
|
291
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
292
|
+
description="${desc}"
|
|
293
|
+
name="${dtpName}">
|
|
294
|
+
<generalInformation>
|
|
295
|
+
<tlogoProperties
|
|
296
|
+
adtcore:language="${language}"
|
|
297
|
+
adtcore:name="${dtpName}"
|
|
298
|
+
adtcore:type="DTPA"
|
|
299
|
+
adtcore:masterLanguage="${language}"
|
|
300
|
+
adtcore:masterSystem="${masterSystem}"
|
|
301
|
+
adtcore:responsible="${responsible}"/>
|
|
302
|
+
</generalInformation>
|
|
303
|
+
<overview>
|
|
304
|
+
<object xsi:type="Dtpa:DTPObject" name="${trfnName}" tlogo="TRFN"/>${args.trfn_name_2 ? `\n <object xsi:type="Dtpa:DTPObject" name="${args.trfn_name_2.toUpperCase()}" tlogo="TRFN"/>` : ''}
|
|
305
|
+
</overview>
|
|
306
|
+
<source name="${sourceNameAttr}" tlogo="${sourceTlogo}" type="${sourceTypeAttr}"/>
|
|
307
|
+
<target name="${tgtName}" tlogo="${tgtType}" type="${tgtType}"/>
|
|
308
|
+
</Dtpa:dataTransferProcess>`;
|
|
309
|
+
const createClient = createClientFromEnv();
|
|
310
|
+
const createCsrf = await createClient.getCsrfToken();
|
|
311
|
+
await createClient.rawPost(`/sap/bw/modeling/dtpa/${dtpLower}?lockHandle=${lockHandle}`, postBody, {
|
|
312
|
+
'Development-Class': pkg,
|
|
313
|
+
'Content-Type': MEDIA_TYPES['dtpa'],
|
|
314
|
+
'Accept': MEDIA_TYPES['dtpa'],
|
|
315
|
+
'x-csrf-token': createCsrf,
|
|
316
|
+
});
|
|
317
|
+
// Step 4: Explicit unlock
|
|
318
|
+
const csrfToken3 = await client.getCsrfToken();
|
|
319
|
+
await client.rawPost(`/sap/bw/modeling/dtpa/${dtpLower}?action=unlock`, '', {
|
|
320
|
+
'Content-Type': MEDIA_TYPES['dtpa'],
|
|
321
|
+
'Accept': MEDIA_TYPES['dtpa'],
|
|
322
|
+
'x-csrf-token': csrfToken3,
|
|
323
|
+
});
|
|
324
|
+
// Step 4b: If description or filter provided, update via Lock → GET → PUT → unlock
|
|
325
|
+
if (desc || (args.filter_field && args.filter_value)) {
|
|
326
|
+
const descLockCsrf = await client.getCsrfToken();
|
|
327
|
+
const descLockResponse = await client.rawPost(`/sap/bw/modeling/dtpa/${dtpLower}?action=lock`, '', {
|
|
328
|
+
'Accept': MEDIA_TYPES['dtpa'],
|
|
329
|
+
'x-csrf-token': descLockCsrf,
|
|
330
|
+
});
|
|
331
|
+
const descLockHandle = descLockResponse.body.match(/<LOCK_HANDLE>([^<]+)<\/LOCK_HANDLE>/)?.[1] ?? '';
|
|
332
|
+
if (!descLockHandle) {
|
|
333
|
+
throw new Error(`No <LOCK_HANDLE> in description/filter lock response:\n${descLockResponse.body}`);
|
|
334
|
+
}
|
|
335
|
+
// GET DTP XML (fresh client) — read timestamp
|
|
336
|
+
const descGetClient = createClientFromEnv();
|
|
337
|
+
const descGetResponse = await descGetClient.get(`/sap/bw/modeling/dtpa/${dtpLower}/m`, MEDIA_TYPES['dtpa']);
|
|
338
|
+
const descTimestamp = descGetResponse.headers['timestamp'] ?? '';
|
|
339
|
+
let descXml = descGetResponse.body;
|
|
340
|
+
// Update description attribute if provided
|
|
341
|
+
if (desc) {
|
|
342
|
+
descXml = descXml.replace(/(<dtpa:dataTransferProcess\b[^>]*\bdescription=)"[^"]*"/, `$1"${desc}"`);
|
|
343
|
+
}
|
|
344
|
+
// Inject filter if provided
|
|
345
|
+
if (args.filter_field && args.filter_value) {
|
|
346
|
+
const fieldBlockRegex = new RegExp(`(<fields[^>]*\\bname="${args.filter_field}"[^>]*>[\\s\\S]*?)<routine\\/>`);
|
|
347
|
+
descXml = descXml.replace(fieldBlockRegex, `$1<routine/>\n <selection excluding="false" operator="Equal">\n <low description="${args.filter_value}" value="${args.filter_value}"/>\n </selection>`);
|
|
348
|
+
}
|
|
349
|
+
// PUT with fresh client
|
|
350
|
+
const descPutClient = createClientFromEnv();
|
|
351
|
+
await descPutClient.put('dtpa', dtpName, descLockHandle, descXml, descTimestamp);
|
|
352
|
+
// Unlock
|
|
353
|
+
const descUnlockCsrf = await client.getCsrfToken();
|
|
354
|
+
await client.rawPost(`/sap/bw/modeling/dtpa/${dtpLower}?action=unlock`, '', {
|
|
355
|
+
'Content-Type': MEDIA_TYPES['dtpa'],
|
|
356
|
+
'Accept': MEDIA_TYPES['dtpa'],
|
|
357
|
+
'x-csrf-token': descUnlockCsrf,
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
// Step 5: Activate
|
|
361
|
+
await bwActivate(client, 'dtpa', dtpName, '');
|
|
362
|
+
return JSON.stringify({
|
|
363
|
+
success: true,
|
|
364
|
+
dtp_name: dtpName,
|
|
365
|
+
transformation: trfnName,
|
|
366
|
+
source: { type: srcType, name: srcName },
|
|
367
|
+
target: { type: tgtType, name: tgtName },
|
|
368
|
+
package: pkg,
|
|
369
|
+
message: `DTP '${dtpName}' created and activated successfully.`,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
// ── bwRunDtp ──────────────────────────────────────────────────────────────────
|
|
373
|
+
/**
|
|
374
|
+
* bw_run_dtp — start (execute) a DTP run.
|
|
375
|
+
*
|
|
376
|
+
* Single POST to /sap/bw/modeling/dtpa/executerun with a minimal <executeRun> body naming the
|
|
377
|
+
* DTP. The server responds 201 Created with a Location header whose last path segment is the new
|
|
378
|
+
* request id.
|
|
379
|
+
*
|
|
380
|
+
* Runs in a fresh session (createClientFromEnv()) — like the DTP-activation path — because the
|
|
381
|
+
* executerun touches the DTP and its transformation and the shared module-level client can hold a
|
|
382
|
+
* stale session buffer; a fresh session also avoids concurrency collisions across runs.
|
|
383
|
+
*
|
|
384
|
+
* The returned request id is a timestamp-based executerun id. It is returned for information only:
|
|
385
|
+
* it is not verified to match the request TSN used by bw_get_request, so load status is monitored
|
|
386
|
+
* via bw_list_requests (by target InfoProvider) and bw_get_request.
|
|
387
|
+
*/
|
|
388
|
+
export async function bwRunDtp(dtpName) {
|
|
389
|
+
const dtpUpper = dtpName.toUpperCase();
|
|
390
|
+
const runClient = createClientFromEnv();
|
|
391
|
+
const csrfToken = await runClient.getCsrfToken();
|
|
392
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?><executeRun dataTransferProcess="${dtpUpper}"></executeRun>`;
|
|
393
|
+
const response = await runClient.rawPost('/sap/bw/modeling/dtpa/executerun', body, {
|
|
394
|
+
'Accept': MEDIA_TYPES['dtpa'],
|
|
395
|
+
'Content-Type': MEDIA_TYPES['dtpa'],
|
|
396
|
+
'x-csrf-token': csrfToken,
|
|
397
|
+
});
|
|
398
|
+
const location = response.headers['location'] ?? response.headers['Location'] ?? '';
|
|
399
|
+
if (!location) {
|
|
400
|
+
throw new Error(`executerun returned no Location header. Response headers: ${JSON.stringify(response.headers)}`);
|
|
401
|
+
}
|
|
402
|
+
const requestId = location.split('/').filter(Boolean).pop() ?? '';
|
|
403
|
+
return JSON.stringify({
|
|
404
|
+
success: true,
|
|
405
|
+
dtp_name: dtpUpper,
|
|
406
|
+
request_id: requestId,
|
|
407
|
+
message: `DTP run started for ${dtpUpper} (request id ${requestId}). ` +
|
|
408
|
+
`Monitor load status via bw_list_requests (by target InfoProvider) and bw_get_request.`,
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* bw_update_dtp — update a DTP (description).
|
|
413
|
+
*
|
|
414
|
+
* Flow: Lock → GET (fresh) → PUT (fresh) → bwActivate (handles unlock).
|
|
415
|
+
*/
|
|
416
|
+
export async function bwUpdateDtp(client, args) {
|
|
417
|
+
const dtpName = args.dtp_name.toUpperCase();
|
|
418
|
+
const dtpLower = args.dtp_name.toLowerCase();
|
|
419
|
+
// Lock (stateful_enqueue — same pattern as bwUpdateInfoObject)
|
|
420
|
+
const lockHandle = await client.lock('dtpa', dtpLower, {}, 'stateful_enqueue');
|
|
421
|
+
// GET current DTP XML (fresh client) — read timestamp
|
|
422
|
+
const getClient = createClientFromEnv();
|
|
423
|
+
const getResponse = await getClient.get(`/sap/bw/modeling/dtpa/${dtpLower}/m`, MEDIA_TYPES['dtpa']);
|
|
424
|
+
const timestamp = getResponse.headers['timestamp'] ?? '';
|
|
425
|
+
// Apply modifications
|
|
426
|
+
let putXml = getResponse.body;
|
|
427
|
+
if (args.description !== undefined) {
|
|
428
|
+
putXml = putXml.replace(/(<dtpa:dataTransferProcess\b[^>]*\bdescription=)"[^"]*"/, `$1"${args.description}"`);
|
|
429
|
+
}
|
|
430
|
+
if (args.filter_field && args.filter_value !== undefined) {
|
|
431
|
+
const excluding = args.filter_excluding ? 'true' : 'false';
|
|
432
|
+
// Preserve empty string (= '' filter) — do not filter(Boolean); deduplicate via Set
|
|
433
|
+
const values = [...new Set(args.filter_value.split(',').map((v) => v.trim()))];
|
|
434
|
+
// Empty string → self-closing <selection> (no <low>); non-empty → <low value="..."/>
|
|
435
|
+
const selectionsXml = values
|
|
436
|
+
.map((v) => v === ''
|
|
437
|
+
? `<selection excluding="${excluding}" operator="Equal"/>`
|
|
438
|
+
: `<selection excluding="${excluding}" operator="Equal">\n <low description="${v}" value="${v}"/>\n </selection>`)
|
|
439
|
+
.join('\n ') + '\n ';
|
|
440
|
+
// 1. Mark field as selected
|
|
441
|
+
putXml = putXml.replace(new RegExp(`(<fields[^>]*\\bname="${args.filter_field}"(?![^>]*\\bselected="true")[^>]*)(>)`), `$1 selected="true"$2`);
|
|
442
|
+
// 2. Remove any existing <selection> elements
|
|
443
|
+
putXml = putXml.replace(new RegExp(`(<fields[^>]*\\bname="${args.filter_field}"[^>]*>)(<selection[^\\s/>][^>]*>[\\s\\S]*?<\\/selection>|<selection[^>]*\\/?>)\\s*(?=<(?:infoObject|operators))`, 'g'), '$1');
|
|
444
|
+
// 3. Remove <routine/> if already present (to avoid duplicates)
|
|
445
|
+
putXml = putXml.replace(new RegExp(`(<fields[^>]*\\bname="${args.filter_field}"[^>]*>)<routine\\/>`), '$1');
|
|
446
|
+
// 4. Insert <routine/> + selections before <infoObject> (InfoObject fields) or <operators> (plain fields)
|
|
447
|
+
putXml = putXml.replace(new RegExp(`(<fields[^>]*\\bname="${args.filter_field}"[^>]*>)(<(?:infoObject|operators))`), `$1<routine/>\n ${selectionsXml}$2`);
|
|
448
|
+
}
|
|
449
|
+
if (args.filter_clear_fields) {
|
|
450
|
+
const fieldsToClear = args.filter_clear_fields.split(',').map((f) => f.trim()).filter(Boolean);
|
|
451
|
+
for (const fieldName of fieldsToClear) {
|
|
452
|
+
// Remove selected="true"
|
|
453
|
+
putXml = putXml.replace(new RegExp(`(<fields[^>]*\\bname="${fieldName}"[^>]*)\\s+selected="true"`), '$1');
|
|
454
|
+
// Remove all <selection> elements (self-closing and with body)
|
|
455
|
+
putXml = putXml.replace(new RegExp(`(<fields[^>]*\\bname="${fieldName}"[^>]*>)([\\s\\S]*?)(<\\/fields>)`, 'g'), (_match, open, body, close) => {
|
|
456
|
+
const cleaned = body
|
|
457
|
+
.replace(/<selection\b[^>]*\/>/g, '')
|
|
458
|
+
.replace(/<selection\b[^>]*>[\s\S]*?<\/selection>/g, '');
|
|
459
|
+
return open + cleaned + close;
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// Extraction mode: rewrite only extractionMode + deltaSettingStatus on the <extractionSettings>
|
|
464
|
+
// element (Full = F/0, Delta = D/2). allowedExtractionModes, packageSize and parallelExtraction
|
|
465
|
+
// are left unchanged; attributes may appear in any order.
|
|
466
|
+
if (args.extraction_mode !== undefined) {
|
|
467
|
+
const extractionMode = args.extraction_mode === 'full' ? 'F' : 'D';
|
|
468
|
+
const deltaSettingStatus = args.extraction_mode === 'full' ? '0' : '2';
|
|
469
|
+
putXml = putXml.replace(/<extractionSettings\b[^>]*\/>/, (tag) => tag
|
|
470
|
+
.replace(/\bextractionMode="[^"]*"/, `extractionMode="${extractionMode}"`)
|
|
471
|
+
.replace(/\bdeltaSettingStatus="[^"]*"/, `deltaSettingStatus="${deltaSettingStatus}"`));
|
|
472
|
+
}
|
|
473
|
+
// PUT on a fresh stateless client — Eclipse uses a separate stateless session for PUT
|
|
474
|
+
const putClient = createClientFromEnv();
|
|
475
|
+
await putClient.put('dtpa', dtpName, lockHandle, putXml, timestamp, args.transport, args.transport_lock_holder);
|
|
476
|
+
// Activate — handles unlock
|
|
477
|
+
await bwActivate(client, 'dtpa', dtpName, lockHandle, args.transport);
|
|
478
|
+
return JSON.stringify({
|
|
479
|
+
success: true,
|
|
480
|
+
dtp_name: dtpName,
|
|
481
|
+
message: `DTP '${dtpName}' updated and activated successfully.`,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* bw_set_dtp_filter_routine — set an ABAP filter routine on a DTP filter field.
|
|
486
|
+
*
|
|
487
|
+
* Flow:
|
|
488
|
+
* 1. Lock (no CREA)
|
|
489
|
+
* 2. POST generateRoutineProgram → ABAP program name from Location header
|
|
490
|
+
* 3. ADT activate the ABAP program (fresh client)
|
|
491
|
+
* 4. GET routineReports → read back routine XML
|
|
492
|
+
* 5. DELETE routineReports (mandatory cleanup)
|
|
493
|
+
* 6. GET DTP XML (fresh client, read timestamp)
|
|
494
|
+
* 7. Convert routineReports XML → DTP PUT format, inject into <fields> block
|
|
495
|
+
* 8. PUT DTP XML (fresh client)
|
|
496
|
+
* 9. bwActivate with lockHandle
|
|
497
|
+
*/
|
|
498
|
+
export async function bwSetDtpFilterRoutine(client, args) {
|
|
499
|
+
const dtpUpper = args.dtp_name.toUpperCase();
|
|
500
|
+
const dtpLower = args.dtp_name.toLowerCase();
|
|
501
|
+
const fieldName = args.field_name;
|
|
502
|
+
// Step 1: Lock (no CREA)
|
|
503
|
+
const lockCsrf = await client.getCsrfToken();
|
|
504
|
+
const lockResponse = await client.rawPost(`/sap/bw/modeling/dtpa/${dtpLower}?action=lock`, '', {
|
|
505
|
+
'Accept': MEDIA_TYPES['dtpa'],
|
|
506
|
+
'x-csrf-token': lockCsrf,
|
|
507
|
+
});
|
|
508
|
+
const lockHandle = lockResponse.body.match(/<LOCK_HANDLE>([^<]+)<\/LOCK_HANDLE>/)?.[1] ?? '';
|
|
509
|
+
if (!lockHandle) {
|
|
510
|
+
throw new Error(`No <LOCK_HANDLE> in lock response:\n${lockResponse.body}`);
|
|
511
|
+
}
|
|
512
|
+
// Step 2: POST generateRoutineProgram
|
|
513
|
+
const escapeXml = (s) => s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
514
|
+
const codeLines = args.routine_code.split('\n');
|
|
515
|
+
const codeXml = codeLines.map(l => ` <line>${escapeXml(l)}</line>`).join('\n');
|
|
516
|
+
let globalXml = '';
|
|
517
|
+
if (args.global_code) {
|
|
518
|
+
const globalLines = args.global_code.split('\n');
|
|
519
|
+
globalXml = ` <globalCode>\n${globalLines.map(l => ` <line>${escapeXml(l)}</line>`).join('\n')}\n </globalCode>\n`;
|
|
520
|
+
}
|
|
521
|
+
const routineBody = `<routine>\n${globalXml} <code>\n${codeXml}\n </code>\n</routine>`;
|
|
522
|
+
const genCsrf = await client.getCsrfToken();
|
|
523
|
+
const genResponse = await client.rawPost(`/sap/bw/modeling/dtpa/${dtpUpper}/${fieldName}/generateRoutineProgram`, routineBody, {
|
|
524
|
+
'Content-Type': 'application/vnd.sap.bw.modeling.dtpa.routine.code-v1_0_0+xml',
|
|
525
|
+
'Accept': MEDIA_TYPES['dtpa'],
|
|
526
|
+
'x-csrf-token': genCsrf,
|
|
527
|
+
});
|
|
528
|
+
const genLocation = genResponse.headers['location'] ?? genResponse.headers['Location'] ?? '';
|
|
529
|
+
if (!genLocation) {
|
|
530
|
+
throw new Error(`generateRoutineProgram returned no Location header. Headers: ${JSON.stringify(genResponse.headers)}`);
|
|
531
|
+
}
|
|
532
|
+
const encodedProgram = genLocation.split('/routineReports/').pop() ?? '';
|
|
533
|
+
const programName = decodeURIComponent(encodedProgram);
|
|
534
|
+
// Step 3: ADT activate ABAP program (fresh client for session isolation)
|
|
535
|
+
const urlEncodedProgram = encodeURIComponent(programName).toLowerCase();
|
|
536
|
+
const adtClient = createClientFromEnv();
|
|
537
|
+
const adtCsrf = await adtClient.getCsrfToken();
|
|
538
|
+
const adtBody = `<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
539
|
+
`<adtcore:objectReferences xmlns:adtcore="http://www.sap.com/adt/core">\n` +
|
|
540
|
+
` <adtcore:objectReference adtcore:uri="/sap/bc/adt/programs/programs/${urlEncodedProgram}"\n` +
|
|
541
|
+
` adtcore:name="${programName.toUpperCase()}"/>\n` +
|
|
542
|
+
`</adtcore:objectReferences>`;
|
|
543
|
+
await adtClient.rawPost('/sap/bc/adt/activation?method=activate&preauditRequested=true', adtBody, {
|
|
544
|
+
'Content-Type': 'application/xml',
|
|
545
|
+
'Accept': 'application/xml',
|
|
546
|
+
'x-csrf-token': adtCsrf,
|
|
547
|
+
});
|
|
548
|
+
// Step 4: GET routineReports (read back routine code as XML)
|
|
549
|
+
const routineGetClient = createClientFromEnv();
|
|
550
|
+
const routineGetResponse = await routineGetClient.get(`/sap/bw/modeling/dtpa/${dtpUpper}/${fieldName}/routineReports/${encodedProgram}`, MEDIA_TYPES['dtpa']);
|
|
551
|
+
const routineXml = routineGetResponse.body;
|
|
552
|
+
// Step 5: DELETE routineReports (mandatory cleanup)
|
|
553
|
+
await client.rawDelete(`/sap/bw/modeling/dtpa/${dtpUpper}/${fieldName}/routineReports/${encodedProgram}`, {
|
|
554
|
+
'Content-Type': MEDIA_TYPES['dtpa'],
|
|
555
|
+
'Accept': MEDIA_TYPES['dtpa'],
|
|
556
|
+
});
|
|
557
|
+
// Step 6: GET current DTP XML (fresh client, read timestamp)
|
|
558
|
+
const dtpGetClient = createClientFromEnv();
|
|
559
|
+
const dtpGetResponse = await dtpGetClient.get(`/sap/bw/modeling/dtpa/${dtpLower}/m`, MEDIA_TYPES['dtpa']);
|
|
560
|
+
const timestamp = dtpGetResponse.headers['timestamp'] ?? '';
|
|
561
|
+
// Step 7: Convert routineReports XML → DTP PUT format
|
|
562
|
+
// Extract code lines from <code>...</code>
|
|
563
|
+
const codeSection = routineXml.match(/<code>([\s\S]*?)<\/code>/)?.[1] ?? '';
|
|
564
|
+
const extractedCodeLines = [...codeSection.matchAll(/<line>([\s\S]*?)<\/line>/g)].map(m => m[1]);
|
|
565
|
+
// Extract global lines from <globalCode>...</globalCode>
|
|
566
|
+
const globalSection = routineXml.match(/<globalCode>([\s\S]*?)<\/globalCode>/)?.[1] ?? '';
|
|
567
|
+
const extractedGlobalLines = [...globalSection.matchAll(/<line>([\s\S]*?)<\/line>/g)].map(m => m[1]);
|
|
568
|
+
// <line> → <code>, empty lines → <code xsi:nil="true"/>
|
|
569
|
+
const codeElements = extractedCodeLines
|
|
570
|
+
.map(line => (line ? `<code>${line}</code>` : `<code xsi:nil="true"/>`))
|
|
571
|
+
.join('\n ');
|
|
572
|
+
const routineInjection = `<routine>\n ${codeElements}\n </routine>`;
|
|
573
|
+
// <globalCode><line> → <globalRoutineCode>
|
|
574
|
+
const globalElements = extractedGlobalLines
|
|
575
|
+
.map(line => ` <globalRoutineCode>${line}</globalRoutineCode>`)
|
|
576
|
+
.join('\n');
|
|
577
|
+
// Step 8: Inject into DTP XML
|
|
578
|
+
let putXml = dtpGetResponse.body;
|
|
579
|
+
// Fix 3: Add selected="true" to the matching <fields> element if not already present
|
|
580
|
+
putXml = putXml.replace(new RegExp(`(<fields[^>]*\\bname="${fieldName}"(?![^>]*\\bselected="true")[^>]*)(>)`), `$1 selected="true"$2`);
|
|
581
|
+
// Fix 1: Inject <routine> before the first <operators> inside the matching fields block.
|
|
582
|
+
// Remove any existing <routine/> or <routine>...</routine> first, then inject before <operators>.
|
|
583
|
+
putXml = putXml.replace(new RegExp(`(<fields[^>]*\\bname="${fieldName}"[^>]*>[\\s\\S]*?)<routine\\s*\\/>`), '$1');
|
|
584
|
+
putXml = putXml.replace(new RegExp(`(<fields[^>]*\\bname="${fieldName}"[^>]*>[\\s\\S]*?)(<operators)`), `$1${routineInjection}\n $2`);
|
|
585
|
+
// Fix 2: Remove all existing <globalRoutineCode> elements before inserting new ones
|
|
586
|
+
putXml = putXml.replace(/<globalRoutineCode>[^<]*<\/globalRoutineCode>\s*/g, '');
|
|
587
|
+
// Append globalRoutineCode elements before </filter>
|
|
588
|
+
if (globalElements) {
|
|
589
|
+
putXml = putXml.replace('</filter>', `${globalElements}\n </filter>`);
|
|
590
|
+
}
|
|
591
|
+
// PUT with fresh client
|
|
592
|
+
const putClient = createClientFromEnv();
|
|
593
|
+
await putClient.put('dtpa', dtpUpper, lockHandle, putXml, timestamp);
|
|
594
|
+
// Step 9: Activate (activation framework handles unlock for dtpa)
|
|
595
|
+
await bwActivate(client, 'dtpa', dtpUpper, lockHandle);
|
|
596
|
+
return JSON.stringify({
|
|
597
|
+
success: true,
|
|
598
|
+
dtp_name: dtpUpper,
|
|
599
|
+
field_name: fieldName,
|
|
600
|
+
message: `Filter routine for field '${fieldName}' on DTP '${dtpUpper}' set and activated successfully.`,
|
|
601
|
+
});
|
|
602
|
+
}
|