@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,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
+ }