@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.
@@ -0,0 +1,631 @@
1
+ import { XMLParser } from 'fast-xml-parser';
2
+ import { createClientFromEnv, MEDIA_TYPES } from '../bw-client.js';
3
+ // Fallback version range used when the discovery document does not advertise a
4
+ // query media type. The exact query version depends on the BW backend SP level,
5
+ // so the version actually accepted by the system is read from discovery at
6
+ // startup (MEDIA_TYPES['query']) and preferred over this static list.
7
+ const QUERY_ACCEPT = 'application/vnd.sap.bw.modeling.query-v1_8_0+xml, ' +
8
+ 'application/vnd.sap.bw.modeling.query-v1_9_0+xml, ' +
9
+ 'application/vnd.sap.bw.modeling.query-v1_10_0+xml, ' +
10
+ 'application/vnd.sap.bw.modeling.query-v1_11_0+xml';
11
+ /**
12
+ * Accept header for query GETs: the discovery-advertised media type first (so
13
+ * systems on a higher or lower SP level negotiate correctly), with the static
14
+ * version range kept as a fallback.
15
+ */
16
+ function queryAccept() {
17
+ const discovered = MEDIA_TYPES['query'];
18
+ return discovered ? `${discovered}, ${QUERY_ACCEPT}` : QUERY_ACCEPT;
19
+ }
20
+ function ensureArray(val) {
21
+ if (val === undefined || val === null)
22
+ return [];
23
+ return Array.isArray(val) ? val : [val];
24
+ }
25
+ function renderFormula(token, variableMap, ckfMap, rkfMap, localMemberMap, depth = 0) {
26
+ if (depth > 50)
27
+ return '...';
28
+ if (!token)
29
+ return '?';
30
+ const type = token['@_xsi:type'];
31
+ switch (type) {
32
+ case 'Qry:FormulaInfixOperator': {
33
+ const children = ensureArray(token['Qry:childToken']);
34
+ if (children.length >= 2) {
35
+ const left = renderFormula(children[0], variableMap, ckfMap, rkfMap, localMemberMap, depth + 1);
36
+ const right = renderFormula(children[1], variableMap, ckfMap, rkfMap, localMemberMap, depth + 1);
37
+ return `(${left} ${token['@_code']} ${right})`;
38
+ }
39
+ return `(${token['@_code']})`;
40
+ }
41
+ case 'Qry:FormulaPrefixOperator': {
42
+ const children = ensureArray(token['Qry:childToken']);
43
+ const code = token['@_code'];
44
+ if (code === 'IF' && children.length === 3) {
45
+ return (`IF(${renderFormula(children[0], variableMap, ckfMap, rkfMap, localMemberMap, depth + 1)}, ` +
46
+ `${renderFormula(children[1], variableMap, ckfMap, rkfMap, localMemberMap, depth + 1)}, ` +
47
+ `${renderFormula(children[2], variableMap, ckfMap, rkfMap, localMemberMap, depth + 1)})`);
48
+ }
49
+ return `${code}(${children.map((c) => renderFormula(c, variableMap, ckfMap, rkfMap, localMemberMap, depth + 1)).join(', ')})`;
50
+ }
51
+ case 'Qry:FormulaIObjectOperand':
52
+ return token['@_infoObject'] ?? '?';
53
+ case 'Qry:FormulaMemberOperand': {
54
+ const memberId = token['@_member'];
55
+ const opType = token['@_operandType'];
56
+ if (opType === 'Variable') {
57
+ return variableMap.get(memberId)?.technicalName ?? memberId;
58
+ }
59
+ if (opType === 'Member') {
60
+ return localMemberMap.get(memberId) ?? ckfMap.get(memberId)?.technicalName ?? rkfMap.get(memberId)?.technicalName ?? memberId;
61
+ }
62
+ return ckfMap.get(memberId)?.technicalName ?? rkfMap.get(memberId)?.technicalName ?? memberId;
63
+ }
64
+ case 'Qry:FormulaConstant':
65
+ return String(token['@_value'] ?? '');
66
+ default:
67
+ return '?';
68
+ }
69
+ }
70
+ function countMembersRecursive(node) {
71
+ const children = ensureArray(node['Qry:childMembers']);
72
+ let count = children.length;
73
+ for (const c of children) {
74
+ count += countMembersRecursive(c);
75
+ }
76
+ return count;
77
+ }
78
+ function buildLocalMemberMap(members) {
79
+ const map = new Map();
80
+ function collect(memberList) {
81
+ for (const m of memberList) {
82
+ const id = m['@_id'];
83
+ const desc = m['Qry:description']?.['@_value'] ?? id ?? '';
84
+ if (id)
85
+ map.set(id, desc);
86
+ const children = ensureArray(m['Qry:childMembers']);
87
+ if (children.length > 0)
88
+ collect(children);
89
+ }
90
+ }
91
+ collect(members);
92
+ return map;
93
+ }
94
+ function parseSelectionGroups(groups, ckfMap, rkfMap) {
95
+ return groups.map((g) => {
96
+ const tokens = ensureArray(g['Qry:tokens']);
97
+ const parsedTokens = tokens.map((t) => {
98
+ const tType = t['@_xsi:type'];
99
+ if (tType === 'Qry:SelectionTokenForComponent') {
100
+ const compId = t['@_component'];
101
+ const ckfEntry = ckfMap.get(compId);
102
+ const rkfEntry = rkfMap.get(compId);
103
+ const entry = ckfEntry ?? rkfEntry;
104
+ return {
105
+ tokenType: 'SelectionTokenForComponent',
106
+ componentId: compId,
107
+ componentTechnicalName: entry?.technicalName ?? compId,
108
+ componentType: ckfEntry ? 'CKF' : 'RKF',
109
+ };
110
+ }
111
+ const fromValue = t['Qry:fromValue'];
112
+ const tok = {
113
+ tokenType: 'SelectionRange',
114
+ selectionType: t['@_selectionType'] ?? '',
115
+ operator: t['@_operator'] ?? '',
116
+ exclude: t['@_exclude'] === 'true' || t['@_exclude'] === true,
117
+ value: fromValue?.['Qry:value'] ?? '',
118
+ };
119
+ const internalValue = fromValue?.['@_internalValue'];
120
+ if (internalValue)
121
+ tok['internalValue'] = internalValue;
122
+ const fromValueDesc = t['@_fromValueDesc'];
123
+ if (fromValueDesc)
124
+ tok['valueDesc'] = fromValueDesc;
125
+ return tok;
126
+ });
127
+ return {
128
+ infoObject: g['@_infoObject'] ?? '',
129
+ description: g['@_description'] ?? '',
130
+ constantSelection: g['@_constantSelection'] === 'true' || g['@_constantSelection'] === true,
131
+ tokens: parsedTokens,
132
+ };
133
+ });
134
+ }
135
+ function parseMemberRecursive(member, variableMap, ckfMap, rkfMap, localMemberMap) {
136
+ const mType = member['@_xsi:type'];
137
+ const id = member['@_id'] ?? '';
138
+ const descNode = member['Qry:description'];
139
+ const desc = descNode?.['@_value'] ?? '';
140
+ const shortDesc = descNode?.['@_shortValue'];
141
+ const visibility = member['Qry:hidden']?.['@_type'] ?? 'showAlways';
142
+ const result = {
143
+ id,
144
+ type: mType === 'Qry:MemberFormula' ? 'MemberFormula' : 'MemberSelection',
145
+ description: desc,
146
+ visibility,
147
+ };
148
+ if (shortDesc)
149
+ result['shortDescription'] = shortDesc;
150
+ if (mType === 'Qry:MemberFormula') {
151
+ const formulaDef = member['Qry:formulaDefinition'];
152
+ const formulaToken = formulaDef?.['Qry:formulaToken'];
153
+ result['formula'] = formulaToken ? renderFormula(formulaToken, variableMap, ckfMap, rkfMap, localMemberMap) : '';
154
+ }
155
+ else {
156
+ result['selections'] = parseSelectionGroups(ensureArray(member['Qry:groups']), ckfMap, rkfMap);
157
+ const defaultHint = member['Qry:defaultHint'];
158
+ if (defaultHint?.['Qry:type'] === 'CINLink') {
159
+ const hintValue = defaultHint?.['Qry:value'];
160
+ if (hintValue) {
161
+ const ckfEntry = ckfMap.get(hintValue);
162
+ const rkfEntry = rkfMap.get(hintValue);
163
+ const entry = ckfEntry ?? rkfEntry;
164
+ if (entry) {
165
+ result['referencedComponent'] = {
166
+ technicalName: entry.technicalName,
167
+ description: entry.description,
168
+ componentType: ckfEntry ? 'CKF' : 'RKF',
169
+ };
170
+ }
171
+ }
172
+ }
173
+ }
174
+ const childMembersRaw = ensureArray(member['Qry:childMembers']);
175
+ if (childMembersRaw.length > 0) {
176
+ result['childMembers'] = childMembersRaw.map((cm) => parseMemberRecursive(cm, variableMap, ckfMap, rkfMap, localMemberMap));
177
+ }
178
+ return result;
179
+ }
180
+ function parseDimElement(elem, variableMap, ckfMap, rkfMap) {
181
+ const type = elem['@_xsi:type'];
182
+ if (type === 'Qry:CustomDimension') {
183
+ const membersRaw = ensureArray(elem['Qry:members']);
184
+ const localMemberMap = buildLocalMemberMap(membersRaw);
185
+ let memberCount = membersRaw.length;
186
+ for (const m of membersRaw) {
187
+ memberCount += countMembersRecursive(m);
188
+ }
189
+ const members = membersRaw.map((m) => parseMemberRecursive(m, variableMap, ckfMap, rkfMap, localMemberMap));
190
+ return {
191
+ type: 'CustomDimension',
192
+ technicalName: elem['@_technicalName'] ?? '',
193
+ description: elem['Qry:description']?.['@_value'] ?? '',
194
+ reusable: elem['@_reusable'] === 'true' || elem['@_reusable'] === true,
195
+ suppressZeros: elem['@_suppressZeros'] === 'true' || elem['@_suppressZeros'] === true,
196
+ memberCount,
197
+ members,
198
+ };
199
+ }
200
+ const additionalInfo = elem['Qry:additionalInfo'];
201
+ const kvPairs = ensureArray(additionalInfo?.['Qry:keyValuePairs']);
202
+ const infoObjectTypeKv = kvPairs.find((kv) => kv['@_key'] === 'infoObjectType');
203
+ const result = {
204
+ type: 'Dimension',
205
+ infoObjectName: elem['@_infoObjectName'] ?? '',
206
+ description: elem['Qry:description']?.['@_value'] ?? '',
207
+ };
208
+ if (infoObjectTypeKv)
209
+ result.infoObjectType = infoObjectTypeKv['@_value'];
210
+ return result;
211
+ }
212
+ function renderQueryText(q) {
213
+ const lines = [];
214
+ const s = (v) => (v != null && v !== '' ? String(v) : '—');
215
+ const bool = (v) => (v === true || v === 'true') ? 'yes' : 'no';
216
+ lines.push(`Query: ${s(q['name'])} — ${s(q['description'])}`);
217
+ lines.push(`InfoProvider: ${s(q['infoProvider'])} (${s(q['providerType'])})`);
218
+ lines.push(`InfoArea: ${s(q['infoArea'])} Package: ${s(q['package'])}`);
219
+ lines.push(`Status: ${s(q['status'])} Changed: ${s(q['changedAt'])} By: ${s(q['responsible'])}`);
220
+ if (q['versionNote'])
221
+ lines.push(`Note: ${q['versionNote']}`);
222
+ const settings = q['settings'] ?? {};
223
+ const zs = settings['zeroSuppression'] ?? {};
224
+ const rp = settings['resultPosition'] ?? {};
225
+ lines.push('');
226
+ lines.push('── Settings ──');
227
+ lines.push(` Zero suppression: rows=${bool(zs['rows'])} columns=${bool(zs['columns'])} mode=${s(zs['mode'])}`);
228
+ lines.push(` Result position: top=${bool(rp['onTop'])} left=${bool(rp['onLeft'])}`);
229
+ lines.push(` RFC=${bool(settings['rfcEnabled'])} OData=${bool(settings['odataSupport'])} EasyQuery=${bool(settings['easyQuery'])}`);
230
+ lines.push(` Sign presentation: ${s(settings['signPresentation'])}`);
231
+ const variables = q['variables'] ?? [];
232
+ if (variables.length > 0) {
233
+ lines.push('');
234
+ lines.push('── Variables ──');
235
+ for (const v of variables) {
236
+ lines.push(` ${s(v['technicalName'])} ${s(v['description'])}`);
237
+ lines.push(` InfoObject: ${s(v['infoObject'])} Type: ${s(v['type'])} ProcType: ${s(v['procType'])}`);
238
+ lines.push(` InputType: ${s(v['inputType'])} Represents: ${s(v['represents'])}`);
239
+ }
240
+ }
241
+ const filter = q['filter'] ?? [];
242
+ if (filter.length > 0) {
243
+ lines.push('');
244
+ lines.push('── Filter ──');
245
+ for (const f of filter) {
246
+ const selections = f['selections'] ?? [];
247
+ lines.push(` ${s(f['infoObject'])}: ${JSON.stringify(selections)}`);
248
+ }
249
+ }
250
+ const rows = q['rows'] ?? [];
251
+ const columns = q['columns'] ?? [];
252
+ const free = q['freeCharacteristics'] ?? [];
253
+ lines.push('');
254
+ lines.push('── Layout ──');
255
+ lines.push(` ROWS (${rows.length}):`);
256
+ for (const r of rows) {
257
+ lines.push(` ${s(r['technicalName'] ?? r['infoObjectName'])} ${s(r['description'])} [${s(r['type'])}]`);
258
+ }
259
+ lines.push(` COLUMNS (${columns.length}):`);
260
+ for (const c of columns) {
261
+ lines.push(` ${s(c['technicalName'] ?? c['infoObjectName'])} ${s(c['description'])} [${s(c['type'])}]`);
262
+ }
263
+ lines.push(` FREE (${free.length}):`);
264
+ for (const f of free) {
265
+ lines.push(` ${s(f['technicalName'] ?? f['infoObjectName'])} ${s(f['description'])} [${s(f['type'])}]`);
266
+ }
267
+ const ckfs = q['calculatedMeasures'] ?? [];
268
+ if (ckfs.length > 0) {
269
+ lines.push('');
270
+ lines.push(`── Calculated Key Figures (${ckfs.length}) ──`);
271
+ for (const c of ckfs) {
272
+ lines.push(` ${s(c['technicalName'])} ${s(c['description'])}`);
273
+ if (c['formula'])
274
+ lines.push(` Formula: ${s(c['formula'])}`);
275
+ }
276
+ }
277
+ const rkfs = q['restrictedMeasures'] ?? [];
278
+ if (rkfs.length > 0) {
279
+ lines.push('');
280
+ lines.push(`── Restricted Key Figures (${rkfs.length}) ──`);
281
+ for (const r of rkfs) {
282
+ lines.push(` ${s(r['technicalName'])} ${s(r['description'])}`);
283
+ const member = r['member'];
284
+ if (member?.['keyFigure'])
285
+ lines.push(` KeyFigure: ${s(member['keyFigure'])}`);
286
+ }
287
+ }
288
+ const exceptions = q['exceptions'] ?? [];
289
+ if (exceptions.length > 0) {
290
+ lines.push('');
291
+ lines.push(`── Exceptions (${exceptions.length}) ──`);
292
+ for (const e of exceptions) {
293
+ lines.push(` ${s(e['description'])} evaluated=${bool(e['evaluated'])}`);
294
+ const thresholds = e['thresholds'] ?? [];
295
+ for (const t of thresholds) {
296
+ lines.push(` Level ${s(t['alertLevel'])}: ${s(t['operator'])} ${s(t['value'])}`);
297
+ }
298
+ }
299
+ }
300
+ const hasCells = q['hasCellDefinitions'];
301
+ if (hasCells) {
302
+ const gridCells = q['gridCells'] ?? [];
303
+ const helpCells = q['helpCells'] ?? [];
304
+ lines.push('');
305
+ lines.push(`── Cell Definitions ──`);
306
+ lines.push(` Grid cells: ${gridCells.length} Help cells: ${helpCells.length}`);
307
+ for (const gc of gridCells) {
308
+ lines.push(` [${s(gc['type'])}] ${s(gc['description'])} coord1=${s(gc['coordinateMember1'])} coord2=${s(gc['coordinateMember2'])}`);
309
+ if (gc['formula'])
310
+ lines.push(` Formula: ${s(gc['formula'])}`);
311
+ }
312
+ }
313
+ return lines.join('\n');
314
+ }
315
+ export async function bwGetQuery(queryName, format = 'text') {
316
+ const client = createClientFromEnv();
317
+ const basePath = `/sap/bw/modeling/query/${queryName.toLowerCase()}`;
318
+ let xmlBody;
319
+ let versionNote;
320
+ const accept = queryAccept();
321
+ try {
322
+ const result = await client.get(`${basePath}/a`, accept);
323
+ xmlBody = result.body;
324
+ }
325
+ catch (err) {
326
+ const msg = err instanceof Error ? err.message : String(err);
327
+ if (msg.includes('HTTP 404')) {
328
+ const result = await client.get(`${basePath}/m`, accept);
329
+ xmlBody = result.body;
330
+ versionNote = 'inactive version returned';
331
+ }
332
+ else {
333
+ throw err;
334
+ }
335
+ }
336
+ const parser = new XMLParser({
337
+ ignoreAttributes: false,
338
+ attributeNamePrefix: '@_',
339
+ isArray: (tagName) => [
340
+ 'Qry:subComponents',
341
+ 'Qry:selections',
342
+ 'Qry:tokens',
343
+ 'Qry:members',
344
+ 'Qry:childMembers',
345
+ 'Qry:childFormulas',
346
+ 'Qry:free',
347
+ 'Qry:rows',
348
+ 'Qry:columns',
349
+ 'Qry:exceptions',
350
+ 'Qry:conditions',
351
+ 'Qry:gridCells',
352
+ 'Qry:helpCells',
353
+ 'Qry:groups',
354
+ 'Qry:childToken',
355
+ 'Qry:referenceCharacteristic',
356
+ 'atom:link',
357
+ ].includes(tagName),
358
+ });
359
+ const parsed = parser.parse(xmlBody);
360
+ const root = parsed['Qry:queryResource'];
361
+ // Step 1: Build subComponent maps
362
+ const variableMap = new Map();
363
+ const ckfMap = new Map();
364
+ const rkfMap = new Map();
365
+ const subComponents = ensureArray(root['Qry:subComponents']);
366
+ for (const sc of subComponents) {
367
+ const scType = sc['@_xsi:type'];
368
+ const id = sc['@_id'];
369
+ if (scType === 'Qry:Variable') {
370
+ variableMap.set(id, {
371
+ technicalName: sc['@_technicalName'] ?? '',
372
+ description: sc['Qry:description']?.['@_value'] ?? '',
373
+ infoObject: sc['@_infoObject'] ?? '',
374
+ type: sc['Qry:type'] ?? '',
375
+ procType: sc['Qry:procType'] ?? '',
376
+ inputType: sc['Qry:inputType'] ?? '',
377
+ represents: sc['Qry:represents'] ?? '',
378
+ defaultSelection: sc['Qry:defaultSelection'],
379
+ });
380
+ }
381
+ else if (scType === 'Qry:CalculatedMeasure') {
382
+ const member = sc['Qry:member'];
383
+ ckfMap.set(id, {
384
+ technicalName: sc['@_technicalName'] ?? '',
385
+ description: sc['Qry:description']?.['@_value'] ?? '',
386
+ formulaDefinition: member?.['Qry:formulaDefinition'],
387
+ });
388
+ }
389
+ else if (scType === 'Qry:RestrictedMeasure') {
390
+ rkfMap.set(id, {
391
+ technicalName: sc['@_technicalName'] ?? '',
392
+ description: sc['Qry:description']?.['@_value'] ?? '',
393
+ member: sc['Qry:member'],
394
+ });
395
+ }
396
+ }
397
+ // Step 3: Parse mainComponent metadata
398
+ const mainComp = root['Qry:mainComponent'];
399
+ const entityProps = mainComp['Qry:entityProperties'];
400
+ const links = ensureArray(entityProps['atom:link']);
401
+ const relatedLink = links.find((l) => l['@_rel'] === 'related');
402
+ const href = relatedLink?.['@_href'] ?? '';
403
+ let providerType;
404
+ if (href.includes('/hcpr/'))
405
+ providerType = 'CompositeProvider';
406
+ else if (href.includes('/alvl/'))
407
+ providerType = 'AggregationLevel';
408
+ else if (href.includes('/adso/'))
409
+ providerType = 'aDSO';
410
+ else
411
+ providerType = 'Unknown';
412
+ const packageRef = entityProps['adtCore:packageRef'];
413
+ // Step 4: Variables in order of subComponents appearance
414
+ const variables = [];
415
+ for (const sc of subComponents) {
416
+ if (sc['@_xsi:type'] !== 'Qry:Variable')
417
+ continue;
418
+ const v = {
419
+ technicalName: sc['@_technicalName'] ?? '',
420
+ description: sc['Qry:description']?.['@_value'] ?? '',
421
+ infoObject: sc['@_infoObject'] ?? '',
422
+ type: sc['Qry:type'] ?? '',
423
+ procType: sc['Qry:procType'] ?? '',
424
+ inputType: sc['Qry:inputType'] ?? '',
425
+ represents: sc['Qry:represents'] ?? '',
426
+ };
427
+ const defaultSel = sc['Qry:defaultSelection'];
428
+ if (defaultSel && defaultSel['@_fromValue'] !== undefined) {
429
+ v['defaultValue'] = String(defaultSel['@_fromValue']);
430
+ }
431
+ variables.push(v);
432
+ }
433
+ // Step 5: Parse filter
434
+ const filterSection = mainComp['Qry:filter'];
435
+ const selections = ensureArray(filterSection?.['Qry:selections']);
436
+ const filter = [];
437
+ for (const sel of selections) {
438
+ const usageType = sel['@_usageType'] ?? '';
439
+ const tokens = ensureArray(sel['Qry:tokens']);
440
+ if (usageType === 'asStartValue' && tokens.length === 0)
441
+ continue;
442
+ const infoObject = sel['@_infoObject'] ?? '';
443
+ const localDim = sel['Qry:localDimension'];
444
+ const description = localDim
445
+ ? localDim['Qry:description']?.['@_value'] ?? infoObject
446
+ : infoObject;
447
+ const item = { infoObject, description, usageType };
448
+ const fixedValues = tokens
449
+ .filter((t) => t['@_xsi:type'] === 'Qry:SelectionRange')
450
+ .map((t) => {
451
+ const fromValue = t['Qry:fromValue'];
452
+ const fv = {
453
+ operator: t['@_operator'] ?? '',
454
+ exclude: t['@_exclude'] === 'true' || t['@_exclude'] === true,
455
+ value: fromValue?.['Qry:value'] ?? '',
456
+ };
457
+ const fromValueDesc = t['@_fromValueDesc'];
458
+ if (fromValueDesc)
459
+ fv['valueDesc'] = fromValueDesc;
460
+ return fv;
461
+ });
462
+ if (fixedValues.length > 0)
463
+ item['fixedValues'] = fixedValues;
464
+ const varToken = tokens.find((t) => t['@_xsi:type'] === 'Qry:SelectionVariable');
465
+ if (varToken) {
466
+ const varId = varToken['@_variable'];
467
+ const varInfo = variableMap.get(varId);
468
+ item['variable'] = {
469
+ technicalName: varInfo?.technicalName ?? varId,
470
+ description: varInfo?.description ?? '',
471
+ };
472
+ }
473
+ filter.push(item);
474
+ }
475
+ // Step 6: Parse layout
476
+ const columnsRaw = ensureArray(mainComp['Qry:columns']);
477
+ const rowsRaw = ensureArray(mainComp['Qry:rows']);
478
+ const freeRaw = ensureArray(mainComp['Qry:free']);
479
+ const columns = columnsRaw.map((elem) => parseDimElement(elem, variableMap, ckfMap, rkfMap));
480
+ const rows = rowsRaw.map((elem) => parseDimElement(elem, variableMap, ckfMap, rkfMap));
481
+ const freeCharacteristics = freeRaw.map((elem) => {
482
+ const additionalInfo = elem['Qry:additionalInfo'];
483
+ const kvPairs = ensureArray(additionalInfo?.['Qry:keyValuePairs']);
484
+ const infoObjectTypeKv = kvPairs.find((kv) => kv['@_key'] === 'infoObjectType');
485
+ const result = {
486
+ infoObjectName: elem['@_infoObjectName'] ?? '',
487
+ description: elem['Qry:description']?.['@_value'] ?? '',
488
+ };
489
+ if (infoObjectTypeKv)
490
+ result['infoObjectType'] = infoObjectTypeKv['@_value'];
491
+ return result;
492
+ });
493
+ // Step 7: Calculated Measures (CKFs only)
494
+ const calculatedMeasures = [];
495
+ for (const [, ckf] of ckfMap) {
496
+ const formulaDef = ckf.formulaDefinition;
497
+ const formulaToken = formulaDef?.['Qry:formulaToken'];
498
+ calculatedMeasures.push({
499
+ technicalName: ckf.technicalName,
500
+ description: ckf.description,
501
+ formula: formulaToken ? renderFormula(formulaToken, variableMap, ckfMap, rkfMap, new Map()) : '',
502
+ });
503
+ }
504
+ // Step 8: Restricted Measures
505
+ const restrictedMeasures = [];
506
+ for (const [, rkf] of rkfMap) {
507
+ restrictedMeasures.push({
508
+ technicalName: rkf.technicalName,
509
+ description: rkf.description,
510
+ selections: parseSelectionGroups(ensureArray(rkf.member?.['Qry:groups']), ckfMap, rkfMap),
511
+ });
512
+ }
513
+ // Step 9: Exceptions
514
+ const exceptionsRaw = ensureArray(mainComp['Qry:exceptions']);
515
+ const exceptions = exceptionsRaw.map((ex) => {
516
+ const exTokens = ensureArray(ex['Qry:tokens']);
517
+ const thresholds = exTokens.map((t) => {
518
+ const fromValue = t['Qry:fromValue'];
519
+ const toValueNode = t['Qry:toValue'];
520
+ const threshold = {
521
+ alertLevel: t['@_alertLevel'] ?? '',
522
+ operator: t['@_operator'] ?? '',
523
+ value: fromValue?.['Qry:value'] ?? '',
524
+ };
525
+ const toVal = toValueNode?.['Qry:value'];
526
+ if (toVal !== undefined)
527
+ threshold['toValue'] = toVal;
528
+ return threshold;
529
+ });
530
+ const exception = {
531
+ id: ex['@_id'] ?? '',
532
+ active: ex['@_active'] === 'true' || ex['@_active'] === true,
533
+ evaluateBeforeListCalc: ex['@_evaluateBeforeListCalc'] === 'true' || ex['@_evaluateBeforeListCalc'] === true,
534
+ affectsChasNotListed: ex['@_affectsChasNotListed'] ?? '',
535
+ affectsDataCells: ex['@_affectsDataCells'] === 'true' || ex['@_affectsDataCells'] === true,
536
+ description: ex['Qry:description']?.['@_value'] ?? '',
537
+ thresholds,
538
+ };
539
+ const firstStruc = ex['Qry:definedCellFirstStruc'];
540
+ const firstMember = firstStruc?.['Qry:member'];
541
+ if (firstMember)
542
+ exception['firstStructureMember'] = firstMember;
543
+ const secondStruc = ex['Qry:definedCellSecondStruc'];
544
+ const secondMember = secondStruc?.['Qry:member'];
545
+ if (secondMember)
546
+ exception['secondStructureMember'] = secondMember;
547
+ return exception;
548
+ });
549
+ // Step 10: Cell definitions
550
+ const gridCellsRaw = ensureArray(mainComp['Qry:gridCells']);
551
+ const helpCellsRaw = ensureArray(mainComp['Qry:helpCells']);
552
+ const hasCellDefinitions = gridCellsRaw.length > 0 || helpCellsRaw.length > 0;
553
+ const gridCells = gridCellsRaw.map((gc) => {
554
+ const gcType = gc['@_xsi:type'];
555
+ const cell = {
556
+ id: gc['@_id'] ?? '',
557
+ type: gcType === 'Qry:FormulaCell' ? 'FormulaCell' : 'ReferenceCell',
558
+ description: gc['Qry:description']?.['@_value'] ?? '',
559
+ coordinateMember1: gc['Qry:coordinateMember1'] ?? '',
560
+ coordinateMember2: gc['Qry:coordinateMember2'] ?? '',
561
+ };
562
+ if (gcType === 'Qry:FormulaCell') {
563
+ const formulaDef = gc['Qry:formulaDefinition'];
564
+ const formulaToken = formulaDef?.['Qry:formulaToken'];
565
+ cell['formula'] = formulaToken ? renderFormula(formulaToken, variableMap, ckfMap, rkfMap, new Map()) : '';
566
+ }
567
+ return cell;
568
+ });
569
+ const helpCells = helpCellsRaw.map((hc) => ({
570
+ id: hc['@_id'] ?? '',
571
+ description: hc['Qry:description']?.['@_value'] ?? '',
572
+ selections: parseSelectionGroups(ensureArray(hc['Qry:groups']), ckfMap, rkfMap),
573
+ }));
574
+ // Step 11: Query-level settings
575
+ const zeroSuppr = mainComp['Qry:zeroSuppression'];
576
+ const planningNode = mainComp['Qry:planning'];
577
+ const resultPosNode = mainComp['Qry:resultPosition'];
578
+ const zeroSuppression = {
579
+ rows: zeroSuppr?.['@_rows'] === 'true' || zeroSuppr?.['@_rows'] === true,
580
+ columns: zeroSuppr?.['@_columns'] === 'true' || zeroSuppr?.['@_columns'] === true,
581
+ };
582
+ if (zeroSuppr?.['@_mode'])
583
+ zeroSuppression['mode'] = zeroSuppr['@_mode'];
584
+ const settings = {
585
+ rfcEnabled: mainComp['@_rfcEnabled'] === 'true' || mainComp['@_rfcEnabled'] === true,
586
+ easyQuery: mainComp['@_easyQuery'] === 'true' || mainComp['@_easyQuery'] === true,
587
+ odataSupport: mainComp['@_odataSupport'] === 'true' || mainComp['@_odataSupport'] === true,
588
+ suppressRepeatedKeyValues: mainComp['@_suppressRepeatedKeyValues'] === 'true' || mainComp['@_suppressRepeatedKeyValues'] === true,
589
+ showScalingFactor: mainComp['@_showScalingFactor'] === 'true' || mainComp['@_showScalingFactor'] === true,
590
+ signPresentation: mainComp['@_signPresentation'] ?? '',
591
+ zeroSuppression,
592
+ planning: {
593
+ inputMode: planningNode?.['@_inputMode'] === 'true' || planningNode?.['@_inputMode'] === true,
594
+ symmetrical: planningNode?.['@_symmetrical'] === 'true' || planningNode?.['@_symmetrical'] === true,
595
+ },
596
+ resultPosition: {
597
+ onTop: resultPosNode?.['@_onTop'] === 'true' || resultPosNode?.['@_onTop'] === true,
598
+ onLeft: resultPosNode?.['@_onLeft'] === 'true' || resultPosNode?.['@_onLeft'] === true,
599
+ },
600
+ };
601
+ const output = {
602
+ name: mainComp['@_technicalName'] ?? queryName.toUpperCase(),
603
+ description: mainComp['Qry:description']?.['@_value'] ?? '',
604
+ infoProvider: mainComp['@_providerName'] ?? '',
605
+ providerType,
606
+ package: packageRef?.['@_adtCore:name'] ?? '',
607
+ infoArea: entityProps['infoArea'] ?? '',
608
+ status: entityProps['objectStatus'] ?? '',
609
+ responsible: entityProps['@_adtCore:responsible'] ?? '',
610
+ changedAt: entityProps['@_adtCore:changedAt'] ?? '',
611
+ createdAt: entityProps['@_adtCore:createdAt'] ?? '',
612
+ timestamp: mainComp['@_timestamp'] ?? '',
613
+ settings,
614
+ variables,
615
+ filter,
616
+ columns,
617
+ rows,
618
+ freeCharacteristics,
619
+ calculatedMeasures,
620
+ restrictedMeasures,
621
+ exceptions,
622
+ hasCellDefinitions,
623
+ gridCells,
624
+ helpCells,
625
+ };
626
+ if (versionNote)
627
+ output['versionNote'] = versionNote;
628
+ if (format === 'raw')
629
+ return JSON.stringify(output, null, 2);
630
+ return renderQueryText(output);
631
+ }