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