@saptools/cf-hana 0.1.2 → 0.1.4

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.js CHANGED
@@ -1,5 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ // src/backup.ts
4
+ import { createHash, randomUUID } from "crypto";
5
+ import { mkdir, writeFile } from "fs/promises";
6
+ import { homedir } from "os";
7
+ import { join } from "path";
8
+
3
9
  // src/errors.ts
4
10
  var CfHanaError = class extends Error {
5
11
  code;
@@ -43,6 +49,91 @@ function errorMessage(error) {
43
49
  return String(error);
44
50
  }
45
51
 
52
+ // src/format.ts
53
+ function cellText(value, nullText) {
54
+ if (value === null) {
55
+ return nullText;
56
+ }
57
+ if (value instanceof Date) {
58
+ return value.toISOString();
59
+ }
60
+ if (Buffer.isBuffer(value)) {
61
+ return `0x${value.toString("hex")}`;
62
+ }
63
+ if (typeof value === "boolean") {
64
+ return value ? "true" : "false";
65
+ }
66
+ return typeof value === "number" ? value.toString() : value;
67
+ }
68
+ function serializeCell(value) {
69
+ if (value === null) {
70
+ return null;
71
+ }
72
+ if (value instanceof Date) {
73
+ return value.toISOString();
74
+ }
75
+ if (Buffer.isBuffer(value)) {
76
+ return `0x${value.toString("hex")}`;
77
+ }
78
+ return value;
79
+ }
80
+ function csvEscape(text) {
81
+ if (/[",\r\n]/.test(text)) {
82
+ return `"${text.replace(/"/g, '""')}"`;
83
+ }
84
+ return text;
85
+ }
86
+ function formatTable(result) {
87
+ if (result.columns.length === 0) {
88
+ return `(${String(result.rowCount)} row(s) affected)`;
89
+ }
90
+ const headers = result.columns.map((column) => column.name);
91
+ const rows = result.rows.map(
92
+ (row) => result.columns.map((column) => cellText(row[column.name] ?? null, "NULL"))
93
+ );
94
+ const widths = headers.map((header, index) => {
95
+ const widest = rows.reduce(
96
+ (max, cells) => Math.max(max, (cells[index] ?? "").length),
97
+ header.length
98
+ );
99
+ return widest;
100
+ });
101
+ const renderRow = (cells) => cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" | ");
102
+ const separator = widths.map((width) => "-".repeat(width)).join("-+-");
103
+ const body = rows.map((cells) => renderRow(cells));
104
+ return [renderRow(headers), separator, ...body].join("\n");
105
+ }
106
+ function formatJson(result) {
107
+ const rows = result.rows.map((row) => {
108
+ const serialized = {};
109
+ for (const [key, value] of Object.entries(row)) {
110
+ serialized[key] = serializeCell(value);
111
+ }
112
+ return serialized;
113
+ });
114
+ return JSON.stringify(rows, null, 2);
115
+ }
116
+ function formatCsv(result) {
117
+ const headers = result.columns.map((column) => column.name);
118
+ const lines = [headers.map((header) => csvEscape(header)).join(",")];
119
+ for (const row of result.rows) {
120
+ lines.push(
121
+ result.columns.map((column) => csvEscape(cellText(row[column.name] ?? null, ""))).join(",")
122
+ );
123
+ }
124
+ return lines.join("\r\n");
125
+ }
126
+ function formatResult(result, format) {
127
+ switch (format) {
128
+ case "table":
129
+ return formatTable(result);
130
+ case "json":
131
+ return formatJson(result);
132
+ case "csv":
133
+ return formatCsv(result);
134
+ }
135
+ }
136
+
46
137
  // src/statements.ts
47
138
  var LEADING_NOISE = /^(?:\s|--[^\n]*\n?|\/\*[\s\S]*?\*\/)+/;
48
139
  var SELECT_KEYWORDS = /* @__PURE__ */ new Set(["SELECT", "WITH"]);
@@ -131,6 +222,182 @@ function assertParamArity(sql, params) {
131
222
  }
132
223
  }
