@saptools/cf-hana 0.2.1 → 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
@@ -1,11 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli.ts
4
- import { formatCurrentCfAppSelector, readCurrentCfTarget } from "@saptools/cf-sync";
5
4
  import { Command } from "commander";
6
5
 
7
6
  // src/backup.ts
8
- import { createHash, randomUUID } from "crypto";
9
7
  import { mkdir, writeFile } from "fs/promises";
10
8
  import { homedir } from "os";
11
9
  import { join } from "path";
@@ -375,6 +373,28 @@ function findTopLevelKeyword(sql, keyword, startIndex = 0) {
375
373
  }
376
374
  return void 0;
377
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
+ }
378
398
  function selectParamsAfterWhere(statementSql, whereIndex, params) {
379
399
  return params.slice(countPlaceholders(statementSql.slice(0, whereIndex)));
380
400
  }
@@ -412,6 +432,24 @@ function buildUpdateBackupPlan(statementSql, params) {
412
432
  params
413
433
  );
414
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
+ }
415
453
  function buildDeleteBackupPlan(statementSql, params) {
416
454
  const deleteIndex = findTopLevelKeyword(statementSql, "DELETE");
417
455
  const fromIndex = deleteIndex === void 0 ? void 0 : findTopLevelKeyword(statementSql, "FROM", deleteIndex + "DELETE".length);
@@ -431,8 +469,18 @@ function buildDeleteBackupPlan(statementSql, params) {
431
469
  function backupTimestamp(now) {
432
470
  return now.toISOString().replace(/:/g, "").replace(".", "");
433
471
  }
434
- function backupHash(statementSql) {
435
- 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("-");
436
484
  }
437
485
  function cfHanaBackupRoot(saptoolsRoot) {
438
486
  return join(saptoolsRoot ?? defaultSaptoolsRoot(), CF_HANA_DIR_NAME, BACKUPS_DIR_NAME);
@@ -440,33 +488,49 @@ function cfHanaBackupRoot(saptoolsRoot) {
440
488
  function buildWriteBackupPlan(sql, params = []) {
441
489
  const statementSql = trimStatementSql(sql);
442
490
  const keyword = firstKeyword(statementSql);
443
- if (keyword !== "UPDATE" && keyword !== "DELETE") {
491
+ if (keyword !== "UPDATE" && keyword !== "UPSERT" && keyword !== "DELETE") {
444
492
  return void 0;
445
493
  }
446
494
  assertParamArity(statementSql, params);
447
495
  if (keyword === "UPDATE") {
448
496
  return buildUpdateBackupPlan(statementSql, params);
449
497
  }
498
+ if (keyword === "UPSERT") {
499
+ return buildUpsertBackupPlan(statementSql, params);
500
+ }
450
501
  return buildDeleteBackupPlan(statementSql, params);
451
502
  }
452
503
  async function writeSqlBackup(input, options = {}) {
453
504
  const now = options.now ?? /* @__PURE__ */ new Date();
454
- const directory = join(
455
- cfHanaBackupRoot(options.saptoolsRoot),
456
- `${backupTimestamp(now)}-${input.operation}-${backupHash(input.statementSql)}`
457
- );
458
- const statementPath = join(directory, "statement.sql");
459
- 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
+ };
460
518
  await mkdir(directory, { recursive: true, mode: 448 });
461
519
  await Promise.all([
462
520
  writeFile(statementPath, `${input.statementSql}
463
521
  `, { encoding: "utf8", mode: 384 }),
464
- 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
+ })
465
528
  ]);
466
529
  return {
467
530
  directory,
468
531
  statementPath,
469
532
  backupPath,
533
+ metadataPath,
470
534
  rowCount: input.result.rowCount
471
535
  };
472
536
  }
@@ -620,44 +684,381 @@ function readSapCredentials(overrides) {
620
684
  return { email, password };
621
685
  }
622
686
 
623
- // src/credentials.ts
624
- import { fetchAppDbBindings, readDbAppView } from "@saptools/cf-sync";
625
- async function resolveAppBindings(selector, options) {
626
- if (options.refresh !== true) {
627
- const view = await readDbAppView(selector);
628
- if (view !== void 0 && view.entry.bindings.length > 0) {
629
- return {
630
- selector: view.entry.selector,
631
- appName: view.entry.appName,
632
- bindings: view.entry.bindings,
633
- source: "cache"
634
- };
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
+ }
635
782
  }
636
783
  }
637
- const credentials = readSapCredentials({
638
- email: options.email,
639
- 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
796
+ });
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
640
813
  });
