@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/index.d.ts CHANGED
@@ -7,7 +7,7 @@ type StatementKind = "select" | "dml" | "ddl" | "unknown";
7
7
  /** Which HANA user from a service binding to authenticate as. */
8
8
  type DbUserRole = "runtime" | "hdi";
9
9
  /** Where resolved credentials came from. */
10
- type CredentialSource = "cache" | "fresh";
10
+ type CredentialSource = "live";
11
11
  /** Output rendering for CLI results. */
12
12
  type OutputFormat = "table" | "json" | "csv";
13
13
  interface QueryResultColumn {
@@ -96,7 +96,7 @@ interface HanaClientInfo {
96
96
  readonly credentialSource: CredentialSource;
97
97
  }
98
98
 
99
- type WriteBackupOperation = "update" | "delete";
99
+ type WriteBackupOperation = "update" | "upsert" | "delete";
100
100
  interface WriteBackupPlan {
101
101
  readonly operation: WriteBackupOperation;
102
102
  readonly statementSql: string;
@@ -107,6 +107,7 @@ interface SqlBackupWriteInput {
107
107
  readonly operation: WriteBackupOperation;
108
108
  readonly statementSql: string;
109
109
  readonly result: QueryResult;
110
+ readonly selector?: string;
110
111
  }
111
112
  interface SqlBackupWriteOptions {
112
113
  readonly now?: Date;
@@ -116,6 +117,7 @@ interface SqlBackupRecord {
116
117
  readonly directory: string;
117
118
  readonly statementPath: string;
118
119
  readonly backupPath: string;
120
+ readonly metadataPath: string;
119
121
  readonly rowCount: number;
120
122
  }
121
123
  declare function cfHanaBackupRoot(saptoolsRoot?: string): string;
package/dist/index.js CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/backup.ts
4
- import { createHash, randomUUID } from "crypto";
5
4
  import { mkdir, writeFile } from "fs/promises";
6
5
  import { homedir } from "os";
7
6
  import { join } from "path";
@@ -302,6 +301,28 @@ function findTopLevelKeyword(sql, keyword, startIndex = 0) {
302
301
  }
303
302
  return void 0;
304
303
  }
304
+ function findTopLevelChar(sql, target, startIndex, endIndex) {
305
+ let index = startIndex;
306
+ let depth = 0;
307
+ while (index < endIndex) {
308
+ const skipped = skipNonCode(sql, index);
309
+ if (skipped !== void 0) {
310
+ index = skipped;
311
+ continue;
312
+ }
313
+ const char = sql[index];
314
+ if (char === target && depth === 0) {
315
+ return index;
316
+ }
317
+ if (char === "(") {
318
+ depth += 1;
319
+ } else if (char === ")" && depth > 0) {
320
+ depth -= 1;
321
+ }
322
+ index += 1;
323
+ }
324
+ return void 0;
325
+ }
305
326
  function selectParamsAfterWhere(statementSql, whereIndex, params) {
306
327
  return params.slice(countPlaceholders(statementSql.slice(0, whereIndex)));
307
328
  }
@@ -339,6 +360,24 @@ function buildUpdateBackupPlan(statementSql, params) {
339
360
  params
340
361
  );
341
362
  }
363
+ function buildUpsertBackupPlan(statementSql, params) {
364
+ const upsertIndex = findTopLevelKeyword(statementSql, "UPSERT");
365
+ const valuesIndex = upsertIndex === void 0 ? void 0 : findTopLevelKeyword(statementSql, "VALUES", upsertIndex + "UPSERT".length);
366
+ if (upsertIndex === void 0 || valuesIndex === void 0) {
367
+ throw new QueryError("UPSERT backup requires UPSERT <target> VALUES syntax");
368
+ }
369
+ const targetStart = upsertIndex + "UPSERT".length;
370
+ const columnListIndex = findTopLevelChar(statementSql, "(", targetStart, valuesIndex);
371
+ const targetEnd = columnListIndex ?? valuesIndex;
372
+ const whereIndex = findTopLevelKeyword(statementSql, "WHERE", valuesIndex + "VALUES".length);
373
+ return buildSelectPlan(
374
+ "upsert",
375
+ statementSql,
376
+ statementSql.slice(targetStart, targetEnd),
377
+ whereIndex,
378
+ params
379
+ );
380
+ }
342
381
  function buildDeleteBackupPlan(statementSql, params) {
343
382
  const deleteIndex = findTopLevelKeyword(statementSql, "DELETE");
344
383
  const fromIndex = deleteIndex === void 0 ? void 0 : findTopLevelKeyword(statementSql, "FROM", deleteIndex + "DELETE".length);
@@ -358,8 +397,18 @@ function buildDeleteBackupPlan(statementSql, params) {
358
397
  function backupTimestamp(now) {
359
398
  return now.toISOString().replace(/:/g, "").replace(".", "");
360
399
  }
361
- function backupHash(statementSql) {
362
- return createHash("sha256").update(statementSql).update("\0").update(randomUUID()).digest("hex").slice(0, 12);
400
+ function backupMonth(now) {
401
+ const year = String(now.getUTCFullYear());
402
+ const month = String(now.getUTCMonth() + 1).padStart(2, "0");
403
+ return `${year}${month}`;
404
+ }
405
+ function sanitizePathPart(value) {
406
+ const trimmed = value?.trim() ?? "";
407
+ const normalized = trimmed.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
408
+ return normalized.length > 0 ? normalized : "unknown-target";
409
+ }
410
+ function backupBaseName(input, now) {
411
+ return [sanitizePathPart(input.selector), input.operation, backupTimestamp(now)].join("-");
363
412
  }
364
413
  function cfHanaBackupRoot(saptoolsRoot) {
365
414
  return join(saptoolsRoot ?? defaultSaptoolsRoot(), CF_HANA_DIR_NAME, BACKUPS_DIR_NAME);
@@ -367,33 +416,49 @@ function cfHanaBackupRoot(saptoolsRoot) {
367
416
  function buildWriteBackupPlan(sql, params = []) {
368
417
  const statementSql = trimStatementSql(sql);
369
418
  const keyword = firstKeyword(statementSql);
370
- if (keyword !== "UPDATE" && keyword !== "DELETE") {
419
+ if (keyword !== "UPDATE" && keyword !== "UPSERT" && keyword !== "DELETE") {
371
420
  return void 0;
372
421
  }
373
422
  assertParamArity(statementSql, params);
374
423
  if (keyword === "UPDATE") {
375
424
  return buildUpdateBackupPlan(statementSql, params);
376
425
  }
426
+ if (keyword === "UPSERT") {
427
+ return buildUpsertBackupPlan(statementSql, params);
428
+ }
377
429
  return buildDeleteBackupPlan(statementSql, params);
378
430
  }
379
431
  async function writeSqlBackup(input, options = {}) {
380
432
  const now = options.now ?? /* @__PURE__ */ new Date();
381
- const directory = join(
382
- cfHanaBackupRoot(options.saptoolsRoot),
383
- `${backupTimestamp(now)}-${input.operation}-${backupHash(input.statementSql)}`
384
- );
385
- const statementPath = join(directory, "statement.sql");
386
- const backupPath = join(directory, "backup.csv");
433
+ const directory = join(cfHanaBackupRoot(options.saptoolsRoot), backupMonth(now));
434
+ const baseName = backupBaseName(input, now);
435
+ const statementPath = join(directory, `${baseName}.statement.sql`);
436
+ const backupPath = join(directory, `${baseName}.sql`);
437
+ const metadataPath = join(directory, `${baseName}.json`);
438
+ const metadata = {
439
+ selector: input.selector ?? null,
440
+ operation: input.operation,
441
+ statementPath,
442
+ backupPath,
443
+ rowCount: input.result.rowCount,
444
+ createdAt: now.toISOString()
445
+ };
387
446
  await mkdir(directory, { recursive: true, mode: 448 });
388
447
  await Promise.all([
389
448
  writeFile(statementPath, `${input.statementSql}
390
449
  `, { encoding: "utf8", mode: 384 }),
391
- writeFile(backupPath, formatCsv(input.result), { encoding: "utf8", mode: 384 })
450
+ writeFile(backupPath, formatCsv(input.result), { encoding: "utf8", mode: 384 }),
451
+ writeFile(metadataPath, `${JSON.stringify(metadata, null, 2)}
452
+ `, {
453
+ encoding: "utf8",
454
+ mode: 384
455
+ })
392
456
  ]);
393
457
  return {
394
458
  directory,
395
459
  statementPath,
396
460
  backupPath,
461
+ metadataPath,
397
462
  rowCount: input.result.rowCount
398
463
  };
399
464
  }
@@ -542,44 +607,381 @@ function readSapCredentials(overrides) {
542
607
  return { email, password };
543
608
  }
544
609
 
545
- // src/credentials.ts
546
- import { fetchAppDbBindings, readDbAppView } from "@saptools/cf-sync";
547
- async function resolveAppBindings(selector, options) {
548
- if (options.refresh !== true) {
549
- const view = await readDbAppView(selector);
550
- if (view !== void 0 && view.entry.bindings.length > 0) {
551
- return {
552
- selector: view.entry.selector,
553
- appName: view.entry.appName,
554
- bindings: view.entry.bindings,
555
- source: "cache"
556
- };
610
+ // src/cf.ts
611
+ import { execFile } from "child_process";
612
+ import { mkdtemp, rm } from "fs/promises";
613
+ import { tmpdir } from "os";
614
+ import { join as join2 } from "path";
615
+ import { promisify } from "util";
616
+ var execFileAsync = promisify(execFile);
617
+ var MAX_BUFFER = 16 * 1024 * 1024;
618
+ var DEFAULT_TIMEOUT_MS = 3e4;
619
+ var CF_RETRY_ATTEMPTS = 3;
620
+ var CF_RETRY_BASE_DELAY_MS = 120;
621
+ var REGION_API_MAP = {
622
+ ae01: "https://api.cf.ae01.hana.ondemand.com",
623
+ ap01: "https://api.cf.ap01.hana.ondemand.com",
624
+ ap10: "https://api.cf.ap10.hana.ondemand.com",
625
+ ap11: "https://api.cf.ap11.hana.ondemand.com",
626
+ ap12: "https://api.cf.ap12.hana.ondemand.com",
627
+ ap20: "https://api.cf.ap20.hana.ondemand.com",
628
+ ap21: "https://api.cf.ap21.hana.ondemand.com",
629
+ ap30: "https://api.cf.ap30.hana.ondemand.com",
630
+ br10: "https://api.cf.br10.hana.ondemand.com",
631
+ br20: "https://api.cf.br20.hana.ondemand.com",
632
+ br30: "https://api.cf.br30.hana.ondemand.com",
633
+ ca10: "https://api.cf.ca10.hana.ondemand.com",
634
+ ca20: "https://api.cf.ca20.hana.ondemand.com",
635
+ ch20: "https://api.cf.ch20.hana.ondemand.com",
636
+ eu10: "https://api.cf.eu10.hana.ondemand.com",
637
+ eu11: "https://api.cf.eu11.hana.ondemand.com",
638
+ eu12: "https://api.cf.eu12.hana.ondemand.com",
639
+ eu20: "https://api.cf.eu20.hana.ondemand.com",
640
+ eu21: "https://api.cf.eu21.hana.ondemand.com",
641
+ eu30: "https://api.cf.eu30.hana.ondemand.com",
642
+ "eu10-002": "https://api.cf.eu10-002.hana.ondemand.com",
643
+ jp10: "https://api.cf.jp10.hana.ondemand.com",
644
+ jp20: "https://api.cf.jp20.hana.ondemand.com",
645
+ jp30: "https://api.cf.jp30.hana.ondemand.com",
646
+ us10: "https://api.cf.us10.hana.ondemand.com",
647
+ us11: "https://api.cf.us11.hana.ondemand.com",
648
+ us20: "https://api.cf.us20.hana.ondemand.com",
649
+ us21: "https://api.cf.us21.hana.ondemand.com",
650
+ us30: "https://api.cf.us30.hana.ondemand.com",
651
+ in30: "https://api.cf.in30.hana.ondemand.com"
652
+ };
653
+ function getApiEndpointForRegion(regionKey) {
654
+ return REGION_API_MAP[regionKey];
655
+ }
656
+ function getRegionKeyForApi(apiEndpoint) {
657
+ const norm = apiEndpoint.trim().replace(/\/+$/, "").toLowerCase();
658
+ for (const [key, endpoint] of Object.entries(REGION_API_MAP)) {
659
+ if (endpoint.toLowerCase() === norm) {
660
+ return key;
661
+ }
662
+ }
663
+ return void 0;
664
+ }
665
+ async function withCfSession(work) {
666
+ const cfHome = await mkdtemp(join2(tmpdir(), "saptools-cf-hana-"));
667
+ const ctx = { cfHome };
668
+ try {
669
+ return await work(ctx);
670
+ } finally {
671
+ await rm(cfHome, { recursive: true, force: true });
672
+ }
673
+ }
674
+ function resolveCfBin() {
675
+ const raw = process.env["CF_HANA_CF_BIN"] ?? "cf";
676
+ if (/\.(?:c|m)?js$/i.test(raw)) {
677
+ return { bin: process.execPath, argsPrefix: [raw] };
678
+ }
679
+ return { bin: raw, argsPrefix: [] };
680
+ }
681
+ function buildEnv(ctx, overrides = {}) {
682
+ const env = { ...process.env, ...overrides };
683
+ delete env["SAP_EMAIL"];
684
+ delete env["SAP_PASSWORD"];
685
+ env["CF_HOME"] = ctx.cfHome;
686
+ return env;
687
+ }
688
+ async function runCf(args, ctx, overrides = {}) {
689
+ const { bin, argsPrefix } = resolveCfBin();
690
+ const env = buildEnv(ctx, overrides);
691
+ let lastErr;
692
+ for (let attempt = 0; attempt < CF_RETRY_ATTEMPTS; attempt++) {
693
+ try {
694
+ const { stdout } = await execFileAsync(bin, [...argsPrefix, ...args], {
695
+ env,
696
+ maxBuffer: MAX_BUFFER,
697
+ timeout: DEFAULT_TIMEOUT_MS
698
+ });
699
+ return stdout;
700
+ } catch (err) {
701
+ lastErr = err;
702
+ if (attempt < CF_RETRY_ATTEMPTS - 1) {
703
+ await new Promise((r) => setTimeout(r, CF_RETRY_BASE_DELAY_MS * (attempt + 1)));
704
+ }
557
705
  }
558
706
  }
559
- const credentials = readSapCredentials({
560
- email: options.email,
561
- password: options.password
707
+ const e = lastErr;
708
+ const detail = e?.stderr ? String(e.stderr) : e?.message ?? "";
709
+ const cmd = args[0] === "auth" ? "cf auth" : `cf ${args.join(" ")}`;
710
+ throw new Error(`${cmd} failed: ${detail}`.trim(), { cause: lastErr });
711
+ }
712
+ async function cfApi(apiEndpoint, ctx) {
713
+ await runCf(["api", apiEndpoint], ctx);
714
+ }
715
+ async function cfAuth(email, password, ctx) {
716
+ await runCf(["auth"], ctx, {
717
+ CF_USERNAME: email,
718
+ CF_PASSWORD: password
719
+ });
720
+ }
721
+ async function cfTargetSpace(orgName, spaceName, ctx) {
722
+ await runCf(["target", "-o", orgName, "-s", spaceName], ctx);
723
+ }
724
+ async function cfEnv(appName, ctx) {
725
+ return await runCf(["env", appName], ctx);
726
+ }
727
+ async function cfEnvDirect(appName) {
728
+ const { bin, argsPrefix } = resolveCfBin();
729
+ const env = { ...process.env };
730
+ delete env["SAP_EMAIL"];
731
+ delete env["SAP_PASSWORD"];
732
+ const { stdout } = await execFileAsync(bin, [...argsPrefix, "env", appName], {
733
+ env,
734
+ maxBuffer: MAX_BUFFER,
735
+ timeout: DEFAULT_TIMEOUT_MS
562
736
  });
563
- if (credentials === void 0) {
564
- throw new CredentialsNotFoundError(
565
- `No cached HANA credentials for "${selector}". Run \`cf-sync db-sync ${selector}\` first, or set SAP_EMAIL and SAP_PASSWORD to fetch them live.`
737
+ return stdout;
738
+ }
739
+ async function readCurrentCfTarget() {
740
+ const { bin, argsPrefix } = resolveCfBin();
741
+ const env = { ...process.env };
742
+ delete env["SAP_EMAIL"];
743
+ delete env["SAP_PASSWORD"];
744
+ try {
745
+ const { stdout } = await execFileAsync(bin, [...argsPrefix, "target"], {
746
+ env,
747
+ maxBuffer: MAX_BUFFER,
748
+ timeout: 1e4
749
+ });
750
+ return parseCfTargetOutput(stdout);
751
+ } catch {
752
+ return void 0;
753
+ }
754
+ }
755
+ function parseCfTargetOutput(stdout) {
756
+ const fields = parseTargetFields(stdout);
757
+ const api = fields.get("api endpoint");
758
+ const org = fields.get("org");
759
+ const space = fields.get("space");
760
+ if (!api || !org || !space) {
761
+ return void 0;
762
+ }
763
+ const regionKey = getRegionKeyForApi(api);
764
+ return {
765
+ apiEndpoint: api,
766
+ orgName: org,
767
+ spaceName: space,
768
+ ...regionKey ? { regionKey } : {}
769
+ };
770
+ }
771
+ function parseTargetFields(stdout) {
772
+ const map = /* @__PURE__ */ new Map();
773
+ for (const line of stdout.split(/\r?\n/)) {
774
+ const idx = line.indexOf(":");
775
+ if (idx < 0) {
776
+ continue;
777
+ }
778
+ const key = line.slice(0, idx).trim().toLowerCase();
779
+ const val = line.slice(idx + 1).trim();
780
+ if (key && val) {
781
+ map.set(key, val);
782
+ }
783
+ }
784
+ return map;
785
+ }
786
+ function formatCurrentCfAppSelector(target, appName) {
787
+ const name = appName.trim();
788
+ if (!name) {
789
+ throw new Error("App name is required.");
790
+ }
791
+ if (!target.regionKey) {
792
+ throw new Error(
793
+ `Current CF API endpoint "${target.apiEndpoint}" does not match a known SAP region. Pass a full region/org/space/app selector.`
566
794
  );
567
795
  }
568
- const fetched = await fetchAppDbBindings({
569
- selector,
570
- email: credentials.email,
571
- password: credentials.password
796
+ return `${target.regionKey}/${target.orgName}/${target.spaceName}/${name}`;
797
+ }
798
+ function extractVcapSection(stdout) {
799
+ const start = stdout.indexOf("VCAP_SERVICES:");
800
+ if (start === -1) {
801
+ throw new Error("VCAP_SERVICES section not found in cf env output");
802
+ }
803
+ const after = stdout.slice(start + "VCAP_SERVICES:".length);
804
+ const end = after.indexOf("VCAP_APPLICATION:");
805
+ const block = end === -1 ? after : after.slice(0, end);
806
+ return block.trim();
807
+ }
808
+ function isRecord(v) {
809
+ return typeof v === "object" && v !== null && !Array.isArray(v);
810
+ }
811
+ function assertCreds(raw) {
812
+ if (!isRecord(raw)) {
813
+ throw new Error("HANA credentials must be an object");
814
+ }
815
+ const req = ["host", "port", "user", "password", "schema", "hdi_user", "hdi_password", "url", "database_id", "certificate"];
816
+ for (const k of req) {
817
+ if (typeof raw[k] !== "string") {
818
+ throw new Error(`Missing/invalid HANA credential field: "${k}"`);
819
+ }
820
+ }
821
+ return raw;
822
+ }
823
+ function mapCreds(raw) {
824
+ return {
825
+ host: raw.host,
826
+ port: raw.port,
827
+ user: raw.user,
828
+ password: raw.password,
829
+ schema: raw.schema,
830
+ hdiUser: raw.hdi_user,
831
+ hdiPassword: raw.hdi_password,
832
+ ...raw.url ? { url: raw.url } : {},
833
+ databaseId: raw.database_id,
834
+ certificate: raw.certificate
835
+ };
836
+ }
837
+ function extractHanaBindingsFromCfEnv(stdout) {
838
+ const jsonText = extractVcapSection(stdout);
839
+ let vcap;
840
+ try {
841
+ vcap = JSON.parse(jsonText);
842
+ } catch {
843
+ throw new Error("VCAP_SERVICES is not valid JSON");
844
+ }
845
+ if (!isRecord(vcap)) {
846
+ throw new Error("VCAP_SERVICES must be an object");
847
+ }
848
+ const hana = vcap["hana"];
849
+ if (hana === void 0) {
850
+ return [];
851
+ }
852
+ if (!Array.isArray(hana)) {
853
+ throw new Error("VCAP_SERVICES.hana must be an array when present");
854
+ }
855
+ return hana.map((b) => {
856
+ if (!isRecord(b)) {
857
+ throw new Error("HANA binding must be an object");
858
+ }
859
+ const credsRaw = assertCreds(b["credentials"]);
860
+ const name = typeof b["name"] === "string" ? b["name"] : void 0;
861
+ return {
862
+ ...name ? { name } : {},
863
+ credentials: mapCreds(credsRaw)
864
+ };
572
865
  });
573
- if (fetched.bindings.length === 0) {
574
- throw new CredentialsNotFoundError(
575
- `App "${fetched.selector}" has no HANA service binding.`
576
- );
866
+ }
867
+ function classifyCfError(stderr = "", stdout = "") {
868
+ const text = `${stderr} ${stdout}`.toLowerCase();
869
+ 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")) {
870
+ return { isAuthError: true, reason: "auth/session issue" };
871
+ }
872
+ return { isAuthError: false, reason: "" };
873
+ }
874
+
875
+ // src/credentials.ts
876
+ async function resolveAppBindings(rawSelector, options) {
877
+ const selector = rawSelector.trim();
878
+ if (!selector) {
879
+ throw new CfHanaError("CONFIG", "App selector is required");
880
+ }
881
+ let target;
882
+ const isBare = !selector.includes("/");
883
+ if (isBare) {
884
+ const current = await readCurrentCfTarget();
885
+ if (!current) {
886
+ throw new CfHanaError(
887
+ "CONFIG",
888
+ "No current CF target found. Run `cf target -o <org> -s <space>` or pass a full region/org/space/app selector."
889
+ );
890
+ }
891
+ const displaySelector = current.regionKey ? formatCurrentCfAppSelector(current, selector) : `current/${current.orgName}/${current.spaceName}/${selector}`;
892
+ target = {
893
+ selector: displaySelector,
894
+ apiEndpoint: current.regionKey ? getApiEndpointForRegion(current.regionKey) : void 0,
895
+ orgName: current.orgName,
896
+ spaceName: current.spaceName,
897
+ appName: selector
898
+ };
899
+ } else {
900
+ const parts = selector.split("/").map((p) => p.trim());
901
+ if (parts.length !== 4 || !parts[0] || !parts[1] || !parts[2] || !parts[3]) {
902
+ throw new CfHanaError(
903
+ "CONFIG",
904
+ `Invalid selector "${selector}". Use region/org/space/app or a bare app name.`
905
+ );
906
+ }
907
+ const [regionKey, orgName, spaceName, appName] = parts;
908
+ const apiEndpoint = getApiEndpointForRegion(regionKey);
909
+ if (!apiEndpoint) {
910
+ throw new CfHanaError(
911
+ "CONFIG",
912
+ `Unknown region key "${regionKey}". Use a known region or the current CF target.`
913
+ );
914
+ }
915
+ target = { selector, apiEndpoint, orgName, spaceName, appName };
916
+ }
917
+ let bindings;
918
+ const source = "live";
919
+ if (isBare) {
920
+ try {
921
+ const stdout = await cfEnvDirect(target.appName);
922
+ bindings = extractHanaBindingsFromCfEnv(stdout);
923
+ } catch (directError) {
924
+ let stderr = "";
925
+ if (directError && typeof directError === "object") {
926
+ const e = directError;
927
+ stderr = (typeof e.stderr === "string" ? e.stderr : "") || (typeof e.message === "string" ? e.message : "");
928
+ }
929
+ const classified = classifyCfError(stderr);
930
+ if (classified.isAuthError) {
931
+ const sap = readSapCredentials({ email: options.email, password: options.password });
932
+ if (!sap) {
933
+ throw new CredentialsNotFoundError(
934
+ `Current CF session problem for bare app "${target.appName}" (${classified.reason}).
935
+ Run "cf login" + "cf target", or provide SAP_EMAIL + SAP_PASSWORD.`,
936
+ { cause: directError }
937
+ );
938
+ }
939
+ if (!target.apiEndpoint) {
940
+ throw new CfHanaError("CONFIG", "Cannot determine API endpoint for fallback auth.");
941
+ }
942
+ const api = target.apiEndpoint;
943
+ bindings = await withCfSession(async (ctx) => {
944
+ await cfApi(api, ctx);
945
+ await cfAuth(sap.email, sap.password, ctx);
946
+ await cfTargetSpace(target.orgName, target.spaceName, ctx);
947
+ const stdout = await cfEnv(target.appName, ctx);
948
+ return extractHanaBindingsFromCfEnv(stdout);
949
+ });
950
+ } else {
951
+ throw new CfHanaError(
952
+ "CONFIG",
953
+ `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}` : ""}`,
954
+ { cause: directError }
955
+ );
956
+ }
957
+ }
958
+ } else {
959
+ const sap = readSapCredentials({ email: options.email, password: options.password });
960
+ if (!sap) {
961
+ throw new CredentialsNotFoundError(
962
+ `SAP_EMAIL and SAP_PASSWORD are required to fetch HANA bindings for explicit selector "${selector}".`
963
+ );
964
+ }
965
+ if (!target.apiEndpoint) {
966
+ throw new CfHanaError("CONFIG", "Invalid explicit selector.");
967
+ }
968
+ const api = target.apiEndpoint;
969
+ bindings = await withCfSession(async (ctx) => {
970
+ await cfApi(api, ctx);
971
+ await cfAuth(sap.email, sap.password, ctx);
972
+ await cfTargetSpace(target.orgName, target.spaceName, ctx);
973
+ const stdout = await cfEnv(target.appName, ctx);
974
+ return extractHanaBindingsFromCfEnv(stdout);
975
+ });
976
+ }
977
+ if (bindings.length === 0) {
978
+ throw new CredentialsNotFoundError(`App "${target.selector}" has no HANA service binding.`);
577
979
  }
578
980
  return {
579
- selector: fetched.selector,
580
- appName: fetched.appName,
581
- bindings: fetched.bindings,
582
- source: "fresh"
981
+ selector: target.selector,
982
+ appName: target.appName,
983
+ bindings,
984
+ source
583
985
  };
584
986
  }
585
987
  function selectBinding(bindings, selector) {
@@ -629,7 +1031,7 @@ function toConnectionTarget(binding, role) {
629
1031
  password: role === "hdi" ? credentials.hdiPassword : credentials.password,
630
1032
  schema: credentials.schema,
631
1033
  certificate: credentials.certificate,
632
- databaseId: credentials.databaseId
1034
+ databaseId: credentials.databaseId ?? ""
633
1035
  };
634
1036
  }
635
1037
 
@@ -956,16 +1358,16 @@ function createDriver(name) {
956
1358
  }
957
1359
 
958
1360
  // src/history.ts
959
- import { appendFile as appendFile2, mkdir as mkdir2, readdir, rm } from "fs/promises";
1361
+ import { appendFile as appendFile2, mkdir as mkdir2, readdir, rm as rm2 } from "fs/promises";
960
1362
  import { homedir as homedir2 } from "os";
961
- import { join as join2 } from "path";
1363
+ import { join as join3 } from "path";
962
1364
  var SAPTOOLS_DIR_NAME2 = ".saptools";
963
1365
  var CF_HANA_DIR_NAME2 = "cf-hana";
964
1366
  var HISTORIES_DIR_NAME = "histories";
965
1367
  var HISTORY_RETENTION_DAYS = 5;
966
1368
  var HISTORY_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
967
1369
  function defaultSaptoolsRoot2() {
968
- return join2(homedir2(), SAPTOOLS_DIR_NAME2);
1370
+ return join3(homedir2(), SAPTOOLS_DIR_NAME2);
969
1371
  }
970
1372
  function padDatePart(value) {
971
1373
  return value.toString().padStart(2, "0");
@@ -987,10 +1389,10 @@ function resolveSaptoolsRoot(root) {
987
1389
  return root ?? defaultSaptoolsRoot2();
988
1390
  }
989
1391
  function cfHanaHistoryDirectory(saptoolsRoot) {
990
- return join2(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME2, HISTORIES_DIR_NAME);
1392
+ return join3(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME2, HISTORIES_DIR_NAME);
991
1393
  }
992
1394
  function sqlHistoryFilePath(now = /* @__PURE__ */ new Date(), saptoolsRoot) {
993
- return join2(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
1395
+ return join3(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
994
1396
  }
995
1397
  async function pruneSqlHistory(now, saptoolsRoot) {
996
1398
  const historyDir = cfHanaHistoryDirectory(saptoolsRoot);
@@ -1006,7 +1408,7 @@ async function pruneSqlHistory(now, saptoolsRoot) {
1006
1408
  const cutoffKey = retentionCutoffKey(now);
1007
1409
  await Promise.all(
1008
1410
  files.filter((file) => HISTORY_FILE_PATTERN.test(file)).filter((file) => file.slice(0, 10) < cutoffKey).map(async (file) => {
1009
- await rm(join2(historyDir, file), { force: true });
1411
+ await rm2(join3(historyDir, file), { force: true });
1010
1412
  })
1011
1413
  );
1012
1414
  }
@@ -1577,7 +1979,8 @@ var HanaClient = class _HanaClient {
1577
1979
  return await writeSqlBackup({
1578
1980
  operation: plan.operation,
1579
1981
  statementSql: plan.statementSql,
1580
- result
1982
+ result,
1983
+ selector: this.info.selector
1581
1984
  });
1582
1985
  });
1583
1986
  }