133
224
 
225
+ // src/backup.ts
226
+ var SAPTOOLS_DIR_NAME = ".saptools";
227
+ var CF_HANA_DIR_NAME = "cf-hana";
228
+ var BACKUPS_DIR_NAME = "backups";
229
+ function defaultSaptoolsRoot() {
230
+ return join(homedir(), SAPTOOLS_DIR_NAME);
231
+ }
232
+ function trimStatementSql(sql) {
233
+ return sql.trim().replace(/;+\s*$/, "").trim();
234
+ }
235
+ function isIdentifierChar(char) {
236
+ return char !== void 0 && /[A-Za-z0-9_$#]/.test(char);
237
+ }
238
+ function skipQuotedText(sql, index) {
239
+ const quote = sql[index];
240
+ let cursor = index + 1;
241
+ while (cursor < sql.length) {
242
+ if (sql[cursor] === quote) {
243
+ if (sql[cursor + 1] === quote) {
244
+ cursor += 2;
245
+ continue;
246
+ }
247
+ return cursor + 1;
248
+ }
249
+ cursor += 1;
250
+ }
251
+ return cursor;
252
+ }
253
+ function skipLineComment(sql, index) {
254
+ let cursor = index + 2;
255
+ while (cursor < sql.length && sql[cursor] !== "\n") {
256
+ cursor += 1;
257
+ }
258
+ return cursor;
259
+ }
260
+ function skipBlockComment(sql, index) {
261
+ let cursor = index + 2;
262
+ while (cursor < sql.length && !(sql[cursor] === "*" && sql[cursor + 1] === "/")) {
263
+ cursor += 1;
264
+ }
265
+ return Math.min(cursor + 2, sql.length);
266
+ }
267
+ function skipNonCode(sql, index) {
268
+ const char = sql[index];
269
+ if (char === "'" || char === '"') {
270
+ return skipQuotedText(sql, index);
271
+ }
272
+ if (char === "-" && sql[index + 1] === "-") {
273
+ return skipLineComment(sql, index);
274
+ }
275
+ if (char === "/" && sql[index + 1] === "*") {
276
+ return skipBlockComment(sql, index);
277
+ }
278
+ return void 0;
279
+ }
280
+ function keywordMatches(sql, index, keyword) {
281
+ const end = index + keyword.length;
282
+ return sql.slice(index, end).toUpperCase() === keyword && !isIdentifierChar(sql[index - 1]) && !isIdentifierChar(sql[end]);
283
+ }
284
+ function findTopLevelKeyword(sql, keyword, startIndex = 0) {
285
+ let index = startIndex;
286
+ let depth = 0;
287
+ while (index < sql.length) {
288
+ const skipped = skipNonCode(sql, index);
289
+ if (skipped !== void 0) {
290
+ index = skipped;
291
+ continue;
292
+ }
293
+ const char = sql[index];
294
+ if (char === "(") {
295
+ depth += 1;
296
+ } else if (char === ")" && depth > 0) {
297
+ depth -= 1;
298
+ } else if (depth === 0 && keywordMatches(sql, index, keyword)) {
299
+ return index;
300
+ }
301
+ index += 1;
302
+ }
303
+ return void 0;
304
+ }
305
+ function selectParamsAfterWhere(statementSql, whereIndex, params) {
306
+ return params.slice(countPlaceholders(statementSql.slice(0, whereIndex)));
307
+ }
308
+ function buildSelectPlan(operation, statementSql, targetSql, whereIndex, params) {
309
+ const target = targetSql.trim();
310
+ if (target.length === 0) {
311
+ throw new QueryError(`${operation.toUpperCase()} backup requires a target table`);
312
+ }
313
+ if (whereIndex === void 0) {
314
+ return { operation, statementSql, selectSql: `SELECT * FROM ${target}`, selectParams: [] };
315
+ }
316
+ const whereSql = statementSql.slice(whereIndex + "WHERE".length).trim();
317
+ if (whereSql.length === 0) {
318
+ throw new QueryError(`${operation.toUpperCase()} backup requires a non-empty WHERE clause`);
319
+ }
320
+ return {
321
+ operation,
322
+ statementSql,
323
+ selectSql: `SELECT * FROM ${target} WHERE ${whereSql}`,
324
+ selectParams: selectParamsAfterWhere(statementSql, whereIndex, params)
325
+ };
326
+ }
327
+ function buildUpdateBackupPlan(statementSql, params) {
328
+ const updateIndex = findTopLevelKeyword(statementSql, "UPDATE");
329
+ const setIndex = updateIndex === void 0 ? void 0 : findTopLevelKeyword(statementSql, "SET", updateIndex + "UPDATE".length);
330
+ if (updateIndex === void 0 || setIndex === void 0) {
331
+ throw new QueryError("UPDATE backup requires UPDATE <target> SET syntax");
332
+ }
333
+ const whereIndex = findTopLevelKeyword(statementSql, "WHERE", setIndex + "SET".length);
334
+ return buildSelectPlan(
335
+ "update",
336
+ statementSql,
337
+ statementSql.slice(updateIndex + "UPDATE".length, setIndex),
338
+ whereIndex,
339
+ params
340
+ );
341
+ }
342
+ function buildDeleteBackupPlan(statementSql, params) {
343
+ const deleteIndex = findTopLevelKeyword(statementSql, "DELETE");
344
+ const fromIndex = deleteIndex === void 0 ? void 0 : findTopLevelKeyword(statementSql, "FROM", deleteIndex + "DELETE".length);
345
+ if (deleteIndex === void 0 || fromIndex === void 0) {
346
+ throw new QueryError("DELETE backup requires DELETE FROM <target> syntax");
347
+ }
348
+ const whereIndex = findTopLevelKeyword(statementSql, "WHERE", fromIndex + "FROM".length);
349
+ const targetEnd = whereIndex ?? statementSql.length;
350
+ return buildSelectPlan(
351
+ "delete",
352
+ statementSql,
353
+ statementSql.slice(fromIndex + "FROM".length, targetEnd),
354
+ whereIndex,
355
+ params
356
+ );
357
+ }
358
+ function backupTimestamp(now) {
359
+ return now.toISOString().replace(/:/g, "").replace(".", "");
360
+ }
361
+ function backupHash(statementSql) {
362
+ return createHash("sha256").update(statementSql).update("\0").update(randomUUID()).digest("hex").slice(0, 12);
363
+ }
364
+ function cfHanaBackupRoot(saptoolsRoot) {
365
+ return join(saptoolsRoot ?? defaultSaptoolsRoot(), CF_HANA_DIR_NAME, BACKUPS_DIR_NAME);
366
+ }
367
+ function buildWriteBackupPlan(sql, params = []) {
368
+ const statementSql = trimStatementSql(sql);
369
+ const keyword = firstKeyword(statementSql);
370
+ if (keyword !== "UPDATE" && keyword !== "DELETE") {
371
+ return void 0;
372
+ }
373
+ assertParamArity(statementSql, params);
374
+ if (keyword === "UPDATE") {
375
+ return buildUpdateBackupPlan(statementSql, params);
376
+ }
377
+ return buildDeleteBackupPlan(statementSql, params);
378
+ }
379
+ async function writeSqlBackup(input, options = {}) {
380
+ 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");
387
+ await mkdir(directory, { recursive: true, mode: 448 });
388
+ await Promise.all([
389
+ writeFile(statementPath, `${input.statementSql}
390
+ `, { encoding: "utf8", mode: 384 }),
391
+ writeFile(backupPath, formatCsv(input.result), { encoding: "utf8", mode: 384 })
392
+ ]);
393
+ return {
394
+ directory,
395
+ statementPath,
396
+ backupPath,
397
+ rowCount: input.result.rowCount
398
+ };
399
+ }
400
+
134
401
  // src/builder.ts
135
402
  function buildWhere(where) {
136
403
  if (where === void 0) {
@@ -243,6 +510,7 @@ async function listColumns(connection, schema, table) {
243
510
  }
244
511
 
245
512
  // src/config.ts
513
+ var CLI_VERSION = "0.1.4";
246
514
  var ENV_PREFIX = "CF_HANA";
247
515
  var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
248
516
  var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
@@ -664,10 +932,85 @@ function createDriver(name) {
664
932
  }
665
933
  }
666
934
 
935
+ // src/history.ts
936
+ import { appendFile, mkdir as mkdir2, readdir, rm } from "fs/promises";
937
+ import { homedir as homedir2 } from "os";
938
+ import { join as join2 } from "path";
939
+ var SAPTOOLS_DIR_NAME2 = ".saptools";
940
+ var CF_HANA_DIR_NAME2 = "cf-hana";
941
+ var HISTORIES_DIR_NAME = "histories";
942
+ var HISTORY_RETENTION_DAYS = 5;
943
+ var HISTORY_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
944
+ function defaultSaptoolsRoot2() {
945
+ return join2(homedir2(), SAPTOOLS_DIR_NAME2);
946
+ }
947
+ function padDatePart(value) {
948
+ return value.toString().padStart(2, "0");
949
+ }
950
+ function dateKey(date) {
951
+ return [
952
+ date.getFullYear().toString(),
953
+ padDatePart(date.getMonth() + 1),
954
+ padDatePart(date.getDate())
955
+ ].join("-");
956
+ }
957
+ function retentionCutoffKey(now) {
958
+ const cutoff = new Date(now);
959
+ cutoff.setHours(0, 0, 0, 0);
960
+ cutoff.setDate(cutoff.getDate() - HISTORY_RETENTION_DAYS);
961
+ return dateKey(cutoff);
962
+ }
963
+ function resolveSaptoolsRoot(root) {
964
+ return root ?? defaultSaptoolsRoot2();
965
+ }
966
+ function cfHanaHistoryDirectory(saptoolsRoot) {
967
+ return join2(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME2, HISTORIES_DIR_NAME);
968
+ }
969
+ function sqlHistoryFilePath(now = /* @__PURE__ */ new Date(), saptoolsRoot) {
970
+ return join2(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
971
+ }
972
+ async function pruneSqlHistory(now, saptoolsRoot) {
973
+ const historyDir = cfHanaHistoryDirectory(saptoolsRoot);
974
+ let files;
975
+ try {
976
+ files = await readdir(historyDir);
977
+ } catch (error) {
978
+ if (error.code === "ENOENT") {
979
+ return;
980
+ }
981
+ throw error;
982
+ }
983
+ const cutoffKey = retentionCutoffKey(now);
984
+ await Promise.all(
985
+ files.filter((file) => HISTORY_FILE_PATTERN.test(file)).filter((file) => file.slice(0, 10) < cutoffKey).map(async (file) => {
986
+ await rm(join2(historyDir, file), { force: true });
987
+ })
988
+ );
989
+ }
990
+ async function appendSqlHistory(input, options = {}) {
991
+ const now = options.now ?? /* @__PURE__ */ new Date();
992
+ const historyDir = cfHanaHistoryDirectory(options.saptoolsRoot);
993
+ const entry = {
994
+ at: now.toISOString(),
995
+ ...input
996
+ };
997
+ await mkdir2(historyDir, { recursive: true, mode: 448 });
998
+ await appendFile(sqlHistoryFilePath(now, options.saptoolsRoot), `${JSON.stringify(entry)}
999
+ `, {
1000
+ encoding: "utf8",
1001
+ mode: 384
1002
+ });
1003
+ try {
1004
+ await pruneSqlHistory(now, options.saptoolsRoot);
1005
+ } catch {
1006
+ }
1007
+ return entry;
1008
+ }
1009
+
667
1010
  // src/safety.ts
668
1011
  var DESTRUCTIVE_DDL_KEYWORDS = /* @__PURE__ */ new Set(["DROP", "TRUNCATE", "ALTER"]);
669
1012
  var UNSCOPED_WRITE_KEYWORDS = /* @__PURE__ */ new Set(["UPDATE", "DELETE"]);
670
- function skipQuotedText(sql, start) {
1013
+ function skipQuotedText2(sql, start) {
671
1014
  const quote = sql[start];
672
1015
  let index = start + 1;
673
1016
  while (index < sql.length) {
@@ -683,14 +1026,14 @@ function skipQuotedText(sql, start) {
683
1026
  }
684
1027
  return index;
685
1028
  }
686
- function skipLineComment(sql, start) {
1029
+ function skipLineComment2(sql, start) {
687
1030
  let index = start + 2;
688
1031
  while (index < sql.length && sql[index] !== "\n") {
689
1032
  index += 1;
690
1033
  }
691
1034
  return index;
692
1035
  }
693
- function skipBlockComment(sql, start) {
1036
+ function skipBlockComment2(sql, start) {
694
1037
  let index = start + 2;
695
1038
  while (index < sql.length && !(sql[index] === "*" && sql[index + 1] === "/")) {
696
1039
  index += 1;
@@ -703,19 +1046,19 @@ function maskIgnoredSqlText(sql) {
703
1046
  while (index < sql.length) {
704
1047
  const char = sql[index];
705
1048
  if (char === "'" || char === '"') {
706
- const end = skipQuotedText(sql, index);
1049
+ const end = skipQuotedText2(sql, index);
707
1050
  masked += " ".repeat(end - index);
708
1051
  index = end;
709
1052
  continue;
710
1053
  }
711
1054
  if (char === "-" && sql[index + 1] === "-") {
712
- const end = skipLineComment(sql, index);
1055
+ const end = skipLineComment2(sql, index);
713
1056
  masked += " ".repeat(end - index);
714
1057
  index = end;
715
1058
  continue;
716
1059
  }
717
1060
  if (char === "/" && sql[index + 1] === "*") {
718
- const end = skipBlockComment(sql, index);
1061
+ const end = skipBlockComment2(sql, index);
719
1062
  masked += " ".repeat(end - index);
720
1063
  index = end;
721
1064
  continue;
@@ -733,11 +1076,11 @@ function trailingLineCommentIndex(sql) {
733
1076
  while (index < sql.length) {
734
1077
  const char = sql[index];
735
1078
  if (char === "'" || char === '"') {
736
- index = skipQuotedText(sql, index);
1079
+ index = skipQuotedText2(sql, index);
737
1080
  continue;
738
1081
  }
739
1082
  if (char === "/" && sql[index + 1] === "*") {
740
- index = skipBlockComment(sql, index);
1083
+ index = skipBlockComment2(sql, index);
741
1084
  continue;
742
1085
  }
743
1086
  if (char === "-" && sql[index + 1] === "-") {
@@ -1162,41 +1505,64 @@ var HanaClient = class _HanaClient {
1162
1505
  }
1163
1506
  /** Run a SELECT (or any read) statement and return typed rows. */
1164
1507
  async query(sql, params, options) {
1165
- return await this.pool.withConnection(
1166
- (connection) => connection.query(sql, params, options)
1167
- );
1508
+ const resolvedParams = params ?? [];
1509
+ const result = await this.runQuery(sql, resolvedParams, options);
1510
+ await this.recordSqlHistory("query", sql, resolvedParams, result);
1511
+ return result;
1168
1512
  }
1169
1513
  /** Run a DML/DDL statement and return its affected-row count. */
1170
1514
  async execute(sql, params, options) {
1171
- return await this.pool.withConnection(
1172
- (connection) => connection.execute(sql, params, options)
1173
- );
1515
+ const resolvedParams = params ?? [];
1516
+ const result = await this.runExecute(sql, resolvedParams, options);
1517
+ await this.recordSqlHistory("execute", sql, resolvedParams, result);
1518
+ return result;
1519
+ }
1520
+ /** Back up rows matched by an UPDATE or DELETE before the caller runs it. */
1521
+ async backupWriteStatement(sql, params, options) {
1522
+ const resolvedParams = params ?? [];
1523
+ const plan = buildWriteBackupPlan(sql, resolvedParams);
1524
+ if (plan === void 0) {
1525
+ return void 0;
1526
+ }
1527
+ return await this.pool.withConnection(async (connection) => {
1528
+ connection.assertAllowed(plan.statementSql, options);
1529
+ const backupQueryOptions = {
1530
+ autoLimit: false,
1531
+ ...options?.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }
1532
+ };
1533
+ const result = await connection.query(plan.selectSql, plan.selectParams, backupQueryOptions);
1534
+ return await writeSqlBackup({
1535
+ operation: plan.operation,
1536
+ statementSql: plan.statementSql,
1537
+ result
1538
+ });
1539
+ });
1174
1540
  }
1175
1541
  /** Run a typed `SELECT` built from a spec. */
1176
1542
  async selectFrom(spec) {
1177
1543
  const built = buildSelect(spec);
1178
- return await this.query(built.sql, built.params);
1544
+ return await this.runQuery(built.sql, built.params);
1179
1545
  }
1180
1546
  /** Count rows in a table, optionally filtered. */
1181
1547
  async count(spec) {
1182
1548
  const built = buildCount(spec);
1183
- const result = await this.query(built.sql, built.params);
1549
+ const result = await this.runQuery(built.sql, built.params);
1184
1550
  return result.rows[0]?.COUNT ?? 0;
1185
1551
  }
1186
1552
  /** Insert a single row. */
1187
1553
  async insertInto(schema, table, values) {
1188
1554
  const built = buildInsert(schema, table, values);
1189
- return await this.execute(built.sql, built.params);
1555
+ return await this.runExecute(built.sql, built.params);
1190
1556
  }
1191
1557
  /** Update rows matching a non-empty `where` filter. */
1192
1558
  async update(schema, table, values, where) {
1193
1559
  const built = buildUpdate(schema, table, values, where);
1194
- return await this.execute(built.sql, built.params);
1560
+ return await this.runExecute(built.sql, built.params);
1195
1561
  }
1196
1562
  /** Delete rows matching a non-empty `where` filter. */
1197
1563
  async deleteFrom(schema, table, where) {
1198
1564
  const built = buildDelete(schema, table, where);
1199
- return await this.execute(built.sql, built.params);
1565
+ return await this.runExecute(built.sql, built.params);
1200
1566
  }
1201
1567
  /** Run `work` inside a transaction, auto-committing on success. */
1202
1568
  async transaction(work) {
@@ -1275,6 +1641,35 @@ var HanaClient = class _HanaClient {
1275
1641
  async close() {
1276
1642
  await this.pool.drain();
1277
1643
  }
1644
+ async runQuery(sql, params, options) {
1645
+ return await this.pool.withConnection(
1646
+ (connection) => connection.query(sql, params, options)
1647
+ );
1648
+ }
1649
+ async runExecute(sql, params, options) {
1650
+ return await this.pool.withConnection(
1651
+ (connection) => connection.execute(sql, params, options)
1652
+ );
1653
+ }
1654
+ async recordSqlHistory(operation, sql, params, result) {
1655
+ try {
1656
+ await appendSqlHistory({
1657
+ version: CLI_VERSION,
1658
+ operation,
1659
+ selector: this.info.selector,
1660
+ appName: this.info.appName,
1661
+ schema: this.info.schema,
1662
+ role: this.info.role,
1663
+ statement: result.statement,
1664
+ sql,
1665
+ paramCount: params.length,
1666
+ rowCount: result.rowCount,
1667
+ truncated: result.truncated,
1668
+ elapsedMs: result.elapsedMs
1669
+ });
1670
+ } catch {
1671
+ }
1672
+ }
1278
1673
  };
1279
1674
 
1280
1675
  // src/api.ts
@@ -1297,91 +1692,6 @@ async function withConnection(selector, work, options) {
1297
1692
  await client.close();
1298
1693
  }
1299
1694
  }
1300
-
1301
- // src/format.ts
1302
- function cellText(value, nullText) {
1303
- if (value === null) {
1304
- return nullText;
1305
- }
1306
- if (value instanceof Date) {
1307
- return value.toISOString();
1308
- }
1309
- if (Buffer.isBuffer(value)) {
1310
- return `0x${value.toString("hex")}`;
1311
- }
1312
- if (typeof value === "boolean") {
1313
- return value ? "true" : "false";
1314
- }
1315
- return typeof value === "number" ? value.toString() : value;
1316
- }
1317
- function serializeCell(value) {
1318
- if (value === null) {
1319
- return null;
1320
- }
1321
- if (value instanceof Date) {
1322
- return value.toISOString();
1323
- }
1324
- if (Buffer.isBuffer(value)) {
1325
- return `0x${value.toString("hex")}`;
1326
- }
1327
- return value;
1328
- }
1329
- function csvEscape(text) {
1330
- if (/[",\r\n]/.test(text)) {
1331
- return `"${text.replace(/"/g, '""')}"`;
1332
- }
1333
- return text;
1334
- }
1335
- function formatTable(result) {
1336
- if (result.columns.length === 0) {
1337
- return `(${String(result.rowCount)} row(s) affected)`;
1338
- }
1339
- const headers = result.columns.map((column) => column.name);
1340
- const rows = result.rows.map(
1341
- (row) => result.columns.map((column) => cellText(row[column.name] ?? null, "NULL"))
1342
- );
1343
- const widths = headers.map((header, index) => {
1344
- const widest = rows.reduce(
1345
- (max, cells) => Math.max(max, (cells[index] ?? "").length),
1346
- header.length
1347
- );
1348
- return widest;
1349
- });
1350
- const renderRow = (cells) => cells.map((cell, index) => cell.padEnd(widths[index] ?? 0)).join(" | ");
1351
- const separator = widths.map((width) => "-".repeat(width)).join("-+-");
1352
- const body = rows.map((cells) => renderRow(cells));
1353
- return [renderRow(headers), separator, ...body].join("\n");
1354
- }
1355
- function formatJson(result) {
1356
- const rows = result.rows.map((row) => {
1357
- const serialized = {};
1358
- for (const [key, value] of Object.entries(row)) {
1359
- serialized[key] = serializeCell(value);
1360
- }
1361
- return serialized;
1362
- });
1363
- return JSON.stringify(rows, null, 2);
1364
- }
1365
- function formatCsv(result) {
1366
- const headers = result.columns.map((column) => column.name);
1367
- const lines = [headers.map((header) => csvEscape(header)).join(",")];
1368
- for (const row of result.rows) {
1369
- lines.push(
1370
- result.columns.map((column) => csvEscape(cellText(row[column.name] ?? null, ""))).join(",")
1371
- );
1372
- }
1373
- return lines.join("\r\n");
1374
- }
1375
- function formatResult(result, format) {
1376
- switch (format) {
1377
- case "table":
1378
- return formatTable(result);
1379
- case "json":
1380
- return formatJson(result);
1381
- case "csv":
1382
- return formatCsv(result);
1383
- }
1384
- }
1385
1695
  export {
1386
1696
  CfHanaError,
1387
1697
  CredentialsNotFoundError,
@@ -1395,6 +1705,8 @@ export {
1395
1705
  buildInsert,
1396
1706
  buildSelect,
1397
1707
  buildUpdate,
1708
+ buildWriteBackupPlan,
1709
+ cfHanaBackupRoot,
1398
1710
  connect,
1399
1711
  createDriver,
1400
1712
  errorMessage,
@@ -1403,6 +1715,7 @@ export {
1403
1715
  formatResult,
1404
1716
  formatTable,
1405
1717
  query,
1406
- withConnection
1718
+ withConnection,
1719
+ writeSqlBackup
1407
1720
  };
1408
1721
  //# sourceMappingURL=index.js.map