@saptools/cf-hana 0.3.2 → 0.3.5

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/dist/index.d.ts CHANGED
@@ -43,7 +43,7 @@ interface ConnectOptions {
43
43
  readonly autoLimit?: number | false;
44
44
  readonly queryTimeoutMs?: number;
45
45
  readonly connectTimeoutMs?: number;
46
- /** Bypass the cached credentials and fetch them live from Cloud Foundry. */
46
+ /** Deprecated compatibility flag. Binding discovery is already live. */
47
47
  readonly refresh?: boolean;
48
48
  /** SAP BTP email for the live credential fetch (else `SAP_EMAIL`). */
49
49
  readonly email?: string;
package/dist/index.js CHANGED
@@ -625,10 +625,10 @@ async function listColumns(connection, schema, table) {
625
625
  }
626
626
 
627
627
  // src/config.ts
628
- var CLI_VERSION = "0.3.2";
628
+ var CLI_VERSION = "0.3.5";
629
629
  var ENV_PREFIX = "CF_HANA";
630
- var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
631
- var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
630
+ var DEFAULT_QUERY_TIMEOUT_MS = 6e4;
631
+ var DEFAULT_CONNECT_TIMEOUT_MS = 6e4;
632
632
  var DEFAULT_POOL_MAX = 4;
633
633
  var DEFAULT_POOL_IDLE_MS = 6e4;
634
634
  var DEFAULT_AUTO_LIMIT = 100;
@@ -661,9 +661,9 @@ import { join as join2 } from "path";
661
661
  import { promisify } from "util";
662
662
  var execFileAsync = promisify(execFile);
663
663
  var MAX_BUFFER = 16 * 1024 * 1024;
664
- var DEFAULT_TIMEOUT_MS = 3e4;
664
+ var DEFAULT_TIMEOUT_MS = 6e4;
665
665
  var CF_RETRY_ATTEMPTS = 3;
666
- var CF_RETRY_BASE_DELAY_MS = 120;
666
+ var CF_RETRY_BASE_DELAY_MS = 500;
667
667
  var REGION_API_MAP = {
668
668
  ae01: "https://api.cf.ae01.hana.ondemand.com",
669
669
  ap01: "https://api.cf.ap01.hana.ondemand.com",
@@ -673,40 +673,94 @@ var REGION_API_MAP = {
673
673
  ap20: "https://api.cf.ap20.hana.ondemand.com",
674
674
  ap21: "https://api.cf.ap21.hana.ondemand.com",
675
675
  ap30: "https://api.cf.ap30.hana.ondemand.com",
676
+ ap31: "https://api.cf.ap31.hana.ondemand.com",
676
677
  br10: "https://api.cf.br10.hana.ondemand.com",
677
678
  br20: "https://api.cf.br20.hana.ondemand.com",
678
679
  br30: "https://api.cf.br30.hana.ondemand.com",
679
680
  ca10: "https://api.cf.ca10.hana.ondemand.com",
680
681
  ca20: "https://api.cf.ca20.hana.ondemand.com",
681
682
  ch20: "https://api.cf.ch20.hana.ondemand.com",
683
+ cn20: "https://api.cf.cn20.platform.sapcloud.cn",
684
+ cn40: "https://api.cf.cn40.platform.sapcloud.cn",
685
+ eu01: "https://api.cf.eu01.hana.ondemand.com",
686
+ eu02: "https://api.cf.eu02.hana.ondemand.com",
682
687
  eu10: "https://api.cf.eu10.hana.ondemand.com",
688
+ "eu10-002": "https://api.cf.eu10-002.hana.ondemand.com",
689
+ "eu10-003": "https://api.cf.eu10-003.hana.ondemand.com",
690
+ "eu10-004": "https://api.cf.eu10-004.hana.ondemand.com",
691
+ "eu10-005": "https://api.cf.eu10-005.hana.ondemand.com",
692
+ "eu10-006": "https://api.cf.eu10-006.hana.ondemand.com",
683
693
  eu11: "https://api.cf.eu11.hana.ondemand.com",
684
694
  eu12: "https://api.cf.eu12.hana.ondemand.com",
695
+ eu13: "https://api.cf.eu13.hana.ondemand.com",
685
696
  eu20: "https://api.cf.eu20.hana.ondemand.com",
697
+ "eu20-001": "https://api.cf.eu20-001.hana.ondemand.com",
698
+ "eu20-002": "https://api.cf.eu20-002.hana.ondemand.com",
686
699
  eu21: "https://api.cf.eu21.hana.ondemand.com",
700
+ eu22: "https://api.cf.eu22.hana.ondemand.com",
687
701
  eu30: "https://api.cf.eu30.hana.ondemand.com",
688
- "eu10-002": "https://api.cf.eu10-002.hana.ondemand.com",
702
+ eu31: "https://api.cf.eu31.hana.ondemand.com",
703
+ il30: "https://api.cf.il30.hana.ondemand.com",
704
+ in30: "https://api.cf.in30.hana.ondemand.com",
705
+ jp01: "https://api.cf.jp01.hana.ondemand.com",
689
706
  jp10: "https://api.cf.jp10.hana.ondemand.com",
690
707
  jp20: "https://api.cf.jp20.hana.ondemand.com",
691
708
  jp30: "https://api.cf.jp30.hana.ondemand.com",
709
+ jp31: "https://api.cf.jp31.hana.ondemand.com",
710
+ sa30: "https://api.cf.sa30.hana.ondemand.com",
711
+ sa31: "https://api.cf.sa31.hana.ondemand.com",
712
+ uk20: "https://api.cf.uk20.hana.ondemand.com",
713
+ us01: "https://api.cf.us01.hana.ondemand.com",
714
+ us02: "https://api.cf.us02.hana.ondemand.com",
692
715
  us10: "https://api.cf.us10.hana.ondemand.com",
716
+ "us10-001": "https://api.cf.us10-001.hana.ondemand.com",
717
+ "us10-002": "https://api.cf.us10-002.hana.ondemand.com",
718
+ "us10-003": "https://api.cf.us10-003.hana.ondemand.com",
693
719
  us11: "https://api.cf.us11.hana.ondemand.com",
694
720
  us20: "https://api.cf.us20.hana.ondemand.com",
695
721
  us21: "https://api.cf.us21.hana.ondemand.com",
722
+ "us21-001": "https://api.cf.us21-001.hana.ondemand.com",
723
+ us22: "https://api.cf.us22.hana.ondemand.com",
696
724
  us30: "https://api.cf.us30.hana.ondemand.com",
697
- in30: "https://api.cf.in30.hana.ondemand.com"
725
+ us32: "https://api.cf.us32.hana.ondemand.com"
698
726
  };
727
+ function normalizeSapCfApiEndpoint(apiEndpoint) {
728
+ const trimmed = apiEndpoint.trim();
729
+ let parsed;
730
+ try {
731
+ parsed = new URL(trimmed);
732
+ } catch {
733
+ throw new Error(`Invalid or untrusted CF API endpoint "${apiEndpoint}".`);
734
+ }
735
+ if (/^https:\/\/[^/]*:\d+(?:[/?#]|$)/i.test(trimmed)) {
736
+ throw new Error(`Invalid or untrusted CF API endpoint "${apiEndpoint}".`);
737
+ }
738
+ if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || parsed.port !== "" || parsed.pathname.replace(/\/+$/, "") !== "" || parsed.search !== "" || parsed.hash !== "") {
739
+ throw new Error(`Invalid or untrusted CF API endpoint "${apiEndpoint}".`);
740
+ }
741
+ const hostname = parsed.hostname.toLowerCase();
742
+ const match = /^api\.cf\.([a-z]{2}\d{2}(?:-\d{3})?)\.(hana\.ondemand\.com|platform\.sapcloud\.cn)$/.exec(hostname);
743
+ if (!match) {
744
+ throw new Error(`Invalid or untrusted CF API endpoint "${apiEndpoint}".`);
745
+ }
746
+ return `https://${hostname}`;
747
+ }
699
748
  function getApiEndpointForRegion(regionKey) {
700
- return REGION_API_MAP[regionKey];
749
+ return REGION_API_MAP[regionKey.trim().toLowerCase()];
701
750
  }
702
751
  function getRegionKeyForApi(apiEndpoint) {
703
- const norm = apiEndpoint.trim().replace(/\/+$/, "").toLowerCase();
752
+ let normalized;
753
+ try {
754
+ normalized = normalizeSapCfApiEndpoint(apiEndpoint);
755
+ } catch {
756
+ return void 0;
757
+ }
704
758
  for (const [key, endpoint] of Object.entries(REGION_API_MAP)) {
705
- if (endpoint.toLowerCase() === norm) {
759
+ if (endpoint.toLowerCase() === normalized) {
706
760
  return key;
707
761
  }
708
762
  }
709
- return void 0;
763
+ return /^https:\/\/api\.cf\.([a-z]{2}\d{2}(?:-\d{3})?)\.hana\.ondemand\.com$/.exec(normalized)?.[1];
710
764
  }
711
765
  async function withCfSession(work) {
712
766
  const cfHome = await mkdtemp(join2(tmpdir(), "saptools-cf-hana-"));
@@ -731,29 +785,45 @@ function buildEnv(ctx, overrides = {}) {
731
785
  env["CF_HOME"] = ctx.cfHome;
732
786
  return env;
733
787
  }
734
- async function runCf(args, ctx, overrides = {}) {
735
- const { bin, argsPrefix } = resolveCfBin();
736
- const env = buildEnv(ctx, overrides);
788
+ async function execWithRetries(bin, args, env, timeoutMs = DEFAULT_TIMEOUT_MS) {
737
789
  let lastErr;
738
790
  for (let attempt = 0; attempt < CF_RETRY_ATTEMPTS; attempt++) {
739
791
  try {
740
- const { stdout } = await execFileAsync(bin, [...argsPrefix, ...args], {
792
+ const { stdout } = await execFileAsync(bin, args, {
741
793
  env,
742
794
  maxBuffer: MAX_BUFFER,
743
- timeout: DEFAULT_TIMEOUT_MS
795
+ timeout: timeoutMs,
796
+ killSignal: "SIGKILL"
744
797
  });
745
798
  return stdout;
746
799
  } catch (err) {
747
800
  lastErr = err;
801
+ const e = err;
802
+ const output = `${e.message ?? ""} ${e.stderr ? String(e.stderr) : ""}`.toLowerCase();
803
+ const isTimeout = e.killed === true;
804
+ const isNetworkFlake = output.includes("error performing request") || output.includes("timeout") || output.includes("connection reset") || output.includes("connection refused") || output.includes("eof") || output.includes("502 bad gateway") || output.includes("503 service unavailable") || output.includes("504 gateway timeout") || output.includes("dial tcp") || output.includes("no such host");
805
+ const isEnoent = e.code === "ENOENT";
806
+ if (isEnoent || !isTimeout && !isNetworkFlake) {
807
+ throw err;
808
+ }
748
809
  if (attempt < CF_RETRY_ATTEMPTS - 1) {
749
810
  await new Promise((r) => setTimeout(r, CF_RETRY_BASE_DELAY_MS * (attempt + 1)));
750
811
  }
751
812
  }
752
813
  }
753
- const e = lastErr;
754
- const detail = e?.stderr ? String(e.stderr) : e?.message ?? "";
755
- const cmd = args[0] === "auth" ? "cf auth" : `cf ${args.join(" ")}`;
756
- throw new Error(`${cmd} failed: ${detail}`.trim(), { cause: lastErr });
814
+ throw lastErr;
815
+ }
816
+ async function runCf(args, ctx, overrides = {}) {
817
+ const { bin, argsPrefix } = resolveCfBin();
818
+ const env = buildEnv(ctx, overrides);
819
+ try {
820
+ return await execWithRetries(bin, [...argsPrefix, ...args], env);
821
+ } catch (lastErr) {
822
+ const e = lastErr;
823
+ const detail = e?.stderr ? String(e.stderr) : e?.message ?? "";
824
+ const cmd = args[0] === "auth" ? "cf auth" : `cf ${args.join(" ")}`;
825
+ throw new Error(`${cmd} failed: ${detail}`.trim(), { cause: lastErr });
826
+ }
757
827
  }
758
828
  async function cfApi(apiEndpoint, ctx) {
759
829
  await runCf(["api", apiEndpoint], ctx);
@@ -775,12 +845,7 @@ async function cfEnvDirect(appName) {
775
845
  const env = { ...process.env };
776
846
  delete env["SAP_EMAIL"];
777
847
  delete env["SAP_PASSWORD"];
778
- const { stdout } = await execFileAsync(bin, [...argsPrefix, "env", appName], {
779
- env,
780
- maxBuffer: MAX_BUFFER,
781
- timeout: DEFAULT_TIMEOUT_MS
782
- });
783
- return stdout;
848
+ return await execWithRetries(bin, [...argsPrefix, "env", appName], env);
784
849
  }
785
850
  async function readCurrentCfTarget() {
786
851
  const { bin, argsPrefix } = resolveCfBin();
@@ -788,11 +853,7 @@ async function readCurrentCfTarget() {
788
853
  delete env["SAP_EMAIL"];
789
854
  delete env["SAP_PASSWORD"];
790
855
  try {
791
- const { stdout } = await execFileAsync(bin, [...argsPrefix, "target"], {
792
- env,
793
- maxBuffer: MAX_BUFFER,
794
- timeout: 1e4
795
- });
856
+ const stdout = await execWithRetries(bin, [...argsPrefix, "target"], env);
796
857
  return parseCfTargetOutput(stdout);
797
858
  } catch {
798
859
  return void 0;
@@ -806,9 +867,15 @@ function parseCfTargetOutput(stdout) {
806
867
  if (!api || !org || !space) {
807
868
  return void 0;
808
869
  }
809
- const regionKey = getRegionKeyForApi(api);
870
+ let normalizedApi;
871
+ try {
872
+ normalizedApi = normalizeSapCfApiEndpoint(api);
873
+ } catch {
874
+ return void 0;
875
+ }
876
+ const regionKey = getRegionKeyForApi(normalizedApi);
810
877
  return {
811
- apiEndpoint: api,
878
+ apiEndpoint: normalizedApi,
812
879
  orgName: org,
813
880
  spaceName: space,
814
881
  ...regionKey ? { regionKey } : {}
@@ -834,12 +901,8 @@ function formatCurrentCfAppSelector(target, appName) {
834
901
  if (!name) {
835
902
  throw new Error("App name is required.");
836
903
  }
837
- if (!target.regionKey) {
838
- throw new Error(
839
- `Current CF API endpoint "${target.apiEndpoint}" does not match a known SAP region. Pass a full region/org/space/app selector.`
840
- );
841
- }
842
- return `${target.regionKey}/${target.orgName}/${target.spaceName}/${name}`;
904
+ const region = target.regionKey ?? "current";
905
+ return `${region}/${target.orgName}/${target.spaceName}/${name}`;
843
906
  }
844
907
  function extractVcapSection(stdout) {
845
908
  const start = stdout.indexOf("VCAP_SERVICES:");
@@ -919,6 +982,9 @@ function classifyCfError(stderr = "", stdout = "") {
919
982
  }
920
983
 
921
984
  // src/credentials.ts
985
+ function errorMessageFromUnknown(error) {
986
+ return error instanceof Error ? error.message : "Invalid or untrusted CF API endpoint.";
987
+ }
922
988
  async function resolveAppBindings(rawSelector, options) {
923
989
  const selector = rawSelector.trim();
924
990
  if (!selector) {
@@ -937,7 +1003,7 @@ async function resolveAppBindings(rawSelector, options) {
937
1003
  const displaySelector = current.regionKey ? formatCurrentCfAppSelector(current, selector) : `current/${current.orgName}/${current.spaceName}/${selector}`;
938
1004
  target = {
939
1005
  selector: displaySelector,
940
- apiEndpoint: current.regionKey ? getApiEndpointForRegion(current.regionKey) : void 0,
1006
+ apiEndpoint: current.apiEndpoint,
941
1007
  orgName: current.orgName,
942
1008
  spaceName: current.spaceName,
943
1009
  appName: selector
@@ -955,10 +1021,16 @@ async function resolveAppBindings(rawSelector, options) {
955
1021
  if (!apiEndpoint) {
956
1022
  throw new CfHanaError(
957
1023
  "CONFIG",
958
- `Unknown region key "${regionKey}". Use a known region or the current CF target.`
1024
+ `Unknown SAP CF region "${regionKey}". Verify the current SAP region list or use the current CF target.`
959
1025
  );
960
1026
  }
961
- target = { selector, apiEndpoint, orgName, spaceName, appName };
1027
+ let normalizedApiEndpoint;
1028
+ try {
1029
+ normalizedApiEndpoint = normalizeSapCfApiEndpoint(apiEndpoint);
1030
+ } catch (error) {
1031
+ throw new CfHanaError("CONFIG", errorMessageFromUnknown(error), { cause: error });
1032
+ }
1033
+ target = { selector, apiEndpoint: normalizedApiEndpoint, orgName, spaceName, appName };
962
1034
  }
963
1035
  let bindings;
964
1036
  const source = "live";
@@ -982,9 +1054,6 @@ Run "cf login" + "cf target", or provide SAP_EMAIL + SAP_PASSWORD.`,
982
1054
  { cause: directError }
983
1055
  );
984
1056
  }
985
- if (!target.apiEndpoint) {
986
- throw new CfHanaError("CONFIG", "Cannot determine API endpoint for fallback auth.");
987
- }
988
1057
  const api = target.apiEndpoint;
989
1058
  bindings = await withCfSession(async (ctx) => {
990
1059
  await cfApi(api, ctx);
@@ -1008,9 +1077,6 @@ Run "cf login" + "cf target", or provide SAP_EMAIL + SAP_PASSWORD.`,
1008
1077
  `SAP_EMAIL and SAP_PASSWORD are required to fetch HANA bindings for explicit selector "${selector}".`
1009
1078
  );
1010
1079
  }
1011
- if (!target.apiEndpoint) {
1012
- throw new CfHanaError("CONFIG", "Invalid explicit selector.");
1013
- }
1014
1080
  const api = target.apiEndpoint;
1015
1081
  bindings = await withCfSession(async (ctx) => {
1016
1082
  await cfApi(api, ctx);
@@ -1726,6 +1792,57 @@ function boundedRows(rows, requestedLimit) {
1726
1792
  truncated: rows.length > requestedLimit
1727
1793
  };
1728
1794
  }
1795
+ function isBooleanColumn(column) {
1796
+ return column.typeName.trim().toUpperCase() === "BOOLEAN";
1797
+ }
1798
+ function normalizeBooleanCell(value) {
1799
+ if (value === 0) {
1800
+ return false;
1801
+ }
1802
+ if (value === 1) {
1803
+ return true;
1804
+ }
1805
+ if (typeof value !== "string") {
1806
+ return value;
1807
+ }
1808
+ const normalized = value.trim().toLowerCase();
1809
+ if (normalized === "0") {
1810
+ return false;
1811
+ }
1812
+ if (normalized === "1") {
1813
+ return true;
1814
+ }
1815
+ if (normalized === "false") {
1816
+ return false;
1817
+ }
1818
+ if (normalized === "true") {
1819
+ return true;
1820
+ }
1821
+ return value;
1822
+ }
1823
+ function normalizeBooleanRow(row, columns) {
1824
+ let normalizedRow;
1825
+ for (const column of columns) {
1826
+ const value = row[column.name];
1827
+ if (value === void 0) {
1828
+ continue;
1829
+ }
1830
+ const normalizedValue = normalizeBooleanCell(value);
1831
+ if (normalizedValue === value) {
1832
+ continue;
1833
+ }
1834
+ normalizedRow ??= { ...row };
1835
+ normalizedRow[column.name] = normalizedValue;
1836
+ }
1837
+ return normalizedRow ?? row;
1838
+ }
1839
+ function normalizeBooleanRows(rows, columns) {
1840
+ const booleanColumns = columns.filter(isBooleanColumn);
1841
+ if (booleanColumns.length === 0 || rows.length === 0) {
1842
+ return rows;
1843
+ }
1844
+ return rows.map((row) => normalizeBooleanRow(row, booleanColumns));
1845
+ }
1729
1846
  async function withTimeout(work, timeoutMs, onTimeout) {
1730
1847
  let timer;
1731
1848
  const timeout = new Promise((_resolve, reject) => {
@@ -1811,7 +1928,8 @@ var Connection = class _Connection {
1811
1928
  }
1812
1929
  );
1813
1930
  const elapsedMs = Date.now() - started;
1814
- const selected = boundedRows(execResult.rows, limited.requestedLimit);
1931
+ const normalizedRows = normalizeBooleanRows(execResult.rows, execResult.columns);
1932
+ const selected = boundedRows(normalizedRows, limited.requestedLimit);
1815
1933
  return {
1816
1934
  rows: selected.rows,
1817
1935
  columns: execResult.columns,