@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,225 @@
|
|
|
1
|
+
const TRCS_MEDIA = 'application/vnd.sap.bw.modeling.trcs-v1_0_0+xml';
|
|
2
|
+
// ── bwGetInfosource ───────────────────────────────────────────────────────────
|
|
3
|
+
/**
|
|
4
|
+
* bw_get_infosource — read the structure of an InfoSource (TRCS).
|
|
5
|
+
*
|
|
6
|
+
* GET /sap/bw/modeling/trcs/{name}/m
|
|
7
|
+
*/
|
|
8
|
+
export async function bwGetInfosource(client, name) {
|
|
9
|
+
const nameLower = name.toLowerCase();
|
|
10
|
+
const result = await client.get(`/sap/bw/modeling/trcs/${nameLower}/m`, TRCS_MEDIA);
|
|
11
|
+
const xml = result.body;
|
|
12
|
+
// Root attributes
|
|
13
|
+
const rootName = (xml.match(/trcs:infoSource[^>]+\bname="([^"]+)"/) ?? [])[1] ?? name.toUpperCase();
|
|
14
|
+
const aggregationAttr = (xml.match(/trcs:infoSource[^>]+\baggregation="([^"]+)"/) ?? [])[1];
|
|
15
|
+
const aggregation = aggregationAttr === 'true';
|
|
16
|
+
// endUserTexts label (top-level)
|
|
17
|
+
const labelMatch = xml.match(/^[\s\S]*?<endUserTexts label="([^"]*)"/);
|
|
18
|
+
const label = labelMatch ? labelMatch[1] : '';
|
|
19
|
+
// tlogoProperties
|
|
20
|
+
const tlogoPart = (xml.match(/<tlogoProperties([^>]*)>/) ?? [])[1] ?? '';
|
|
21
|
+
const description = (tlogoPart.match(/adtcore:description="([^"]*)"/) ?? [])[1] ?? '';
|
|
22
|
+
const objectStatus = (tlogoPart.match(/adtcore:version="([^"]*)"/) ?? [])[1] ?? '';
|
|
23
|
+
// <infoArea>
|
|
24
|
+
const infoArea = (xml.match(/<infoArea>([^<]*)<\/infoArea>/) ?? [])[1] ?? '';
|
|
25
|
+
// Collect keyElement names: #///NAME → NAME
|
|
26
|
+
const keyElements = new Set();
|
|
27
|
+
const keyRe = /<keyElement>#\/\/\/([^<]+)<\/keyElement>/g;
|
|
28
|
+
let km;
|
|
29
|
+
while ((km = keyRe.exec(xml)) !== null) {
|
|
30
|
+
keyElements.add(km[1].toUpperCase());
|
|
31
|
+
}
|
|
32
|
+
// Parse <element> blocks
|
|
33
|
+
const fields = [];
|
|
34
|
+
const elemRe = /<element\b([\s\S]*?)<\/element>/g;
|
|
35
|
+
let em;
|
|
36
|
+
while ((em = elemRe.exec(xml)) !== null) {
|
|
37
|
+
const block = em[1];
|
|
38
|
+
const fieldName = (block.match(/\bname="([^"]+)"/) ?? [])[1];
|
|
39
|
+
if (!fieldName)
|
|
40
|
+
continue;
|
|
41
|
+
const iObjName = (block.match(/\binfoObjectName="([^"]+)"/) ?? [])[1];
|
|
42
|
+
const fieldLabel = (block.match(/<endUserTexts label="([^"]*)"/) ??
|
|
43
|
+
block.match(/<descriptions label="([^"]*)"/) ?? [])[1] ?? '';
|
|
44
|
+
const dataType = (block.match(/<inlineType\b[^>]*\bname="([^"]+)"/) ?? [])[1] ?? '';
|
|
45
|
+
const lengthStr = (block.match(/<inlineType\b[^>]*\blength="([^"]+)"/) ?? [])[1];
|
|
46
|
+
const aggrBehav = (block.match(/\baggregationBehavior="([^"]+)"/) ?? [])[1];
|
|
47
|
+
const field = {
|
|
48
|
+
name: fieldName,
|
|
49
|
+
label: fieldLabel,
|
|
50
|
+
data_type: dataType,
|
|
51
|
+
is_key: keyElements.has(fieldName.toUpperCase()),
|
|
52
|
+
};
|
|
53
|
+
if (iObjName)
|
|
54
|
+
field['infoobject_name'] = iObjName;
|
|
55
|
+
if (lengthStr !== undefined)
|
|
56
|
+
field['length'] = Number(lengthStr);
|
|
57
|
+
if (aggrBehav)
|
|
58
|
+
field['aggregation_behavior'] = aggrBehav;
|
|
59
|
+
fields.push(field);
|
|
60
|
+
}
|
|
61
|
+
return JSON.stringify({
|
|
62
|
+
name: rootName,
|
|
63
|
+
label,
|
|
64
|
+
description,
|
|
65
|
+
info_area: infoArea,
|
|
66
|
+
object_status: objectStatus,
|
|
67
|
+
aggregation,
|
|
68
|
+
fields,
|
|
69
|
+
}, null, 2);
|
|
70
|
+
}
|
|
71
|
+
function buildElement(field) {
|
|
72
|
+
const name = field.name.toUpperCase();
|
|
73
|
+
const aggr = field.aggregationBehavior ?? 'NONE';
|
|
74
|
+
if (field.infoObjectName) {
|
|
75
|
+
const iobj = field.infoObjectName.toUpperCase();
|
|
76
|
+
return (` <element xsi:type="BwCore:BwElement"\n` +
|
|
77
|
+
` name="${name}"\n` +
|
|
78
|
+
` keep="false"\n` +
|
|
79
|
+
` aggregationBehavior="${aggr}"\n` +
|
|
80
|
+
` attributeHierarchyDefaultMember=""\n` +
|
|
81
|
+
` infoObjectName="${iobj}"\n` +
|
|
82
|
+
` displayFolder="">\n` +
|
|
83
|
+
` <endUserTexts label="${field.label}"/>\n` +
|
|
84
|
+
` <inlineType name="${field.type}" length="${field.length}" globalElementName="${iobj}"/>\n` +
|
|
85
|
+
` <fixedCurrency></fixedCurrency>\n` +
|
|
86
|
+
` <fixedUnit></fixedUnit>\n` +
|
|
87
|
+
` <associationType>1</associationType>\n` +
|
|
88
|
+
` </element>`);
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
return (` <element xsi:type="BwCore:BwElement"\n` +
|
|
92
|
+
` name="${name}"\n` +
|
|
93
|
+
` aggregationBehavior="${aggr}"\n` +
|
|
94
|
+
` conversionRoutine="">\n` +
|
|
95
|
+
` <inlineType name="${field.type}" length="${field.length}" precision="0" scale="0" semanticType="empty"/>\n` +
|
|
96
|
+
` <localProperties xsi:type="BwCore:LocalCharacteristicProperties">\n` +
|
|
97
|
+
` <descriptions label="${field.label}"/>\n` +
|
|
98
|
+
` </localProperties>\n` +
|
|
99
|
+
` </element>`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// ── bw_create_infosource ──────────────────────────────────────────────────────
|
|
103
|
+
/**
|
|
104
|
+
* bw_create_infosource — create a new InfoSource (TRCS) shell.
|
|
105
|
+
*
|
|
106
|
+
* Optionally copies fields from an existing object (aDSO, CompositeProvider,
|
|
107
|
+
* DataSource, or InfoObject) via copyFrom* parameters.
|
|
108
|
+
*
|
|
109
|
+
* Workflow: Lock (CREA) → POST → Unlock
|
|
110
|
+
* After creation the InfoSource is inactive — call bw_activate with object_type "trcs".
|
|
111
|
+
*/
|
|
112
|
+
export async function bwCreateInfosource(client, name, description, infoArea, pkg = '$TMP', copyFromObjectName, copyFromObjectType, copyFromObjectSubType, copyFromSourceSystem) {
|
|
113
|
+
const nameUpper = name.toUpperCase();
|
|
114
|
+
const infoAreaUpper = infoArea.toUpperCase();
|
|
115
|
+
const lockHandle = await client.lock('trcs', name, { 'activity_context': 'CREA' });
|
|
116
|
+
// Build URL — add copyFrom params before lockHandle when provided
|
|
117
|
+
let url = `/sap/bw/modeling/trcs/${name.toLowerCase()}`;
|
|
118
|
+
const qs = [];
|
|
119
|
+
if (copyFromObjectType && copyFromObjectName) {
|
|
120
|
+
let encodedName = copyFromObjectName;
|
|
121
|
+
if (copyFromObjectType === 'RSDS' && copyFromSourceSystem) {
|
|
122
|
+
encodedName = copyFromObjectName.padEnd(30) + copyFromSourceSystem.padEnd(10);
|
|
123
|
+
}
|
|
124
|
+
qs.push(`copyFromObjectName=${encodeURIComponent(encodedName)}`);
|
|
125
|
+
qs.push(`copyFromObjectType=${encodeURIComponent(copyFromObjectType)}`);
|
|
126
|
+
if (copyFromObjectSubType) {
|
|
127
|
+
qs.push(`copyFromObjectSubType=${encodeURIComponent(copyFromObjectSubType)}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
qs.push(`lockHandle=${lockHandle}`);
|
|
131
|
+
url += '?' + qs.join('&');
|
|
132
|
+
const language = process.env.BW_LANGUAGE ?? 'DE';
|
|
133
|
+
const body = `<?xml version="1.0" encoding="UTF-8"?>\n` +
|
|
134
|
+
`<trcs:infoSource\n` +
|
|
135
|
+
` xmlns:adtcore="http://www.sap.com/adt/core"\n` +
|
|
136
|
+
` xmlns:trcs="http://www.sap.com/bw/modeling/trcs.ecore"\n` +
|
|
137
|
+
` name="${nameUpper}">\n` +
|
|
138
|
+
` <endUserTexts label="${description}"/>\n` +
|
|
139
|
+
` <tlogoProperties\n` +
|
|
140
|
+
` adtcore:language="${language}"\n` +
|
|
141
|
+
` adtcore:name="${nameUpper}"\n` +
|
|
142
|
+
` adtcore:type="TRCS"\n` +
|
|
143
|
+
` adtcore:masterLanguage="${language}">\n` +
|
|
144
|
+
` <infoArea>${infoAreaUpper}</infoArea>\n` +
|
|
145
|
+
` </tlogoProperties>\n` +
|
|
146
|
+
`</trcs:infoSource>`;
|
|
147
|
+
try {
|
|
148
|
+
await client.postWithCsrf(url, body, `application/xml, ${TRCS_MEDIA}`, {
|
|
149
|
+
'Development-Class': pkg,
|
|
150
|
+
Accept: TRCS_MEDIA,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
await client.unlock('trcs', name).catch(() => { });
|
|
155
|
+
throw err;
|
|
156
|
+
}
|
|
157
|
+
await client.unlock('trcs', name);
|
|
158
|
+
const fromParts = [];
|
|
159
|
+
if (copyFromObjectType && copyFromObjectName) {
|
|
160
|
+
fromParts.push(` from ${copyFromObjectType} ${copyFromObjectName.toUpperCase()}`);
|
|
161
|
+
if (copyFromObjectSubType)
|
|
162
|
+
fromParts.push(` (${copyFromObjectSubType})`);
|
|
163
|
+
}
|
|
164
|
+
return JSON.stringify({
|
|
165
|
+
success: true,
|
|
166
|
+
message: `InfoSource ${nameUpper} created${fromParts.join('')} in package ${pkg}. Call bw_activate to activate.`,
|
|
167
|
+
infosource_name: nameUpper,
|
|
168
|
+
object_type: 'trcs',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
// ── bw_update_infosource ──────────────────────────────────────────────────────
|
|
172
|
+
/**
|
|
173
|
+
* bw_update_infosource — replace the field list of an InfoSource.
|
|
174
|
+
*
|
|
175
|
+
* Workflow: Lock → GET full XML → replace element/keyElement sections → PUT
|
|
176
|
+
* tlogoProperties are passed through unchanged.
|
|
177
|
+
* Returns lock_handle for bw_activate.
|
|
178
|
+
*/
|
|
179
|
+
export async function bwUpdateInfosource(client, name, description, fields, transport) {
|
|
180
|
+
const nameUpper = name.toUpperCase();
|
|
181
|
+
const trcsPath = `/sap/bw/modeling/trcs/${name.toLowerCase()}/m`;
|
|
182
|
+
// 1. Lock (no activity_context = update mode)
|
|
183
|
+
const lockHandle = await client.lock('trcs', name);
|
|
184
|
+
// 2. GET current XML
|
|
185
|
+
const getResult = await client.get(trcsPath, TRCS_MEDIA);
|
|
186
|
+
const timestamp = getResult.headers['timestamp'] ?? getResult.headers['TIMESTAMP'];
|
|
187
|
+
let xml = getResult.body;
|
|
188
|
+
// 3. Update description
|
|
189
|
+
if (description !== undefined) {
|
|
190
|
+
xml = xml.replace(/<endUserTexts label="[^"]*"\s*\/>/, `<endUserTexts label="${description}"/>`);
|
|
191
|
+
}
|
|
192
|
+
// 4. Replace elements and keyElements
|
|
193
|
+
if (fields !== undefined) {
|
|
194
|
+
// Strip all existing <element> blocks (may span multiple lines)
|
|
195
|
+
xml = xml.replace(/[ \t]*<element\b[\s\S]*?<\/element>\n?/g, '');
|
|
196
|
+
// Strip all existing <keyElement> entries
|
|
197
|
+
xml = xml.replace(/[ \t]*<keyElement>[^<]*<\/keyElement>\n?/g, '');
|
|
198
|
+
const elementBlocks = fields.map(buildElement);
|
|
199
|
+
const keyBlocks = fields
|
|
200
|
+
.filter((f) => f.isKey)
|
|
201
|
+
.map((f) => ` <keyElement>#///${f.name.toUpperCase()}</keyElement>`);
|
|
202
|
+
const insertBlock = [...elementBlocks, ...keyBlocks].join('\n') + '\n';
|
|
203
|
+
if (xml.includes('<tlogoProperties')) {
|
|
204
|
+
xml = xml.replace('<tlogoProperties', insertBlock + ' <tlogoProperties');
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
xml = xml.replace('</trcs:infoSource>', insertBlock + '</trcs:infoSource>');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
// 5. PUT
|
|
211
|
+
try {
|
|
212
|
+
await client.put('trcs', name, lockHandle, xml, timestamp, transport);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
await client.unlock('trcs', name).catch(() => { });
|
|
216
|
+
throw err;
|
|
217
|
+
}
|
|
218
|
+
return JSON.stringify({
|
|
219
|
+
success: true,
|
|
220
|
+
message: `InfoSource ${nameUpper} updated. Call bw_activate to activate.`,
|
|
221
|
+
lock_handle: lockHandle,
|
|
222
|
+
infosource_name: nameUpper,
|
|
223
|
+
object_type: 'trcs',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
const ACCEPT = 'application/vnd.sap.bw4.modeling.processchain-v1_0_0+json';
|
|
2
|
+
async function fetchVariantDetail(client, processType, variantName) {
|
|
3
|
+
const NO_DETAIL_TYPES = new Set(['OR', 'AND', 'EXOR', 'CHAIN', 'DTP_LOAD', 'DTP_ADSO']);
|
|
4
|
+
if (NO_DETAIL_TYPES.has(processType.toUpperCase()))
|
|
5
|
+
return null;
|
|
6
|
+
try {
|
|
7
|
+
const url = `/sap/bw4/v1/modeling/processtypes/${processType.toLowerCase()}/variants/${variantName.toLowerCase()}/m`;
|
|
8
|
+
const result = await client.rawGet(url, { Accept: '*/*' });
|
|
9
|
+
if (result.body.trim().startsWith('<'))
|
|
10
|
+
return null;
|
|
11
|
+
const parsed = JSON.parse(result.body);
|
|
12
|
+
const detail = parsed.oDetail;
|
|
13
|
+
if (!detail || (typeof detail === 'string' && detail.trim() === '') || (typeof detail === 'object' && Object.keys(detail).length === 0))
|
|
14
|
+
return null;
|
|
15
|
+
return JSON.stringify(detail, null, 2);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function bwGetProcessChain(client, chainName, format = 'text', includeVariantDetails = true) {
|
|
22
|
+
const url = `/sap/bw/modeling/rspc/${encodeURIComponent(chainName.toLowerCase())}/m`;
|
|
23
|
+
const result = await client.rawGet(url, { Accept: ACCEPT });
|
|
24
|
+
const parsed = JSON.parse(result.body);
|
|
25
|
+
if (format === 'raw') {
|
|
26
|
+
return JSON.stringify(parsed, null, 2);
|
|
27
|
+
}
|
|
28
|
+
const variantDetailMap = {};
|
|
29
|
+
if (format === 'text' && includeVariantDetails) {
|
|
30
|
+
for (const node of parsed.aNode ?? []) {
|
|
31
|
+
const type = node.sProcessType;
|
|
32
|
+
const variant = node.sProcessVariant;
|
|
33
|
+
if (type && variant) {
|
|
34
|
+
const detail = await fetchVariantDetail(client, type, variant);
|
|
35
|
+
if (detail)
|
|
36
|
+
variantDetailMap[variant] = detail;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return renderText(parsed, variantDetailMap);
|
|
41
|
+
}
|
|
42
|
+
function renderText(pc, variantDetailMap = {}) {
|
|
43
|
+
const lines = [];
|
|
44
|
+
const header = pc.oHeader ?? {};
|
|
45
|
+
const sched = header.oSchedulingAttributes ?? {};
|
|
46
|
+
const mon = header.oMonitoringAttributes ?? {};
|
|
47
|
+
const nodes = pc.aNode ?? [];
|
|
48
|
+
const edges = pc.aEdge ?? [];
|
|
49
|
+
const variants = pc.aInlineVariant ?? [];
|
|
50
|
+
// Section 1 — header
|
|
51
|
+
lines.push(`Process Chain: ${header.sProcessChainId ?? ''}`);
|
|
52
|
+
lines.push(`Description: ${header.sDescription ?? ''}`);
|
|
53
|
+
lines.push(`Status: ${header.sObjectStatus ?? ''} / Version: ${header.sObjectVersion ?? ''}`);
|
|
54
|
+
lines.push(`InfoArea: ${header.sLocation ?? ''} — ${header.sLocationDescription ?? ''}`);
|
|
55
|
+
lines.push(`Active: ${header.bActive ?? false}`);
|
|
56
|
+
// Section 2 — Scheduling
|
|
57
|
+
lines.push('');
|
|
58
|
+
lines.push('── Scheduling ──');
|
|
59
|
+
lines.push(` Job Priority: ${sched.sJobPriority ?? ''} (A=high B=normal C=low)`);
|
|
60
|
+
lines.push(` Job Owner: ${sched.oJobOwner?.sJobOwner || '(not set)'}`);
|
|
61
|
+
lines.push(` Server: ${sched.sExecutionServer || '(not set)'}`);
|
|
62
|
+
lines.push(` Streaming: ${sched.bStreaming ?? false}`);
|
|
63
|
+
// Section 3 — Monitoring
|
|
64
|
+
lines.push('');
|
|
65
|
+
lines.push('── Monitoring ──');
|
|
66
|
+
lines.push(` Auto-Monitored: ${mon.bAutoMonitored ?? false}`);
|
|
67
|
+
lines.push(` Error Notification: ${mon.bErrorNotification ?? false}`);
|
|
68
|
+
lines.push(` Keep-Alive: ${mon.bKeepAlive ?? false}`);
|
|
69
|
+
lines.push(` Auto-Reset: ${mon.bAutoResetFailures ?? false}`);
|
|
70
|
+
// Section 4 — Steps
|
|
71
|
+
lines.push('');
|
|
72
|
+
lines.push(`── Steps (${nodes.length}) ──`);
|
|
73
|
+
nodes.forEach((node, idx) => {
|
|
74
|
+
const variantLabel = node.sVariantDescription || node.sProcessVariant || '';
|
|
75
|
+
lines.push(` [${idx}] ${node.sTypeDescription ?? ''} — ${variantLabel}`);
|
|
76
|
+
lines.push(` Type: ${node.sProcessType ?? ''}`);
|
|
77
|
+
lines.push(` Variant: ${node.sProcessVariant ?? ''}`);
|
|
78
|
+
lines.push(` Status: ${node.sStatus || 'neutral'}`);
|
|
79
|
+
lines.push(` Skipped: ${node.bSkipped ?? false}`);
|
|
80
|
+
if (node.bIsReference === true) {
|
|
81
|
+
lines.push(` Sub-Chain: ${node.sProcessVariant ?? ''} (referenced)`);
|
|
82
|
+
}
|
|
83
|
+
for (const d of node.aDetail ?? []) {
|
|
84
|
+
if (d.sValue && d.sValue.length > 0) {
|
|
85
|
+
lines.push(` ${d.sName ?? ''}: ${d.sValue}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (node.sProcessType === 'DECISION') {
|
|
89
|
+
const branches = (node.aSocket ?? []).filter((s) => !(s.sSubStatus === '00' && s.sStatus === 'negative'));
|
|
90
|
+
if (branches.length > 0) {
|
|
91
|
+
lines.push(` Branches:`);
|
|
92
|
+
for (const b of branches) {
|
|
93
|
+
lines.push(` [${b.sSubStatus ?? ''}] ${b.sDescription ?? ''} (status: ${b.sStatus ?? ''})`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (node.sProcessType === 'OR') {
|
|
98
|
+
lines.push(` (join node — merges multiple incoming branches)`);
|
|
99
|
+
}
|
|
100
|
+
const detailJson = variantDetailMap[node.sProcessVariant ?? ''];
|
|
101
|
+
if (detailJson) {
|
|
102
|
+
lines.push(` ── Variant Detail ──`);
|
|
103
|
+
for (const line of detailJson.split('\n')) {
|
|
104
|
+
lines.push(` ${line}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
// Section 5 — Dependencies
|
|
109
|
+
lines.push('');
|
|
110
|
+
lines.push(`── Dependencies (${edges.length}) ──`);
|
|
111
|
+
for (const edge of edges) {
|
|
112
|
+
const source = nodes[edge.iNodeIndexFrom];
|
|
113
|
+
const target = nodes[edge.iNodeIndexTo];
|
|
114
|
+
const sourceType = source?.sProcessType ?? '?';
|
|
115
|
+
const targetType = target?.sProcessType ?? '?';
|
|
116
|
+
let condition;
|
|
117
|
+
if (edge.sSubStatus && edge.sSubStatus !== '00') {
|
|
118
|
+
const socket = source?.aSocket?.find((s) => s.sSubStatus === edge.sSubStatus);
|
|
119
|
+
condition = socket?.sDescription || edge.sStatus || '';
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
condition = edge.sStatus || '';
|
|
123
|
+
}
|
|
124
|
+
lines.push(` Step ${edge.iNodeIndexFrom} (${sourceType}) → Step ${edge.iNodeIndexTo} (${targetType})`);
|
|
125
|
+
lines.push(` Condition: ${condition} Strength: ${edge.sStrength ?? ''}`);
|
|
126
|
+
}
|
|
127
|
+
// Section 6 — Variants
|
|
128
|
+
lines.push('');
|
|
129
|
+
lines.push(`── Variants (${variants.length}) ──`);
|
|
130
|
+
for (const v of variants) {
|
|
131
|
+
const keys = Object.keys(v);
|
|
132
|
+
const isStub = keys.length === 1 && keys[0] === 'sProcessVariant';
|
|
133
|
+
if (isStub) {
|
|
134
|
+
lines.push(` ${v.sProcessVariant ?? ''} (no detail — auto-generated variant)`);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
lines.push(` ${v.sProcessVariant ?? ''} (active: ${v.bActive ?? false})`);
|
|
138
|
+
lines.push(` ${v.sVariantDescription || '(no description)'}`);
|
|
139
|
+
const detail = v.oDetail;
|
|
140
|
+
if (detail) {
|
|
141
|
+
if (detail.PROGRAM && detail.PROGRAM.length >= 1) {
|
|
142
|
+
const p = detail.PROGRAM[0];
|
|
143
|
+
lines.push(` ABAP Program: ${p.key ?? ''} Package: ${p.row?.package || '(unknown)'}`);
|
|
144
|
+
}
|
|
145
|
+
if (detail.eventid && detail.eventid.length >= 1) {
|
|
146
|
+
lines.push(` Trigger Event: ${detail.eventid[0].key ?? ''} Param: ${detail.eventparm ?? ''}`);
|
|
147
|
+
}
|
|
148
|
+
if (detail.startdttyp !== undefined) {
|
|
149
|
+
lines.push(` Start Type: ${detail.startdttyp} (E=event I=immediate P=periodic)`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return lines.join('\n');
|
|
154
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export async function bwGetProcessVariant(client, processType, variantName, format = 'text') {
|
|
2
|
+
const url = `/sap/bw4/v1/modeling/processtypes/${encodeURIComponent(processType.toLowerCase())}/variants/${encodeURIComponent(variantName.toLowerCase())}/m`;
|
|
3
|
+
const result = await client.rawGet(url, { Accept: 'application/json' });
|
|
4
|
+
const parsed = JSON.parse(result.body);
|
|
5
|
+
if (format === 'raw') {
|
|
6
|
+
return JSON.stringify(parsed, null, 2);
|
|
7
|
+
}
|
|
8
|
+
return renderText(parsed, processType, variantName);
|
|
9
|
+
}
|
|
10
|
+
function renderText(pv, processType, variantName) {
|
|
11
|
+
const lines = [];
|
|
12
|
+
lines.push(`Process Variant: ${variantName.toUpperCase()}`);
|
|
13
|
+
lines.push(`Type: ${processType.toUpperCase()}`);
|
|
14
|
+
lines.push(`Description: ${pv.sVariantDescription || '(none)'}`);
|
|
15
|
+
lines.push(`Active: ${pv.bActive ?? false}`);
|
|
16
|
+
lines.push('');
|
|
17
|
+
lines.push('── Detail ──');
|
|
18
|
+
const detail = pv.oDetail;
|
|
19
|
+
const detailEmpty = detail === undefined ||
|
|
20
|
+
detail === null ||
|
|
21
|
+
(typeof detail === 'string' && detail.length === 0);
|
|
22
|
+
if (detailEmpty) {
|
|
23
|
+
lines.push(' (no detail available)');
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
const indented = JSON.stringify(detail, null, 2)
|
|
27
|
+
.split('\n')
|
|
28
|
+
.map((l) => ` ${l}`)
|
|
29
|
+
.join('\n');
|
|
30
|
+
lines.push(indented);
|
|
31
|
+
}
|
|
32
|
+
const sockets = pv.aSocket ?? [];
|
|
33
|
+
if (sockets.length > 0) {
|
|
34
|
+
lines.push('');
|
|
35
|
+
lines.push('── Sockets ──');
|
|
36
|
+
for (const s of sockets) {
|
|
37
|
+
lines.push(` [${s.sSubStatus ?? ''}] ${s.sStatus ?? ''} — ${s.sDescription ?? ''}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const options = (pv.aExecutionOption ?? []).filter((o) => o.name && o.name.length > 0);
|
|
41
|
+
if (options.length > 0) {
|
|
42
|
+
lines.push('');
|
|
43
|
+
lines.push('── Execution Options ──');
|
|
44
|
+
for (const o of options) {
|
|
45
|
+
lines.push(` [${o.name}] ${o.description ?? ''}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return lines.join('\n');
|
|
49
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import https from 'https';
|
|
3
|
+
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
|
|
4
|
+
function getEnv(key) {
|
|
5
|
+
const val = process.env[key];
|
|
6
|
+
if (!val)
|
|
7
|
+
throw new Error(`Environment variable ${key} is not set`);
|
|
8
|
+
return val;
|
|
9
|
+
}
|
|
10
|
+
function buildAuth() {
|
|
11
|
+
const user = getEnv('BW_USER');
|
|
12
|
+
const pass = getEnv('BW_PASSWORD');
|
|
13
|
+
return 'Basic ' + Buffer.from(`${user}:${pass}`).toString('base64');
|
|
14
|
+
}
|
|
15
|
+
function pushBase(adsoName) {
|
|
16
|
+
const baseUrl = getEnv('BW_URL').replace(/\/$/, '');
|
|
17
|
+
return `${baseUrl}/sap/bw4/v1/push/dataStores/${adsoName.toLowerCase()}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* bw_push_data — push records into an aDSO write-interface inbound table.
|
|
21
|
+
*
|
|
22
|
+
* Flow (One Step):
|
|
23
|
+
* 1. GET /requests with x-csrf-token: Fetch → extract token + session cookies
|
|
24
|
+
* 2. POST /dataSend with JSON array body → expect HTTP 204
|
|
25
|
+
*/
|
|
26
|
+
export async function bwPushData(adsoName, records, mode = 'one_step') {
|
|
27
|
+
const base = pushBase(adsoName);
|
|
28
|
+
const auth = buildAuth();
|
|
29
|
+
// Step 1: fetch CSRF token and session cookies
|
|
30
|
+
const csrfRes = await axios.get(`${base}/requests`, {
|
|
31
|
+
httpsAgent,
|
|
32
|
+
headers: {
|
|
33
|
+
'Authorization': auth,
|
|
34
|
+
'x-csrf-token': 'Fetch',
|
|
35
|
+
},
|
|
36
|
+
validateStatus: () => true,
|
|
37
|
+
});
|
|
38
|
+
const csrfToken = csrfRes.headers['x-csrf-token'];
|
|
39
|
+
if (!csrfToken || csrfToken.toLowerCase() === 'required') {
|
|
40
|
+
throw new Error(`Failed to fetch CSRF token. HTTP ${csrfRes.status}: ${csrfRes.data}`);
|
|
41
|
+
}
|
|
42
|
+
// Extract session cookies from set-cookie header
|
|
43
|
+
const rawCookies = Array.isArray(csrfRes.headers['set-cookie'])
|
|
44
|
+
? csrfRes.headers['set-cookie']
|
|
45
|
+
: csrfRes.headers['set-cookie'] ? [csrfRes.headers['set-cookie']] : [];
|
|
46
|
+
const cookieParts = rawCookies
|
|
47
|
+
.map((c) => c.split(';')[0].trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
const cookieHeader = cookieParts.join('; ');
|
|
50
|
+
// Step 2: POST dataSend
|
|
51
|
+
const sendUrl = mode === 'messaging'
|
|
52
|
+
? `${base}/dataSend?request=MESSAGING`
|
|
53
|
+
: `${base}/dataSend`;
|
|
54
|
+
const sendRes = await axios.post(sendUrl, records, {
|
|
55
|
+
httpsAgent,
|
|
56
|
+
headers: {
|
|
57
|
+
'Authorization': auth,
|
|
58
|
+
'Content-Type': 'application/json',
|
|
59
|
+
'x-csrf-token': csrfToken,
|
|
60
|
+
...(cookieHeader ? { 'Cookie': cookieHeader } : {}),
|
|
61
|
+
},
|
|
62
|
+
validateStatus: () => true,
|
|
63
|
+
});
|
|
64
|
+
if (sendRes.status === 204) {
|
|
65
|
+
return JSON.stringify({
|
|
66
|
+
success: true,
|
|
67
|
+
message: `${records.length} record(s) pushed to aDSO ${adsoName.toUpperCase()} (mode: ${mode}).`,
|
|
68
|
+
adso_name: adsoName.toUpperCase(),
|
|
69
|
+
record_count: records.length,
|
|
70
|
+
mode,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Error — include response body for diagnosis
|
|
74
|
+
const errorBody = typeof sendRes.data === 'string'
|
|
75
|
+
? sendRes.data
|
|
76
|
+
: JSON.stringify(sendRes.data);
|
|
77
|
+
throw new Error(`Push to ${adsoName.toUpperCase()} failed (HTTP ${sendRes.status}): ${errorBody}`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* bw_get_push_schema — fetch the JSON schema for an aDSO's write interface.
|
|
81
|
+
*
|
|
82
|
+
* Returns the field list, types, and required fields so the caller knows
|
|
83
|
+
* what to include in bw_push_data records.
|
|
84
|
+
*/
|
|
85
|
+
export async function bwGetPushSchema(adsoName) {
|
|
86
|
+
const base = pushBase(adsoName);
|
|
87
|
+
const auth = buildAuth();
|
|
88
|
+
const res = await axios.get(base, {
|
|
89
|
+
httpsAgent,
|
|
90
|
+
headers: {
|
|
91
|
+
'Authorization': auth,
|
|
92
|
+
'Accept': 'application/json',
|
|
93
|
+
},
|
|
94
|
+
validateStatus: () => true,
|
|
95
|
+
});
|
|
96
|
+
if (res.status !== 200) {
|
|
97
|
+
throw new Error(`Failed to fetch push schema for ${adsoName.toUpperCase()} (HTTP ${res.status}): ${JSON.stringify(res.data)}`);
|
|
98
|
+
}
|
|
99
|
+
return `Push schema for aDSO ${adsoName.toUpperCase()}:\n\n${JSON.stringify(res.data, null, 2)}`;
|
|
100
|
+
}
|