641
- if (credentials === void 0) {
642
- throw new CredentialsNotFoundError(
643
- `No cached HANA credentials for "${selector}". Run \`cf-sync db-sync ${selector}\` first, or set SAP_EMAIL and SAP_PASSWORD to fetch them live.`
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.`
644
871
  );
645
872
  }
646
- const fetched = await fetchAppDbBindings({
647
- selector,
648
- email: credentials.email,
649
- 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
+ };
650
942
  });
651
- if (fetched.bindings.length === 0) {
652
- throw new CredentialsNotFoundError(
653
- `App "${fetched.selector}" has no HANA service binding.`
654
- );
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.`);
655
1056
  }
656
1057
  return {
657
- selector: fetched.selector,
658
- appName: fetched.appName,
659
- bindings: fetched.bindings,
660
- source: "fresh"
1058
+ selector: target.selector,
1059
+ appName: target.appName,
1060
+ bindings,
1061
+ source
661
1062
  };
662
1063
  }
663
1064
  function selectBinding(bindings, selector) {
@@ -707,7 +1108,7 @@ function toConnectionTarget(binding, role) {
707
1108
  password: role === "hdi" ? credentials.hdiPassword : credentials.password,
708
1109
  schema: credentials.schema,
709
1110
  certificate: credentials.certificate,
710
- databaseId: credentials.databaseId
1111
+ databaseId: credentials.databaseId ?? ""
711
1112
  };
712
1113
  }
713
1114
 
@@ -1034,16 +1435,16 @@ function createDriver(name) {
1034
1435
  }
1035
1436
 
1036
1437
  // src/history.ts
1037
- 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";
1038
1439
  import { homedir as homedir2 } from "os";
1039
- import { join as join2 } from "path";
1440
+ import { join as join3 } from "path";
1040
1441
  var SAPTOOLS_DIR_NAME2 = ".saptools";
1041
1442
  var CF_HANA_DIR_NAME2 = "cf-hana";
1042
1443
  var HISTORIES_DIR_NAME = "histories";
1043
1444
  var HISTORY_RETENTION_DAYS = 5;
1044
1445
  var HISTORY_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
1045
1446
  function defaultSaptoolsRoot2() {
1046
- return join2(homedir2(), SAPTOOLS_DIR_NAME2);
1447
+ return join3(homedir2(), SAPTOOLS_DIR_NAME2);
1047
1448
  }
