@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,117 @@
|
|
|
1
|
+
import { MEDIA_TYPES } from '../bw-client.js';
|
|
2
|
+
/**
|
|
3
|
+
* bw_move_object — move any BW object to a different InfoArea.
|
|
4
|
+
*
|
|
5
|
+
* Single POST to /sap/bw/modeling/move_requests — no lock needed.
|
|
6
|
+
*/
|
|
7
|
+
export async function bwMoveObject(client, args) {
|
|
8
|
+
const typeLower = args.objectType.toLowerCase();
|
|
9
|
+
const nameLower = args.objectName.toLowerCase();
|
|
10
|
+
const targetUpper = args.targetInfoArea.toUpperCase();
|
|
11
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
12
|
+
<atom:feed xmlns:atom="http://www.w3.org/2005/Atom" xmlns:bwModel="http://www.sap.com/bw/modeling">
|
|
13
|
+
<atom:entry>
|
|
14
|
+
<atom:content type="application/xml">
|
|
15
|
+
<bwModel:moveProperties
|
|
16
|
+
targetObjectType="AREA"
|
|
17
|
+
targetObjectName="${targetUpper}"
|
|
18
|
+
movePosition="CHILD"
|
|
19
|
+
version="inactive"
|
|
20
|
+
lockHandle="">
|
|
21
|
+
</bwModel:moveProperties>
|
|
22
|
+
</atom:content>
|
|
23
|
+
<atom:link
|
|
24
|
+
href="/sap/bw/modeling/${typeLower}/${nameLower}/m"
|
|
25
|
+
type="application/*"
|
|
26
|
+
rel="self">
|
|
27
|
+
</atom:link>
|
|
28
|
+
</atom:entry>
|
|
29
|
+
</atom:feed>`;
|
|
30
|
+
await client.postRaw('/sap/bw/modeling/move_requests', xml, 'application/atom+xml;type=entry');
|
|
31
|
+
return JSON.stringify({
|
|
32
|
+
success: true,
|
|
33
|
+
objectType: typeLower,
|
|
34
|
+
objectName: nameLower,
|
|
35
|
+
targetInfoArea: targetUpper,
|
|
36
|
+
message: `Object '${args.objectName.toUpperCase()}' moved to InfoArea '${targetUpper}'.`,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* bw_create_infoarea — create a new InfoArea (immediately active, no activation step needed).
|
|
41
|
+
*
|
|
42
|
+
* Flow:
|
|
43
|
+
* 1. Lock (CREA, no parent_name/parent_type) → lockHandle
|
|
44
|
+
* 2. POST with XML body → InfoArea created and active
|
|
45
|
+
* (unlock is automatic after POST)
|
|
46
|
+
*/
|
|
47
|
+
export async function bwCreateInfoArea(client, args) {
|
|
48
|
+
const nameUpper = args.name.toUpperCase();
|
|
49
|
+
const nameLower = args.name.toLowerCase();
|
|
50
|
+
const pkg = args.package ?? '$TMP';
|
|
51
|
+
const desc = args.description ?? '';
|
|
52
|
+
const parentUpper = args.parent_info_area?.toUpperCase() ?? '';
|
|
53
|
+
const language = process.env.BW_LANGUAGE?.toUpperCase() ?? 'DE';
|
|
54
|
+
const user = process.env.BW_USER?.toUpperCase() ?? '';
|
|
55
|
+
// Step 1: Lock with CREA (no parent_name / parent_type for InfoArea)
|
|
56
|
+
const lockHandle = await client.lock('area', nameLower, {
|
|
57
|
+
activity_context: 'CREA',
|
|
58
|
+
}, 'stateful_enqueue');
|
|
59
|
+
// Step 2: POST — creates and activates the InfoArea in one step
|
|
60
|
+
const parentAttr = parentUpper ? ` parentInfoArea="${parentUpper}"` : '';
|
|
61
|
+
const parentElement = parentUpper ? parentUpper : '';
|
|
62
|
+
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
63
|
+
<InfoArea:infoArea
|
|
64
|
+
xmlns:InfoArea="http://www.sap.com/bw/modeling/BwInfoArea.ecore"
|
|
65
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
66
|
+
name="${nameUpper}"${parentAttr}>
|
|
67
|
+
<longDescription>${desc}</longDescription>
|
|
68
|
+
<tlogoProperties
|
|
69
|
+
adtcore:language="${language}"
|
|
70
|
+
adtcore:name="${nameUpper}"
|
|
71
|
+
adtcore:type="AREA"
|
|
72
|
+
adtcore:masterLanguage="${language}"
|
|
73
|
+
adtcore:responsible="${user}">
|
|
74
|
+
<infoArea>${parentElement}</infoArea>
|
|
75
|
+
</tlogoProperties>
|
|
76
|
+
</InfoArea:infoArea>`;
|
|
77
|
+
await client.create('area', nameLower, lockHandle, xml, { 'Development-Class': pkg });
|
|
78
|
+
await client.unlock('area', nameLower);
|
|
79
|
+
return JSON.stringify({
|
|
80
|
+
success: true,
|
|
81
|
+
name: nameUpper,
|
|
82
|
+
parentInfoArea: parentUpper || null,
|
|
83
|
+
description: desc,
|
|
84
|
+
package: pkg,
|
|
85
|
+
message: `InfoArea '${nameUpper}' created and active.`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// ── bwGetInfoarea ─────────────────────────────────────────────────────────────
|
|
89
|
+
/**
|
|
90
|
+
* bw_get_infoarea — read an InfoArea definition.
|
|
91
|
+
*
|
|
92
|
+
* GET /sap/bw/modeling/area/{name}
|
|
93
|
+
*/
|
|
94
|
+
export async function bwGetInfoarea(client, name) {
|
|
95
|
+
const nameLower = name.toLowerCase();
|
|
96
|
+
const result = await client.get(`/sap/bw/modeling/area/${nameLower}`, MEDIA_TYPES['area']);
|
|
97
|
+
const body = result.body;
|
|
98
|
+
try {
|
|
99
|
+
const parsed = JSON.parse(body);
|
|
100
|
+
const infoAreaName = parsed['name'] ?? name.toUpperCase();
|
|
101
|
+
const label = parsed['endUserTexts']?.['label'] ??
|
|
102
|
+
parsed['descriptions']?.['label'] ??
|
|
103
|
+
parsed['label'] ??
|
|
104
|
+
'';
|
|
105
|
+
const parentArea = parsed['tlogoProperties']?.['infoArea'] ??
|
|
106
|
+
parsed['parentInfoArea'] ??
|
|
107
|
+
null;
|
|
108
|
+
const objectStatus = parsed['tlogoProperties']?.['adtcore:version'] ??
|
|
109
|
+
parsed['tlogoProperties']?.['objectStatus'] ??
|
|
110
|
+
parsed['objectStatus'] ??
|
|
111
|
+
'';
|
|
112
|
+
return JSON.stringify({ name: infoAreaName, label, parent_area: parentArea || null, object_status: objectStatus }, null, 2);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
return JSON.stringify({ raw: body });
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
import { MEDIA_TYPES, createClientFromEnv } from '../bw-client.js';
|
|
2
|
+
// ── KYF: objectSpecificDataType → keyfigureType / semantics ──────────────────
|
|
3
|
+
const KYF_TYPE_MAP = {
|
|
4
|
+
DEC: { keyfigureType: 'NUM', semantics: 'NUM' },
|
|
5
|
+
CURR: { keyfigureType: 'AMT', semantics: 'AMT' },
|
|
6
|
+
FLTP: { keyfigureType: 'NUM', semantics: 'NUM' },
|
|
7
|
+
QUAN: { keyfigureType: 'QUA', semantics: 'QUA' },
|
|
8
|
+
DATS: { keyfigureType: 'DAT', semantics: 'DAT' },
|
|
9
|
+
INT4: { keyfigureType: 'INT', semantics: 'INT4' },
|
|
10
|
+
INT8: { keyfigureType: 'INT', semantics: 'INT8' },
|
|
11
|
+
TIMS: { keyfigureType: 'NUM', semantics: 'NUM' },
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* bw_create_infoobject — create a new InfoObject (CHA or KYF, inactive).
|
|
15
|
+
*
|
|
16
|
+
* KYF flow (POST body accepted by API — no PUT needed):
|
|
17
|
+
* 1. Lock (CREA, stateful_enqueue) → lockHandle
|
|
18
|
+
* 2. Create-POST with full KYF body → object created with correct values
|
|
19
|
+
* 3. Unlock
|
|
20
|
+
* 4. Return lockHandle="" (KYF activation requires no lock)
|
|
21
|
+
*
|
|
22
|
+
* CHA flow (POST body ignored — PUT required):
|
|
23
|
+
* 1. Lock (CREA, stateful_enqueue) → lockHandle
|
|
24
|
+
* 2. Create-POST (minimal body) → object created with server defaults
|
|
25
|
+
* 3. GET /iobj/{name}/m → server-enriched XML + timestamp header
|
|
26
|
+
* 4. Lock (normal, no CREA) → same lockHandle returned (CREA lock still active)
|
|
27
|
+
* 5. PUT /iobj/{name}/m → GET response with desired values substituted
|
|
28
|
+
* 6. Return lockHandle for caller to use with bw_activate
|
|
29
|
+
*/
|
|
30
|
+
export async function bwCreateInfoObject(client, args) {
|
|
31
|
+
const isobjType = (args.infoobject_type ?? 'CHA').toUpperCase();
|
|
32
|
+
const nameLower = args.name.toLowerCase();
|
|
33
|
+
const nameUpper = args.name.toUpperCase();
|
|
34
|
+
const pkg = args.package ?? '$TMP';
|
|
35
|
+
const desc = args.description;
|
|
36
|
+
const infoArea = args.info_area.toUpperCase();
|
|
37
|
+
// Step 1: Lock with CREA headers (stateful_enqueue session)
|
|
38
|
+
const lockHandle = await client.lock('iobj', nameLower, {
|
|
39
|
+
activity_context: 'CREA',
|
|
40
|
+
parent_name: infoArea,
|
|
41
|
+
parent_type: 'AREA',
|
|
42
|
+
}, 'stateful_enqueue');
|
|
43
|
+
// ── KYF: POST body accepted — fixedUnit/fixedCurrency via GET+PUT ────────────
|
|
44
|
+
if (isobjType === 'KYF') {
|
|
45
|
+
const ost = (args.object_specific_data_type ?? 'DEC').toUpperCase();
|
|
46
|
+
const mapped = KYF_TYPE_MAP[ost];
|
|
47
|
+
const keyfigureType = mapped?.keyfigureType ?? ost;
|
|
48
|
+
const semantics = mapped?.semantics ?? ost;
|
|
49
|
+
const aggregationType = (args.aggregation_type ?? 'SUM').toUpperCase();
|
|
50
|
+
// SAP ignores fixedUnit/fixedCurrency in the POST body — omit them here
|
|
51
|
+
const language = process.env.BW_LANGUAGE ?? 'DE';
|
|
52
|
+
const kyfXml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
53
|
+
<InfoObject:infoObject
|
|
54
|
+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
55
|
+
xmlns:InfoObject="http://www.sap.com/bw/modeling/BwIobj.ecore"
|
|
56
|
+
xmlns:adtcore="http://www.sap.com/adt/core"
|
|
57
|
+
xsi:type="InfoObject:Keyfigure"
|
|
58
|
+
name="${nameUpper}"
|
|
59
|
+
shortDescriptionSet="false"
|
|
60
|
+
keyFigureSemantic="_"
|
|
61
|
+
keyfigureType="${keyfigureType}"
|
|
62
|
+
objectSpecificDataType="${ost}">
|
|
63
|
+
<infoObjectType>KYF</infoObjectType>
|
|
64
|
+
<dataElement/>
|
|
65
|
+
<longDescription>${desc}</longDescription>
|
|
66
|
+
<shortDescription>${desc}</shortDescription>
|
|
67
|
+
<tlogoProperties adtcore:language="${language}" adtcore:name="${nameUpper}" adtcore:type="IOBJ"
|
|
68
|
+
adtcore:masterLanguage="${language}" adtcore:responsible="${process.env.BW_USER}">
|
|
69
|
+
<infoArea>${infoArea}</infoArea>
|
|
70
|
+
</tlogoProperties>
|
|
71
|
+
<referencedInfoObject/>
|
|
72
|
+
<semantics>${semantics}</semantics>
|
|
73
|
+
<aggregationType>${aggregationType}</aggregationType>
|
|
74
|
+
<exceptionAggregation>
|
|
75
|
+
<referencedCharacteristic/>
|
|
76
|
+
</exceptionAggregation>
|
|
77
|
+
<displayProperties/>
|
|
78
|
+
<inventoryContext/>
|
|
79
|
+
<elimination/>
|
|
80
|
+
<stockCoverageProperties calculationType="B">
|
|
81
|
+
<referencedStockKeyfigure/>
|
|
82
|
+
<referencedDemandKeyfigure/>
|
|
83
|
+
<timeGranularityOfStockCoverage/>
|
|
84
|
+
</stockCoverageProperties>
|
|
85
|
+
</InfoObject:infoObject>`;
|
|
86
|
+
await client.create('iobj', nameLower, lockHandle, kyfXml, { 'Development-Class': pkg });
|
|
87
|
+
await client.unlock('iobj', nameLower);
|
|
88
|
+
// fixedUnit / fixedCurrency: SAP only accepts these via PUT after creation
|
|
89
|
+
if (args.fixed_unit || args.fixed_currency) {
|
|
90
|
+
const freshClient = createClientFromEnv();
|
|
91
|
+
const getResult = await freshClient.get(`/sap/bw/modeling/iobj/${nameLower}/m`, MEDIA_TYPES['iobj']);
|
|
92
|
+
const timestamp = getResult.headers['timestamp'] ?? getResult.headers['TIMESTAMP'];
|
|
93
|
+
let xml = getResult.body;
|
|
94
|
+
const putLockHandle = await client.lock('iobj', nameLower, undefined, 'stateful_enqueue');
|
|
95
|
+
if (args.fixed_unit) {
|
|
96
|
+
const unit = args.fixed_unit.toUpperCase();
|
|
97
|
+
xml = xml.replace('<unitCurrencyInfoObjectRef', `<fixedUnit>${unit}</fixedUnit><unitCurrencyInfoObjectRef`);
|
|
98
|
+
}
|
|
99
|
+
if (args.fixed_currency) {
|
|
100
|
+
const cur = args.fixed_currency.toUpperCase();
|
|
101
|
+
xml = xml.replace('<unitCurrencyInfoObjectRef', `<fixedCurrency>${cur}</fixedCurrency><unitCurrencyInfoObjectRef`);
|
|
102
|
+
}
|
|
103
|
+
await freshClient.put('iobj', nameLower, putLockHandle, xml, timestamp, args.transport);
|
|
104
|
+
await client.unlock('iobj', nameLower);
|
|
105
|
+
}
|
|
106
|
+
const kyfResult = {
|
|
107
|
+
success: true,
|
|
108
|
+
infoobjectType: 'KYF',
|
|
109
|
+
name: nameUpper,
|
|
110
|
+
infoArea: args.info_area,
|
|
111
|
+
description: desc,
|
|
112
|
+
package: pkg,
|
|
113
|
+
lockHandle: '',
|
|
114
|
+
objectSpecificDataType: ost,
|
|
115
|
+
keyfigureType,
|
|
116
|
+
semantics,
|
|
117
|
+
aggregationType,
|
|
118
|
+
};
|
|
119
|
+
if (args.fixed_unit)
|
|
120
|
+
kyfResult['fixedUnit'] = args.fixed_unit.toUpperCase();
|
|
121
|
+
if (args.fixed_currency)
|
|
122
|
+
kyfResult['fixedCurrency'] = args.fixed_currency.toUpperCase();
|
|
123
|
+
kyfResult['message'] = `InfoObject '${nameUpper}' created (inactive). Call bw_activate with lock_handle="" to activate.`;
|
|
124
|
+
return JSON.stringify(kyfResult);
|
|
125
|
+
}
|
|
126
|
+
// ── CHA: POST body ignored — GET + PUT required ────────────────────────────
|
|
127
|
+
// Step 2: Create-POST — API ignores body, creates object with defaults
|
|
128
|
+
const minimalXml = `<?xml version="1.0" encoding="UTF-8"?><iobj:infoObject xmlns:iobj="http://www.sap.com/bw/modeling/BwIobj.ecore" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="iobj:Characteristic" name="${nameUpper}"/>`;
|
|
129
|
+
await client.create('iobj', nameLower, lockHandle, minimalXml, { 'Development-Class': pkg });
|
|
130
|
+
// Step 3: GET server-created object (defaults) to obtain enriched XML + timestamp
|
|
131
|
+
const getResult = await client.get(`/sap/bw/modeling/iobj/${nameLower}/m`, MEDIA_TYPES['iobj']);
|
|
132
|
+
const timestamp = getResult.headers['timestamp'] ?? getResult.headers['TIMESTAMP'];
|
|
133
|
+
// Step 4: Lock again (stateful_enqueue) — same session, server returns same lockHandle
|
|
134
|
+
await client.lock('iobj', nameLower, undefined, 'stateful_enqueue');
|
|
135
|
+
// Step 5: Build PUT body — substitute desired values into the GET response
|
|
136
|
+
let putXml = getResult.body;
|
|
137
|
+
// Remove fieldName attribute — it tells SAP the DDIC field already exists with the old
|
|
138
|
+
// type/length, causing SAP to ignore dataType/length changes in the PUT body.
|
|
139
|
+
putXml = putXml.replace(/\s+fieldName="[^"]*"/, '');
|
|
140
|
+
// Normalise masterDataAccess — GET returns type="GEN" but PUT expects explicit empty attrs
|
|
141
|
+
putXml = putXml.replace(/<masterDataAccess[^/]*\/>/, '<masterDataAccess readClass="" sapHanaPackage="" sapHanaView=""/>');
|
|
142
|
+
const chaDataType = (args.data_type ?? 'CHAR').toUpperCase();
|
|
143
|
+
const chaLength = args.length ?? 10;
|
|
144
|
+
const defaultConv = (chaDataType === 'CHAR' || chaDataType === 'NUMC') ? 'ALPHA' : '';
|
|
145
|
+
const chaConv = args.conversion_routine ?? defaultConv;
|
|
146
|
+
const withMasterData = args.with_master_data ?? false;
|
|
147
|
+
const withTexts = args.with_texts ?? false;
|
|
148
|
+
// Root element attributes
|
|
149
|
+
putXml = putXml.replace(/\bobjectSpecificDataType="[^"]*"/, `objectSpecificDataType="${chaDataType}"`);
|
|
150
|
+
putXml = putXml.replace(/\boutputLength="[^"]*"/, `outputLength="${chaLength}"`);
|
|
151
|
+
if (/\bconversionRoutine="/.test(putXml)) {
|
|
152
|
+
putXml = putXml.replace(/\bconversionRoutine="[^"]*"/, `conversionRoutine="${chaConv}"`);
|
|
153
|
+
}
|
|
154
|
+
else if (chaConv) {
|
|
155
|
+
// Add conversionRoutine attribute to root element if not present
|
|
156
|
+
putXml = putXml.replace(/(<iobj:infoObject\b)/, `$1 conversionRoutine="${chaConv}"`);
|
|
157
|
+
}
|
|
158
|
+
// CHA child elements
|
|
159
|
+
putXml = putXml.replace(/<dataType>[^<]*<\/dataType>/, `<dataType>${chaDataType}</dataType>`);
|
|
160
|
+
putXml = putXml.replace(/<length>[^<]*<\/length>/, `<length>${chaLength}</length>`);
|
|
161
|
+
// masterDataProperties — GET response has NO withMasterData attribute; PUT must add it
|
|
162
|
+
putXml = putXml.replace(/<masterDataProperties(\s[^>]*)?>/, `<masterDataProperties withMasterData="${withMasterData}">`);
|
|
163
|
+
// textProperties — set withTexts / shortTextAvailable / languageDependentTextAvailable
|
|
164
|
+
if (withTexts) {
|
|
165
|
+
putXml = putXml.replace(/<textProperties(\s[^>]*)?>/, `<textProperties shortTextAvailable="true" withTexts="true">`);
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
putXml = putXml.replace(/<textProperties(\s[^>]*)?>/, `<textProperties languageDependentTextAvailable="false" shortTextAvailable="false" withTexts="false">`);
|
|
169
|
+
}
|
|
170
|
+
// Common substitutions — descriptions, InfoArea, texts element, hana mapping
|
|
171
|
+
// SAP XML texts use 1-letter language codes (D, E, F…); BW_LANGUAGE may be ISO (DE, EN…)
|
|
172
|
+
const ISO_TO_SAP = {
|
|
173
|
+
DE: 'D', EN: 'E', FR: 'F', IT: 'I', ES: 'S', NL: 'N', PT: 'P',
|
|
174
|
+
RU: 'R', JA: 'J', KO: 'K', ZH: '1', PL: 'L', CS: 'C',
|
|
175
|
+
};
|
|
176
|
+
const bwLangRaw = process.env.BW_LANGUAGE;
|
|
177
|
+
const bwLangSap = bwLangRaw
|
|
178
|
+
? (ISO_TO_SAP[bwLangRaw.toUpperCase()] ?? bwLangRaw)
|
|
179
|
+
: undefined;
|
|
180
|
+
putXml = putXml.replace(/<longDescription>[^<]*<\/longDescription>/, `<longDescription>${desc}</longDescription>`);
|
|
181
|
+
putXml = putXml.replace(/<shortDescription>[^<]*<\/shortDescription>/, `<shortDescription>${desc}</shortDescription>`);
|
|
182
|
+
putXml = putXml.replace(/(<texts\b[^/]*?)longText="[^"]*"/, `$1longText="${desc}"`);
|
|
183
|
+
putXml = putXml.replace(/(<texts\b[^/]*?)shortText="[^"]*"/, `$1shortText="${desc}"`);
|
|
184
|
+
if (bwLangSap) {
|
|
185
|
+
putXml = putXml.replace(/(<texts\b[^/]*?)language="[^"]*"/, `$1language="${bwLangSap}"`);
|
|
186
|
+
}
|
|
187
|
+
putXml = putXml.replace(/\badtcore:description="[^"]*"/, `adtcore:description="${desc}"`);
|
|
188
|
+
putXml = putXml.replace(/<infoArea>[^<]*<\/infoArea>/, `<infoArea>${infoArea}</infoArea>`);
|
|
189
|
+
putXml = putXml.replace(/(<sourceField\b[^/]*?)\bdescription="[^"]*"/, `$1description="${desc}"`);
|
|
190
|
+
putXml = putXml.replace(/(<sourceField\b[^/]*?)\bshortDescription="[^"]*"/, `$1shortDescription="${desc}"`);
|
|
191
|
+
// Step 6: Insert compoundParent elements if compound_infoobjects is provided (CHA only)
|
|
192
|
+
if (args.compound_infoobjects && args.compound_infoobjects.length > 0) {
|
|
193
|
+
const freshClient = createClientFromEnv();
|
|
194
|
+
const compoundParentElements = [];
|
|
195
|
+
for (const parent of args.compound_infoobjects) {
|
|
196
|
+
const parentLower = parent.toLowerCase();
|
|
197
|
+
const parentUpper = parent.toUpperCase();
|
|
198
|
+
const parentGet = await freshClient.get(`/sap/bw/modeling/iobj/${parentLower}/a`, MEDIA_TYPES['iobj']);
|
|
199
|
+
const parentXml = parentGet.body;
|
|
200
|
+
const parentProps = parseInfoObjectProps(parentXml);
|
|
201
|
+
const parentLongDescMatch = parentXml.match(/<longDescription>([^<]+)<\/longDescription>/);
|
|
202
|
+
const parentDescription = parentLongDescMatch?.[1] ?? parentProps.label;
|
|
203
|
+
compoundParentElements.push(`<compoundParent` +
|
|
204
|
+
` description="${parentDescription}"` +
|
|
205
|
+
` infoObjectType="CHA"` +
|
|
206
|
+
` name="${parentUpper}"` +
|
|
207
|
+
` ref="../../${parentLower}/a/model.iobj#//"` +
|
|
208
|
+
` dataType="${parentProps.dataType}"` +
|
|
209
|
+
` length="${parentProps.length}">` +
|
|
210
|
+
`<referencedCharacteristic xsi:type="iobj:ReferencedInfoObject" description=""/>` +
|
|
211
|
+
`</compoundParent>`);
|
|
212
|
+
}
|
|
213
|
+
const compoundParentXml = compoundParentElements.join('\n') + '\n';
|
|
214
|
+
const insertionPoints = [
|
|
215
|
+
'<externalSAPHANAView',
|
|
216
|
+
'<runtimeProperties',
|
|
217
|
+
'<sidTable',
|
|
218
|
+
'</iobj:infoObject>',
|
|
219
|
+
];
|
|
220
|
+
let inserted = false;
|
|
221
|
+
for (const point of insertionPoints) {
|
|
222
|
+
if (putXml.includes(point)) {
|
|
223
|
+
putXml = putXml.replace(point, compoundParentXml + point);
|
|
224
|
+
inserted = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (!inserted) {
|
|
229
|
+
throw new Error('Could not find insertion point for compoundParent elements in PUT XML');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Step 7: PUT — applies the correct values
|
|
233
|
+
await client.put('iobj', nameLower, lockHandle, putXml, timestamp, args.transport);
|
|
234
|
+
const resultExtra = {
|
|
235
|
+
dataType: chaDataType,
|
|
236
|
+
length: chaLength,
|
|
237
|
+
conversionRoutine: chaConv,
|
|
238
|
+
withMasterData,
|
|
239
|
+
withTexts,
|
|
240
|
+
};
|
|
241
|
+
if (args.referenced_infoobject)
|
|
242
|
+
resultExtra['referencedInfoObject'] = args.referenced_infoobject.toUpperCase();
|
|
243
|
+
if (args.compound_infoobjects?.length)
|
|
244
|
+
resultExtra['compoundParents'] = args.compound_infoobjects.map(p => p.toUpperCase());
|
|
245
|
+
return JSON.stringify({
|
|
246
|
+
success: true,
|
|
247
|
+
infoobjectType: 'CHA',
|
|
248
|
+
name: nameUpper,
|
|
249
|
+
infoArea: args.info_area,
|
|
250
|
+
description: desc,
|
|
251
|
+
package: pkg,
|
|
252
|
+
lockHandle,
|
|
253
|
+
...resultExtra,
|
|
254
|
+
message: `InfoObject '${nameUpper}' created (inactive). Call bw_activate with lock_handle="${lockHandle}" to activate.`,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Parse key properties from an InfoObject XML response.
|
|
259
|
+
* Used by bw_update_adso and bw_update_transformation.
|
|
260
|
+
*/
|
|
261
|
+
export function parseInfoObjectProps(xml) {
|
|
262
|
+
const convMatch = xml.match(/conversionRoutine="([^"]+)"/);
|
|
263
|
+
const shortDescMatch = xml.match(/<shortDescription>([^<]+)<\/shortDescription>/);
|
|
264
|
+
const dataTypeMatch = xml.match(/<dataType>([^<]+)<\/dataType>/);
|
|
265
|
+
const lengthMatch = xml.match(/<length>([^<]+)<\/length>/);
|
|
266
|
+
return {
|
|
267
|
+
conversionRoutine: convMatch?.[1] ?? '',
|
|
268
|
+
label: shortDescMatch?.[1] ?? '',
|
|
269
|
+
dataType: dataTypeMatch?.[1] ?? 'CHAR',
|
|
270
|
+
length: lengthMatch?.[1] ?? '20',
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* bw_update_infoobject — replace the attribute list of a Characteristic InfoObject.
|
|
275
|
+
*
|
|
276
|
+
* Flow:
|
|
277
|
+
* 1. Lock (update — no activity_context)
|
|
278
|
+
* 2. GET /iobj/{name}/m → current XML + timestamp
|
|
279
|
+
* 3. For each attribute: GET /iobj/{attr}/a → description, shortDescription, dataType, length
|
|
280
|
+
* 4. Remove all existing <attributeN .../> elements
|
|
281
|
+
* 5. Remove existing <hanaAttributeMapping type="02"> block
|
|
282
|
+
* 6. Insert new <attributeN> elements before <externalSAPHANAView>
|
|
283
|
+
* 7. If attributes present: insert <hanaAttributeMapping type="02"> before type="03"
|
|
284
|
+
* 8. PUT full XML with timestamp
|
|
285
|
+
* 9. Activate + Unlock (unlock in finally)
|
|
286
|
+
*/
|
|
287
|
+
export async function bwUpdateInfoObject(client, args) {
|
|
288
|
+
const nameLower = args.name.toLowerCase();
|
|
289
|
+
const nameUpper = args.name.toUpperCase();
|
|
290
|
+
const attributes = args.attributes ?? [];
|
|
291
|
+
// Step 1: Lock with the original client (holds the lock session)
|
|
292
|
+
const lockHandle = await client.lock('iobj', nameLower, undefined, 'stateful_enqueue');
|
|
293
|
+
try {
|
|
294
|
+
// Step 2: GET current XML — fresh session to avoid SAP session state pollution
|
|
295
|
+
const freshClient = createClientFromEnv();
|
|
296
|
+
const getResult = await freshClient.get(`/sap/bw/modeling/iobj/${nameLower}/m`, MEDIA_TYPES['iobj']);
|
|
297
|
+
const timestamp = getResult.headers['timestamp'] ?? getResult.headers['TIMESTAMP'];
|
|
298
|
+
let xml = getResult.body;
|
|
299
|
+
// ── KYF fast path: only patch fixedUnit / fixedCurrency / description ─────
|
|
300
|
+
const isKyf = /<infoObjectType>KYF<\/infoObjectType>/.test(xml);
|
|
301
|
+
if (isKyf) {
|
|
302
|
+
if (args.description) {
|
|
303
|
+
const desc = args.description;
|
|
304
|
+
xml = xml.replace(/<longDescription>[^<]*<\/longDescription>/, `<longDescription>${desc}</longDescription>`);
|
|
305
|
+
xml = xml.replace(/<shortDescription>[^<]*<\/shortDescription>/, `<shortDescription>${desc}</shortDescription>`);
|
|
306
|
+
xml = xml.replace(/(<texts\b[^/]*?)longText="[^"]*"/, `$1longText="${desc}"`);
|
|
307
|
+
xml = xml.replace(/(<texts\b[^/]*?)shortText="[^"]*"/, `$1shortText="${desc}"`);
|
|
308
|
+
xml = xml.replace(/\badtcore:description="[^"]*"/, `adtcore:description="${desc}"`);
|
|
309
|
+
}
|
|
310
|
+
if (args.fixed_unit) {
|
|
311
|
+
const unit = args.fixed_unit.toUpperCase();
|
|
312
|
+
if (/<fixedUnit>/.test(xml)) {
|
|
313
|
+
xml = xml.replace(/<fixedUnit>[^<]*<\/fixedUnit>/, `<fixedUnit>${unit}</fixedUnit>`);
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
xml = xml.replace('<unitCurrencyInfoObjectRef', `<fixedUnit>${unit}</fixedUnit><unitCurrencyInfoObjectRef`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (args.fixed_currency) {
|
|
320
|
+
const cur = args.fixed_currency.toUpperCase();
|
|
321
|
+
if (/<fixedCurrency>/.test(xml)) {
|
|
322
|
+
xml = xml.replace(/<fixedCurrency>[^<]*<\/fixedCurrency>/, `<fixedCurrency>${cur}</fixedCurrency>`);
|
|
323
|
+
}
|
|
324
|
+
else {
|
|
325
|
+
xml = xml.replace('<unitCurrencyInfoObjectRef', `<fixedCurrency>${cur}</fixedCurrency><unitCurrencyInfoObjectRef`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
await freshClient.put('iobj', nameLower, lockHandle, xml, timestamp, args.transport);
|
|
329
|
+
const activationClient = createClientFromEnv();
|
|
330
|
+
await activationClient.activate('iobj', nameLower, lockHandle);
|
|
331
|
+
return JSON.stringify({
|
|
332
|
+
success: true,
|
|
333
|
+
name: nameUpper,
|
|
334
|
+
message: `InfoObject '${nameUpper}' (KYF) updated and activated.`,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
// Step 3: Fetch referenced InfoObject properties for each attribute
|
|
338
|
+
const attrXmlParts = [];
|
|
339
|
+
const sourceFieldParts = [];
|
|
340
|
+
for (const attr of attributes) {
|
|
341
|
+
const attrUpper = attr.name.toUpperCase();
|
|
342
|
+
const attrLower = attr.name.toLowerCase();
|
|
343
|
+
const attrGet = await freshClient.get(`/sap/bw/modeling/iobj/${attrLower}/a`, MEDIA_TYPES['iobj']);
|
|
344
|
+
const attrXml = attrGet.body;
|
|
345
|
+
const props = parseInfoObjectProps(attrXml);
|
|
346
|
+
const longDescMatch = attrXml.match(/<longDescription>([^<]+)<\/longDescription>/);
|
|
347
|
+
const description = longDescMatch?.[1] ?? props.label;
|
|
348
|
+
const shortDescription = props.label;
|
|
349
|
+
const displayInQuery = attr.displayInQuery ?? true;
|
|
350
|
+
const useTextOfOrig = attr.useTextOfOriginalCharacteristic ?? true;
|
|
351
|
+
const ref = `../../${attrLower}/a/model.iobj#//`;
|
|
352
|
+
if (attr.type === 'NAV') {
|
|
353
|
+
const timeDependent = attr.timeDependent ?? false;
|
|
354
|
+
attrXmlParts.push(`<attributeN` +
|
|
355
|
+
` description="${description}"` +
|
|
356
|
+
` infoObjectType="CHA"` +
|
|
357
|
+
` name="${attrUpper}"` +
|
|
358
|
+
` ref="${ref}"` +
|
|
359
|
+
` shortDescription="${shortDescription}"` +
|
|
360
|
+
` calculationScenarioNavigationAttribute="true"` +
|
|
361
|
+
` dataType="${props.dataType}"` +
|
|
362
|
+
` displayInQuery="${displayInQuery}"` +
|
|
363
|
+
` f4HelpOrder="0"` +
|
|
364
|
+
` length="${props.length}"` +
|
|
365
|
+
(timeDependent ? ` timeDependent="true"` : '') +
|
|
366
|
+
` type="NAV"` +
|
|
367
|
+
` useTextOfOriginalCharacteristic="${useTextOfOrig}"/>`);
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
attrXmlParts.push(`<attributeN` +
|
|
371
|
+
` description="${description}"` +
|
|
372
|
+
` infoObjectType="CHA"` +
|
|
373
|
+
` name="${attrUpper}"` +
|
|
374
|
+
` ref="${ref}"` +
|
|
375
|
+
` shortDescription="${shortDescription}"` +
|
|
376
|
+
` dataType="${props.dataType}"` +
|
|
377
|
+
` displayInQuery="${displayInQuery}"` +
|
|
378
|
+
` f4HelpOrder="0"` +
|
|
379
|
+
` hasAttributes="false"` +
|
|
380
|
+
` length="${props.length}"` +
|
|
381
|
+
` partOfIndex="false"` +
|
|
382
|
+
` sidKeyFigure="false"` +
|
|
383
|
+
` type="DIS"` +
|
|
384
|
+
` useTextOfOriginalCharacteristic="${useTextOfOrig}"/>`);
|
|
385
|
+
}
|
|
386
|
+
sourceFieldParts.push(`<sourceField description="${description}" infoObjectType="CHA"` +
|
|
387
|
+
` name="${attrUpper}" shortDescription="${shortDescription}"/>`);
|
|
388
|
+
}
|
|
389
|
+
// Step 4: Remove all existing <attributeN .../> self-closing elements
|
|
390
|
+
xml = xml.replace(/\s*<attributeN\b[^>]*\/>/g, '');
|
|
391
|
+
// Step 5: Remove existing hanaAttributeMapping type="02" block — only when rebuilding
|
|
392
|
+
// with new attributes. When removing all attributes (empty list), SAP cleans up
|
|
393
|
+
// hanaAttributeMapping type="02" automatically on activation.
|
|
394
|
+
if (attrXmlParts.length > 0) {
|
|
395
|
+
xml = xml.replace(/\s*<hanaAttributeMapping type="02">[\s\S]*?<\/hanaAttributeMapping>/g, '');
|
|
396
|
+
}
|
|
397
|
+
// Step 6: Insert new attributeN elements before <runtimeProperties
|
|
398
|
+
if (attrXmlParts.length > 0) {
|
|
399
|
+
const attrBlock = attrXmlParts.join('\n') + '\n';
|
|
400
|
+
xml = xml.replace(/(<runtimeProperties)/, attrBlock + '$1');
|
|
401
|
+
}
|
|
402
|
+
// Step 7: Insert hanaAttributeMapping type="02" before type="03" if attributes present
|
|
403
|
+
if (sourceFieldParts.length > 0) {
|
|
404
|
+
const hanaMappingBlock = `<hanaAttributeMapping type="02">\n` +
|
|
405
|
+
sourceFieldParts.join('\n') + '\n' +
|
|
406
|
+
`</hanaAttributeMapping>\n`;
|
|
407
|
+
xml = xml.replace('<hanaAttributeMapping type="03">', hanaMappingBlock + '<hanaAttributeMapping type="03">');
|
|
408
|
+
}
|
|
409
|
+
// Patch description if provided
|
|
410
|
+
if (args.description) {
|
|
411
|
+
const desc = args.description;
|
|
412
|
+
xml = xml.replace(/<longDescription>[^<]*<\/longDescription>/, `<longDescription>${desc}</longDescription>`);
|
|
413
|
+
xml = xml.replace(/<shortDescription>[^<]*<\/shortDescription>/, `<shortDescription>${desc}</shortDescription>`);
|
|
414
|
+
xml = xml.replace(/(<texts\b[^/]*?)longText="[^"]*"/, `$1longText="${desc}"`);
|
|
415
|
+
xml = xml.replace(/(<texts\b[^/]*?)shortText="[^"]*"/, `$1shortText="${desc}"`);
|
|
416
|
+
xml = xml.replace(/\badtcore:description="[^"]*"/, `adtcore:description="${desc}"`);
|
|
417
|
+
}
|
|
418
|
+
// Step 8: PUT full XML — same fresh session as GET
|
|
419
|
+
await freshClient.put('iobj', nameLower, lockHandle, xml, timestamp, args.transport);
|
|
420
|
+
// Step 9: Activate — another fresh session (mirrors TRFN pattern)
|
|
421
|
+
const activationClient = createClientFromEnv();
|
|
422
|
+
await activationClient.activate('iobj', nameLower, lockHandle);
|
|
423
|
+
}
|
|
424
|
+
finally {
|
|
425
|
+
// Unlock with the original lock-session client
|
|
426
|
+
await client.unlock('iobj', nameLower).catch(() => undefined);
|
|
427
|
+
}
|
|
428
|
+
return JSON.stringify({
|
|
429
|
+
success: true,
|
|
430
|
+
name: nameUpper,
|
|
431
|
+
attributeCount: attributes.length,
|
|
432
|
+
attributes: attributes.map((a) => ({ name: a.name.toUpperCase(), type: a.type })),
|
|
433
|
+
message: `InfoObject '${nameUpper}' updated and activated with ${attributes.length} attribute(s).`,
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
437
|
+
/**
|
|
438
|
+
* bw_get_infoobject — read an InfoObject definition (inactive version).
|
|
439
|
+
* Returns raw XML + object status from response headers.
|
|
440
|
+
*/
|
|
441
|
+
export async function bwGetInfoObject(client, infoObjectName) {
|
|
442
|
+
const accept = MEDIA_TYPES['iobj'];
|
|
443
|
+
const path = `/sap/bw/modeling/iobj/${infoObjectName.toLowerCase()}/m`;
|
|
444
|
+
const result = await client.get(path, accept);
|
|
445
|
+
const status = result.headers['object_status'] ?? result.headers['OBJECT_STATUS'] ?? 'unknown';
|
|
446
|
+
return `InfoObject: ${infoObjectName.toUpperCase()}\nStatus: ${status}\n\n${result.body}`;
|
|
447
|
+
}
|