@saptools/cf-hana 0.4.1 → 0.5.1
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 +64 -0
- package/README.md +91 -1
- package/dist/cli.js +1060 -142
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +35 -4
- package/dist/index.js +1024 -120
- 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.1";
|
|
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;
|
|
@@ -417,7 +438,7 @@ function findTopLevelChar(sql, target, startIndex, endIndex) {
|
|
|
417
438
|
}
|
|
418
439
|
|
|
419
440
|
// src/statements.ts
|
|
420
|
-
var SELECT_KEYWORDS = /* @__PURE__ */ new Set(["SELECT"
|
|
441
|
+
var SELECT_KEYWORDS = /* @__PURE__ */ new Set(["SELECT"]);
|
|
421
442
|
var DML_KEYWORDS = /* @__PURE__ */ new Set(["INSERT", "UPDATE", "DELETE", "MERGE", "UPSERT", "REPLACE"]);
|
|
422
443
|
var DDL_KEYWORDS = /* @__PURE__ */ new Set(["CREATE", "DROP", "ALTER", "TRUNCATE", "RENAME", "COMMENT"]);
|
|
423
444
|
function firstKeyword(sql) {
|
|
@@ -457,8 +478,165 @@ function firstKeyword(sql) {
|
|
|
457
478
|
}
|
|
458
479
|
return sql.slice(index, keywordEnd).toUpperCase();
|
|
459
480
|
}
|
|
460
|
-
function
|
|
461
|
-
const
|
|
481
|
+
function skipQuotedText2(sql, start) {
|
|
482
|
+
const quote = sql[start];
|
|
483
|
+
let index = start + 1;
|
|
484
|
+
while (index < sql.length) {
|
|
485
|
+
if (sql[index] === quote) {
|
|
486
|
+
if (sql[index + 1] === quote) {
|
|
487
|
+
index += 2;
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
index += 1;
|
|
491
|
+
break;
|
|
492
|
+
}
|
|
493
|
+
index += 1;
|
|
494
|
+
}
|
|
495
|
+
return index;
|
|
496
|
+
}
|
|
497
|
+
function skipLineComment2(sql, start) {
|
|
498
|
+
let index = start + 2;
|
|
499
|
+
while (index < sql.length && sql[index] !== "\n") {
|
|
500
|
+
index += 1;
|
|
501
|
+
}
|
|
502
|
+
return index;
|
|
503
|
+
}
|
|
504
|
+
function skipBlockComment2(sql, start) {
|
|
505
|
+
let index = start + 2;
|
|
506
|
+
while (index < sql.length && !(sql[index] === "*" && sql[index + 1] === "/")) {
|
|
507
|
+
index += 1;
|
|
508
|
+
}
|
|
509
|
+
return Math.min(index + 2, sql.length);
|
|
510
|
+
}
|
|
511
|
+
function maskIgnoredSqlText(sql) {
|
|
512
|
+
let masked = "";
|
|
513
|
+
let index = 0;
|
|
514
|
+
while (index < sql.length) {
|
|
515
|
+
const char = sql[index];
|
|
516
|
+
if (char === "'" || char === '"') {
|
|
517
|
+
const end = skipQuotedText2(sql, index);
|
|
518
|
+
masked += " ".repeat(end - index);
|
|
519
|
+
index = end;
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
if (char === "-" && sql[index + 1] === "-") {
|
|
523
|
+
const end = skipLineComment2(sql, index);
|
|
524
|
+
masked += " ".repeat(end - index);
|
|
525
|
+
index = end;
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
if (char === "/" && sql[index + 1] === "*") {
|
|
529
|
+
const end = skipBlockComment2(sql, index);
|
|
530
|
+
masked += " ".repeat(end - index);
|
|
531
|
+
index = end;
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
masked += char ?? "";
|
|
535
|
+
index += 1;
|
|
536
|
+
}
|
|
537
|
+
return masked;
|
|
538
|
+
}
|
|
539
|
+
function topLevelKeywordIndex(sql, keyword) {
|
|
540
|
+
const masked = maskIgnoredSqlText(sql);
|
|
541
|
+
let depth = 0;
|
|
542
|
+
for (let index = 0; index < masked.length; index += 1) {
|
|
543
|
+
const char = masked[index];
|
|
544
|
+
if (char === "(") {
|
|
545
|
+
depth += 1;
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (char === ")") {
|
|
549
|
+
depth = Math.max(0, depth - 1);
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
if (depth === 0 && masked.slice(index, index + keyword.length).toUpperCase() === keyword && !/[A-Za-z0-9_$#]/.test(masked.charAt(index - 1)) && !/[A-Za-z0-9_$#]/.test(masked.charAt(index + keyword.length))) {
|
|
553
|
+
return index;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
return void 0;
|
|
557
|
+
}
|
|
558
|
+
function hasTopLevelKeyword(sql, keyword) {
|
|
559
|
+
return topLevelKeywordIndex(sql, keyword) !== void 0;
|
|
560
|
+
}
|
|
561
|
+
function isIdentifierChar2(char) {
|
|
562
|
+
return /[A-Za-z0-9_$#]/.test(char);
|
|
563
|
+
}
|
|
564
|
+
function skipWhitespace(masked, start) {
|
|
565
|
+
let index = start;
|
|
566
|
+
while (index < masked.length && /\s/.test(masked.charAt(index))) {
|
|
567
|
+
index += 1;
|
|
568
|
+
}
|
|
569
|
+
return index;
|
|
570
|
+
}
|
|
571
|
+
function matchingCloseParenIndex(masked, openIndex) {
|
|
572
|
+
let depth = 0;
|
|
573
|
+
for (let index = openIndex; index < masked.length; index += 1) {
|
|
574
|
+
const char = masked.charAt(index);
|
|
575
|
+
if (char === "(") {
|
|
576
|
+
depth += 1;
|
|
577
|
+
} else if (char === ")") {
|
|
578
|
+
depth -= 1;
|
|
579
|
+
if (depth === 0) {
|
|
580
|
+
return index;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return void 0;
|
|
585
|
+
}
|
|
586
|
+
function isAsKeywordAt(masked, index) {
|
|
587
|
+
return masked.slice(index, index + 2).toUpperCase() === "AS" && !isIdentifierChar2(masked.charAt(index - 1)) && !isIdentifierChar2(masked.charAt(index + 2));
|
|
588
|
+
}
|
|
589
|
+
function cteListEndIndex(masked, afterWith) {
|
|
590
|
+
let index = afterWith;
|
|
591
|
+
for (; ; ) {
|
|
592
|
+
index = skipWhitespace(masked, index);
|
|
593
|
+
while (index < masked.length && isIdentifierChar2(masked.charAt(index))) {
|
|
594
|
+
index += 1;
|
|
595
|
+
}
|
|
596
|
+
index = skipWhitespace(masked, index);
|
|
597
|
+
if (masked.charAt(index) === "(") {
|
|
598
|
+
const columnListEnd = matchingCloseParenIndex(masked, index);
|
|
599
|
+
if (columnListEnd === void 0) {
|
|
600
|
+
return void 0;
|
|
601
|
+
}
|
|
602
|
+
index = skipWhitespace(masked, columnListEnd + 1);
|
|
603
|
+
}
|
|
604
|
+
if (!isAsKeywordAt(masked, index)) {
|
|
605
|
+
return void 0;
|
|
606
|
+
}
|
|
607
|
+
index = skipWhitespace(masked, index + 2);
|
|
608
|
+
if (masked.charAt(index) !== "(") {
|
|
609
|
+
return void 0;
|
|
610
|
+
}
|
|
611
|
+
const closeIndex = matchingCloseParenIndex(masked, index);
|
|
612
|
+
if (closeIndex === void 0) {
|
|
613
|
+
return void 0;
|
|
614
|
+
}
|
|
615
|
+
index = skipWhitespace(masked, closeIndex + 1);
|
|
616
|
+
if (masked.charAt(index) === ",") {
|
|
617
|
+
index += 1;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
return index;
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
function resolveWithStatement(sql) {
|
|
624
|
+
const withIndex = topLevelKeywordIndex(sql, "WITH");
|
|
625
|
+
if (withIndex === void 0) {
|
|
626
|
+
return void 0;
|
|
627
|
+
}
|
|
628
|
+
const masked = maskIgnoredSqlText(sql);
|
|
629
|
+
const trailingIndex = cteListEndIndex(masked, withIndex + "WITH".length);
|
|
630
|
+
if (trailingIndex === void 0) {
|
|
631
|
+
return void 0;
|
|
632
|
+
}
|
|
633
|
+
let keywordEnd = trailingIndex;
|
|
634
|
+
while (keywordEnd < masked.length && isIdentifierChar2(masked.charAt(keywordEnd))) {
|
|
635
|
+
keywordEnd += 1;
|
|
636
|
+
}
|
|
637
|
+
return { keyword: masked.slice(trailingIndex, keywordEnd).toUpperCase(), index: trailingIndex };
|
|
638
|
+
}
|
|
639
|
+
function classifyByKeyword(keyword) {
|
|
462
640
|
if (SELECT_KEYWORDS.has(keyword)) {
|
|
463
641
|
return "select";
|
|
464
642
|
}
|
|
@@ -470,6 +648,14 @@ function classifyStatement(sql) {
|
|
|
470
648
|
}
|
|
471
649
|
return "unknown";
|
|
472
650
|
}
|
|
651
|
+
function classifyStatement(sql) {
|
|
652
|
+
const keyword = firstKeyword(sql);
|
|
653
|
+
if (keyword === "WITH") {
|
|
654
|
+
const resolved = resolveWithStatement(sql);
|
|
655
|
+
return resolved === void 0 ? "unknown" : classifyByKeyword(resolved.keyword);
|
|
656
|
+
}
|
|
657
|
+
return classifyByKeyword(keyword);
|
|
658
|
+
}
|
|
473
659
|
function quoteIdentifier(identifier) {
|
|
474
660
|
if (identifier.length === 0) {
|
|
475
661
|
throw new QueryError("A SQL identifier must not be empty");
|
|
@@ -633,11 +819,11 @@ function mergeTargetReference(statementSql, target, usingStart) {
|
|
|
633
819
|
return target.reference;
|
|
634
820
|
}
|
|
635
821
|
if (/^PARTITION\b/i.test(suffix)) {
|
|
636
|
-
const
|
|
637
|
-
if (
|
|
822
|
+
const open2 = suffix.indexOf("(");
|
|
823
|
+
if (open2 === -1) {
|
|
638
824
|
return void 0;
|
|
639
825
|
}
|
|
640
|
-
const close = findTopLevelChar(suffix, ")",
|
|
826
|
+
const close = findTopLevelChar(suffix, ")", open2 + 1, suffix.length);
|
|
641
827
|
if (close === void 0) {
|
|
642
828
|
return void 0;
|
|
643
829
|
}
|
|
@@ -820,13 +1006,19 @@ function dispatchWriteBackupPlan(keyword, statementSql, params) {
|
|
|
820
1006
|
}
|
|
821
1007
|
}
|
|
822
1008
|
function buildWriteBackupPlan(sql, params = []) {
|
|
823
|
-
const
|
|
824
|
-
const
|
|
1009
|
+
const trimmed = trimStatementSql(sql);
|
|
1010
|
+
const leading = firstKeyword(trimmed);
|
|
1011
|
+
const withResolved = leading === "WITH" ? resolveWithStatement(trimmed) : void 0;
|
|
1012
|
+
const keyword = withResolved?.keyword ?? leading;
|
|
825
1013
|
if (!isWriteKeyword(keyword)) {
|
|
826
1014
|
return void 0;
|
|
827
1015
|
}
|
|
828
|
-
|
|
829
|
-
|
|
1016
|
+
const startIndex = withResolved?.index ?? 0;
|
|
1017
|
+
const statementSql = trimmed.slice(startIndex);
|
|
1018
|
+
const slicedParams = params.slice(countPlaceholders(trimmed.slice(0, startIndex)));
|
|
1019
|
+
assertParamArity(statementSql, slicedParams);
|
|
1020
|
+
const plan = dispatchWriteBackupPlan(keyword, statementSql, slicedParams);
|
|
1021
|
+
return plan === void 0 ? void 0 : { ...plan, statementSql: trimmed };
|
|
830
1022
|
}
|
|
831
1023
|
|
|
832
1024
|
// src/backup.ts
|
|
@@ -1155,9 +1347,9 @@ function buildEnv(ctx, overrides = {}) {
|
|
|
1155
1347
|
env["CF_HOME"] = ctx.cfHome;
|
|
1156
1348
|
return env;
|
|
1157
1349
|
}
|
|
1158
|
-
async function execWithRetries(bin, args, env, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
1350
|
+
async function execWithRetries(bin, args, env, timeoutMs = DEFAULT_TIMEOUT_MS, maxAttempts = CF_RETRY_ATTEMPTS) {
|
|
1159
1351
|
let lastErr;
|
|
1160
|
-
for (let attempt = 0; attempt <
|
|
1352
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1161
1353
|
try {
|
|
1162
1354
|
const { stdout } = await execFileAsync(bin, args, {
|
|
1163
1355
|
env,
|
|
@@ -1176,18 +1368,24 @@ async function execWithRetries(bin, args, env, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
1176
1368
|
if (isEnoent || !isTimeout && !isNetworkFlake) {
|
|
1177
1369
|
throw err;
|
|
1178
1370
|
}
|
|
1179
|
-
if (attempt <
|
|
1371
|
+
if (attempt < maxAttempts - 1) {
|
|
1180
1372
|
await new Promise((r) => setTimeout(r, CF_RETRY_BASE_DELAY_MS * (attempt + 1)));
|
|
1181
1373
|
}
|
|
1182
1374
|
}
|
|
1183
1375
|
}
|
|
1184
1376
|
throw lastErr;
|
|
1185
1377
|
}
|
|
1186
|
-
async function runCf(args, ctx, overrides = {}) {
|
|
1378
|
+
async function runCf(args, ctx, overrides = {}, execOptions = {}) {
|
|
1187
1379
|
const { bin, argsPrefix } = resolveCfBin();
|
|
1188
1380
|
const env = buildEnv(ctx, overrides);
|
|
1189
1381
|
try {
|
|
1190
|
-
return await execWithRetries(
|
|
1382
|
+
return await execWithRetries(
|
|
1383
|
+
bin,
|
|
1384
|
+
[...argsPrefix, ...args],
|
|
1385
|
+
env,
|
|
1386
|
+
execOptions.timeoutMs,
|
|
1387
|
+
execOptions.maxAttempts
|
|
1388
|
+
);
|
|
1191
1389
|
} catch (lastErr) {
|
|
1192
1390
|
const e = lastErr;
|
|
1193
1391
|
const detail = e?.stderr ? String(e.stderr) : e?.message ?? "";
|
|
@@ -1217,6 +1415,27 @@ async function cfEnvDirect(appName) {
|
|
|
1217
1415
|
delete env["SAP_PASSWORD"];
|
|
1218
1416
|
return await execWithRetries(bin, [...argsPrefix, "env", appName], env);
|
|
1219
1417
|
}
|
|
1418
|
+
async function cfApps(ctx, timeoutMs) {
|
|
1419
|
+
return await runCf(
|
|
1420
|
+
["apps"],
|
|
1421
|
+
ctx,
|
|
1422
|
+
{},
|
|
1423
|
+
timeoutMs === void 0 ? {} : { timeoutMs, maxAttempts: 1 }
|
|
1424
|
+
);
|
|
1425
|
+
}
|
|
1426
|
+
async function cfAppsDirect(timeoutMs) {
|
|
1427
|
+
const { bin, argsPrefix } = resolveCfBin();
|
|
1428
|
+
const env = { ...process.env };
|
|
1429
|
+
delete env["SAP_EMAIL"];
|
|
1430
|
+
delete env["SAP_PASSWORD"];
|
|
1431
|
+
return await execWithRetries(
|
|
1432
|
+
bin,
|
|
1433
|
+
[...argsPrefix, "apps"],
|
|
1434
|
+
env,
|
|
1435
|
+
timeoutMs,
|
|
1436
|
+
timeoutMs === void 0 ? void 0 : 1
|
|
1437
|
+
);
|
|
1438
|
+
}
|
|
1220
1439
|
async function readCurrentCfTarget() {
|
|
1221
1440
|
const { bin, argsPrefix } = resolveCfBin();
|
|
1222
1441
|
const env = { ...process.env };
|
|
@@ -1266,6 +1485,26 @@ function parseTargetFields(stdout) {
|
|
|
1266
1485
|
}
|
|
1267
1486
|
return map;
|
|
1268
1487
|
}
|
|
1488
|
+
function parseCfAppsOutput(stdout) {
|
|
1489
|
+
const lines = stdout.split(/\r?\n/);
|
|
1490
|
+
const headerIndex = lines.findIndex((line) => /^\s*name\s+\S/i.test(line));
|
|
1491
|
+
if (headerIndex === -1) {
|
|
1492
|
+
return [];
|
|
1493
|
+
}
|
|
1494
|
+
const rows = [];
|
|
1495
|
+
for (const line of lines.slice(headerIndex + 1)) {
|
|
1496
|
+
const trimmed = line.trim();
|
|
1497
|
+
if (trimmed.length === 0) {
|
|
1498
|
+
continue;
|
|
1499
|
+
}
|
|
1500
|
+
const [name, state] = trimmed.split(/\s+/);
|
|
1501
|
+
if (name === void 0 || state === void 0) {
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
rows.push({ name, state });
|
|
1505
|
+
}
|
|
1506
|
+
return rows;
|
|
1507
|
+
}
|
|
1269
1508
|
function formatCurrentCfAppSelector(target, appName) {
|
|
1270
1509
|
const name = appName.trim();
|
|
1271
1510
|
if (!name) {
|
|
@@ -1577,7 +1816,10 @@ async function resolveAppBindings(rawSelector, options) {
|
|
|
1577
1816
|
source: "live",
|
|
1578
1817
|
selectorSource: target.selectorSource,
|
|
1579
1818
|
regionConfirmed: target.regionConfirmed,
|
|
1580
|
-
selectorCanBePinned: target.selectorCanBePinned
|
|
1819
|
+
selectorCanBePinned: target.selectorCanBePinned,
|
|
1820
|
+
apiEndpoint: target.apiEndpoint,
|
|
1821
|
+
orgName: target.orgName,
|
|
1822
|
+
spaceName: target.spaceName
|
|
1581
1823
|
};
|
|
1582
1824
|
}
|
|
1583
1825
|
async function readExplicitBindings(target, options) {
|
|
@@ -1643,6 +1885,7 @@ function toConnectionTarget(binding, role) {
|
|
|
1643
1885
|
// src/driver/fake.ts
|
|
1644
1886
|
import { appendFile } from "fs/promises";
|
|
1645
1887
|
var catalogFailureInjected = false;
|
|
1888
|
+
var connectFailureInjectedOnce = false;
|
|
1646
1889
|
function catalogObjects() {
|
|
1647
1890
|
return [
|
|
1648
1891
|
{ SCHEMA_NAME: "APP_SCHEMA", OBJECT_NAME: "EXISTING_TABLE", OBJECT_TYPE: "TABLE" },
|
|
@@ -1851,10 +2094,28 @@ var FakeConnection = class {
|
|
|
1851
2094
|
return this.closed;
|
|
1852
2095
|
}
|
|
1853
2096
|
};
|
|
2097
|
+
function maybeFailConnect() {
|
|
2098
|
+
const mode = readEnv(envName("FAKE_FAIL_CONNECT"));
|
|
2099
|
+
if (mode !== "once" && mode !== "always") {
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
if (mode === "once" && connectFailureInjectedOnce) {
|
|
2103
|
+
return;
|
|
2104
|
+
}
|
|
2105
|
+
connectFailureInjectedOnce = true;
|
|
2106
|
+
const cause = Object.assign(
|
|
2107
|
+
new Error("Could not connect to any host: [ fake-host:443 - forced fixture failure ]"),
|
|
2108
|
+
{ code: "EHDBOPENCONN" }
|
|
2109
|
+
);
|
|
2110
|
+
throw new CfHanaError("CONNECTION", `Failed to connect to HANA: ${cause.message}`, { cause });
|
|
2111
|
+
}
|
|
1854
2112
|
function createFakeDriver() {
|
|
1855
2113
|
return {
|
|
1856
2114
|
name: "fake",
|
|
1857
|
-
connect: (_params) =>
|
|
2115
|
+
connect: (_params) => {
|
|
2116
|
+
maybeFailConnect();
|
|
2117
|
+
return Promise.resolve(new FakeConnection());
|
|
2118
|
+
}
|
|
1858
2119
|
};
|
|
1859
2120
|
}
|
|
1860
2121
|
|
|
@@ -2079,7 +2340,15 @@ async function connectHdb(params) {
|
|
|
2079
2340
|
user: params.user,
|
|
2080
2341
|
password: params.password,
|
|
2081
2342
|
ca: params.certificate,
|
|
2082
|
-
useTLS: true
|
|
2343
|
+
useTLS: true,
|
|
2344
|
+
// HANA Cloud can reactively redirect a connection mid-auth to an
|
|
2345
|
+
// internal per-node hostname (pod-locality routing). That target is
|
|
2346
|
+
// often unreachable outside SAP's own network, and for a tunneled
|
|
2347
|
+
// connection it would silently abandon the SSH forward for a fresh,
|
|
2348
|
+
// untunneled socket. The original bound host is always a real, working
|
|
2349
|
+
// endpoint, so disabling the redirect is safe on the direct path too.
|
|
2350
|
+
disableCloudRedirect: true,
|
|
2351
|
+
...params.servername === void 0 ? {} : { servername: params.servername }
|
|
2083
2352
|
});
|
|
2084
2353
|
await openClient(client, params.connectTimeoutMs);
|
|
2085
2354
|
try {
|
|
@@ -2188,83 +2457,6 @@ async function appendSqlHistory(input, options = {}) {
|
|
|
2188
2457
|
// src/safety.ts
|
|
2189
2458
|
var DESTRUCTIVE_DDL_KEYWORDS = /* @__PURE__ */ new Set(["DROP", "TRUNCATE", "ALTER"]);
|
|
2190
2459
|
var UNSCOPED_WRITE_KEYWORDS = /* @__PURE__ */ new Set(["UPDATE", "DELETE"]);
|
|
2191
|
-
function skipQuotedText2(sql, start) {
|
|
2192
|
-
const quote = sql[start];
|
|
2193
|
-
let index = start + 1;
|
|
2194
|
-
while (index < sql.length) {
|
|
2195
|
-
if (sql[index] === quote) {
|
|
2196
|
-
if (sql[index + 1] === quote) {
|
|
2197
|
-
index += 2;
|
|
2198
|
-
continue;
|
|
2199
|
-
}
|
|
2200
|
-
index += 1;
|
|
2201
|
-
break;
|
|
2202
|
-
}
|
|
2203
|
-
index += 1;
|
|
2204
|
-
}
|
|
2205
|
-
return index;
|
|
2206
|
-
}
|
|
2207
|
-
function skipLineComment2(sql, start) {
|
|
2208
|
-
let index = start + 2;
|
|
2209
|
-
while (index < sql.length && sql[index] !== "\n") {
|
|
2210
|
-
index += 1;
|
|
2211
|
-
}
|
|
2212
|
-
return index;
|
|
2213
|
-
}
|
|
2214
|
-
function skipBlockComment2(sql, start) {
|
|
2215
|
-
let index = start + 2;
|
|
2216
|
-
while (index < sql.length && !(sql[index] === "*" && sql[index + 1] === "/")) {
|
|
2217
|
-
index += 1;
|
|
2218
|
-
}
|
|
2219
|
-
return Math.min(index + 2, sql.length);
|
|
2220
|
-
}
|
|
2221
|
-
function maskIgnoredSqlText(sql) {
|
|
2222
|
-
let masked = "";
|
|
2223
|
-
let index = 0;
|
|
2224
|
-
while (index < sql.length) {
|
|
2225
|
-
const char = sql[index];
|
|
2226
|
-
if (char === "'" || char === '"') {
|
|
2227
|
-
const end = skipQuotedText2(sql, index);
|
|
2228
|
-
masked += " ".repeat(end - index);
|
|
2229
|
-
index = end;
|
|
2230
|
-
continue;
|
|
2231
|
-
}
|
|
2232
|
-
if (char === "-" && sql[index + 1] === "-") {
|
|
2233
|
-
const end = skipLineComment2(sql, index);
|
|
2234
|
-
masked += " ".repeat(end - index);
|
|
2235
|
-
index = end;
|
|
2236
|
-
continue;
|
|
2237
|
-
}
|
|
2238
|
-
if (char === "/" && sql[index + 1] === "*") {
|
|
2239
|
-
const end = skipBlockComment2(sql, index);
|
|
2240
|
-
masked += " ".repeat(end - index);
|
|
2241
|
-
index = end;
|
|
2242
|
-
continue;
|
|
2243
|
-
}
|
|
2244
|
-
masked += char ?? "";
|
|
2245
|
-
index += 1;
|
|
2246
|
-
}
|
|
2247
|
-
return masked;
|
|
2248
|
-
}
|
|
2249
|
-
function hasTopLevelKeyword(sql, keyword) {
|
|
2250
|
-
const masked = maskIgnoredSqlText(sql);
|
|
2251
|
-
let depth = 0;
|
|
2252
|
-
for (let index = 0; index < masked.length; index += 1) {
|
|
2253
|
-
const char = masked[index];
|
|
2254
|
-
if (char === "(") {
|
|
2255
|
-
depth += 1;
|
|
2256
|
-
continue;
|
|
2257
|
-
}
|
|
2258
|
-
if (char === ")") {
|
|
2259
|
-
depth = Math.max(0, depth - 1);
|
|
2260
|
-
continue;
|
|
2261
|
-
}
|
|
2262
|
-
if (depth === 0 && masked.slice(index, index + keyword.length).toUpperCase() === keyword && !/[A-Za-z0-9_$#]/.test(masked.charAt(index - 1)) && !/[A-Za-z0-9_$#]/.test(masked.charAt(index + keyword.length))) {
|
|
2263
|
-
return true;
|
|
2264
|
-
}
|
|
2265
|
-
}
|
|
2266
|
-
return false;
|
|
2267
|
-
}
|
|
2268
2460
|
function isUnconditionalMergeDelete(sql) {
|
|
2269
2461
|
return /\bWHEN\s+MATCHED\s+THEN\s+DELETE\b/i.test(maskIgnoredSqlText(sql));
|
|
2270
2462
|
}
|
|
@@ -2307,17 +2499,23 @@ function appendLimit(sql, limit) {
|
|
|
2307
2499
|
}
|
|
2308
2500
|
function inspectStatement(sql) {
|
|
2309
2501
|
const kind = classifyStatement(sql);
|
|
2310
|
-
const
|
|
2502
|
+
const leading = firstKeyword(sql);
|
|
2503
|
+
const withResolved = leading === "WITH" ? resolveWithStatement(sql) : void 0;
|
|
2504
|
+
const keyword = withResolved?.keyword ?? leading;
|
|
2505
|
+
const tail = withResolved === void 0 ? sql : sql.slice(withResolved.index);
|
|
2311
2506
|
if (kind === "ddl") {
|
|
2312
2507
|
return { kind, destructive: DESTRUCTIVE_DDL_KEYWORDS.has(keyword) };
|
|
2313
2508
|
}
|
|
2314
2509
|
if (kind === "dml") {
|
|
2315
|
-
const destructive = UNSCOPED_WRITE_KEYWORDS.has(keyword) && !hasTopLevelKeyword(
|
|
2510
|
+
const destructive = UNSCOPED_WRITE_KEYWORDS.has(keyword) && !hasTopLevelKeyword(tail, "WHERE") || keyword === "MERGE" && isUnconditionalMergeDelete(tail) || keyword === "REPLACE" && isMalformedReplace(tail);
|
|
2316
2511
|
return {
|
|
2317
2512
|
kind,
|
|
2318
2513
|
destructive
|
|
2319
2514
|
};
|
|
2320
2515
|
}
|
|
2516
|
+
if (kind === "unknown") {
|
|
2517
|
+
return { kind, destructive: keyword === "CALL" };
|
|
2518
|
+
}
|
|
2321
2519
|
return { kind, destructive: false };
|
|
2322
2520
|
}
|
|
2323
2521
|
function evaluateGuard(sql, config) {
|
|
@@ -2360,6 +2558,707 @@ function applyAutoLimit(sql, limit) {
|
|
|
2360
2558
|
};
|
|
2361
2559
|
}
|
|
2362
2560
|
|
|
2561
|
+
// src/tunnel/cache.ts
|
|
2562
|
+
import { createHash } from "crypto";
|
|
2563
|
+
import { mkdir as mkdir3, readdir as readdir2, readFile as readFile2, rename, rm as rm4, writeFile as writeFile2 } from "fs/promises";
|
|
2564
|
+
import { homedir as homedir3 } from "os";
|
|
2565
|
+
import { join as join5 } from "path";
|
|
2566
|
+
|
|
2567
|
+
// src/tunnel/process.ts
|
|
2568
|
+
import { spawn } from "child_process";
|
|
2569
|
+
import { mkdtemp as mkdtemp2, open, readFile, rm as rm3 } from "fs/promises";
|
|
2570
|
+
import { connect as netConnect, createServer } from "net";
|
|
2571
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
2572
|
+
import { join as join4 } from "path";
|
|
2573
|
+
var PORT_POLL_INTERVAL_MS = 100;
|
|
2574
|
+
var DEFAULT_PORT_PROBE_TIMEOUT_MS = 500;
|
|
2575
|
+
function assertPositiveSafeInteger(label, value) {
|
|
2576
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
2577
|
+
throw new CfHanaError("CONFIG", `${label} must be a positive integer`);
|
|
2578
|
+
}
|
|
2579
|
+
}
|
|
2580
|
+
async function allocateLocalPort() {
|
|
2581
|
+
return await new Promise((resolve, reject) => {
|
|
2582
|
+
const server = createServer();
|
|
2583
|
+
server.once("error", reject);
|
|
2584
|
+
server.listen(0, "127.0.0.1", () => {
|
|
2585
|
+
const address = server.address();
|
|
2586
|
+
server.close(() => {
|
|
2587
|
+
if (address === null || typeof address === "string") {
|
|
2588
|
+
reject(new CfHanaError("CONNECTION", "Could not allocate a local port for a tunnel"));
|
|
2589
|
+
return;
|
|
2590
|
+
}
|
|
2591
|
+
resolve(address.port);
|
|
2592
|
+
});
|
|
2593
|
+
});
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
function probeLocalPort(port, timeoutMs = DEFAULT_PORT_PROBE_TIMEOUT_MS) {
|
|
2597
|
+
return new Promise((resolve) => {
|
|
2598
|
+
let settled = false;
|
|
2599
|
+
const socket = netConnect({ host: "127.0.0.1", port });
|
|
2600
|
+
const finish = (result) => {
|
|
2601
|
+
if (settled) {
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
settled = true;
|
|
2605
|
+
clearTimeout(timer);
|
|
2606
|
+
socket.destroy();
|
|
2607
|
+
resolve(result);
|
|
2608
|
+
};
|
|
2609
|
+
const timer = setTimeout(() => {
|
|
2610
|
+
finish(false);
|
|
2611
|
+
}, timeoutMs);
|
|
2612
|
+
timer.unref();
|
|
2613
|
+
socket.once("connect", () => {
|
|
2614
|
+
finish(true);
|
|
2615
|
+
});
|
|
2616
|
+
socket.once("error", () => {
|
|
2617
|
+
finish(false);
|
|
2618
|
+
});
|
|
2619
|
+
});
|
|
2620
|
+
}
|
|
2621
|
+
function killTunnelProcess(pid) {
|
|
2622
|
+
if (pid === void 0) {
|
|
2623
|
+
return;
|
|
2624
|
+
}
|
|
2625
|
+
try {
|
|
2626
|
+
process.kill(pid, "SIGTERM");
|
|
2627
|
+
} catch {
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
async function withScopedCfSession(selectorSource, target, sapCredentials, work) {
|
|
2631
|
+
if (selectorSource === "ambient") {
|
|
2632
|
+
return await work();
|
|
2633
|
+
}
|
|
2634
|
+
if (sapCredentials === void 0) {
|
|
2635
|
+
throw new CredentialsNotFoundError(
|
|
2636
|
+
"SAP credentials are required to open a tunnel session for an explicit selector"
|
|
2637
|
+
);
|
|
2638
|
+
}
|
|
2639
|
+
const cfHome = await mkdtemp2(join4(tmpdir2(), "saptools-cf-hana-tunnel-"));
|
|
2640
|
+
try {
|
|
2641
|
+
const ctx = { cfHome };
|
|
2642
|
+
await cfApi(target.apiEndpoint, ctx);
|
|
2643
|
+
await cfAuth(sapCredentials.email, sapCredentials.password, ctx);
|
|
2644
|
+
await cfTargetSpace(target.orgName, target.spaceName, ctx);
|
|
2645
|
+
return await work(ctx);
|
|
2646
|
+
} finally {
|
|
2647
|
+
await rm3(cfHome, { recursive: true, force: true });
|
|
2648
|
+
}
|
|
2649
|
+
}
|
|
2650
|
+
function raceTunnelReadiness(child, localPort, boundMs, probePort, killProcess) {
|
|
2651
|
+
return new Promise((resolve) => {
|
|
2652
|
+
let settled = false;
|
|
2653
|
+
const timers = {};
|
|
2654
|
+
const finish = (result) => {
|
|
2655
|
+
if (settled) {
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
settled = true;
|
|
2659
|
+
clearInterval(timers.poll);
|
|
2660
|
+
clearTimeout(timers.deadline);
|
|
2661
|
+
child.removeListener("exit", onAbort);
|
|
2662
|
+
child.removeListener("error", onAbort);
|
|
2663
|
+
if (result === void 0) {
|
|
2664
|
+
killProcess(child.pid);
|
|
2665
|
+
} else {
|
|
2666
|
+
child.unref();
|
|
2667
|
+
}
|
|
2668
|
+
resolve(result);
|
|
2669
|
+
};
|
|
2670
|
+
function onAbort() {
|
|
2671
|
+
finish();
|
|
2672
|
+
}
|
|
2673
|
+
child.on("exit", onAbort);
|
|
2674
|
+
child.on("error", onAbort);
|
|
2675
|
+
timers.poll = setInterval(() => {
|
|
2676
|
+
void probePort(localPort).then((open2) => {
|
|
2677
|
+
if (open2 && child.pid !== void 0) {
|
|
2678
|
+
finish({ localPort, pid: child.pid });
|
|
2679
|
+
}
|
|
2680
|
+
});
|
|
2681
|
+
}, PORT_POLL_INTERVAL_MS);
|
|
2682
|
+
timers.deadline = setTimeout(finish, boundMs);
|
|
2683
|
+
});
|
|
2684
|
+
}
|
|
2685
|
+
var STDERR_TAIL_MAX_CHARS = 4096;
|
|
2686
|
+
async function openStderrCapture() {
|
|
2687
|
+
try {
|
|
2688
|
+
const dir = await mkdtemp2(join4(tmpdir2(), "saptools-cf-hana-tunnel-log-"));
|
|
2689
|
+
const path = join4(dir, "stderr.log");
|
|
2690
|
+
const handle = await open(path, "w");
|
|
2691
|
+
return {
|
|
2692
|
+
path,
|
|
2693
|
+
fd: handle.fd,
|
|
2694
|
+
close: async () => {
|
|
2695
|
+
await handle.close().catch(() => {
|
|
2696
|
+
});
|
|
2697
|
+
await rm3(dir, { recursive: true, force: true });
|
|
2698
|
+
}
|
|
2699
|
+
};
|
|
2700
|
+
} catch {
|
|
2701
|
+
return void 0;
|
|
2702
|
+
}
|
|
2703
|
+
}
|
|
2704
|
+
async function readStderrTail(path) {
|
|
2705
|
+
try {
|
|
2706
|
+
const content = (await readFile(path, "utf8")).trim().replace(/\s+/g, " ");
|
|
2707
|
+
if (content.length === 0) {
|
|
2708
|
+
return void 0;
|
|
2709
|
+
}
|
|
2710
|
+
return content.length > STDERR_TAIL_MAX_CHARS ? content.slice(-STDERR_TAIL_MAX_CHARS) : content;
|
|
2711
|
+
} catch {
|
|
2712
|
+
return void 0;
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
async function spawnTunnel(params, deps = {}) {
|
|
2716
|
+
assertPositiveSafeInteger("tunnel keepalive seconds", params.keepaliveSeconds);
|
|
2717
|
+
const remainingMs = params.deadline - Date.now();
|
|
2718
|
+
if (remainingMs <= 0) {
|
|
2719
|
+
return void 0;
|
|
2720
|
+
}
|
|
2721
|
+
const boundMs = Math.min(remainingMs, params.candidateTimeoutMs);
|
|
2722
|
+
const spawnProcess = deps.spawnProcess ?? spawn;
|
|
2723
|
+
const probePort = deps.probePort ?? probeLocalPort;
|
|
2724
|
+
const allocatePort = deps.allocatePort ?? allocateLocalPort;
|
|
2725
|
+
const killProcess = deps.killProcess ?? killTunnelProcess;
|
|
2726
|
+
const localPort = await allocatePort();
|
|
2727
|
+
const { bin, argsPrefix } = resolveCfBin();
|
|
2728
|
+
const forward = `${String(localPort)}:${params.hanaHost}:${String(params.hanaPort)}`;
|
|
2729
|
+
const args = [
|
|
2730
|
+
...argsPrefix,
|
|
2731
|
+
"ssh",
|
|
2732
|
+
params.app,
|
|
2733
|
+
"-L",
|
|
2734
|
+
forward,
|
|
2735
|
+
"-c",
|
|
2736
|
+
`sleep ${String(params.keepaliveSeconds)}`
|
|
2737
|
+
];
|
|
2738
|
+
const env = params.cfHome === void 0 ? { ...process.env } : { ...process.env, CF_HOME: params.cfHome };
|
|
2739
|
+
const openCapture = deps.openStderrCapture ?? openStderrCapture;
|
|
2740
|
+
const capture = await openCapture();
|
|
2741
|
+
const stdio = capture === void 0 ? "ignore" : ["ignore", "ignore", capture.fd];
|
|
2742
|
+
const child = spawnProcess(bin, args, { detached: true, stdio, env });
|
|
2743
|
+
const result = await raceTunnelReadiness(child, localPort, boundMs, probePort, killProcess);
|
|
2744
|
+
if (capture !== void 0) {
|
|
2745
|
+
if (result === void 0) {
|
|
2746
|
+
try {
|
|
2747
|
+
const tail = await readStderrTail(capture.path);
|
|
2748
|
+
if (tail !== void 0) {
|
|
2749
|
+
deps.onFailureDiagnostic?.(tail);
|
|
2750
|
+
}
|
|
2751
|
+
} catch {
|
|
2752
|
+
}
|
|
2753
|
+
}
|
|
2754
|
+
await capture.close();
|
|
2755
|
+
}
|
|
2756
|
+
return result;
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
// src/tunnel/cache.ts
|
|
2760
|
+
var DEFAULT_ESTABLISHING_STALE_MS = 2 * DEFAULT_TUNNEL_FALLBACK_BUDGET_MS;
|
|
2761
|
+
var DEFAULT_POLL_INTERVAL_MS = 300;
|
|
2762
|
+
function isRecord2(value) {
|
|
2763
|
+
return typeof value === "object" && value !== null;
|
|
2764
|
+
}
|
|
2765
|
+
function defaultIsProcessAlive(pid) {
|
|
2766
|
+
try {
|
|
2767
|
+
process.kill(pid, 0);
|
|
2768
|
+
return true;
|
|
2769
|
+
} catch (error) {
|
|
2770
|
+
return isRecord2(error) && error["code"] !== "ESRCH";
|
|
2771
|
+
}
|
|
2772
|
+
}
|
|
2773
|
+
function sleep(ms) {
|
|
2774
|
+
return new Promise((resolve) => {
|
|
2775
|
+
setTimeout(resolve, ms);
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
function tunnelCacheRoot(saptoolsRoot) {
|
|
2779
|
+
return join5(saptoolsRoot ?? join5(homedir3(), ".saptools"), "cf-hana", "tunnel");
|
|
2780
|
+
}
|
|
2781
|
+
function tunnelCacheKey(host) {
|
|
2782
|
+
return createHash("sha256").update(host).digest("hex");
|
|
2783
|
+
}
|
|
2784
|
+
function tunnelCachePath(host, saptoolsRoot) {
|
|
2785
|
+
return join5(tunnelCacheRoot(saptoolsRoot), `${tunnelCacheKey(host)}.json`);
|
|
2786
|
+
}
|
|
2787
|
+
function isOrgKey(value) {
|
|
2788
|
+
return isRecord2(value) && typeof value["apiEndpoint"] === "string" && typeof value["orgName"] === "string";
|
|
2789
|
+
}
|
|
2790
|
+
function hasEstablishingFields(value) {
|
|
2791
|
+
return value["status"] === "establishing" && typeof value["host"] === "string" && typeof value["startedAt"] === "string" && typeof value["ownerPid"] === "number" && isOrgKey(value["orgKey"]);
|
|
2792
|
+
}
|
|
2793
|
+
function hasReadyFields(value) {
|
|
2794
|
+
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"]);
|
|
2795
|
+
}
|
|
2796
|
+
function isTunnelCacheRecord(value) {
|
|
2797
|
+
return isRecord2(value) && value["version"] === 1 && (hasEstablishingFields(value) || hasReadyFields(value));
|
|
2798
|
+
}
|
|
2799
|
+
async function parseTunnelCacheFile(path) {
|
|
2800
|
+
try {
|
|
2801
|
+
const raw = JSON.parse(await readFile2(path, "utf8"));
|
|
2802
|
+
return isTunnelCacheRecord(raw) ? raw : void 0;
|
|
2803
|
+
} catch {
|
|
2804
|
+
return void 0;
|
|
2805
|
+
}
|
|
2806
|
+
}
|
|
2807
|
+
async function readTunnelCacheEntry(host, options = {}) {
|
|
2808
|
+
const entry = await parseTunnelCacheFile(tunnelCachePath(host, options.saptoolsRoot));
|
|
2809
|
+
return entry?.host === host ? entry : void 0;
|
|
2810
|
+
}
|
|
2811
|
+
function isExpired(entry, now) {
|
|
2812
|
+
const expiresAt = Date.parse(entry.expiresAt);
|
|
2813
|
+
return !Number.isFinite(expiresAt) || now.getTime() >= expiresAt;
|
|
2814
|
+
}
|
|
2815
|
+
async function isTunnelUsable(entry, options = {}) {
|
|
2816
|
+
const isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
2817
|
+
if (!isAlive(entry.pid)) {
|
|
2818
|
+
return false;
|
|
2819
|
+
}
|
|
2820
|
+
if (isExpired(entry, options.now?.() ?? /* @__PURE__ */ new Date())) {
|
|
2821
|
+
return false;
|
|
2822
|
+
}
|
|
2823
|
+
const probePort = options.probePort ?? probeLocalPort;
|
|
2824
|
+
return await probePort(entry.localPort);
|
|
2825
|
+
}
|
|
2826
|
+
function isEstablishingStale(entry, options) {
|
|
2827
|
+
const isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
2828
|
+
if (!isAlive(entry.ownerPid)) {
|
|
2829
|
+
return true;
|
|
2830
|
+
}
|
|
2831
|
+
const startedAt = Date.parse(entry.startedAt);
|
|
2832
|
+
if (!Number.isFinite(startedAt)) {
|
|
2833
|
+
return true;
|
|
2834
|
+
}
|
|
2835
|
+
const now = options.now?.() ?? /* @__PURE__ */ new Date();
|
|
2836
|
+
const staleAfterMs = options.staleAfterMs ?? DEFAULT_ESTABLISHING_STALE_MS;
|
|
2837
|
+
return now.getTime() - startedAt >= staleAfterMs;
|
|
2838
|
+
}
|
|
2839
|
+
async function writeEstablishingMarker(host, ownerPid, orgKey, options) {
|
|
2840
|
+
const root = tunnelCacheRoot(options.saptoolsRoot);
|
|
2841
|
+
await mkdir3(root, { recursive: true, mode: 448 });
|
|
2842
|
+
const record = {
|
|
2843
|
+
version: 1,
|
|
2844
|
+
status: "establishing",
|
|
2845
|
+
host,
|
|
2846
|
+
startedAt: (options.now?.() ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
2847
|
+
ownerPid,
|
|
2848
|
+
orgKey
|
|
2849
|
+
};
|
|
2850
|
+
try {
|
|
2851
|
+
await writeFile2(tunnelCachePath(host, options.saptoolsRoot), `${JSON.stringify(record)}
|
|
2852
|
+
`, {
|
|
2853
|
+
encoding: "utf8",
|
|
2854
|
+
mode: 384,
|
|
2855
|
+
flag: "wx"
|
|
2856
|
+
});
|
|
2857
|
+
return true;
|
|
2858
|
+
} catch (error) {
|
|
2859
|
+
if (isRecord2(error) && error["code"] === "EEXIST") {
|
|
2860
|
+
return false;
|
|
2861
|
+
}
|
|
2862
|
+
throw error;
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
async function claimEstablishing(host, ownerPid, orgKey, options = {}) {
|
|
2866
|
+
if (await writeEstablishingMarker(host, ownerPid, orgKey, options)) {
|
|
2867
|
+
return { outcome: "claimed" };
|
|
2868
|
+
}
|
|
2869
|
+
const existing = await readTunnelCacheEntry(host, options);
|
|
2870
|
+
if (existing === void 0) {
|
|
2871
|
+
return await writeEstablishingMarker(host, ownerPid, orgKey, options) ? { outcome: "claimed" } : { outcome: "wait" };
|
|
2872
|
+
}
|
|
2873
|
+
if (existing.status === "ready") {
|
|
2874
|
+
return { outcome: "already-ready", record: existing };
|
|
2875
|
+
}
|
|
2876
|
+
if (!isEstablishingStale(existing, options)) {
|
|
2877
|
+
return { outcome: "wait" };
|
|
2878
|
+
}
|
|
2879
|
+
await rm4(tunnelCachePath(host, options.saptoolsRoot), { force: true });
|
|
2880
|
+
return await writeEstablishingMarker(host, ownerPid, orgKey, options) ? { outcome: "claimed" } : { outcome: "wait" };
|
|
2881
|
+
}
|
|
2882
|
+
async function waitForEstablishment(host, deadline, pollIntervalMs = DEFAULT_POLL_INTERVAL_MS, options = {}) {
|
|
2883
|
+
for (; ; ) {
|
|
2884
|
+
const entry = await readTunnelCacheEntry(host, options);
|
|
2885
|
+
if (entry === void 0) {
|
|
2886
|
+
return void 0;
|
|
2887
|
+
}
|
|
2888
|
+
if (entry.status === "ready") {
|
|
2889
|
+
return entry;
|
|
2890
|
+
}
|
|
2891
|
+
const remainingMs = deadline - Date.now();
|
|
2892
|
+
if (remainingMs <= 0) {
|
|
2893
|
+
return void 0;
|
|
2894
|
+
}
|
|
2895
|
+
await sleep(Math.min(pollIntervalMs, remainingMs));
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
async function finalizeEstablishingReady(host, record, options = {}) {
|
|
2899
|
+
const root = tunnelCacheRoot(options.saptoolsRoot);
|
|
2900
|
+
const path = tunnelCachePath(host, options.saptoolsRoot);
|
|
2901
|
+
const tempPath = `${path}.tmp-${String(process.pid)}`;
|
|
2902
|
+
const stored = { version: 1, status: "ready", host, ...record };
|
|
2903
|
+
await mkdir3(root, { recursive: true, mode: 448 });
|
|
2904
|
+
await rm4(tempPath, { force: true });
|
|
2905
|
+
await writeFile2(tempPath, `${JSON.stringify(stored)}
|
|
2906
|
+
`, { encoding: "utf8", mode: 384 });
|
|
2907
|
+
await rename(tempPath, path);
|
|
2908
|
+
}
|
|
2909
|
+
async function finalizeEstablishingFailed(host, options = {}) {
|
|
2910
|
+
await rm4(tunnelCachePath(host, options.saptoolsRoot), { force: true });
|
|
2911
|
+
}
|
|
2912
|
+
async function evictTunnelCache(host, options = {}) {
|
|
2913
|
+
await rm4(tunnelCachePath(host, options.saptoolsRoot), { force: true });
|
|
2914
|
+
}
|
|
2915
|
+
function sameOrg(a, b) {
|
|
2916
|
+
return a.apiEndpoint === b.apiEndpoint && a.orgName === b.orgName;
|
|
2917
|
+
}
|
|
2918
|
+
async function reapOneFile(path, currentOrgKey, options) {
|
|
2919
|
+
const entry = await parseTunnelCacheFile(path);
|
|
2920
|
+
if (entry === void 0) {
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
const killProcess = options.killProcess ?? killTunnelProcess;
|
|
2924
|
+
if (entry.status === "establishing") {
|
|
2925
|
+
if (isEstablishingStale(entry, options)) {
|
|
2926
|
+
await rm4(path, { force: true });
|
|
2927
|
+
}
|
|
2928
|
+
return;
|
|
2929
|
+
}
|
|
2930
|
+
const isAlive = options.isProcessAlive ?? defaultIsProcessAlive;
|
|
2931
|
+
if (!isAlive(entry.pid)) {
|
|
2932
|
+
await rm4(path, { force: true });
|
|
2933
|
+
return;
|
|
2934
|
+
}
|
|
2935
|
+
const now = options.now?.() ?? /* @__PURE__ */ new Date();
|
|
2936
|
+
if (isExpired(entry, now) || !sameOrg(entry.orgKey, currentOrgKey)) {
|
|
2937
|
+
killProcess(entry.pid);
|
|
2938
|
+
await rm4(path, { force: true });
|
|
2939
|
+
}
|
|
2940
|
+
}
|
|
2941
|
+
async function reapStaleAndCrossOrgTunnels(currentOrgKey, options = {}) {
|
|
2942
|
+
const root = tunnelCacheRoot(options.saptoolsRoot);
|
|
2943
|
+
let files;
|
|
2944
|
+
try {
|
|
2945
|
+
files = await readdir2(root);
|
|
2946
|
+
} catch {
|
|
2947
|
+
return;
|
|
2948
|
+
}
|
|
2949
|
+
await Promise.all(
|
|
2950
|
+
files.filter((file) => file.endsWith(".json")).map(async (file) => {
|
|
2951
|
+
await reapOneFile(join5(root, file), currentOrgKey, options);
|
|
2952
|
+
})
|
|
2953
|
+
);
|
|
2954
|
+
}
|
|
2955
|
+
|
|
2956
|
+
// src/tunnel/candidates.ts
|
|
2957
|
+
var RUNNING_STATE = "started";
|
|
2958
|
+
var VALID_APP_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
2959
|
+
function isValidAppName(name) {
|
|
2960
|
+
return VALID_APP_NAME.test(name);
|
|
2961
|
+
}
|
|
2962
|
+
function discoveredAppNames(stdout, targetAppName) {
|
|
2963
|
+
if (stdout === void 0) {
|
|
2964
|
+
return [];
|
|
2965
|
+
}
|
|
2966
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2967
|
+
const names = [];
|
|
2968
|
+
for (const row of parseCfAppsOutput(stdout)) {
|
|
2969
|
+
if (row.state !== RUNNING_STATE) {
|
|
2970
|
+
continue;
|
|
2971
|
+
}
|
|
2972
|
+
if (row.name === targetAppName || seen.has(row.name)) {
|
|
2973
|
+
continue;
|
|
2974
|
+
}
|
|
2975
|
+
if (!isValidAppName(row.name)) {
|
|
2976
|
+
continue;
|
|
2977
|
+
}
|
|
2978
|
+
seen.add(row.name);
|
|
2979
|
+
names.push(row.name);
|
|
2980
|
+
}
|
|
2981
|
+
return names;
|
|
2982
|
+
}
|
|
2983
|
+
function buildCandidateList(targetAppName, discoveredStdout, maxCandidates) {
|
|
2984
|
+
const discovered = discoveredAppNames(discoveredStdout, targetAppName);
|
|
2985
|
+
return [targetAppName, ...discovered.slice(0, maxCandidates)];
|
|
2986
|
+
}
|
|
2987
|
+
|
|
2988
|
+
// src/tunnel/classifier.ts
|
|
2989
|
+
var CONNECT_TIMEOUT_MESSAGE = "HANA connection timed out";
|
|
2990
|
+
var OPEN_CONNECTION_CODE = "EHDBOPENCONN";
|
|
2991
|
+
var MAX_CAUSE_DEPTH = 5;
|
|
2992
|
+
function causeCode(value) {
|
|
2993
|
+
return typeof value === "object" && value !== null ? value.code : void 0;
|
|
2994
|
+
}
|
|
2995
|
+
function causeOf(value) {
|
|
2996
|
+
return typeof value === "object" && value !== null ? value.cause : void 0;
|
|
2997
|
+
}
|
|
2998
|
+
function hasOpenConnectionCause(cause, depth) {
|
|
2999
|
+
if (depth > MAX_CAUSE_DEPTH) {
|
|
3000
|
+
return false;
|
|
3001
|
+
}
|
|
3002
|
+
if (causeCode(cause) === OPEN_CONNECTION_CODE) {
|
|
3003
|
+
return true;
|
|
3004
|
+
}
|
|
3005
|
+
return hasOpenConnectionCause(causeOf(cause), depth + 1);
|
|
3006
|
+
}
|
|
3007
|
+
function isConnectivityFailure(error) {
|
|
3008
|
+
if (!(error instanceof CfHanaError)) {
|
|
3009
|
+
return false;
|
|
3010
|
+
}
|
|
3011
|
+
if (error.code === "TIMEOUT") {
|
|
3012
|
+
return error.message.includes(CONNECT_TIMEOUT_MESSAGE);
|
|
3013
|
+
}
|
|
3014
|
+
if (error.code !== "CONNECTION") {
|
|
3015
|
+
return false;
|
|
3016
|
+
}
|
|
3017
|
+
return hasOpenConnectionCause(error.cause, 0);
|
|
3018
|
+
}
|
|
3019
|
+
function isUnattributedQueryFailure(error) {
|
|
3020
|
+
return error instanceof QueryError && error.databaseCode === void 0 && error.sqlState === void 0;
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// src/tunnel/fallback.ts
|
|
3024
|
+
var EXPIRY_SAFETY_MARGIN_MS = 3e4;
|
|
3025
|
+
function directParamsOf(config) {
|
|
3026
|
+
return {
|
|
3027
|
+
host: config.host,
|
|
3028
|
+
port: config.port,
|
|
3029
|
+
user: config.user,
|
|
3030
|
+
password: config.password,
|
|
3031
|
+
schema: config.schema,
|
|
3032
|
+
certificate: config.certificate,
|
|
3033
|
+
connectTimeoutMs: config.connectTimeoutMs
|
|
3034
|
+
};
|
|
3035
|
+
}
|
|
3036
|
+
function tunneledParams(directParams, localPort) {
|
|
3037
|
+
return { ...directParams, host: "127.0.0.1", port: localPort, servername: directParams.host };
|
|
3038
|
+
}
|
|
3039
|
+
function orgKeyOf(config) {
|
|
3040
|
+
return { apiEndpoint: config.apiEndpoint, orgName: config.orgName };
|
|
3041
|
+
}
|
|
3042
|
+
function tunnelExpiryIso() {
|
|
3043
|
+
return new Date(
|
|
3044
|
+
Date.now() + DEFAULT_TUNNEL_KEEPALIVE_SECONDS * 1e3 - EXPIRY_SAFETY_MARGIN_MS
|
|
3045
|
+
).toISOString();
|
|
3046
|
+
}
|
|
3047
|
+
async function discardQuietly(work) {
|
|
3048
|
+
await work.catch(() => {
|
|
3049
|
+
});
|
|
3050
|
+
}
|
|
3051
|
+
function shouldDiscardTunnel(error) {
|
|
3052
|
+
return isConnectivityFailure(error) || isUnattributedQueryFailure(error);
|
|
3053
|
+
}
|
|
3054
|
+
async function tryReadyRecord(record, driver, config, directParams, cacheOptions) {
|
|
3055
|
+
try {
|
|
3056
|
+
const connection = await driver.connect(tunneledParams(directParams, record.localPort));
|
|
3057
|
+
config.onTunnelStatus?.(`connected via SSH tunnel through ${record.app}`);
|
|
3058
|
+
return connection;
|
|
3059
|
+
} catch (error) {
|
|
3060
|
+
if (shouldDiscardTunnel(error)) {
|
|
3061
|
+
await discardQuietly(evictTunnelCache(config.host, cacheOptions));
|
|
3062
|
+
return void 0;
|
|
3063
|
+
}
|
|
3064
|
+
throw error;
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
function knownCandidates(targetAppName, hintApp) {
|
|
3068
|
+
return hintApp === void 0 || hintApp === targetAppName ? [targetAppName] : [targetAppName, hintApp];
|
|
3069
|
+
}
|
|
3070
|
+
async function discoverAppsStdout(ctx, timeoutMs) {
|
|
3071
|
+
try {
|
|
3072
|
+
return ctx === void 0 ? await cfAppsDirect(timeoutMs) : await cfApps(ctx, timeoutMs);
|
|
3073
|
+
} catch {
|
|
3074
|
+
return;
|
|
3075
|
+
}
|
|
3076
|
+
}
|
|
3077
|
+
async function discoverExtraCandidates(config, ctx, alreadyTried, deadline) {
|
|
3078
|
+
const remainingMs = deadline - Date.now();
|
|
3079
|
+
if (remainingMs <= 0) {
|
|
3080
|
+
return [];
|
|
3081
|
+
}
|
|
3082
|
+
const stdout = await discoverAppsStdout(ctx, remainingMs);
|
|
3083
|
+
const base = buildCandidateList(config.appName, stdout, DEFAULT_TUNNEL_MAX_CANDIDATES);
|
|
3084
|
+
return base.filter((app) => !alreadyTried.includes(app));
|
|
3085
|
+
}
|
|
3086
|
+
async function finalizeCandidateConnection(app, spawned, driver, config, directParams, cacheOptions) {
|
|
3087
|
+
const readyRecord = {
|
|
3088
|
+
localPort: spawned.localPort,
|
|
3089
|
+
pid: spawned.pid,
|
|
3090
|
+
app,
|
|
3091
|
+
orgKey: orgKeyOf(config),
|
|
3092
|
+
expiresAt: tunnelExpiryIso()
|
|
3093
|
+
};
|
|
3094
|
+
try {
|
|
3095
|
+
const connection = await driver.connect(tunneledParams(directParams, spawned.localPort));
|
|
3096
|
+
await discardQuietly(finalizeEstablishingReady(config.host, readyRecord, cacheOptions));
|
|
3097
|
+
config.onTunnelStatus?.(`connected via SSH tunnel through ${app}`);
|
|
3098
|
+
return connection;
|
|
3099
|
+
} catch (error) {
|
|
3100
|
+
if (shouldDiscardTunnel(error)) {
|
|
3101
|
+
await discardQuietly(finalizeEstablishingFailed(config.host, cacheOptions));
|
|
3102
|
+
killTunnelProcess(spawned.pid);
|
|
3103
|
+
return void 0;
|
|
3104
|
+
}
|
|
3105
|
+
await discardQuietly(finalizeEstablishingReady(config.host, readyRecord, cacheOptions));
|
|
3106
|
+
throw error;
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
async function tryCandidate(app, ctx, driver, config, directParams, deadline, overrides) {
|
|
3110
|
+
const cacheOptions = overrides.cache ?? {};
|
|
3111
|
+
config.onTunnelStatus?.(`trying to reach ${config.host} via app ${app}...`);
|
|
3112
|
+
const claim = await claimEstablishing(config.host, process.pid, orgKeyOf(config), cacheOptions);
|
|
3113
|
+
if (claim.outcome === "already-ready") {
|
|
3114
|
+
return await tryReadyRecord(claim.record, driver, config, directParams, cacheOptions);
|
|
3115
|
+
}
|
|
3116
|
+
if (claim.outcome === "wait") {
|
|
3117
|
+
const waitDeadline = Math.min(deadline, Date.now() + DEFAULT_TUNNEL_CANDIDATE_TIMEOUT_MS);
|
|
3118
|
+
const ready = await waitForEstablishment(config.host, waitDeadline, void 0, cacheOptions);
|
|
3119
|
+
return ready === void 0 ? void 0 : await tryReadyRecord(ready, driver, config, directParams, cacheOptions);
|
|
3120
|
+
}
|
|
3121
|
+
const spawned = await spawnTunnel(
|
|
3122
|
+
{
|
|
3123
|
+
cfHome: ctx?.cfHome,
|
|
3124
|
+
app,
|
|
3125
|
+
hanaHost: config.host,
|
|
3126
|
+
hanaPort: config.port,
|
|
3127
|
+
keepaliveSeconds: DEFAULT_TUNNEL_KEEPALIVE_SECONDS,
|
|
3128
|
+
deadline,
|
|
3129
|
+
candidateTimeoutMs: DEFAULT_TUNNEL_CANDIDATE_TIMEOUT_MS
|
|
3130
|
+
},
|
|
3131
|
+
{
|
|
3132
|
+
...overrides.process,
|
|
3133
|
+
onFailureDiagnostic: (message) => {
|
|
3134
|
+
config.onTunnelStatus?.(`tunnel via ${app} failed: ${message}`);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
);
|
|
3138
|
+
if (spawned === void 0) {
|
|
3139
|
+
await discardQuietly(finalizeEstablishingFailed(config.host, cacheOptions));
|
|
3140
|
+
return void 0;
|
|
3141
|
+
}
|
|
3142
|
+
return await finalizeCandidateConnection(app, spawned, driver, config, directParams, cacheOptions);
|
|
3143
|
+
}
|
|
3144
|
+
async function tryCachedTunnel(driver, config, directParams, cacheOptions) {
|
|
3145
|
+
if (config.refreshTunnel) {
|
|
3146
|
+
const superseded = await readTunnelCacheEntry(config.host, cacheOptions);
|
|
3147
|
+
await discardQuietly(evictTunnelCache(config.host, cacheOptions));
|
|
3148
|
+
if (superseded?.status === "ready") {
|
|
3149
|
+
(cacheOptions.killProcess ?? killTunnelProcess)(superseded.pid);
|
|
3150
|
+
}
|
|
3151
|
+
return { connection: void 0, hintApp: void 0 };
|
|
3152
|
+
}
|
|
3153
|
+
const entry = await readTunnelCacheEntry(config.host, cacheOptions);
|
|
3154
|
+
if (entry?.status !== "ready") {
|
|
3155
|
+
return { connection: void 0, hintApp: void 0 };
|
|
3156
|
+
}
|
|
3157
|
+
if (!await isTunnelUsable(entry, cacheOptions)) {
|
|
3158
|
+
await discardQuietly(evictTunnelCache(config.host, cacheOptions));
|
|
3159
|
+
return { connection: void 0, hintApp: entry.app };
|
|
3160
|
+
}
|
|
3161
|
+
const connection = await tryReadyRecord(entry, driver, config, directParams, cacheOptions);
|
|
3162
|
+
return { connection, hintApp: entry.app };
|
|
3163
|
+
}
|
|
3164
|
+
async function tryDirectConnect(driver, config, directParams) {
|
|
3165
|
+
if (config.tunnelMode !== "auto") {
|
|
3166
|
+
return { connection: void 0, directError: void 0 };
|
|
3167
|
+
}
|
|
3168
|
+
const boundedDirectParams = {
|
|
3169
|
+
...directParams,
|
|
3170
|
+
connectTimeoutMs: Math.min(directParams.connectTimeoutMs, DEFAULT_TUNNEL_CANDIDATE_TIMEOUT_MS)
|
|
3171
|
+
};
|
|
3172
|
+
try {
|
|
3173
|
+
return { connection: await driver.connect(boundedDirectParams), directError: void 0 };
|
|
3174
|
+
} catch (error) {
|
|
3175
|
+
if (!isConnectivityFailure(error)) {
|
|
3176
|
+
throw error;
|
|
3177
|
+
}
|
|
3178
|
+
return { connection: void 0, directError: error };
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
function buildExhaustionError(host, directError, candidates) {
|
|
3182
|
+
if (directError !== void 0) {
|
|
3183
|
+
return directError;
|
|
3184
|
+
}
|
|
3185
|
+
return new CfHanaError(
|
|
3186
|
+
"CONNECTION",
|
|
3187
|
+
`Could not establish an SSH tunnel to ${host} through any candidate app (tried: ${candidates.length > 0 ? candidates.join(", ") : "none"})`
|
|
3188
|
+
);
|
|
3189
|
+
}
|
|
3190
|
+
async function tryCandidateList(apps, ctx, driver, config, directParams, deadline, overrides, tried) {
|
|
3191
|
+
for (const app of apps) {
|
|
3192
|
+
if (Date.now() >= deadline) {
|
|
3193
|
+
return void 0;
|
|
3194
|
+
}
|
|
3195
|
+
tried.push(app);
|
|
3196
|
+
const connection = await tryCandidate(app, ctx, driver, config, directParams, deadline, overrides);
|
|
3197
|
+
if (connection !== void 0) {
|
|
3198
|
+
return connection;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
return void 0;
|
|
3202
|
+
}
|
|
3203
|
+
async function runCandidateLoop(ctx, driver, config, directParams, deadline, hintApp, overrides) {
|
|
3204
|
+
const tried = [];
|
|
3205
|
+
const known = knownCandidates(config.appName, hintApp);
|
|
3206
|
+
const knownConnection = await tryCandidateList(
|
|
3207
|
+
known,
|
|
3208
|
+
ctx,
|
|
3209
|
+
driver,
|
|
3210
|
+
config,
|
|
3211
|
+
directParams,
|
|
3212
|
+
deadline,
|
|
3213
|
+
overrides,
|
|
3214
|
+
tried
|
|
3215
|
+
);
|
|
3216
|
+
if (knownConnection !== void 0) {
|
|
3217
|
+
return { connection: knownConnection, candidatesTried: tried };
|
|
3218
|
+
}
|
|
3219
|
+
const discovered = await discoverExtraCandidates(config, ctx, tried, deadline);
|
|
3220
|
+
const discoveredConnection = await tryCandidateList(
|
|
3221
|
+
discovered,
|
|
3222
|
+
ctx,
|
|
3223
|
+
driver,
|
|
3224
|
+
config,
|
|
3225
|
+
directParams,
|
|
3226
|
+
deadline,
|
|
3227
|
+
overrides,
|
|
3228
|
+
tried
|
|
3229
|
+
);
|
|
3230
|
+
return { connection: discoveredConnection, candidatesTried: tried };
|
|
3231
|
+
}
|
|
3232
|
+
async function connectWithTunnelFallback(driver, config, overrides = {}) {
|
|
3233
|
+
const directParams = directParamsOf(config);
|
|
3234
|
+
const cacheOptions = overrides.cache ?? {};
|
|
3235
|
+
await discardQuietly(reapStaleAndCrossOrgTunnels(orgKeyOf(config), cacheOptions));
|
|
3236
|
+
const cached = await tryCachedTunnel(driver, config, directParams, cacheOptions);
|
|
3237
|
+
if (cached.connection !== void 0) {
|
|
3238
|
+
return cached.connection;
|
|
3239
|
+
}
|
|
3240
|
+
const direct = await tryDirectConnect(driver, config, directParams);
|
|
3241
|
+
if (direct.connection !== void 0) {
|
|
3242
|
+
return direct.connection;
|
|
3243
|
+
}
|
|
3244
|
+
const deadline = Date.now() + DEFAULT_TUNNEL_FALLBACK_BUDGET_MS;
|
|
3245
|
+
const target = {
|
|
3246
|
+
apiEndpoint: config.apiEndpoint,
|
|
3247
|
+
orgName: config.orgName,
|
|
3248
|
+
spaceName: config.spaceName
|
|
3249
|
+
};
|
|
3250
|
+
const loopResult = await withScopedCfSession(
|
|
3251
|
+
config.selectorSource,
|
|
3252
|
+
target,
|
|
3253
|
+
config.sapCredentials,
|
|
3254
|
+
(ctx) => runCandidateLoop(ctx, driver, config, directParams, deadline, cached.hintApp, overrides)
|
|
3255
|
+
);
|
|
3256
|
+
if (loopResult.connection !== void 0) {
|
|
3257
|
+
return loopResult.connection;
|
|
3258
|
+
}
|
|
3259
|
+
throw buildExhaustionError(config.host, direct.directError, loopResult.candidatesTried);
|
|
3260
|
+
}
|
|
3261
|
+
|
|
2363
3262
|
// src/connection.ts
|
|
2364
3263
|
function assertValidAutoLimit(value) {
|
|
2365
3264
|
if (value !== false && (!Number.isSafeInteger(value) || value <= 0 || value >= Number.MAX_SAFE_INTEGER)) {
|
|
@@ -2453,15 +3352,7 @@ var Connection = class _Connection {
|
|
|
2453
3352
|
driverConnection;
|
|
2454
3353
|
config;
|
|
2455
3354
|
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
|
-
});
|
|
3355
|
+
const driverConnection = await connectWithTunnelFallback(driver, config);
|
|
2465
3356
|
return new _Connection(driverConnection, config);
|
|
2466
3357
|
}
|
|
2467
3358
|
async query(sql, params, options) {
|
|
@@ -2743,6 +3634,31 @@ function toPoolOptions(options) {
|
|
|
2743
3634
|
}
|
|
2744
3635
|
return options.pool ?? {};
|
|
2745
3636
|
}
|
|
3637
|
+
function buildConnectionConfig(resolved, target, options) {
|
|
3638
|
+
const sapCredentials = readSapCredentials({ email: options.email, password: options.password });
|
|
3639
|
+
return {
|
|
3640
|
+
host: target.host,
|
|
3641
|
+
port: target.port,
|
|
3642
|
+
user: target.user,
|
|
3643
|
+
password: target.password,
|
|
3644
|
+
schema: target.schema,
|
|
3645
|
+
certificate: target.certificate,
|
|
3646
|
+
connectTimeoutMs: options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS,
|
|
3647
|
+
queryTimeoutMs: options.queryTimeoutMs ?? DEFAULT_QUERY_TIMEOUT_MS,
|
|
3648
|
+
readOnly: options.readOnly ?? false,
|
|
3649
|
+
allowDestructive: options.allowDestructive ?? false,
|
|
3650
|
+
autoLimit: options.autoLimit ?? DEFAULT_AUTO_LIMIT,
|
|
3651
|
+
appName: resolved.appName,
|
|
3652
|
+
orgName: resolved.orgName,
|
|
3653
|
+
spaceName: resolved.spaceName,
|
|
3654
|
+
apiEndpoint: resolved.apiEndpoint,
|
|
3655
|
+
selectorSource: resolved.selectorSource,
|
|
3656
|
+
tunnelMode: options.tunnel === true ? "always" : "auto",
|
|
3657
|
+
refreshTunnel: options.refreshTunnel ?? false,
|
|
3658
|
+
...sapCredentials === void 0 ? {} : { sapCredentials },
|
|
3659
|
+
...options.onTunnelStatus === void 0 ? {} : { onTunnelStatus: options.onTunnelStatus }
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
2746
3662
|
function toSelectedBindingInfo(bindings, selected) {
|
|
2747
3663
|
const availableBindingNames = bindings.flatMap(
|
|
2748
3664
|
(binding) => binding.name === void 0 ? [] : [binding.name]
|
|
@@ -2770,19 +3686,7 @@ var HanaClient = class _HanaClient {
|
|
|
2770
3686
|
const bindingInfo = toSelectedBindingInfo(resolved.bindings, binding);
|
|
2771
3687
|
const target = toConnectionTarget(binding, role);
|
|
2772
3688
|
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
|
-
};
|
|
3689
|
+
const config = buildConnectionConfig(resolved, target, options);
|
|
2786
3690
|
const poolOptions = toPoolOptions(options);
|
|
2787
3691
|
const info = {
|
|
2788
3692
|
selector: resolved.selector,
|