@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,536 @@
|
|
|
1
|
+
const BASE = '/sap/bw/modeling/repo/datasourcestructure';
|
|
2
|
+
const BASE_PREFIX = `${BASE}/`;
|
|
3
|
+
function parseEntries(xml) {
|
|
4
|
+
const entries = [];
|
|
5
|
+
const entryRegex = /<atom:entry\b[^>]*>([\s\S]*?)<\/atom:entry>/g;
|
|
6
|
+
let em;
|
|
7
|
+
while ((em = entryRegex.exec(xml)) !== null) {
|
|
8
|
+
const body = em[1];
|
|
9
|
+
const bwObjMatch = body.match(/<bwModel:object\b([\s\S]*?)(?:\/>|>)/);
|
|
10
|
+
const bwAttrs = bwObjMatch?.[1] ?? '';
|
|
11
|
+
const objectName = bwAttrs.match(/\bobjectName="([^"]*)"/)?.[1] ?? '';
|
|
12
|
+
const objectType = bwAttrs.match(/\bobjectType="([^"]*)"/)?.[1] ?? '';
|
|
13
|
+
const objectSubtype = bwAttrs.match(/\bobjectSubtype="([^"]*)"/)?.[1] ?? null;
|
|
14
|
+
const objectStatus = bwAttrs.match(/\bobjectStatus="([^"]*)"/)?.[1] ?? null;
|
|
15
|
+
const displayObjectName = bwAttrs.match(/\bdisplayObjectName="([^"]*)"/)?.[1] ?? null;
|
|
16
|
+
const title = body.match(/<atom:title[^>]*>([^<]*)<\/atom:title>/)?.[1] ?? '';
|
|
17
|
+
let selfHref = null;
|
|
18
|
+
let childrenHref = null;
|
|
19
|
+
const linkRegex = /<atom:link\b([\s\S]*?)(?:\/>|>)/g;
|
|
20
|
+
let lm;
|
|
21
|
+
while ((lm = linkRegex.exec(body)) !== null) {
|
|
22
|
+
const attrs = lm[1];
|
|
23
|
+
const rel = attrs.match(/\brel="([^"]*)"/)?.[1] ?? '';
|
|
24
|
+
const href = attrs.match(/\bhref="([^"]*)"/)?.[1] ?? '';
|
|
25
|
+
if (rel === 'self')
|
|
26
|
+
selfHref = href || null;
|
|
27
|
+
else if (rel === 'http://www.sap.com/bw/modeling/relations:children')
|
|
28
|
+
childrenHref = href || null;
|
|
29
|
+
}
|
|
30
|
+
entries.push({ objectName, objectType, objectSubtype, objectStatus, displayObjectName, title, selfHref, childrenHref });
|
|
31
|
+
}
|
|
32
|
+
return entries;
|
|
33
|
+
}
|
|
34
|
+
function stripBase(href) {
|
|
35
|
+
return href.startsWith(BASE_PREFIX) ? href.slice(BASE_PREFIX.length) : href;
|
|
36
|
+
}
|
|
37
|
+
export async function bwListSourceSystems(client, sourceSystemType) {
|
|
38
|
+
const ssysUrls = [];
|
|
39
|
+
if (sourceSystemType) {
|
|
40
|
+
ssysUrls.push(`${BASE}/ssys/${sourceSystemType.toLowerCase()}`);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
const { body: rootBody } = await client.get(BASE, 'application/atom+xml');
|
|
44
|
+
for (const e of parseEntries(rootBody)) {
|
|
45
|
+
if (e.childrenHref)
|
|
46
|
+
ssysUrls.push(e.childrenHref);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const sourceSystems = [];
|
|
50
|
+
for (const url of ssysUrls) {
|
|
51
|
+
const { body } = await client.get(url, 'application/atom+xml');
|
|
52
|
+
for (const e of parseEntries(body)) {
|
|
53
|
+
if (e.objectType !== 'LSYS')
|
|
54
|
+
continue;
|
|
55
|
+
sourceSystems.push({
|
|
56
|
+
name: e.objectName,
|
|
57
|
+
description: e.title,
|
|
58
|
+
source_system_type: e.objectSubtype,
|
|
59
|
+
status: e.objectStatus,
|
|
60
|
+
self_url: e.selfHref,
|
|
61
|
+
children_path: e.childrenHref ? stripBase(e.childrenHref) : null,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return JSON.stringify({ count: sourceSystems.length, source_systems: sourceSystems }, null, 2);
|
|
66
|
+
}
|
|
67
|
+
export async function bwListDatasources(client, sourceSystem, format = 'text', apcoPathFilter) {
|
|
68
|
+
const filterSegments = apcoPathFilter
|
|
69
|
+
? apcoPathFilter.split('>').map(s => s.trim().toLowerCase()).filter(s => s.length > 0)
|
|
70
|
+
: [];
|
|
71
|
+
const filterLen = filterSegments.length;
|
|
72
|
+
const segmentMatches = (title, name, idx) => {
|
|
73
|
+
const seg = filterSegments[idx];
|
|
74
|
+
return title.trim().toLowerCase() === seg || name.trim().toLowerCase() === seg;
|
|
75
|
+
};
|
|
76
|
+
const advanceMatch = (currentPtr, title, name) => {
|
|
77
|
+
if (filterLen === 0)
|
|
78
|
+
return 0;
|
|
79
|
+
if (currentPtr >= filterLen)
|
|
80
|
+
return currentPtr;
|
|
81
|
+
if (segmentMatches(title, name, currentPtr))
|
|
82
|
+
return currentPtr + 1;
|
|
83
|
+
if (currentPtr > 0 && segmentMatches(title, name, 0))
|
|
84
|
+
return 1;
|
|
85
|
+
return 0;
|
|
86
|
+
};
|
|
87
|
+
const datasources = [];
|
|
88
|
+
const rawBlocks = [];
|
|
89
|
+
const sourceSystemUpper = sourceSystem.toUpperCase();
|
|
90
|
+
async function recurse(url, apcoPath, matchPtr) {
|
|
91
|
+
const { body } = await client.get(url, 'application/atom+xml');
|
|
92
|
+
const inside = matchPtr >= filterLen;
|
|
93
|
+
if (format === 'raw') {
|
|
94
|
+
if (inside)
|
|
95
|
+
rawBlocks.push(`Source System: ${sourceSystemUpper}\n${body}`);
|
|
96
|
+
for (const e of parseEntries(body)) {
|
|
97
|
+
if (e.objectType === 'APCO' && e.childrenHref) {
|
|
98
|
+
const nextPtr = advanceMatch(matchPtr, e.title, e.objectName);
|
|
99
|
+
await recurse(e.childrenHref, [...apcoPath, e.title], nextPtr);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
for (const e of parseEntries(body)) {
|
|
105
|
+
if (e.objectType === 'RSDS') {
|
|
106
|
+
if (!inside)
|
|
107
|
+
continue;
|
|
108
|
+
const name = e.displayObjectName
|
|
109
|
+
? e.displayObjectName.split(' (')[0]
|
|
110
|
+
: e.objectName.trim().split(' ')[0];
|
|
111
|
+
datasources.push({
|
|
112
|
+
name,
|
|
113
|
+
description: e.title,
|
|
114
|
+
status: e.objectStatus,
|
|
115
|
+
self_url: e.selfHref,
|
|
116
|
+
apco_path: [...apcoPath],
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
else if (e.objectType === 'APCO' && e.childrenHref) {
|
|
120
|
+
const nextPtr = advanceMatch(matchPtr, e.title, e.objectName);
|
|
121
|
+
await recurse(e.childrenHref, [...apcoPath, e.title], nextPtr);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await recurse(`${BASE}/lsys/${sourceSystem.toLowerCase()}`, [], 0);
|
|
126
|
+
if (format === 'raw')
|
|
127
|
+
return rawBlocks.join('\n\n');
|
|
128
|
+
const p = (s, n) => s.padEnd(n);
|
|
129
|
+
const header = `${p('NAME', 30)} ${p('STATUS', 9)} ${p('APCO PATH', 32)} ${p('DESCRIPTION', 36)} URL`;
|
|
130
|
+
const sep = '-'.repeat(header.length);
|
|
131
|
+
const headerLines = [
|
|
132
|
+
`Source System: ${sourceSystemUpper}`,
|
|
133
|
+
`DataSources: ${datasources.length}`,
|
|
134
|
+
];
|
|
135
|
+
if (apcoPathFilter)
|
|
136
|
+
headerLines.push(`APCO Path Filter: ${apcoPathFilter}`);
|
|
137
|
+
const lines = [
|
|
138
|
+
...headerLines,
|
|
139
|
+
'',
|
|
140
|
+
header,
|
|
141
|
+
sep,
|
|
142
|
+
];
|
|
143
|
+
for (const ds of datasources) {
|
|
144
|
+
const apco = ds.apco_path.join(' > ');
|
|
145
|
+
lines.push(`${p(ds.name, 30)} ${p(ds.status ?? '', 9)} ${p(apco, 32)} ${p(ds.description, 36)} ${ds.self_url ?? ''}`);
|
|
146
|
+
}
|
|
147
|
+
return lines.join('\n');
|
|
148
|
+
}
|
|
149
|
+
const RSDS_ACCEPT = 'application/vnd.sap.bw.modeling.rsds-v1_0_0+xml, application/vnd.sap.bw.modeling.rsds-v1_1_0+xml';
|
|
150
|
+
function summarizeDatasource(d) {
|
|
151
|
+
const lines = [];
|
|
152
|
+
lines.push(`DataSource: ${d.name ?? ''}`);
|
|
153
|
+
lines.push(`Source System: ${d.source_system ?? ''}`);
|
|
154
|
+
lines.push(`Status: ${d.status ?? ''} | Type: ${d.type ?? ''} | Delta: ${d.delta ?? ''} | Direct Access: ${d.direct_access ?? ''}`);
|
|
155
|
+
lines.push(`Description: ${d.description ?? ''}`);
|
|
156
|
+
lines.push(`Application Component: ${d.application_component ?? ''}`);
|
|
157
|
+
lines.push(`Changed: ${d.changed_at ?? ''} by ${d.changed_by ?? ''}`);
|
|
158
|
+
lines.push(`Created: ${d.created_at ?? ''} by ${d.created_by ?? ''}`);
|
|
159
|
+
lines.push(`Package: ${d.package ?? ''}`);
|
|
160
|
+
// ── Fields ────────────────────────────────────────────────────────────────
|
|
161
|
+
lines.push('');
|
|
162
|
+
lines.push(`── Fields (${d.field_count}) ──`);
|
|
163
|
+
const compactLabel = (f) => {
|
|
164
|
+
const len = f.length !== null
|
|
165
|
+
? String(f.length)
|
|
166
|
+
: (f.precision !== undefined && f.scale !== undefined ? `P${f.precision}/S${f.scale}` : '');
|
|
167
|
+
return `${f.name}(${f.type ?? ''}/${len})`;
|
|
168
|
+
};
|
|
169
|
+
const notTransferred = d.fields.filter(f => f.transfer === false);
|
|
170
|
+
const keyFields = d.fields.filter(f => f.is_key);
|
|
171
|
+
const transferred = d.fields.filter(f => f.transfer === true)
|
|
172
|
+
.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
|
173
|
+
const ntLabels = notTransferred.map(compactLabel);
|
|
174
|
+
if (ntLabels.length === 0) {
|
|
175
|
+
lines.push(`Not transferred (0):`);
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
const chunks = [];
|
|
179
|
+
for (let i = 0; i < ntLabels.length; i += 10)
|
|
180
|
+
chunks.push(ntLabels.slice(i, i + 10));
|
|
181
|
+
if (chunks.length === 1) {
|
|
182
|
+
lines.push(`Not transferred (${notTransferred.length}): ${chunks[0].join(', ')}`);
|
|
183
|
+
}
|
|
184
|
+
else {
|
|
185
|
+
lines.push(`Not transferred (${notTransferred.length}):`);
|
|
186
|
+
for (const chunk of chunks)
|
|
187
|
+
lines.push(` ${chunk.join(', ')}`);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
lines.push(`Key fields (${keyFields.length}): ${keyFields.map(compactLabel).join(', ')}`);
|
|
191
|
+
lines.push('');
|
|
192
|
+
lines.push(`Transferred (${transferred.length}):`);
|
|
193
|
+
const pe = (s, n) => s.padEnd(n);
|
|
194
|
+
const pr = (s, n) => s.padStart(n);
|
|
195
|
+
lines.push(` ${'POS'.padEnd(4)} ${'NAME'.padEnd(30)} ${'TYPE'.padEnd(7)} ${'LEN'.padStart(5)} ${'KEY'.padEnd(4)} ${'SEL'.padEnd(4)} DESCRIPTION`);
|
|
196
|
+
for (const f of transferred) {
|
|
197
|
+
const pos = String(f.position ?? 0).padStart(4, '0');
|
|
198
|
+
const lenStr = f.length !== null
|
|
199
|
+
? String(f.length)
|
|
200
|
+
: (f.precision !== undefined && f.scale !== undefined ? `P${f.precision}/S${f.scale}` : '');
|
|
201
|
+
const keyStr = f.is_key ? 'key' : '';
|
|
202
|
+
const selStr = String(f.selection_options ?? '');
|
|
203
|
+
let desc = f.description ?? '';
|
|
204
|
+
if (f.conversion_exit)
|
|
205
|
+
desc += ` [conv: ${f.conversion_exit}]`;
|
|
206
|
+
if (f.unit_currency_ref)
|
|
207
|
+
desc += ` [unit: ${f.unit_currency_ref}]`;
|
|
208
|
+
lines.push(` ${pos} ${pe(f.name, 30)} ${pe(f.type ?? '', 7)} ${pr(lenStr, 5)} ${pe(keyStr, 4)} ${pe(selStr, 4)} ${desc}`);
|
|
209
|
+
}
|
|
210
|
+
// ── Adapter ────────────────────────────────────────────────────────────────
|
|
211
|
+
lines.push('');
|
|
212
|
+
lines.push('── Adapter ──');
|
|
213
|
+
for (const [key, value] of Object.entries(d.adapter)) {
|
|
214
|
+
lines.push(`${key}: ${value ?? ''}`);
|
|
215
|
+
}
|
|
216
|
+
return lines.join('\n');
|
|
217
|
+
}
|
|
218
|
+
function parseDescLabel(xml) {
|
|
219
|
+
const re = /<description\b([^>]*)\/>/g;
|
|
220
|
+
let m;
|
|
221
|
+
while ((m = re.exec(xml)) !== null) {
|
|
222
|
+
const attrs = m[1];
|
|
223
|
+
if (attrs.includes('textType="3"'))
|
|
224
|
+
return attrs.match(/\blabel="([^"]*)"/)?.[1] ?? null;
|
|
225
|
+
}
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
export async function bwGetDatasource(client, datasourceName, sourceSystem, format = 'text') {
|
|
229
|
+
const url = `/sap/bw/modeling/rsds/${datasourceName}/${sourceSystem.toUpperCase()}/m`;
|
|
230
|
+
const { body, headers } = await client.get(url, RSDS_ACCEPT);
|
|
231
|
+
if (format === 'raw')
|
|
232
|
+
return body;
|
|
233
|
+
// Root element attributes
|
|
234
|
+
const rootMatch = body.match(/<rsds:dataSource\b([\s\S]*?)>/);
|
|
235
|
+
const rootAttrs = rootMatch?.[1] ?? '';
|
|
236
|
+
const name = rootAttrs.match(/(?:^|\s)name="([^"]*)"/)?.[1] ?? null;
|
|
237
|
+
const sourceSystemAttr = rootAttrs.match(/\bsourceSystemName="([^"]*)"/)?.[1] ?? null;
|
|
238
|
+
const type = rootAttrs.match(/(?<![a-zA-Z:])type="([^"]*)"/)?.[1] ?? null;
|
|
239
|
+
const applicationComponent = rootAttrs.match(/\bapplicationComponent="([^"]*)"/)?.[1] ?? null;
|
|
240
|
+
const directAccess = rootAttrs.match(/\bdirectAccess="([^"]*)"/)?.[1] ?? null;
|
|
241
|
+
// delta from <deltaProperties>
|
|
242
|
+
const delta = body.match(/<deltaProperties\b[\s\S]*?\bdelta="([^"]*)"/)?.[1] ?? null;
|
|
243
|
+
// DataSource-level description (header section, before first <segment>)
|
|
244
|
+
const segStart = body.indexOf('<segment');
|
|
245
|
+
const headerSection = segStart >= 0 ? body.slice(0, segStart) : body;
|
|
246
|
+
const description = parseDescLabel(headerSection);
|
|
247
|
+
// Status from response header (axios lowercases all header names)
|
|
248
|
+
const status = headers['object_status'] ?? null;
|
|
249
|
+
// tlogoProperties
|
|
250
|
+
const tlogoMatch = body.match(/<tlogoProperties\b([\s\S]*?)>/);
|
|
251
|
+
const tlogoAttrs = tlogoMatch?.[1] ?? '';
|
|
252
|
+
const changedAt = tlogoAttrs.match(/\badtcore:changedAt="([^"]*)"/)?.[1] ?? null;
|
|
253
|
+
const changedBy = tlogoAttrs.match(/\badtcore:changedBy="([^"]*)"/)?.[1] ?? null;
|
|
254
|
+
const createdAt = tlogoAttrs.match(/\badtcore:createdAt="([^"]*)"/)?.[1] ?? null;
|
|
255
|
+
const createdBy = tlogoAttrs.match(/\badtcore:createdBy="([^"]*)"/)?.[1] ?? null;
|
|
256
|
+
// Package from <adtcore:packageRef adtcore:name="...">
|
|
257
|
+
const pkg = body.match(/<adtcore:packageRef\b[\s\S]*?\badtcore:name="([^"]*)"/)?.[1] ?? null;
|
|
258
|
+
// Segment ID="0001" — key fields and field list
|
|
259
|
+
const segMatch = body.match(/<segment\b[^>]*ID="0001"[^>]*>([\s\S]*?)<\/segment>/);
|
|
260
|
+
const segBody = segMatch?.[1] ?? '';
|
|
261
|
+
const keyFields = new Set();
|
|
262
|
+
const kfRe = /<keyField>([^<]*)<\/keyField>/g;
|
|
263
|
+
let kfm;
|
|
264
|
+
while ((kfm = kfRe.exec(segBody)) !== null) {
|
|
265
|
+
const kv = kfm[1].trim().match(/^#\/\/\/0001\/(.+)$/);
|
|
266
|
+
if (kv)
|
|
267
|
+
keyFields.add(kv[1]);
|
|
268
|
+
}
|
|
269
|
+
const fields = [];
|
|
270
|
+
const fieldRe = /<field\b([\s\S]*?)>([\s\S]*?)<\/field>/g;
|
|
271
|
+
let fm;
|
|
272
|
+
while ((fm = fieldRe.exec(segBody)) !== null) {
|
|
273
|
+
const fTag = fm[1];
|
|
274
|
+
const fBody = fm[2];
|
|
275
|
+
const fieldName = fTag.match(/\bname="([^"]*)"/)?.[1] ?? '';
|
|
276
|
+
const itMatch = fBody.match(/<inlineType\b([\s\S]*?)(?:\/>|>)/);
|
|
277
|
+
const itAttrs = itMatch?.[1] ?? '';
|
|
278
|
+
const fType = itAttrs.match(/\bname="([^"]*)"/)?.[1] ?? null;
|
|
279
|
+
const lengthRaw = itAttrs.match(/\blength="([^"]*)"/)?.[1];
|
|
280
|
+
const length = lengthRaw !== undefined ? parseInt(lengthRaw, 10) : null;
|
|
281
|
+
const precisionRaw = itAttrs.match(/\bprecision="([^"]*)"/)?.[1];
|
|
282
|
+
const scaleRaw = itAttrs.match(/\bscale="([^"]*)"/)?.[1];
|
|
283
|
+
const fpMatch = fBody.match(/<fieldProperties\b([\s\S]*?)(?:\/>|>)/);
|
|
284
|
+
const fpAttrs = fpMatch?.[1] ?? '';
|
|
285
|
+
const transferRaw = fpAttrs.match(/\btransfer="([^"]*)"/)?.[1];
|
|
286
|
+
const selOptRaw = fpAttrs.match(/\bselectionOptions="([^"]*)"/)?.[1];
|
|
287
|
+
const posRaw = fpAttrs.match(/\bposition="([^"]*)"/)?.[1];
|
|
288
|
+
const convExit = fpAttrs.match(/\bconversionExitSource="([^"]*)"/)?.[1] || undefined;
|
|
289
|
+
const ucRaw = fBody.match(/<unitCurrencyElement>([^<]*)<\/unitCurrencyElement>/)?.[1];
|
|
290
|
+
const field = {
|
|
291
|
+
name: fieldName,
|
|
292
|
+
description: parseDescLabel(fBody),
|
|
293
|
+
type: fType,
|
|
294
|
+
length,
|
|
295
|
+
transfer: transferRaw === 'true' ? true : transferRaw === 'false' ? false : null,
|
|
296
|
+
selection_options: selOptRaw !== undefined ? parseInt(selOptRaw, 10) : null,
|
|
297
|
+
position: posRaw !== undefined ? parseInt(posRaw, 10) : null,
|
|
298
|
+
is_key: keyFields.has(fieldName),
|
|
299
|
+
};
|
|
300
|
+
if (precisionRaw !== undefined)
|
|
301
|
+
field['precision'] = parseInt(precisionRaw, 10);
|
|
302
|
+
if (scaleRaw !== undefined)
|
|
303
|
+
field['scale'] = parseInt(scaleRaw, 10);
|
|
304
|
+
if (convExit)
|
|
305
|
+
field['conversion_exit'] = convExit;
|
|
306
|
+
if (ucRaw)
|
|
307
|
+
field['unit_currency_ref'] = ucRaw.replace(/^#\/\/\/0001\//, '');
|
|
308
|
+
fields.push(field);
|
|
309
|
+
}
|
|
310
|
+
// Active adapter(s)
|
|
311
|
+
const adapter = {};
|
|
312
|
+
const adapterTagRe = /<adapter\b([\s\S]*?)(?:\/>|>)/g;
|
|
313
|
+
let adm;
|
|
314
|
+
while ((adm = adapterTagRe.exec(body)) !== null) {
|
|
315
|
+
const aAttrs = adm[1];
|
|
316
|
+
if (!aAttrs.includes('currentlyUsed="true"'))
|
|
317
|
+
continue;
|
|
318
|
+
const rawAType = aAttrs.match(/\bxsi:type="([^"]*)"/)?.[1] ?? '';
|
|
319
|
+
const aType = rawAType.replace(/^rsds:/, '');
|
|
320
|
+
const extObj = aAttrs.match(/\bexternalObject="([^"]*)"/)?.[1] || null;
|
|
321
|
+
if (aType === 'ConverterCSVFL') {
|
|
322
|
+
const dataSep = aAttrs.match(/\bdataSeparator="([^"]*)"/)?.[1] ?? null;
|
|
323
|
+
const escChar = aAttrs.match(/\bescapeCharacter="([^"]*)"/)?.[1] ?? null;
|
|
324
|
+
if (dataSep !== null)
|
|
325
|
+
adapter['data_separator'] = dataSep;
|
|
326
|
+
if (escChar !== null)
|
|
327
|
+
adapter['escape_character'] = escChar;
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
adapter['adapter_name'] = aAttrs.match(/(?:^|\s)name="([^"]*)"/)?.[1] ?? null;
|
|
331
|
+
adapter['adapter_type'] = aType;
|
|
332
|
+
if (extObj)
|
|
333
|
+
adapter['external_object'] = extObj;
|
|
334
|
+
if (aType === 'ExtractorODP') {
|
|
335
|
+
adapter['context_description'] = aAttrs.match(/\bcontextDescription="([^"]*)"/)?.[1] ?? null;
|
|
336
|
+
adapter['semantics'] = aAttrs.match(/\bsemantics="([^"]*)"/)?.[1] ?? null;
|
|
337
|
+
}
|
|
338
|
+
else if (aType === 'ExtractorHANA') {
|
|
339
|
+
adapter['hana_type'] = aAttrs.match(/\bhanaType="([^"]*)"/)?.[1] ?? null;
|
|
340
|
+
adapter['schema'] = aAttrs.match(/\bschema="([^"]*)"/)?.[1] ?? null;
|
|
341
|
+
adapter['remote_source'] = aAttrs.match(/\bremoteSource="([^"]*)"/)?.[1] ?? null;
|
|
342
|
+
}
|
|
343
|
+
else if (aType.startsWith('ExtractorFile')) {
|
|
344
|
+
const ignoreLines = aAttrs.match(/\bignoreLines="([^"]*)"/)?.[1] || null;
|
|
345
|
+
if (ignoreLines !== null)
|
|
346
|
+
adapter['ignore_lines'] = ignoreLines;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const data = {
|
|
351
|
+
name,
|
|
352
|
+
source_system: sourceSystemAttr,
|
|
353
|
+
type,
|
|
354
|
+
application_component: applicationComponent,
|
|
355
|
+
direct_access: directAccess,
|
|
356
|
+
delta,
|
|
357
|
+
description,
|
|
358
|
+
status,
|
|
359
|
+
changed_at: changedAt,
|
|
360
|
+
changed_by: changedBy,
|
|
361
|
+
created_at: createdAt,
|
|
362
|
+
created_by: createdBy,
|
|
363
|
+
package: pkg,
|
|
364
|
+
field_count: fields.length,
|
|
365
|
+
fields,
|
|
366
|
+
adapter,
|
|
367
|
+
};
|
|
368
|
+
return summarizeDatasource(data);
|
|
369
|
+
}
|
|
370
|
+
const LSYS_ACCEPT = 'application/vnd.sap.bw.modeling.lsys-v1_0_0+xml, application/vnd.sap.bw.modeling.lsys-v1_1_0+xml';
|
|
371
|
+
function deriveSourceSystemType(xsiType, context, hanaType) {
|
|
372
|
+
if (xsiType === 'SourceSystemFILE')
|
|
373
|
+
return 'FILE';
|
|
374
|
+
if (xsiType === 'SourceSystemHANA') {
|
|
375
|
+
return hanaType === '1' ? 'HANA_LOCAL' : 'HANA_SDA';
|
|
376
|
+
}
|
|
377
|
+
if (xsiType === 'SourceSystemODP') {
|
|
378
|
+
if (context === 'SAPI')
|
|
379
|
+
return 'ODP_SAP';
|
|
380
|
+
if (context === 'ABAP_CDS')
|
|
381
|
+
return 'ODP_CDS';
|
|
382
|
+
if (context === 'BW')
|
|
383
|
+
return 'ODP_BW';
|
|
384
|
+
return 'ODP';
|
|
385
|
+
}
|
|
386
|
+
return xsiType;
|
|
387
|
+
}
|
|
388
|
+
export async function bwGetSourceSystem(client, sourceSystem) {
|
|
389
|
+
const url = `/sap/bw/modeling/lsys/${sourceSystem.toLowerCase()}/a`;
|
|
390
|
+
const { body, headers } = await client.get(url, LSYS_ACCEPT);
|
|
391
|
+
// Root element opening tag attributes (up to first closing >)
|
|
392
|
+
const rootMatch = body.match(/<lsys:sourceSystem\b([\s\S]*?)>/);
|
|
393
|
+
const rootAttrs = rootMatch?.[1] ?? '';
|
|
394
|
+
const name = rootAttrs.match(/(?:^|\s)name="([^"]*)"/)?.[1] ?? null;
|
|
395
|
+
const rawXsiType = rootAttrs.match(/\bxsi:type="([^"]*)"/)?.[1] ?? '';
|
|
396
|
+
const xsiType = rawXsiType.replace(/^lsys:/, '');
|
|
397
|
+
// Use negative lookbehind to avoid matching xsi:type or adtcore:type
|
|
398
|
+
const type = rootAttrs.match(/(?<![a-zA-Z:])type="([^"]*)"/)?.[1] ?? null;
|
|
399
|
+
// ODP-specific root attributes
|
|
400
|
+
const context = rootAttrs.match(/\bcontext="([^"]*)"/)?.[1] ?? null;
|
|
401
|
+
const destination = rootAttrs.match(/\bdestination="([^"]*)"/)?.[1] ?? null;
|
|
402
|
+
const destinationValid = rootAttrs.match(/\bdestinationValid="([^"]*)"/)?.[1] ?? null;
|
|
403
|
+
const treeRemote = rootAttrs.match(/\btreeRemote="([^"]*)"/)?.[1] ?? null;
|
|
404
|
+
const treeReplicatable = rootAttrs.match(/\btreeReplicatable="([^"]*)"/)?.[1] ?? null;
|
|
405
|
+
// HANA-specific root attributes
|
|
406
|
+
const hanaType = rootAttrs.match(/\bhanaType="([^"]*)"/)?.[1] ?? null;
|
|
407
|
+
const remoteSource = rootAttrs.match(/\bremoteSource="([^"]*)"/)?.[1] ?? null;
|
|
408
|
+
const database = rootAttrs.match(/\bdatabase="([^"]*)"/)?.[1] ?? null;
|
|
409
|
+
const schema = rootAttrs.match(/\bschema="([^"]*)"/)?.[1] ?? null;
|
|
410
|
+
const sdiAdapter = rootAttrs.match(/\bsdiAdapter="([^"]*)"/)?.[1] ?? null;
|
|
411
|
+
// <description textType="3" label="..."/>
|
|
412
|
+
let description = null;
|
|
413
|
+
const descRegex = /<description\b([^>]*?)(?:\/>|>)/g;
|
|
414
|
+
let dm;
|
|
415
|
+
while ((dm = descRegex.exec(body)) !== null) {
|
|
416
|
+
const dAttrs = dm[1];
|
|
417
|
+
if (dAttrs.includes('textType="3"')) {
|
|
418
|
+
description = dAttrs.match(/\blabel="([^"]*)"/)?.[1] ?? null;
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// tlogoProperties attributes
|
|
423
|
+
const tlogoMatch = body.match(/<tlogoProperties\b([\s\S]*?)>/);
|
|
424
|
+
const tlogoAttrs = tlogoMatch?.[1] ?? '';
|
|
425
|
+
const changedAt = tlogoAttrs.match(/\badtcore:changedAt="([^"]*)"/)?.[1] ?? null;
|
|
426
|
+
const changedBy = tlogoAttrs.match(/\badtcore:changedBy="([^"]*)"/)?.[1] ?? null;
|
|
427
|
+
// <objectStatus> text content
|
|
428
|
+
const objectStatus = body.match(/<objectStatus>([^<]*)<\/objectStatus>/)?.[1] ?? null;
|
|
429
|
+
// Response header (axios lowercases header names)
|
|
430
|
+
const status = headers['object_status'] ?? null;
|
|
431
|
+
const sourceSystemType = deriveSourceSystemType(xsiType, context, hanaType);
|
|
432
|
+
const result = {
|
|
433
|
+
name,
|
|
434
|
+
xsi_type: xsiType,
|
|
435
|
+
type,
|
|
436
|
+
status,
|
|
437
|
+
description,
|
|
438
|
+
changed_at: changedAt,
|
|
439
|
+
changed_by: changedBy,
|
|
440
|
+
object_status: objectStatus,
|
|
441
|
+
source_system_type: sourceSystemType,
|
|
442
|
+
};
|
|
443
|
+
if (xsiType === 'SourceSystemODP') {
|
|
444
|
+
result['context'] = context;
|
|
445
|
+
result['destination'] = destination;
|
|
446
|
+
result['destination_valid'] = destinationValid === 'true' ? true : destinationValid === 'false' ? false : null;
|
|
447
|
+
result['tree_remote'] = treeRemote === 'true' ? true : treeRemote === 'false' ? false : null;
|
|
448
|
+
result['tree_replicatable'] = treeReplicatable === 'true' ? true : treeReplicatable === 'false' ? false : null;
|
|
449
|
+
}
|
|
450
|
+
else if (xsiType === 'SourceSystemHANA') {
|
|
451
|
+
result['hana_type'] = hanaType;
|
|
452
|
+
result['remote_source'] = remoteSource;
|
|
453
|
+
result['database'] = database;
|
|
454
|
+
result['schema'] = schema;
|
|
455
|
+
result['sdi_adapter'] = sdiAdapter;
|
|
456
|
+
}
|
|
457
|
+
return JSON.stringify(result, null, 2);
|
|
458
|
+
}
|
|
459
|
+
export async function bwPreviewDatasource(client, datasourceName, sourceSystem, records = 20) {
|
|
460
|
+
const structureUrl = `/sap/bw/modeling/rsds/${datasourceName.toLowerCase()}/${sourceSystem.toUpperCase()}/m`;
|
|
461
|
+
const { body: structureBody } = await client.get(structureUrl, RSDS_ACCEPT);
|
|
462
|
+
const segMatch = structureBody.match(/<segment\b[^>]*ID="0001"[^>]*>([\s\S]*?)<\/segment>/);
|
|
463
|
+
const segBody = segMatch?.[1] ?? '';
|
|
464
|
+
const fieldEntries = [];
|
|
465
|
+
const fieldRe = /<field\b([\s\S]*?)>([\s\S]*?)<\/field>/g;
|
|
466
|
+
let fm;
|
|
467
|
+
while ((fm = fieldRe.exec(segBody)) !== null) {
|
|
468
|
+
const fTag = fm[1];
|
|
469
|
+
const fBody = fm[2];
|
|
470
|
+
const fieldName = fTag.match(/\bname="([^"]*)"/)?.[1] ?? '';
|
|
471
|
+
const fpMatch = fBody.match(/<fieldProperties\b([\s\S]*?)(?:\/>|>)/);
|
|
472
|
+
const fpAttrs = fpMatch?.[1] ?? '';
|
|
473
|
+
const transferRaw = fpAttrs.match(/\btransfer="([^"]*)"/)?.[1];
|
|
474
|
+
if (transferRaw === 'false')
|
|
475
|
+
continue;
|
|
476
|
+
const posRaw = fpAttrs.match(/\bposition="([^"]*)"/)?.[1];
|
|
477
|
+
const position = posRaw !== undefined ? parseInt(posRaw, 10) : 0;
|
|
478
|
+
fieldEntries.push({ name: fieldName, position });
|
|
479
|
+
}
|
|
480
|
+
fieldEntries.sort((a, b) => a.position - b.position);
|
|
481
|
+
const fieldNames = fieldEntries.map(f => f.name);
|
|
482
|
+
const url = `/sap/bw/modeling/rsdsint/dataprev/${datasourceName.toUpperCase()}/${sourceSystem.toUpperCase()}?records=${records}&external=true`;
|
|
483
|
+
const csrfToken = await client.getCsrfToken();
|
|
484
|
+
const { body: previewBody } = await client.rawPost(url, '', {
|
|
485
|
+
'x-csrf-token': csrfToken,
|
|
486
|
+
'X-sap-adt-profiling': 'server-time',
|
|
487
|
+
});
|
|
488
|
+
const contentMatch = previewBody.match(/<simpleParams\b[^>]*\bcontent="([^"]*)"/);
|
|
489
|
+
const content = contentMatch?.[1] ?? '';
|
|
490
|
+
const decoded = Buffer.from(content, 'base64').toString('utf-8');
|
|
491
|
+
const parsed = JSON.parse(decoded);
|
|
492
|
+
const rows = parsed['T_DATA_0001'] ?? [];
|
|
493
|
+
const summaryHeader = [
|
|
494
|
+
`DataSource: ${datasourceName.toUpperCase()} / Source System: ${sourceSystem.toUpperCase()}`,
|
|
495
|
+
`Records: ${rows.length} (requested: ${records})`,
|
|
496
|
+
].join('\n');
|
|
497
|
+
if (rows.length === 0) {
|
|
498
|
+
return `${summaryHeader}\n(no data returned)`;
|
|
499
|
+
}
|
|
500
|
+
const columnCount = rows[0].length;
|
|
501
|
+
let headers;
|
|
502
|
+
let mismatchWarning = null;
|
|
503
|
+
if (fieldNames.length !== columnCount) {
|
|
504
|
+
headers = Array.from({ length: columnCount }, (_, i) => `COL_${i + 1}`);
|
|
505
|
+
mismatchWarning = `Warning: field count mismatch (fields: ${fieldNames.length}, columns: ${columnCount}) — column names may be incorrect.`;
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
headers = fieldNames;
|
|
509
|
+
}
|
|
510
|
+
const MAX_WIDTH = 30;
|
|
511
|
+
const truncate = (s) => (s.length > MAX_WIDTH ? s.slice(0, 27) + '...' : s);
|
|
512
|
+
const truncatedRows = rows.map(row => row.map(truncate));
|
|
513
|
+
const truncatedHeaders = headers.map(truncate);
|
|
514
|
+
const widths = truncatedHeaders.map((h, i) => {
|
|
515
|
+
let w = h.length;
|
|
516
|
+
for (const row of truncatedRows) {
|
|
517
|
+
const cell = row[i] ?? '';
|
|
518
|
+
if (cell.length > w)
|
|
519
|
+
w = cell.length;
|
|
520
|
+
}
|
|
521
|
+
return Math.min(w, MAX_WIDTH);
|
|
522
|
+
});
|
|
523
|
+
const formatRow = (cells) => cells.map((c, i) => c.padEnd(widths[i])).join(' ');
|
|
524
|
+
const headerLine = formatRow(truncatedHeaders);
|
|
525
|
+
const sepLine = widths.map(w => '-'.repeat(w)).join(' ');
|
|
526
|
+
const lines = [summaryHeader, '', headerLine, sepLine];
|
|
527
|
+
for (const row of truncatedRows) {
|
|
528
|
+
const cells = headers.map((_, i) => row[i] ?? '');
|
|
529
|
+
lines.push(formatRow(cells));
|
|
530
|
+
}
|
|
531
|
+
if (mismatchWarning) {
|
|
532
|
+
lines.push('');
|
|
533
|
+
lines.push(mismatchWarning);
|
|
534
|
+
}
|
|
535
|
+
return lines.join('\n');
|
|
536
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { MEDIA_TYPES } from '../bw-client.js';
|
|
2
|
+
function parseAtomTitles(xml) {
|
|
3
|
+
return [...xml.matchAll(/<atom:title[^>]*>([^<]+)<\/atom:title>/g)].map(m => m[1]);
|
|
4
|
+
}
|
|
5
|
+
export async function bwDelete(client, objectType, objectName) {
|
|
6
|
+
const typeLower = objectType.toLowerCase();
|
|
7
|
+
const mediaType = MEDIA_TYPES[typeLower] ?? 'application/xml';
|
|
8
|
+
// 1. Lock — uses /m before ?action=lock (delete-specific lock URL)
|
|
9
|
+
const lockHandle = await client.lockForDelete(typeLower, objectName, mediaType);
|
|
10
|
+
// 2. DELETE
|
|
11
|
+
const deleteResult = await client.delete(typeLower, objectName, lockHandle, mediaType);
|
|
12
|
+
// 3. Unlock — no /m (same as normal unlock)
|
|
13
|
+
await client.unlock(typeLower, objectName);
|
|
14
|
+
// 4. Parse atom feed response
|
|
15
|
+
const messages = parseAtomTitles(deleteResult);
|
|
16
|
+
return JSON.stringify({
|
|
17
|
+
success: true,
|
|
18
|
+
object_type: objectType.toUpperCase(),
|
|
19
|
+
object_name: objectName.toUpperCase(),
|
|
20
|
+
messages,
|
|
21
|
+
});
|
|
22
|
+
}
|