@abapify/adt-rfc 0.4.33 → 0.4.36
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/README.md +13 -26
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +43 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# @abapify/adt-rfc
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@abapify/adt-rfc)
|
|
4
|
+
[](https://github.com/abapify/adt-cli/blob/main/LICENSE)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
SAP RFC transport for @abapify — SOAP-over-HTTP RFC implementation (/sap/bc/soap/rfc).
|
|
6
7
|
|
|
7
8
|
## Install
|
|
8
9
|
|
|
9
10
|
```bash
|
|
10
|
-
|
|
11
|
+
npm install @abapify/adt-rfc
|
|
11
12
|
```
|
|
12
13
|
|
|
13
14
|
## Usage
|
|
@@ -19,7 +20,7 @@ import { createRfcClient } from '@abapify/adt-rfc';
|
|
|
19
20
|
const adt = createAdtClient({
|
|
20
21
|
baseUrl: 'https://my.sap.example.com',
|
|
21
22
|
username: 'DEVELOPER',
|
|
22
|
-
password
|
|
23
|
+
password,
|
|
23
24
|
client: '100',
|
|
24
25
|
});
|
|
25
26
|
|
|
@@ -33,30 +34,16 @@ console.log(resp.ECHOTEXT); // → "hello"
|
|
|
33
34
|
console.log(resp.RESPTEXT); // → "SAP R/3 Rel. 7.55 Sysid: TRL …"
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
##
|
|
37
|
+
## Features
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
- RFC calls over SOAP-over-HTTP (`/sap/bc/soap/rfc`)
|
|
40
|
+
- Reuses the ADT client session
|
|
41
|
+
- Typed parameter mapping and RFC error handling
|
|
39
42
|
|
|
40
|
-
|
|
41
|
-
| --------- | ------------------------------------- |
|
|
42
|
-
| Scalar | `string \| number \| boolean \| null` |
|
|
43
|
-
| Structure | `{ FIELD1: …, FIELD2: … }` |
|
|
44
|
-
| Table | `[{ FIELD: … }, { FIELD: … }]` |
|
|
43
|
+
## Documentation
|
|
45
44
|
|
|
46
|
-
|
|
45
|
+
**[adt-cli.netlify.app/sdk/packages/adt-rfc](https://adt-cli.netlify.app/sdk/packages/adt-rfc)**
|
|
47
46
|
|
|
48
|
-
|
|
47
|
+
---
|
|
49
48
|
|
|
50
|
-
-
|
|
51
|
-
- **`RfcTransportUnavailable`** — the `/sap/bc/soap/rfc` endpoint returned 401/403/404/501. Some SAP systems disable SOAP-RFC; catch this to downgrade gracefully.
|
|
52
|
-
|
|
53
|
-
## Limitations
|
|
54
|
-
|
|
55
|
-
- **SOAP-RFC only.** Native `libsapnwrfc` binding (via `node-rfc`) is deliberately out of scope — track `@abapify/adt-plugin-rfc-native` for that future path.
|
|
56
|
-
- **Type encoding.** SAP's SOAP-RFC wrapper does not round-trip every RFC data type perfectly. Binary fields come back as base64-looking strings; dates/times come back as SAP internal strings (`YYYYMMDD`, `HHMMSS`). Callers convert.
|
|
57
|
-
- **Authentication.** Reuses the ADT client's session/CSRF/cookie state — no separate auth plumbing.
|
|
58
|
-
|
|
59
|
-
## See also
|
|
60
|
-
|
|
61
|
-
- Spec: `docs/roadmap/epics/e13-startrfc.md`
|
|
62
|
-
- Reference implementation: `tmp/sapcli-ref/sapcli/sap/rfc/`
|
|
49
|
+
Part of [**abapify**](https://github.com/abapify/adt-cli) — open, typed tooling for SAP ABAP Development Tools (ADT): CLI, MCP server, and SDK.
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/lib/transport/types.ts","../src/lib/client/rfc-client.ts","../src/lib/transport/soap-rfc.ts"],"mappings":";;;;;;;KAQY;;UAGK;GACd,gBAAgB,YAAY,eAAe;;;KAIlC,WAAW;;KAGX,YAAY,eAAe,YAAY,eAAe;;KAGtD,cAAc,eAAe,YAAY,eAAe;;;;UAKnD;;EAEf;;EAEA,SAAS;;;;;cAME,qBAAqB;WAEd;WACA;WACA;EAFA,YAAA,mBACA,qBACA;;;;;;;cAYP,gCAAgC;WAEzB;WACA;WACA;EAFA,YAAA,gBACA,aACA;;;;;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/lib/transport/types.ts","../src/lib/client/rfc-client.ts","../src/lib/transport/soap-rfc.ts"],"mappings":";;;;;;;KAQY;;UAGK;GACd,gBAAgB,YAAY,eAAe;;;KAIlC,WAAW;;KAGX,YAAY,eAAe,YAAY,eAAe;;KAGtD,cAAc,eAAe,YAAY,eAAe;;;;UAKnD;;EAEf;;EAEA,SAAS;;;;;cAME,qBAAqB;WAEd;WACA;WACA;EAFA,YAAA,mBACA,qBACA;;;;;;;cAYP,gCAAgC;WAEzB;WACA;WACA;EAFA,YAAA,gBACA,aACA;;;;;;;KC5BR,cACV,aACA;EACE;EACA,UAAU;EACV;MAEC;UAEY;;;;;;;EAOf,OAAO;;;;;EAMP;;UAGe;;;;;;;;EAQf,KACE,gBACA,SAAS,WACT,UAAU,iBACT,QAAQ;;;;;;;;;;;;;;;;;iBAwBG,gBAAgB,QAAQ,kBAAkB;;cChE7C;;cAGA;;;;;;;;;iBAsDG,qBACd,gBACA,SAAQ;;;;;;;iBAoMM,qBAAqB,cAAc"}
|
package/dist/index.mjs
CHANGED
|
@@ -53,7 +53,7 @@ const RFC_SOAP_NS = "urn:sap-com:document:sap:rfc:functions";
|
|
|
53
53
|
/** SOAP 1.1 envelope namespace. */
|
|
54
54
|
const SOAP_ENV_NS = "http://schemas.xmlsoap.org/soap/envelope/";
|
|
55
55
|
function escapeXml(value) {
|
|
56
|
-
return value.replaceAll(
|
|
56
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
57
57
|
}
|
|
58
58
|
function renderScalar(value) {
|
|
59
59
|
if (value === null || value === void 0) return "";
|
|
@@ -90,6 +90,39 @@ const TAG_NAME_RE = /^\/?([A-Za-z_][\w.:-]*)/;
|
|
|
90
90
|
function tagNameOf(inside) {
|
|
91
91
|
return TAG_NAME_RE.exec(inside)?.[1];
|
|
92
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Skip XML prolog (<?...?>), DOCTYPE, comments (<!-- -->), and any
|
|
95
|
+
* directive (<!...) — non-structural for RFC parsing.
|
|
96
|
+
*/
|
|
97
|
+
function isDirectiveInside(inside) {
|
|
98
|
+
return inside.startsWith("?") || inside.startsWith("!--") || inside.startsWith("!DOCTYPE") || inside.startsWith("!");
|
|
99
|
+
}
|
|
100
|
+
/** Push trailing text after a tag when it carries non-whitespace content. */
|
|
101
|
+
function pushTrailingText(tokens, trailing) {
|
|
102
|
+
if (trailing && trailing.trim().length > 0) tokens.push({
|
|
103
|
+
kind: "text",
|
|
104
|
+
text: trailing
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Push the structural token for a parsed tag body. */
|
|
108
|
+
function pushTagToken(tokens, inside, name) {
|
|
109
|
+
if (!name) tokens.push({
|
|
110
|
+
kind: "text",
|
|
111
|
+
text: "<" + inside + ">"
|
|
112
|
+
});
|
|
113
|
+
else if (inside.startsWith("/")) tokens.push({
|
|
114
|
+
kind: "close",
|
|
115
|
+
name
|
|
116
|
+
});
|
|
117
|
+
else if (inside.endsWith("/")) tokens.push({
|
|
118
|
+
kind: "self",
|
|
119
|
+
name
|
|
120
|
+
});
|
|
121
|
+
else tokens.push({
|
|
122
|
+
kind: "open",
|
|
123
|
+
name
|
|
124
|
+
});
|
|
125
|
+
}
|
|
93
126
|
function tokenize(xml) {
|
|
94
127
|
const tokens = [];
|
|
95
128
|
const parts = xml.split("<");
|
|
@@ -109,34 +142,12 @@ function tokenize(xml) {
|
|
|
109
142
|
}
|
|
110
143
|
const inside = part.slice(0, gt);
|
|
111
144
|
const trailing = part.slice(gt + 1);
|
|
112
|
-
if (
|
|
113
|
-
|
|
114
|
-
kind: "text",
|
|
115
|
-
text: trailing
|
|
116
|
-
});
|
|
145
|
+
if (isDirectiveInside(inside)) {
|
|
146
|
+
pushTrailingText(tokens, trailing);
|
|
117
147
|
continue;
|
|
118
148
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
kind: "text",
|
|
122
|
-
text: "<" + inside + ">"
|
|
123
|
-
});
|
|
124
|
-
else if (inside.startsWith("/")) tokens.push({
|
|
125
|
-
kind: "close",
|
|
126
|
-
name
|
|
127
|
-
});
|
|
128
|
-
else if (inside.endsWith("/")) tokens.push({
|
|
129
|
-
kind: "self",
|
|
130
|
-
name
|
|
131
|
-
});
|
|
132
|
-
else tokens.push({
|
|
133
|
-
kind: "open",
|
|
134
|
-
name
|
|
135
|
-
});
|
|
136
|
-
if (trailing && trailing.trim().length > 0) tokens.push({
|
|
137
|
-
kind: "text",
|
|
138
|
-
text: trailing
|
|
139
|
-
});
|
|
149
|
+
pushTagToken(tokens, inside, tagNameOf(inside));
|
|
150
|
+
pushTrailingText(tokens, trailing);
|
|
140
151
|
}
|
|
141
152
|
return tokens;
|
|
142
153
|
}
|
|
@@ -145,7 +156,7 @@ function localName(qname) {
|
|
|
145
156
|
return i < 0 ? qname : qname.slice(i + 1);
|
|
146
157
|
}
|
|
147
158
|
function unescapeXml(s) {
|
|
148
|
-
return s.replaceAll(
|
|
159
|
+
return s.replaceAll("<", "<").replaceAll(">", ">").replaceAll(""", "\"").replaceAll("'", "'").replaceAll("&", "&");
|
|
149
160
|
}
|
|
150
161
|
function newNode() {
|
|
151
162
|
return {
|
|
@@ -239,6 +250,8 @@ function parseRfcSoapResponse(xml) {
|
|
|
239
250
|
* with no workspace dependencies (and avoids a circular graph once the
|
|
240
251
|
* CLI wires the two together).
|
|
241
252
|
*/
|
|
253
|
+
/** Explicit `String()` coercion for untyped values. */
|
|
254
|
+
const asText = String;
|
|
242
255
|
function buildSoapRfcUrl(client) {
|
|
243
256
|
const base = "/sap/bc/soap/rfc";
|
|
244
257
|
if (!client) return base;
|
|
@@ -286,9 +299,9 @@ function createRfcClient(config) {
|
|
|
286
299
|
if (typeof value === "object") try {
|
|
287
300
|
return JSON.stringify(value);
|
|
288
301
|
} catch {
|
|
289
|
-
return
|
|
302
|
+
return asText(value);
|
|
290
303
|
}
|
|
291
|
-
return
|
|
304
|
+
return asText(value);
|
|
292
305
|
};
|
|
293
306
|
const xml = coerceToXml(response);
|
|
294
307
|
return parseRfcSoapResponse(xml);
|
package/dist/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../src/lib/transport/types.ts","../src/lib/transport/soap-rfc.ts","../src/lib/client/rfc-client.ts"],"sourcesContent":["/**\n * RFC transport primitives.\n *\n * Flat value types kept deliberately loose — SAP RFC function modules\n * accept scalars, structures (objects), and tables (arrays of objects).\n */\n\n/** A scalar RFC value — string, number, boolean, or bytes (base64) — or null. */\nexport type RfcScalar = string | number | boolean | null;\n\n/** A structure — flat field->scalar mapping. Nested structures allowed. */\nexport interface RfcStructure {\n [field: string]: RfcScalar | RfcStructure | RfcTable;\n}\n\n/** A table — array of structures. */\nexport type RfcTable = RfcStructure[];\n\n/** Top-level RFC parameter map. */\nexport type RfcParams = Record<string, RfcScalar | RfcStructure | RfcTable>;\n\n/** Top-level RFC response map (same shape). */\nexport type RfcResponse = Record<string, RfcScalar | RfcStructure | RfcTable>;\n\n/**\n * Options for one RFC call.\n */\nexport interface RfcCallOptions {\n /** Explicit sap-client override (otherwise uses the client's default). */\n client?: string;\n /** Abort signal. */\n signal?: AbortSignal;\n}\n\n/**\n * SOAP fault raised when the server returns a `soap:Fault`.\n */\nexport class RfcSoapFault extends Error {\n constructor(\n public readonly faultcode: string,\n public readonly faultstring: string,\n public readonly raw: string,\n ) {\n super(`SOAP Fault: ${faultcode}: ${faultstring}`);\n this.name = 'RfcSoapFault';\n }\n}\n\n/**\n * Raised when the HTTP endpoint /sap/bc/soap/rfc is unavailable\n * (401/403/404) — useful for gracefully skipping real-e2e tests\n * against systems where SOAP-RFC is disabled.\n */\nexport class RfcTransportUnavailable extends Error {\n constructor(\n public readonly status: number,\n public readonly url: string,\n public readonly body: string,\n ) {\n super(\n `SOAP-RFC transport unavailable at ${url}: HTTP ${status}. ` +\n `The endpoint /sap/bc/soap/rfc may be disabled on this SAP system.`,\n );\n this.name = 'RfcTransportUnavailable';\n }\n}\n","/**\n * SOAP-over-HTTP RFC transport for SAP.\n *\n * Wraps an RFC call in a SOAP 1.1 envelope targeted at\n * `/sap/bc/soap/rfc?sap-client=<n>` — the SAP NetWeaver built-in\n * wrapper around the NW RFC library. Avoids the native `libsapnwrfc`\n * dependency of node-rfc at the cost of a slightly restricted\n * parameter type space (scalars, structures, tables).\n *\n * No third-party XML parser is used — SAP's SOAP output for RFC is\n * well-formed, stable, and simple enough to handle with a minimal\n * hand-rolled parser (see {@link parseRfcSoapResponse}).\n *\n * Reference: https://wiki.scn.sap.com/wiki/display/Snippets/Calling+RFC+enabled+Function+Modules+via+SOAP\n */\n\nimport {\n RfcSoapFault,\n type RfcParams,\n type RfcResponse,\n type RfcScalar,\n type RfcStructure,\n type RfcTable,\n} from './types';\n\n/** SAP RFC SOAP namespace — fixed. */\nexport const RFC_SOAP_NS = 'urn:sap-com:document:sap:rfc:functions';\n\n/** SOAP 1.1 envelope namespace. */\nexport const SOAP_ENV_NS = 'http://schemas.xmlsoap.org/soap/envelope/';\n\n// ────────────────────────────────────────────────────────────────────────────\n// Envelope builder\n// ────────────────────────────────────────────────────────────────────────────\n\nfunction escapeXml(value: string): string {\n return value\n .replaceAll(/&/g, '&')\n .replaceAll(/</g, '<')\n .replaceAll(/>/g, '>');\n}\n\nfunction renderScalar(value: RfcScalar): string {\n if (value === null || value === undefined) return '';\n if (typeof value === 'boolean') return value ? 'X' : ' ';\n return escapeXml(String(value));\n}\n\nfunction renderStructure(obj: RfcStructure): string {\n let out = '';\n for (const [field, v] of Object.entries(obj)) {\n out += renderField(field, v);\n }\n return out;\n}\n\nfunction renderTable(name: string, rows: RfcTable): string {\n // SAP RFC SOAP convention: <TABLE_NAME><item>…</item><item>…</item></TABLE_NAME>\n const items = rows\n .map((row) => `<item>${renderStructure(row)}</item>`)\n .join('');\n return `<${name}>${items}</${name}>`;\n}\n\nfunction renderField(\n name: string,\n value: RfcScalar | RfcStructure | RfcTable,\n): string {\n if (Array.isArray(value)) return renderTable(name, value);\n if (value !== null && typeof value === 'object') {\n return `<${name}>${renderStructure(value)}</${name}>`;\n }\n return `<${name}>${renderScalar(value)}</${name}>`;\n}\n\n/**\n * Build the SOAP request envelope for an RFC function module call.\n *\n * @param fmName - The RFC function module name (will be upper-cased).\n * @param params - Flat map of importing / changing / tables parameters.\n * @returns XML string suitable for `POST /sap/bc/soap/rfc` with\n * `Content-Type: text/xml; charset=utf-8`.\n */\nexport function buildRfcSoapEnvelope(\n fmName: string,\n params: RfcParams = {},\n): string {\n const fm = fmName.toUpperCase();\n const body = renderStructure(params as RfcStructure);\n\n return (\n `<?xml version=\"1.0\" encoding=\"utf-8\"?>` +\n `<soap-env:Envelope xmlns:soap-env=\"${SOAP_ENV_NS}\">` +\n `<soap-env:Body>` +\n `<urn:${fm} xmlns:urn=\"${RFC_SOAP_NS}\">` +\n body +\n `</urn:${fm}>` +\n `</soap-env:Body>` +\n `</soap-env:Envelope>`\n );\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Envelope parser — minimal, regex + stack based\n// ────────────────────────────────────────────────────────────────────────────\n\ninterface Token {\n kind: 'open' | 'close' | 'self' | 'text';\n name?: string;\n text?: string;\n}\n\n// Linear tokenizer via split on '<' + indexOf('>'). Zero regex on the\n// hot path means CodeQL cannot flag it as polynomial-redos and the\n// runtime is guaranteed O(N).\nconst TAG_NAME_RE = /^\\/?([A-Za-z_][\\w.:-]*)/;\n\nfunction tagNameOf(inside: string): string | undefined {\n const m = TAG_NAME_RE.exec(inside);\n return m?.[1];\n}\n\nfunction tokenize(xml: string): Token[] {\n const tokens: Token[] = [];\n const parts = xml.split('<');\n // parts[0] is whatever text precedes the first '<'.\n if (parts[0] && parts[0].trim().length > 0) {\n tokens.push({ kind: 'text', text: parts[0] });\n }\n for (let i = 1; i < parts.length; i++) {\n const part = parts[i];\n const gt = part.indexOf('>');\n if (gt < 0) {\n // malformed (no '>'): treat everything as literal text\n if (part.trim().length > 0) {\n tokens.push({ kind: 'text', text: '<' + part });\n }\n continue;\n }\n const inside = part.slice(0, gt);\n const trailing = part.slice(gt + 1);\n // Skip XML prolog (<?...?>), DOCTYPE, comments (<!-- -->), and any\n // directive (<!...) — non-structural for RFC parsing.\n if (\n inside.startsWith('?') ||\n inside.startsWith('!--') ||\n inside.startsWith('!DOCTYPE') ||\n inside.startsWith('!')\n ) {\n if (trailing && trailing.trim().length > 0) {\n tokens.push({ kind: 'text', text: trailing });\n }\n continue;\n }\n const name = tagNameOf(inside);\n if (!name) {\n tokens.push({ kind: 'text', text: '<' + inside + '>' });\n } else if (inside.startsWith('/')) {\n tokens.push({ kind: 'close', name });\n } else if (inside.endsWith('/')) {\n tokens.push({ kind: 'self', name });\n } else {\n tokens.push({ kind: 'open', name });\n }\n if (trailing && trailing.trim().length > 0) {\n tokens.push({ kind: 'text', text: trailing });\n }\n }\n return tokens;\n}\n\nfunction localName(qname: string): string {\n const i = qname.indexOf(':');\n return i < 0 ? qname : qname.slice(i + 1);\n}\n\nfunction unescapeXml(s: string): string {\n return s\n .replaceAll(/</g, '<')\n .replaceAll(/>/g, '>')\n .replaceAll(/"/g, '\"')\n .replaceAll(/'/g, \"'\")\n .replaceAll(/&/g, '&');\n}\n\ninterface ParsedNode {\n text: string;\n children: Array<{ name: string; node: ParsedNode }>;\n}\n\nfunction newNode(): ParsedNode {\n return { text: '', children: [] };\n}\n\n/**\n * Parse an XML fragment (no `<?xml …?>` prolog required) into a tree of\n * `ParsedNode`s keyed by local element name. Repeated element names are\n * preserved as separate `children` entries (so `<item>` tables parse as\n * multiple children with the same name).\n */\nfunction parseXmlFragment(xml: string): ParsedNode {\n const tokens = tokenize(xml);\n const root = newNode();\n const stack: ParsedNode[] = [root];\n\n for (const tok of tokens) {\n const top = stack.at(-1)!;\n if (tok.kind === 'open') {\n const child = newNode();\n top.children.push({ name: localName(tok.name!), node: child });\n stack.push(child);\n } else if (tok.kind === 'close') {\n stack.pop();\n } else if (tok.kind === 'self') {\n const child = newNode();\n top.children.push({ name: localName(tok.name!), node: child });\n } else if (tok.kind === 'text') {\n top.text += tok.text ?? '';\n }\n }\n return root;\n}\n\n/**\n * Convert a `ParsedNode` into a JS value:\n * - leaf → string (unescaped)\n * - mixed children with all name=\"item\" → array of objects (table)\n * - object otherwise (structure)\n */\nfunction nodeToValue(node: ParsedNode): RfcScalar | RfcStructure | RfcTable {\n if (node.children.length === 0) {\n return unescapeXml(node.text.trim());\n }\n // table detection: all children named \"item\" (case-insensitive)\n const allItems =\n node.children.length > 0 &&\n node.children.every((c) => c.name.toLowerCase() === 'item');\n if (allItems) {\n return node.children.map((c) => nodeToValue(c.node) as RfcStructure);\n }\n // structure: collapse children into an object; duplicate names → arrays\n const obj: RfcStructure = {};\n for (const { name, node: child } of node.children) {\n const val = nodeToValue(child);\n if (name in obj) {\n const existing = obj[name];\n if (Array.isArray(existing)) {\n existing.push(val as RfcStructure);\n } else {\n obj[name] = [existing as unknown as RfcStructure, val as RfcStructure];\n }\n } else {\n obj[name] = val;\n }\n }\n return obj;\n}\n\n/**\n * Parse a SOAP-RFC response envelope and return the RFC response params.\n *\n * @param xml - Raw response body as returned by `/sap/bc/soap/rfc`.\n * @throws {@link RfcSoapFault} if the envelope contains a `soap:Fault`.\n */\nexport function parseRfcSoapResponse(xml: string): RfcResponse {\n const root = parseXmlFragment(xml);\n\n // Find soap:Envelope/soap:Body\n const envelope = root.children.find(\n (c) => c.name.toLowerCase() === 'envelope',\n );\n if (!envelope) {\n throw new Error(\n 'Invalid SOAP response: no <Envelope> element found. Got: ' +\n xml.slice(0, 200),\n );\n }\n const body = envelope.node.children.find(\n (c) => c.name.toLowerCase() === 'body',\n );\n if (!body) {\n throw new Error('Invalid SOAP response: no <Body> element inside envelope');\n }\n\n // Check for Fault first\n const fault = body.node.children.find(\n (c) => c.name.toLowerCase() === 'fault',\n );\n if (fault) {\n const faultObj = nodeToValue(fault.node) as RfcStructure;\n const faultcode = String(\n (faultObj.faultcode as string | number | undefined) ?? 'Server',\n );\n const faultstring = String(\n (faultObj.faultstring as string | number | undefined) ??\n 'Unknown SOAP fault',\n );\n throw new RfcSoapFault(faultcode, faultstring, xml);\n }\n\n // The RFC response is the single non-fault child of <Body>.\n // SAP names it either `<FMNAME.Response>` or `<FMNAMEResponse>` depending\n // on system version.\n const rfcRoot = body.node.children[0];\n if (!rfcRoot) return {};\n\n const result = nodeToValue(rfcRoot.node);\n if (result === null || typeof result !== 'object') {\n // degenerate scalar response (string, number, boolean, null)\n return {};\n }\n if (Array.isArray(result)) {\n // should not happen for an RFC response root\n return { items: result };\n }\n return result;\n}\n","/**\n * High-level RFC client — SOAP-over-HTTP transport by default.\n *\n * The client is deliberately decoupled from `@abapify/adt-client` at the\n * type level: callers pass a *fetcher* function matching the minimal\n * shape of `AdtClient.fetch()`. This keeps `adt-rfc` a leaf package\n * with no workspace dependencies (and avoids a circular graph once the\n * CLI wires the two together).\n */\n\nimport {\n buildRfcSoapEnvelope,\n parseRfcSoapResponse,\n} from '../transport/soap-rfc';\nimport {\n RfcTransportUnavailable,\n type RfcCallOptions,\n type RfcParams,\n type RfcResponse,\n} from '../transport/types';\n\n/**\n * Raw fetcher signature the RFC client depends on.\n * Matches `AdtClient.fetch()` — callers pass\n * `(url, options) => client.fetch(url, options)`.\n */\nexport type RawFetcher = (\n url: string,\n options: {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n headers?: Record<string, string>;\n body?: string;\n },\n) => Promise<unknown>;\n\nexport interface RfcClientConfig {\n /**\n * A fetcher that performs authenticated HTTP requests against the SAP\n * system. Must already handle CSRF / session cookies.\n *\n * Signature matches `AdtClient.fetch()`.\n */\n fetch: RawFetcher;\n\n /**\n * Default `sap-client` (e.g. \"100\") appended as query parameter.\n * Optional — most SAP deployments can infer it from the cookie.\n */\n client?: string;\n}\n\nexport interface RfcClient {\n /**\n * Invoke an RFC function module synchronously.\n *\n * @param fmName - Function module name (case-insensitive).\n * @param params - Importing / changing / tables parameters (flat map).\n * @param options - Per-call overrides.\n */\n call(\n fmName: string,\n params?: RfcParams,\n options?: RfcCallOptions,\n ): Promise<RfcResponse>;\n}\n\nfunction buildSoapRfcUrl(client?: string): string {\n const base = '/sap/bc/soap/rfc';\n if (!client) return base;\n return `${base}?sap-client=${encodeURIComponent(client)}`;\n}\n\n/**\n * Create an RFC client backed by the SOAP-over-HTTP transport.\n *\n * @example\n * ```ts\n * import { createAdtClient } from '@abapify/adt-client';\n * import { createRfcClient } from '@abapify/adt-rfc';\n *\n * const adt = createAdtClient({ …auth });\n * const rfc = createRfcClient({ fetch: (url, opts) => adt.fetch(url, opts) });\n *\n * const resp = await rfc.call('STFC_CONNECTION', { REQUTEXT: 'hello' });\n * console.log(resp.ECHOTEXT); // \"hello\"\n * ```\n */\nexport function createRfcClient(config: RfcClientConfig): RfcClient {\n return {\n async call(\n fmName: string,\n params: RfcParams = {},\n options: RfcCallOptions = {},\n ): Promise<RfcResponse> {\n const envelope = buildRfcSoapEnvelope(fmName, params);\n const url = buildSoapRfcUrl(options.client ?? config.client);\n\n let response: unknown;\n try {\n response = await config.fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'text/xml; charset=utf-8',\n // SAP expects SOAPAction — empty is fine for RFC SOAP wrapper.\n SOAPAction: '\"\"',\n Accept: 'text/xml',\n },\n body: envelope,\n });\n } catch (err) {\n // Detect HTTP-level \"endpoint not available\" errors raised by the\n // underlying fetcher. `AdtClient.fetch()` throws errors whose\n // `message` usually contains the status code.\n const msg = err instanceof Error ? err.message : String(err);\n const statusMatch = /\\b(401|403|404|501)\\b/.exec(msg);\n if (statusMatch) {\n throw new RfcTransportUnavailable(Number(statusMatch[1]), url, msg);\n }\n throw err;\n }\n\n // Fallback coercion of the fetch result to XML text. JSON.stringify\n // can throw on circular refs or BigInt; guard against that so an\n // unrelated serialization error doesn't mask the real SOAP failure\n // downstream in parseRfcSoapResponse.\n const coerceToXml = (value: unknown): string => {\n if (typeof value === 'string') return value;\n if (value === null || value === undefined) return '';\n if (typeof value === 'object') {\n try {\n return JSON.stringify(value);\n } catch {\n return String(value);\n }\n }\n return String(value);\n };\n const xml = coerceToXml(response);\n return parseRfcSoapResponse(xml);\n },\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/lib/transport/types.ts","../src/lib/transport/soap-rfc.ts","../src/lib/client/rfc-client.ts"],"sourcesContent":["/**\n * RFC transport primitives.\n *\n * Flat value types kept deliberately loose — SAP RFC function modules\n * accept scalars, structures (objects), and tables (arrays of objects).\n */\n\n/** A scalar RFC value — string, number, boolean, or bytes (base64) — or null. */\nexport type RfcScalar = string | number | boolean | null;\n\n/** A structure — flat field->scalar mapping. Nested structures allowed. */\nexport interface RfcStructure {\n [field: string]: RfcScalar | RfcStructure | RfcTable;\n}\n\n/** A table — array of structures. */\nexport type RfcTable = RfcStructure[];\n\n/** Top-level RFC parameter map. */\nexport type RfcParams = Record<string, RfcScalar | RfcStructure | RfcTable>;\n\n/** Top-level RFC response map (same shape). */\nexport type RfcResponse = Record<string, RfcScalar | RfcStructure | RfcTable>;\n\n/**\n * Options for one RFC call.\n */\nexport interface RfcCallOptions {\n /** Explicit sap-client override (otherwise uses the client's default). */\n client?: string;\n /** Abort signal. */\n signal?: AbortSignal;\n}\n\n/**\n * SOAP fault raised when the server returns a `soap:Fault`.\n */\nexport class RfcSoapFault extends Error {\n constructor(\n public readonly faultcode: string,\n public readonly faultstring: string,\n public readonly raw: string,\n ) {\n super(`SOAP Fault: ${faultcode}: ${faultstring}`);\n this.name = 'RfcSoapFault';\n }\n}\n\n/**\n * Raised when the HTTP endpoint /sap/bc/soap/rfc is unavailable\n * (401/403/404) — useful for gracefully skipping real-e2e tests\n * against systems where SOAP-RFC is disabled.\n */\nexport class RfcTransportUnavailable extends Error {\n constructor(\n public readonly status: number,\n public readonly url: string,\n public readonly body: string,\n ) {\n super(\n `SOAP-RFC transport unavailable at ${url}: HTTP ${status}. ` +\n `The endpoint /sap/bc/soap/rfc may be disabled on this SAP system.`,\n );\n this.name = 'RfcTransportUnavailable';\n }\n}\n","/**\n * SOAP-over-HTTP RFC transport for SAP.\n *\n * Wraps an RFC call in a SOAP 1.1 envelope targeted at\n * `/sap/bc/soap/rfc?sap-client=<n>` — the SAP NetWeaver built-in\n * wrapper around the NW RFC library. Avoids the native `libsapnwrfc`\n * dependency of node-rfc at the cost of a slightly restricted\n * parameter type space (scalars, structures, tables).\n *\n * No third-party XML parser is used — SAP's SOAP output for RFC is\n * well-formed, stable, and simple enough to handle with a minimal\n * hand-rolled parser (see {@link parseRfcSoapResponse}).\n *\n * Reference: https://wiki.scn.sap.com/wiki/display/Snippets/Calling+RFC+enabled+Function+Modules+via+SOAP\n */\n\nimport {\n RfcSoapFault,\n type RfcParams,\n type RfcResponse,\n type RfcScalar,\n type RfcStructure,\n type RfcTable,\n} from './types';\n\n/** SAP RFC SOAP namespace — fixed. */\nexport const RFC_SOAP_NS = 'urn:sap-com:document:sap:rfc:functions';\n\n/** SOAP 1.1 envelope namespace. */\nexport const SOAP_ENV_NS = 'http://schemas.xmlsoap.org/soap/envelope/';\n\n// ────────────────────────────────────────────────────────────────────────────\n// Envelope builder\n// ────────────────────────────────────────────────────────────────────────────\n\nfunction escapeXml(value: string): string {\n return value\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>');\n}\n\nfunction renderScalar(value: RfcScalar): string {\n if (value === null || value === undefined) return '';\n if (typeof value === 'boolean') return value ? 'X' : ' ';\n return escapeXml(String(value));\n}\n\nfunction renderStructure(obj: RfcStructure): string {\n let out = '';\n for (const [field, v] of Object.entries(obj)) {\n out += renderField(field, v);\n }\n return out;\n}\n\nfunction renderTable(name: string, rows: RfcTable): string {\n // SAP RFC SOAP convention: <TABLE_NAME><item>…</item><item>…</item></TABLE_NAME>\n const items = rows\n .map((row) => `<item>${renderStructure(row)}</item>`)\n .join('');\n return `<${name}>${items}</${name}>`;\n}\n\nfunction renderField(\n name: string,\n value: RfcScalar | RfcStructure | RfcTable,\n): string {\n if (Array.isArray(value)) return renderTable(name, value);\n if (value !== null && typeof value === 'object') {\n return `<${name}>${renderStructure(value)}</${name}>`;\n }\n return `<${name}>${renderScalar(value)}</${name}>`;\n}\n\n/**\n * Build the SOAP request envelope for an RFC function module call.\n *\n * @param fmName - The RFC function module name (will be upper-cased).\n * @param params - Flat map of importing / changing / tables parameters.\n * @returns XML string suitable for `POST /sap/bc/soap/rfc` with\n * `Content-Type: text/xml; charset=utf-8`.\n */\nexport function buildRfcSoapEnvelope(\n fmName: string,\n params: RfcParams = {},\n): string {\n const fm = fmName.toUpperCase();\n const body = renderStructure(params as RfcStructure);\n\n return (\n `<?xml version=\"1.0\" encoding=\"utf-8\"?>` +\n `<soap-env:Envelope xmlns:soap-env=\"${SOAP_ENV_NS}\">` +\n `<soap-env:Body>` +\n `<urn:${fm} xmlns:urn=\"${RFC_SOAP_NS}\">` +\n body +\n `</urn:${fm}>` +\n `</soap-env:Body>` +\n `</soap-env:Envelope>`\n );\n}\n\n// ────────────────────────────────────────────────────────────────────────────\n// Envelope parser — minimal, regex + stack based\n// ────────────────────────────────────────────────────────────────────────────\n\ninterface Token {\n kind: 'open' | 'close' | 'self' | 'text';\n name?: string;\n text?: string;\n}\n\n// Linear tokenizer via split on '<' + indexOf('>'). Zero regex on the\n// hot path means CodeQL cannot flag it as polynomial-redos and the\n// runtime is guaranteed O(N).\nconst TAG_NAME_RE = /^\\/?([A-Za-z_][\\w.:-]*)/;\n\nfunction tagNameOf(inside: string): string | undefined {\n const m = TAG_NAME_RE.exec(inside);\n return m?.[1];\n}\n\n/**\n * Skip XML prolog (<?...?>), DOCTYPE, comments (<!-- -->), and any\n * directive (<!...) — non-structural for RFC parsing.\n */\nfunction isDirectiveInside(inside: string): boolean {\n return (\n inside.startsWith('?') ||\n inside.startsWith('!--') ||\n inside.startsWith('!DOCTYPE') ||\n inside.startsWith('!')\n );\n}\n\n/** Push trailing text after a tag when it carries non-whitespace content. */\nfunction pushTrailingText(tokens: Token[], trailing: string): void {\n if (trailing && trailing.trim().length > 0) {\n tokens.push({ kind: 'text', text: trailing });\n }\n}\n\n/** Push the structural token for a parsed tag body. */\nfunction pushTagToken(\n tokens: Token[],\n inside: string,\n name: string | undefined,\n): void {\n if (!name) {\n tokens.push({ kind: 'text', text: '<' + inside + '>' });\n } else if (inside.startsWith('/')) {\n tokens.push({ kind: 'close', name });\n } else if (inside.endsWith('/')) {\n tokens.push({ kind: 'self', name });\n } else {\n tokens.push({ kind: 'open', name });\n }\n}\n\nfunction tokenize(xml: string): Token[] {\n const tokens: Token[] = [];\n const parts = xml.split('<');\n // parts[0] is whatever text precedes the first '<'.\n if (parts[0] && parts[0].trim().length > 0) {\n tokens.push({ kind: 'text', text: parts[0] });\n }\n for (let i = 1; i < parts.length; i++) {\n const part = parts[i];\n const gt = part.indexOf('>');\n if (gt < 0) {\n // malformed (no '>'): treat everything as literal text\n if (part.trim().length > 0) {\n tokens.push({ kind: 'text', text: '<' + part });\n }\n continue;\n }\n const inside = part.slice(0, gt);\n const trailing = part.slice(gt + 1);\n if (isDirectiveInside(inside)) {\n pushTrailingText(tokens, trailing);\n continue;\n }\n pushTagToken(tokens, inside, tagNameOf(inside));\n pushTrailingText(tokens, trailing);\n }\n return tokens;\n}\n\nfunction localName(qname: string): string {\n const i = qname.indexOf(':');\n return i < 0 ? qname : qname.slice(i + 1);\n}\n\nfunction unescapeXml(s: string): string {\n return s\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('"', '\"')\n .replaceAll(''', \"'\")\n .replaceAll('&', '&');\n}\n\ninterface ParsedNode {\n text: string;\n children: Array<{ name: string; node: ParsedNode }>;\n}\n\nfunction newNode(): ParsedNode {\n return { text: '', children: [] };\n}\n\n/**\n * Parse an XML fragment (no `<?xml …?>` prolog required) into a tree of\n * `ParsedNode`s keyed by local element name. Repeated element names are\n * preserved as separate `children` entries (so `<item>` tables parse as\n * multiple children with the same name).\n */\nfunction parseXmlFragment(xml: string): ParsedNode {\n const tokens = tokenize(xml);\n const root = newNode();\n const stack: ParsedNode[] = [root];\n\n for (const tok of tokens) {\n const top = stack.at(-1)!;\n if (tok.kind === 'open') {\n const child = newNode();\n top.children.push({ name: localName(tok.name!), node: child });\n stack.push(child);\n } else if (tok.kind === 'close') {\n stack.pop();\n } else if (tok.kind === 'self') {\n const child = newNode();\n top.children.push({ name: localName(tok.name!), node: child });\n } else if (tok.kind === 'text') {\n top.text += tok.text ?? '';\n }\n }\n return root;\n}\n\n/**\n * Convert a `ParsedNode` into a JS value:\n * - leaf → string (unescaped)\n * - mixed children with all name=\"item\" → array of objects (table)\n * - object otherwise (structure)\n */\nfunction nodeToValue(node: ParsedNode): RfcScalar | RfcStructure | RfcTable {\n if (node.children.length === 0) {\n return unescapeXml(node.text.trim());\n }\n // table detection: all children named \"item\" (case-insensitive)\n const allItems =\n node.children.length > 0 &&\n node.children.every((c) => c.name.toLowerCase() === 'item');\n if (allItems) {\n return node.children.map((c) => nodeToValue(c.node) as RfcStructure);\n }\n // structure: collapse children into an object; duplicate names → arrays\n const obj: RfcStructure = {};\n for (const { name, node: child } of node.children) {\n const val = nodeToValue(child);\n if (name in obj) {\n const existing = obj[name];\n if (Array.isArray(existing)) {\n existing.push(val as RfcStructure);\n } else {\n obj[name] = [existing as unknown as RfcStructure, val as RfcStructure];\n }\n } else {\n obj[name] = val;\n }\n }\n return obj;\n}\n\n/**\n * Parse a SOAP-RFC response envelope and return the RFC response params.\n *\n * @param xml - Raw response body as returned by `/sap/bc/soap/rfc`.\n * @throws {@link RfcSoapFault} if the envelope contains a `soap:Fault`.\n */\nexport function parseRfcSoapResponse(xml: string): RfcResponse {\n const root = parseXmlFragment(xml);\n\n // Find soap:Envelope/soap:Body\n const envelope = root.children.find(\n (c) => c.name.toLowerCase() === 'envelope',\n );\n if (!envelope) {\n throw new Error(\n 'Invalid SOAP response: no <Envelope> element found. Got: ' +\n xml.slice(0, 200),\n );\n }\n const body = envelope.node.children.find(\n (c) => c.name.toLowerCase() === 'body',\n );\n if (!body) {\n throw new Error('Invalid SOAP response: no <Body> element inside envelope');\n }\n\n // Check for Fault first\n const fault = body.node.children.find(\n (c) => c.name.toLowerCase() === 'fault',\n );\n if (fault) {\n const faultObj = nodeToValue(fault.node) as RfcStructure;\n const faultcode = String(\n (faultObj.faultcode as string | number | undefined) ?? 'Server',\n );\n const faultstring = String(\n (faultObj.faultstring as string | number | undefined) ??\n 'Unknown SOAP fault',\n );\n throw new RfcSoapFault(faultcode, faultstring, xml);\n }\n\n // The RFC response is the single non-fault child of <Body>.\n // SAP names it either `<FMNAME.Response>` or `<FMNAMEResponse>` depending\n // on system version.\n const rfcRoot = body.node.children[0];\n if (!rfcRoot) return {};\n\n const result = nodeToValue(rfcRoot.node);\n if (result === null || typeof result !== 'object') {\n // degenerate scalar response (string, number, boolean, null)\n return {};\n }\n if (Array.isArray(result)) {\n // should not happen for an RFC response root\n return { items: result };\n }\n return result;\n}\n","/**\n * High-level RFC client — SOAP-over-HTTP transport by default.\n *\n * The client is deliberately decoupled from `@abapify/adt-client` at the\n * type level: callers pass a *fetcher* function matching the minimal\n * shape of `AdtClient.fetch()`. This keeps `adt-rfc` a leaf package\n * with no workspace dependencies (and avoids a circular graph once the\n * CLI wires the two together).\n */\n\nimport {\n buildRfcSoapEnvelope,\n parseRfcSoapResponse,\n} from '../transport/soap-rfc';\nimport {\n RfcTransportUnavailable,\n type RfcCallOptions,\n type RfcParams,\n type RfcResponse,\n} from '../transport/types';\n\n/** Explicit `String()` coercion for untyped values. */\nconst asText: (value: unknown) => string = String;\n\n/**\n * Raw fetcher signature the RFC client depends on.\n * Matches `AdtClient.fetch()` — callers pass\n * `(url, options) => client.fetch(url, options)`.\n */\nexport type RawFetcher = (\n url: string,\n options: {\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';\n headers?: Record<string, string>;\n body?: string;\n },\n) => Promise<unknown>;\n\nexport interface RfcClientConfig {\n /**\n * A fetcher that performs authenticated HTTP requests against the SAP\n * system. Must already handle CSRF / session cookies.\n *\n * Signature matches `AdtClient.fetch()`.\n */\n fetch: RawFetcher;\n\n /**\n * Default `sap-client` (e.g. \"100\") appended as query parameter.\n * Optional — most SAP deployments can infer it from the cookie.\n */\n client?: string;\n}\n\nexport interface RfcClient {\n /**\n * Invoke an RFC function module synchronously.\n *\n * @param fmName - Function module name (case-insensitive).\n * @param params - Importing / changing / tables parameters (flat map).\n * @param options - Per-call overrides.\n */\n call(\n fmName: string,\n params?: RfcParams,\n options?: RfcCallOptions,\n ): Promise<RfcResponse>;\n}\n\nfunction buildSoapRfcUrl(client?: string): string {\n const base = '/sap/bc/soap/rfc';\n if (!client) return base;\n return `${base}?sap-client=${encodeURIComponent(client)}`;\n}\n\n/**\n * Create an RFC client backed by the SOAP-over-HTTP transport.\n *\n * @example\n * ```ts\n * import { createAdtClient } from '@abapify/adt-client';\n * import { createRfcClient } from '@abapify/adt-rfc';\n *\n * const adt = createAdtClient({ …auth });\n * const rfc = createRfcClient({ fetch: (url, opts) => adt.fetch(url, opts) });\n *\n * const resp = await rfc.call('STFC_CONNECTION', { REQUTEXT: 'hello' });\n * console.log(resp.ECHOTEXT); // \"hello\"\n * ```\n */\nexport function createRfcClient(config: RfcClientConfig): RfcClient {\n return {\n async call(\n fmName: string,\n params: RfcParams = {},\n options: RfcCallOptions = {},\n ): Promise<RfcResponse> {\n const envelope = buildRfcSoapEnvelope(fmName, params);\n const url = buildSoapRfcUrl(options.client ?? config.client);\n\n let response: unknown;\n try {\n response = await config.fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'text/xml; charset=utf-8',\n // SAP expects SOAPAction — empty is fine for RFC SOAP wrapper.\n SOAPAction: '\"\"',\n Accept: 'text/xml',\n },\n body: envelope,\n });\n } catch (err) {\n // Detect HTTP-level \"endpoint not available\" errors raised by the\n // underlying fetcher. `AdtClient.fetch()` throws errors whose\n // `message` usually contains the status code.\n const msg = err instanceof Error ? err.message : String(err);\n const statusMatch = /\\b(401|403|404|501)\\b/.exec(msg);\n if (statusMatch) {\n throw new RfcTransportUnavailable(Number(statusMatch[1]), url, msg);\n }\n throw err;\n }\n\n // Fallback coercion of the fetch result to XML text. JSON.stringify\n // can throw on circular refs or BigInt; guard against that so an\n // unrelated serialization error doesn't mask the real SOAP failure\n // downstream in parseRfcSoapResponse.\n const coerceToXml = (value: unknown): string => {\n if (typeof value === 'string') return value;\n if (value === null || value === undefined) return '';\n if (typeof value === 'object') {\n try {\n return JSON.stringify(value);\n } catch {\n return asText(value);\n }\n }\n return asText(value);\n };\n const xml = coerceToXml(response);\n return parseRfcSoapResponse(xml);\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED