@mcp-abap-adt/sap-rfc-lite 0.1.0 → 0.2.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 +31 -0
- package/NOTICE +5 -0
- package/README.md +150 -0
- package/binding.gyp +2 -0
- package/package.json +8 -6
- package/src/cpp/Client.cc +49 -0
- package/src/cpp/Client.h +2 -0
- package/src/cpp/addon.cc +1 -1
- package/src/cpp/noderfc.h +9 -2
- package/src/ts/binding.ts +1 -0
- package/src/ts/client.ts +19 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@mcp-abap-adt/sap-rfc-lite` are documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
Releases before 0.2.0 were not recorded here.
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-09-27
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **`client.resetServerContext(): Promise<void>`** wraps the SDK's
|
|
15
|
+
`RfcResetServerContext`. It ends the ABAP session context the connection
|
|
16
|
+
holds and keeps the connection open, so the next call runs in a fresh
|
|
17
|
+
context without a new logon.
|
|
18
|
+
|
|
19
|
+
What the context held is gone after the reset, including enqueue locks and
|
|
20
|
+
program buffers. That is how a stateless call gets a clean session on a
|
|
21
|
+
connection that stays open.
|
|
22
|
+
|
|
23
|
+
Measured on E19 (SAP_BASIS 816) through ADT's `SADT_REST_RFC_ENDPOINT`:
|
|
24
|
+
- Without a reset, a package created or written on a kept connection could
|
|
25
|
+
not be written there again (400 PAK/058, `CL_PACKAGE`'s session buffer).
|
|
26
|
+
- With a reset after each call, create, read, two writes and a delete all
|
|
27
|
+
passed on one connection.
|
|
28
|
+
- A reset costs about 0.1 s. Opening a new connection for each call costs
|
|
29
|
+
about 0.5 s.
|
|
30
|
+
|
|
31
|
+
[0.2.0]: https://github.com/fr0ster/sap-rfc-lite/releases/tag/v0.2.0
|
package/NOTICE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
This project is based on node-rfc (https://github.com/SAP/node-rfc)
|
|
2
|
+
Copyright 2014 SAP SE, Srdjan Boskovic <srdjan.boskovic@sap.com>
|
|
3
|
+
Licensed under the Apache License, Version 2.0
|
|
4
|
+
|
|
5
|
+
Modifications: Removed Pool, Server, Throughput classes. Simplified Client API to Promise-only.
|
package/README.md
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# sap-rfc-lite
|
|
2
|
+
[](https://stand-with-ukraine.pp.ua)
|
|
3
|
+
|
|
4
|
+
Lightweight Node.js bindings for SAP NetWeaver RFC SDK.
|
|
5
|
+
|
|
6
|
+
## Motivation
|
|
7
|
+
|
|
8
|
+
The only existing open-source Node.js binding for SAP NW RFC SDK is [node-rfc](https://github.com/SAP/node-rfc) by SAP. While it served the community well, it has accumulated significant technical debt:
|
|
9
|
+
|
|
10
|
+
- **24 known vulnerabilities** (5 low, 7 moderate, 12 high) in its dependency tree
|
|
11
|
+
- **18 outdated packages**, including runtime dependencies like `bluebird` (a Promise library unnecessary since Node.js 8+) and `decimal.js`
|
|
12
|
+
- **Repository archived** — the source has moved to [SAP-archive/node-rfc](https://github.com/SAP-archive/node-rfc), signaling deprecation
|
|
13
|
+
- **Last published 2+ years ago** with no signs of active maintenance
|
|
14
|
+
- **~14,000 lines of source code** for what is fundamentally an RFC client binding
|
|
15
|
+
|
|
16
|
+
SAP has since created a proprietary replacement (`@sap-rfc/node-rfc-library`), but it is only available through a private npm registry and requires an S-user with a SAP Build Code license — making it inaccessible to the broader community.
|
|
17
|
+
|
|
18
|
+
No other lightweight alternative exists in the Node.js ecosystem. The only other package, [sapnwrfc](https://www.npmjs.com/package/sapnwrfc), was last published over 10 years ago.
|
|
19
|
+
|
|
20
|
+
### Comparison
|
|
21
|
+
|
|
22
|
+
| | sap-rfc-lite | node-rfc |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| Source code | ~2,200 lines | ~14,200 lines |
|
|
25
|
+
| Runtime dependencies | 2 | 4 (includes bluebird, decimal.js) |
|
|
26
|
+
| Vulnerabilities (npm audit) | **0** | **24** |
|
|
27
|
+
| Outdated packages | 4 (dev only) | 18 (including runtime) |
|
|
28
|
+
| API | Promise-based Client | Client, Pool, Server, Throughput |
|
|
29
|
+
| Node.js requirement | >= 18 | >= 18 |
|
|
30
|
+
| N-API version | 8 | 8 |
|
|
31
|
+
|
|
32
|
+
This project focuses on what most users actually need — a clean, modern, Promise-based RFC client — without the baggage of unused features and vulnerable dependencies.
|
|
33
|
+
|
|
34
|
+
## Prerequisites
|
|
35
|
+
|
|
36
|
+
- Node.js >= 18
|
|
37
|
+
- SAP NetWeaver RFC SDK installed
|
|
38
|
+
- C++ build tools (Visual Studio Build Tools on Windows, GCC/Clang on Linux/macOS)
|
|
39
|
+
|
|
40
|
+
Set the `SAPNWRFC_HOME` environment variable to the SDK root directory (the folder containing `lib/` and `include/`).
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @mcp-abap-adt/sap-rfc-lite
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Runtime library loading
|
|
49
|
+
|
|
50
|
+
The addon is linked against the SDK shared libraries (`libsapnwrfc`, `libsapucum`) and, on Linux, the system `libuuid`. The dynamic loader must find all of them when the addon is loaded.
|
|
51
|
+
|
|
52
|
+
On Linux and macOS the build embeds an rpath to `$SAPNWRFC_HOME/lib`, so the SDK libraries are found without extra settings as long as the SDK stays where it was at build time. Set the library search path when:
|
|
53
|
+
|
|
54
|
+
- the SDK was moved, or the built addon is deployed to a machine where the SDK lives elsewhere;
|
|
55
|
+
- Node.js ships its own glibc and does not search the system library directories (for example, Node.js installed via Homebrew on Linux). Loading then fails with `ERR_DLOPEN_FAILED` and `libuuid.so.1: cannot open shared object file`.
|
|
56
|
+
|
|
57
|
+
| OS | Variable |
|
|
58
|
+
|---|---|
|
|
59
|
+
| Linux | `LD_LIBRARY_PATH` |
|
|
60
|
+
| macOS | `DYLD_LIBRARY_PATH` |
|
|
61
|
+
| Windows | `PATH` (must include `%SAPNWRFC_HOME%\lib`) |
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# SDK moved or deployed elsewhere
|
|
65
|
+
export LD_LIBRARY_PATH="$SAPNWRFC_HOME/lib:$LD_LIBRARY_PATH"
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For a Node.js with its own glibc, do **not** add the whole system library directory (`/lib/x86_64-linux-gnu`): the loader would then take the system glibc, which may be older than the one Node.js was built against (`GLIBC_2.38 not found`). Expose only the missing library instead:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
mkdir -p ~/.local/lib/sap-rfc-lite
|
|
72
|
+
ln -sf /lib/x86_64-linux-gnu/libuuid.so.1 ~/.local/lib/sap-rfc-lite/
|
|
73
|
+
export LD_LIBRARY_PATH="$HOME/.local/lib/sap-rfc-lite:$SAPNWRFC_HOME/lib:$LD_LIBRARY_PATH"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`binding.bindingVersions` reports the package version and the loaded SDK version, which is a quick check that the addon loads:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
node -e "console.log(require('@mcp-abap-adt/sap-rfc-lite').binding.bindingVersions)"
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
import { Client } from '@mcp-abap-adt/sap-rfc-lite';
|
|
86
|
+
|
|
87
|
+
const client = new Client({
|
|
88
|
+
ashost: '10.0.0.1',
|
|
89
|
+
sysnr: '00',
|
|
90
|
+
client: '100',
|
|
91
|
+
user: 'USER',
|
|
92
|
+
passwd: 'PASSWORD',
|
|
93
|
+
lang: 'EN',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
await client.open();
|
|
97
|
+
|
|
98
|
+
const result = await client.call('STFC_CONNECTION', {
|
|
99
|
+
REQUTEXT: 'Hello from sap-rfc-lite',
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log(result.ECHOTEXT);
|
|
103
|
+
|
|
104
|
+
await client.close();
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## API
|
|
108
|
+
|
|
109
|
+
### `new Client(connectionParameters)`
|
|
110
|
+
|
|
111
|
+
Creates a new RFC client instance.
|
|
112
|
+
|
|
113
|
+
- `connectionParameters` — an object with SAP connection parameters (`ashost`, `sysnr`, `client`, `user`, `passwd`, `lang`, etc.)
|
|
114
|
+
|
|
115
|
+
### `client.open(): Promise<Client>`
|
|
116
|
+
|
|
117
|
+
Opens the RFC connection.
|
|
118
|
+
|
|
119
|
+
### `client.call(rfmName, rfmParams?): Promise<RfcObject>`
|
|
120
|
+
|
|
121
|
+
Invokes a remote function module.
|
|
122
|
+
|
|
123
|
+
- `rfmName` — name of the function module
|
|
124
|
+
- `rfmParams` — optional input parameters
|
|
125
|
+
|
|
126
|
+
### `client.resetServerContext(): Promise<void>`
|
|
127
|
+
|
|
128
|
+
Resets the ABAP session context the connection holds and keeps the connection
|
|
129
|
+
open (`RfcResetServerContext`). The next call runs in a fresh context, without
|
|
130
|
+
a new logon.
|
|
131
|
+
|
|
132
|
+
Everything the context held goes with it, including enqueue locks and program
|
|
133
|
+
buffers. This is how a stateless call gets a clean session on a connection
|
|
134
|
+
that is kept open.
|
|
135
|
+
|
|
136
|
+
### `client.close(): Promise<void>`
|
|
137
|
+
|
|
138
|
+
Closes the RFC connection.
|
|
139
|
+
|
|
140
|
+
### `client.id: number`
|
|
141
|
+
|
|
142
|
+
Unique client instance identifier.
|
|
143
|
+
|
|
144
|
+
### `client.alive: boolean`
|
|
145
|
+
|
|
146
|
+
Whether the connection is currently open.
|
|
147
|
+
|
|
148
|
+
## License
|
|
149
|
+
|
|
150
|
+
[Apache-2.0](LICENSE)
|
package/binding.gyp
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
'nwrfcsdk_lib_dir': '<(nwrfcsdk_dir)/lib',
|
|
14
14
|
'napi_include_dir': "<!(node -p \"require('node-addon-api').include_dir\")",
|
|
15
15
|
'napi_version': "<!(node -p \"require('./package.json').config.napi_version\")",
|
|
16
|
+
'package_version': "<!(node -p \"require('./package.json').version\")",
|
|
16
17
|
'node_abi_version': '<!(node -p "process.versions.modules")',
|
|
17
18
|
# per NodeJS build requirements: https://github.com/nodejs/node/blob/main/BUILDING.md
|
|
18
19
|
'macosx_version_min': '10.15',
|
|
@@ -92,6 +93,7 @@
|
|
|
92
93
|
'SAPwithTHREADS',
|
|
93
94
|
'NAPI_CPP_EXCEPTIONS',
|
|
94
95
|
'NAPI_VERSION=<(napi_version)',
|
|
96
|
+
'NODERFC_VERSION=<(package_version)',
|
|
95
97
|
'sapnwrfc_EXPORTS'
|
|
96
98
|
],
|
|
97
99
|
'conditions': [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mcp-abap-adt/sap-rfc-lite",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Lightweight Node.js bindings for SAP NW RFC SDK",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"binding.gyp",
|
|
10
10
|
"src",
|
|
11
|
-
"lib"
|
|
11
|
+
"lib",
|
|
12
|
+
"NOTICE",
|
|
13
|
+
"CHANGELOG.md"
|
|
12
14
|
],
|
|
13
15
|
"scripts": {
|
|
14
16
|
"install": "node-gyp-build",
|
|
@@ -25,15 +27,15 @@
|
|
|
25
27
|
"napi_version": 8
|
|
26
28
|
},
|
|
27
29
|
"dependencies": {
|
|
28
|
-
"node-addon-api": "^
|
|
30
|
+
"node-addon-api": "^8.9.2",
|
|
29
31
|
"node-gyp-build": "^4.6.0"
|
|
30
32
|
},
|
|
31
33
|
"devDependencies": {
|
|
32
|
-
"@biomejs/biome": "^2.
|
|
33
|
-
"@types/jest": "^
|
|
34
|
+
"@biomejs/biome": "^2.5.14",
|
|
35
|
+
"@types/jest": "^30.0.0",
|
|
34
36
|
"@types/node": "^20.0.0",
|
|
35
37
|
"husky": "^9.1.7",
|
|
36
|
-
"jest": "^
|
|
38
|
+
"jest": "^30.5.1",
|
|
37
39
|
"ts-jest": "^29.0.0",
|
|
38
40
|
"ts-node": "^10.9.2",
|
|
39
41
|
"typescript": "^5.0.0"
|
package/src/cpp/Client.cc
CHANGED
|
@@ -48,6 +48,7 @@ Napi::Object Client::Init(Napi::Env env, Napi::Object exports) {
|
|
|
48
48
|
InstanceAccessor("_alive", &Client::AliveGetter, nullptr),
|
|
49
49
|
InstanceMethod("open", &Client::Open),
|
|
50
50
|
InstanceMethod("close", &Client::Close),
|
|
51
|
+
InstanceMethod("resetServerContext", &Client::ResetServerContext),
|
|
51
52
|
InstanceMethod("invoke", &Client::Invoke),
|
|
52
53
|
});
|
|
53
54
|
|
|
@@ -186,6 +187,42 @@ class CloseAsync : public Napi::AsyncWorker {
|
|
|
186
187
|
bool conn_closed = false;
|
|
187
188
|
};
|
|
188
189
|
|
|
190
|
+
// RfcResetServerContext: ends the ABAP session context ("user context") the
|
|
191
|
+
// connection holds and keeps the connection open, so the next call runs in a
|
|
192
|
+
// fresh context without a new logon.
|
|
193
|
+
class ResetServerContextAsync : public Napi::AsyncWorker {
|
|
194
|
+
public:
|
|
195
|
+
ResetServerContextAsync(Napi::Function& callback, Client* client)
|
|
196
|
+
: Napi::AsyncWorker(callback), client(client) {}
|
|
197
|
+
~ResetServerContextAsync() {}
|
|
198
|
+
|
|
199
|
+
void Execute() {
|
|
200
|
+
client->LockMutex();
|
|
201
|
+
conn_closed = (client->connectionHandle == nullptr);
|
|
202
|
+
if (!conn_closed) {
|
|
203
|
+
RfcResetServerContext(client->connectionHandle, &errorInfo);
|
|
204
|
+
}
|
|
205
|
+
client->UnlockMutex();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
void OnOK() {
|
|
209
|
+
Napi::HandleScope scope(Env());
|
|
210
|
+
if (conn_closed) {
|
|
211
|
+
Callback().Call({client->connectionClosedError("resetServerContext()")});
|
|
212
|
+
} else if (errorInfo.code != RFC_OK) {
|
|
213
|
+
Callback().Call({rfcSdkError(&errorInfo)});
|
|
214
|
+
} else {
|
|
215
|
+
Callback().Call({});
|
|
216
|
+
}
|
|
217
|
+
Callback().Reset();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private:
|
|
221
|
+
Client* client;
|
|
222
|
+
RFC_ERROR_INFO errorInfo;
|
|
223
|
+
bool conn_closed = false;
|
|
224
|
+
};
|
|
225
|
+
|
|
189
226
|
class InvokeAsync : public Napi::AsyncWorker {
|
|
190
227
|
public:
|
|
191
228
|
InvokeAsync(Napi::Function& callback,
|
|
@@ -412,6 +449,18 @@ Napi::Value Client::Close(const Napi::CallbackInfo& info) {
|
|
|
412
449
|
return info.Env().Undefined();
|
|
413
450
|
}
|
|
414
451
|
|
|
452
|
+
Napi::Value Client::ResetServerContext(const Napi::CallbackInfo& info) {
|
|
453
|
+
if (!info[0].IsFunction()) {
|
|
454
|
+
Napi::TypeError::New(
|
|
455
|
+
info.Env(), "Client resetServerContext() requires a callback function")
|
|
456
|
+
.ThrowAsJavaScriptException();
|
|
457
|
+
return info.Env().Undefined();
|
|
458
|
+
}
|
|
459
|
+
Napi::Function callback = info[0].As<Napi::Function>();
|
|
460
|
+
(new ResetServerContextAsync(callback, this))->Queue();
|
|
461
|
+
return info.Env().Undefined();
|
|
462
|
+
}
|
|
463
|
+
|
|
415
464
|
Napi::Value Client::Invoke(const Napi::CallbackInfo& info) {
|
|
416
465
|
Napi::Array notRequested = Napi::Array::New(info.Env());
|
|
417
466
|
Napi::Value bcd;
|
package/src/cpp/Client.h
CHANGED
|
@@ -16,6 +16,7 @@ class Client : public Napi::ObjectWrap<Client> {
|
|
|
16
16
|
public:
|
|
17
17
|
friend class OpenAsync;
|
|
18
18
|
friend class CloseAsync;
|
|
19
|
+
friend class ResetServerContextAsync;
|
|
19
20
|
friend class PrepareAsync;
|
|
20
21
|
friend class InvokeAsync;
|
|
21
22
|
static Napi::Object Init(Napi::Env env, Napi::Object exports);
|
|
@@ -39,6 +40,7 @@ class Client : public Napi::ObjectWrap<Client> {
|
|
|
39
40
|
|
|
40
41
|
Napi::Value Open(const Napi::CallbackInfo& info);
|
|
41
42
|
Napi::Value Close(const Napi::CallbackInfo& info);
|
|
43
|
+
Napi::Value ResetServerContext(const Napi::CallbackInfo& info);
|
|
42
44
|
Napi::Value Invoke(const Napi::CallbackInfo& info);
|
|
43
45
|
|
|
44
46
|
RfmErrorPath errorPath;
|
package/src/cpp/addon.cc
CHANGED
|
@@ -21,7 +21,7 @@ Napi::Value BindingVersions(Napi::Env env) {
|
|
|
21
21
|
nwrfcsdk.Set("patchLevel", patchLevel);
|
|
22
22
|
|
|
23
23
|
Napi::Object version = Napi::Object::New(env);
|
|
24
|
-
version.Set("version",
|
|
24
|
+
version.Set("version", NODERFC_VERSION_STRING);
|
|
25
25
|
version.Set("nwrfcsdk", nwrfcsdk);
|
|
26
26
|
|
|
27
27
|
return scope.Escape(version);
|
package/src/cpp/noderfc.h
CHANGED
|
@@ -17,8 +17,15 @@
|
|
|
17
17
|
#define uint_t uint32_t
|
|
18
18
|
#define pointer_t uintptr_t
|
|
19
19
|
|
|
20
|
-
// client binding version
|
|
21
|
-
|
|
20
|
+
// client binding version, injected unquoted by binding.gyp from package.json
|
|
21
|
+
// (stringified here to avoid platform-specific quoting of compiler defines)
|
|
22
|
+
#define NODERFC_STRINGIFY_(x) #x
|
|
23
|
+
#define NODERFC_STRINGIFY(x) NODERFC_STRINGIFY_(x)
|
|
24
|
+
#ifdef NODERFC_VERSION
|
|
25
|
+
#define NODERFC_VERSION_STRING NODERFC_STRINGIFY(NODERFC_VERSION)
|
|
26
|
+
#else
|
|
27
|
+
#define NODERFC_VERSION_STRING "unknown"
|
|
28
|
+
#endif
|
|
22
29
|
|
|
23
30
|
// surpress unused parameter warnings
|
|
24
31
|
#define UNUSED(x) (void)(x)
|
package/src/ts/binding.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface RfcClientBinding {
|
|
|
15
15
|
_alive: boolean;
|
|
16
16
|
open(callback: (err?: unknown) => void): void;
|
|
17
17
|
close(callback: (err?: unknown) => void): void;
|
|
18
|
+
resetServerContext(callback: (err?: unknown) => void): void;
|
|
18
19
|
invoke(
|
|
19
20
|
rfmName: string,
|
|
20
21
|
rfmParams: RfcObject,
|
package/src/ts/client.ts
CHANGED
|
@@ -56,6 +56,25 @@ export class Client {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Resets the ABAP session context this connection holds and keeps the
|
|
61
|
+
* connection open (`RfcResetServerContext`). The next call runs in a fresh
|
|
62
|
+
* context without a new logon. Whatever the context held is gone, including
|
|
63
|
+
* enqueue locks and program buffers.
|
|
64
|
+
*/
|
|
65
|
+
resetServerContext(): Promise<void> {
|
|
66
|
+
return new Promise((resolve, reject) => {
|
|
67
|
+
try {
|
|
68
|
+
this.__client.resetServerContext((err: unknown) => {
|
|
69
|
+
if (err === undefined) resolve();
|
|
70
|
+
else reject(err);
|
|
71
|
+
});
|
|
72
|
+
} catch (ex) {
|
|
73
|
+
reject(ex);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
59
78
|
call(rfmName: string, rfmParams: RfcObject = {}): Promise<RfcObject> {
|
|
60
79
|
return new Promise((resolve, reject) => {
|
|
61
80
|
if (typeof rfmName !== 'string' || rfmName.length === 0) {
|