@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/CHANGELOG.md +9 -0
- package/README.md +29 -10
- package/dist/cli.js +118 -12
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +118 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
1166
|
-
|
|
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
|
-
|
|
1172
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|