@saptools/cf-hana 0.2.0 → 0.2.2

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/cli.js CHANGED
@@ -4,7 +4,6 @@
4
4
  import { Command } from "commander";
5
5
 
6
6
  // src/backup.ts
7
- import { createHash, randomUUID } from "crypto";
8
7
  import { mkdir, writeFile } from "fs/promises";
9
8
  import { homedir } from "os";
10
9
  import { join } from "path";
@@ -374,6 +373,28 @@ function findTopLevelKeyword(sql, keyword, startIndex = 0) {
374
373
  }
375
374
  return void 0;
376
375
  }
376
+ function findTopLevelChar(sql, target, startIndex, endIndex) {
377
+ let index = startIndex;
378
+ let depth = 0;
379
+ while (index < endIndex) {
380
+ const skipped = skipNonCode(sql, index);
381
+ if (skipped !== void 0) {
382
+ index = skipped;
383
+ continue;
384
+ }
385
+ const char = sql[index];
386
+ if (char === target && depth === 0) {
387
+ return index;
388
+ }
389
+ if (char === "(") {
390
+ depth += 1;
391
+ } else if (char === ")" && depth > 0) {
392
+ depth -= 1;
393
+ }
394
+ index += 1;
395
+ }
396
+ return void 0;
397
+ }
377
398
  function selectParamsAfterWhere(statementSql, whereIndex, params) {
378
399
  return params.slice(countPlaceholders(statementSql.slice(0, whereIndex)));
379
400
  }
@@ -411,6 +432,24 @@ function buildUpdateBackupPlan(statementSql, params) {
411
432
  params
412
433
  );
413
434
  }