1048
1449
  function padDatePart(value) {
1049
1450
  return value.toString().padStart(2, "0");
@@ -1065,10 +1466,10 @@ function resolveSaptoolsRoot(root) {
1065
1466
  return root ?? defaultSaptoolsRoot2();
1066
1467
  }
1067
1468
  function cfHanaHistoryDirectory(saptoolsRoot) {
1068
- return join2(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME2, HISTORIES_DIR_NAME);
1469
+ return join3(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME2, HISTORIES_DIR_NAME);
1069
1470
  }
1070
1471
  function sqlHistoryFilePath(now = /* @__PURE__ */ new Date(), saptoolsRoot) {
1071
- return join2(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
1472
+ return join3(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
1072
1473
  }
1073
1474
  async function pruneSqlHistory(now, saptoolsRoot) {
1074
1475
  const historyDir = cfHanaHistoryDirectory(saptoolsRoot);
@@ -1084,7 +1485,7 @@ async function pruneSqlHistory(now, saptoolsRoot) {
1084
1485
  const cutoffKey = retentionCutoffKey(now);
1085
1486
  await Promise.all(
1086
1487
  files.filter((file) => HISTORY_FILE_PATTERN.test(file)).filter((file) => file.slice(0, 10) < cutoffKey).map(async (file) => {
1087
- await rm(join2(historyDir, file), { force: true });
1488
+ await rm2(join3(historyDir, file), { force: true });
1088
1489
  })
1089
1490
  );
1090
1491
  }
@@ -1655,7 +2056,8 @@ var HanaClient = class _HanaClient {
1655
2056
  return await writeSqlBackup({
1656
2057
  operation: plan.operation,
1657
2058
  statementSql: plan.statementSql,
1658
- result
2059
+ result,
2060
+ selector: this.info.selector
1659
2061
  });
1660
2062
  });
1661
2063
  }
@@ -2066,19 +2468,19 @@ function searchResultSession(session, searchTerm, options) {
2066
2468
 
2067
2469
  // src/result-store.ts
2068
2470
  import { randomBytes } from "crypto";
2069
- 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";
2070
2472
  import { homedir as homedir3 } from "os";
2071
- import { join as join3 } from "path";
2473
+ import { join as join4 } from "path";
2072
2474
  var RESULT_REF_PATTERN = /^q[0-9a-f]{8}$/;
2073
2475
  var MANIFEST_FILE_NAME = "manifest.json";
2074
2476
  function resultsRoot(saptoolsRoot) {
2075
- return join3(saptoolsRoot ?? join3(homedir3(), ".saptools"), "cf-hana", "results");
2477
+ return join4(saptoolsRoot ?? join4(homedir3(), ".saptools"), "cf-hana", "results");
2076
2478
  }
2077
2479
  function sessionDirectory(ref, saptoolsRoot) {
2078
- return join3(resultsRoot(saptoolsRoot), ref);
2480
+ return join4(resultsRoot(saptoolsRoot), ref);
2079
2481
  }
2080
2482
  function manifestPath(ref, saptoolsRoot) {
2081
- return join3(sessionDirectory(ref, saptoolsRoot), MANIFEST_FILE_NAME);
2483
+ return join4(sessionDirectory(ref, saptoolsRoot), MANIFEST_FILE_NAME);
2082
2484
  }
2083
2485
  function encodeCell(value) {
2084
2486
  if (value === null) {
@@ -2177,11 +2579,11 @@ function toStoredSession(input, ref, now) {
2177
2579
  result: encodeResult(input.result)
2178
2580
  };
2179
2581
  }
2180
- function isRecord(value) {
2582
+ function isRecord2(value) {
2181
2583
  return typeof value === "object" && value !== null && !Array.isArray(value);
2182
2584
  }
2183
2585
  function isStoredSession(value) {
2184
- if (!isRecord(value) || !isRecord(value["result"]) || !isRecord(value["info"])) {
2586
+ if (!isRecord2(value) || !isRecord2(value["result"]) || !isRecord2(value["info"])) {
2185
2587
  return false;
2186
2588
  }
2187
2589
  const result = value["result"];
@@ -2230,16 +2632,16 @@ async function createResultSession(input, options = {}) {
2230
2632
  const finalDirectory = sessionDirectory(ref, options.saptoolsRoot);
2231
2633
  const tempDirectory = `${finalDirectory}.tmp-${process.pid.toString()}`;
2232
2634
  await mkdir3(root, { recursive: true, mode: 448 });
2233
- await rm2(tempDirectory, { recursive: true, force: true });
2635
+ await rm3(tempDirectory, { recursive: true, force: true });
2234
2636
  await mkdir3(tempDirectory, { mode: 448 });
2235
2637
  try {
2236
- await writeFile2(join3(tempDirectory, MANIFEST_FILE_NAME), serialized, {
2638
+ await writeFile2(join4(tempDirectory, MANIFEST_FILE_NAME), serialized, {
2237
2639
  encoding: "utf8",
2238
2640
  mode: 384
2239
2641
  });
2240
2642
  await rename(tempDirectory, finalDirectory);
2241
2643
  } catch (error) {
2242
- await rm2(tempDirectory, { recursive: true, force: true });
2644
+ await rm3(tempDirectory, { recursive: true, force: true });
2243
2645
  throw error;
2244
2646
  }
2245
2647
  return toSession(stored, options.saptoolsRoot);
@@ -2275,7 +2677,7 @@ async function pruneResultSessions(options = {}) {
2275
2677
  for (const ref of refs) {
2276
2678
  const stored = await readStoredSession(manifestPath(ref, options.saptoolsRoot));
2277
2679
  if (stored === void 0 || Date.parse(stored.expiresAt) <= now) {
2278
- await rm2(sessionDirectory(ref, options.saptoolsRoot), { recursive: true, force: true });
2680
+ await rm3(sessionDirectory(ref, options.saptoolsRoot), { recursive: true, force: true });
2279
2681
  removed += 1;
2280
2682
  }
2281
2683
  }
@@ -2285,7 +2687,7 @@ async function clearResultSessions(options = {}) {
2285
2687
  const refs = await listSessionRefs(options.saptoolsRoot);
2286
2688
  await Promise.all(
2287
2689
  refs.map(async (ref) => {
2288
- await rm2(sessionDirectory(ref, options.saptoolsRoot), { recursive: true, force: true });
2690
+ await rm3(sessionDirectory(ref, options.saptoolsRoot), { recursive: true, force: true });
2289
2691
  })
2290
2692
  );
2291
2693
  return refs.length;