@mseep/bw-modeling-mcp 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,171 @@
1
+ import { MEDIA_TYPES, createClientFromEnv } from '../bw-client.js';
2
+ /**
3
+ * Parse all <atom:title> entries from an activation/atom feed response.
4
+ * Used to extract success messages and deactivated DTP names.
5
+ */
6
+ export function parseActivationMessages(xml) {
7
+ const messages = [];
8
+ const regex = /<atom:title>([^<]+)<\/atom:title>/g;
9
+ let match;
10
+ while ((match = regex.exec(xml)) !== null) {
11
+ messages.push(match[1]);
12
+ }
13
+ return messages;
14
+ }
15
+ /**
16
+ * Parse DTP names that were deactivated by impact analysis from activation response.
17
+ * Matches both German and English SAP system messages containing a DTP name and a deactivation keyword.
18
+ */
19
+ export function parseDtpsDeactivated(xml) {
20
+ const dtps = [];
21
+ const messages = parseActivationMessages(xml);
22
+ for (const msg of messages) {
23
+ // Match DTP name pattern in German or English activation messages
24
+ const dtpMatch = msg.match(/\b(DTP_[A-Z0-9]+)\b/i);
25
+ if (dtpMatch && (msg.toLowerCase().includes('deaktiv') || msg.toLowerCase().includes('deactiv'))) {
26
+ dtps.push(dtpMatch[1].toUpperCase());
27
+ }
28
+ }
29
+ return dtps;
30
+ }
31
+ /**
32
+ * bw_activate — activate one BW object (aDSO, Transformation, or DTP).
33
+ *
34
+ * Sequence:
35
+ * 1. POST /sap/bw/modeling/activation (with lockHandle in body)
36
+ * 2. POST ?action=unlock (skipped for DTP — no unlock needed)
37
+ *
38
+ * For DTP activation pass lock_handle="" (empty string).
39
+ * The lockHandle is obtained from bw_update_adso or bw_update_transformation.
40
+ *
41
+ * Returns all messages from the activation response, including any DTPs
42
+ * that were deactivated by impact analysis (these must be re-activated with bw_activate).
43
+ */
44
+ export async function bwActivate(client, objectType, objectName, lockHandle, corrNr, sourceSystem) {
45
+ const typeLower = objectType.toLowerCase();
46
+ // Validate object type
47
+ if (!['adso', 'trfn', 'dtpa', 'iobj', 'trcs', 'rsds'].includes(typeLower)) {
48
+ return JSON.stringify({
49
+ success: false,
50
+ message: `Unknown object type: ${objectType}. Supported: adso, trfn, dtpa, iobj, trcs, rsds`,
51
+ });
52
+ }
53
+ // RSDS (DataSource) has a compound key — the source system is mandatory.
54
+ if (typeLower === 'rsds' && !sourceSystem) {
55
+ return JSON.stringify({
56
+ success: false,
57
+ message: `object_type "rsds" requires source_system (a DataSource is identified by DataSource name plus source system).`,
58
+ });
59
+ }
60
+ // Step 2: Activate
61
+ // trfn and dtpa activate in a fresh SAP session. The shared module-level client carries a
62
+ // stale session buffer that still sees the transformation as inactive (the state from DTP
63
+ // creation time); a fresh session reads the current DB state and avoids the false
64
+ // "transformation inactive" rejection, as well as cross-call session/CSRF collisions.
65
+ const activationClient = (typeLower === 'trfn' || typeLower === 'dtpa')
66
+ ? createClientFromEnv()
67
+ : client;
68
+ // Step 1: For trfn/dtpa, GET the object first to prime SAP's internal HANA cache refresh.
69
+ // This mirrors Eclipse's behavior of GETting before activating. For dtpa the priming GET must
70
+ // run in the SAME fresh session as the activation POST, otherwise the buffer it refreshes is
71
+ // not the one performing the activation. trfn keeps its historical throwaway-GET behavior.
72
+ if (typeLower === 'trfn' || typeLower === 'dtpa') {
73
+ const mediaKey = typeLower;
74
+ const primingClient = typeLower === 'dtpa' ? activationClient : createClientFromEnv();
75
+ await primingClient.get(`/sap/bw/modeling/${typeLower}/${objectName.toLowerCase()}/m`, MEDIA_TYPES[mediaKey]);
76
+ }
77
+ const activationXml = await activationClient.activate(typeLower, objectName, lockHandle, corrNr, sourceSystem);
78
+ // Step 3: Unlock (skipped for dtpa and rsds — both are standalone activations with no lock)
79
+ // Always use the original client session — BW locks are session-bound and can only
80
+ // be released by the session that acquired them. activationClient is a fresh session
81
+ // for trfn and would silently fail to release the lock.
82
+ if (lockHandle && typeLower !== 'dtpa' && typeLower !== 'rsds') {
83
+ await client.unlock(typeLower, objectName);
84
+ }
85
+ // Parse result messages
86
+ const messages = parseActivationMessages(activationXml);
87
+ const deactivatedDtps = parseDtpsDeactivated(activationXml);
88
+ // Check for errors in the response
89
+ const hasError = activationXml.includes('messageType="Error"') ||
90
+ activationXml.includes("messageType='Error'");
91
+ const hasWarning = activationXml.includes('messageType="Warning"') ||
92
+ activationXml.includes("messageType='Warning'");
93
+ // BW pattern: when a transformation contains a mapping rule for a field that no
94
+ // longer exists in the target aDSO, BW fails the first activation with an Error
95
+ // but simultaneously deletes the invalid rule. A single retry then succeeds.
96
+ // Detect this by looking for "is not valid and is being deleted" in the messages.
97
+ const hasDeletedRule = messages.some((m) => m.toLowerCase().includes('is not valid and is being deleted'));
98
+ if (hasError && hasDeletedRule && typeLower === 'trfn') {
99
+ const retryClient = createClientFromEnv();
100
+ const retryXml = await retryClient.activate(typeLower, objectName, lockHandle, corrNr);
101
+ if (lockHandle) {
102
+ await client.unlock(typeLower, objectName);
103
+ }
104
+ const retryMessages = parseActivationMessages(retryXml);
105
+ const retryDeactivatedDtps = parseDtpsDeactivated(retryXml);
106
+ const retryHasError = retryXml.includes('messageType="Error"') ||
107
+ retryXml.includes("messageType='Error'");
108
+ const retryHasWarning = retryXml.includes('messageType="Warning"') ||
109
+ retryXml.includes("messageType='Warning'");
110
+ const retryResult = {
111
+ success: !retryHasError,
112
+ object_type: objectType.toUpperCase(),
113
+ object_name: objectName.toUpperCase(),
114
+ messages: retryMessages,
115
+ retried: true,
116
+ };
117
+ if (retryHasWarning)
118
+ retryResult['warning'] = true;
119
+ if (retryDeactivatedDtps.length > 0) {
120
+ retryResult['dtps_deactivated_by_impact_analysis'] = retryDeactivatedDtps;
121
+ retryResult['next_step'] =
122
+ `Re-activate the deactivated DTPs using bw_activate with object_type="dtpa" and lock_handle="".`;
123
+ }
124
+ return JSON.stringify(retryResult, null, 2);
125
+ }
126
+ // BW pattern: when activating a trfn fails and a non-empty lockHandle was passed,
127
+ // retry once with an empty lockHandle after a short delay. This handles cases where
128
+ // the transformation was never explicitly locked by the caller (stale lockHandle).
129
+ if (hasError && typeLower === 'trfn' && lockHandle) {
130
+ await new Promise((resolve) => setTimeout(resolve, 1000));
131
+ const retryClient = createClientFromEnv();
132
+ const retryXml = await retryClient.activate(typeLower, objectName, '', corrNr);
133
+ // No unlock needed here — retrying with empty lockHandle means the object was not locked
134
+ const retryMessages = parseActivationMessages(retryXml);
135
+ const retryDeactivatedDtps = parseDtpsDeactivated(retryXml);
136
+ const retryHasError = retryXml.includes('messageType="Error"') ||
137
+ retryXml.includes("messageType='Error'");
138
+ const retryHasWarning = retryXml.includes('messageType="Warning"') ||
139
+ retryXml.includes("messageType='Warning'");
140
+ const retryResult = {
141
+ success: !retryHasError,
142
+ object_type: objectType.toUpperCase(),
143
+ object_name: objectName.toUpperCase(),
144
+ messages: retryMessages,
145
+ retried: true,
146
+ };
147
+ if (retryHasWarning)
148
+ retryResult['warning'] = true;
149
+ if (retryDeactivatedDtps.length > 0) {
150
+ retryResult['dtps_deactivated_by_impact_analysis'] = retryDeactivatedDtps;
151
+ retryResult['next_step'] =
152
+ `Re-activate the deactivated DTPs using bw_activate with object_type="dtpa" and lock_handle="".`;
153
+ }
154
+ return JSON.stringify(retryResult, null, 2);
155
+ }
156
+ const result = {
157
+ success: !hasError,
158
+ object_type: objectType.toUpperCase(),
159
+ object_name: objectName.toUpperCase(),
160
+ messages,
161
+ };
162
+ if (hasWarning) {
163
+ result['warning'] = true;
164
+ }
165
+ if (deactivatedDtps.length > 0) {
166
+ result['dtps_deactivated_by_impact_analysis'] = deactivatedDtps;
167
+ result['next_step'] =
168
+ `Re-activate the deactivated DTPs using bw_activate with object_type="dtpa" and lock_handle="".`;
169
+ }
170
+ return JSON.stringify(result, null, 2);
171
+ }