@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/CHANGELOG.md +18 -0
- package/README.md +41 -31
- package/dist/cli.js +176 -77
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +167 -49
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
<!-- cspell:words VARCHAR -->
|
|
4
|
+
|
|
5
|
+
## 0.3.5 - 2026-07-06
|
|
6
|
+
|
|
7
|
+
- Normalize HANA `BOOLEAN` result columns that arrive from the driver as `1`/`0`
|
|
8
|
+
into JavaScript `true`/`false` values before CLI/API formatting and saved refs.
|
|
9
|
+
|
|
10
|
+
## 0.3.4 - 2026-07-02
|
|
11
|
+
|
|
12
|
+
- Keep explicit selector region lookup map-backed so unknown technical keys fail before isolated authentication unless they are added to the maintained SAP CF region catalog.
|
|
13
|
+
|
|
14
|
+
## 0.3.3 - 2026-07-02
|
|
15
|
+
|
|
16
|
+
- Fix Cloud Foundry region resolution for indexed SAP regions such as `eu10-005`, representative `eu20`/`us10` indexed endpoints, and China `platform.sapcloud.cn` endpoints.
|
|
17
|
+
- Keep bare CLI selectors on the core current-session path so healthy `cf env <app>` calls do not require isolated SAP re-authentication.
|
|
18
|
+
- Preserve and validate the current CF API endpoint for auth fallback, reject unsafe endpoint shapes, and keep binding discovery live-only without restoring `cf-sync` cache reads.
|
|
19
|
+
- Update `--refresh`, README, and cf-hana skill text to describe the retained compatibility flag truthfully.
|
|
20
|
+
|
|
3
21
|
## 0.3.2 - 2026-07-01
|
|
4
22
|
|
|
5
23
|
- Remove the `cf-hana: saved result expires at...` stderr notice from `query --save` output while keeping result refs available for inspection.
|
package/README.md
CHANGED
|
@@ -2,19 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
> Run SQL directly against SAP HANA Cloud databases bound to a Cloud Foundry app — addressed by a `region/org/space/app` selector.
|
|
4
4
|
|
|
5
|
-
`@saptools/cf-hana`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
It is fast because credentials come from `cf-sync`'s on-disk cache (no 5–15s CF
|
|
11
|
-
login on the hot path) and connections are pooled and reused within a process.
|
|
5
|
+
`@saptools/cf-hana` opens live SAP HANA Cloud connections from Cloud Foundry app
|
|
6
|
+
bindings. Pass a selector, get a connected, pooled client, and run `SELECT` /
|
|
7
|
+
`INSERT` / `UPDATE` / `DELETE` / DDL. Bare app names use your active `cf target`
|
|
8
|
+
and current CF session first, so a healthy local CF login does not require SAP
|
|
9
|
+
password re-authentication.
|
|
12
10
|
|
|
13
11
|
## Features
|
|
14
12
|
|
|
15
13
|
- **Selector-based connect** — `region/org/space/app` or a bare app name.
|
|
16
|
-
- **Credentials, handled for you** —
|
|
17
|
-
|
|
14
|
+
- **Credentials, handled for you** — HANA bindings are read live from Cloud Foundry.
|
|
15
|
+
Bare app names use the current CF session first; explicit selectors use isolated live authentication.
|
|
18
16
|
- **Parameterized queries** — values always travel as bound `?` parameters, never
|
|
19
17
|
string-concatenated.
|
|
20
18
|
- **Connection pooling** — pooled, reused connections; opt out with `pool: false`.
|
|
@@ -74,8 +72,9 @@ Every entry point takes a selector as its first argument:
|
|
|
74
72
|
|
|
75
73
|
- **Explicit** — `region/org/space/app` (e.g.
|
|
76
74
|
`eu10/example-org/space-demo/app-demo`). Works without any cached topology.
|
|
77
|
-
- **Bare app name** — `app-demo`. Resolved against the
|
|
78
|
-
|
|
75
|
+
- **Bare app name** — `app-demo`. Resolved only against the active `cf target`
|
|
76
|
+
(current org/space/API endpoint) and fetched with `cf env app-demo` using your
|
|
77
|
+
existing CF session before any isolated re-auth fallback.
|
|
79
78
|
|
|
80
79
|
## CLI
|
|
81
80
|
|
|
@@ -89,7 +88,7 @@ cf-hana info <selector> Print the resolved connection metada
|
|
|
89
88
|
cf-hana result <command> Inspect saved query refs
|
|
90
89
|
```
|
|
91
90
|
|
|
92
|
-
Common options: `--refresh
|
|
91
|
+
Common options: `--refresh` (deprecated compatibility flag; binding discovery is already live), `--role <runtime|hdi>`, `--binding <name>` /
|
|
93
92
|
`--binding-index <n>`, `--timeout <ms>`, `--read-only`, `--allow-destructive`,
|
|
94
93
|
`--limit <n>`, `--no-auto-limit`. The `query` command also accepts
|
|
95
94
|
`--param <value>` (repeatable), `--cell-limit <n>`, `--save`,
|
|
@@ -170,10 +169,11 @@ atomically, and expire after exactly 30 minutes. The cache key is derived from
|
|
|
170
169
|
non-secret connection identity: selector, app name, host, active schema, role,
|
|
171
170
|
and driver. It does not include passwords, certificates, tokens, SQL parameter
|
|
172
171
|
values, result rows, or table data, and malformed cache files are treated as
|
|
173
|
-
misses. Pass `--refresh-metadata` to bypass this cache for a query
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
172
|
+
misses. Pass `--refresh-metadata` to bypass this cache for a query. The legacy
|
|
173
|
+
`--refresh` flag is accepted for compatibility but does not bypass the metadata
|
|
174
|
+
cache and is not a credential-cache control because binding discovery is already
|
|
175
|
+
live. If metadata lookup or cache I/O fails, cf-hana preserves the original
|
|
176
|
+
query error and simply omits suggestions.
|
|
177
177
|
|
|
178
178
|
## Programmatic API
|
|
179
179
|
|
|
@@ -187,21 +187,31 @@ and simply omits suggestions.
|
|
|
187
187
|
|
|
188
188
|
`ConnectOptions` highlights: `role` (`runtime` | `hdi`), `bindingName` /
|
|
189
189
|
`bindingIndex`, `readOnly`, `allowDestructive`, `autoLimit`, `queryTimeoutMs`,
|
|
190
|
-
`connectTimeoutMs`, `refresh`, `pool`.
|
|
190
|
+
`connectTimeoutMs`, deprecated `refresh`, `pool`.
|
|
191
191
|
|
|
192
192
|
## Credentials
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
194
|
+
Credential discovery is **live-only** and does not read `@saptools/cf-sync`
|
|
195
|
+
snapshots or `~/.saptools/cf-db-bindings.json`.
|
|
196
|
+
|
|
197
|
+
- Bare app selectors (`app-demo`) read the active `cf target`, preserve its exact
|
|
198
|
+
validated API endpoint, and first run `cf env app-demo` with your current CF
|
|
199
|
+
session. This path does not require `SAP_EMAIL` or `SAP_PASSWORD` while the
|
|
200
|
+
current session is healthy.
|
|
201
|
+
- If that direct bare-app call fails with an auth/session error, cf-hana falls
|
|
202
|
+
back to an isolated temporary `CF_HOME`, runs `cf api <current-endpoint>`,
|
|
203
|
+
authenticates with `SAP_EMAIL` / `SAP_PASSWORD` (or the programmatic `email` /
|
|
204
|
+
`password` options), targets the current org/space, and reads `cf env`.
|
|
205
|
+
- Explicit `region/org/space/app` selectors always use isolated live
|
|
206
|
+
authentication. Region keys support current SAP CF technical keys, including
|
|
207
|
+
indexed regions such as `eu10-005`, `eu20-001`, `us10-002`, and China
|
|
208
|
+
endpoints such as `cn40` on `platform.sapcloud.cn`.
|
|
209
|
+
|
|
210
|
+
The legacy `refresh` / `--refresh` option is retained only for compatibility and
|
|
211
|
+
has no credential-cache meaning; binding discovery is already live. Use
|
|
212
|
+
`--refresh-metadata` when you specifically want to bypass the table/view
|
|
213
|
+
suggestion metadata cache. Credential resolution writes no binding credentials
|
|
214
|
+
under `~/.saptools/`.
|
|
205
215
|
|
|
206
216
|
## SQL history
|
|
207
217
|
|
|
@@ -264,9 +274,9 @@ parameters.
|
|
|
264
274
|
## Requirements
|
|
265
275
|
|
|
266
276
|
- Node.js >= 20.
|
|
267
|
-
- A HANA binding reachable from your network. Resolving a bare app name
|
|
268
|
-
|
|
269
|
-
`
|
|
277
|
+
- A HANA binding reachable from your network. Resolving a bare app name needs the Cloud Foundry CLI and an active `cf target`.
|
|
278
|
+
Isolated fallback or explicit selectors additionally need `SAP_EMAIL` /
|
|
279
|
+
`SAP_PASSWORD`.
|
|
270
280
|
|
|
271
281
|
## Development
|
|
272
282
|
|
package/dist/cli.js
CHANGED
|
@@ -698,10 +698,10 @@ async function listColumns(connection, schema, table) {
|
|
|
698
698
|
|
|
699
699
|
// src/config.ts
|
|
700
700
|
var CLI_NAME = "cf-hana";
|
|
701
|
-
var CLI_VERSION = "0.3.
|
|
701
|
+
var CLI_VERSION = "0.3.5";
|
|
702
702
|
var ENV_PREFIX = "CF_HANA";
|
|
703
|
-
var DEFAULT_QUERY_TIMEOUT_MS =
|
|
704
|
-
var DEFAULT_CONNECT_TIMEOUT_MS =
|
|
703
|
+
var DEFAULT_QUERY_TIMEOUT_MS = 6e4;
|
|
704
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 6e4;
|
|
705
705
|
var DEFAULT_POOL_MAX = 4;
|
|
706
706
|
var DEFAULT_POOL_IDLE_MS = 6e4;
|
|
707
707
|
var DEFAULT_AUTO_LIMIT = 100;
|
|
@@ -738,9 +738,9 @@ import { join as join2 } from "path";
|
|
|
738
738
|
import { promisify } from "util";
|
|
739
739
|
var execFileAsync = promisify(execFile);
|
|
740
740
|
var MAX_BUFFER = 16 * 1024 * 1024;
|
|
741
|
-
var DEFAULT_TIMEOUT_MS =
|
|
741
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
742
742
|
var CF_RETRY_ATTEMPTS = 3;
|
|
743
|
-
var CF_RETRY_BASE_DELAY_MS =
|
|
743
|
+
var CF_RETRY_BASE_DELAY_MS = 500;
|
|
744
744
|
var REGION_API_MAP = {
|
|
745
745
|
ae01: "https://api.cf.ae01.hana.ondemand.com",
|
|
746
746
|
ap01: "https://api.cf.ap01.hana.ondemand.com",
|
|
@@ -750,40 +750,94 @@ var REGION_API_MAP = {
|
|
|
750
750
|
ap20: "https://api.cf.ap20.hana.ondemand.com",
|
|
751
751
|
ap21: "https://api.cf.ap21.hana.ondemand.com",
|
|
752
752
|
ap30: "https://api.cf.ap30.hana.ondemand.com",
|
|
753
|
+
ap31: "https://api.cf.ap31.hana.ondemand.com",
|
|
753
754
|
br10: "https://api.cf.br10.hana.ondemand.com",
|
|
754
755
|
br20: "https://api.cf.br20.hana.ondemand.com",
|
|
755
756
|
br30: "https://api.cf.br30.hana.ondemand.com",
|
|
756
757
|
ca10: "https://api.cf.ca10.hana.ondemand.com",
|
|
757
758
|
ca20: "https://api.cf.ca20.hana.ondemand.com",
|
|
758
759
|
ch20: "https://api.cf.ch20.hana.ondemand.com",
|
|
760
|
+
cn20: "https://api.cf.cn20.platform.sapcloud.cn",
|
|
761
|
+
cn40: "https://api.cf.cn40.platform.sapcloud.cn",
|
|
762
|
+
eu01: "https://api.cf.eu01.hana.ondemand.com",
|
|
763
|
+
eu02: "https://api.cf.eu02.hana.ondemand.com",
|
|
759
764
|
eu10: "https://api.cf.eu10.hana.ondemand.com",
|
|
765
|
+
"eu10-002": "https://api.cf.eu10-002.hana.ondemand.com",
|
|
766
|
+
"eu10-003": "https://api.cf.eu10-003.hana.ondemand.com",
|
|
767
|
+
"eu10-004": "https://api.cf.eu10-004.hana.ondemand.com",
|
|
768
|
+
"eu10-005": "https://api.cf.eu10-005.hana.ondemand.com",
|
|
769
|
+
"eu10-006": "https://api.cf.eu10-006.hana.ondemand.com",
|
|
760
770
|
eu11: "https://api.cf.eu11.hana.ondemand.com",
|
|
761
771
|
eu12: "https://api.cf.eu12.hana.ondemand.com",
|
|
772
|
+
eu13: "https://api.cf.eu13.hana.ondemand.com",
|
|
762
773
|
eu20: "https://api.cf.eu20.hana.ondemand.com",
|
|
774
|
+
"eu20-001": "https://api.cf.eu20-001.hana.ondemand.com",
|
|
775
|
+
"eu20-002": "https://api.cf.eu20-002.hana.ondemand.com",
|
|
763
776
|
eu21: "https://api.cf.eu21.hana.ondemand.com",
|
|
777
|
+
eu22: "https://api.cf.eu22.hana.ondemand.com",
|
|
764
778
|
eu30: "https://api.cf.eu30.hana.ondemand.com",
|
|
765
|
-
|
|
779
|
+
eu31: "https://api.cf.eu31.hana.ondemand.com",
|
|
780
|
+
il30: "https://api.cf.il30.hana.ondemand.com",
|
|
781
|
+
in30: "https://api.cf.in30.hana.ondemand.com",
|
|
782
|
+
jp01: "https://api.cf.jp01.hana.ondemand.com",
|
|
766
783
|
jp10: "https://api.cf.jp10.hana.ondemand.com",
|
|
767
784
|
jp20: "https://api.cf.jp20.hana.ondemand.com",
|
|
768
785
|
jp30: "https://api.cf.jp30.hana.ondemand.com",
|
|
786
|
+
jp31: "https://api.cf.jp31.hana.ondemand.com",
|
|
787
|
+
sa30: "https://api.cf.sa30.hana.ondemand.com",
|
|
788
|
+
sa31: "https://api.cf.sa31.hana.ondemand.com",
|
|
789
|
+
uk20: "https://api.cf.uk20.hana.ondemand.com",
|
|
790
|
+
us01: "https://api.cf.us01.hana.ondemand.com",
|
|
791
|
+
us02: "https://api.cf.us02.hana.ondemand.com",
|
|
769
792
|
us10: "https://api.cf.us10.hana.ondemand.com",
|
|
793
|
+
"us10-001": "https://api.cf.us10-001.hana.ondemand.com",
|
|
794
|
+
"us10-002": "https://api.cf.us10-002.hana.ondemand.com",
|
|
795
|
+
"us10-003": "https://api.cf.us10-003.hana.ondemand.com",
|
|
770
796
|
us11: "https://api.cf.us11.hana.ondemand.com",
|
|
771
797
|
us20: "https://api.cf.us20.hana.ondemand.com",
|
|
772
798
|
us21: "https://api.cf.us21.hana.ondemand.com",
|
|
799
|
+
"us21-001": "https://api.cf.us21-001.hana.ondemand.com",
|
|
800
|
+
us22: "https://api.cf.us22.hana.ondemand.com",
|
|
773
801
|
us30: "https://api.cf.us30.hana.ondemand.com",
|
|
774
|
-
|
|
802
|
+
us32: "https://api.cf.us32.hana.ondemand.com"
|
|
775
803
|
};
|
|
804
|
+
function normalizeSapCfApiEndpoint(apiEndpoint) {
|
|
805
|
+
const trimmed = apiEndpoint.trim();
|
|
806
|
+
let parsed;
|
|
807
|
+
try {
|
|
808
|
+
parsed = new URL(trimmed);
|
|
809
|
+
} catch {
|
|
810
|
+
throw new Error(`Invalid or untrusted CF API endpoint "${apiEndpoint}".`);
|
|
811
|
+
}
|
|
812
|
+
if (/^https:\/\/[^/]*:\d+(?:[/?#]|$)/i.test(trimmed)) {
|
|
813
|
+
throw new Error(`Invalid or untrusted CF API endpoint "${apiEndpoint}".`);
|
|
814
|
+
}
|
|
815
|
+
if (parsed.protocol !== "https:" || parsed.username !== "" || parsed.password !== "" || parsed.port !== "" || parsed.pathname.replace(/\/+$/, "") !== "" || parsed.search !== "" || parsed.hash !== "") {
|
|
816
|
+
throw new Error(`Invalid or untrusted CF API endpoint "${apiEndpoint}".`);
|
|
817
|
+
}
|
|
818
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
819
|
+
const match = /^api\.cf\.([a-z]{2}\d{2}(?:-\d{3})?)\.(hana\.ondemand\.com|platform\.sapcloud\.cn)$/.exec(hostname);
|
|
820
|
+
if (!match) {
|
|
821
|
+
throw new Error(`Invalid or untrusted CF API endpoint "${apiEndpoint}".`);
|
|
822
|
+
}
|
|
823
|
+
return `https://${hostname}`;
|
|
824
|
+
}
|
|
776
825
|
function getApiEndpointForRegion(regionKey) {
|
|
777
|
-
return REGION_API_MAP[regionKey];
|
|
826
|
+
return REGION_API_MAP[regionKey.trim().toLowerCase()];
|
|
778
827
|
}
|
|
779
828
|
function getRegionKeyForApi(apiEndpoint) {
|
|
780
|
-
|
|
829
|
+
let normalized;
|
|
830
|
+
try {
|
|
831
|
+
normalized = normalizeSapCfApiEndpoint(apiEndpoint);
|
|
832
|
+
} catch {
|
|
833
|
+
return void 0;
|
|
834
|
+
}
|
|
781
835
|
for (const [key, endpoint] of Object.entries(REGION_API_MAP)) {
|
|
782
|
-
if (endpoint.toLowerCase() ===
|
|
836
|
+
if (endpoint.toLowerCase() === normalized) {
|
|
783
837
|
return key;
|
|
784
838
|
}
|
|
785
839
|
}
|
|
786
|
-
return
|
|
840
|
+
return /^https:\/\/api\.cf\.([a-z]{2}\d{2}(?:-\d{3})?)\.hana\.ondemand\.com$/.exec(normalized)?.[1];
|
|
787
841
|
}
|
|
788
842
|
async function withCfSession(work) {
|
|
789
843
|
const cfHome = await mkdtemp(join2(tmpdir(), "saptools-cf-hana-"));
|
|
@@ -808,29 +862,45 @@ function buildEnv(ctx, overrides = {}) {
|
|
|
808
862
|
env["CF_HOME"] = ctx.cfHome;
|
|
809
863
|
return env;
|
|
810
864
|
}
|
|
811
|
-
async function
|
|
812
|
-
const { bin, argsPrefix } = resolveCfBin();
|
|
813
|
-
const env = buildEnv(ctx, overrides);
|
|
865
|
+
async function execWithRetries(bin, args, env, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
814
866
|
let lastErr;
|
|
815
867
|
for (let attempt = 0; attempt < CF_RETRY_ATTEMPTS; attempt++) {
|
|
816
868
|
try {
|
|
817
|
-
const { stdout } = await execFileAsync(bin,
|
|
869
|
+
const { stdout } = await execFileAsync(bin, args, {
|
|
818
870
|
env,
|
|
819
871
|
maxBuffer: MAX_BUFFER,
|
|
820
|
-
timeout:
|
|
872
|
+
timeout: timeoutMs,
|
|
873
|
+
killSignal: "SIGKILL"
|
|
821
874
|
});
|
|
822
875
|
return stdout;
|
|
823
876
|
} catch (err) {
|
|
824
877
|
lastErr = err;
|
|
878
|
+
const e = err;
|
|
879
|
+
const output = `${e.message ?? ""} ${e.stderr ? String(e.stderr) : ""}`.toLowerCase();
|
|
880
|
+
const isTimeout = e.killed === true;
|
|
881
|
+
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");
|
|
882
|
+
const isEnoent = e.code === "ENOENT";
|
|
883
|
+
if (isEnoent || !isTimeout && !isNetworkFlake) {
|
|
884
|
+
throw err;
|
|
885
|
+
}
|
|
825
886
|
if (attempt < CF_RETRY_ATTEMPTS - 1) {
|
|
826
887
|
await new Promise((r) => setTimeout(r, CF_RETRY_BASE_DELAY_MS * (attempt + 1)));
|
|
827
888
|
}
|
|
828
889
|
}
|
|
829
890
|
}
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
891
|
+
throw lastErr;
|
|
892
|
+
}
|
|
893
|
+
async function runCf(args, ctx, overrides = {}) {
|
|
894
|
+
const { bin, argsPrefix } = resolveCfBin();
|
|
895
|
+
const env = buildEnv(ctx, overrides);
|
|
896
|
+
try {
|
|
897
|
+
return await execWithRetries(bin, [...argsPrefix, ...args], env);
|
|
898
|
+
} catch (lastErr) {
|
|
899
|
+
const e = lastErr;
|
|
900
|
+
const detail = e?.stderr ? String(e.stderr) : e?.message ?? "";
|
|
901
|
+
const cmd = args[0] === "auth" ? "cf auth" : `cf ${args.join(" ")}`;
|
|
902
|
+
throw new Error(`${cmd} failed: ${detail}`.trim(), { cause: lastErr });
|
|
903
|
+
}
|
|
834
904
|
}
|
|
835
905
|
async function cfApi(apiEndpoint, ctx) {
|
|
836
906
|
await runCf(["api", apiEndpoint], ctx);
|
|
@@ -852,12 +922,7 @@ async function cfEnvDirect(appName) {
|
|
|
852
922
|
const env = { ...process.env };
|
|
853
923
|
delete env["SAP_EMAIL"];
|
|
854
924
|
delete env["SAP_PASSWORD"];
|
|
855
|
-
|
|
856
|
-
env,
|
|
857
|
-
maxBuffer: MAX_BUFFER,
|
|
858
|
-
timeout: DEFAULT_TIMEOUT_MS
|
|
859
|
-
});
|
|
860
|
-
return stdout;
|
|
925
|
+
return await execWithRetries(bin, [...argsPrefix, "env", appName], env);
|
|
861
926
|
}
|
|
862
927
|
async function readCurrentCfTarget() {
|
|
863
928
|
const { bin, argsPrefix } = resolveCfBin();
|
|
@@ -865,11 +930,7 @@ async function readCurrentCfTarget() {
|
|
|
865
930
|
delete env["SAP_EMAIL"];
|
|
866
931
|
delete env["SAP_PASSWORD"];
|
|
867
932
|
try {
|
|
868
|
-
const
|
|
869
|
-
env,
|
|
870
|
-
maxBuffer: MAX_BUFFER,
|
|
871
|
-
timeout: 1e4
|
|
872
|
-
});
|
|
933
|
+
const stdout = await execWithRetries(bin, [...argsPrefix, "target"], env);
|
|
873
934
|
return parseCfTargetOutput(stdout);
|
|
874
935
|
} catch {
|
|
875
936
|
return void 0;
|
|
@@ -883,9 +944,15 @@ function parseCfTargetOutput(stdout) {
|
|
|
883
944
|
if (!api || !org || !space) {
|
|
884
945
|
return void 0;
|
|
885
946
|
}
|
|
886
|
-
|
|
947
|
+
let normalizedApi;
|
|
948
|
+
try {
|
|
949
|
+
normalizedApi = normalizeSapCfApiEndpoint(api);
|
|
950
|
+
} catch {
|
|
951
|
+
return void 0;
|
|
952
|
+
}
|
|
953
|
+
const regionKey = getRegionKeyForApi(normalizedApi);
|
|
887
954
|
return {
|
|
888
|
-
apiEndpoint:
|
|
955
|
+
apiEndpoint: normalizedApi,
|
|
889
956
|
orgName: org,
|
|
890
957
|
spaceName: space,
|
|
891
958
|
...regionKey ? { regionKey } : {}
|
|
@@ -911,12 +978,8 @@ function formatCurrentCfAppSelector(target, appName) {
|
|
|
911
978
|
if (!name) {
|
|
912
979
|
throw new Error("App name is required.");
|
|
913
980
|
}
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
`Current CF API endpoint "${target.apiEndpoint}" does not match a known SAP region. Pass a full region/org/space/app selector.`
|
|
917
|
-
);
|
|
918
|
-
}
|
|
919
|
-
return `${target.regionKey}/${target.orgName}/${target.spaceName}/${name}`;
|
|
981
|
+
const region = target.regionKey ?? "current";
|
|
982
|
+
return `${region}/${target.orgName}/${target.spaceName}/${name}`;
|
|
920
983
|
}
|
|
921
984
|
function extractVcapSection(stdout) {
|
|
922
985
|
const start = stdout.indexOf("VCAP_SERVICES:");
|
|
@@ -996,6 +1059,9 @@ function classifyCfError(stderr = "", stdout = "") {
|
|
|
996
1059
|
}
|
|
997
1060
|
|
|
998
1061
|
// src/credentials.ts
|
|
1062
|
+
function errorMessageFromUnknown(error) {
|
|
1063
|
+
return error instanceof Error ? error.message : "Invalid or untrusted CF API endpoint.";
|
|
1064
|
+
}
|
|
999
1065
|
async function resolveAppBindings(rawSelector, options) {
|
|
1000
1066
|
const selector = rawSelector.trim();
|
|
1001
1067
|
if (!selector) {
|
|
@@ -1014,7 +1080,7 @@ async function resolveAppBindings(rawSelector, options) {
|
|
|
1014
1080
|
const displaySelector = current.regionKey ? formatCurrentCfAppSelector(current, selector) : `current/${current.orgName}/${current.spaceName}/${selector}`;
|
|
1015
1081
|
target = {
|
|
1016
1082
|
selector: displaySelector,
|
|
1017
|
-
apiEndpoint: current.
|
|
1083
|
+
apiEndpoint: current.apiEndpoint,
|
|
1018
1084
|
orgName: current.orgName,
|
|
1019
1085
|
spaceName: current.spaceName,
|
|
1020
1086
|
appName: selector
|
|
@@ -1032,10 +1098,16 @@ async function resolveAppBindings(rawSelector, options) {
|
|
|
1032
1098
|
if (!apiEndpoint) {
|
|
1033
1099
|
throw new CfHanaError(
|
|
1034
1100
|
"CONFIG",
|
|
1035
|
-
`Unknown region
|
|
1101
|
+
`Unknown SAP CF region "${regionKey}". Verify the current SAP region list or use the current CF target.`
|
|
1036
1102
|
);
|
|
1037
1103
|
}
|
|
1038
|
-
|
|
1104
|
+
let normalizedApiEndpoint;
|
|
1105
|
+
try {
|
|
1106
|
+
normalizedApiEndpoint = normalizeSapCfApiEndpoint(apiEndpoint);
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
throw new CfHanaError("CONFIG", errorMessageFromUnknown(error), { cause: error });
|
|
1109
|
+
}
|
|
1110
|
+
target = { selector, apiEndpoint: normalizedApiEndpoint, orgName, spaceName, appName };
|
|
1039
1111
|
}
|
|
1040
1112
|
let bindings;
|
|
1041
1113
|
const source = "live";
|
|
@@ -1059,9 +1131,6 @@ Run "cf login" + "cf target", or provide SAP_EMAIL + SAP_PASSWORD.`,
|
|
|
1059
1131
|
{ cause: directError }
|
|
1060
1132
|
);
|
|
1061
1133
|
}
|
|
1062
|
-
if (!target.apiEndpoint) {
|
|
1063
|
-
throw new CfHanaError("CONFIG", "Cannot determine API endpoint for fallback auth.");
|
|
1064
|
-
}
|
|
1065
1134
|
const api = target.apiEndpoint;
|
|
1066
1135
|
bindings = await withCfSession(async (ctx) => {
|
|
1067
1136
|
await cfApi(api, ctx);
|
|
@@ -1085,9 +1154,6 @@ Run "cf login" + "cf target", or provide SAP_EMAIL + SAP_PASSWORD.`,
|
|
|
1085
1154
|
`SAP_EMAIL and SAP_PASSWORD are required to fetch HANA bindings for explicit selector "${selector}".`
|
|
1086
1155
|
);
|
|
1087
1156
|
}
|
|
1088
|
-
if (!target.apiEndpoint) {
|
|
1089
|
-
throw new CfHanaError("CONFIG", "Invalid explicit selector.");
|
|
1090
|
-
}
|
|
1091
1157
|
const api = target.apiEndpoint;
|
|
1092
1158
|
bindings = await withCfSession(async (ctx) => {
|
|
1093
1159
|
await cfApi(api, ctx);
|
|
@@ -1803,6 +1869,57 @@ function boundedRows(rows, requestedLimit) {
|
|
|
1803
1869
|
truncated: rows.length > requestedLimit
|
|
1804
1870
|
};
|
|
1805
1871
|
}
|
|
1872
|
+
function isBooleanColumn(column) {
|
|
1873
|
+
return column.typeName.trim().toUpperCase() === "BOOLEAN";
|
|
1874
|
+
}
|
|
1875
|
+
function normalizeBooleanCell(value) {
|
|
1876
|
+
if (value === 0) {
|
|
1877
|
+
return false;
|
|
1878
|
+
}
|
|
1879
|
+
if (value === 1) {
|
|
1880
|
+
return true;
|
|
1881
|
+
}
|
|
1882
|
+
if (typeof value !== "string") {
|
|
1883
|
+
return value;
|
|
1884
|
+
}
|
|
1885
|
+
const normalized = value.trim().toLowerCase();
|
|
1886
|
+
if (normalized === "0") {
|
|
1887
|
+
return false;
|
|
1888
|
+
}
|
|
1889
|
+
if (normalized === "1") {
|
|
1890
|
+
return true;
|
|
1891
|
+
}
|
|
1892
|
+
if (normalized === "false") {
|
|
1893
|
+
return false;
|
|
1894
|
+
}
|
|
1895
|
+
if (normalized === "true") {
|
|
1896
|
+
return true;
|
|
1897
|
+
}
|
|
1898
|
+
return value;
|
|
1899
|
+
}
|
|
1900
|
+
function normalizeBooleanRow(row, columns) {
|
|
1901
|
+
let normalizedRow;
|
|
1902
|
+
for (const column of columns) {
|
|
1903
|
+
const value = row[column.name];
|
|
1904
|
+
if (value === void 0) {
|
|
1905
|
+
continue;
|
|
1906
|
+
}
|
|
1907
|
+
const normalizedValue = normalizeBooleanCell(value);
|
|
1908
|
+
if (normalizedValue === value) {
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
normalizedRow ??= { ...row };
|
|
1912
|
+
normalizedRow[column.name] = normalizedValue;
|
|
1913
|
+
}
|
|
1914
|
+
return normalizedRow ?? row;
|
|
1915
|
+
}
|
|
1916
|
+
function normalizeBooleanRows(rows, columns) {
|
|
1917
|
+
const booleanColumns = columns.filter(isBooleanColumn);
|
|
1918
|
+
if (booleanColumns.length === 0 || rows.length === 0) {
|
|
1919
|
+
return rows;
|
|
1920
|
+
}
|
|
1921
|
+
return rows.map((row) => normalizeBooleanRow(row, booleanColumns));
|
|
1922
|
+
}
|
|
1806
1923
|
async function withTimeout(work, timeoutMs, onTimeout) {
|
|
1807
1924
|
let timer;
|
|
1808
1925
|
const timeout = new Promise((_resolve, reject) => {
|
|
@@ -1888,7 +2005,8 @@ var Connection = class _Connection {
|
|
|
1888
2005
|
}
|
|
1889
2006
|
);
|
|
1890
2007
|
const elapsedMs = Date.now() - started;
|
|
1891
|
-
const
|
|
2008
|
+
const normalizedRows = normalizeBooleanRows(execResult.rows, execResult.columns);
|
|
2009
|
+
const selected = boundedRows(normalizedRows, limited.requestedLimit);
|
|
1892
2010
|
return {
|
|
1893
2011
|
rows: selected.rows,
|
|
1894
2012
|
columns: execResult.columns,
|
|
@@ -3529,25 +3647,6 @@ function parseQualifiedName(value) {
|
|
|
3529
3647
|
}
|
|
3530
3648
|
return { schema: value.slice(0, dot), table: value.slice(dot + 1) };
|
|
3531
3649
|
}
|
|
3532
|
-
async function resolveSelectorArgument(selector) {
|
|
3533
|
-
if (selector.includes("/")) {
|
|
3534
|
-
return selector;
|
|
3535
|
-
}
|
|
3536
|
-
const current = await readCurrentCfTarget().catch((error) => {
|
|
3537
|
-
throw new CfHanaError(
|
|
3538
|
-
"CONFIG",
|
|
3539
|
-
"No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector.",
|
|
3540
|
-
{ cause: error }
|
|
3541
|
-
);
|
|
3542
|
-
});
|
|
3543
|
-
if (current === void 0) {
|
|
3544
|
-
throw new CfHanaError(
|
|
3545
|
-
"CONFIG",
|
|
3546
|
-
"No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector."
|
|
3547
|
-
);
|
|
3548
|
-
}
|
|
3549
|
-
return formatCurrentCfAppSelector(current, selector);
|
|
3550
|
-
}
|
|
3551
3650
|
function toConnectOptions(opts) {
|
|
3552
3651
|
assertPositiveOption("--limit", opts.limit);
|
|
3553
3652
|
assertPositiveOption("--timeout", opts.timeout);
|
|
@@ -3602,7 +3701,7 @@ function formatInfo(info) {
|
|
|
3602
3701
|
].join("\n");
|
|
3603
3702
|
}
|
|
3604
3703
|
function withConnectionOptions(command) {
|
|
3605
|
-
return command.option("--refresh", "
|
|
3704
|
+
return command.option("--refresh", "deprecated compatibility flag; binding discovery is already live", false).option("--role <role>", "HANA user role: runtime or hdi", "runtime").option("--binding <name>", "select a HANA binding by service name").option("--binding-index <n>", "select a HANA binding by index", parseIntOption2).option("--read-only", "block every DML and DDL statement", false).option("--allow-destructive", "permit destructive statements", false).option("--timeout <ms>", "connection and query timeout in milliseconds", parseIntOption2).option("--limit <n>", "row cap auto-applied to bare SELECT statements", parseIntOption2).option("--no-auto-limit", "disable the automatic SELECT row cap").option("--refresh-metadata", "bypass the 30-minute table/view metadata suggestion cache", false);
|
|
3606
3705
|
}
|
|
3607
3706
|
function withFormattedConnectionOptions(command) {
|
|
3608
3707
|
return withConnectionOptions(command).option(
|
|
@@ -3690,14 +3789,14 @@ async function runQuery(selector, sql, command) {
|
|
|
3690
3789
|
const opts = command.opts();
|
|
3691
3790
|
assertQueryOptions(sql, opts);
|
|
3692
3791
|
const cellLimit = resolveCellLimit(opts.cellLimit);
|
|
3693
|
-
const client = await connect(
|
|
3792
|
+
const client = await connect(selector, toConnectOptions(opts));
|
|
3694
3793
|
try {
|
|
3695
3794
|
const params = opts.param ?? [];
|
|
3696
3795
|
let backup;
|
|
3697
3796
|
try {
|
|
3698
3797
|
backup = await client.backupWriteStatement(sql, params);
|
|
3699
3798
|
} catch (error) {
|
|
3700
|
-
await enrichAndRethrowQueryError(error, client, sql, opts.refreshMetadata
|
|
3799
|
+
await enrichAndRethrowQueryError(error, client, sql, opts.refreshMetadata);
|
|
3701
3800
|
}
|
|
3702
3801
|
if (backup !== void 0) {
|
|
3703
3802
|
process.stderr.write(`${CLI_NAME}: backup saved to ${backup.directory}
|
|
@@ -3708,7 +3807,7 @@ async function runQuery(selector, sql, command) {
|
|
|
3708
3807
|
error,
|
|
3709
3808
|
client,
|
|
3710
3809
|
sql,
|
|
3711
|
-
opts.refreshMetadata
|
|
3810
|
+
opts.refreshMetadata
|
|
3712
3811
|
);
|
|
3713
3812
|
});
|
|
3714
3813
|
if (result.statement === "select") {
|
|
@@ -3741,7 +3840,7 @@ async function runQuery(selector, sql, command) {
|
|
|
3741
3840
|
}
|
|
3742
3841
|
async function runTables(selector, schema, command) {
|
|
3743
3842
|
const opts = command.opts();
|
|
3744
|
-
const client = await connect(
|
|
3843
|
+
const client = await connect(selector, toConnectOptions(opts));
|
|
3745
3844
|
try {
|
|
3746
3845
|
const tables = await client.listTables(schema ?? client.info.schema);
|
|
3747
3846
|
const rows = tables.map((table) => ({
|
|
@@ -3757,7 +3856,7 @@ async function runTables(selector, schema, command) {
|
|
|
3757
3856
|
async function runColumns(selector, target, command) {
|
|
3758
3857
|
const opts = command.opts();
|
|
3759
3858
|
const { schema, table } = parseQualifiedName(target);
|
|
3760
|
-
const client = await connect(
|
|
3859
|
+
const client = await connect(selector, toConnectOptions(opts));
|
|
3761
3860
|
try {
|
|
3762
3861
|
const columns = await client.listColumns(schema, table);
|
|
3763
3862
|
const rows = columns.map((column) => ({
|
|
@@ -3775,7 +3874,7 @@ async function runColumns(selector, target, command) {
|
|
|
3775
3874
|
async function runCount(selector, target, command) {
|
|
3776
3875
|
const opts = command.opts();
|
|
3777
3876
|
const { schema, table } = parseQualifiedName(target);
|
|
3778
|
-
const client = await connect(
|
|
3877
|
+
const client = await connect(selector, toConnectOptions(opts));
|
|
3779
3878
|
try {
|
|
3780
3879
|
const total = await client.count({ schema, table });
|
|
3781
3880
|
print2(String(total));
|
|
@@ -3785,7 +3884,7 @@ async function runCount(selector, target, command) {
|
|
|
3785
3884
|
}
|
|
3786
3885
|
async function runPing(selector, command) {
|
|
3787
3886
|
const opts = command.opts();
|
|
3788
|
-
const client = await connect(
|
|
3887
|
+
const client = await connect(selector, toConnectOptions(opts));
|
|
3789
3888
|
try {
|
|
3790
3889
|
const started = Date.now();
|
|
3791
3890
|
await client.query("SELECT 1 FROM DUMMY");
|
|
@@ -3798,7 +3897,7 @@ async function runPing(selector, command) {
|
|
|
3798
3897
|
}
|
|
3799
3898
|
async function runInfo(selector, command) {
|
|
3800
3899
|
const opts = command.opts();
|
|
3801
|
-
const client = await connect(
|
|
3900
|
+
const client = await connect(selector, toConnectOptions(opts));
|
|
3802
3901
|
try {
|
|
3803
3902
|
print2(formatInfo(client.info));
|
|
3804
3903
|
} finally {
|