435
+ function buildUpsertBackupPlan(statementSql, params) {
436
+ const upsertIndex = findTopLevelKeyword(statementSql, "UPSERT");
437
+ const valuesIndex = upsertIndex === void 0 ? void 0 : findTopLevelKeyword(statementSql, "VALUES", upsertIndex + "UPSERT".length);
438
+ if (upsertIndex === void 0 || valuesIndex === void 0) {
439
+ throw new QueryError("UPSERT backup requires UPSERT <target> VALUES syntax");
440
+ }
441
+ const targetStart = upsertIndex + "UPSERT".length;
442
+ const columnListIndex = findTopLevelChar(statementSql, "(", targetStart, valuesIndex);
443
+ const targetEnd = columnListIndex ?? valuesIndex;
444
+ const whereIndex = findTopLevelKeyword(statementSql, "WHERE", valuesIndex + "VALUES".length);
445
+ return buildSelectPlan(
446
+ "upsert",
447
+ statementSql,
448
+ statementSql.slice(targetStart, targetEnd),
449
+ whereIndex,
450
+ params
451
+ );
452
+ }
414
453
  function buildDeleteBackupPlan(statementSql, params) {
415
454
  const deleteIndex = findTopLevelKeyword(statementSql, "DELETE");
416
455
  const fromIndex = deleteIndex === void 0 ? void 0 : findTopLevelKeyword(statementSql, "FROM", deleteIndex + "DELETE".length);
@@ -430,8 +469,18 @@ function buildDeleteBackupPlan(statementSql, params) {
430
469
  function backupTimestamp(now) {
431
470
  return now.toISOString().replace(/:/g, "").replace(".", "");
432
471
  }
433
- function backupHash(statementSql) {
434
- return createHash("sha256").update(statementSql).update("\0").update(randomUUID()).digest("hex").slice(0, 12);
472
+ function backupMonth(now) {
473
+ const year = String(now.getUTCFullYear());
474
+ const month = String(now.getUTCMonth() + 1).padStart(2, "0");
475
+ return `${year}${month}`;
476
+ }
477
+ function sanitizePathPart(value) {
478
+ const trimmed = value?.trim() ?? "";
479
+ const normalized = trimmed.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
480
+ return normalized.length > 0 ? normalized : "unknown-target";
481
+ }
482
+ function backupBaseName(input, now) {
483
+ return [sanitizePathPart(input.selector), input.operation, backupTimestamp(now)].join("-");
435
484
  }
436
485
  function cfHanaBackupRoot(saptoolsRoot) {
437
486
  return join(saptoolsRoot ?? defaultSaptoolsRoot(), CF_HANA_DIR_NAME, BACKUPS_DIR_NAME);
@@ -439,33 +488,49 @@ function cfHanaBackupRoot(saptoolsRoot) {
439
488
  function buildWriteBackupPlan(sql, params = []) {
440
489
  const statementSql = trimStatementSql(sql);
441
490
  const keyword = firstKeyword(statementSql);
442
- if (keyword !== "UPDATE" && keyword !== "DELETE") {
491
+ if (keyword !== "UPDATE" && keyword !== "UPSERT" && keyword !== "DELETE") {
443
492
  return void 0;
444
493
  }
445
494
  assertParamArity(statementSql, params);
446
495
  if (keyword === "UPDATE") {
447
496
  return buildUpdateBackupPlan(statementSql, params);
448
497
  }
498
+ if (keyword === "UPSERT") {
499
+ return buildUpsertBackupPlan(statementSql, params);
500
+ }
449
501
  return buildDeleteBackupPlan(statementSql, params);
450
502
  }
451
503
  async function writeSqlBackup(input, options = {}) {
452
504
  const now = options.now ?? /* @__PURE__ */ new Date();
453
- const directory = join(
454
- cfHanaBackupRoot(options.saptoolsRoot),
455
- `${backupTimestamp(now)}-${input.operation}-${backupHash(input.statementSql)}`
456
- );
457
- const statementPath = join(directory, "statement.sql");
458
- const backupPath = join(directory, "backup.csv");
505
+ const directory = join(cfHanaBackupRoot(options.saptoolsRoot), backupMonth(now));
506
+ const baseName = backupBaseName(input, now);
507
+ const statementPath = join(directory, `${baseName}.statement.sql`);
508
+ const backupPath = join(directory, `${baseName}.sql`);
509
+ const metadataPath = join(directory, `${baseName}.json`);
510
+ const metadata = {
511
+ selector: input.selector ?? null,
512
+ operation: input.operation,
513
+ statementPath,
514
+ backupPath,
515
+ rowCount: input.result.rowCount,
516
+ createdAt: now.toISOString()
517
+ };
459
518
  await mkdir(directory, { recursive: true, mode: 448 });
460
519
  await Promise.all([
461
520
  writeFile(statementPath, `${input.statementSql}
462
521
  `, { encoding: "utf8", mode: 384 }),
463
- writeFile(backupPath, formatCsv(input.result), { encoding: "utf8", mode: 384 })
522
+ writeFile(backupPath, formatCsv(input.result), { encoding: "utf8", mode: 384 }),
523
+ writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}
524
+ `, {
525
+ encoding: "utf8",
526
+ mode: 384
527
+ })
464
528
  ]);
465
529
  return {
466
530
  directory,
467
531
  statementPath,
468
532
  backupPath,
533
+ metadataPath,
469
534
  rowCount: input.result.rowCount
470
535
  };
471
536
  }
@@ -587,7 +652,7 @@ async function listColumns(connection, schema, table) {
587
652
 
588
653
  // src/config.ts
589
654
  var CLI_NAME = "cf-hana";
590
- var CLI_VERSION = "0.2.0";
655
+ var CLI_VERSION = "0.2.1";
591
656
  var ENV_PREFIX = "CF_HANA";
592
657
  var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
593
658
  var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
@@ -619,44 +684,381 @@ function readSapCredentials(overrides) {
619
684
  return { email, password };
620
685
  }
621
686
 
622
- // src/credentials.ts
623
- import { fetchAppDbBindings, readDbAppView } from "@saptools/cf-sync";
624
- async function resolveAppBindings(selector, options) {
625
- if (options.refresh !== true) {
626
- const view = await readDbAppView(selector);
627
- if (view !== void 0 && view.entry.bindings.length > 0) {
628
- return {
629
- selector: view.entry.selector,
630
- appName: view.entry.appName,
631
- bindings: view.entry.bindings,
632
- source: "cache"
633
- };
687
+ // src/cf.ts
688
+ import { execFile } from "child_process";
689
+ import { mkdtemp, rm } from "fs/promises";
690
+ import { tmpdir } from "os";
691
+ import { join as join2 } from "path";
692
+ import { promisify } from "util";
693
+ var execFileAsync = promisify(execFile);
694
+ var MAX_BUFFER = 16 * 1024 * 1024;
695
+ var DEFAULT_TIMEOUT_MS = 3e4;
696
+ var CF_RETRY_ATTEMPTS = 3;
697
+ var CF_RETRY_BASE_DELAY_MS = 120;
698
+ var REGION_API_MAP = {
699
+ ae01: "https://api.cf.ae01.hana.ondemand.com",
700
+ ap01: "https://api.cf.ap01.hana.ondemand.com",
701
+ ap10: "https://api.cf.ap10.hana.ondemand.com",
702
+ ap11: "https://api.cf.ap11.hana.ondemand.com",
703
+ ap12: "https://api.cf.ap12.hana.ondemand.com",
704
+ ap20: "https://api.cf.ap20.hana.ondemand.com",
705
+ ap21: "https://api.cf.ap21.hana.ondemand.com",
706
+ ap30: "https://api.cf.ap30.hana.ondemand.com",
707
+ br10: "https://api.cf.br10.hana.ondemand.com",
708
+ br20: "https://api.cf.br20.hana.ondemand.com",
709
+ br30: "https://api.cf.br30.hana.ondemand.com",
710
+ ca10: "https://api.cf.ca10.hana.ondemand.com",
711
+ ca20: "https://api.cf.ca20.hana.ondemand.com",
712
+ ch20: "https://api.cf.ch20.hana.ondemand.com",
713
+ eu10: "https://api.cf.eu10.hana.ondemand.com",
714
+ eu11: "https://api.cf.eu11.hana.ondemand.com",
715
+ eu12: "https://api.cf.eu12.hana.ondemand.com",
716
+ eu20: "https://api.cf.eu20.hana.ondemand.com",
717
+ eu21: "https://api.cf.eu21.hana.ondemand.com",
718
+ eu30: "https://api.cf.eu30.hana.ondemand.com",
719
+ "eu10-002": "https://api.cf.eu10-002.hana.ondemand.com",
720
+ jp10: "https://api.cf.jp10.hana.ondemand.com",
721
+ jp20: "https://api.cf.jp20.hana.ondemand.com",
722
+ jp30: "https://api.cf.jp30.hana.ondemand.com",
723
+ us10: "https://api.cf.us10.hana.ondemand.com",
724
+ us11: "https://api.cf.us11.hana.ondemand.com",
725
+ us20: "https://api.cf.us20.hana.ondemand.com",
726
+ us21: "https://api.cf.us21.hana.ondemand.com",
727
+ us30: "https://api.cf.us30.hana.ondemand.com",
728
+ in30: "https://api.cf.in30.hana.ondemand.com"
729
+ };
730
+ function getApiEndpointForRegion(regionKey) {
731
+ return REGION_API_MAP[regionKey];
732
+ }
733
+ function getRegionKeyForApi(apiEndpoint) {
734
+ const norm = apiEndpoint.trim().replace(/\/+$/, "").toLowerCase();
735
+ for (const [key, endpoint] of Object.entries(REGION_API_MAP)) {
736
+ if (endpoint.toLowerCase() === norm) {
737
+ return key;
738
+ }
739
+ }
740
+ return void 0;
741
+ }
742
+ async function withCfSession(work) {
743
+ const cfHome = await mkdtemp(join2(tmpdir(), "saptools-cf-hana-"));
744
+ const ctx = { cfHome };
745
+ try {
746
+ return await work(ctx);
747
+ } finally {
748
+ await rm(cfHome, { recursive: true, force: true });
749
+ }
750
+ }
751
+ function resolveCfBin() {
752
+ const raw = process.env["CF_HANA_CF_BIN"] ?? "cf";
753
+ if (/\.(?:c|m)?js$/i.test(raw)) {
754
+ return { bin: process.execPath, argsPrefix: [raw] };
755
+ }
756
+ return { bin: raw, argsPrefix: [] };
757
+ }
758
+ function buildEnv(ctx, overrides = {}) {
759
+ const env = { ...process.env, ...overrides };
760
+ delete env["SAP_EMAIL"];
761
+ delete env["SAP_PASSWORD"];
762
+ env["CF_HOME"] = ctx.cfHome;
763
+ return env;
764
+ }
765
+ async function runCf(args, ctx, overrides = {}) {
766
+ const { bin, argsPrefix } = resolveCfBin();
767
+ const env = buildEnv(ctx, overrides);
768
+ let lastErr;
769
+ for (let attempt = 0; attempt < CF_RETRY_ATTEMPTS; attempt++) {
770
+ try {
771
+ const { stdout } = await execFileAsync(bin, [...argsPrefix, ...args], {
772
+ env,
773
+ maxBuffer: MAX_BUFFER,
774
+ timeout: DEFAULT_TIMEOUT_MS
775
+ });
776
+ return stdout;
777
+ } catch (err) {
778
+ lastErr = err;
779
+ if (attempt < CF_RETRY_ATTEMPTS - 1) {
780
+ await new Promise((r) => setTimeout(r, CF_RETRY_BASE_DELAY_MS * (attempt + 1)));
781
+ }
634
782
  }
635
783
  }
636
- const credentials = readSapCredentials({
637
- email: options.email,
638
- password: options.password
784
+ const e = lastErr;
785
+ const detail = e?.stderr ? String(e.stderr) : e?.message ?? "";
786
+ const cmd = args[0] === "auth" ? "cf auth" : `cf ${args.join(" ")}`;
787
+ throw new Error(`${cmd} failed: ${detail}`.trim(), { cause: lastErr });
788
+ }
789
+ async function cfApi(apiEndpoint, ctx) {
790
+ await runCf(["api", apiEndpoint], ctx);
791
+ }
792
+ async function cfAuth(email, password, ctx) {
793
+ await runCf(["auth"], ctx, {
794
+ CF_USERNAME: email,
795
+ CF_PASSWORD: password
639
796
  });
640
- if (credentials === void 0) {
641
- throw new CredentialsNotFoundError(
642
- `No cached HANA credentials for "${selector}". Run \`cf-sync db-sync ${selector}\` first, or set SAP_EMAIL and SAP_PASSWORD to fetch them live.`
797
+ }
798
+ async function cfTargetSpace(orgName, spaceName, ctx) {
799
+ await runCf(["target", "-o", orgName, "-s", spaceName], ctx);
800
+ }
801
+ async function cfEnv(appName, ctx) {
802
+ return await runCf(["env", appName], ctx);
803
+ }
804
+ async function cfEnvDirect(appName) {
805
+ const { bin, argsPrefix } = resolveCfBin();
806
+ const env = { ...process.env };
807
+ delete env["SAP_EMAIL"];
808
+ delete env["SAP_PASSWORD"];
809
+ const { stdout } = await execFileAsync(bin, [...argsPrefix, "env", appName], {
810
+ env,
811
+ maxBuffer: MAX_BUFFER,
812
+ timeout: DEFAULT_TIMEOUT_MS
813
+ });
814
+ return stdout;
815
+ }
816
+ async function readCurrentCfTarget() {
817
+ const { bin, argsPrefix } = resolveCfBin();
818
+ const env = { ...process.env };
819
+ delete env["SAP_EMAIL"];
820
+ delete env["SAP_PASSWORD"];
821
+ try {
822
+ const { stdout } = await execFileAsync(bin, [...argsPrefix, "target"], {
823
+ env,
824
+ maxBuffer: MAX_BUFFER,
825
+ timeout: 1e4
826
+ });
827
+ return parseCfTargetOutput(stdout);
828
+ } catch {
829
+ return void 0;
830
+ }
831
+ }
832
+ function parseCfTargetOutput(stdout) {
833
+ const fields = parseTargetFields(stdout);
834
+ const api = fields.get("api endpoint");
835
+ const org = fields.get("org");
836
+ const space = fields.get("space");
837
+ if (!api || !org || !space) {
838
+ return void 0;
839
+ }
840
+ const regionKey = getRegionKeyForApi(api);
841
+ return {
842
+ apiEndpoint: api,
843
+ orgName: org,
844
+ spaceName: space,
845
+ ...regionKey ? { regionKey } : {}
846
+ };
847
+ }
848
+ function parseTargetFields(stdout) {
849
+ const map = /* @__PURE__ */ new Map();
850
+ for (const line of stdout.split(/\r?\n/)) {
851
+ const idx = line.indexOf(":");
852
+ if (idx < 0) {
853
+ continue;
854
+ }
855
+ const key = line.slice(0, idx).trim().toLowerCase();
856
+ const val = line.slice(idx + 1).trim();
857
+ if (key && val) {
858
+ map.set(key, val);
859
+ }
860
+ }
861
+ return map;
862
+ }
863
+ function formatCurrentCfAppSelector(target, appName) {
864
+ const name = appName.trim();
865
+ if (!name) {
866
+ throw new Error("App name is required.");
867
+ }
868
+ if (!target.regionKey) {
869
+ throw new Error(
870
+ `Current CF API endpoint "${target.apiEndpoint}" does not match a known SAP region. Pass a full region/org/space/app selector.`
643
871
  );
644
872
  }
645
- const fetched = await fetchAppDbBindings({
646
- selector,
647
- email: credentials.email,
648
- password: credentials.password
873
+ return `${target.regionKey}/${target.orgName}/${target.spaceName}/${name}`;
874
+ }
875
+ function extractVcapSection(stdout) {
876
+ const start = stdout.indexOf("VCAP_SERVICES:");
877
+ if (start === -1) {
878
+ throw new Error("VCAP_SERVICES section not found in cf env output");
879
+ }
880
+ const after = stdout.slice(start + "VCAP_SERVICES:".length);
881
+ const end = after.indexOf("VCAP_APPLICATION:");
882
+ const block = end === -1 ? after : after.slice(0, end);
883
+ return block.trim();
884
+ }
885
+ function isRecord(v) {
886
+ return typeof v === "object" && v !== null && !Array.isArray(v);
887
+ }
888
+ function assertCreds(raw) {
889
+ if (!isRecord(raw)) {
890
+ throw new Error("HANA credentials must be an object");
891
+ }
892
+ const req = ["host", "port", "user", "password", "schema", "hdi_user", "hdi_password", "url", "database_id", "certificate"];
893
+ for (const k of req) {
894
+ if (typeof raw[k] !== "string") {
895
+ throw new Error(`Missing/invalid HANA credential field: "${k}"`);
896
+ }
897
+ }
898
+ return raw;
899
+ }
900
+ function mapCreds(raw) {
901
+ return {
902
+ host: raw.host,
903
+ port: raw.port,
904
+ user: raw.user,
905
+ password: raw.password,
906
+ schema: raw.schema,
907
+ hdiUser: raw.hdi_user,
908
+ hdiPassword: raw.hdi_password,
909
+ ...raw.url ? { url: raw.url } : {},
910
+ databaseId: raw.database_id,
911
+ certificate: raw.certificate
912
+ };
913
+ }
914
+ function extractHanaBindingsFromCfEnv(stdout) {
915
+ const jsonText = extractVcapSection(stdout);
916
+ let vcap;
917
+ try {
918
+ vcap = JSON.parse(jsonText);
919
+ } catch {
920
+ throw new Error("VCAP_SERVICES is not valid JSON");
921
+ }
922
+ if (!isRecord(vcap)) {
923
+ throw new Error("VCAP_SERVICES must be an object");
924
+ }
925
+ const hana = vcap["hana"];
926
+ if (hana === void 0) {
927
+ return [];
928
+ }
929
+ if (!Array.isArray(hana)) {
930
+ throw new Error("VCAP_SERVICES.hana must be an array when present");
931
+ }
932
+ return hana.map((b) => {
933
+ if (!isRecord(b)) {
934
+ throw new Error("HANA binding must be an object");
935
+ }
936
+ const credsRaw = assertCreds(b["credentials"]);
937
+ const name = typeof b["name"] === "string" ? b["name"] : void 0;
938
+ return {
939
+ ...name ? { name } : {},
940
+ credentials: mapCreds(credsRaw)
941
+ };
649
942
  });
650
- if (fetched.bindings.length === 0) {
651
- throw new CredentialsNotFoundError(
652
- `App "${fetched.selector}" has no HANA service binding.`
653
- );
943
+ }
944
+ function classifyCfError(stderr = "", stdout = "") {
945
+ const text = `${stderr} ${stdout}`.toLowerCase();
946
+ if (text.includes("not logged in") || text.includes("unauthorized") || text.includes("forbidden") || text.includes("credentials were rejected") || text.includes("authentication failed") || text.includes("cf auth") || text.includes("token") || text.includes("expired") || text.includes("session")) {
947
+ return { isAuthError: true, reason: "auth/session issue" };
948
+ }
949
+ return { isAuthError: false, reason: "" };
950
+ }
951
+
952
+ // src/credentials.ts
953
+ async function resolveAppBindings(rawSelector, options) {
954
+ const selector = rawSelector.trim();
955
+ if (!selector) {
956
+ throw new CfHanaError("CONFIG", "App selector is required");
957
+ }
958
+ let target;
959
+ const isBare = !selector.includes("/");
960
+ if (isBare) {
961
+ const current = await readCurrentCfTarget();
962
+ if (!current) {
963
+ throw new CfHanaError(
964
+ "CONFIG",
965
+ "No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector."
966
+ );
967
+ }
968
+ const displaySelector = current.regionKey ? formatCurrentCfAppSelector(current, selector) : `current/${current.orgName}/${current.spaceName}/${selector}`;
969
+ target = {
970
+ selector: displaySelector,
971
+ apiEndpoint: current.regionKey ? getApiEndpointForRegion(current.regionKey) : void 0,
972
+ orgName: current.orgName,
973
+ spaceName: current.spaceName,
974
+ appName: selector
975
+ };
976
+ } else {
977
+ const parts = selector.split("/").map((p) => p.trim());
978
+ if (parts.length !== 4 || !parts[0] || !parts[1] || !parts[2] || !parts[3]) {
979
+ throw new CfHanaError(
980
+ "CONFIG",
981
+ `Invalid selector "${selector}". Use region/org/space/app or a bare app name.`
982
+ );
983
+ }
984
+ const [regionKey, orgName, spaceName, appName] = parts;
985
+ const apiEndpoint = getApiEndpointForRegion(regionKey);
986
+ if (!apiEndpoint) {
987
+ throw new CfHanaError(
988
+ "CONFIG",
989
+ `Unknown region key "${regionKey}". Use a known region or the current CF target.`
990
+ );
991
+ }
992
+ target = { selector, apiEndpoint, orgName, spaceName, appName };
993
+ }
994
+ let bindings;
995
+ const source = "live";
996
+ if (isBare) {
997
+ try {
998
+ const stdout = await cfEnvDirect(target.appName);
999
+ bindings = extractHanaBindingsFromCfEnv(stdout);
1000
+ } catch (directError) {
1001
+ let stderr = "";
1002
+ if (directError && typeof directError === "object") {
1003
+ const e = directError;
1004
+ stderr = (typeof e.stderr === "string" ? e.stderr : "") || (typeof e.message === "string" ? e.message : "");
1005
+ }
1006
+ const classified = classifyCfError(stderr);
1007
+ if (classified.isAuthError) {
1008
+ const sap = readSapCredentials({ email: options.email, password: options.password });
1009
+ if (!sap) {
1010
+ throw new CredentialsNotFoundError(
1011
+ `Current CF session problem for bare app "${target.appName}" (${classified.reason}).
1012
+ Run "cf login" + "cf target", or provide SAP_EMAIL + SAP_PASSWORD.`,
1013
+ { cause: directError }
1014
+ );
1015
+ }
1016
+ if (!target.apiEndpoint) {
1017
+ throw new CfHanaError("CONFIG", "Cannot determine API endpoint for fallback auth.");
1018
+ }
1019
+ const api = target.apiEndpoint;
1020
+ bindings = await withCfSession(async (ctx) => {
1021
+ await cfApi(api, ctx);
1022
+ await cfAuth(sap.email, sap.password, ctx);
1023
+ await cfTargetSpace(target.orgName, target.spaceName, ctx);
1024
+ const stdout = await cfEnv(target.appName, ctx);
1025
+ return extractHanaBindingsFromCfEnv(stdout);
1026
+ });
1027
+ } else {
1028
+ throw new CfHanaError(
1029
+ "CONFIG",
1030
+ `Failed to get HANA bindings for bare app "${target.appName}" using current target (org=${target.orgName}, space=${target.spaceName}). Verify with "cf target" and "cf env ${target.appName}". ${stderr ? `Details: ${stderr}` : ""}`,
1031
+ { cause: directError }
1032
+ );
1033
+ }
1034
+ }
1035
+ } else {
1036
+ const sap = readSapCredentials({ email: options.email, password: options.password });
1037
+ if (!sap) {
1038
+ throw new CredentialsNotFoundError(
1039
+ `SAP_EMAIL and SAP_PASSWORD are required to fetch HANA bindings for explicit selector "${selector}".`
1040
+ );
1041
+ }
1042
+ if (!target.apiEndpoint) {
1043
+ throw new CfHanaError("CONFIG", "Invalid explicit selector.");
1044
+ }
1045
+ const api = target.apiEndpoint;
1046
+ bindings = await withCfSession(async (ctx) => {
1047
+ await cfApi(api, ctx);
1048
+ await cfAuth(sap.email, sap.password, ctx);
1049
+ await cfTargetSpace(target.orgName, target.spaceName, ctx);
1050
+ const stdout = await cfEnv(target.appName, ctx);
1051
+ return extractHanaBindingsFromCfEnv(stdout);
1052
+ });
1053
+ }
1054
+ if (bindings.length === 0) {
1055
+ throw new CredentialsNotFoundError(`App "${target.selector}" has no HANA service binding.`);
654
1056
  }
655
1057
  return {
656
- selector: fetched.selector,
657
- appName: fetched.appName,
658
- bindings: fetched.bindings,
659
- source: "fresh"
1058
+ selector: target.selector,
1059
+ appName: target.appName,
1060
+ bindings,
1061
+ source
660
1062
  };
661
1063
  }
662
1064
  function selectBinding(bindings, selector) {
@@ -706,7 +1108,7 @@ function toConnectionTarget(binding, role) {
706
1108
  password: role === "hdi" ? credentials.hdiPassword : credentials.password,
707
1109
  schema: credentials.schema,
708
1110
  certificate: credentials.certificate,
709
- databaseId: credentials.databaseId
1111
+ databaseId: credentials.databaseId ?? ""
710
1112
  };
711
1113
  }
712
1114
 
@@ -1033,16 +1435,16 @@ function createDriver(name) {
1033
1435
  }
1034
1436
 
1035
1437
  // src/history.ts
1036
- import { appendFile as appendFile2, mkdir as mkdir2, readdir, rm } from "fs/promises";
1438
+ import { appendFile as appendFile2, mkdir as mkdir2, readdir, rm as rm2 } from "fs/promises";
1037
1439
  import { homedir as homedir2 } from "os";
1038
- import { join as join2 } from "path";
1440
+ import { join as join3 } from "path";
1039
1441
  var SAPTOOLS_DIR_NAME2 = ".saptools";
1040
1442
  var CF_HANA_DIR_NAME2 = "cf-hana";
1041
1443
  var HISTORIES_DIR_NAME = "histories";
1042
1444
  var HISTORY_RETENTION_DAYS = 5;
1043
1445
  var HISTORY_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1044
1446
  function defaultSaptoolsRoot2() {
1045
- return join2(homedir2(), SAPTOOLS_DIR_NAME2);
1447
+ return join3(homedir2(), SAPTOOLS_DIR_NAME2);
1046
1448
  }
1047
1449
  function padDatePart(value) {
1048
1450
  return value.toString().padStart(2, "0");
@@ -1064,10 +1466,10 @@ function resolveSaptoolsRoot(root) {
1064
1466
  return root ?? defaultSaptoolsRoot2();
1065
1467
  }
1066
1468
  function cfHanaHistoryDirectory(saptoolsRoot) {
1067
- return join2(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME2, HISTORIES_DIR_NAME);
1469
+ return join3(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME2, HISTORIES_DIR_NAME);
1068
1470
  }
1069
1471
  function sqlHistoryFilePath(now = /* @__PURE__ */ new Date(), saptoolsRoot) {
1070
- return join2(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
1472
+ return join3(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
1071
1473
  }
1072
1474
  async function pruneSqlHistory(now, saptoolsRoot) {
1073
1475
  const historyDir = cfHanaHistoryDirectory(saptoolsRoot);
@@ -1083,7 +1485,7 @@ async function pruneSqlHistory(now, saptoolsRoot) {
1083
1485
  const cutoffKey = retentionCutoffKey(now);
1084
1486
  await Promise.all(
1085
1487
  files.filter((file) => HISTORY_FILE_PATTERN.test(file)).filter((file) => file.slice(0, 10) < cutoffKey).map(async (file) => {
1086
- await rm(join2(historyDir, file), { force: true });
1488
+ await rm2(join3(historyDir, file), { force: true });
1087
1489
  })
1088
1490
  );
1089
1491
  }
@@ -1654,7 +2056,8 @@ var HanaClient = class _HanaClient {
1654
2056
  return await writeSqlBackup({
1655
2057
  operation: plan.operation,
1656
2058
  statementSql: plan.statementSql,
1657
- result
2059
+ result,
2060
+ selector: this.info.selector
1658
2061
  });
1659
2062
  });
1660
2063
  }
@@ -2065,19 +2468,19 @@ function searchResultSession(session, searchTerm, options) {
2065
2468
 
2066
2469
  // src/result-store.ts
2067
2470
  import { randomBytes } from "crypto";
2068
- import { mkdir as mkdir3, readFile, readdir as readdir2, rename, rm as rm2, writeFile as writeFile2 } from "fs/promises";
2471
+ import { mkdir as mkdir3, readFile, readdir as readdir2, rename, rm as rm3, writeFile as writeFile2 } from "fs/promises";
2069
2472
  import { homedir as homedir3 } from "os";
2070
- import { join as join3 } from "path";
2473
+ import { join as join4 } from "path";
2071
2474
  var RESULT_REF_PATTERN = /^q[0-9a-f]{8}$/;
2072
2475
  var MANIFEST_FILE_NAME = "manifest.json";
2073
2476
  function resultsRoot(saptoolsRoot) {
2074
- return join3(saptoolsRoot ?? join3(homedir3(), ".saptools"), "cf-hana", "results");
2477
+ return join4(saptoolsRoot ?? join4(homedir3(), ".saptools"), "cf-hana", "results");
2075
2478
  }
2076
2479
  function sessionDirectory(ref, saptoolsRoot) {
2077
- return join3(resultsRoot(saptoolsRoot), ref);
2480
+ return join4(resultsRoot(saptoolsRoot), ref);
2078
2481
  }
2079
2482
  function manifestPath(ref, saptoolsRoot) {
2080
- return join3(sessionDirectory(ref, saptoolsRoot), MANIFEST_FILE_NAME);
2483
+ return join4(sessionDirectory(ref, saptoolsRoot), MANIFEST_FILE_NAME);
2081
2484
  }
2082
2485
  function encodeCell(value) {
2083
2486
  if (value === null) {
@@ -2176,11 +2579,11 @@ function toStoredSession(input, ref, now) {
2176
2579
  result: encodeResult(input.result)
2177
2580
  };
2178
2581
  }
2179
- function isRecord(value) {
2582
+ function isRecord2(value) {
2180
2583
  return typeof value === "object" && value !== null && !Array.isArray(value);
2181
2584
  }
2182
2585
  function isStoredSession(value) {
2183
- if (!isRecord(value) || !isRecord(value["result"]) || !isRecord(value["info"])) {
2586
+ if (!isRecord2(value) || !isRecord2(value["result"]) || !isRecord2(value["info"])) {
2184
2587
  return false;
2185
2588
  }
2186
2589
  const result = value["result"];
@@ -2229,16 +2632,16 @@ async function createResultSession(input, options = {}) {
2229
2632
  const finalDirectory = sessionDirectory(ref, options.saptoolsRoot);
2230
2633
  const tempDirectory = `${finalDirectory}.tmp-${process.pid.toString()}`;
2231
2634
  await mkdir3(root, { recursive: true, mode: 448 });
2232
- await rm2(tempDirectory, { recursive: true, force: true });
2635
+ await rm3(tempDirectory, { recursive: true, force: true });
2233
2636
  await mkdir3(tempDirectory, { mode: 448 });
2234
2637
  try {
2235
- await writeFile2(join3(tempDirectory, MANIFEST_FILE_NAME), serialized, {
2638
+ await writeFile2(join4(tempDirectory, MANIFEST_FILE_NAME), serialized, {
2236
2639
  encoding: "utf8",
2237
2640
  mode: 384
2238
2641
  });
2239
2642
  await rename(tempDirectory, finalDirectory);
2240
2643
  } catch (error) {
2241
- await rm2(tempDirectory, { recursive: true, force: true });
2644
+ await rm3(tempDirectory, { recursive: true, force: true });
2242
2645
  throw error;
2243
2646
  }
2244
2647
  return toSession(stored, options.saptoolsRoot);
@@ -2274,7 +2677,7 @@ async function pruneResultSessions(options = {}) {
2274
2677
  for (const ref of refs) {
2275
2678
  const stored = await readStoredSession(manifestPath(ref, options.saptoolsRoot));
2276
2679
  if (stored === void 0 || Date.parse(stored.expiresAt) <= now) {
2277
- await rm2(sessionDirectory(ref, options.saptoolsRoot), { recursive: true, force: true });
2680
+ await rm3(sessionDirectory(ref, options.saptoolsRoot), { recursive: true, force: true });
2278
2681
  removed += 1;
2279
2682
  }
2280
2683
  }
@@ -2284,7 +2687,7 @@ async function clearResultSessions(options = {}) {
2284
2687
  const refs = await listSessionRefs(options.saptoolsRoot);
2285
2688
  await Promise.all(
2286
2689
  refs.map(async (ref) => {
2287
- await rm2(sessionDirectory(ref, options.saptoolsRoot), { recursive: true, force: true });
2690
+ await rm3(sessionDirectory(ref, options.saptoolsRoot), { recursive: true, force: true });
2288
2691
  })
2289
2692
  );
2290
2693
  return refs.length;
@@ -2544,6 +2947,25 @@ function parseQualifiedName(value) {
2544
2947
  }
2545
2948
  return { schema: value.slice(0, dot), table: value.slice(dot + 1) };
2546
2949
  }
2950
+ async function resolveSelectorArgument(selector) {
2951
+ if (selector.includes("/")) {
2952
+ return selector;
2953
+ }
2954
+ const current = await readCurrentCfTarget().catch((error) => {
2955
+ throw new CfHanaError(
2956
+ "CONFIG",
2957
+ "No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector.",
2958
+ { cause: error }
2959
+ );
2960
+ });
2961
+ if (current === void 0) {
2962
+ throw new CfHanaError(
2963
+ "CONFIG",
2964
+ "No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector."
2965
+ );
2966
+ }
2967
+ return formatCurrentCfAppSelector(current, selector);
2968
+ }
2547
2969
  function toConnectOptions(opts) {
2548
2970
  assertPositiveOption("--limit", opts.limit);
2549
2971
  assertPositiveOption("--timeout", opts.timeout);
@@ -2611,7 +3033,7 @@ async function runQuery(selector, sql, command) {
2611
3033
  const opts = command.opts();
2612
3034
  assertQueryOptions(sql, opts);
2613
3035
  const cellLimit = resolveCellLimit(opts.cellLimit);
2614
- const client = await connect(selector, toConnectOptions(opts));
3036
+ const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
2615
3037
  try {
2616
3038
  const params = opts.param ?? [];
2617
3039
  const backup = await client.backupWriteStatement(sql, params);
@@ -2652,7 +3074,7 @@ async function runQuery(selector, sql, command) {
2652
3074
  }
2653
3075
  async function runTables(selector, schema, command) {
2654
3076
  const opts = command.opts();
2655
- const client = await connect(selector, toConnectOptions(opts));
3077
+ const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
2656
3078
  try {
2657
3079
  const tables = await client.listTables(schema ?? client.info.schema);
2658
3080
  const rows = tables.map((table) => ({
@@ -2668,7 +3090,7 @@ async function runTables(selector, schema, command) {
2668
3090
  async function runColumns(selector, target, command) {
2669
3091
  const opts = command.opts();
2670
3092
  const { schema, table } = parseQualifiedName(target);
2671
- const client = await connect(selector, toConnectOptions(opts));
3093
+ const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
2672
3094
  try {
2673
3095
  const columns = await client.listColumns(schema, table);
2674
3096
  const rows = columns.map((column) => ({
@@ -2686,7 +3108,7 @@ async function runColumns(selector, target, command) {
2686
3108
  async function runCount(selector, target, command) {
2687
3109
  const opts = command.opts();
2688
3110
  const { schema, table } = parseQualifiedName(target);
2689
- const client = await connect(selector, toConnectOptions(opts));
3111
+ const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
2690
3112
  try {
2691
3113
  const total = await client.count({ schema, table });
2692
3114
  print2(String(total));
@@ -2696,7 +3118,7 @@ async function runCount(selector, target, command) {
2696
3118
  }
2697
3119
  async function runPing(selector, command) {
2698
3120
  const opts = command.opts();
2699
- const client = await connect(selector, toConnectOptions(opts));
3121
+ const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
2700
3122
  try {
2701
3123
  const started = Date.now();
2702
3124
  await client.query("SELECT 1 FROM DUMMY");
@@ -2709,7 +3131,7 @@ async function runPing(selector, command) {
2709
3131
  }
2710
3132
  async function runInfo(selector, command) {
2711
3133
  const opts = command.opts();
2712
- const client = await connect(selector, toConnectOptions(opts));
3134
+ const client = await connect(await resolveSelectorArgument(selector), toConnectOptions(opts));
2713
3135
  try {
2714
3136
  print2(formatInfo(client.info));
2715
3137
  } finally {