@hedeqiang/dsh-nl2sql 0.0.0-stage → 0.11.1
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 +52 -0
- package/LICENSE +21 -0
- package/README.en.md +63 -0
- package/README.md +88 -2
- package/analysis.js +888 -0
- package/channels/dingtalk.js +574 -0
- package/channels/http.js +63 -0
- package/channels/index.js +138 -0
- package/channels/lark.js +1180 -0
- package/channels/message.js +267 -0
- package/channels/telegram.js +663 -0
- package/channels/wecom.js +585 -0
- package/cordis.patch.yml +68 -0
- package/docs/CHANNELS.md +184 -0
- package/drivers.js +694 -0
- package/filters.js +237 -0
- package/icon.svg +13 -0
- package/im.js +8 -0
- package/index.js +658 -0
- package/lark-bot.js +9 -0
- package/lib/client.echarts.js +57 -0
- package/lib/client.js +5757 -0
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -0
- package/package.json +95 -4
- package/policy.js +362 -0
- package/service/access.js +239 -0
- package/service/admin.js +555 -0
- package/service/alerts.js +233 -0
- package/service/bots.js +665 -0
- package/service/channels.js +363 -0
- package/service/dashboards.js +359 -0
- package/service/datasources.js +415 -0
- package/service/evaluation.js +121 -0
- package/service/push.js +163 -0
- package/service/query.js +411 -0
- package/service/schema.js +507 -0
- package/service/shared.js +424 -0
- package/service/suggestions.js +772 -0
- package/service/uploads.js +258 -0
- package/sql-guard.js +222 -0
- package/sqlite-driver.js +677 -0
- package/store.js +225 -0
- package/xlsx.js +1001 -0
package/store.js
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable result store and query cache on SQLite (node:sqlite).
|
|
3
|
+
*
|
|
4
|
+
* Results survive restarts so tool cards, dashboards and IM pushes can still
|
|
5
|
+
* open them; the cache serves identical queries within a TTL.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash } from 'node:crypto'
|
|
8
|
+
import { DatabaseSync } from 'node:sqlite'
|
|
9
|
+
import { chmodSync, closeSync, existsSync, openSync } from 'node:fs'
|
|
10
|
+
|
|
11
|
+
const MAX_RESULTS = 5000
|
|
12
|
+
const MAX_AGE_MS = 30 * 24 * 3600 * 1000
|
|
13
|
+
const MAX_RESULT_BYTES = 8 * 1024 * 1024
|
|
14
|
+
|
|
15
|
+
export class ResultStore {
|
|
16
|
+
constructor(file) {
|
|
17
|
+
this.file = file
|
|
18
|
+
if (!existsSync(file)) closeSync(openSync(file, 'a', 0o600))
|
|
19
|
+
this.db = new DatabaseSync(file)
|
|
20
|
+
for (const f of [file, `${file}-wal`, `${file}-shm`]) { try { chmodSync(f, 0o600) } catch {} }
|
|
21
|
+
this.db.exec(`
|
|
22
|
+
PRAGMA journal_mode = WAL;
|
|
23
|
+
PRAGMA synchronous = NORMAL;
|
|
24
|
+
CREATE TABLE IF NOT EXISTS results (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
created_at INTEGER NOT NULL,
|
|
27
|
+
datasource_id TEXT,
|
|
28
|
+
title TEXT,
|
|
29
|
+
bytes INTEGER,
|
|
30
|
+
body TEXT NOT NULL
|
|
31
|
+
);
|
|
32
|
+
CREATE INDEX IF NOT EXISTS results_created ON results(created_at);
|
|
33
|
+
CREATE TABLE IF NOT EXISTS cache (
|
|
34
|
+
key TEXT PRIMARY KEY,
|
|
35
|
+
result_id TEXT NOT NULL,
|
|
36
|
+
created_at INTEGER NOT NULL
|
|
37
|
+
);
|
|
38
|
+
CREATE TABLE IF NOT EXISTS snapshots (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
kind TEXT NOT NULL,
|
|
41
|
+
ref TEXT,
|
|
42
|
+
created_at INTEGER NOT NULL,
|
|
43
|
+
body TEXT NOT NULL
|
|
44
|
+
);
|
|
45
|
+
CREATE INDEX IF NOT EXISTS snapshots_ref ON snapshots(kind, ref, created_at);
|
|
46
|
+
-- Append-mostly records: history (audit), feedback, deliveries, alert events, admin log, eval runs, schema changes.
|
|
47
|
+
CREATE TABLE IF NOT EXISTS records (
|
|
48
|
+
kind TEXT NOT NULL,
|
|
49
|
+
id TEXT NOT NULL,
|
|
50
|
+
at INTEGER NOT NULL,
|
|
51
|
+
ref TEXT,
|
|
52
|
+
status TEXT,
|
|
53
|
+
body TEXT NOT NULL,
|
|
54
|
+
PRIMARY KEY (kind, id)
|
|
55
|
+
);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS records_kind_at ON records(kind, at DESC);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS records_kind_ref ON records(kind, ref, at DESC);
|
|
58
|
+
`)
|
|
59
|
+
this.putStmt = this.db.prepare('INSERT OR REPLACE INTO results (id, created_at, datasource_id, title, bytes, body) VALUES (?, ?, ?, ?, ?, ?)')
|
|
60
|
+
this.getStmt = this.db.prepare('SELECT body FROM results WHERE id = ?')
|
|
61
|
+
this.cacheGet = this.db.prepare('SELECT result_id, created_at FROM cache WHERE key = ?')
|
|
62
|
+
this.cachePut = this.db.prepare('INSERT OR REPLACE INTO cache (key, result_id, created_at) VALUES (?, ?, ?)')
|
|
63
|
+
this.writes = 0
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Persist a result; oversized results keep their first rows only. */
|
|
67
|
+
put(result) {
|
|
68
|
+
let body = JSON.stringify(result)
|
|
69
|
+
if (body.length > MAX_RESULT_BYTES) {
|
|
70
|
+
const keep = Math.max(1, Math.floor(result.rows.length * MAX_RESULT_BYTES / body.length * 0.9))
|
|
71
|
+
body = JSON.stringify({ ...result, rows: result.rows.slice(0, keep), truncated: true, storedRows: keep })
|
|
72
|
+
}
|
|
73
|
+
this.putStmt.run(result.resultId, result.createdAt ?? Date.now(), result.datasourceId ?? null, result.title ?? '', body.length, body)
|
|
74
|
+
if (++this.writes % 200 === 0) this.prune()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get(id) {
|
|
78
|
+
const row = this.getStmt.get(id)
|
|
79
|
+
if (!row) return undefined
|
|
80
|
+
try { return JSON.parse(row.body) } catch { return undefined }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
prune(now = Date.now()) {
|
|
84
|
+
this.db.prepare('DELETE FROM results WHERE created_at < ?').run(now - MAX_AGE_MS)
|
|
85
|
+
this.db.prepare(`DELETE FROM results WHERE id IN (SELECT id FROM results ORDER BY created_at DESC LIMIT -1 OFFSET ${MAX_RESULTS})`).run()
|
|
86
|
+
this.db.prepare('DELETE FROM cache WHERE result_id NOT IN (SELECT id FROM results)').run()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Cached result for a key younger than ttlMs. */
|
|
90
|
+
cached(key, ttlMs, now = Date.now()) {
|
|
91
|
+
if (!(ttlMs > 0)) return undefined
|
|
92
|
+
const hit = this.cacheGet.get(key)
|
|
93
|
+
if (!hit || now - hit.created_at > ttlMs) return undefined
|
|
94
|
+
return this.get(hit.result_id)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
remember(key, resultId, now = Date.now()) {
|
|
98
|
+
this.cachePut.run(key, resultId, now)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
clearCache() {
|
|
102
|
+
this.db.exec('DELETE FROM cache')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
stats() {
|
|
106
|
+
const r = this.db.prepare('SELECT COUNT(*) AS n, COALESCE(SUM(bytes), 0) AS bytes FROM results').get()
|
|
107
|
+
const c = this.db.prepare('SELECT COUNT(*) AS n FROM cache').get()
|
|
108
|
+
return { results: Number(r.n), bytes: Number(r.bytes), cacheEntries: Number(c.n) }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Snapshots: schema versions, alert series, dashboard snapshots.
|
|
112
|
+
putSnapshot({ id, kind, ref, createdAt = Date.now(), body }) {
|
|
113
|
+
this.db.prepare('INSERT OR REPLACE INTO snapshots (id, kind, ref, created_at, body) VALUES (?, ?, ?, ?, ?)').run(id, kind, ref ?? null, createdAt, JSON.stringify(body))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
listSnapshots(kind, ref, limit = 50) {
|
|
117
|
+
const rows = ref === undefined
|
|
118
|
+
? this.db.prepare('SELECT id, kind, ref, created_at, body FROM snapshots WHERE kind = ? ORDER BY created_at DESC LIMIT ?').all(kind, limit)
|
|
119
|
+
: this.db.prepare('SELECT id, kind, ref, created_at, body FROM snapshots WHERE kind = ? AND ref = ? ORDER BY created_at DESC LIMIT ?').all(kind, ref, limit)
|
|
120
|
+
return rows.map(r => ({ id: r.id, kind: r.kind, ref: r.ref, createdAt: Number(r.created_at), body: JSON.parse(r.body) }))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getSnapshot(id) {
|
|
124
|
+
const r = this.db.prepare('SELECT id, kind, ref, created_at, body FROM snapshots WHERE id = ?').get(id)
|
|
125
|
+
return r ? { id: r.id, kind: r.kind, ref: r.ref, createdAt: Number(r.created_at), body: JSON.parse(r.body) } : undefined
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
deleteSnapshots(kind, ref) {
|
|
129
|
+
this.db.prepare('DELETE FROM snapshots WHERE kind = ? AND ref = ?').run(kind, ref)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ------------------------------------------------------------ records
|
|
133
|
+
|
|
134
|
+
/** Insert or replace one record. `ref` is a filter key (datasource / alert / actor). */
|
|
135
|
+
putRecord(kind, rec, { ref, status } = {}) {
|
|
136
|
+
this.recPut ??= this.db.prepare('INSERT OR REPLACE INTO records (kind, id, at, ref, status, body) VALUES (?, ?, ?, ?, ?, ?)')
|
|
137
|
+
this.recPut.run(kind, String(rec.id), Number(rec.at ?? rec.startedAt ?? Date.now()), ref ?? null, status ?? rec.status ?? null, JSON.stringify(rec))
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getRecord(kind, id) {
|
|
141
|
+
this.recGet ??= this.db.prepare('SELECT body FROM records WHERE kind = ? AND id = ?')
|
|
142
|
+
const r = this.recGet.get(kind, String(id))
|
|
143
|
+
return r ? JSON.parse(r.body) : undefined
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Newest-first records with optional ref / status filters and a JS predicate
|
|
148
|
+
* applied while scanning (stops after `limit` matches).
|
|
149
|
+
*/
|
|
150
|
+
listRecords(kind, { ref, status, limit = 100, since, filter } = {}) {
|
|
151
|
+
const where = ['kind = ?']
|
|
152
|
+
const args = [kind]
|
|
153
|
+
if (ref !== undefined && ref !== null) { where.push('ref = ?'); args.push(ref) }
|
|
154
|
+
if (status) { where.push('status = ?'); args.push(status) }
|
|
155
|
+
if (since) { where.push('at >= ?'); args.push(since) }
|
|
156
|
+
// Same-millisecond records: the later insert comes first.
|
|
157
|
+
const stmt = this.db.prepare(`SELECT body FROM records WHERE ${where.join(' AND ')} ORDER BY at DESC, rowid DESC`)
|
|
158
|
+
const out = []
|
|
159
|
+
for (const row of stmt.iterate(...args)) {
|
|
160
|
+
const rec = JSON.parse(row.body)
|
|
161
|
+
if (filter && !filter(rec)) continue
|
|
162
|
+
out.push(rec)
|
|
163
|
+
if (out.length >= limit) break
|
|
164
|
+
}
|
|
165
|
+
return out
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
countRecords(kind, { ref, status, since } = {}) {
|
|
169
|
+
const where = ['kind = ?']
|
|
170
|
+
const args = [kind]
|
|
171
|
+
if (ref) { where.push('ref = ?'); args.push(ref) }
|
|
172
|
+
if (status) { where.push('status = ?'); args.push(status) }
|
|
173
|
+
if (since) { where.push('at >= ?'); args.push(since) }
|
|
174
|
+
return Number(this.db.prepare(`SELECT COUNT(*) AS n FROM records WHERE ${where.join(' AND ')}`).get(...args).n)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
deleteRecord(kind, id) {
|
|
178
|
+
this.db.prepare('DELETE FROM records WHERE kind = ? AND id = ?').run(kind, String(id))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
clearRecords(kind) {
|
|
182
|
+
this.db.prepare('DELETE FROM records WHERE kind = ?').run(kind)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Keep the newest `keep` records of a kind. */
|
|
186
|
+
trimRecords(kind, keep) {
|
|
187
|
+
this.db.prepare(`DELETE FROM records WHERE kind = ? AND rowid IN (SELECT rowid FROM records WHERE kind = ? ORDER BY at DESC, rowid DESC LIMIT -1 OFFSET ${Math.max(0, Math.floor(keep))})`).run(kind, kind)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ------------------------------------------------------------ card action tokens
|
|
191
|
+
|
|
192
|
+
/** Opaque Lark card-button token → server-side context (expires; bounded). */
|
|
193
|
+
putActionToken(token, body, now = Date.now()) {
|
|
194
|
+
this.db.exec('CREATE TABLE IF NOT EXISTS action_tokens (token TEXT PRIMARY KEY, created_at INTEGER NOT NULL, body TEXT NOT NULL)')
|
|
195
|
+
this.db.prepare('INSERT OR REPLACE INTO action_tokens (token, created_at, body) VALUES (?, ?, ?)').run(token, now, JSON.stringify(body))
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
getActionToken(token) {
|
|
199
|
+
try {
|
|
200
|
+
const r = this.db.prepare('SELECT created_at, body FROM action_tokens WHERE token = ?').get(token)
|
|
201
|
+
return r ? { ...JSON.parse(r.body), created: Number(r.created_at) } : undefined
|
|
202
|
+
} catch { return undefined }
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
pruneActionTokens(maxAgeMs, keep, now = Date.now()) {
|
|
206
|
+
try {
|
|
207
|
+
this.db.prepare('DELETE FROM action_tokens WHERE created_at < ?').run(now - maxAgeMs)
|
|
208
|
+
this.db.prepare(`DELETE FROM action_tokens WHERE token IN (SELECT token FROM action_tokens ORDER BY created_at DESC LIMIT -1 OFFSET ${Math.max(0, Math.floor(keep))})`).run()
|
|
209
|
+
} catch {}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Run fn inside one transaction (bulk migration). */
|
|
213
|
+
transaction(fn) {
|
|
214
|
+
this.db.exec('BEGIN')
|
|
215
|
+
try { const r = fn(); this.db.exec('COMMIT'); return r } catch (e) { this.db.exec('ROLLBACK'); throw e }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
close() {
|
|
219
|
+
try { this.db.close() } catch {}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function cacheKey(parts) {
|
|
224
|
+
return createHash('sha256').update(JSON.stringify(parts)).digest('hex').slice(0, 40)
|
|
225
|
+
}
|