@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,169 @@
|
|
|
1
|
+
const HCPR_ACCEPT = [
|
|
2
|
+
'application/vnd.sap.bw.modeling.hcpr-v1_0_0+xml',
|
|
3
|
+
'application/vnd.sap.bw.modeling.hcpr-v1_4_0+xml',
|
|
4
|
+
'application/vnd.sap.bw.modeling.hcpr-v1_7_0+xml',
|
|
5
|
+
'application/vnd.sap.bw.modeling.hcpr-v1_8_0+xml',
|
|
6
|
+
'application/vnd.sap.bw.modeling.hcpr-v1_9_0+xml',
|
|
7
|
+
'application/vnd.sap.bw.modeling.hcpr-v1_10_0+xml',
|
|
8
|
+
'application/vnd.sap.bw.modeling.hcpr-v1_11_0+xml',
|
|
9
|
+
'application/vnd.sap.bw.modeling.hcpr-v1_12_0+xml',
|
|
10
|
+
'application/vnd.sap.bw.modeling.hcpr-v1_13_0+xml',
|
|
11
|
+
'application/vnd.sap.bw.modeling.hcpr-v1_14_0+xml',
|
|
12
|
+
'application/vnd.sap.bw.modeling.hcpr-v1_15_0+xml',
|
|
13
|
+
'application/vnd.sap.bw.modeling.hcpr-v9_99_9+xml',
|
|
14
|
+
].join(',');
|
|
15
|
+
function attr(str, key) {
|
|
16
|
+
return str.match(new RegExp(`\\b${key}="([^"]*)"`))?.[1] ?? '';
|
|
17
|
+
}
|
|
18
|
+
export async function bwGetCompositeProvider(client, compositeProviderName) {
|
|
19
|
+
const path = `/sap/bw/modeling/hcpr/${compositeProviderName.toLowerCase()}/m`;
|
|
20
|
+
const result = await client.get(path, HCPR_ACCEPT);
|
|
21
|
+
const xml = result.body;
|
|
22
|
+
const objectStatus = result.headers['object_status'] ?? result.headers['OBJECT_STATUS'] ?? 'unknown';
|
|
23
|
+
const timestamp = result.headers['timestamp'] ?? result.headers['TIMESTAMP'] ?? '';
|
|
24
|
+
// Root element attributes
|
|
25
|
+
const rootAttrs = xml.match(/<Composite:compositeView\b([\s\S]*?)>/)?.[1] ?? '';
|
|
26
|
+
const cpName = attr(rootAttrs, 'name');
|
|
27
|
+
const temporalJoinFlag = attr(rootAttrs, 'temporalJoin');
|
|
28
|
+
const stackableFlag = attr(rootAttrs, 'stackable');
|
|
29
|
+
const defaultNode = attr(rootAttrs, 'defaultNode');
|
|
30
|
+
const aggregationBehaviour = attr(rootAttrs, 'aggregationBehaviour');
|
|
31
|
+
// Description
|
|
32
|
+
const description = xml.match(/<endUserTexts\b[^>]*\blabel="([^"]*)"/)?.[1] ?? '';
|
|
33
|
+
// tlogoProperties block (opening tag only — attributes span multiple lines)
|
|
34
|
+
const tlogoAttrs = xml.match(/<tlogoProperties\b([\s\S]*?)>/)?.[1] ?? '';
|
|
35
|
+
const responsible = attr(tlogoAttrs, 'adtcore:responsible');
|
|
36
|
+
const changedAt = attr(tlogoAttrs, 'adtcore:changedAt');
|
|
37
|
+
const changedBy = attr(tlogoAttrs, 'adtcore:changedBy');
|
|
38
|
+
const infoArea = xml.match(/<infoArea>([^<]+)<\/infoArea>/)?.[1] ?? '';
|
|
39
|
+
const packageName = xml.match(/adtcore:packageRef[^>]*adtcore:name="([^"]+)"/)?.[1] ?? '';
|
|
40
|
+
// viewNode
|
|
41
|
+
const viewNodeMatch = xml.match(/<viewNode\b([\s\S]*?)>([\s\S]*?)<\/viewNode>/);
|
|
42
|
+
const viewNodeAttrs = viewNodeMatch?.[1] ?? '';
|
|
43
|
+
const viewNodeBody = viewNodeMatch?.[2] ?? '';
|
|
44
|
+
const viewNodeName = attr(viewNodeAttrs, 'name');
|
|
45
|
+
// Strip namespace prefix and normalise type name
|
|
46
|
+
const rawViewType = attr(viewNodeAttrs, 'xsi:type');
|
|
47
|
+
const localViewType = rawViewType.split(':').pop() ?? rawViewType;
|
|
48
|
+
const viewType = localViewType === 'JoinNode' ? 'Join' : localViewType === 'Union' ? 'Union' : localViewType;
|
|
49
|
+
// Fields
|
|
50
|
+
const fields = [];
|
|
51
|
+
const elemRegex = /<element\b([\s\S]*?)(?:\/>|>([\s\S]*?)<\/element>)/g;
|
|
52
|
+
let em;
|
|
53
|
+
while ((em = elemRegex.exec(viewNodeBody)) !== null) {
|
|
54
|
+
const elemAttrs = em[1];
|
|
55
|
+
const name = attr(elemAttrs, 'name');
|
|
56
|
+
if (!name)
|
|
57
|
+
continue;
|
|
58
|
+
const infoObjectName = attr(elemAttrs, 'infoObjectName');
|
|
59
|
+
const dimension = attr(elemAttrs, 'dimension');
|
|
60
|
+
const dimName = dimension.match(/#\/\/\/([^§]*)§/)?.[1] ?? dimension;
|
|
61
|
+
const isKeyFigure = dimName.includes('__KEYFIGURES');
|
|
62
|
+
fields.push({
|
|
63
|
+
name,
|
|
64
|
+
...(infoObjectName ? { info_object_name: infoObjectName } : {}),
|
|
65
|
+
dimension: dimName,
|
|
66
|
+
is_key_figure: isKeyFigure,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const totalFields = fields.length;
|
|
70
|
+
const keyFigureCount = fields.filter(f => f['is_key_figure']).length;
|
|
71
|
+
const characteristicCount = totalFields - keyFigureCount;
|
|
72
|
+
// Inputs (source providers)
|
|
73
|
+
const inputs = [];
|
|
74
|
+
const inputRegex = /<input\b([\s\S]*?)>([\s\S]*?)<\/input>/g;
|
|
75
|
+
let im;
|
|
76
|
+
while ((im = inputRegex.exec(viewNodeBody)) !== null) {
|
|
77
|
+
const inputAttrs = im[1];
|
|
78
|
+
const inputBody = im[2];
|
|
79
|
+
const name = attr(inputAttrs, 'name');
|
|
80
|
+
if (!name)
|
|
81
|
+
continue;
|
|
82
|
+
const alias = attr(inputAttrs, 'alias');
|
|
83
|
+
const lastModified = attr(inputAttrs, 'lastModified');
|
|
84
|
+
const providerType = alias.split('.')[1] ?? '';
|
|
85
|
+
const allMappings = [...inputBody.matchAll(/<mapping\b[^>]*/g)];
|
|
86
|
+
const constantMappings = allMappings
|
|
87
|
+
.filter(m => m[0].includes('ConstantElementMapping'))
|
|
88
|
+
.map(m => ({
|
|
89
|
+
target: attr(m[0], 'targetName'),
|
|
90
|
+
value: attr(m[0], 'value'),
|
|
91
|
+
}));
|
|
92
|
+
inputs.push({
|
|
93
|
+
name,
|
|
94
|
+
alias,
|
|
95
|
+
...(lastModified ? { last_modified: lastModified } : {}),
|
|
96
|
+
provider_type: providerType,
|
|
97
|
+
mapping_count: allMappings.length,
|
|
98
|
+
regular_mapping_count: allMappings.length - constantMappings.length,
|
|
99
|
+
constant_mappings: constantMappings,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
// Build result
|
|
103
|
+
const output = {
|
|
104
|
+
object_type: 'hcpr',
|
|
105
|
+
name: cpName.toUpperCase(),
|
|
106
|
+
description,
|
|
107
|
+
object_status: objectStatus,
|
|
108
|
+
timestamp,
|
|
109
|
+
temporal_join: temporalJoinFlag === 'true',
|
|
110
|
+
stackable: stackableFlag === 'true',
|
|
111
|
+
aggregation_behaviour: aggregationBehaviour,
|
|
112
|
+
default_node: defaultNode,
|
|
113
|
+
info_area: infoArea,
|
|
114
|
+
package: packageName,
|
|
115
|
+
responsible_user: responsible,
|
|
116
|
+
last_changed_at: changedAt,
|
|
117
|
+
last_changed_by: changedBy,
|
|
118
|
+
view_node: { name: viewNodeName, type: viewType },
|
|
119
|
+
inputs,
|
|
120
|
+
fields: {
|
|
121
|
+
total: totalFields,
|
|
122
|
+
characteristic_count: characteristicCount,
|
|
123
|
+
key_figure_count: keyFigureCount,
|
|
124
|
+
list: fields,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
// Join condition (Join CPs only)
|
|
128
|
+
if (viewType === 'Join') {
|
|
129
|
+
const joinMatch = viewNodeBody.match(/<join\b([\s\S]*?)>([\s\S]*?)<\/join>/);
|
|
130
|
+
if (joinMatch) {
|
|
131
|
+
const joinAttrs = joinMatch[1];
|
|
132
|
+
const joinBody = joinMatch[2];
|
|
133
|
+
// "#///J1/J1.IOBJ.2" → last non-empty path segment = alias
|
|
134
|
+
const extractAlias = (ref) => ref.split('/').filter(Boolean).pop() ?? '';
|
|
135
|
+
const leftKeys = [...joinBody.matchAll(/<leftElementName>([^<]+)<\/leftElementName>/g)].map(m => m[1]);
|
|
136
|
+
const rightKeys = [...joinBody.matchAll(/<rightElementName>([^<]+)<\/rightElementName>/g)].map(m => m[1]);
|
|
137
|
+
output['join_condition'] = {
|
|
138
|
+
join_type: attr(joinAttrs, 'joinType'),
|
|
139
|
+
cardinality: attr(joinAttrs, 'cardinality'),
|
|
140
|
+
left_input_alias: extractAlias(attr(joinAttrs, 'leftInput')),
|
|
141
|
+
right_input_alias: extractAlias(attr(joinAttrs, 'rightInput')),
|
|
142
|
+
left_key_fields: leftKeys,
|
|
143
|
+
right_key_fields: rightKeys,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Temporal join details
|
|
148
|
+
if (temporalJoinFlag === 'true') {
|
|
149
|
+
const extractAlias = (ref) => ref.split('/').filter(Boolean).pop() ?? '';
|
|
150
|
+
const aqRef = xml.match(/<temporalJoinProvider\b[^>]*type="AQ"[^>]*input="([^"]*)"/)?.[1] ?? '';
|
|
151
|
+
const cqRef = xml.match(/<temporalJoinProvider\b[^>]*type="CQ"[^>]*input="([^"]*)"/)?.[1] ?? '';
|
|
152
|
+
const operands = [...xml.matchAll(/<temporalOperand\b([\s\S]*?)(?:\/>|>)/g)].map(m => {
|
|
153
|
+
const opAttrs = m[1];
|
|
154
|
+
const temporalArg = attr(opAttrs, 'temporalArgument');
|
|
155
|
+
const field = temporalArg.split('/').filter(Boolean).pop() ?? temporalArg;
|
|
156
|
+
return {
|
|
157
|
+
type: attr(opAttrs, 'type'),
|
|
158
|
+
field,
|
|
159
|
+
input_alias: extractAlias(attr(opAttrs, 'input')),
|
|
160
|
+
};
|
|
161
|
+
});
|
|
162
|
+
output['temporal_join_details'] = {
|
|
163
|
+
anchor_query_alias: extractAlias(aqRef),
|
|
164
|
+
characteristic_query_alias: extractAlias(cqRef),
|
|
165
|
+
operands,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
return JSON.stringify(output, null, 2);
|
|
169
|
+
}
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { XMLParser } from 'fast-xml-parser';
|
|
2
|
+
const CKF_ACCEPT = [
|
|
3
|
+
'application/vnd.sap.bw.modeling.ckf-v1_8_0+xml',
|
|
4
|
+
'application/vnd.sap.bw.modeling.ckf-v1_9_0+xml',
|
|
5
|
+
'application/vnd.sap.bw.modeling.ckf-v1_10_0+xml',
|
|
6
|
+
].join(',');
|
|
7
|
+
const RKF_ACCEPT = [
|
|
8
|
+
'application/vnd.sap.bw.modeling.rkf-v1_8_0+xml',
|
|
9
|
+
'application/vnd.sap.bw.modeling.rkf-v1_9_0+xml',
|
|
10
|
+
'application/vnd.sap.bw.modeling.rkf-v1_10_0+xml',
|
|
11
|
+
].join(',');
|
|
12
|
+
const STRUCTURE_ACCEPT = [
|
|
13
|
+
'application/vnd.sap.bw.modeling.structure-v1_8_0+xml',
|
|
14
|
+
'application/vnd.sap.bw.modeling.structure-v1_9_0+xml',
|
|
15
|
+
].join(',');
|
|
16
|
+
// ── XML Parser ───────────────────────────────────────────────────────────────
|
|
17
|
+
const ALWAYS_ARRAY = new Set([
|
|
18
|
+
'Qry:subComponents',
|
|
19
|
+
'Qry:groups',
|
|
20
|
+
'Qry:tokens',
|
|
21
|
+
'Qry:members',
|
|
22
|
+
'Qry:childMembers',
|
|
23
|
+
'Qry:childToken',
|
|
24
|
+
'atom:link',
|
|
25
|
+
]);
|
|
26
|
+
function makeParser() {
|
|
27
|
+
return new XMLParser({
|
|
28
|
+
ignoreAttributes: false,
|
|
29
|
+
attributeNamePrefix: '@_',
|
|
30
|
+
isArray: (tagName) => ALWAYS_ARRAY.has(tagName),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
// ── Utilities ────────────────────────────────────────────────────────────────
|
|
34
|
+
function ensureArray(val) {
|
|
35
|
+
if (val === undefined || val === null)
|
|
36
|
+
return [];
|
|
37
|
+
return Array.isArray(val) ? val : [val];
|
|
38
|
+
}
|
|
39
|
+
function authoringLabel(code) {
|
|
40
|
+
if (code === 'T')
|
|
41
|
+
return 'Eclipse';
|
|
42
|
+
if (code === '3')
|
|
43
|
+
return 'Query Designer';
|
|
44
|
+
return code;
|
|
45
|
+
}
|
|
46
|
+
function buildSubComponentMaps(subComponents) {
|
|
47
|
+
const ckfMap = new Map();
|
|
48
|
+
const rkfMap = new Map();
|
|
49
|
+
for (const sc of subComponents) {
|
|
50
|
+
const id = sc['@_id'];
|
|
51
|
+
if (!id)
|
|
52
|
+
continue;
|
|
53
|
+
const scType = sc['@_xsi:type'];
|
|
54
|
+
const technicalName = sc['@_technicalName'] ?? '';
|
|
55
|
+
const description = sc['Qry:description']?.['@_value'] ?? '';
|
|
56
|
+
if (scType === 'Qry:CalculatedMeasure')
|
|
57
|
+
ckfMap.set(id, { technicalName, description });
|
|
58
|
+
else if (scType === 'Qry:RestrictedMeasure')
|
|
59
|
+
rkfMap.set(id, { technicalName, description });
|
|
60
|
+
}
|
|
61
|
+
return { ckfMap, rkfMap };
|
|
62
|
+
}
|
|
63
|
+
// ── Formula rendering (same approach as query.ts renderFormula) ──────────────
|
|
64
|
+
// No variableMap needed — CP components don't reference variables.
|
|
65
|
+
function renderFormula(token, ckfMap, rkfMap, localMemberMap, depth = 0) {
|
|
66
|
+
if (depth > 50)
|
|
67
|
+
return '...';
|
|
68
|
+
if (!token)
|
|
69
|
+
return '?';
|
|
70
|
+
const type = token['@_xsi:type'];
|
|
71
|
+
switch (type) {
|
|
72
|
+
case 'Qry:FormulaInfixOperator': {
|
|
73
|
+
const children = ensureArray(token['Qry:childToken']);
|
|
74
|
+
if (children.length >= 2) {
|
|
75
|
+
const left = renderFormula(children[0], ckfMap, rkfMap, localMemberMap, depth + 1);
|
|
76
|
+
const right = renderFormula(children[1], ckfMap, rkfMap, localMemberMap, depth + 1);
|
|
77
|
+
return `(${left} ${token['@_code']} ${right})`;
|
|
78
|
+
}
|
|
79
|
+
return `(${token['@_code']})`;
|
|
80
|
+
}
|
|
81
|
+
case 'Qry:FormulaPrefixOperator': {
|
|
82
|
+
const children = ensureArray(token['Qry:childToken']);
|
|
83
|
+
const code = token['@_code'];
|
|
84
|
+
if (code === 'IF' && children.length === 3) {
|
|
85
|
+
return (`IF(${renderFormula(children[0], ckfMap, rkfMap, localMemberMap, depth + 1)}, ` +
|
|
86
|
+
`${renderFormula(children[1], ckfMap, rkfMap, localMemberMap, depth + 1)}, ` +
|
|
87
|
+
`${renderFormula(children[2], ckfMap, rkfMap, localMemberMap, depth + 1)})`);
|
|
88
|
+
}
|
|
89
|
+
return `${code}(${children
|
|
90
|
+
.map((c) => renderFormula(c, ckfMap, rkfMap, localMemberMap, depth + 1))
|
|
91
|
+
.join(', ')})`;
|
|
92
|
+
}
|
|
93
|
+
case 'Qry:FormulaIObjectOperand':
|
|
94
|
+
return token['@_infoObject'] ?? '?';
|
|
95
|
+
case 'Qry:FormulaMemberOperand': {
|
|
96
|
+
const memberId = token['@_member'];
|
|
97
|
+
const opType = token['@_operandType'];
|
|
98
|
+
if (opType === 'Member') {
|
|
99
|
+
return (localMemberMap.get(memberId) ??
|
|
100
|
+
ckfMap.get(memberId)?.technicalName ??
|
|
101
|
+
rkfMap.get(memberId)?.technicalName ??
|
|
102
|
+
memberId);
|
|
103
|
+
}
|
|
104
|
+
return ckfMap.get(memberId)?.technicalName ?? rkfMap.get(memberId)?.technicalName ?? memberId;
|
|
105
|
+
}
|
|
106
|
+
case 'Qry:FormulaConstant':
|
|
107
|
+
return String(token['@_value'] ?? '');
|
|
108
|
+
default:
|
|
109
|
+
return '?';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// ── Metadata extraction (common to CKF, RKF, Structure) ─────────────────────
|
|
113
|
+
function extractMetadata(mainComp) {
|
|
114
|
+
const entityProps = (mainComp['Qry:entityProperties'] ?? {});
|
|
115
|
+
const packageRef = entityProps['adtCore:packageRef'];
|
|
116
|
+
const rawInfoArea = entityProps['infoArea'];
|
|
117
|
+
const infoArea = typeof rawInfoArea === 'string' ? rawInfoArea : '';
|
|
118
|
+
return {
|
|
119
|
+
timestamp: mainComp['@_timestamp'] ?? '',
|
|
120
|
+
authored_by: authoringLabel(mainComp['@_authoringTool'] ?? ''),
|
|
121
|
+
created_by: entityProps['@_adtCore:createdBy'] ?? '',
|
|
122
|
+
created_at: entityProps['@_adtCore:createdAt'] ?? '',
|
|
123
|
+
changed_by: entityProps['@_adtCore:changedBy'] ?? '',
|
|
124
|
+
changed_at: entityProps['@_adtCore:changedAt'] ?? '',
|
|
125
|
+
package: packageRef?.['@_adtCore:name'] ?? '',
|
|
126
|
+
info_area: infoArea,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function componentDescription(comp) {
|
|
130
|
+
const descNode = comp['Qry:description'];
|
|
131
|
+
if (descNode?.['@_value'])
|
|
132
|
+
return descNode['@_value'];
|
|
133
|
+
const entityProps = comp['Qry:entityProperties'];
|
|
134
|
+
return entityProps?.['@_adtCore:description'] ?? '';
|
|
135
|
+
}
|
|
136
|
+
function buildDependencies(subComponents) {
|
|
137
|
+
return subComponents.map((sc) => ({
|
|
138
|
+
technical_name: sc['@_technicalName'] ?? '',
|
|
139
|
+
description: sc['Qry:description']?.['@_value'] ?? '',
|
|
140
|
+
type: sc['@_xsi:type'] === 'Qry:CalculatedMeasure' ? 'CKF' : 'RKF',
|
|
141
|
+
}));
|
|
142
|
+
}
|
|
143
|
+
// ── bw_get_ckf ───────────────────────────────────────────────────────────────
|
|
144
|
+
export async function bwGetCkf(client, componentName) {
|
|
145
|
+
const path = `/sap/bw/modeling/ckf/${componentName.toLowerCase()}/a`;
|
|
146
|
+
const { body } = await client.get(path, CKF_ACCEPT);
|
|
147
|
+
const parser = makeParser();
|
|
148
|
+
const parsed = parser.parse(body);
|
|
149
|
+
const root = parsed['Qry:queryResource'];
|
|
150
|
+
const subComponents = ensureArray(root['Qry:subComponents']);
|
|
151
|
+
const { ckfMap, rkfMap } = buildSubComponentMaps(subComponents);
|
|
152
|
+
const mainComp = root['Qry:mainComponent'];
|
|
153
|
+
const technicalName = mainComp['@_technicalName'] ?? componentName.toUpperCase();
|
|
154
|
+
const providerName = mainComp['@_providerName'] ?? '';
|
|
155
|
+
const description = componentDescription(mainComp);
|
|
156
|
+
// Formula: mainComponent → Qry:member → Qry:formulaDefinition → Qry:formulaToken
|
|
157
|
+
const member = mainComp['Qry:member'];
|
|
158
|
+
const formulaDef = member?.['Qry:formulaDefinition'];
|
|
159
|
+
const formulaToken = formulaDef?.['Qry:formulaToken'];
|
|
160
|
+
const formula = formulaToken ? renderFormula(formulaToken, ckfMap, rkfMap, new Map()) : '';
|
|
161
|
+
const dependencies = buildDependencies(subComponents);
|
|
162
|
+
return JSON.stringify({
|
|
163
|
+
object_type: 'ckf',
|
|
164
|
+
technical_name: technicalName,
|
|
165
|
+
description,
|
|
166
|
+
provider_name: providerName,
|
|
167
|
+
component_type: 'CKF',
|
|
168
|
+
...extractMetadata(mainComp),
|
|
169
|
+
formula,
|
|
170
|
+
dependency_count: dependencies.length,
|
|
171
|
+
dependencies,
|
|
172
|
+
}, null, 2);
|
|
173
|
+
}
|
|
174
|
+
// ── bw_get_rkf ───────────────────────────────────────────────────────────────
|
|
175
|
+
export async function bwGetRkf(client, componentName) {
|
|
176
|
+
const path = `/sap/bw/modeling/rkf/${componentName.toLowerCase()}/a`;
|
|
177
|
+
const { body } = await client.get(path, RKF_ACCEPT);
|
|
178
|
+
const parser = makeParser();
|
|
179
|
+
const parsed = parser.parse(body);
|
|
180
|
+
const root = parsed['Qry:queryResource'];
|
|
181
|
+
const subComponents = ensureArray(root['Qry:subComponents']);
|
|
182
|
+
const { ckfMap, rkfMap } = buildSubComponentMaps(subComponents);
|
|
183
|
+
const mainComp = root['Qry:mainComponent'];
|
|
184
|
+
const technicalName = mainComp['@_technicalName'] ?? componentName.toUpperCase();
|
|
185
|
+
const providerName = mainComp['@_providerName'] ?? '';
|
|
186
|
+
const description = componentDescription(mainComp);
|
|
187
|
+
const member = mainComp['Qry:member'];
|
|
188
|
+
const groups = ensureArray(member?.['Qry:groups']);
|
|
189
|
+
let baseMeasure = '';
|
|
190
|
+
const filters = [];
|
|
191
|
+
for (const g of groups) {
|
|
192
|
+
const infoObject = g['@_infoObject'] ?? '';
|
|
193
|
+
const tokens = ensureArray(g['Qry:tokens']);
|
|
194
|
+
if (infoObject === '1KYFNM') {
|
|
195
|
+
const token = tokens[0];
|
|
196
|
+
if (!token)
|
|
197
|
+
continue;
|
|
198
|
+
const tType = token['@_xsi:type'];
|
|
199
|
+
if (tType === 'Qry:SelectionTokenForComponent') {
|
|
200
|
+
const compId = token['@_component'];
|
|
201
|
+
baseMeasure =
|
|
202
|
+
ckfMap.get(compId)?.technicalName ??
|
|
203
|
+
rkfMap.get(compId)?.technicalName ??
|
|
204
|
+
compId;
|
|
205
|
+
}
|
|
206
|
+
else if (tType === 'Qry:SelectionRange') {
|
|
207
|
+
// Direct base IOBJ key figure
|
|
208
|
+
const fromValue = token['Qry:fromValue'];
|
|
209
|
+
baseMeasure = fromValue?.['Qry:value'] ?? '';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
// Characteristic filter — one entry per SelectionRange token
|
|
214
|
+
for (const token of tokens) {
|
|
215
|
+
const fromValue = token['Qry:fromValue'];
|
|
216
|
+
const internalValue = fromValue?.['@_internalValue'] ??
|
|
217
|
+
fromValue?.['Qry:value'] ??
|
|
218
|
+
'';
|
|
219
|
+
filters.push({
|
|
220
|
+
infoObject,
|
|
221
|
+
operator: token['@_operator'] ?? '',
|
|
222
|
+
exclude: token['@_exclude'] === 'true' || token['@_exclude'] === true,
|
|
223
|
+
values: internalValue ? [internalValue] : [],
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
const dependencies = buildDependencies(subComponents);
|
|
229
|
+
return JSON.stringify({
|
|
230
|
+
object_type: 'rkf',
|
|
231
|
+
technical_name: technicalName,
|
|
232
|
+
description,
|
|
233
|
+
provider_name: providerName,
|
|
234
|
+
component_type: 'RKF',
|
|
235
|
+
...extractMetadata(mainComp),
|
|
236
|
+
base_measure: baseMeasure,
|
|
237
|
+
filters,
|
|
238
|
+
dependency_count: dependencies.length,
|
|
239
|
+
dependencies,
|
|
240
|
+
}, null, 2);
|
|
241
|
+
}
|
|
242
|
+
// ── bw_get_structure ─────────────────────────────────────────────────────────
|
|
243
|
+
function buildLocalMemberMap(members) {
|
|
244
|
+
const map = new Map();
|
|
245
|
+
function collect(list) {
|
|
246
|
+
for (const m of list) {
|
|
247
|
+
const id = m['@_id'];
|
|
248
|
+
const desc = m['Qry:description']?.['@_value'] ??
|
|
249
|
+
id ??
|
|
250
|
+
'';
|
|
251
|
+
if (id)
|
|
252
|
+
map.set(id, desc);
|
|
253
|
+
const children = ensureArray(m['Qry:childMembers']);
|
|
254
|
+
if (children.length > 0)
|
|
255
|
+
collect(children);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
collect(members);
|
|
259
|
+
return map;
|
|
260
|
+
}
|
|
261
|
+
function parseMember(member, position, ckfMap, rkfMap, localMemberMap) {
|
|
262
|
+
const mType = member['@_xsi:type'];
|
|
263
|
+
const id = member['@_id'] ?? '';
|
|
264
|
+
const descNode = member['Qry:description'];
|
|
265
|
+
const desc = descNode?.['@_value'] ?? '';
|
|
266
|
+
const memberType = mType === 'Qry:MemberFormula' ? 'Formula' : 'Selection';
|
|
267
|
+
const result = { id, description: desc, member_type: memberType, position };
|
|
268
|
+
if (mType === 'Qry:MemberFormula') {
|
|
269
|
+
const formulaDef = member['Qry:formulaDefinition'];
|
|
270
|
+
const formulaToken = formulaDef?.['Qry:formulaToken'];
|
|
271
|
+
result['formula'] = formulaToken
|
|
272
|
+
? renderFormula(formulaToken, ckfMap, rkfMap, localMemberMap)
|
|
273
|
+
: '';
|
|
274
|
+
}
|
|
275
|
+
else {
|
|
276
|
+
const groups = ensureArray(member['Qry:groups']);
|
|
277
|
+
let referencedComponent;
|
|
278
|
+
const characteristicFilters = [];
|
|
279
|
+
for (const g of groups) {
|
|
280
|
+
const infoObject = g['@_infoObject'] ?? '';
|
|
281
|
+
const tokens = ensureArray(g['Qry:tokens']);
|
|
282
|
+
if (infoObject === '1KYFNM') {
|
|
283
|
+
const token = tokens[0];
|
|
284
|
+
if (!token)
|
|
285
|
+
continue;
|
|
286
|
+
const tType = token['@_xsi:type'];
|
|
287
|
+
if (tType === 'Qry:SelectionTokenForComponent') {
|
|
288
|
+
const compId = token['@_component'];
|
|
289
|
+
referencedComponent =
|
|
290
|
+
ckfMap.get(compId)?.technicalName ??
|
|
291
|
+
rkfMap.get(compId)?.technicalName ??
|
|
292
|
+
compId;
|
|
293
|
+
}
|
|
294
|
+
else if (tType === 'Qry:SelectionRange') {
|
|
295
|
+
const fromValue = token['Qry:fromValue'];
|
|
296
|
+
referencedComponent = fromValue?.['Qry:value'] ?? '';
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
const values = tokens
|
|
301
|
+
.map((t) => {
|
|
302
|
+
const fv = t['Qry:fromValue'];
|
|
303
|
+
return fv?.['@_internalValue'] ?? fv?.['Qry:value'] ?? '';
|
|
304
|
+
})
|
|
305
|
+
.filter(Boolean);
|
|
306
|
+
if (values.length > 0)
|
|
307
|
+
characteristicFilters.push({ infoObject, values });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (referencedComponent !== undefined)
|
|
311
|
+
result['referenced_component'] = referencedComponent;
|
|
312
|
+
if (characteristicFilters.length > 0)
|
|
313
|
+
result['characteristic_filters'] = characteristicFilters;
|
|
314
|
+
}
|
|
315
|
+
const childMembersRaw = ensureArray(member['Qry:childMembers']);
|
|
316
|
+
if (childMembersRaw.length > 0) {
|
|
317
|
+
result['child_members'] = childMembersRaw.map((cm, idx) => parseMember(cm, idx + 1, ckfMap, rkfMap, localMemberMap));
|
|
318
|
+
}
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
export async function bwGetStructure(client, componentName) {
|
|
322
|
+
const path = `/sap/bw/modeling/structure/${componentName.toLowerCase()}/a`;
|
|
323
|
+
const { body } = await client.get(path, STRUCTURE_ACCEPT);
|
|
324
|
+
const parser = makeParser();
|
|
325
|
+
const parsed = parser.parse(body);
|
|
326
|
+
const root = parsed['Qry:queryResource'];
|
|
327
|
+
const subComponents = ensureArray(root['Qry:subComponents']);
|
|
328
|
+
const { ckfMap, rkfMap } = buildSubComponentMaps(subComponents);
|
|
329
|
+
const mainComp = root['Qry:mainComponent'];
|
|
330
|
+
const technicalName = mainComp['@_technicalName'] ?? componentName.toUpperCase();
|
|
331
|
+
const providerName = mainComp['@_providerName'] ?? '';
|
|
332
|
+
const description = componentDescription(mainComp);
|
|
333
|
+
const membersRaw = ensureArray(mainComp['Qry:members']);
|
|
334
|
+
const localMemberMap = buildLocalMemberMap(membersRaw);
|
|
335
|
+
const members = membersRaw.map((m, idx) => parseMember(m, idx + 1, ckfMap, rkfMap, localMemberMap));
|
|
336
|
+
const dependencies = buildDependencies(subComponents);
|
|
337
|
+
return JSON.stringify({
|
|
338
|
+
object_type: 'structure',
|
|
339
|
+
technical_name: technicalName,
|
|
340
|
+
description,
|
|
341
|
+
provider_name: providerName,
|
|
342
|
+
...extractMetadata(mainComp),
|
|
343
|
+
members,
|
|
344
|
+
dependency_count: dependencies.length,
|
|
345
|
+
dependencies,
|
|
346
|
+
}, null, 2);
|
|
347
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const DMOD_ACCEPT = 'application/vnd.sap.bw.modeling.dmod-v1_0_0+xml';
|
|
2
|
+
const BASE = '/sap/bw/modeling/dmod/8TRANSIENT';
|
|
3
|
+
function parseNodes(xml) {
|
|
4
|
+
const nodes = [];
|
|
5
|
+
const nodeRe = /<node\b([\s\S]*?)>([\s\S]*?)<\/node>/g;
|
|
6
|
+
let m;
|
|
7
|
+
while ((m = nodeRe.exec(xml)) !== null) {
|
|
8
|
+
const attrs = m[1];
|
|
9
|
+
const body = m[2];
|
|
10
|
+
const id = parseInt(attrs.match(/\bnodeID="([^"]*)"/)?.[1] ?? '0', 10);
|
|
11
|
+
const objectName = attrs.match(/\bobjectName="([^"]*)"/)?.[1] ?? '';
|
|
12
|
+
const objectType = attrs.match(/\bobjectType="([^"]*)"/)?.[1] ?? '';
|
|
13
|
+
const objectSubType = attrs.match(/\bobjectSubType="([^"]*)"/)?.[1] ?? '';
|
|
14
|
+
const objectDescription = attrs.match(/\bobjectDescription="([^"]*)"/)?.[1] ?? '';
|
|
15
|
+
const objectStatus = attrs.match(/\bobjectStatus="([^"]*)"/)?.[1] ?? '';
|
|
16
|
+
const persistent = attrs.match(/\bpersistent="([^"]*)"/)?.[1] === 'true';
|
|
17
|
+
const exists = attrs.match(/\bexists="([^"]*)"/)?.[1] === 'true';
|
|
18
|
+
const sourceNodeIds = [];
|
|
19
|
+
const sourceRe = /<sourceNode>#\/\/\/(\d+)<\/sourceNode>/g;
|
|
20
|
+
let sm;
|
|
21
|
+
while ((sm = sourceRe.exec(body)) !== null) {
|
|
22
|
+
sourceNodeIds.push(parseInt(sm[1], 10));
|
|
23
|
+
}
|
|
24
|
+
const targetNodeIds = [];
|
|
25
|
+
const targetRe = /<targetNode>#\/\/\/(\d+)<\/targetNode>/g;
|
|
26
|
+
let tm;
|
|
27
|
+
while ((tm = targetRe.exec(body)) !== null) {
|
|
28
|
+
targetNodeIds.push(parseInt(tm[1], 10));
|
|
29
|
+
}
|
|
30
|
+
nodes.push({ id, objectName, objectType, objectSubType, objectDescription, objectStatus, persistent, exists, sourceNodeIds, targetNodeIds });
|
|
31
|
+
}
|
|
32
|
+
return nodes;
|
|
33
|
+
}
|
|
34
|
+
function buildDisplayName(objectName, objectType) {
|
|
35
|
+
if (objectType === 'RSDS') {
|
|
36
|
+
return objectName.trimEnd().replace(/\s+(\S+)$/, ' / $1');
|
|
37
|
+
}
|
|
38
|
+
return objectName.trim();
|
|
39
|
+
}
|
|
40
|
+
function truncate(s, n) {
|
|
41
|
+
return s.length > n ? s.slice(0, n - 1) + '…' : s;
|
|
42
|
+
}
|
|
43
|
+
function renderFlatTable(nodes, header) {
|
|
44
|
+
const lines = [
|
|
45
|
+
header,
|
|
46
|
+
'',
|
|
47
|
+
`${'TYPE'.padEnd(6)} ${'NAME'.padEnd(34)} ${'DESCRIPTION'.padEnd(38)} STATUS`,
|
|
48
|
+
`${'------'} ${'----------------------------------'.padEnd(34)} ${'--------------------------------------'.padEnd(38)} -------`,
|
|
49
|
+
];
|
|
50
|
+
const typeOrder = ['LSYS', 'RSDS', 'TRCS', 'TRFN', 'DTPA', 'IOBJ', 'ADSO', 'HCPR', 'ELEM'];
|
|
51
|
+
const sorted = [...nodes].sort((a, b) => {
|
|
52
|
+
const ai = typeOrder.indexOf(a.objectType);
|
|
53
|
+
const bi = typeOrder.indexOf(b.objectType);
|
|
54
|
+
const diff = (ai === -1 ? 99 : ai) - (bi === -1 ? 99 : bi);
|
|
55
|
+
if (diff !== 0)
|
|
56
|
+
return diff;
|
|
57
|
+
return a.objectName.localeCompare(b.objectName);
|
|
58
|
+
});
|
|
59
|
+
for (const n of sorted) {
|
|
60
|
+
const name = truncate(buildDisplayName(n.objectName, n.objectType), 34);
|
|
61
|
+
const desc = truncate(n.objectDescription, 38);
|
|
62
|
+
const status = n.objectStatus || '-';
|
|
63
|
+
lines.push(`${n.objectType.padEnd(6)} ${name.padEnd(34)} ${desc.padEnd(38)} ${status}`);
|
|
64
|
+
}
|
|
65
|
+
return lines.join('\n');
|
|
66
|
+
}
|
|
67
|
+
function renderTree(nodes, header, direction) {
|
|
68
|
+
if (nodes.length === 0)
|
|
69
|
+
return `${header}\n\n(no nodes)`;
|
|
70
|
+
const byId = new Map(nodes.map((n) => [n.id, n]));
|
|
71
|
+
const reverseDir = direction === 'downwards';
|
|
72
|
+
const roots = reverseDir
|
|
73
|
+
? nodes.filter((n) => n.targetNodeIds.length === 0)
|
|
74
|
+
: nodes.filter((n) => n.sourceNodeIds.length === 0);
|
|
75
|
+
const lines = [header, ''];
|
|
76
|
+
function nodeLabel(n) {
|
|
77
|
+
const name = buildDisplayName(n.objectName, n.objectType);
|
|
78
|
+
const subType = n.objectSubType ? `:${n.objectSubType}` : '';
|
|
79
|
+
const desc = n.objectDescription ? ` — ${n.objectDescription}` : '';
|
|
80
|
+
const status = n.objectStatus && n.objectStatus !== 'active' ? ` (${n.objectStatus})` : n.objectStatus === 'active' ? ' (active)' : '';
|
|
81
|
+
return `[${n.objectType}${subType}] ${name}${desc}${status}`;
|
|
82
|
+
}
|
|
83
|
+
const visited = new Set();
|
|
84
|
+
function renderNode(n, displayPrefix, childContinuation) {
|
|
85
|
+
if (visited.has(n.id)) {
|
|
86
|
+
lines.push(`${displayPrefix}${nodeLabel(n)} ↑ already shown`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
lines.push(`${displayPrefix}${nodeLabel(n)}`);
|
|
90
|
+
visited.add(n.id);
|
|
91
|
+
const nextIds = reverseDir ? n.sourceNodeIds : n.targetNodeIds;
|
|
92
|
+
const children = nextIds.map((id) => byId.get(id)).filter((x) => x !== undefined);
|
|
93
|
+
for (let i = 0; i < children.length; i++) {
|
|
94
|
+
const isLast = i === children.length - 1;
|
|
95
|
+
const branch = isLast ? '└─ ' : '├─ ';
|
|
96
|
+
const nextContinuation = childContinuation + (isLast ? ' ' : '│ ');
|
|
97
|
+
renderNode(children[i], childContinuation + branch, nextContinuation);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
for (let i = 0; i < roots.length; i++) {
|
|
101
|
+
renderNode(roots[i], '', '');
|
|
102
|
+
if (i < roots.length - 1)
|
|
103
|
+
lines.push('');
|
|
104
|
+
}
|
|
105
|
+
return lines.join('\n');
|
|
106
|
+
}
|
|
107
|
+
export async function bwGetDataflow(client, objectName, objectType, sourceSystem, direction, levels, format) {
|
|
108
|
+
const typeUpper = objectType.toUpperCase();
|
|
109
|
+
if (typeUpper === 'RSDS') {
|
|
110
|
+
if (!sourceSystem) {
|
|
111
|
+
throw new Error('bw_get_dataflow with object_type RSDS requires source_system parameter.');
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// Build objectname param
|
|
115
|
+
let encodedName;
|
|
116
|
+
if (typeUpper === 'RSDS') {
|
|
117
|
+
const padded = objectName.toUpperCase().padEnd(30) + sourceSystem.toUpperCase();
|
|
118
|
+
encodedName = padded.replace(/ /g, '+');
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
encodedName = encodeURIComponent(objectName.toUpperCase());
|
|
122
|
+
}
|
|
123
|
+
// Build query string
|
|
124
|
+
const params = [
|
|
125
|
+
`objecttype=${typeUpper}`,
|
|
126
|
+
`objectname=${encodedName}`,
|
|
127
|
+
];
|
|
128
|
+
if (direction === 'upwards' || direction === 'both') {
|
|
129
|
+
params.push(`levelupwards=${levels}`);
|
|
130
|
+
}
|
|
131
|
+
if (direction === 'downwards' || direction === 'both') {
|
|
132
|
+
params.push(`leveldownwards=${levels}`);
|
|
133
|
+
}
|
|
134
|
+
const url = `${BASE}?${params.join('&')}`;
|
|
135
|
+
const { body } = await client.get(url, DMOD_ACCEPT);
|
|
136
|
+
if (format === 'raw')
|
|
137
|
+
return body;
|
|
138
|
+
const nodes = parseNodes(body);
|
|
139
|
+
const dirLabel = direction === 'both' ? 'upwards + downwards' : direction;
|
|
140
|
+
const displayObjectName = typeUpper === 'RSDS'
|
|
141
|
+
? `${typeUpper} ${objectName.toUpperCase()} / ${sourceSystem.toUpperCase()}`
|
|
142
|
+
: `${typeUpper} ${objectName.toUpperCase()}`;
|
|
143
|
+
const header = `Dataflow: ${displayObjectName} (${dirLabel}, ${nodes.length} nodes)`;
|
|
144
|
+
if (nodes.length > 30) {
|
|
145
|
+
return renderFlatTable(nodes, header);
|
|
146
|
+
}
|
|
147
|
+
return renderTree(nodes, header, direction);
|
|
148
|
+
}
|