abap-local-client 1.0.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/README.md +22 -0
- package/basic-auth.mjs +197 -0
- package/bin/cli.cjs +8 -0
- package/bin/config.cjs +8 -0
- package/config-cli.mjs +325 -0
- package/config-manager.mjs +278 -0
- package/package.json +46 -0
- package/saml-login.mjs +372 -0
- package/sap-rfc-client.js +852 -0
- package/snc-auth.mjs +194 -0
- package/spnego-auth.mjs +362 -0
- package/sso-sap-client.mjs +687 -0
- package/x509-auth.mjs +395 -0
|
@@ -0,0 +1,852 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SAP RFC Client — ADT over SAProuter
|
|
3
|
+
*
|
|
4
|
+
* Uses koffi + sapnwrfc.dll to call the ABAP function module
|
|
5
|
+
* SADT_REST_RFC_ENDPOINT, which tunnels HTTP requests over RFC.
|
|
6
|
+
*
|
|
7
|
+
* Protocol (discovered by capturing Eclipse traffic):
|
|
8
|
+
* 1. Eclipse connects to SAP Gateway port 3300 via SAProuter (NI_ROUTE)
|
|
9
|
+
* 2. RFC SDK handles NI handshake automatically
|
|
10
|
+
* 3. Eclipse calls SADT_REST_RFC_ENDPOINT with method/URI/headers/body
|
|
11
|
+
* 4. SAP executes the HTTP request internally and returns the HTTP response
|
|
12
|
+
*
|
|
13
|
+
* Config object:
|
|
14
|
+
* host – SAP application server hostname (e.g. vhvcddevci.sap.vissimo.com.br)
|
|
15
|
+
* sysnr – System number (e.g. '00')
|
|
16
|
+
* client – SAP client (e.g. '100')
|
|
17
|
+
* username – SAP logon user
|
|
18
|
+
* password – SAP logon password
|
|
19
|
+
* language – Logon language (default 'EN')
|
|
20
|
+
* saprouter – SAProuter string (e.g. '/H/52.202.238.115')
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import koffi from 'koffi';
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Load sapnwrfc.dll (must be in PATH, usually C:\Windows\System32)
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
let lib;
|
|
30
|
+
try {
|
|
31
|
+
lib = koffi.load('sapnwrfc.dll');
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Cannot load sapnwrfc.dll: ${err.message}\n` +
|
|
35
|
+
'Make sure SAP NW RFC SDK is installed and sapnwrfc.dll is in C:\\Windows\\System32.'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// koffi Type Definitions
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
// RFC_ERROR_INFO — must exactly match sapnwrfc.h RFC_ERROR_INFO struct
|
|
44
|
+
const RFC_ERROR_INFO = koffi.struct('RFC_ERROR_INFO', {
|
|
45
|
+
code: 'int32',
|
|
46
|
+
group: 'int32',
|
|
47
|
+
key: koffi.array('uint16', 128),
|
|
48
|
+
message: koffi.array('uint16', 512),
|
|
49
|
+
abapMsgClass: koffi.array('uint16', 20),
|
|
50
|
+
abapMsgType: koffi.array('uint16', 1),
|
|
51
|
+
abapMsgNumber:koffi.array('uint16', 3),
|
|
52
|
+
abapMsgV1: koffi.array('uint16', 50),
|
|
53
|
+
abapMsgV2: koffi.array('uint16', 50),
|
|
54
|
+
abapMsgV3: koffi.array('uint16', 50),
|
|
55
|
+
abapMsgV4: koffi.array('uint16', 50),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// RFC_CONNECTION_PARAMETER — name/value pair (SAP_UC*)
|
|
59
|
+
const RFC_CONNECTION_PARAMETER = koffi.struct('RFC_CONNECTION_PARAMETER', {
|
|
60
|
+
name: koffi.pointer('uint16'),
|
|
61
|
+
value: koffi.pointer('uint16'),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// RFC_PARAMETER_DESC — describes one parameter of a function module.
|
|
65
|
+
// Layout taken from sapnwrfc.h.
|
|
66
|
+
const RFC_PARAMETER_DESC = koffi.struct('RFC_PARAMETER_DESC', {
|
|
67
|
+
name: koffi.array('uint16', 31), // 30 chars + NUL
|
|
68
|
+
type: 'int32', // RFCTYPE
|
|
69
|
+
direction: 'int32', // RFC_DIRECTION
|
|
70
|
+
nucLength: 'uint32',
|
|
71
|
+
ucLength: 'uint32',
|
|
72
|
+
decimals: 'uint32',
|
|
73
|
+
typeDescHandle: 'void *',
|
|
74
|
+
defaultValue: koffi.array('uint16', 31), // SAP_UC[31]
|
|
75
|
+
parameterText: koffi.array('uint16', 80),
|
|
76
|
+
optional: 'uint8',
|
|
77
|
+
extendedDescription: 'void *',
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const RFC_DIRECTION_NAMES = {
|
|
81
|
+
1: 'IMPORT',
|
|
82
|
+
2: 'EXPORT',
|
|
83
|
+
3: 'CHANGING',
|
|
84
|
+
7: 'TABLES',
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const RFC_TYPE_NAMES = {
|
|
88
|
+
0: 'CHAR', 1: 'DATE', 2: 'BCD', 3: 'TIME',
|
|
89
|
+
4: 'BYTE', 5: 'TABLE', 6: 'NUM', 7: 'FLOAT',
|
|
90
|
+
8: 'INT', 9: 'INT2', 10: 'INT1', 14: 'NULL',
|
|
91
|
+
16: 'ABAPOBJ', 17: 'STRUCT', 23: 'DECF16', 24: 'DECF34',
|
|
92
|
+
28: 'XMLDATA', 29: 'STRING', 30: 'XSTRING', 31: 'INT8',
|
|
93
|
+
32: 'UTCLONG',
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// RFC SDK Function Bindings
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
const RfcOpenConnection = lib.func('RfcOpenConnection', 'void *', [
|
|
101
|
+
koffi.pointer(RFC_CONNECTION_PARAMETER),
|
|
102
|
+
'uint32',
|
|
103
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
104
|
+
]);
|
|
105
|
+
|
|
106
|
+
const RfcCloseConnection = lib.func('RfcCloseConnection', 'int32', [
|
|
107
|
+
'void *',
|
|
108
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
109
|
+
]);
|
|
110
|
+
|
|
111
|
+
const RfcGetFunctionDesc = lib.func('RfcGetFunctionDesc', 'void *', [
|
|
112
|
+
'void *',
|
|
113
|
+
koffi.pointer('uint16'),
|
|
114
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const RfcCreateFunction = lib.func('RfcCreateFunction', 'void *', [
|
|
118
|
+
'void *',
|
|
119
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
120
|
+
]);
|
|
121
|
+
|
|
122
|
+
const RfcDestroyFunction = lib.func('RfcDestroyFunction', 'int32', [
|
|
123
|
+
'void *',
|
|
124
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
const RfcInvoke = lib.func('RfcInvoke', 'int32', [
|
|
128
|
+
'void *',
|
|
129
|
+
'void *',
|
|
130
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
131
|
+
]);
|
|
132
|
+
|
|
133
|
+
// DATA_CONTAINER_HANDLE is used for both function handles and structure handles
|
|
134
|
+
const RfcGetStructure = lib.func('RfcGetStructure', 'int32', [
|
|
135
|
+
'void *', // data container
|
|
136
|
+
koffi.pointer('uint16'), // field name (SAP_UC*)
|
|
137
|
+
koffi.out(koffi.pointer('void *')), // output: RFC_STRUCTURE_HANDLE*
|
|
138
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
const RfcGetTable = lib.func('RfcGetTable', 'int32', [
|
|
142
|
+
'void *',
|
|
143
|
+
koffi.pointer('uint16'),
|
|
144
|
+
koffi.out(koffi.pointer('void *')), // output: RFC_TABLE_HANDLE*
|
|
145
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
const RfcAppendNewRow = lib.func('RfcAppendNewRow', 'void *', [
|
|
149
|
+
'void *',
|
|
150
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
151
|
+
]);
|
|
152
|
+
|
|
153
|
+
// RfcGetRowCount — API is:
|
|
154
|
+
// RFC_RC RfcGetRowCount(RFC_TABLE_HANDLE tableHandle,
|
|
155
|
+
// unsigned *rowCount,
|
|
156
|
+
// RFC_ERROR_INFO *errorInfo);
|
|
157
|
+
// Returns RFC_RC; row count is written to the out-pointer.
|
|
158
|
+
const RfcGetRowCount = lib.func('RfcGetRowCount', 'int32', [
|
|
159
|
+
'void *',
|
|
160
|
+
koffi.out(koffi.pointer('uint32')),
|
|
161
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
const RfcMoveToFirstRow = lib.func('RfcMoveToFirstRow', 'int32', [
|
|
165
|
+
'void *',
|
|
166
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const RfcMoveToNextRow = lib.func('RfcMoveToNextRow', 'int32', [
|
|
170
|
+
'void *',
|
|
171
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
172
|
+
]);
|
|
173
|
+
|
|
174
|
+
const RfcGetCurrentRow = lib.func('RfcGetCurrentRow', 'void *', [
|
|
175
|
+
'void *',
|
|
176
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
// RfcSetString — write a string value into a field (SAP_UC* value)
|
|
180
|
+
const RfcSetString = lib.func('RfcSetString', 'int32', [
|
|
181
|
+
'void *',
|
|
182
|
+
koffi.pointer('uint16'), // field name
|
|
183
|
+
koffi.pointer('uint16'), // value (SAP_UC*)
|
|
184
|
+
'uint32', // value length in chars (NOT bytes)
|
|
185
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
// RfcSetXString — write binary data into a XSTRING field
|
|
189
|
+
const RfcSetXString = lib.func('RfcSetXString', 'int32', [
|
|
190
|
+
'void *',
|
|
191
|
+
koffi.pointer('uint16'), // field name
|
|
192
|
+
koffi.pointer('uint8'), // byte buffer (SAP_RAW*)
|
|
193
|
+
'uint32', // byte length
|
|
194
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
195
|
+
]);
|
|
196
|
+
|
|
197
|
+
// RfcGetStringLength — get the char length of a string field
|
|
198
|
+
const RfcGetStringLength = lib.func('RfcGetStringLength', 'int32', [
|
|
199
|
+
'void *',
|
|
200
|
+
koffi.pointer('uint16'),
|
|
201
|
+
koffi.out(koffi.pointer('uint32')),
|
|
202
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
// RfcGetString — read a string field into a caller-provided SAP_UC buffer
|
|
206
|
+
const RfcGetString = lib.func('RfcGetString', 'int32', [
|
|
207
|
+
'void *',
|
|
208
|
+
koffi.pointer('uint16'), // field name
|
|
209
|
+
koffi.pointer('uint16'), // output buffer (SAP_UC*) ← C writes here
|
|
210
|
+
'uint32', // buffer capacity in chars
|
|
211
|
+
koffi.out(koffi.pointer('uint32')), // actual length written
|
|
212
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
213
|
+
]);
|
|
214
|
+
|
|
215
|
+
// RfcGetXString — read a XSTRING field into a caller-provided byte buffer
|
|
216
|
+
// Pass bufferLength=0 first to get the required length (returns RFC_BUFFER_TOO_SMALL=1)
|
|
217
|
+
const RfcGetXString = lib.func('RfcGetXString', 'int32', [
|
|
218
|
+
'void *',
|
|
219
|
+
koffi.pointer('uint16'), // field name
|
|
220
|
+
koffi.pointer('uint8'), // output byte buffer ← C writes here
|
|
221
|
+
'uint32', // buffer capacity in bytes
|
|
222
|
+
koffi.out(koffi.pointer('uint32')), // actual byte length
|
|
223
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
224
|
+
]);
|
|
225
|
+
|
|
226
|
+
// RfcGetChars — read a CHAR field (fixed-length) into a caller-provided SAP_UC buffer
|
|
227
|
+
// Used for fields like ABAPTXT255.LINE (CHAR255) where RfcGetString does not work.
|
|
228
|
+
const RfcGetChars = lib.func('RfcGetChars', 'int32', [
|
|
229
|
+
'void *',
|
|
230
|
+
koffi.pointer('uint16'), // field name
|
|
231
|
+
koffi.pointer('uint16'), // output buffer (SAP_UC*)
|
|
232
|
+
'uint32', // buffer capacity in chars
|
|
233
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
// Metadata introspection — list parameters of a function module
|
|
237
|
+
const RfcGetParameterCount = lib.func('RfcGetParameterCount', 'int32', [
|
|
238
|
+
'void *', // RFC_FUNCTION_DESC_HANDLE
|
|
239
|
+
koffi.out(koffi.pointer('uint32')),
|
|
240
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
241
|
+
]);
|
|
242
|
+
|
|
243
|
+
const RfcGetParameterDescByIndex = lib.func('RfcGetParameterDescByIndex', 'int32', [
|
|
244
|
+
'void *', // RFC_FUNCTION_DESC_HANDLE
|
|
245
|
+
'uint32', // index
|
|
246
|
+
koffi.out(koffi.pointer(RFC_PARAMETER_DESC)),
|
|
247
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
248
|
+
]);
|
|
249
|
+
|
|
250
|
+
// RFC_FIELD_DESC — describes one field inside a structure/table row.
|
|
251
|
+
// Layout from sapnwrfc.h.
|
|
252
|
+
const RFC_FIELD_DESC = koffi.struct('RFC_FIELD_DESC', {
|
|
253
|
+
name: koffi.array('uint16', 31),
|
|
254
|
+
type: 'int32',
|
|
255
|
+
nucLength: 'uint32',
|
|
256
|
+
ucLength: 'uint32',
|
|
257
|
+
nucOffset: 'uint32',
|
|
258
|
+
ucOffset: 'uint32',
|
|
259
|
+
decimals: 'uint32',
|
|
260
|
+
typeDescHandle: 'void *',
|
|
261
|
+
extendedDescription: 'void *',
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Introspect fields of a type (structure / table row).
|
|
265
|
+
const RfcGetFieldCount = lib.func('RfcGetFieldCount', 'int32', [
|
|
266
|
+
'void *', // RFC_TYPE_DESC_HANDLE
|
|
267
|
+
koffi.out(koffi.pointer('uint32')),
|
|
268
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
269
|
+
]);
|
|
270
|
+
|
|
271
|
+
const RfcGetFieldDescByIndex = lib.func('RfcGetFieldDescByIndex', 'int32', [
|
|
272
|
+
'void *', // RFC_TYPE_DESC_HANDLE
|
|
273
|
+
'uint32', // index
|
|
274
|
+
koffi.out(koffi.pointer(RFC_FIELD_DESC)),
|
|
275
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
276
|
+
]);
|
|
277
|
+
|
|
278
|
+
// Retrieve the type description handle for a table parameter.
|
|
279
|
+
const RfcDescribeType = lib.func('RfcDescribeType', 'void *', [
|
|
280
|
+
'void *', // data container (table handle or struct handle)
|
|
281
|
+
koffi.out(koffi.pointer(RFC_ERROR_INFO)),
|
|
282
|
+
]);
|
|
283
|
+
|
|
284
|
+
// ============================================================================
|
|
285
|
+
// Helper Utilities
|
|
286
|
+
// ============================================================================
|
|
287
|
+
|
|
288
|
+
/** Convert a JS string to a SAP_UC (UTF-16LE) null-terminated Buffer */
|
|
289
|
+
function sapStr(s) {
|
|
290
|
+
const buf = Buffer.alloc((s.length + 1) * 2, 0);
|
|
291
|
+
buf.write(s, 0, 'utf16le');
|
|
292
|
+
return buf;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Decode a uint16[] array (from RFC_ERROR_INFO) to a JS string */
|
|
296
|
+
function decodeU16Array(arr) {
|
|
297
|
+
const end = arr.findIndex(c => c === 0);
|
|
298
|
+
const slice = end >= 0 ? arr.slice(0, end) : arr;
|
|
299
|
+
return Buffer.from(new Uint16Array(slice).buffer).toString('utf16le');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Create a blank RFC_ERROR_INFO JS object */
|
|
303
|
+
function newErr() {
|
|
304
|
+
return {
|
|
305
|
+
code: 0, group: 0,
|
|
306
|
+
key: new Array(128).fill(0),
|
|
307
|
+
message: new Array(512).fill(0),
|
|
308
|
+
abapMsgClass: new Array(20).fill(0),
|
|
309
|
+
abapMsgType: new Array(1).fill(0),
|
|
310
|
+
abapMsgNumber:new Array(3).fill(0),
|
|
311
|
+
abapMsgV1: new Array(50).fill(0),
|
|
312
|
+
abapMsgV2: new Array(50).fill(0),
|
|
313
|
+
abapMsgV3: new Array(50).fill(0),
|
|
314
|
+
abapMsgV4: new Array(50).fill(0),
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Read a STRING field value from a data container.
|
|
320
|
+
* Returns '' on any error.
|
|
321
|
+
*/
|
|
322
|
+
function readStringField(container, fieldName) {
|
|
323
|
+
const lenPtr = [0];
|
|
324
|
+
const e = newErr();
|
|
325
|
+
const rc = RfcGetStringLength(container, sapStr(fieldName), lenPtr, e);
|
|
326
|
+
if (rc !== 0 || lenPtr[0] === 0) { return ''; }
|
|
327
|
+
const buf = Buffer.alloc((lenPtr[0] + 1) * 2, 0);
|
|
328
|
+
const actualLen = [0];
|
|
329
|
+
RfcGetString(container, sapStr(fieldName), buf, lenPtr[0] + 1, actualLen, e);
|
|
330
|
+
return buf.slice(0, actualLen[0] * 2).toString('utf16le');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Read ALL fields of a row container and return them pipe-separated.
|
|
335
|
+
* Uses RfcDescribeType → RfcGetTypeFieldCount → RfcGetFieldDescByIndex to
|
|
336
|
+
* discover the field names at runtime, then reads each via STRING or CHAR.
|
|
337
|
+
*
|
|
338
|
+
* Falls back to readStringField('LINE') when type introspection fails
|
|
339
|
+
* (e.g. flat-char structures without a proper type handle).
|
|
340
|
+
*
|
|
341
|
+
* @param {*} row - RFC row handle (from RfcGetCurrentRow)
|
|
342
|
+
* @param {string[]} [hint] - optional ordered field-name list; when provided
|
|
343
|
+
* the type-introspection step is skipped.
|
|
344
|
+
* @returns {string} pipe-separated field values, trailing spaces stripped.
|
|
345
|
+
*/
|
|
346
|
+
function readAllFields(row, hint) {
|
|
347
|
+
const e = newErr();
|
|
348
|
+
|
|
349
|
+
// Fast path: caller already knows the field names.
|
|
350
|
+
if (hint && hint.length > 0) {
|
|
351
|
+
return hint.map(f => {
|
|
352
|
+
const sv = readStringField(row, f);
|
|
353
|
+
if (sv !== '') { return sv.trimEnd(); }
|
|
354
|
+
return readCharField(row, f).trimEnd();
|
|
355
|
+
}).join('|');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Introspect the type associated with this row.
|
|
359
|
+
const typeHandle = RfcDescribeType(row, e);
|
|
360
|
+
if (!typeHandle) {
|
|
361
|
+
// Flat-char fallback: try LINE (ABAPTXT255, ABAPSOURCE, etc.)
|
|
362
|
+
const line = readStringField(row, 'LINE');
|
|
363
|
+
if (line !== '') { return line.trimEnd(); }
|
|
364
|
+
return readCharField(row, 'LINE').trimEnd();
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const countPtr = [0];
|
|
368
|
+
const rcCount = RfcGetFieldCount(typeHandle, countPtr, e);
|
|
369
|
+
if (rcCount !== 0 || countPtr[0] === 0) {
|
|
370
|
+
return readCharField(row, 'LINE').trimEnd();
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const parts = [];
|
|
374
|
+
for (let i = 0; i < countPtr[0]; i++) {
|
|
375
|
+
const fd = {
|
|
376
|
+
name: new Array(31).fill(0),
|
|
377
|
+
type: 0, nucLength: 0, ucLength: 0,
|
|
378
|
+
nucOffset: 0, ucOffset: 0, decimals: 0,
|
|
379
|
+
typeDescHandle: null, extendedDescription: null,
|
|
380
|
+
};
|
|
381
|
+
if (RfcGetFieldDescByIndex(typeHandle, i, fd, e) !== 0) { continue; }
|
|
382
|
+
const fname = decodeU16Array(fd.name);
|
|
383
|
+
if (!fname) { continue; }
|
|
384
|
+
const sv = readStringField(row, fname);
|
|
385
|
+
if (sv !== '') { parts.push(sv.trimEnd()); continue; }
|
|
386
|
+
parts.push(readCharField(row, fname).trimEnd());
|
|
387
|
+
}
|
|
388
|
+
return parts.join('|');
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Read a CHAR field (fixed-length, like ABAPTXT255.LINE = CHAR255).
|
|
393
|
+
* Allocates a fixed buffer; default 4096 chars covers ABAPSOURCE/ABAPTXT*.
|
|
394
|
+
* Returns the value with trailing NULLs and spaces stripped.
|
|
395
|
+
* Returns '' if the field is not CHAR or read fails.
|
|
396
|
+
*/
|
|
397
|
+
function readCharField(container, fieldName, maxChars = 4096) {
|
|
398
|
+
const e = newErr();
|
|
399
|
+
const buf = Buffer.alloc(maxChars * 2, 0);
|
|
400
|
+
const rc = RfcGetChars(container, sapStr(fieldName), buf, maxChars, e);
|
|
401
|
+
if (rc !== 0) { return ''; }
|
|
402
|
+
return buf.toString('utf16le').replace(/[\u0000\s]+$/, '');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Read an XSTRING field. Two-pass: first call discovers the required buffer
|
|
407
|
+
* size (RFC returns RFC_BUFFER_TOO_SMALL=1), second call retrieves the data.
|
|
408
|
+
* Returns a Buffer (possibly empty).
|
|
409
|
+
*/
|
|
410
|
+
function readXStringField(container, fieldName) {
|
|
411
|
+
const e = newErr();
|
|
412
|
+
const lenPtr = [0];
|
|
413
|
+
const dummy = Buffer.alloc(1, 0);
|
|
414
|
+
// First pass — get size
|
|
415
|
+
RfcGetXString(container, sapStr(fieldName), dummy, 0, lenPtr, e);
|
|
416
|
+
if (lenPtr[0] === 0) { return Buffer.alloc(0); }
|
|
417
|
+
const buf = Buffer.alloc(lenPtr[0], 0);
|
|
418
|
+
const actualLen = [0];
|
|
419
|
+
RfcGetXString(container, sapStr(fieldName), buf, lenPtr[0], actualLen, e);
|
|
420
|
+
return buf.slice(0, actualLen[0]);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// ============================================================================
|
|
424
|
+
// Build RFC Connection Parameters
|
|
425
|
+
// ============================================================================
|
|
426
|
+
|
|
427
|
+
function buildConnParams(config) {
|
|
428
|
+
const pairs = [
|
|
429
|
+
['ASHOST', config.host],
|
|
430
|
+
['SYSNR', config.sysnr || '00'],
|
|
431
|
+
['CLIENT', config.client || '100'],
|
|
432
|
+
['USER', config.username],
|
|
433
|
+
['PASSWD', config.password],
|
|
434
|
+
['LANG', config.language || 'EN'],
|
|
435
|
+
];
|
|
436
|
+
if (config.saprouter) {
|
|
437
|
+
pairs.push(['SAPROUTER', config.saprouter]);
|
|
438
|
+
}
|
|
439
|
+
// SNC parameters (Secure Network Communication)
|
|
440
|
+
if (config.sncMode) pairs.push(['SNC_MODE', config.sncMode]);
|
|
441
|
+
if (config.sncMyName) pairs.push(['SNC_MYNAME', config.sncMyName]);
|
|
442
|
+
if (config.sncPartnerName) pairs.push(['SNC_PARTNERNAME', config.sncPartnerName]);
|
|
443
|
+
if (config.sncQop) pairs.push(['SNC_QOP', config.sncQop]);
|
|
444
|
+
if (config.sncLib) pairs.push(['SNC_LIB', config.sncLib]);
|
|
445
|
+
// Keep buffers alive to prevent GC during the call
|
|
446
|
+
const bufs = pairs.map(([n, v]) => ({ nameBuf: sapStr(n), valueBuf: sapStr(v || '') }));
|
|
447
|
+
return bufs.map(b => ({ name: b.nameBuf, value: b.valueBuf }));
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// ============================================================================
|
|
451
|
+
// SAP RFC Client Class
|
|
452
|
+
// ============================================================================
|
|
453
|
+
|
|
454
|
+
export class SAPRFCClient {
|
|
455
|
+
constructor(config) {
|
|
456
|
+
this.config = config;
|
|
457
|
+
this.conn = null;
|
|
458
|
+
this.csrfToken = null;
|
|
459
|
+
const sncInfo = config.sncMode ? ` SNC=yes (${config.sncMyName || 'auto'})` : '';
|
|
460
|
+
console.log(`[RFC] SAP RFC client configured — host=${config.host} sysnr=${config.sysnr || '00'} saprouter=${config.saprouter || 'none'}${sncInfo}`);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/** Open RFC connection (keep-alive between calls) */
|
|
464
|
+
async connect() {
|
|
465
|
+
if (this.conn) { return; }
|
|
466
|
+
console.log('[RFC] Opening RFC connection...');
|
|
467
|
+
const params = buildConnParams(this.config);
|
|
468
|
+
const errInfo = newErr();
|
|
469
|
+
this.conn = RfcOpenConnection(params, params.length, errInfo);
|
|
470
|
+
if (!this.conn) {
|
|
471
|
+
const msg = decodeU16Array(errInfo.message);
|
|
472
|
+
throw new Error(`RFC connection failed (code=${errInfo.code}): ${msg}`);
|
|
473
|
+
}
|
|
474
|
+
console.log('[RFC] RFC connection established ✅');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* List all parameters of a function module as the RFC SDK sees them.
|
|
479
|
+
* Returns an array of { name, direction, type, length } objects and
|
|
480
|
+
* also prints them to the console (handy for diagnostics).
|
|
481
|
+
*/
|
|
482
|
+
async inspectFM(fmName) {
|
|
483
|
+
await this.connect();
|
|
484
|
+
const e = newErr();
|
|
485
|
+
const funcDesc = RfcGetFunctionDesc(this.conn, sapStr(fmName), e);
|
|
486
|
+
if (!funcDesc) {
|
|
487
|
+
throw new Error(`RfcGetFunctionDesc(${fmName}) failed (code=${e.code}): ${decodeU16Array(e.message)}`);
|
|
488
|
+
}
|
|
489
|
+
const countPtr = [0];
|
|
490
|
+
const rcCount = RfcGetParameterCount(funcDesc, countPtr, e);
|
|
491
|
+
if (rcCount !== 0) {
|
|
492
|
+
throw new Error(`RfcGetParameterCount(${fmName}) failed (code=${e.code}): ${decodeU16Array(e.message)}`);
|
|
493
|
+
}
|
|
494
|
+
const total = countPtr[0];
|
|
495
|
+
console.log(`[RFC] ${fmName} has ${total} parameter(s) according to SAP metadata:`);
|
|
496
|
+
const params = [];
|
|
497
|
+
for (let i = 0; i < total; i++) {
|
|
498
|
+
const desc = {
|
|
499
|
+
name: new Array(31).fill(0),
|
|
500
|
+
type: 0, direction: 0, nucLength: 0, ucLength: 0, decimals: 0,
|
|
501
|
+
typeDescHandle: null,
|
|
502
|
+
defaultValue: new Array(31).fill(0),
|
|
503
|
+
parameterText: new Array(80).fill(0),
|
|
504
|
+
optional: 0,
|
|
505
|
+
extendedDescription: null,
|
|
506
|
+
};
|
|
507
|
+
const rc = RfcGetParameterDescByIndex(funcDesc, i, desc, e);
|
|
508
|
+
if (rc !== 0) {
|
|
509
|
+
console.log(` [${i}] (read failed: ${decodeU16Array(e.message)})`);
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
const name = decodeU16Array(desc.name);
|
|
513
|
+
const dir = RFC_DIRECTION_NAMES[desc.direction] || `dir=${desc.direction}`;
|
|
514
|
+
const type = RFC_TYPE_NAMES[desc.type] || `type=${desc.type}`;
|
|
515
|
+
console.log(` [${i}] ${dir.padEnd(8)} ${name.padEnd(30)} ${type} (${desc.ucLength / 2} chars)`);
|
|
516
|
+
params.push({ name, direction: dir, type, length: desc.ucLength / 2 });
|
|
517
|
+
}
|
|
518
|
+
return params;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Close the persistent RFC connection */
|
|
522
|
+
disconnect() {
|
|
523
|
+
if (this.conn) {
|
|
524
|
+
RfcCloseConnection(this.conn, newErr());
|
|
525
|
+
this.conn = null;
|
|
526
|
+
console.log('[RFC] RFC connection closed');
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Call SADT_REST_RFC_ENDPOINT to tunnel an ADT HTTP request.
|
|
532
|
+
*
|
|
533
|
+
* @param {string} method – HTTP method (GET, POST, PUT, DELETE)
|
|
534
|
+
* @param {string} uri – Relative path + query string (/sap/bc/adt/...)
|
|
535
|
+
* @param {Object} headers – HTTP headers as key→value
|
|
536
|
+
* @param {string|Buffer} body – Request body (may be empty)
|
|
537
|
+
* @returns {Object} { status, statusText, headers, data }
|
|
538
|
+
*/
|
|
539
|
+
async callADT(method, uri, headers, body) {
|
|
540
|
+
await this.connect();
|
|
541
|
+
|
|
542
|
+
const e = newErr();
|
|
543
|
+
|
|
544
|
+
// ── Get function description (cached by RFC SDK after first call) ──
|
|
545
|
+
const funcDesc = RfcGetFunctionDesc(this.conn, sapStr('SADT_REST_RFC_ENDPOINT'), e);
|
|
546
|
+
if (!funcDesc) {
|
|
547
|
+
throw new Error(`RfcGetFunctionDesc failed (code=${e.code}): ${decodeU16Array(e.message)}`);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// ── Create function call container ──
|
|
551
|
+
const func = RfcCreateFunction(funcDesc, e);
|
|
552
|
+
if (!func) {
|
|
553
|
+
throw new Error(`RfcCreateFunction failed: ${decodeU16Array(e.message)}`);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
// ── Set REQUEST parameter (deep structure) ──
|
|
558
|
+
// Structure: REQUEST_LINE (struct), HEADER_FIELDS (table), MESSAGE_BODY (XSTRING)
|
|
559
|
+
const requestRef = [null];
|
|
560
|
+
RfcGetStructure(func, sapStr('REQUEST'), requestRef, e);
|
|
561
|
+
const request = requestRef[0];
|
|
562
|
+
|
|
563
|
+
if (request) {
|
|
564
|
+
// REQUEST_LINE sub-structure
|
|
565
|
+
const reqLineRef = [null];
|
|
566
|
+
RfcGetStructure(request, sapStr('REQUEST_LINE'), reqLineRef, e);
|
|
567
|
+
const reqLine = reqLineRef[0];
|
|
568
|
+
if (reqLine) {
|
|
569
|
+
RfcSetString(reqLine, sapStr('METHOD'), sapStr(method), method.length, e);
|
|
570
|
+
RfcSetString(reqLine, sapStr('URI'), sapStr(uri), uri.length, e);
|
|
571
|
+
RfcSetString(reqLine, sapStr('VERSION'), sapStr('HTTP/1.1'), 'HTTP/1.1'.length, e);
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// HEADER_FIELDS table
|
|
575
|
+
const headerTableRef = [null];
|
|
576
|
+
RfcGetTable(request, sapStr('HEADER_FIELDS'), headerTableRef, e);
|
|
577
|
+
const headerTable = headerTableRef[0];
|
|
578
|
+
if (headerTable) {
|
|
579
|
+
for (const [name, value] of Object.entries(headers || {})) {
|
|
580
|
+
const row = RfcAppendNewRow(headerTable, e);
|
|
581
|
+
if (row) {
|
|
582
|
+
const v = String(value);
|
|
583
|
+
RfcSetString(row, sapStr('NAME'), sapStr(name), name.length, e);
|
|
584
|
+
RfcSetString(row, sapStr('VALUE'), sapStr(v), v.length, e);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// MESSAGE_BODY (XSTRING)
|
|
590
|
+
if (body && body.length > 0) {
|
|
591
|
+
const bodyBuf = Buffer.isBuffer(body) ? body : Buffer.from(body, 'utf8');
|
|
592
|
+
RfcSetXString(request, sapStr('MESSAGE_BODY'), bodyBuf, bodyBuf.length, e);
|
|
593
|
+
}
|
|
594
|
+
} else {
|
|
595
|
+
// Fallback: flat parameter layout (no REQUEST wrapper)
|
|
596
|
+
this._setFlatParams(func, method, uri, headers, body, e);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ── Invoke the function module ──
|
|
600
|
+
const invokeErr = newErr();
|
|
601
|
+
const rc = RfcInvoke(this.conn, func, invokeErr);
|
|
602
|
+
if (rc !== 0) {
|
|
603
|
+
const msg = decodeU16Array(invokeErr.message);
|
|
604
|
+
// If connection is broken, reset it so next call reconnects
|
|
605
|
+
if (invokeErr.code === 1 || invokeErr.code === 3) { // RFC_COMMUNICATION_FAILURE or RFC_ABAP_EXCEPTION
|
|
606
|
+
this.conn = null;
|
|
607
|
+
}
|
|
608
|
+
throw new Error(`SADT_REST_RFC_ENDPOINT failed (rc=${rc} code=${invokeErr.code}): ${msg}`);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ── Read RESPONSE parameter ──
|
|
612
|
+
const responseRef = [null];
|
|
613
|
+
RfcGetStructure(func, sapStr('RESPONSE'), responseRef, e);
|
|
614
|
+
const response = responseRef[0];
|
|
615
|
+
|
|
616
|
+
let statusCode = 200;
|
|
617
|
+
let statusText = 'OK';
|
|
618
|
+
const responseHeaders = {};
|
|
619
|
+
let responseBody = Buffer.alloc(0);
|
|
620
|
+
|
|
621
|
+
if (response) {
|
|
622
|
+
// STATUS_LINE
|
|
623
|
+
const statusLineRef = [null];
|
|
624
|
+
RfcGetStructure(response, sapStr('STATUS_LINE'), statusLineRef, e);
|
|
625
|
+
const statusLine = statusLineRef[0];
|
|
626
|
+
if (statusLine) {
|
|
627
|
+
const code = readStringField(statusLine, 'STATUS_CODE');
|
|
628
|
+
const phrase = readStringField(statusLine, 'REASON_PHRASE');
|
|
629
|
+
if (code) { statusCode = parseInt(code) || 200; }
|
|
630
|
+
if (phrase) { statusText = phrase; }
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// HEADER_FIELDS table
|
|
634
|
+
const respHeadersRef = [null];
|
|
635
|
+
RfcGetTable(response, sapStr('HEADER_FIELDS'), respHeadersRef, e);
|
|
636
|
+
const respHeadersTable = respHeadersRef[0];
|
|
637
|
+
if (respHeadersTable) {
|
|
638
|
+
const rowCountPtr = [0];
|
|
639
|
+
RfcGetRowCount(respHeadersTable, rowCountPtr, e);
|
|
640
|
+
const rowCount = rowCountPtr[0];
|
|
641
|
+
if (rowCount > 0) {
|
|
642
|
+
RfcMoveToFirstRow(respHeadersTable, e);
|
|
643
|
+
for (let i = 0; i < rowCount; i++) {
|
|
644
|
+
const row = RfcGetCurrentRow(respHeadersTable, e);
|
|
645
|
+
if (row) {
|
|
646
|
+
const name = readStringField(row, 'NAME');
|
|
647
|
+
const value = readStringField(row, 'VALUE');
|
|
648
|
+
if (name) { responseHeaders[name.toLowerCase()] = value; }
|
|
649
|
+
}
|
|
650
|
+
if (i < rowCount - 1) { RfcMoveToNextRow(respHeadersTable, e); }
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// MESSAGE_BODY (XSTRING)
|
|
656
|
+
responseBody = readXStringField(response, 'MESSAGE_BODY');
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// Decode body as UTF-8 (ADT always returns XML / text)
|
|
660
|
+
const dataStr = responseBody.length > 0 ? responseBody.toString('utf8') : '';
|
|
661
|
+
|
|
662
|
+
// Extract CSRF token if present
|
|
663
|
+
if (responseHeaders['x-csrf-token']) {
|
|
664
|
+
this.csrfToken = responseHeaders['x-csrf-token'];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return {
|
|
668
|
+
status: statusCode,
|
|
669
|
+
statusText: statusText,
|
|
670
|
+
headers: responseHeaders,
|
|
671
|
+
data: dataStr,
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
} finally {
|
|
675
|
+
RfcDestroyFunction(func, newErr());
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
/** Fetch a fresh CSRF token via GET /sap/bc/adt/discovery */
|
|
680
|
+
async fetchCsrfToken() {
|
|
681
|
+
console.log('[RFC] Fetching CSRF token...');
|
|
682
|
+
const resp = await this.callADT('GET', '/sap/bc/adt/discovery', {
|
|
683
|
+
'X-CSRF-Token': 'Fetch',
|
|
684
|
+
'Accept': 'application/atomsvc+xml',
|
|
685
|
+
}, null);
|
|
686
|
+
if (resp.headers['x-csrf-token']) {
|
|
687
|
+
this.csrfToken = resp.headers['x-csrf-token'];
|
|
688
|
+
console.log(`[RFC] CSRF token: ${this.csrfToken.substring(0, 15)}...`);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Call any RFC function module with simple string importing parameters
|
|
694
|
+
* and read table output parameters.
|
|
695
|
+
*
|
|
696
|
+
* @param {string} fmName - Function module name (e.g. 'ZREAD_ABAP_SOURCE')
|
|
697
|
+
* @param {Object} importing - Key/value pairs for IMPORTING params (strings)
|
|
698
|
+
* @param {string[]} tables - Names of TABLES parameters to read
|
|
699
|
+
* @param {string[]} tableFields - Field names to try on each row (default: ['LINE'])
|
|
700
|
+
* @returns {Object} - { [tableName]: string[] }
|
|
701
|
+
*/
|
|
702
|
+
async callFM(fmName, importing = {}, tables = [], tableFields = ['LINE']) {
|
|
703
|
+
await this.connect();
|
|
704
|
+
|
|
705
|
+
const e = newErr();
|
|
706
|
+
|
|
707
|
+
const funcDesc = RfcGetFunctionDesc(this.conn, sapStr(fmName), e);
|
|
708
|
+
if (!funcDesc) {
|
|
709
|
+
throw new Error(`RfcGetFunctionDesc(${fmName}) failed (code=${e.code}): ${decodeU16Array(e.message)}`);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const func = RfcCreateFunction(funcDesc, e);
|
|
713
|
+
if (!func) {
|
|
714
|
+
throw new Error(`RfcCreateFunction(${fmName}) failed: ${decodeU16Array(e.message)}`);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
// Set IMPORTING parameters
|
|
719
|
+
for (const [name, value] of Object.entries(importing)) {
|
|
720
|
+
const v = String(value);
|
|
721
|
+
const rc = RfcSetString(func, sapStr(name), sapStr(v), v.length, e);
|
|
722
|
+
if (rc !== 0) {
|
|
723
|
+
console.warn(`[RFC] RfcSetString(${name}) rc=${rc}: ${decodeU16Array(e.message)}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Invoke the function module
|
|
728
|
+
const invokeErr = newErr();
|
|
729
|
+
const rc = RfcInvoke(this.conn, func, invokeErr);
|
|
730
|
+
if (rc !== 0) {
|
|
731
|
+
const msg = decodeU16Array(invokeErr.message);
|
|
732
|
+
// Reset connection on communication or system failure
|
|
733
|
+
if (invokeErr.code === 1 || invokeErr.code === 3) { this.conn = null; }
|
|
734
|
+
throw new Error(`${fmName} failed (rc=${rc} code=${invokeErr.code}): ${msg}`);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
// Read output TABLES parameters
|
|
738
|
+
const result = {};
|
|
739
|
+
for (const tableName of tables) {
|
|
740
|
+
const tableRef = [null];
|
|
741
|
+
const getTableErr = newErr();
|
|
742
|
+
const rcGetTable = RfcGetTable(func, sapStr(tableName), tableRef, getTableErr);
|
|
743
|
+
if (rcGetTable !== 0) {
|
|
744
|
+
console.log(`[RFC] RfcGetTable(${tableName}) rc=${rcGetTable} code=${getTableErr.code}: ${decodeU16Array(getTableErr.message)}`);
|
|
745
|
+
}
|
|
746
|
+
const tableHandle = tableRef[0];
|
|
747
|
+
const lines = [];
|
|
748
|
+
|
|
749
|
+
if (tableHandle) {
|
|
750
|
+
const rowCountErr = newErr();
|
|
751
|
+
const rowCountPtr = [0];
|
|
752
|
+
RfcGetRowCount(tableHandle, rowCountPtr, rowCountErr);
|
|
753
|
+
const rowCount = rowCountPtr[0];
|
|
754
|
+
console.log(`[RFC] Table ${tableName}: ${rowCount} row(s) returned`);
|
|
755
|
+
// Iterate via cursor (don't trust rowCount alone — walk until move fails)
|
|
756
|
+
let moveRc = RfcMoveToFirstRow(tableHandle, rowCountErr);
|
|
757
|
+
let idx = 0;
|
|
758
|
+
while (moveRc === 0) {
|
|
759
|
+
const row = RfcGetCurrentRow(tableHandle, rowCountErr);
|
|
760
|
+
if (!row) { break; }
|
|
761
|
+
// tableFields=['LINE'] → hint=null (auto-discover or flat fallback)
|
|
762
|
+
// tableFields=[...] with real names → use as hint (skip introspection)
|
|
763
|
+
const hint = (tableFields.length === 1 && tableFields[0] === 'LINE')
|
|
764
|
+
? null
|
|
765
|
+
: tableFields;
|
|
766
|
+
const value = readAllFields(row, hint);
|
|
767
|
+
if (idx === 0) {
|
|
768
|
+
console.log(`[RFC] row[0] value="${value.substring(0, 120)}"`);
|
|
769
|
+
}
|
|
770
|
+
lines.push(value);
|
|
771
|
+
idx++;
|
|
772
|
+
moveRc = RfcMoveToNextRow(tableHandle, rowCountErr);
|
|
773
|
+
}
|
|
774
|
+
} else {
|
|
775
|
+
console.log(`[RFC] Table ${tableName}: handle is NULL — parameter not found in FM ${fmName}`);
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
result[tableName] = lines;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// If we asked for tables but none returned any rows, dump the FM signature
|
|
782
|
+
// so it's obvious whether the parameter names match what SAP exports.
|
|
783
|
+
const askedAny = tables.length > 0;
|
|
784
|
+
const gotAny = tables.some(t => (result[t] || []).length > 0);
|
|
785
|
+
if (askedAny && !gotAny) {
|
|
786
|
+
try {
|
|
787
|
+
const countPtr = [0];
|
|
788
|
+
const errInspect = newErr();
|
|
789
|
+
if (RfcGetParameterCount(funcDesc, countPtr, errInspect) === 0) {
|
|
790
|
+
console.log(`[RFC] Diagnostic — ${fmName} parameters as SAP reports them:`);
|
|
791
|
+
for (let i = 0; i < countPtr[0]; i++) {
|
|
792
|
+
const desc = {
|
|
793
|
+
name: new Array(31).fill(0),
|
|
794
|
+
type: 0, direction: 0, nucLength: 0, ucLength: 0, decimals: 0,
|
|
795
|
+
typeDescHandle: null,
|
|
796
|
+
defaultValue: new Array(31).fill(0),
|
|
797
|
+
parameterText: new Array(80).fill(0),
|
|
798
|
+
optional: 0,
|
|
799
|
+
extendedDescription: null,
|
|
800
|
+
};
|
|
801
|
+
if (RfcGetParameterDescByIndex(funcDesc, i, desc, errInspect) === 0) {
|
|
802
|
+
const pname = decodeU16Array(desc.name);
|
|
803
|
+
const pdir = RFC_DIRECTION_NAMES[desc.direction] || `dir=${desc.direction}`;
|
|
804
|
+
const ptype = RFC_TYPE_NAMES[desc.type] || `type=${desc.type}`;
|
|
805
|
+
console.log(` [${i}] ${pdir.padEnd(8)} ${pname.padEnd(30)} ${ptype}`);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
} catch { /* diagnostic best effort */ }
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return result;
|
|
813
|
+
|
|
814
|
+
} finally {
|
|
815
|
+
RfcDestroyFunction(func, newErr());
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* Fallback: set flat IMPORTING parameters (no REQUEST wrapper).
|
|
821
|
+
* Used if the function module doesn't have a top-level REQUEST structure.
|
|
822
|
+
*/
|
|
823
|
+
_setFlatParams(func, method, uri, headers, body, e) {
|
|
824
|
+
const reqLineRef = [null];
|
|
825
|
+
RfcGetStructure(func, sapStr('REQUEST_LINE'), reqLineRef, e);
|
|
826
|
+
const reqLine = reqLineRef[0];
|
|
827
|
+
if (reqLine) {
|
|
828
|
+
RfcSetString(reqLine, sapStr('METHOD'), sapStr(method), method.length, e);
|
|
829
|
+
RfcSetString(reqLine, sapStr('URI'), sapStr(uri), uri.length, e);
|
|
830
|
+
RfcSetString(reqLine, sapStr('VERSION'), sapStr('HTTP/1.1'), 'HTTP/1.1'.length, e);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
const headerTableRef = [null];
|
|
834
|
+
RfcGetTable(func, sapStr('HEADER_FIELDS'), headerTableRef, e);
|
|
835
|
+
const headerTable = headerTableRef[0];
|
|
836
|
+
if (headerTable) {
|
|
837
|
+
for (const [name, value] of Object.entries(headers || {})) {
|
|
838
|
+
const row = RfcAppendNewRow(headerTable, e);
|
|
839
|
+
if (row) {
|
|
840
|
+
const v = String(value);
|
|
841
|
+
RfcSetString(row, sapStr('NAME'), sapStr(name), name.length, e);
|
|
842
|
+
RfcSetString(row, sapStr('VALUE'), sapStr(v), v.length, e);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (body && body.length > 0) {
|
|
848
|
+
const bodyBuf = Buffer.isBuffer(body) ? body : Buffer.from(body, 'utf8');
|
|
849
|
+
RfcSetXString(func, sapStr('MESSAGE_BODY'), bodyBuf, bodyBuf.length, e);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|