@saptools/cf-hana 0.4.1 → 0.5.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 +18 -0
- package/README.md +43 -1
- package/dist/cli.js +740 -48
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +35 -4
- package/dist/index.js +704 -26
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import { homedir } from "os";
|
|
|
6
6
|
import { join } from "path";
|
|
7
7
|
|
|
8
8
|
// src/config.ts
|
|
9
|
-
var CLI_VERSION = "0.
|
|
9
|
+
var CLI_VERSION = "0.5.0";
|
|
10
10
|
var ENV_PREFIX = "CF_HANA";
|
|
11
11
|
var DEFAULT_QUERY_TIMEOUT_MS = 6e4;
|
|
12
12
|
var DEFAULT_CONNECT_TIMEOUT_MS = 6e4;
|
|
@@ -15,6 +15,27 @@ var DEFAULT_POOL_IDLE_MS = 6e4;
|
|
|
15
15
|
var DEFAULT_AUTO_LIMIT = 100;
|
|
16
16
|
var DEFAULT_MAX_RESULT_STORE_BYTES = 256 * 1024 * 1024;
|
|
17
17
|
var MAX_RESULT_STORE_BYTES = resolveMaxResultStoreBytes();
|
|
18
|
+
function resolvePositiveIntEnv(suffix, defaultValue) {
|
|
19
|
+
const raw = readEnv(envName(suffix));
|
|
20
|
+
if (raw === void 0) {
|
|
21
|
+
return defaultValue;
|
|
22
|
+
}
|
|
23
|
+
const parsed = Number(raw);
|
|
24
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : defaultValue;
|
|
25
|
+
}
|
|
26
|
+
var DEFAULT_TUNNEL_FALLBACK_BUDGET_MS = resolvePositiveIntEnv(
|
|
27
|
+
"TUNNEL_FALLBACK_BUDGET_MS",
|
|
28
|
+
25e3
|
|
29
|
+
);
|
|
30
|
+
var DEFAULT_TUNNEL_CANDIDATE_TIMEOUT_MS = resolvePositiveIntEnv(
|
|
31
|
+
"TUNNEL_CANDIDATE_TIMEOUT_MS",
|
|
32
|
+
15e3
|
|
33
|
+
);
|
|
34
|
+
var DEFAULT_TUNNEL_MAX_CANDIDATES = resolvePositiveIntEnv("TUNNEL_MAX_CANDIDATES", 3);
|
|
35
|
+
var DEFAULT_TUNNEL_KEEPALIVE_SECONDS = resolvePositiveIntEnv(
|
|
36
|
+
"TUNNEL_KEEPALIVE_SECONDS",
|
|
37
|
+
1200
|
|
38
|
+
);
|
|
18
39
|
function resolveMaxResultStoreBytes() {
|
|
19
40
|
if (readEnv(envName("DRIVER")) !== "fake") {
|
|
20
41
|
return DEFAULT_MAX_RESULT_STORE_BYTES;
|
|
@@ -1217,6 +1238,16 @@ async function cfEnvDirect(appName) {
|
|
|
1217
1238
|
delete env["SAP_PASSWORD"];
|
|
1218
1239
|
return await execWithRetries(bin, [...argsPrefix, "env", appName], env);
|
|
1219
1240
|
}
|
|
1241
|
+
async function cfApps(ctx) {
|
|
1242
|
+
return await runCf(["apps"], ctx);
|
|
1243
|
+
}
|
|
1244
|
+
async function cfAppsDirect() {
|
|
1245
|
+
const { bin, argsPrefix } = resolveCfBin();
|
|
1246
|
+
const env = { ...process.env };
|
|
1247
|
+
delete env["SAP_EMAIL"];
|
|
1248
|
+
delete env["SAP_PASSWORD"];
|
|
1249
|
+
return await execWithRetries(bin, [...argsPrefix, "apps"], env);
|
|
1250
|
+
}
|
|
1220
1251
|
async function readCurrentCfTarget() {
|
|
1221
1252
|
const { bin, argsPrefix } = resolveCfBin();
|
|
1222
1253
|
const env = { ...process.env };
|
|
@@ -1266,6 +1297,26 @@ function parseTargetFields(stdout) {
|
|
|
1266
1297
|
}
|
|
1267
1298
|
return map;
|
|
1268
1299
|
}
|
|
1300
|
+
function parseCfAppsOutput(stdout) {
|
|
1301
|
+
const lines = stdout.split(/\r?\n/);
|
|
1302
|
+
const headerIndex = lines.findIndex((line) => /^\s*name\s+\S/i.test(line));
|
|
1303
|
+
if (headerIndex === -1) {
|
|
1304
|
+
return [];
|
|
1305
|
+
}
|
|
1306
|
+
const rows = [];
|
|
1307
|
+
for (const line of lines.slice(headerIndex + 1)) {
|
|
1308
|
+
const trimmed = line.trim();
|
|
1309
|
+
if (trimmed.length === 0) {
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
const [name, state] = trimmed.split(/\s+/);
|
|
1313
|
+
if (name === void 0 || state === void 0) {
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
rows.push({ name, state });
|
|
1317
|
+
}
|
|
1318
|
+
return rows;
|
|
1319
|
+
}
|
|
1269
1320
|
function formatCurrentCfAppSelector(target, appName) {
|
|
1270
1321
|
const name = appName.trim();
|
|
1271
1322
|
if (!name) {
|
|
@@ -1577,7 +1628,10 @@ async function resolveAppBindings(rawSelector, options) {
|
|
|
1577
1628
|
source: "live",
|
|
1578
1629
|
selectorSource: target.selectorSource,
|
|
1579
1630
|
regionConfirmed: target.regionConfirmed,
|
|
1580
|
-
selectorCanBePinned: target.selectorCanBePinned
|
|
1631
|
+
selectorCanBePinned: target.selectorCanBePinned,
|
|
1632
|
+
apiEndpoint: target.apiEndpoint,
|
|
1633
|
+
orgName: target.orgName,
|
|
1634
|
+
spaceName: target.spaceName
|
|
1581
1635
|
};
|
|
1582
1636
|
}
|
|
1583
1637
|
async function readExplicitBindings(target, options) {
|
|
@@ -1643,6 +1697,7 @@ function toConnectionTarget(binding, role) {
|
|
|
1643
1697
|
// src/driver/fake.ts
|
|
1644
1698
|
import { appendFile } from "fs/promises";
|
|
1645
1699
|
var catalogFailureInjected = false;
|
|
1700
|
+
var connectFailureInjectedOnce = false;
|
|
1646
1701
|
function catalogObjects() {
|
|
1647
1702
|
return [
|
|
1648
1703
|
{ SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "EXISTING_TABLE", OBJECT_TYPE: "TABLE" },
|
|
@@ -1851,10 +1906,28 @@ var FakeConnection = class {
|
|
|
1851
1906
|
return this.closed;
|
|
1852
1907
|
}
|
|
1853
1908
|
};
|
|
1909
|
+
function maybeFailConnect() {
|
|
1910
|
+
const mode = readEnv(envName("FAKE_FAIL_CONNECT"));
|
|
1911
|
+
if (mode !== "once" && mode !== "always") {
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
if (mode === "once" && connectFailureInjectedOnce) {
|
|
1915
|
+
return;
|
|
1916
|
+
}
|
|
1917
|
+
connectFailureInjectedOnce = true;
|
|
1918
|
+
const cause = Object.assign(
|
|
1919
|
+
new Error("Could not connect to any host: [ fake-host:443 - forced fixture failure ]"),
|
|
1920
|
+
{ code: "EHDBOPENCONN" }
|
|
1921
|
+
);
|
|
1922
|
+
throw new CfHanaError("CONNECTION", `Failed to connect to HANA: ${cause.message}`, { cause });
|
|
1923
|
+
}
|
|
1854
1924
|
function createFakeDriver() {
|
|
1855
1925
|
return {
|
|
1856
1926
|
name: "fake",
|
|
1857
|
-
connect: (_params) =>
|
|
1927
|
+
connect: (_params) => {
|
|
1928
|
+
maybeFailConnect();
|
|
1929
|
+
return Promise.resolve(new FakeConnection());
|
|
1930
|
+
}
|
|
1858
1931
|
};
|
|
1859
1932
|
}
|
|
1860
1933
|
|
|
@@ -2079,7 +2152,8 @@ async function connectHdb(params) {
|
|
|
2079
2152
|
user: params.user,
|
|
2080
2153
|
password: params.password,
|
|
2081
2154
|
ca: params.certificate,
|
|
2082
|
-
useTLS: true
|
|
2155
|
+
useTLS: true,
|
|
2156
|
+
...params.servername === void 0 ? {} : { servername: params.servername }
|
|
2083
2157
|
});
|
|
2084
2158
|
await openClient(client, params.connectTimeoutMs);
|
|
2085
2159
|
try {
|
|
@@ -2360,6 +2434,605 @@ function applyAutoLimit(sql, limit) {
|
|
|
2360
2434
|
};
|
|
2361
2435
|
}
|
|
2362
2436
|
|
|
2437
|
+
// src/tunnel/cache.ts
|
|
2438
|
+
import { createHash } from "crypto";
|
|
2439
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile, rename, rm as rm4, writeFile as writeFile2 } from "fs/promises";
|
|
2440
|
+
import { homedir as homedir3 } from "os";
|
|
2441
|
+
import { join as join5 } from "path";
|
|
2442
|
+
|
|
2443
|
+
// src/tunnel/process.ts
|
|
2444
|
+
import { spawn } from "child_process";
|
|
2445
|
+
import { mkdtemp as mkdtemp2, rm as rm3 } from "fs/promises";
|
|
2446
|
+
import { connect as netConnect, createServer } from "net";
|
|
2447
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2448
|
+
import { join as join4 } from "path";
|
|
2449
|
+
var PORT_POLL_INTERVAL_MS = 100;
|
|
2450
|
+
var DEFAULT_PORT_PROBE_TIMEOUT_MS = 500;
|
|
2451
|
+
function assertPositiveSafeInteger(label, value) {
|
|
2452
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
2453
|
+
throw new CfHanaError("CONFIG", `${label} must be a positive integer`);
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
async function allocateLocalPort() {
|
|
2457
|
+
return await new Promise((resolve, reject) => {
|
|
2458
|
+
const server = createServer();
|
|
2459
|
+
server.once("error", reject);
|
|
2460
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2461
|
+
const address = server.address();
|
|
2462
|
+
server.close(() => {
|
|
2463
|
+
if (address === null || typeof address === "string") {
|
|
2464
|
+
reject(new CfHanaError("CONNECTION", "Could not allocate a local port for a tunnel"));
|
|
2465
|
+
return;
|
|
2466
|
+
}
|
|
2467
|
+
resolve(address.port);
|
|
2468
|
+
});
|
|
2469
|
+
});
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
function probeLocalPort(port, timeoutMs = DEFAULT_PORT_PROBE_TIMEOUT_MS) {
|
|
2473
|
+
return new Promise((resolve) => {
|
|
2474
|
+
let settled = false;
|
|
2475
|
+
const socket = netConnect({ host: "127.0.0.1", port });
|
|
2476
|
+
const finish = (result) => {
|
|
2477
|
+
if (settled) {
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
settled = true;
|
|
2481
|
+
clearTimeout(timer);
|
|
2482
|
+
socket.destroy();
|
|
2483
|
+
resolve(result);
|
|
2484
|
+
};
|
|
2485
|
+
const timer = setTimeout(() => {
|
|
2486
|
+
finish(false);
|
|
2487
|
+
}, timeoutMs);
|
|
2488
|
+
timer.unref();
|
|
2489
|
+
socket.once("connect", () => {
|
|
2490
|
+
finish(true);
|
|
2491
|
+
});
|
|
2492
|
+
socket.once("error", () => {
|
|
2493
|
+
finish(false);
|
|
2494
|
+
});
|
|
2495
|
+
});
|
|
2496
|
+
}
|
|
2497
|
+
function killTunnelProcess(pid) {
|
|
2498
|
+
if (pid === void 0) {
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
try {
|
|
2502
|
+
process.kill(pid, "SIGTERM");
|
|
2503
|
+
} catch {
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
async function withScopedCfSession(selectorSource, target, sapCredentials, work) {
|
|
2507
|
+
if (selectorSource === "ambient") {
|
|
2508
|
+
return await work();
|
|
2509
|
+
}
|
|
2510
|
+
if (sapCredentials === void 0) {
|
|
2511
|
+
throw new CredentialsNotFoundError(
|
|
2512
|
+
"SAP credentials are required to open a tunnel session for an explicit selector"
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
const cfHome = await mkdtemp2(join4(tmpdir2(), "saptools-cf-hana-tunnel-"));
|
|
2516
|
+
try {
|
|
2517
|
+
const ctx = { cfHome };
|
|
2518
|
+
await cfApi(target.apiEndpoint, ctx);
|
|
2519
|
+
await cfAuth(sapCredentials.email, sapCredentials.password, ctx);
|
|
2520
|
+
await cfTargetSpace(target.orgName, target.spaceName, ctx);
|
|
2521
|
+
return await work(ctx);
|
|
2522
|
+
} finally {
|
|
2523
|
+
await rm3(cfHome, { recursive: true, force: true });
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function raceTunnelReadiness(child, localPort, boundMs, probePort, killProcess) {
|
|
2527
|
+
return new Promise((resolve) => {
|
|
2528
|
+
let settled = false;
|
|
2529
|
+
const timers = {};
|
|
2530
|
+
const finish = (result) => {
|
|
2531
|
+
if (settled) {
|
|
2532
|
+
return;
|
|
2533
|
+
}
|
|
2534
|
+
settled = true;
|
|
2535
|
+
clearInterval(timers.poll);
|
|
2536
|
+
clearTimeout(timers.deadline);
|
|
2537
|
+
child.removeListener("exit", onAbort);
|
|
2538
|
+
child.removeListener("error", onAbort);
|
|
2539
|
+
if (result === void 0) {
|
|
2540
|
+
killProcess(child.pid);
|
|
2541
|
+
} else {
|
|
2542
|
+
child.unref();
|
|
2543
|
+
}
|
|
2544
|
+
resolve(result);
|
|
2545
|
+
};
|
|
2546
|
+
function onAbort() {
|
|
2547
|
+
finish();
|
|
2548
|
+
}
|
|
2549
|
+
child.on("exit", onAbort);
|
|
2550
|
+
child.on("error", onAbort);
|
|
2551
|
+
timers.poll = setInterval(() => {
|
|
2552
|
+
void probePort(localPort).then((open) => {
|
|
2553
|
+
if (open && child.pid !== void 0) {
|
|
2554
|
+
finish({ localPort, pid: child.pid });
|
|
2555
|
+
}
|
|
2556
|
+
});
|
|
2557
|
+
}, PORT_POLL_INTERVAL_MS);
|
|
2558
|
+
timers.deadline = setTimeout(finish, boundMs);
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
async function spawnTunnel(params, deps = {}) {
|
|
2562
|
+
assertPositiveSafeInteger("tunnel keepalive seconds", params.keepaliveSeconds);
|
|
2563
|
+
const remainingMs = params.deadline - Date.now();
|
|
2564
|
+
if (remainingMs <= 0) {
|
|
2565
|
+
return void 0;
|
|
2566
|
+
}
|
|
2567
|
+
const boundMs = Math.min(remainingMs, params.candidateTimeoutMs);
|
|
2568
|
+
const spawnProcess = deps.spawnProcess ?? spawn;
|
|
2569
|
+
const probePort = deps.probePort ?? probeLocalPort;
|
|
2570
|
+
const allocatePort = deps.allocatePort ?? allocateLocalPort;
|
|
2571
|
+
const killProcess = deps.killProcess ?? killTunnelProcess;
|
|
2572
|
+
const localPort = await allocatePort();
|
|
2573
|
+
const { bin, argsPrefix } = resolveCfBin();
|
|
2574
|
+
const forward = `${String(localPort)}:${params.hanaHost}:${String(params.hanaPort)}`;
|
|
2575
|
+
const args = [
|
|
2576
|
+
...argsPrefix,
|
|
2577
|
+
"ssh",
|
|
2578
|
+
params.app,
|
|
2579
|
+
"-L",
|
|
2580
|
+
forward,
|
|
2581
|
+
"-c",
|
|
2582
|
+
`sleep ${String(params.keepaliveSeconds)}`
|
|
2583
|
+
];
|
|
2584
|
+
const env = params.cfHome === void 0 ? { ...process.env } : { ...process.env, CF_HOME: params.cfHome };
|
|
2585
|
+
const child = spawnProcess(bin, args, { detached: true, stdio: "ignore", env });
|
|
2586
|
+
return await raceTunnelReadiness(child, localPort, boundMs, probePort, killProcess);
|
|
2587
|
+
}
|
|
2588
|
+
|
|
2589
|
+
// src/tunnel/cache.ts
|
|
2590
|
+
var DEFAULT_ESTABLISHING_STALE_MS = 2 * DEFAULT_TUNNEL_FALLBACK_BUDGET_MS;
|
|
2591
|
+
var DEFAULT_POLL_INTERVAL_MS = 300;
|
|
2592
|
+
function isRecord2(value) {
|
|
2593
|
+
return typeof value === "object" && value !== null;
|
|
2594
|
+
}
|
|
2595
|
+
function defaultIsProcessAlive(pid) {
|
|
2596
|
+
try {
|
|
2597
|
+
process.kill(pid, 0);
|
|
2598
|
+
return true;
|
|
2599
|
+
} catch (error) {
|
|
2600
|
+
return isRecord2(error) && error["code"] !== "ESRCH";
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
function sleep(ms) {
|
|
2604
|
+
return new Promise((resolve) => {
|
|
2605
|
+
setTimeout(resolve, ms);
|
|
2606
|
+
});
|
|
2607
|
+
}
|
|
2608
|
+
function tunnelCacheRoot(saptoolsRoot) {
|
|
2609
|
+
return join5(saptoolsRoot ?? join5(homedir3(), ".saptools"), "cf-hana", "tunnel");
|
|
2610
|
+
}
|
|
2611
|
+
function tunnelCacheKey(host) {
|
|
2612
|
+
return createHash("sha256").update(host).digest("hex");
|
|
2613
|
+
}
|
|
2614
|
+
function tunnelCachePath(host, saptoolsRoot) {
|
|
2615
|
+
return join5(tunnelCacheRoot(saptoolsRoot), `${tunnelCacheKey(host)}.json`);
|
|
2616
|
+
}
|
|
2617
|
+
function isOrgKey(value) {
|
|
2618
|
+
return isRecord2(value) && typeof value["apiEndpoint"] === "string" && typeof value["orgName"] === "string";
|
|
2619
|
+
}
|
|
2620
|
+
function hasEstablishingFields(value) {
|
|
2621
|
+
return value["status"] === "establishing" && typeof value["host"] === "string" && typeof value["startedAt"] === "string" && typeof value["ownerPid"] === "number" && isOrgKey(value["orgKey"]);
|
|
2622
|
+
}
|
|
2623
|
+
function hasReadyFields(value) {
|
|
2624
|
+
return value["status"] === "ready" && typeof value["host"] === "string" && typeof value["localPort"] === "number" && typeof value["pid"] === "number" && typeof value["app"] === "string" && typeof value["expiresAt"] === "string" && isOrgKey(value["orgKey"]);
|
|
2625
|
+
}
|
|
2626
|
+
function isTunnelCacheRecord(value) {
|
|
2627
|
+
return isRecord2(value) && value["version"] === 1 && (hasEstablishingFields(value) || hasReadyFields(value));
|
|
2628
|
+
}
|
|
2629
|
+
async function parseTunnelCacheFile(path) {
|
|
2630
|
+
try {
|
|
2631
|
+
const raw = JSON.parse(await readFile(path, "utf8"));
|
|
2632
|
+
return isTunnelCacheRecord(raw) ? raw : void 0;
|
|
2633
|
+
} catch {
|
|
2634
|
+
return void 0;
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
async function readTunnelCacheEntry(host, options = {}) {
|
|
2638
|
+
const entry = await parseTunnelCacheFile(tunnelCachePath(host, options.saptoolsRoot));
|
|
2639
|
+
return entry?.host === host ? entry : void 0;
|
|
2640
|
+
}
|
|
2641
|
+
function isExpired(entry, now) {
|
|
2642
|
+
const expiresAt = Date.parse(entry.expiresAt);
|
|
2643
|
+
return !Number.isFinite(expiresAt) || now.getTime() >= expiresAt;
|
|
2644
|
+
}
|
|
2645
|
+
async function isTunnelUsable(entry, options = {}) {
|
|
2646
|
+
const isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
2647
|
+
if (!isAlive(entry.pid)) {
|
|
2648
|
+
return false;
|
|
2649
|
+
}
|
|
2650
|
+
if (isExpired(entry, options.now?.() ?? /* @__PURE__ */ new Date())) {
|
|
2651
|
+
return false;
|
|
2652
|
+
}
|
|
2653
|
+
const probePort = options.probePort ?? probeLocalPort;
|
|
2654
|
+
return await probePort(entry.localPort);
|
|
2655
|
+
}
|
|
2656
|
+
function isEstablishingStale(entry, options) {
|
|
2657
|
+
const isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
2658
|
+
if (!isAlive(entry.ownerPid)) {
|
|
2659
|
+
return true;
|
|
2660
|
+
}
|
|
2661
|
+
const startedAt = Date.parse(entry.startedAt);
|
|
2662
|
+
if (!Number.isFinite(startedAt)) {
|
|
2663
|
+
return true;
|
|
2664
|
+
}
|
|
2665
|
+
const now = options.now?.() ?? /* @__PURE__ */ new Date();
|
|
2666
|
+
const staleAfterMs = options.staleAfterMs ?? DEFAULT_ESTABLISHING_STALE_MS;
|
|
2667
|
+
return now.getTime() - startedAt >= staleAfterMs;
|
|
2668
|
+
}
|
|
2669
|
+
async function writeEstablishingMarker(host, ownerPid, orgKey, options) {
|
|
2670
|
+
const root = tunnelCacheRoot(options.saptoolsRoot);
|
|
2671
|
+
await mkdir3(root, { recursive: true, mode: 448 });
|
|
2672
|
+
const record = {
|
|
2673
|
+
version: 1,
|
|
2674
|
+
status: "establishing",
|
|
2675
|
+
host,
|
|
2676
|
+
startedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
2677
|
+
ownerPid,
|
|
2678
|
+
orgKey
|
|
2679
|
+
};
|
|
2680
|
+
try {
|
|
2681
|
+
await writeFile2(tunnelCachePath(host, options.saptoolsRoot), `${JSON.stringify(record)}
|
|
2682
|
+
`, {
|
|
2683
|
+
encoding: "utf8",
|
|
2684
|
+
mode: 384,
|
|
2685
|
+
flag: "wx"
|
|
2686
|
+
});
|
|
2687
|
+
return true;
|
|
2688
|
+
} catch (error) {
|
|
2689
|
+
if (isRecord2(error) && error["code"] === "EEXIST") {
|
|
2690
|
+
return false;
|
|
2691
|
+
}
|
|
2692
|
+
throw error;
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
async function claimEstablishing(host, ownerPid, orgKey, options = {}) {
|
|
2696
|
+
if (await writeEstablishingMarker(host, ownerPid, orgKey, options)) {
|
|
2697
|
+
return { outcome: "claimed" };
|
|
2698
|
+
}
|
|
2699
|
+
const existing = await readTunnelCacheEntry(host, options);
|
|
2700
|
+
if (existing === void 0) {
|
|
2701
|
+
return await writeEstablishingMarker(host, ownerPid, orgKey, options) ? { outcome: "claimed" } : { outcome: "wait" };
|
|
2702
|
+
}
|
|
2703
|
+
if (existing.status === "ready") {
|
|
2704
|
+
return { outcome: "already-ready", record: existing };
|
|
2705
|
+
}
|
|
2706
|
+
if (!isEstablishingStale(existing, options)) {
|
|
2707
|
+
return { outcome: "wait" };
|
|
2708
|
+
}
|
|
2709
|
+
await rm4(tunnelCachePath(host, options.saptoolsRoot), { force: true });
|
|
2710
|
+
return await writeEstablishingMarker(host, ownerPid, orgKey, options) ? { outcome: "claimed" } : { outcome: "wait" };
|
|
2711
|
+
}
|
|
2712
|
+
async function waitForEstablishment(host, deadline, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, options = {}) {
|
|
2713
|
+
for (; ; ) {
|
|
2714
|
+
const entry = await readTunnelCacheEntry(host, options);
|
|
2715
|
+
if (entry === void 0) {
|
|
2716
|
+
return void 0;
|
|
2717
|
+
}
|
|
2718
|
+
if (entry.status === "ready") {
|
|
2719
|
+
return entry;
|
|
2720
|
+
}
|
|
2721
|
+
const remainingMs = deadline - Date.now();
|
|
2722
|
+
if (remainingMs <= 0) {
|
|
2723
|
+
return void 0;
|
|
2724
|
+
}
|
|
2725
|
+
await sleep(Math.min(pollIntervalMs, remainingMs));
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
async function finalizeEstablishingReady(host, record, options = {}) {
|
|
2729
|
+
const root = tunnelCacheRoot(options.saptoolsRoot);
|
|
2730
|
+
const path = tunnelCachePath(host, options.saptoolsRoot);
|
|
2731
|
+
const tempPath = `${path}.tmp-${String(process.pid)}`;
|
|
2732
|
+
const stored = { version: 1, status: "ready", host, ...record };
|
|
2733
|
+
await mkdir3(root, { recursive: true, mode: 448 });
|
|
2734
|
+
await rm4(tempPath, { force: true });
|
|
2735
|
+
await writeFile2(tempPath, `${JSON.stringify(stored)}
|
|
2736
|
+
`, { encoding: "utf8", mode: 384 });
|
|
2737
|
+
await rename(tempPath, path);
|
|
2738
|
+
}
|
|
2739
|
+
async function finalizeEstablishingFailed(host, options = {}) {
|
|
2740
|
+
await rm4(tunnelCachePath(host, options.saptoolsRoot), { force: true });
|
|
2741
|
+
}
|
|
2742
|
+
async function evictTunnelCache(host, options = {}) {
|
|
2743
|
+
await rm4(tunnelCachePath(host, options.saptoolsRoot), { force: true });
|
|
2744
|
+
}
|
|
2745
|
+
function sameOrg(a, b) {
|
|
2746
|
+
return a.apiEndpoint === b.apiEndpoint && a.orgName === b.orgName;
|
|
2747
|
+
}
|
|
2748
|
+
async function reapOneFile(path, currentOrgKey, options) {
|
|
2749
|
+
const entry = await parseTunnelCacheFile(path);
|
|
2750
|
+
if (entry === void 0) {
|
|
2751
|
+
return;
|
|
2752
|
+
}
|
|
2753
|
+
const killProcess = options.killProcess ?? killTunnelProcess;
|
|
2754
|
+
if (entry.status === "establishing") {
|
|
2755
|
+
if (isEstablishingStale(entry, options)) {
|
|
2756
|
+
await rm4(path, { force: true });
|
|
2757
|
+
}
|
|
2758
|
+
return;
|
|
2759
|
+
}
|
|
2760
|
+
const isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
2761
|
+
if (!isAlive(entry.pid)) {
|
|
2762
|
+
await rm4(path, { force: true });
|
|
2763
|
+
return;
|
|
2764
|
+
}
|
|
2765
|
+
const now = options.now?.() ?? /* @__PURE__ */ new Date();
|
|
2766
|
+
if (isExpired(entry, now) || !sameOrg(entry.orgKey, currentOrgKey)) {
|
|
2767
|
+
killProcess(entry.pid);
|
|
2768
|
+
await rm4(path, { force: true });
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
async function reapStaleAndCrossOrgTunnels(currentOrgKey, options = {}) {
|
|
2772
|
+
const root = tunnelCacheRoot(options.saptoolsRoot);
|
|
2773
|
+
let files;
|
|
2774
|
+
try {
|
|
2775
|
+
files = await readdir2(root);
|
|
2776
|
+
} catch {
|
|
2777
|
+
return;
|
|
2778
|
+
}
|
|
2779
|
+
await Promise.all(
|
|
2780
|
+
files.filter((file) => file.endsWith(".json")).map(async (file) => {
|
|
2781
|
+
await reapOneFile(join5(root, file), currentOrgKey, options);
|
|
2782
|
+
})
|
|
2783
|
+
);
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
// src/tunnel/candidates.ts
|
|
2787
|
+
var RUNNING_STATE = "started";
|
|
2788
|
+
var VALID_APP_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
2789
|
+
function isValidAppName(name) {
|
|
2790
|
+
return VALID_APP_NAME.test(name);
|
|
2791
|
+
}
|
|
2792
|
+
function discoveredAppNames(stdout, targetAppName) {
|
|
2793
|
+
if (stdout === void 0) {
|
|
2794
|
+
return [];
|
|
2795
|
+
}
|
|
2796
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2797
|
+
const names = [];
|
|
2798
|
+
for (const row of parseCfAppsOutput(stdout)) {
|
|
2799
|
+
if (row.state !== RUNNING_STATE) {
|
|
2800
|
+
continue;
|
|
2801
|
+
}
|
|
2802
|
+
if (row.name === targetAppName || seen.has(row.name)) {
|
|
2803
|
+
continue;
|
|
2804
|
+
}
|
|
2805
|
+
if (!isValidAppName(row.name)) {
|
|
2806
|
+
continue;
|
|
2807
|
+
}
|
|
2808
|
+
seen.add(row.name);
|
|
2809
|
+
names.push(row.name);
|
|
2810
|
+
}
|
|
2811
|
+
return names;
|
|
2812
|
+
}
|
|
2813
|
+
function buildCandidateList(targetAppName, discoveredStdout, maxCandidates) {
|
|
2814
|
+
const discovered = discoveredAppNames(discoveredStdout, targetAppName);
|
|
2815
|
+
return [targetAppName, ...discovered.slice(0, maxCandidates)];
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// src/tunnel/classifier.ts
|
|
2819
|
+
var CONNECT_TIMEOUT_MESSAGE = "HANA connection timed out";
|
|
2820
|
+
var OPEN_CONNECTION_CODE = "EHDBOPENCONN";
|
|
2821
|
+
var MAX_CAUSE_DEPTH = 5;
|
|
2822
|
+
function causeCode(value) {
|
|
2823
|
+
return typeof value === "object" && value !== null ? value.code : void 0;
|
|
2824
|
+
}
|
|
2825
|
+
function causeOf(value) {
|
|
2826
|
+
return typeof value === "object" && value !== null ? value.cause : void 0;
|
|
2827
|
+
}
|
|
2828
|
+
function hasOpenConnectionCause(cause, depth) {
|
|
2829
|
+
if (depth > MAX_CAUSE_DEPTH) {
|
|
2830
|
+
return false;
|
|
2831
|
+
}
|
|
2832
|
+
if (causeCode(cause) === OPEN_CONNECTION_CODE) {
|
|
2833
|
+
return true;
|
|
2834
|
+
}
|
|
2835
|
+
return hasOpenConnectionCause(causeOf(cause), depth + 1);
|
|
2836
|
+
}
|
|
2837
|
+
function isConnectivityFailure(error) {
|
|
2838
|
+
if (!(error instanceof CfHanaError)) {
|
|
2839
|
+
return false;
|
|
2840
|
+
}
|
|
2841
|
+
if (error.code === "TIMEOUT") {
|
|
2842
|
+
return error.message.includes(CONNECT_TIMEOUT_MESSAGE);
|
|
2843
|
+
}
|
|
2844
|
+
if (error.code !== "CONNECTION") {
|
|
2845
|
+
return false;
|
|
2846
|
+
}
|
|
2847
|
+
return hasOpenConnectionCause(error.cause, 0);
|
|
2848
|
+
}
|
|
2849
|
+
|
|
2850
|
+
// src/tunnel/fallback.ts
|
|
2851
|
+
var EXPIRY_SAFETY_MARGIN_MS = 3e4;
|
|
2852
|
+
function directParamsOf(config) {
|
|
2853
|
+
return {
|
|
2854
|
+
host: config.host,
|
|
2855
|
+
port: config.port,
|
|
2856
|
+
user: config.user,
|
|
2857
|
+
password: config.password,
|
|
2858
|
+
schema: config.schema,
|
|
2859
|
+
certificate: config.certificate,
|
|
2860
|
+
connectTimeoutMs: config.connectTimeoutMs
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
function tunneledParams(directParams, localPort) {
|
|
2864
|
+
return { ...directParams, host: "127.0.0.1", port: localPort, servername: directParams.host };
|
|
2865
|
+
}
|
|
2866
|
+
function orgKeyOf(config) {
|
|
2867
|
+
return { apiEndpoint: config.apiEndpoint, orgName: config.orgName };
|
|
2868
|
+
}
|
|
2869
|
+
function tunnelExpiryIso() {
|
|
2870
|
+
return new Date(
|
|
2871
|
+
Date.now() + DEFAULT_TUNNEL_KEEPALIVE_SECONDS * 1e3 - EXPIRY_SAFETY_MARGIN_MS
|
|
2872
|
+
).toISOString();
|
|
2873
|
+
}
|
|
2874
|
+
async function discardQuietly(work) {
|
|
2875
|
+
await work.catch(() => {
|
|
2876
|
+
});
|
|
2877
|
+
}
|
|
2878
|
+
async function tryReadyRecord(record, driver, config, directParams, cacheOptions) {
|
|
2879
|
+
try {
|
|
2880
|
+
const connection = await driver.connect(tunneledParams(directParams, record.localPort));
|
|
2881
|
+
config.onTunnelStatus?.(`connected via SSH tunnel through ${record.app}`);
|
|
2882
|
+
return connection;
|
|
2883
|
+
} catch (error) {
|
|
2884
|
+
if (isConnectivityFailure(error)) {
|
|
2885
|
+
await discardQuietly(evictTunnelCache(config.host, cacheOptions));
|
|
2886
|
+
return void 0;
|
|
2887
|
+
}
|
|
2888
|
+
throw error;
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
async function discoverAppsStdout(ctx) {
|
|
2892
|
+
try {
|
|
2893
|
+
return ctx === void 0 ? await cfAppsDirect() : await cfApps(ctx);
|
|
2894
|
+
} catch {
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
async function buildCandidates(config, ctx, hintApp) {
|
|
2899
|
+
const stdout = await discoverAppsStdout(ctx);
|
|
2900
|
+
const base = buildCandidateList(config.appName, stdout, DEFAULT_TUNNEL_MAX_CANDIDATES);
|
|
2901
|
+
return hintApp === void 0 || base.includes(hintApp) ? base : [hintApp, ...base];
|
|
2902
|
+
}
|
|
2903
|
+
async function finalizeCandidateConnection(app, spawned, driver, config, directParams, cacheOptions) {
|
|
2904
|
+
const readyRecord = {
|
|
2905
|
+
localPort: spawned.localPort,
|
|
2906
|
+
pid: spawned.pid,
|
|
2907
|
+
app,
|
|
2908
|
+
orgKey: orgKeyOf(config),
|
|
2909
|
+
expiresAt: tunnelExpiryIso()
|
|
2910
|
+
};
|
|
2911
|
+
try {
|
|
2912
|
+
const connection = await driver.connect(tunneledParams(directParams, spawned.localPort));
|
|
2913
|
+
await discardQuietly(finalizeEstablishingReady(config.host, readyRecord, cacheOptions));
|
|
2914
|
+
config.onTunnelStatus?.(`connected via SSH tunnel through ${app}`);
|
|
2915
|
+
return connection;
|
|
2916
|
+
} catch (error) {
|
|
2917
|
+
if (isConnectivityFailure(error)) {
|
|
2918
|
+
await discardQuietly(finalizeEstablishingFailed(config.host, cacheOptions));
|
|
2919
|
+
killTunnelProcess(spawned.pid);
|
|
2920
|
+
return void 0;
|
|
2921
|
+
}
|
|
2922
|
+
await discardQuietly(finalizeEstablishingReady(config.host, readyRecord, cacheOptions));
|
|
2923
|
+
throw error;
|
|
2924
|
+
}
|
|
2925
|
+
}
|
|
2926
|
+
async function tryCandidate(app, ctx, driver, config, directParams, deadline, overrides) {
|
|
2927
|
+
const cacheOptions = overrides.cache ?? {};
|
|
2928
|
+
config.onTunnelStatus?.(`trying to reach ${config.host} via app ${app}...`);
|
|
2929
|
+
const claim = await claimEstablishing(config.host, process.pid, orgKeyOf(config), cacheOptions);
|
|
2930
|
+
if (claim.outcome === "already-ready") {
|
|
2931
|
+
return await tryReadyRecord(claim.record, driver, config, directParams, cacheOptions);
|
|
2932
|
+
}
|
|
2933
|
+
if (claim.outcome === "wait") {
|
|
2934
|
+
const ready = await waitForEstablishment(config.host, deadline, void 0, cacheOptions);
|
|
2935
|
+
return ready === void 0 ? void 0 : await tryReadyRecord(ready, driver, config, directParams, cacheOptions);
|
|
2936
|
+
}
|
|
2937
|
+
const spawned = await spawnTunnel(
|
|
2938
|
+
{
|
|
2939
|
+
cfHome: ctx?.cfHome,
|
|
2940
|
+
app,
|
|
2941
|
+
hanaHost: config.host,
|
|
2942
|
+
hanaPort: config.port,
|
|
2943
|
+
keepaliveSeconds: DEFAULT_TUNNEL_KEEPALIVE_SECONDS,
|
|
2944
|
+
deadline,
|
|
2945
|
+
candidateTimeoutMs: DEFAULT_TUNNEL_CANDIDATE_TIMEOUT_MS
|
|
2946
|
+
},
|
|
2947
|
+
overrides.process ?? {}
|
|
2948
|
+
);
|
|
2949
|
+
if (spawned === void 0) {
|
|
2950
|
+
await discardQuietly(finalizeEstablishingFailed(config.host, cacheOptions));
|
|
2951
|
+
return void 0;
|
|
2952
|
+
}
|
|
2953
|
+
return await finalizeCandidateConnection(app, spawned, driver, config, directParams, cacheOptions);
|
|
2954
|
+
}
|
|
2955
|
+
async function tryCachedTunnel(driver, config, directParams, cacheOptions) {
|
|
2956
|
+
if (config.refreshTunnel) {
|
|
2957
|
+
await discardQuietly(evictTunnelCache(config.host, cacheOptions));
|
|
2958
|
+
return { connection: void 0, hintApp: void 0 };
|
|
2959
|
+
}
|
|
2960
|
+
const entry = await readTunnelCacheEntry(config.host, cacheOptions);
|
|
2961
|
+
if (entry?.status !== "ready") {
|
|
2962
|
+
return { connection: void 0, hintApp: void 0 };
|
|
2963
|
+
}
|
|
2964
|
+
if (!await isTunnelUsable(entry, cacheOptions)) {
|
|
2965
|
+
await discardQuietly(evictTunnelCache(config.host, cacheOptions));
|
|
2966
|
+
return { connection: void 0, hintApp: entry.app };
|
|
2967
|
+
}
|
|
2968
|
+
const connection = await tryReadyRecord(entry, driver, config, directParams, cacheOptions);
|
|
2969
|
+
return { connection, hintApp: entry.app };
|
|
2970
|
+
}
|
|
2971
|
+
async function tryDirectConnect(driver, config, directParams) {
|
|
2972
|
+
if (config.tunnelMode !== "auto") {
|
|
2973
|
+
return { connection: void 0, directError: void 0 };
|
|
2974
|
+
}
|
|
2975
|
+
try {
|
|
2976
|
+
return { connection: await driver.connect(directParams), directError: void 0 };
|
|
2977
|
+
} catch (error) {
|
|
2978
|
+
if (!isConnectivityFailure(error)) {
|
|
2979
|
+
throw error;
|
|
2980
|
+
}
|
|
2981
|
+
return { connection: void 0, directError: error };
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
function buildExhaustionError(host, directError, candidates) {
|
|
2985
|
+
if (directError !== void 0) {
|
|
2986
|
+
return directError;
|
|
2987
|
+
}
|
|
2988
|
+
return new CfHanaError(
|
|
2989
|
+
"CONNECTION",
|
|
2990
|
+
`Could not establish an SSH tunnel to ${host} through any candidate app (tried: ${candidates.length > 0 ? candidates.join(", ") : "none"})`
|
|
2991
|
+
);
|
|
2992
|
+
}
|
|
2993
|
+
async function runCandidateLoop(ctx, driver, config, directParams, deadline, hintApp, overrides) {
|
|
2994
|
+
const candidatesTried = await buildCandidates(config, ctx, hintApp);
|
|
2995
|
+
for (const app of candidatesTried) {
|
|
2996
|
+
if (Date.now() >= deadline) {
|
|
2997
|
+
break;
|
|
2998
|
+
}
|
|
2999
|
+
const connection = await tryCandidate(app, ctx, driver, config, directParams, deadline, overrides);
|
|
3000
|
+
if (connection !== void 0) {
|
|
3001
|
+
return { connection, candidatesTried };
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
return { connection: void 0, candidatesTried };
|
|
3005
|
+
}
|
|
3006
|
+
async function connectWithTunnelFallback(driver, config, overrides = {}) {
|
|
3007
|
+
const directParams = directParamsOf(config);
|
|
3008
|
+
const cacheOptions = overrides.cache ?? {};
|
|
3009
|
+
await discardQuietly(reapStaleAndCrossOrgTunnels(orgKeyOf(config), cacheOptions));
|
|
3010
|
+
const cached = await tryCachedTunnel(driver, config, directParams, cacheOptions);
|
|
3011
|
+
if (cached.connection !== void 0) {
|
|
3012
|
+
return cached.connection;
|
|
3013
|
+
}
|
|
3014
|
+
const direct = await tryDirectConnect(driver, config, directParams);
|
|
3015
|
+
if (direct.connection !== void 0) {
|
|
3016
|
+
return direct.connection;
|
|
3017
|
+
}
|
|
3018
|
+
const deadline = Date.now() + DEFAULT_TUNNEL_FALLBACK_BUDGET_MS;
|
|
3019
|
+
const target = {
|
|
3020
|
+
apiEndpoint: config.apiEndpoint,
|
|
3021
|
+
orgName: config.orgName,
|
|
3022
|
+
spaceName: config.spaceName
|
|
3023
|
+
};
|
|
3024
|
+
const loopResult = await withScopedCfSession(
|
|
3025
|
+
config.selectorSource,
|
|
3026
|
+
target,
|
|
3027
|
+
config.sapCredentials,
|
|
3028
|
+
(ctx) => runCandidateLoop(ctx, driver, config, directParams, deadline, cached.hintApp, overrides)
|
|
3029
|
+
);
|
|
3030
|
+
if (loopResult.connection !== void 0) {
|
|
3031
|
+
return loopResult.connection;
|
|
3032
|
+
}
|
|
3033
|
+
throw buildExhaustionError(config.host, direct.directError, loopResult.candidatesTried);
|
|
3034
|
+
}
|
|
3035
|
+
|
|
2363
3036
|
// src/connection.ts
|
|
2364
3037
|
function assertValidAutoLimit(value) {
|
|
2365
3038
|
if (value !== false && (!Number.isSafeInteger(value) || value <= 0 || value >= Number.MAX_SAFE_INTEGER)) {
|
|
@@ -2453,15 +3126,7 @@ var Connection = class _Connection {
|
|
|
2453
3126
|
driverConnection;
|
|
2454
3127
|
config;
|
|
2455
3128
|
static async open(driver, config) {
|
|
2456
|
-
const driverConnection = await driver
|
|
2457
|
-
host: config.host,
|
|
2458
|
-
port: config.port,
|
|
2459
|
-
user: config.user,
|
|
2460
|
-
password: config.password,
|
|
2461
|
-
schema: config.schema,
|
|
2462
|
-
certificate: config.certificate,
|
|
2463
|
-
connectTimeoutMs: config.connectTimeoutMs
|
|
2464
|
-
});
|
|
3129
|
+
const driverConnection = await connectWithTunnelFallback(driver, config);
|
|
2465
3130
|
return new _Connection(driverConnection, config);
|
|
2466
3131
|
}
|
|
2467
3132
|
async query(sql, params, options) {
|
|
@@ -2743,6 +3408,31 @@ function toPoolOptions(options) {
|
|
|
2743
3408
|
}
|
|
2744
3409
|
return options.pool ?? {};
|
|
2745
3410
|
}
|
|
3411
|
+
function buildConnectionConfig(resolved, target, options) {
|
|
3412
|
+
const sapCredentials = readSapCredentials({ email: options.email, password: options.password });
|
|
3413
|
+
return {
|
|
3414
|
+
host: target.host,
|
|
3415
|
+
port: target.port,
|
|
3416
|
+
user: target.user,
|
|
3417
|
+
password: target.password,
|
|
3418
|
+
schema: target.schema,
|
|
3419
|
+
certificate: target.certificate,
|
|
3420
|
+
connectTimeoutMs: options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
|
|
3421
|
+
queryTimeoutMs: options.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
3422
|
+
readOnly: options.readOnly ?? false,
|
|
3423
|
+
allowDestructive: options.allowDestructive ?? false,
|
|
3424
|
+
autoLimit: options.autoLimit ?? DEFAULT_AUTO_LIMIT,
|
|
3425
|
+
appName: resolved.appName,
|
|
3426
|
+
orgName: resolved.orgName,
|
|
3427
|
+
spaceName: resolved.spaceName,
|
|
3428
|
+
apiEndpoint: resolved.apiEndpoint,
|
|
3429
|
+
selectorSource: resolved.selectorSource,
|
|
3430
|
+
tunnelMode: options.tunnel === true ? "always" : "auto",
|
|
3431
|
+
refreshTunnel: options.refreshTunnel ?? false,
|
|
3432
|
+
...sapCredentials === void 0 ? {} : { sapCredentials },
|
|
3433
|
+
...options.onTunnelStatus === void 0 ? {} : { onTunnelStatus: options.onTunnelStatus }
|
|
3434
|
+
};
|
|
3435
|
+
}
|
|
2746
3436
|
function toSelectedBindingInfo(bindings, selected) {
|
|
2747
3437
|
const availableBindingNames = bindings.flatMap(
|
|
2748
3438
|
(binding) => binding.name === void 0 ? [] : [binding.name]
|
|
@@ -2770,19 +3460,7 @@ var HanaClient = class _HanaClient {
|
|
|
2770
3460
|
const bindingInfo = toSelectedBindingInfo(resolved.bindings, binding);
|
|
2771
3461
|
const target = toConnectionTarget(binding, role);
|
|
2772
3462
|
const driver = createDriver();
|
|
2773
|
-
const config =
|
|
2774
|
-
host: target.host,
|
|
2775
|
-
port: target.port,
|
|
2776
|
-
user: target.user,
|
|
2777
|
-
password: target.password,
|
|
2778
|
-
schema: target.schema,
|
|
2779
|
-
certificate: target.certificate,
|
|
2780
|
-
connectTimeoutMs: options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
|
|
2781
|
-
queryTimeoutMs: options.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
2782
|
-
readOnly: options.readOnly ?? false,
|
|
2783
|
-
allowDestructive: options.allowDestructive ?? false,
|
|
2784
|
-
autoLimit: options.autoLimit ?? DEFAULT_AUTO_LIMIT
|
|
2785
|
-
};
|
|
3463
|
+
const config = buildConnectionConfig(resolved, target, options);
|
|
2786
3464
|
const poolOptions = toPoolOptions(options);
|
|
2787
3465
|
const info = {
|
|
2788
3466
|
selector: resolved.selector,
|