@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.3 - 2026-06-23
|
|
4
|
+
|
|
5
|
+
- Add local SQL history for successful direct `query` and `execute` calls under
|
|
6
|
+
`~/.saptools/cf-hana/histories/YYYY-MM-DD.jsonl`.
|
|
7
|
+
- Rotate SQL history with five-day retention and keep parameter values,
|
|
8
|
+
credentials, certificates, and result rows out of the history file.
|
|
9
|
+
- Keep helper-driven catalog SQL out of user SQL history and document the new
|
|
10
|
+
local state behavior.
|
|
11
|
+
|
|
3
12
|
## 0.1.2 - 2026-06-23
|
|
4
13
|
|
|
5
14
|
- Harden connection pooling so queued callers continue after transient reconnect failures.
|
package/README.md
CHANGED
|
@@ -22,6 +22,8 @@ login on the hot path) and connections are pooled and reused within a process.
|
|
|
22
22
|
- **Query-builder shorthands** — `selectFrom`, `count`, `insertInto`, `update`,
|
|
23
23
|
`deleteFrom` — query a table by name without writing SQL.
|
|
24
24
|
- **Schema introspection** — list schemas, tables, and columns.
|
|
25
|
+
- **Local SQL history** — direct SQL calls are appended to dated JSONL files
|
|
26
|
+
under `~/.saptools/cf-hana/histories/` with five-day retention.
|
|
25
27
|
- **Safety guard** — opt-in read-only mode and a destructive-statement guard
|
|
26
28
|
(blocks `DROP`/`TRUNCATE`/`ALTER` and unscoped `UPDATE`/`DELETE`).
|
|
27
29
|
- **Typed results** — `query<TRow>()` returns typed rows.
|
|
@@ -44,7 +46,7 @@ driver is bundled as a dependency — there is no native build step.
|
|
|
44
46
|
import { connect, query } from "@saptools/cf-hana";
|
|
45
47
|
|
|
46
48
|
// Open a reusable, pooled client for one CF app's HANA database.
|
|
47
|
-
const db = await connect("eu10/
|
|
49
|
+
const db = await connect("eu10/example-org/space-demo/app-demo");
|
|
48
50
|
|
|
49
51
|
const open = await db.query("SELECT ID, STATUS FROM ORDERS WHERE STATUS = ?", ["OPEN"]);
|
|
50
52
|
console.log(open.rows);
|
|
@@ -58,16 +60,16 @@ await db.transaction(async (tx) => {
|
|
|
58
60
|
await db.close();
|
|
59
61
|
|
|
60
62
|
// One-shot: connect, run one query, close.
|
|
61
|
-
const rows = await query("
|
|
63
|
+
const rows = await query("app-demo", "SELECT COUNT(*) AS N FROM ORDERS");
|
|
62
64
|
```
|
|
63
65
|
|
|
64
66
|
## The selector
|
|
65
67
|
|
|
66
68
|
Every entry point takes a selector as its first argument:
|
|
67
69
|
|
|
68
|
-
- **Explicit** — `region/org/space/app` (e.g.
|
|
69
|
-
without any cached topology.
|
|
70
|
-
- **Bare app name** — `
|
|
70
|
+
- **Explicit** — `region/org/space/app` (e.g.
|
|
71
|
+
`eu10/example-org/space-demo/app-demo`). Works without any cached topology.
|
|
72
|
+
- **Bare app name** — `app-demo`. Resolved against the topology cached by
|
|
71
73
|
`cf-sync sync`; throws if the name is ambiguous across spaces.
|
|
72
74
|
|
|
73
75
|
## CLI
|
|
@@ -87,11 +89,11 @@ Common options: `--format <table|json|csv>`, `--refresh`, `--role <runtime|hdi>`
|
|
|
87
89
|
accepts `--param <value>` (repeatable) to bind `?` placeholders.
|
|
88
90
|
|
|
89
91
|
```bash
|
|
90
|
-
cf-hana query eu10/
|
|
92
|
+
cf-hana query eu10/example-org/space-demo/app-demo "SELECT ID, STATUS FROM ORDERS WHERE STATUS = ?" \
|
|
91
93
|
--param OPEN --format json
|
|
92
|
-
cf-hana tables
|
|
93
|
-
cf-hana columns
|
|
94
|
-
cf-hana ping eu10/
|
|
94
|
+
cf-hana tables app-demo
|
|
95
|
+
cf-hana columns app-demo ORDERS_APP.ORDERS
|
|
96
|
+
cf-hana ping eu10/example-org/space-demo/app-demo
|
|
95
97
|
```
|
|
96
98
|
|
|
97
99
|
## Programmatic API
|
|
@@ -117,11 +119,28 @@ Credentials are resolved **cache-first**:
|
|
|
117
119
|
live from Cloud Foundry. The live fetch needs `SAP_EMAIL` and `SAP_PASSWORD`
|
|
118
120
|
(or the `email` / `password` options) and never persists anything to disk.
|
|
119
121
|
|
|
120
|
-
|
|
122
|
+
Credential resolution writes nothing under `~/.saptools/` — it only reads what
|
|
121
123
|
`cf-sync` cached. The connection pool is in-process and in-memory only, so it is
|
|
122
124
|
safe to run many `cf-hana` processes in parallel and alongside any `cf-sync`
|
|
123
125
|
command.
|
|
124
126
|
|
|
127
|
+
## SQL history
|
|
128
|
+
|
|
129
|
+
Successful direct SQL calls are appended to daily JSONL files:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
~/.saptools/cf-hana/histories/YYYY-MM-DD.jsonl
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Each entry includes the timestamp, package version, selector, app name, schema,
|
|
136
|
+
role, operation (`query` or `execute`), statement kind, SQL text, parameter
|
|
137
|
+
count, row count, truncation flag, and elapsed time. Parameter values,
|
|
138
|
+
credentials, certificates, and result rows are not stored.
|
|
139
|
+
|
|
140
|
+
History retention runs opportunistically after each append and deletes dated
|
|
141
|
+
history files older than five days. Helper-driven catalog SQL such as `tables`
|
|
142
|
+
and `columns` is not recorded as user SQL history.
|
|
143
|
+
|
|
125
144
|
## Safety
|
|
126
145
|
|
|
127
146
|
- **Read-only mode** (`readOnly` / `--read-only`) rejects every DML and DDL statement.
|
package/dist/cli.js
CHANGED
|
@@ -247,7 +247,7 @@ async function listColumns(connection, schema, table) {
|
|
|
247
247
|
|
|
248
248
|
// src/config.ts
|
|
249
249
|
var CLI_NAME = "cf-hana";
|
|
250
|
-
var CLI_VERSION = "0.1.
|
|
250
|
+
var CLI_VERSION = "0.1.3";
|
|
251
251
|
var ENV_PREFIX = "CF_HANA";
|
|
252
252
|
var DEFAULT_QUERY_TIMEOUT_MS = 3e4;
|
|
253
253
|
var DEFAULT_CONNECT_TIMEOUT_MS = 3e4;
|
|
@@ -669,6 +669,81 @@ function createDriver(name) {
|
|
|
669
669
|
}
|
|
670
670
|
}
|
|
671
671
|
|
|
672
|
+
// src/history.ts
|
|
673
|
+
import { appendFile, mkdir, readdir, rm } from "fs/promises";
|
|
674
|
+
import { homedir } from "os";
|
|
675
|
+
import { join } from "path";
|
|
676
|
+
var SAPTOOLS_DIR_NAME = ".saptools";
|
|
677
|
+
var CF_HANA_DIR_NAME = "cf-hana";
|
|
678
|
+
var HISTORIES_DIR_NAME = "histories";
|
|
679
|
+
var HISTORY_RETENTION_DAYS = 5;
|
|
680
|
+
var HISTORY_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
681
|
+
function defaultSaptoolsRoot() {
|
|
682
|
+
return join(homedir(), SAPTOOLS_DIR_NAME);
|
|
683
|
+
}
|
|
684
|
+
function padDatePart(value) {
|
|
685
|
+
return value.toString().padStart(2, "0");
|
|
686
|
+
}
|
|
687
|
+
function dateKey(date) {
|
|
688
|
+
return [
|
|
689
|
+
date.getFullYear().toString(),
|
|
690
|
+
padDatePart(date.getMonth() + 1),
|
|
691
|
+
padDatePart(date.getDate())
|
|
692
|
+
].join("-");
|
|
693
|
+
}
|
|
694
|
+
function retentionCutoffKey(now) {
|
|
695
|
+
const cutoff = new Date(now);
|
|
696
|
+
cutoff.setHours(0, 0, 0, 0);
|
|
697
|
+
cutoff.setDate(cutoff.getDate() - HISTORY_RETENTION_DAYS);
|
|
698
|
+
return dateKey(cutoff);
|
|
699
|
+
}
|
|
700
|
+
function resolveSaptoolsRoot(root) {
|
|
701
|
+
return root ?? defaultSaptoolsRoot();
|
|
702
|
+
}
|
|
703
|
+
function cfHanaHistoryDirectory(saptoolsRoot) {
|
|
704
|
+
return join(resolveSaptoolsRoot(saptoolsRoot), CF_HANA_DIR_NAME, HISTORIES_DIR_NAME);
|
|
705
|
+
}
|
|
706
|
+
function sqlHistoryFilePath(now = /* @__PURE__ */ new Date(), saptoolsRoot) {
|
|
707
|
+
return join(cfHanaHistoryDirectory(saptoolsRoot), `${dateKey(now)}.jsonl`);
|
|
708
|
+
}
|
|
709
|
+
async function pruneSqlHistory(now, saptoolsRoot) {
|
|
710
|
+
const historyDir = cfHanaHistoryDirectory(saptoolsRoot);
|
|
711
|
+
let files;
|
|
712
|
+
try {
|
|
713
|
+
files = await readdir(historyDir);
|
|
714
|
+
} catch (error) {
|
|
715
|
+
if (error.code === "ENOENT") {
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
throw error;
|
|
719
|
+
}
|
|
720
|
+
const cutoffKey = retentionCutoffKey(now);
|
|
721
|
+
await Promise.all(
|
|
722
|
+
files.filter((file) => HISTORY_FILE_PATTERN.test(file)).filter((file) => file.slice(0, 10) < cutoffKey).map(async (file) => {
|
|
723
|
+
await rm(join(historyDir, file), { force: true });
|
|
724
|
+
})
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
async function appendSqlHistory(input, options = {}) {
|
|
728
|
+
const now = options.now ?? /* @__PURE__ */ new Date();
|
|
729
|
+
const historyDir = cfHanaHistoryDirectory(options.saptoolsRoot);
|
|
730
|
+
const entry = {
|
|
731
|
+
at: now.toISOString(),
|
|
732
|
+
...input
|
|
733
|
+
};
|
|
734
|
+
await mkdir(historyDir, { recursive: true, mode: 448 });
|
|
735
|
+
await appendFile(sqlHistoryFilePath(now, options.saptoolsRoot), `${JSON.stringify(entry)}
|
|
736
|
+
`, {
|
|
737
|
+
encoding: "utf8",
|
|
738
|
+
mode: 384
|
|
739
|
+
});
|
|
740
|
+
try {
|
|
741
|
+
await pruneSqlHistory(now, options.saptoolsRoot);
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
return entry;
|
|
745
|
+
}
|
|
746
|
+
|
|
672
747
|
// src/safety.ts
|
|
673
748
|
var DESTRUCTIVE_DDL_KEYWORDS = /* @__PURE__ */ new Set(["DROP", "TRUNCATE", "ALTER"]);
|
|
674
749
|
var UNSCOPED_WRITE_KEYWORDS = /* @__PURE__ */ new Set(["UPDATE", "DELETE"]);
|
|
@@ -1167,41 +1242,43 @@ var HanaClient = class _HanaClient {
|
|
|
1167
1242
|
}
|
|
1168
1243
|
/** Run a SELECT (or any read) statement and return typed rows. */
|
|
1169
1244
|
async query(sql, params, options) {
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
);
|
|
1245
|
+
const resolvedParams = params ?? [];
|
|
1246
|
+
const result = await this.runQuery(sql, resolvedParams, options);
|
|
1247
|
+
await this.recordSqlHistory("query", sql, resolvedParams, result);
|
|
1248
|
+
return result;
|
|
1173
1249
|
}
|
|
1174
1250
|
/** Run a DML/DDL statement and return its affected-row count. */
|
|
1175
1251
|
async execute(sql, params, options) {
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
);
|
|
1252
|
+
const resolvedParams = params ?? [];
|
|
1253
|
+
const result = await this.runExecute(sql, resolvedParams, options);
|
|
1254
|
+
await this.recordSqlHistory("execute", sql, resolvedParams, result);
|
|
1255
|
+
return result;
|
|
1179
1256
|
}
|
|
1180
1257
|
/** Run a typed `SELECT` built from a spec. */
|
|
1181
1258
|
async selectFrom(spec) {
|
|
1182
1259
|
const built = buildSelect(spec);
|
|
1183
|
-
return await this.
|
|
1260
|
+
return await this.runQuery(built.sql, built.params);
|
|
1184
1261
|
}
|
|
1185
1262
|
/** Count rows in a table, optionally filtered. */
|
|
1186
1263
|
async count(spec) {
|
|
1187
1264
|
const built = buildCount(spec);
|
|
1188
|
-
const result = await this.
|
|
1265
|
+
const result = await this.runQuery(built.sql, built.params);
|
|
1189
1266
|
return result.rows[0]?.COUNT ?? 0;
|
|
1190
1267
|
}
|
|
1191
1268
|
/** Insert a single row. */
|
|
1192
1269
|
async insertInto(schema, table, values) {
|
|
1193
1270
|
const built = buildInsert(schema, table, values);
|
|
1194
|
-
return await this.
|
|
1271
|
+
return await this.runExecute(built.sql, built.params);
|
|
1195
1272
|
}
|
|
1196
1273
|
/** Update rows matching a non-empty `where` filter. */
|
|
1197
1274
|
async update(schema, table, values, where) {
|
|
1198
1275
|
const built = buildUpdate(schema, table, values, where);
|
|
1199
|
-
return await this.
|
|
1276
|
+
return await this.runExecute(built.sql, built.params);
|
|
1200
1277
|
}
|
|
1201
1278
|
/** Delete rows matching a non-empty `where` filter. */
|
|
1202
1279
|
async deleteFrom(schema, table, where) {
|
|
1203
1280
|
const built = buildDelete(schema, table, where);
|
|
1204
|
-
return await this.
|
|
1281
|
+
return await this.runExecute(built.sql, built.params);
|
|
1205
1282
|
}
|
|
1206
1283
|
/** Run `work` inside a transaction, auto-committing on success. */
|
|
1207
1284
|
async transaction(work) {
|
|
@@ -1280,6 +1357,35 @@ var HanaClient = class _HanaClient {
|
|
|
1280
1357
|
async close() {
|
|
1281
1358
|
await this.pool.drain();
|
|
1282
1359
|
}
|
|
1360
|
+
async runQuery(sql, params, options) {
|
|
1361
|
+
return await this.pool.withConnection(
|
|
1362
|
+
(connection) => connection.query(sql, params, options)
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
async runExecute(sql, params, options) {
|
|
1366
|
+
return await this.pool.withConnection(
|
|
1367
|
+
(connection) => connection.execute(sql, params, options)
|
|
1368
|
+
);
|
|
1369
|
+
}
|
|
1370
|
+
async recordSqlHistory(operation, sql, params, result) {
|
|
1371
|
+
try {
|
|
1372
|
+
await appendSqlHistory({
|
|
1373
|
+
version: CLI_VERSION,
|
|
1374
|
+
operation,
|
|
1375
|
+
selector: this.info.selector,
|
|
1376
|
+
appName: this.info.appName,
|
|
1377
|
+
schema: this.info.schema,
|
|
1378
|
+
role: this.info.role,
|
|
1379
|
+
statement: result.statement,
|
|
1380
|
+
sql,
|
|
1381
|
+
paramCount: params.length,
|
|
1382
|
+
rowCount: result.rowCount,
|
|
1383
|
+
truncated: result.truncated,
|
|
1384
|
+
elapsedMs: result.elapsedMs
|
|
1385
|
+
});
|
|
1386
|
+
} catch {
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1283
1389
|
};
|
|
1284
1390
|
|
|
1285
1391
|
// src/api.ts
|