@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,558 @@
|
|
|
1
|
+
const BICS_ACCEPT = 'application/vnd.sap.bw.modeling.bicsresponse-v1_1_0+xml';
|
|
2
|
+
const BICS_CONTENT_TYPE = 'application/vnd.sap.bw.modeling.bicsrequest-v1_1_0+xml';
|
|
3
|
+
const VALUE_HELP_ACCEPT = 'application/vnd.sap-bw-modeling.isvaluehelp-v1_0_0+xml';
|
|
4
|
+
// ── XML helpers ────────────────────────────────────────────────────────────────
|
|
5
|
+
function attr(s, name) {
|
|
6
|
+
const m = s.match(new RegExp(`\\b${name}="([^"]*)"`));
|
|
7
|
+
return m ? m[1] : null;
|
|
8
|
+
}
|
|
9
|
+
function xmlEscape(s) {
|
|
10
|
+
return s
|
|
11
|
+
.replace(/&/g, '&')
|
|
12
|
+
.replace(/</g, '<')
|
|
13
|
+
.replace(/>/g, '>')
|
|
14
|
+
.replace(/"/g, '"');
|
|
15
|
+
}
|
|
16
|
+
function parseQueryViewAttrs(xml) {
|
|
17
|
+
const m = xml.match(/<queryView\b([\s\S]*?)>/);
|
|
18
|
+
const a = m?.[1] ?? '';
|
|
19
|
+
return {
|
|
20
|
+
name: attr(a, 'name'),
|
|
21
|
+
txt: attr(a, 'txt'),
|
|
22
|
+
dataRollup: attr(a, 'dataRollup'),
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function parseMetaData(xml) {
|
|
26
|
+
const mdMatch = xml.match(/<metaData\b([\s\S]*?)>([\s\S]*?)<\/metaData>/);
|
|
27
|
+
if (!mdMatch)
|
|
28
|
+
return null;
|
|
29
|
+
const mdAttrs = mdMatch[1];
|
|
30
|
+
const mdBody = mdMatch[2];
|
|
31
|
+
const keyFigures = [];
|
|
32
|
+
const kfBlock = mdBody.match(/<keyFigures>([\s\S]*?)<\/keyFigures>/)?.[1] ?? '';
|
|
33
|
+
const entryRe = /<entry\b([\s\S]*?)(?:\/>|>)/g;
|
|
34
|
+
let em;
|
|
35
|
+
while ((em = entryRe.exec(kfBlock)) !== null) {
|
|
36
|
+
const a = em[1];
|
|
37
|
+
keyFigures.push({
|
|
38
|
+
name: attr(a, 'name') ?? '',
|
|
39
|
+
txt: attr(a, 'txt') ?? '',
|
|
40
|
+
id: attr(a, 'id') ?? '',
|
|
41
|
+
dataType: attr(a, 'dataType') ?? '',
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
const characteristics = [];
|
|
45
|
+
const chaBlock = mdBody.match(/<characteristics>([\s\S]*?)<\/characteristics>/)?.[1] ?? '';
|
|
46
|
+
entryRe.lastIndex = 0;
|
|
47
|
+
while ((em = entryRe.exec(chaBlock)) !== null) {
|
|
48
|
+
const a = em[1];
|
|
49
|
+
characteristics.push({
|
|
50
|
+
name: attr(a, 'name') ?? '',
|
|
51
|
+
txt: attr(a, 'txt') ?? '',
|
|
52
|
+
id: attr(a, 'id') ?? '',
|
|
53
|
+
axis: attr(a, 'axis') ?? '',
|
|
54
|
+
isStructure: attr(a, 'isStructure') ?? 'false',
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
infoProvider: attr(mdAttrs, 'infoProvider'),
|
|
59
|
+
infoProviderText: attr(mdAttrs, 'infoProviderText'),
|
|
60
|
+
keyFigures,
|
|
61
|
+
characteristics,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function parseVariablesContainer(xml) {
|
|
65
|
+
const vcMatch = xml.match(/<variablesContainer\b([\s\S]*?)>([\s\S]*?)<\/variablesContainer>/);
|
|
66
|
+
if (!vcMatch)
|
|
67
|
+
return null;
|
|
68
|
+
const vcAttrs = vcMatch[1];
|
|
69
|
+
const vcBody = vcMatch[2];
|
|
70
|
+
const variables = [];
|
|
71
|
+
const varRe = /<variable\b([\s\S]*?)>([\s\S]*?)<\/variable>/g;
|
|
72
|
+
let vm;
|
|
73
|
+
while ((vm = varRe.exec(vcBody)) !== null) {
|
|
74
|
+
const vAttrs = vm[1];
|
|
75
|
+
const vBody = vm[2];
|
|
76
|
+
const selectValues = [];
|
|
77
|
+
const svRe = /<selectValue\b([\s\S]*?)(?:\/>|>)/g;
|
|
78
|
+
let sv;
|
|
79
|
+
while ((sv = svRe.exec(vBody)) !== null) {
|
|
80
|
+
const svA = sv[1];
|
|
81
|
+
const high = attr(svA, 'high');
|
|
82
|
+
const svObj = {
|
|
83
|
+
low: attr(svA, 'low') ?? '',
|
|
84
|
+
op: attr(svA, 'op') ?? '',
|
|
85
|
+
sign: attr(svA, 'sign') ?? '',
|
|
86
|
+
presentationMode: attr(svA, 'presentationMode') ?? '',
|
|
87
|
+
};
|
|
88
|
+
if (high !== null)
|
|
89
|
+
svObj.high = high;
|
|
90
|
+
selectValues.push(svObj);
|
|
91
|
+
}
|
|
92
|
+
variables.push({
|
|
93
|
+
id: attr(vAttrs, 'id') ?? '',
|
|
94
|
+
name: attr(vAttrs, 'name') ?? '',
|
|
95
|
+
altName: attr(vAttrs, 'altName') ?? '',
|
|
96
|
+
txt: attr(vAttrs, 'txt') ?? '',
|
|
97
|
+
iobj: attr(vAttrs, 'iobj') ?? '',
|
|
98
|
+
mandatory: attr(vAttrs, 'mandatory') ?? '',
|
|
99
|
+
inputEnabled: attr(vAttrs, 'inputEnabled') ?? '',
|
|
100
|
+
selectValues,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
size: attr(vcAttrs, 'size'),
|
|
105
|
+
inputRequired: attr(vcAttrs, 'inputRequired'),
|
|
106
|
+
variables,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function parseSpace(xml) {
|
|
110
|
+
const spaceMatch = xml.match(/<space>([\s\S]*?)<\/space>/);
|
|
111
|
+
if (!spaceMatch)
|
|
112
|
+
return [];
|
|
113
|
+
const result = [];
|
|
114
|
+
const ioRe = /<infoObject\b([\s\S]*?)>([\s\S]*?)<\/infoObject>/g;
|
|
115
|
+
let io;
|
|
116
|
+
while ((io = ioRe.exec(spaceMatch[1])) !== null) {
|
|
117
|
+
const ioAttrs = io[1];
|
|
118
|
+
const ioBody = io[2];
|
|
119
|
+
const selectValues = [];
|
|
120
|
+
const svRe = /<selectValue\b([\s\S]*?)(?:\/>|>)/g;
|
|
121
|
+
let sv;
|
|
122
|
+
while ((sv = svRe.exec(ioBody)) !== null) {
|
|
123
|
+
const svA = sv[1];
|
|
124
|
+
selectValues.push({
|
|
125
|
+
lowInt: attr(svA, 'lowInt') ?? attr(svA, 'low') ?? '',
|
|
126
|
+
op: attr(svA, 'op') ?? '',
|
|
127
|
+
sign: attr(svA, 'sign') ?? '',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
result.push({
|
|
131
|
+
id: attr(ioAttrs, 'id') ?? '',
|
|
132
|
+
name: attr(ioAttrs, 'name') ?? '',
|
|
133
|
+
selectValues,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
function parseTupleSection(sectionXml) {
|
|
139
|
+
const headersBlock = sectionXml.match(/<headers>([\s\S]*?)<\/headers>/)?.[1] ?? '';
|
|
140
|
+
const headers = [];
|
|
141
|
+
const hRe = /<entry\b([\s\S]*?)(?:\/>|>)/g;
|
|
142
|
+
let hm;
|
|
143
|
+
while ((hm = hRe.exec(headersBlock)) !== null) {
|
|
144
|
+
const ha = hm[1];
|
|
145
|
+
headers.push({ name: attr(ha, 'name') ?? '', txt: attr(ha, 'txt') ?? '', id: attr(ha, 'id') ?? '' });
|
|
146
|
+
}
|
|
147
|
+
const tuplesBlock = sectionXml.match(/<tuples\b[^>]*>([\s\S]*?)<\/tuples>/)?.[1] ?? '';
|
|
148
|
+
const tuples = [];
|
|
149
|
+
const tRe = /<tuple\b([\s\S]*?)>([\s\S]*?)<\/tuple>/g;
|
|
150
|
+
let tm;
|
|
151
|
+
while ((tm = tRe.exec(tuplesBlock)) !== null) {
|
|
152
|
+
const tAttrs = tm[1];
|
|
153
|
+
const tBody = tm[2];
|
|
154
|
+
const values = [];
|
|
155
|
+
const vRe = /<value\b([\s\S]*?)(?:\/>|>)/g;
|
|
156
|
+
let vm;
|
|
157
|
+
while ((vm = vRe.exec(tBody)) !== null) {
|
|
158
|
+
const va = vm[1];
|
|
159
|
+
const v = {
|
|
160
|
+
id: attr(va, 'id') ?? '',
|
|
161
|
+
sid: attr(va, 'sid') ?? '',
|
|
162
|
+
selType: attr(va, 'selType') ?? '',
|
|
163
|
+
extKey: attr(va, 'extKey') ?? '',
|
|
164
|
+
intKey: attr(va, 'intKey') ?? '',
|
|
165
|
+
txt: attr(va, 'txt') ?? '',
|
|
166
|
+
};
|
|
167
|
+
const drillSt = attr(va, 'drillSt');
|
|
168
|
+
const dispLvl = attr(va, 'dispLvl');
|
|
169
|
+
if (drillSt !== null)
|
|
170
|
+
v.drillSt = drillSt;
|
|
171
|
+
if (dispLvl !== null)
|
|
172
|
+
v.dispLvl = dispLvl;
|
|
173
|
+
values.push(v);
|
|
174
|
+
}
|
|
175
|
+
tuples.push({ tid: attr(tAttrs, 'tid') ?? '', values });
|
|
176
|
+
}
|
|
177
|
+
return { headers, tuples };
|
|
178
|
+
}
|
|
179
|
+
function parseResultSet(xml) {
|
|
180
|
+
if (/<resultSet\s*\/>/.test(xml)) {
|
|
181
|
+
return {
|
|
182
|
+
fromRow: '0',
|
|
183
|
+
toRow: '1000',
|
|
184
|
+
columnHeaders: [],
|
|
185
|
+
columnTuples: [],
|
|
186
|
+
rowHeaders: [],
|
|
187
|
+
rowTuples: [],
|
|
188
|
+
cells: [],
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const rsMatch = xml.match(/<resultSet\b([\s\S]*?)>([\s\S]*?)<\/resultSet>/);
|
|
192
|
+
const rsAttrs = rsMatch?.[1] ?? '';
|
|
193
|
+
const rsBody = rsMatch?.[2] ?? '';
|
|
194
|
+
const columnsMatch = rsBody.match(/<columns\b[^>]*>([\s\S]*?)<\/columns>/);
|
|
195
|
+
const rowsMatch = rsBody.match(/<rows\b[^>]*>([\s\S]*?)<\/rows>/);
|
|
196
|
+
const colParsed = columnsMatch ? parseTupleSection(columnsMatch[1]) : { headers: [], tuples: [] };
|
|
197
|
+
const rowParsed = rowsMatch ? parseTupleSection(rowsMatch[1]) : { headers: [], tuples: [] };
|
|
198
|
+
const cells = [];
|
|
199
|
+
const dataBlock = rsBody.match(/<data\b[^>]*>([\s\S]*?)<\/data>/)?.[1] ?? '';
|
|
200
|
+
const cellRe = /<cell\b([\s\S]*?)(?:\/>|>)/g;
|
|
201
|
+
let cm;
|
|
202
|
+
while ((cm = cellRe.exec(dataBlock)) !== null) {
|
|
203
|
+
const ca = cm[1];
|
|
204
|
+
cells.push({
|
|
205
|
+
crv: attr(ca, 'crv') ?? '',
|
|
206
|
+
txt: attr(ca, 'txt') ?? '',
|
|
207
|
+
row: attr(ca, 'row') ?? '',
|
|
208
|
+
col: attr(ca, 'col') ?? '',
|
|
209
|
+
mcu: attr(ca, 'mcu') === 'true',
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
fromRow: attr(rsAttrs, 'fromRow') ?? '0',
|
|
214
|
+
toRow: attr(rsAttrs, 'toRow') ?? '1000',
|
|
215
|
+
columnHeaders: colParsed.headers,
|
|
216
|
+
columnTuples: colParsed.tuples,
|
|
217
|
+
rowHeaders: rowParsed.headers,
|
|
218
|
+
rowTuples: rowParsed.tuples,
|
|
219
|
+
cells,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
function parseMessages(xml) {
|
|
223
|
+
const msgMatch = xml.match(/<messages>([\s\S]*?)<\/messages>/);
|
|
224
|
+
if (!msgMatch)
|
|
225
|
+
return [];
|
|
226
|
+
const msgs = [];
|
|
227
|
+
const eRe = /<entry\b([\s\S]*?)(?:\/>|>)/g;
|
|
228
|
+
let em;
|
|
229
|
+
while ((em = eRe.exec(msgMatch[1])) !== null) {
|
|
230
|
+
const ea = em[1];
|
|
231
|
+
msgs.push({ type: attr(ea, 'type') ?? '', txt: attr(ea, 'txt') ?? '' });
|
|
232
|
+
}
|
|
233
|
+
return msgs;
|
|
234
|
+
}
|
|
235
|
+
// ── Text rendering ──────────────────────────────────────────────────────────────
|
|
236
|
+
// Renders a human-readable label for a tuple's values.
|
|
237
|
+
// Handles: normal members, hierarchy nodes (negative sid), TOTAL rows,
|
|
238
|
+
// structure members (selType STRU1/STRU2 — identical rendering for both),
|
|
239
|
+
// and REST_H (not assigned) nodes.
|
|
240
|
+
// When a tuple has multiple values (multi-dimension drilldown), labels are
|
|
241
|
+
// joined with ' / '.
|
|
242
|
+
function tupleLabel(values) {
|
|
243
|
+
return values.map(v => {
|
|
244
|
+
if (v.selType === 'TOTAL' || v.intKey === 'SUMME')
|
|
245
|
+
return 'Total';
|
|
246
|
+
if (v.intKey === 'REST_H')
|
|
247
|
+
return '(not assigned)';
|
|
248
|
+
const sid = parseInt(v.sid, 10);
|
|
249
|
+
const label = v.txt || v.extKey || v.intKey;
|
|
250
|
+
if (sid < 0) {
|
|
251
|
+
const lvl = parseInt(v.dispLvl ?? '0', 10);
|
|
252
|
+
const indent = ' '.repeat(lvl);
|
|
253
|
+
const prefix = v.drillSt === '3' ? '+' : '+--';
|
|
254
|
+
return `${indent}${prefix} ${label}`;
|
|
255
|
+
}
|
|
256
|
+
if ((v.selType === 'STRU1' || v.selType === 'STRU2') && v.dispLvl !== undefined) {
|
|
257
|
+
const lvl = parseInt(v.dispLvl, 10);
|
|
258
|
+
const indent = ' '.repeat(lvl);
|
|
259
|
+
const prefix = v.drillSt === '3' ? 'v ' : v.drillSt === '2' ? '> ' : '';
|
|
260
|
+
return `${indent}${prefix}${label}`;
|
|
261
|
+
}
|
|
262
|
+
return label;
|
|
263
|
+
}).filter(s => s).join(' / ');
|
|
264
|
+
}
|
|
265
|
+
function renderQueryDataText(xml, isGet) {
|
|
266
|
+
const lines = [];
|
|
267
|
+
// 1. Header
|
|
268
|
+
const viewAttrs = parseQueryViewAttrs(xml);
|
|
269
|
+
lines.push(`Query/Provider: ${viewAttrs.name ?? ''}`);
|
|
270
|
+
if (viewAttrs.txt)
|
|
271
|
+
lines.push(`Description: ${viewAttrs.txt}`);
|
|
272
|
+
const metaData = isGet ? parseMetaData(xml) : null;
|
|
273
|
+
if (metaData) {
|
|
274
|
+
lines.push(`InfoProvider: ${metaData.infoProvider ?? ''}${metaData.infoProviderText ? ` (${metaData.infoProviderText})` : ''}`);
|
|
275
|
+
}
|
|
276
|
+
if (viewAttrs.dataRollup)
|
|
277
|
+
lines.push(`Data as of: ${viewAttrs.dataRollup}`);
|
|
278
|
+
const rs = parseResultSet(xml);
|
|
279
|
+
lines.push(`Row range: ${rs.fromRow}–${rs.toRow}`);
|
|
280
|
+
// 2. Variables
|
|
281
|
+
const vc = parseVariablesContainer(xml);
|
|
282
|
+
if (vc && vc.variables.length > 0) {
|
|
283
|
+
lines.push('');
|
|
284
|
+
lines.push(`── Variables (inputRequired=${vc.inputRequired}) ──`);
|
|
285
|
+
const allEmpty = vc.variables.every(v => v.selectValues.length === 0);
|
|
286
|
+
if (vc.inputRequired === 'true' && allEmpty) {
|
|
287
|
+
lines.push(' NOTE: Input required — fill variables via POST before data is available.');
|
|
288
|
+
lines.push(' Use bw_get_filter_values to look up valid characteristic values.');
|
|
289
|
+
}
|
|
290
|
+
for (const v of vc.variables) {
|
|
291
|
+
const req = v.mandatory === 'true' ? ' [REQUIRED]' : '';
|
|
292
|
+
const vals = v.selectValues.length > 0
|
|
293
|
+
? v.selectValues
|
|
294
|
+
.map(sv => `${sv.sign}${sv.op} ${sv.low}${sv.high ? `..${sv.high}` : ''}`)
|
|
295
|
+
.join(', ')
|
|
296
|
+
: '(not set)';
|
|
297
|
+
lines.push(` ${v.name.trimEnd()} (${v.txt})${req}: ${vals}`);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
// 3. Background filters from <space>
|
|
301
|
+
const spaceFilters = parseSpace(xml);
|
|
302
|
+
if (spaceFilters.length > 0) {
|
|
303
|
+
lines.push('');
|
|
304
|
+
lines.push('── Background Filters (query-defined, read-only) ──');
|
|
305
|
+
for (const f of spaceFilters) {
|
|
306
|
+
const vals = f.selectValues.map(sv => `${sv.sign}${sv.op} ${sv.lowInt}`).join(', ');
|
|
307
|
+
lines.push(` ${f.name}: ${vals}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// 4. Result table
|
|
311
|
+
lines.push('');
|
|
312
|
+
lines.push(`── Result (${rs.rowTuples.length} rows × ${rs.columnTuples.length} columns) ──`);
|
|
313
|
+
if (rs.columnTuples.length === 0 && rs.rowTuples.length === 0) {
|
|
314
|
+
lines.push(' (no data)');
|
|
315
|
+
}
|
|
316
|
+
else {
|
|
317
|
+
const cellMap = new Map();
|
|
318
|
+
for (const c of rs.cells) {
|
|
319
|
+
cellMap.set(`${c.row}:${c.col}`, { txt: c.txt, mcu: c.mcu });
|
|
320
|
+
}
|
|
321
|
+
const colLabels = rs.columnTuples.map(ct => tupleLabel(ct.values));
|
|
322
|
+
const rowAxisLabels = rs.rowHeaders.map(h => h.txt || h.name);
|
|
323
|
+
lines.push([...rowAxisLabels, ...colLabels].join(' | '));
|
|
324
|
+
lines.push('-'.repeat(Math.min(200, [...rowAxisLabels, ...colLabels].join(' | ').length)));
|
|
325
|
+
for (let ri = 0; ri < rs.rowTuples.length; ri++) {
|
|
326
|
+
const rt = rs.rowTuples[ri];
|
|
327
|
+
const rowIdx = ri + 1;
|
|
328
|
+
const isTotal = rt.values.some(v => v.selType === 'TOTAL' || v.intKey === 'SUMME');
|
|
329
|
+
if (isTotal)
|
|
330
|
+
lines.push('');
|
|
331
|
+
const rowLabel = tupleLabel(rt.values);
|
|
332
|
+
const cellValues = rs.columnTuples.map((_, ci) => {
|
|
333
|
+
const cell = cellMap.get(`${rowIdx}:${ci + 1}`);
|
|
334
|
+
if (!cell)
|
|
335
|
+
return '-';
|
|
336
|
+
return cell.mcu ? '* (multi-currency)' : cell.txt;
|
|
337
|
+
});
|
|
338
|
+
lines.push([rowLabel, ...cellValues].join(' | '));
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// 5. Messages
|
|
342
|
+
const msgs = parseMessages(xml);
|
|
343
|
+
if (msgs.length > 0) {
|
|
344
|
+
lines.push('');
|
|
345
|
+
lines.push('── Messages ──');
|
|
346
|
+
for (const m of msgs) {
|
|
347
|
+
lines.push(` [${m.type}] ${m.txt}`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// 6. Metadata (GET only)
|
|
351
|
+
if (isGet && metaData) {
|
|
352
|
+
if (metaData.keyFigures.length > 0) {
|
|
353
|
+
lines.push('');
|
|
354
|
+
lines.push('── Available Key Figures ──');
|
|
355
|
+
for (const kf of metaData.keyFigures) {
|
|
356
|
+
lines.push(` ${kf.name} (${kf.txt}) [${kf.dataType}] id=${kf.id}`);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (metaData.characteristics.length > 0) {
|
|
360
|
+
lines.push('');
|
|
361
|
+
lines.push('── Available Characteristics ──');
|
|
362
|
+
for (const cha of metaData.characteristics) {
|
|
363
|
+
const struct = cha.isStructure === 'true' ? ' [structure]' : '';
|
|
364
|
+
lines.push(` ${cha.name} (${cha.txt}) axis=${cha.axis}${struct} id=${cha.id}`);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return lines.join('\n');
|
|
369
|
+
}
|
|
370
|
+
// ── POST body builder ──────────────────────────────────────────────────────────
|
|
371
|
+
function buildPostBody(compId, state, variables, drillOperations) {
|
|
372
|
+
const parts = [];
|
|
373
|
+
parts.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
|
374
|
+
parts.push(`<querySelector name="${xmlEscape(compId)}">`);
|
|
375
|
+
parts.push(` <selection>`);
|
|
376
|
+
if (variables && variables.length > 0) {
|
|
377
|
+
parts.push(` <variablesContainer>`);
|
|
378
|
+
for (const v of variables) {
|
|
379
|
+
const txt = v.txt ? ` txt="${xmlEscape(v.txt)}"` : '';
|
|
380
|
+
const altName = v.altName ? ` altName="${xmlEscape(v.altName)}"` : '';
|
|
381
|
+
const type = ` type="${xmlEscape(v.type ?? 'charMember')}"`;
|
|
382
|
+
const inputEnabled = ` inputEnabled="${v.inputEnabled !== undefined ? v.inputEnabled : true}"`;
|
|
383
|
+
const mandatory = v.mandatory !== undefined ? ` mandatory="${v.mandatory}"` : '';
|
|
384
|
+
const iobj = v.iobj ? ` iobj="${xmlEscape(v.iobj)}"` : '';
|
|
385
|
+
parts.push(` <variable name="${xmlEscape(v.name)}" id="${xmlEscape(v.id)}"${txt}${altName}${type}${inputEnabled}${mandatory}${iobj}>`);
|
|
386
|
+
let svId = 0;
|
|
387
|
+
for (const sv of v.values) {
|
|
388
|
+
const op = sv.op ?? 'EQ';
|
|
389
|
+
const sign = sv.sign ?? 'I';
|
|
390
|
+
const high = sv.high ? ` high="${xmlEscape(sv.high)}"` : '';
|
|
391
|
+
parts.push(` <selectValue id="${svId++}" low="${xmlEscape(sv.low)}"${high} nodeId="0" hryMinLvl="0" op="${op}" sign="${sign}" presentationMode="EXT"/>`);
|
|
392
|
+
}
|
|
393
|
+
parts.push(` </variable>`);
|
|
394
|
+
}
|
|
395
|
+
parts.push(` </variablesContainer>`);
|
|
396
|
+
}
|
|
397
|
+
if (state && state.infoObjects.length > 0) {
|
|
398
|
+
parts.push(` <state>`);
|
|
399
|
+
for (const io of state.infoObjects) {
|
|
400
|
+
const hasFilter = io.filterValues && io.filterValues.length > 0;
|
|
401
|
+
if (hasFilter || io.hierarchy) {
|
|
402
|
+
parts.push(` <infoObject name="${xmlEscape(io.name)}" id="${xmlEscape(io.id)}" axis="${xmlEscape(io.axis)}" pos="0">`);
|
|
403
|
+
if (io.hierarchy) {
|
|
404
|
+
const hFrom = io.hierarchy.hryDateFrom ?? '00000000';
|
|
405
|
+
const hTo = io.hierarchy.hryDateTo ?? '99991231';
|
|
406
|
+
parts.push(` <hierarchy id="${xmlEscape(io.hierarchy.id)}" name="${xmlEscape(io.hierarchy.name)}" hryId="${xmlEscape(io.hierarchy.hryId)}" hryDateFrom="${hFrom}" hryDateTo="${hTo}"/>`);
|
|
407
|
+
}
|
|
408
|
+
let svId = 0;
|
|
409
|
+
for (const fv of (io.filterValues ?? [])) {
|
|
410
|
+
const op = fv.op ?? 'EQ';
|
|
411
|
+
const sign = fv.sign ?? 'I';
|
|
412
|
+
const lowText = fv.lowText ? ` lowText="${xmlEscape(fv.lowText)}"` : '';
|
|
413
|
+
const high = fv.high ? ` high="${xmlEscape(fv.high)}"` : '';
|
|
414
|
+
const nodeId = fv.nodeId ?? 0;
|
|
415
|
+
if (fv.lowInt) {
|
|
416
|
+
parts.push(` <selectValue id="${svId++}" lowInt="${xmlEscape(fv.lowInt)}"${high} nodeId="${nodeId}" hryMinLvl="0" op="${op}" sign="${sign}" presentationMode="INT"/>`);
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
parts.push(` <selectValue id="${svId++}" low="${xmlEscape(fv.low ?? '')}"${lowText}${high} nodeId="${nodeId}" hryMinLvl="0" op="${op}" sign="${sign}" presentationMode="EXT_NC"/>`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
parts.push(` </infoObject>`);
|
|
423
|
+
}
|
|
424
|
+
else {
|
|
425
|
+
parts.push(` <infoObject name="${xmlEscape(io.name)}" id="${xmlEscape(io.id)}" axis="${xmlEscape(io.axis)}" pos="0"/>`);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
parts.push(` </state>`);
|
|
429
|
+
}
|
|
430
|
+
if (drillOperations && drillOperations.length > 0) {
|
|
431
|
+
parts.push(` <drillOps>`);
|
|
432
|
+
drillOperations.forEach((op, i) => {
|
|
433
|
+
parts.push(` <operation step="${i + 1}" axis="${op.axis}" drillSt="${op.drill_state}" ` +
|
|
434
|
+
`drillLvl="1" tupleIdx="${op.tuple_idx}" elementIdx="${op.element_idx}"/>`);
|
|
435
|
+
});
|
|
436
|
+
parts.push(` </drillOps>`);
|
|
437
|
+
}
|
|
438
|
+
parts.push(` </selection>`);
|
|
439
|
+
parts.push(`</querySelector>`);
|
|
440
|
+
return parts.join('\n');
|
|
441
|
+
}
|
|
442
|
+
// ── Exported functions ─────────────────────────────────────────────────────────
|
|
443
|
+
export async function bwQueryData(client, compId, isProvider = false, format = 'text', state, variables, fromRow = 0, toRow = 1000, drillOperations) {
|
|
444
|
+
const effectiveCompId = isProvider ? `!${compId}` : compId;
|
|
445
|
+
const url = `/sap/bw/modeling/comp/reporting?compid=${encodeURIComponent(effectiveCompId)}`;
|
|
446
|
+
const isPost = !!(state || variables || (drillOperations && drillOperations.length > 0));
|
|
447
|
+
let responseXml;
|
|
448
|
+
if (isPost) {
|
|
449
|
+
const postBody = buildPostBody(compId, state, variables, drillOperations);
|
|
450
|
+
const doPost = async () => {
|
|
451
|
+
const csrfToken = await client.getCsrfToken();
|
|
452
|
+
const postResult = await client.rawPost(url, postBody, {
|
|
453
|
+
'Content-Type': BICS_CONTENT_TYPE,
|
|
454
|
+
'X-CSRF-Token': csrfToken,
|
|
455
|
+
Accept: BICS_ACCEPT,
|
|
456
|
+
InclMetadata: 'false',
|
|
457
|
+
InclExceptDef: 'true',
|
|
458
|
+
InclObjectValues: 'true',
|
|
459
|
+
HryLvlAbsRs: 'false',
|
|
460
|
+
FromRow: String(fromRow),
|
|
461
|
+
ToRow: String(toRow),
|
|
462
|
+
'X-sap-adt-sessiontype': 'stateless',
|
|
463
|
+
});
|
|
464
|
+
return postResult.body;
|
|
465
|
+
};
|
|
466
|
+
try {
|
|
467
|
+
responseXml = await doPost();
|
|
468
|
+
}
|
|
469
|
+
catch (err) {
|
|
470
|
+
// SAP returns HTTP 403 when the CSRF token has expired (session idle timeout).
|
|
471
|
+
// Retry once with a fresh token.
|
|
472
|
+
if (/csrf|403/i.test(String(err))) {
|
|
473
|
+
client.clearCsrfToken();
|
|
474
|
+
responseXml = await doPost();
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
throw err;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
const { body } = await client.rawGet(url, {
|
|
483
|
+
Accept: BICS_ACCEPT,
|
|
484
|
+
InclMetadata: 'true',
|
|
485
|
+
InclExceptDef: 'true',
|
|
486
|
+
InclObjectValues: 'true',
|
|
487
|
+
CompactMode: 'false',
|
|
488
|
+
HryLvlAbsRs: 'false',
|
|
489
|
+
FromRow: String(fromRow),
|
|
490
|
+
ToRow: String(toRow),
|
|
491
|
+
'X-sap-adt-sessiontype': 'stateless',
|
|
492
|
+
});
|
|
493
|
+
responseXml = body;
|
|
494
|
+
}
|
|
495
|
+
if (format === 'raw')
|
|
496
|
+
return responseXml;
|
|
497
|
+
return renderQueryDataText(responseXml, !isPost);
|
|
498
|
+
}
|
|
499
|
+
export async function bwGetFilterValues(client, characteristicName, searchString, infoProvider, maxRows = 201) {
|
|
500
|
+
let url = `/sap/bw/modeling/is/values/characteristicvalues` +
|
|
501
|
+
`?characteristicname=${encodeURIComponent(characteristicName)}` +
|
|
502
|
+
`&maxrows=${maxRows}` +
|
|
503
|
+
`&readtexts=x` +
|
|
504
|
+
`&searchstring=${encodeURIComponent(searchString)}`;
|
|
505
|
+
if (infoProvider) {
|
|
506
|
+
url += `&infoprovider=${encodeURIComponent(infoProvider)}&readmode=d`;
|
|
507
|
+
}
|
|
508
|
+
const { body } = await client.rawGet(url, { Accept: VALUE_HELP_ACCEPT, 'X-sap-adt-sessiontype': 'stateless' });
|
|
509
|
+
// Parse metadata
|
|
510
|
+
const metaAttrs = body.match(/<valueHelpMetaInformation\b([^>]*?)(?:\/>|>)/)?.[1] ?? '';
|
|
511
|
+
const totalCount = attr(metaAttrs, 'valueHelpLines') ?? '';
|
|
512
|
+
const refChar = attr(metaAttrs, 'referenceCharacteristic') ?? characteristicName;
|
|
513
|
+
// Parse column names
|
|
514
|
+
const colNames = [];
|
|
515
|
+
const colRe = /<columnname>([^<]*)<\/columnname>/g;
|
|
516
|
+
let cm;
|
|
517
|
+
while ((cm = colRe.exec(body)) !== null) {
|
|
518
|
+
colNames.push(cm[1].trim());
|
|
519
|
+
}
|
|
520
|
+
// Parse rows
|
|
521
|
+
const rows = [];
|
|
522
|
+
const rowRe = /<row>([\s\S]*?)<\/row>/g;
|
|
523
|
+
let rm;
|
|
524
|
+
while ((rm = rowRe.exec(body)) !== null) {
|
|
525
|
+
const vals = [];
|
|
526
|
+
const valRe = /<value>([^<]*)<\/value>/g;
|
|
527
|
+
let vm;
|
|
528
|
+
while ((vm = valRe.exec(rm[1])) !== null) {
|
|
529
|
+
vals.push(vm[1]);
|
|
530
|
+
}
|
|
531
|
+
rows.push(vals);
|
|
532
|
+
}
|
|
533
|
+
const lines = [];
|
|
534
|
+
lines.push(`Characteristic: ${refChar}`);
|
|
535
|
+
lines.push(`Search: "${searchString}"${infoProvider ? ` | Provider: ${infoProvider}` : ''}`);
|
|
536
|
+
lines.push(`Results: ${rows.length}${totalCount ? ` of ${totalCount}` : ''}`);
|
|
537
|
+
lines.push('');
|
|
538
|
+
lines.push('NOTE: Use CHAVL_EXT for state filters (presentationMode="EXT"). Use CHAVL_INT for variable inputs. When CHAVL_EXT and CHAVL_INT are identical, either works.');
|
|
539
|
+
lines.push('');
|
|
540
|
+
if (rows.length === 0) {
|
|
541
|
+
lines.push('(no values returned)');
|
|
542
|
+
return lines.join('\n');
|
|
543
|
+
}
|
|
544
|
+
// Column widths
|
|
545
|
+
const widths = colNames.map(n => n.length);
|
|
546
|
+
for (const row of rows) {
|
|
547
|
+
for (let i = 0; i < row.length; i++) {
|
|
548
|
+
widths[i] = Math.max(widths[i] ?? 0, (row[i] ?? '').length);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
const pad = (s, w) => s.padEnd(w);
|
|
552
|
+
lines.push(colNames.map((n, i) => pad(n, widths[i])).join(' '));
|
|
553
|
+
lines.push(widths.map(w => '-'.repeat(w)).join(' '));
|
|
554
|
+
for (const row of rows) {
|
|
555
|
+
lines.push(row.map((v, i) => pad(v, widths[i] ?? 0)).join(' '));
|
|
556
|
+
}
|
|
557
|
+
return lines.join('\n');
|
|
558
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const BASE = '/sap/bw/modeling/repo/infoproviderstructure';
|
|
2
|
+
const CHILDREN_PREFIX = `${BASE}/`;
|
|
3
|
+
function parseAtomFeed(xml) {
|
|
4
|
+
const entries = [];
|
|
5
|
+
const entryRegex = /<atom:entry\b[^>]*>([\s\S]*?)<\/atom:entry>/g;
|
|
6
|
+
let em;
|
|
7
|
+
while ((em = entryRegex.exec(xml)) !== null) {
|
|
8
|
+
const body = em[1];
|
|
9
|
+
// bwModel:object attributes (may be self-closing or open)
|
|
10
|
+
const bwObjMatch = body.match(/<bwModel:object\b([\s\S]*?)(?:\/>|>)/);
|
|
11
|
+
const bwAttrs = bwObjMatch?.[1] ?? '';
|
|
12
|
+
const objectName = bwAttrs.match(/\bobjectName="([^"]*)"/)?.[1] ?? '';
|
|
13
|
+
const objectType = bwAttrs.match(/\bobjectType="([^"]*)"/)?.[1] ?? '';
|
|
14
|
+
const objectStatusRaw = bwAttrs.match(/\bobjectStatus="([^"]*)"/)?.[1];
|
|
15
|
+
const objectSubtypeRaw = bwAttrs.match(/\bobjectSubtype="([^"]*)"/)?.[1];
|
|
16
|
+
const objectStatus = objectStatusRaw !== undefined ? objectStatusRaw : null;
|
|
17
|
+
const objectSubtype = objectSubtypeRaw !== undefined ? objectSubtypeRaw : null;
|
|
18
|
+
// atom:title
|
|
19
|
+
const title = body.match(/<atom:title[^>]*>([^<]*)<\/atom:title>/)?.[1] ?? '';
|
|
20
|
+
// atom:link elements
|
|
21
|
+
let selfHref = null;
|
|
22
|
+
let selfLinkType = null;
|
|
23
|
+
let childrenHref = null;
|
|
24
|
+
const linkRegex = /<atom:link\b([\s\S]*?)(?:\/>|>)/g;
|
|
25
|
+
let lm;
|
|
26
|
+
while ((lm = linkRegex.exec(body)) !== null) {
|
|
27
|
+
const attrs = lm[1];
|
|
28
|
+
const rel = attrs.match(/\brel="([^"]*)"/)?.[1] ?? '';
|
|
29
|
+
const href = attrs.match(/\bhref="([^"]*)"/)?.[1] ?? '';
|
|
30
|
+
const type = attrs.match(/\btype="([^"]*)"/)?.[1] ?? '';
|
|
31
|
+
if (rel === 'self') {
|
|
32
|
+
selfHref = href || null;
|
|
33
|
+
selfLinkType = type || null;
|
|
34
|
+
}
|
|
35
|
+
else if (rel === 'http://www.sap.com/bw/modeling/relations:children') {
|
|
36
|
+
childrenHref = href || null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Fiori URL detection (RSPC and similar objects with no REST endpoint)
|
|
40
|
+
const fioriOnly = selfLinkType === 'application/vnd.sap-bw-modeling.url' ||
|
|
41
|
+
(selfHref !== null && selfHref.includes('#BWProcessChain'));
|
|
42
|
+
// Chain ID (RSPC objects only)
|
|
43
|
+
let chainId;
|
|
44
|
+
if (fioriOnly && selfHref) {
|
|
45
|
+
const match = selfHref.match(/[?&]chainId=([^&#]+)/);
|
|
46
|
+
if (match)
|
|
47
|
+
chainId = decodeURIComponent(match[1]);
|
|
48
|
+
}
|
|
49
|
+
// children_path: strip the base prefix to give a path usable with this tool
|
|
50
|
+
let childrenPath = null;
|
|
51
|
+
if (childrenHref) {
|
|
52
|
+
childrenPath = childrenHref.startsWith(CHILDREN_PREFIX)
|
|
53
|
+
? childrenHref.slice(CHILDREN_PREFIX.length)
|
|
54
|
+
: childrenHref;
|
|
55
|
+
}
|
|
56
|
+
const entry = {
|
|
57
|
+
name: objectName,
|
|
58
|
+
description: title,
|
|
59
|
+
object_type: objectType,
|
|
60
|
+
object_subtype: objectSubtype,
|
|
61
|
+
status: objectStatus,
|
|
62
|
+
has_children: childrenHref !== null,
|
|
63
|
+
self_url: selfHref,
|
|
64
|
+
fiori_only: fioriOnly,
|
|
65
|
+
children_path: childrenPath,
|
|
66
|
+
};
|
|
67
|
+
if (chainId !== undefined)
|
|
68
|
+
entry.chain_id = chainId;
|
|
69
|
+
entries.push(entry);
|
|
70
|
+
}
|
|
71
|
+
return entries;
|
|
72
|
+
}
|
|
73
|
+
export async function bwListContents(client, path) {
|
|
74
|
+
// Normalise: strip leading/trailing slashes, lowercase
|
|
75
|
+
const normalizedPath = path.toLowerCase().replace(/^\/+/, '').replace(/\/+$/, '');
|
|
76
|
+
const url = normalizedPath ? `${BASE}/${normalizedPath}` : BASE;
|
|
77
|
+
const { body } = await client.get(url, 'application/atom+xml');
|
|
78
|
+
const entries = parseAtomFeed(body);
|
|
79
|
+
return JSON.stringify({
|
|
80
|
+
path: path || '/',
|
|
81
|
+
count: entries.length,
|
|
82
|
+
entries,
|
|
83
|
+
}, null, 2);
|
|
84
|
+
}
|