@saptools/cf-hana 0.1.2 → 0.1.3

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
@@ -243,6 +243,7 @@ async function listColumns(connection, schema, table) {
243
243
  }
244
244
 
245
245
  // src/config.ts
246
+ var CLI_VERSION = "0.1.3";
246
247
  var ENV_PREFIX = "CF_HANA";
247
248
  var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
248
249
  var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
@@ -664,6 +665,81 @@ function createDriver(name) {
664
665
  }
665
666
  }
666
667
 
668
+ // src/history.ts
669
+ import { appendFile, mkdir, readdir, rm } from "fs/promises";
670
+ import { homedir } from "os";
671
+ import { join } from "path";
672
+ var SAPTOOLS_DIR_NAME = ".saptools";
673
+ var CF_HANA_DIR_NAME = "cf-hana";
674
+ var HISTORIES_DIR_NAME = "histories";
675
+ var HISTORY_RETENTION_DAYS = 5;
676
+ var HISTORY_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
677
+ function defaultSaptoolsRoot() {
678
+ return join(homedir(), SAPTOOLS_DIR_NAME);
679
+ }
680
+ function padDatePart(value) {
681
+ return value.toString().padStart(2, "0");
682
+ }
683
+ function dateKey(date) {
684
+ return [
685
+ date.getFullYear().toString(),
686
+ padDatePart(date.getMonth() + 1),
687
+ padDatePart(date.getDate())
688
+ ].join("-");
689
+ }
690
+ function retentionCutoffKey(now) {
691
+ const cutoff = new Date(now);
692
+ cutoff.setHours(0, 0, 0, 0);
693
+ cutoff.setDate(cutoff.getDate() - HISTORY_RETENTION_DAYS);
694
+ return dateKey(cutoff);
695
+ }
696
+ function resolveSaptoolsRoot(root) {
697
+ return root ?? defaultSaptoolsRoot();
698
+ }
699
+ function cfHanaHistoryDirectory(saptoolsRoot) {
700
+ return join(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME, HISTORIES_DIR_NAME);
701
+ }
702
+ function sqlHistoryFilePath(now = /* @__PURE__ */ new Date(), saptoolsRoot) {
703
+ return join(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
704
+ }
705
+ async function pruneSqlHistory(now, saptoolsRoot) {
706
+ const historyDir = cfHanaHistoryDirectory(saptoolsRoot);
707
+ let files;
708
+ try {
709
+ files = await readdir(historyDir);
710
+ } catch (error) {
711
+ if (error.code === "ENOENT") {
712
+ return;
713
+ }
714
+ throw error;
715
+ }
716
+ const cutoffKey = retentionCutoffKey(now);
717
+ await Promise.all(
718
+ files.filter((file) => HISTORY_FILE_PATTERN.test(file)).filter((file) => file.slice(0, 10) < cutoffKey).map(async (file) => {
719
+ await rm(join(historyDir, file), { force: true });
720
+ })
721
+ );
722
+ }
723
+ async function appendSqlHistory(input, options = {}) {
724
+ const now = options.now ?? /* @__PURE__ */ new Date();
725
+ const historyDir = cfHanaHistoryDirectory(options.saptoolsRoot);
726
+ const entry = {
727
+ at: now.toISOString(),
728
+ ...input
729
+ };
730
+ await mkdir(historyDir, { recursive: true, mode: 448 });
731
+ await appendFile(sqlHistoryFilePath(now, options.saptoolsRoot), `${JSON.stringify(entry)}
732
+ `, {
733
+ encoding: "utf8",
734
+ mode: 384
735
+ });
736
+ try {
737
+ await pruneSqlHistory(now, options.saptoolsRoot);
738
+ } catch {
739
+ }
740
+ return entry;
741
+ }
742
+
667
743
  // src/safety.ts
668
744
  var DESTRUCTIVE_DDL_KEYWORDS = /* @__PURE__ */ new Set(["DROP", "TRUNCATE", "ALTER"]);
669
745
  var UNSCOPED_WRITE_KEYWORDS = /* @__PURE__ */ new Set(["UPDATE", "DELETE"]);
@@ -1162,41 +1238,43 @@ var HanaClient = class _HanaClient {
1162
1238
  }
1163
1239
  /** Run a SELECT (or any read) statement and return typed rows. */
1164
1240
  async query(sql, params, options) {
1165
- return await this.pool.withConnection(
1166
- (connection) => connection.query(sql, params, options)
1167
- );
1241
+ const resolvedParams = params ?? [];
1242
+ const result = await this.runQuery(sql, resolvedParams, options);
1243
+ await this.recordSqlHistory("query", sql, resolvedParams, result);
1244
+ return result;
1168
1245
  }
1169
1246
  /** Run a DML/DDL statement and return its affected-row count. */
1170
1247
  async execute(sql, params, options) {
1171
- return await this.pool.withConnection(
1172
- (connection) => connection.execute(sql, params, options)
1173
- );
1248
+ const resolvedParams = params ?? [];
1249
+ const result = await this.runExecute(sql, resolvedParams, options);
1250
+ await this.recordSqlHistory("execute", sql, resolvedParams, result);
1251
+ return result;
1174
1252
  }
1175
1253
  /** Run a typed `SELECT` built from a spec. */
1176
1254
  async selectFrom(spec) {
1177
1255
  const built = buildSelect(spec);
1178
- return await this.query(built.sql, built.params);
1256
+ return await this.runQuery(built.sql, built.params);
1179
1257
  }
1180
1258
  /** Count rows in a table, optionally filtered. */
1181
1259
  async count(spec) {
1182
1260
  const built = buildCount(spec);
1183
- const result = await this.query(built.sql, built.params);
1261
+ const result = await this.runQuery(built.sql, built.params);
1184
1262
  return result.rows[0]?.COUNT ?? 0;
1185
1263
  }
1186
1264
  /** Insert a single row. */
1187
1265
  async insertInto(schema, table, values) {
1188
1266
  const built = buildInsert(schema, table, values);
1189
- return await this.execute(built.sql, built.params);
1267
+ return await this.runExecute(built.sql, built.params);
1190
1268
  }
1191
1269
  /** Update rows matching a non-empty `where` filter. */
1192
1270
  async update(schema, table, values, where) {
1193
1271
  const built = buildUpdate(schema, table, values, where);
1194
- return await this.execute(built.sql, built.params);
1272
+ return await this.runExecute(built.sql, built.params);
1195
1273
  }
1196
1274
  /** Delete rows matching a non-empty `where` filter. */
1197
1275
  async deleteFrom(schema, table, where) {
1198
1276
  const built = buildDelete(schema, table, where);
1199
- return await this.execute(built.sql, built.params);
1277
+ return await this.runExecute(built.sql, built.params);
1200
1278
  }
1201
1279
  /** Run `work` inside a transaction, auto-committing on success. */
1202
1280
  async transaction(work) {
@@ -1275,6 +1353,35 @@ var HanaClient = class _HanaClient {
1275
1353
  async close() {
1276
1354
  await this.pool.drain();
1277
1355
  }
1356
+ async runQuery(sql, params, options) {
1357
+ return await this.pool.withConnection(
1358
+ (connection) => connection.query(sql, params, options)
1359
+ );
1360
+ }
1361
+ async runExecute(sql, params, options) {
1362
+ return await this.pool.withConnection(
1363
+ (connection) => connection.execute(sql, params, options)
1364
+ );
1365
+ }
1366
+ async recordSqlHistory(operation, sql, params, result) {
1367
+ try {
1368
+ await appendSqlHistory({
1369
+ version: CLI_VERSION,
1370
+ operation,
1371
+ selector: this.info.selector,
1372
+ appName: this.info.appName,
1373
+ schema: this.info.schema,
1374
+ role: this.info.role,
1375
+ statement: result.statement,
1376
+ sql,
1377
+ paramCount: params.length,
1378
+ rowCount: result.rowCount,
1379
+ truncated: result.truncated,
1380
+ elapsedMs: result.elapsedMs
1381
+ });
1382
+ } catch {
1383
+ }
1384
+ }
1278
1385
  };
1279
1386
 
1280
1387
  // src/api.ts