@heyhru/app-dms-server 0.1.3 → 0.1.5
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 +162 -187
- package/package.json +2 -2
- package/dist/chunk-QGM4M3NI.js +0 -37
- package/dist/pg.adapter-BNI42SHT.js +0 -5181
- package/dist/sqlite.adapter-ARBIRN6Z.js +0 -31
package/dist/index.js
CHANGED
|
@@ -1,31 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// src/db/index.ts
|
|
4
|
-
var _db = null;
|
|
5
|
-
var _driver = "sqlite";
|
|
6
|
-
function parseDbDriver(url) {
|
|
7
|
-
if (url.startsWith("postgres://") || url.startsWith("postgresql://")) return "postgres";
|
|
8
|
-
return "sqlite";
|
|
9
|
-
}
|
|
10
|
-
async function createDmsDb(url) {
|
|
11
|
-
if (_db) throw new Error("DmsDb already initialized. Call close() first.");
|
|
12
|
-
_driver = parseDbDriver(url);
|
|
13
|
-
if (_driver === "postgres") {
|
|
14
|
-
const { createPgAdapter } = await import("./pg.adapter-BNI42SHT.js");
|
|
15
|
-
_db = createPgAdapter(url);
|
|
16
|
-
} else {
|
|
17
|
-
const { createSqliteAdapter } = await import("./sqlite.adapter-ARBIRN6Z.js");
|
|
18
|
-
_db = createSqliteAdapter(url.replace("sqlite://", ""));
|
|
19
|
-
}
|
|
20
|
-
return _db;
|
|
21
|
-
}
|
|
22
|
-
function getDb() {
|
|
23
|
-
if (!_db) throw new Error("DmsDb not initialized. Call createDmsDb() first.");
|
|
24
|
-
return _db;
|
|
25
|
-
}
|
|
26
|
-
function getDriver() {
|
|
27
|
-
return _driver;
|
|
28
|
-
}
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { createSqlite } from "@heyhru/server-util-sqlite";
|
|
29
3
|
|
|
30
4
|
// src/app.ts
|
|
31
5
|
import Fastify from "fastify";
|
|
@@ -111,10 +85,8 @@ import { hashPassword as hashPassword2, verifyPassword } from "@heyhru/server-ut
|
|
|
111
85
|
// src/users/users.service.ts
|
|
112
86
|
import { hashPassword } from "@heyhru/server-util-crypto";
|
|
113
87
|
|
|
114
|
-
// src/
|
|
115
|
-
|
|
116
|
-
return getDriver() === "postgres" ? "NOW()" : "datetime('now')";
|
|
117
|
-
}
|
|
88
|
+
// src/users/users.model.ts
|
|
89
|
+
import { getSqlite as getDb } from "@heyhru/server-util-sqlite";
|
|
118
90
|
|
|
119
91
|
// src/users/users.sql.ts
|
|
120
92
|
var LIST = `
|
|
@@ -133,9 +105,9 @@ var CREATE = `
|
|
|
133
105
|
INSERT INTO users (username, email, password_hash, role)
|
|
134
106
|
VALUES (?, ?, ?, ?)
|
|
135
107
|
RETURNING id, username, email, role, created_at`;
|
|
136
|
-
var UPDATE_PASSWORD =
|
|
108
|
+
var UPDATE_PASSWORD = `
|
|
137
109
|
UPDATE users
|
|
138
|
-
SET password_hash = ?, updated_at =
|
|
110
|
+
SET password_hash = ?, updated_at = datetime('now')
|
|
139
111
|
WHERE id = ?`;
|
|
140
112
|
var DELETE = `
|
|
141
113
|
DELETE FROM users
|
|
@@ -143,18 +115,18 @@ WHERE id = ?`;
|
|
|
143
115
|
|
|
144
116
|
// src/users/users.model.ts
|
|
145
117
|
function listUsers() {
|
|
146
|
-
return getDb().
|
|
118
|
+
return getDb().prepare(LIST).all();
|
|
147
119
|
}
|
|
148
120
|
function getUserById(id) {
|
|
149
|
-
return getDb().
|
|
121
|
+
return getDb().prepare(FIND_BY_ID).get(id);
|
|
150
122
|
}
|
|
151
123
|
function getUserByUsername(username) {
|
|
152
|
-
return getDb().
|
|
124
|
+
return getDb().prepare(FIND_BY_USERNAME).get(username);
|
|
153
125
|
}
|
|
154
126
|
function createUserRow(username, email, hash, role) {
|
|
155
|
-
return getDb().
|
|
127
|
+
return getDb().prepare(CREATE).get(username, email, hash, role);
|
|
156
128
|
}
|
|
157
|
-
|
|
129
|
+
function updateUserRow(id, data) {
|
|
158
130
|
const fields = [];
|
|
159
131
|
const values = [];
|
|
160
132
|
if (data.email) {
|
|
@@ -166,18 +138,18 @@ async function updateUserRow(id, data) {
|
|
|
166
138
|
values.push(data.role);
|
|
167
139
|
}
|
|
168
140
|
if (!fields.length) return getUserById(id);
|
|
169
|
-
fields.push(
|
|
141
|
+
fields.push("updated_at = datetime('now')");
|
|
170
142
|
values.push(id);
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
);
|
|
143
|
+
const bound = values;
|
|
144
|
+
return getDb().prepare(
|
|
145
|
+
`UPDATE users SET ${fields.join(", ")} WHERE id = ? RETURNING id, username, email, role, created_at`
|
|
146
|
+
).get(...bound);
|
|
175
147
|
}
|
|
176
148
|
function deleteUser(id) {
|
|
177
|
-
return getDb().run(
|
|
149
|
+
return getDb().prepare(DELETE).run(id);
|
|
178
150
|
}
|
|
179
151
|
function updatePasswordHash(id, hash) {
|
|
180
|
-
return getDb().
|
|
152
|
+
return getDb().prepare(UPDATE_PASSWORD).run(hash, id);
|
|
181
153
|
}
|
|
182
154
|
|
|
183
155
|
// src/users/users.service.ts
|
|
@@ -187,30 +159,30 @@ function getUserByUsername2(username) {
|
|
|
187
159
|
function updateUserPassword(id, hash) {
|
|
188
160
|
return updatePasswordHash(id, hash);
|
|
189
161
|
}
|
|
190
|
-
|
|
191
|
-
return reply.send(
|
|
162
|
+
function userList(_req, reply) {
|
|
163
|
+
return reply.send(listUsers());
|
|
192
164
|
}
|
|
193
|
-
|
|
165
|
+
function userGet(req, reply) {
|
|
194
166
|
const { id } = req.body ?? {};
|
|
195
|
-
const user =
|
|
167
|
+
const user = getUserById(id);
|
|
196
168
|
if (!user) return reply.code(404).send({ error: "\u672A\u627E\u5230" });
|
|
197
169
|
return reply.send(user);
|
|
198
170
|
}
|
|
199
171
|
async function userCreate(req, reply) {
|
|
200
172
|
const body = req.body ?? {};
|
|
201
173
|
const hash = await hashPassword(body.password);
|
|
202
|
-
const user =
|
|
174
|
+
const user = createUserRow(body.username, body.email, hash, body.role);
|
|
203
175
|
return reply.code(201).send(user);
|
|
204
176
|
}
|
|
205
|
-
|
|
177
|
+
function userUpdate(req, reply) {
|
|
206
178
|
const { id, ...rest } = req.body ?? {};
|
|
207
|
-
const user =
|
|
179
|
+
const user = updateUserRow(id, rest);
|
|
208
180
|
if (!user) return reply.code(404).send({ error: "\u672A\u627E\u5230" });
|
|
209
181
|
return reply.send(user);
|
|
210
182
|
}
|
|
211
|
-
|
|
183
|
+
function userDelete(req, reply) {
|
|
212
184
|
const { id } = req.body ?? {};
|
|
213
|
-
|
|
185
|
+
deleteUser(id);
|
|
214
186
|
return reply.code(204).send();
|
|
215
187
|
}
|
|
216
188
|
|
|
@@ -220,7 +192,7 @@ async function authLogin(req, reply) {
|
|
|
220
192
|
if (!username || !password) {
|
|
221
193
|
return reply.code(400).send({ error: "\u7528\u6237\u540D\u548C\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A" });
|
|
222
194
|
}
|
|
223
|
-
const user =
|
|
195
|
+
const user = getUserByUsername2(username);
|
|
224
196
|
if (!user || !await verifyPassword(password, user.password_hash)) {
|
|
225
197
|
logger.warn("Login failed for user: %s", username);
|
|
226
198
|
return reply.code(401).send({ error: "\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF" });
|
|
@@ -242,12 +214,12 @@ async function authChangePassword(req, reply) {
|
|
|
242
214
|
if (!currentPassword || !newPassword) {
|
|
243
215
|
return reply.code(400).send({ error: "\u5F53\u524D\u5BC6\u7801\u548C\u65B0\u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A" });
|
|
244
216
|
}
|
|
245
|
-
const user =
|
|
217
|
+
const user = getUserByUsername2(req.user.username);
|
|
246
218
|
if (!user || !await verifyPassword(currentPassword, user.password_hash)) {
|
|
247
219
|
return reply.code(400).send({ error: "\u5F53\u524D\u5BC6\u7801\u4E0D\u6B63\u786E" });
|
|
248
220
|
}
|
|
249
221
|
const hash = await hashPassword2(newPassword);
|
|
250
|
-
|
|
222
|
+
updateUserPassword(user.id, hash);
|
|
251
223
|
logger.info("Password changed for user: %s", req.user.username);
|
|
252
224
|
return reply.send({ ok: true });
|
|
253
225
|
}
|
|
@@ -274,13 +246,16 @@ import { encrypt, decrypt } from "@heyhru/server-util-crypto";
|
|
|
274
246
|
import { createPool as createMysqlPool } from "@heyhru/server-util-mysql";
|
|
275
247
|
import { createPool as createPgPool } from "@heyhru/server-util-pg";
|
|
276
248
|
|
|
249
|
+
// src/datasources/datasources.model.ts
|
|
250
|
+
import { getSqlite as getDb2 } from "@heyhru/server-util-sqlite";
|
|
251
|
+
|
|
277
252
|
// src/datasources/datasources.sql.ts
|
|
278
253
|
var LIST2 = `
|
|
279
|
-
SELECT id, name, type, host, port, database, username,
|
|
254
|
+
SELECT id, name, type, host, port, database, username, pool_min, pool_max, created_by, created_at
|
|
280
255
|
FROM data_sources
|
|
281
256
|
ORDER BY created_at DESC`;
|
|
282
257
|
var FIND_BY_ID2 = `
|
|
283
|
-
SELECT id, name, type, host, port, database, username,
|
|
258
|
+
SELECT id, name, type, host, port, database, username, pool_min, pool_max, created_by, created_at
|
|
284
259
|
FROM data_sources
|
|
285
260
|
WHERE id = ?`;
|
|
286
261
|
var FIND_WITH_PASSWORD = `
|
|
@@ -288,26 +263,26 @@ SELECT *
|
|
|
288
263
|
FROM data_sources
|
|
289
264
|
WHERE id = ?`;
|
|
290
265
|
var CREATE2 = `
|
|
291
|
-
INSERT INTO data_sources (name, type, host, port, database, username, password_encrypted,
|
|
292
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
293
|
-
RETURNING id, name, type, host, port, database, username,
|
|
294
|
-
var UPDATE_FIELDS = ["name", "type", "host", "port", "database", "username", "
|
|
266
|
+
INSERT INTO data_sources (name, type, host, port, database, username, password_encrypted, pool_min, pool_max, created_by)
|
|
267
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
268
|
+
RETURNING id, name, type, host, port, database, username, pool_min, pool_max, created_by, created_at`;
|
|
269
|
+
var UPDATE_FIELDS = ["name", "type", "host", "port", "database", "username", "pool_min", "pool_max"];
|
|
295
270
|
var DELETE2 = `
|
|
296
271
|
DELETE FROM data_sources
|
|
297
272
|
WHERE id = ?`;
|
|
298
273
|
|
|
299
274
|
// src/datasources/datasources.model.ts
|
|
300
275
|
function listDataSources() {
|
|
301
|
-
return
|
|
276
|
+
return getDb2().prepare(LIST2).all();
|
|
302
277
|
}
|
|
303
278
|
function getDataSourceById(id) {
|
|
304
|
-
return
|
|
279
|
+
return getDb2().prepare(FIND_BY_ID2).get(id);
|
|
305
280
|
}
|
|
306
281
|
function getDataSourceRow(id) {
|
|
307
|
-
return
|
|
282
|
+
return getDb2().prepare(FIND_WITH_PASSWORD).get(id);
|
|
308
283
|
}
|
|
309
284
|
function insertDataSource(data, encryptedPassword, createdBy) {
|
|
310
|
-
return
|
|
285
|
+
return getDb2().prepare(CREATE2).get(
|
|
311
286
|
data.name,
|
|
312
287
|
data.type,
|
|
313
288
|
data.host,
|
|
@@ -315,13 +290,12 @@ function insertDataSource(data, encryptedPassword, createdBy) {
|
|
|
315
290
|
data.database,
|
|
316
291
|
data.username,
|
|
317
292
|
encryptedPassword,
|
|
318
|
-
data.ssl,
|
|
319
293
|
data.pool_min,
|
|
320
294
|
data.pool_max,
|
|
321
295
|
createdBy
|
|
322
|
-
|
|
296
|
+
);
|
|
323
297
|
}
|
|
324
|
-
|
|
298
|
+
function updateDataSource(id, data, encryptedPassword) {
|
|
325
299
|
const fields = [];
|
|
326
300
|
const values = [];
|
|
327
301
|
for (const key of UPDATE_FIELDS) {
|
|
@@ -336,13 +310,13 @@ async function updateDataSource(id, data, encryptedPassword) {
|
|
|
336
310
|
}
|
|
337
311
|
if (!fields.length) return getDataSourceById(id);
|
|
338
312
|
values.push(id);
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
);
|
|
313
|
+
const bound = values;
|
|
314
|
+
return getDb2().prepare(
|
|
315
|
+
`UPDATE data_sources SET ${fields.join(", ")} WHERE id = ? RETURNING id, name, type, host, port, database, username, pool_min, pool_max, created_by, created_at`
|
|
316
|
+
).get(...bound);
|
|
343
317
|
}
|
|
344
318
|
function removeDataSource(id) {
|
|
345
|
-
return
|
|
319
|
+
return getDb2().prepare(DELETE2).run(id);
|
|
346
320
|
}
|
|
347
321
|
|
|
348
322
|
// src/datasources/datasources.service.ts
|
|
@@ -367,15 +341,14 @@ function getPool(ds) {
|
|
|
367
341
|
username: ds.username,
|
|
368
342
|
password: ds.password,
|
|
369
343
|
poolMin: ds.pool_min,
|
|
370
|
-
poolMax: ds.pool_max
|
|
371
|
-
ssl: ds.ssl
|
|
344
|
+
poolMax: ds.pool_max
|
|
372
345
|
});
|
|
373
346
|
pools.set(key, pool);
|
|
374
347
|
logger.info("Connection pool created (ds=%s, db=%s, type=%s)", ds.id, ds.database, ds.type);
|
|
375
348
|
return pool;
|
|
376
349
|
}
|
|
377
|
-
|
|
378
|
-
const ds =
|
|
350
|
+
function getPoolForDatabase(dataSourceId, database) {
|
|
351
|
+
const ds = getDataSourceWithPassword(dataSourceId);
|
|
379
352
|
if (!ds) return null;
|
|
380
353
|
return getPool({ ...ds, database });
|
|
381
354
|
}
|
|
@@ -389,8 +362,8 @@ async function destroyPool(id) {
|
|
|
389
362
|
}
|
|
390
363
|
}
|
|
391
364
|
}
|
|
392
|
-
|
|
393
|
-
const row =
|
|
365
|
+
function getDataSourceWithPassword(id) {
|
|
366
|
+
const row = getDataSourceRow(id);
|
|
394
367
|
if (!row) return null;
|
|
395
368
|
return {
|
|
396
369
|
id: row.id,
|
|
@@ -400,24 +373,23 @@ async function getDataSourceWithPassword(id) {
|
|
|
400
373
|
database: row.database ?? null,
|
|
401
374
|
username: row.username,
|
|
402
375
|
password: decrypt(row.password_encrypted, config.encryptionKey),
|
|
403
|
-
ssl: row.ssl,
|
|
404
376
|
pool_min: row.pool_min,
|
|
405
377
|
pool_max: row.pool_max
|
|
406
378
|
};
|
|
407
379
|
}
|
|
408
|
-
|
|
409
|
-
return reply.send(
|
|
380
|
+
function datasourceList(_req, reply) {
|
|
381
|
+
return reply.send(listDataSources());
|
|
410
382
|
}
|
|
411
|
-
|
|
383
|
+
function datasourceGet(req, reply) {
|
|
412
384
|
const { id } = req.body ?? {};
|
|
413
|
-
const ds =
|
|
385
|
+
const ds = getDataSourceById(id);
|
|
414
386
|
if (!ds) return reply.code(404).send({ error: "\u672A\u627E\u5230" });
|
|
415
387
|
return reply.send(ds);
|
|
416
388
|
}
|
|
417
|
-
|
|
389
|
+
function datasourceCreate(req, reply) {
|
|
418
390
|
const body = req.body ?? {};
|
|
419
|
-
const ds =
|
|
420
|
-
{ ...body,
|
|
391
|
+
const ds = insertDataSource(
|
|
392
|
+
{ ...body, pool_min: body.pool_min ?? 1, pool_max: body.pool_max ?? 10 },
|
|
421
393
|
encrypt(body.password, config.encryptionKey),
|
|
422
394
|
req.user.id
|
|
423
395
|
);
|
|
@@ -425,17 +397,17 @@ async function datasourceCreate(req, reply) {
|
|
|
425
397
|
}
|
|
426
398
|
async function datasourceUpdate(req, reply) {
|
|
427
399
|
const { id, password, ...rest } = req.body ?? {};
|
|
428
|
-
const existing =
|
|
400
|
+
const existing = getDataSourceById(id);
|
|
429
401
|
if (!existing) return reply.code(404).send({ error: "\u672A\u627E\u5230" });
|
|
430
402
|
const encryptedPassword = password ? encrypt(password, config.encryptionKey) : void 0;
|
|
431
403
|
await destroyPool(id);
|
|
432
|
-
const ds =
|
|
404
|
+
const ds = updateDataSource(id, rest, encryptedPassword);
|
|
433
405
|
return reply.send(ds);
|
|
434
406
|
}
|
|
435
407
|
async function datasourceDelete(req, reply) {
|
|
436
408
|
const { id } = req.body ?? {};
|
|
437
409
|
await destroyPool(id);
|
|
438
|
-
|
|
410
|
+
removeDataSource(id);
|
|
439
411
|
return reply.code(204).send();
|
|
440
412
|
}
|
|
441
413
|
|
|
@@ -451,6 +423,9 @@ function datasourceController(app) {
|
|
|
451
423
|
// src/sql/sql.service.ts
|
|
452
424
|
import NodeSqlParser from "node-sql-parser";
|
|
453
425
|
|
|
426
|
+
// src/audit/audit.model.ts
|
|
427
|
+
import { getSqlite as getDb3 } from "@heyhru/server-util-sqlite";
|
|
428
|
+
|
|
454
429
|
// src/audit/audit.sql.ts
|
|
455
430
|
var INSERT = `
|
|
456
431
|
INSERT INTO audit_logs (user_id, data_source_id, action, sql_text, result_summary, ip_address)
|
|
@@ -463,16 +438,16 @@ WHERE 1=1`;
|
|
|
463
438
|
|
|
464
439
|
// src/audit/audit.model.ts
|
|
465
440
|
function insertAuditLog(entry) {
|
|
466
|
-
return
|
|
441
|
+
return getDb3().prepare(INSERT).run(
|
|
467
442
|
entry.userId,
|
|
468
443
|
entry.dataSourceId ?? null,
|
|
469
444
|
entry.action,
|
|
470
445
|
entry.sqlText ?? null,
|
|
471
446
|
entry.resultSummary ?? null,
|
|
472
447
|
entry.ipAddress ?? null
|
|
473
|
-
|
|
448
|
+
);
|
|
474
449
|
}
|
|
475
|
-
|
|
450
|
+
function queryAuditLogs(filters) {
|
|
476
451
|
let query = LIST3;
|
|
477
452
|
const params = [];
|
|
478
453
|
if (filters?.userId) {
|
|
@@ -485,7 +460,8 @@ async function queryAuditLogs(filters) {
|
|
|
485
460
|
}
|
|
486
461
|
query += " ORDER BY al.created_at DESC LIMIT ? OFFSET ?";
|
|
487
462
|
params.push(filters?.limit ?? 50, filters?.offset ?? 0);
|
|
488
|
-
|
|
463
|
+
const bound = params;
|
|
464
|
+
return getDb3().prepare(query).all(...bound);
|
|
489
465
|
}
|
|
490
466
|
|
|
491
467
|
// src/audit/audit.service.ts
|
|
@@ -540,8 +516,8 @@ async function sqlExecute({ dataSourceId, database, sql, userId, ip }) {
|
|
|
540
516
|
if (category !== "select") {
|
|
541
517
|
throw new Error("\u4EC5\u5141\u8BB8\u76F4\u63A5\u6267\u884C SELECT \u8BED\u53E5");
|
|
542
518
|
}
|
|
543
|
-
const pool = database ?
|
|
544
|
-
const ds =
|
|
519
|
+
const pool = database ? getPoolForDatabase(dataSourceId, database) : (() => {
|
|
520
|
+
const ds = getDataSourceWithPassword(dataSourceId);
|
|
545
521
|
return ds ? getPool(ds) : null;
|
|
546
522
|
})();
|
|
547
523
|
if (!pool) throw new Error("\u6570\u636E\u6E90\u672A\u627E\u5230");
|
|
@@ -556,48 +532,46 @@ async function sqlExecute({ dataSourceId, database, sql, userId, ip }) {
|
|
|
556
532
|
});
|
|
557
533
|
return rows;
|
|
558
534
|
}
|
|
559
|
-
|
|
535
|
+
function postSqlDatabases(req, reply) {
|
|
560
536
|
const { dataSourceId } = req.body ?? {};
|
|
561
537
|
if (!dataSourceId) {
|
|
562
538
|
return reply.code(400).send({ error: "\u6570\u636E\u6E90 ID \u4E0D\u80FD\u4E3A\u7A7A" });
|
|
563
539
|
}
|
|
564
|
-
const ds =
|
|
540
|
+
const ds = getDataSourceWithPassword(dataSourceId);
|
|
565
541
|
if (!ds) return reply.code(404).send({ error: "\u6570\u636E\u6E90\u672A\u627E\u5230" });
|
|
566
542
|
const pool = getPool({
|
|
567
543
|
...ds,
|
|
568
544
|
database: ds.database ?? (ds.type === "postgres" ? "postgres" : "")
|
|
569
545
|
});
|
|
570
546
|
const sql = ds.type === "mysql" ? "SHOW DATABASES" : "SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname";
|
|
571
|
-
|
|
572
|
-
const rows = await pool.execute(sql);
|
|
547
|
+
return pool.execute(sql).then((rows) => {
|
|
573
548
|
const names = rows.map(
|
|
574
549
|
(r) => Object.values(r)[0]
|
|
575
550
|
);
|
|
576
551
|
return reply.send(names);
|
|
577
|
-
}
|
|
552
|
+
}).catch((err) => {
|
|
578
553
|
logger.error(err, "Failed to list databases (ds=%s)", dataSourceId);
|
|
579
554
|
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
580
|
-
}
|
|
555
|
+
});
|
|
581
556
|
}
|
|
582
|
-
|
|
557
|
+
function postSqlTables(req, reply) {
|
|
583
558
|
const { dataSourceId, database } = req.body ?? {};
|
|
584
559
|
if (!dataSourceId || !database) {
|
|
585
560
|
return reply.code(400).send({ error: "\u6570\u636E\u6E90 ID \u548C\u6570\u636E\u5E93\u540D\u4E0D\u80FD\u4E3A\u7A7A" });
|
|
586
561
|
}
|
|
587
|
-
const pool =
|
|
562
|
+
const pool = getPoolForDatabase(dataSourceId, database);
|
|
588
563
|
if (!pool) return reply.code(404).send({ error: "\u6570\u636E\u6E90\u672A\u627E\u5230" });
|
|
589
|
-
const ds =
|
|
564
|
+
const ds = getDataSourceWithPassword(dataSourceId);
|
|
590
565
|
const sql = ds.type === "mysql" ? `SELECT TABLE_NAME FROM information_schema.TABLES WHERE TABLE_SCHEMA = '${database}' ORDER BY TABLE_NAME` : "SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename";
|
|
591
|
-
|
|
592
|
-
const rows = await pool.execute(sql);
|
|
566
|
+
return pool.execute(sql).then((rows) => {
|
|
593
567
|
const names = rows.map(
|
|
594
568
|
(r) => Object.values(r)[0]
|
|
595
569
|
);
|
|
596
570
|
return reply.send(names);
|
|
597
|
-
}
|
|
571
|
+
}).catch((err) => {
|
|
598
572
|
logger.error(err, "Failed to list tables (ds=%s, db=%s)", dataSourceId, database);
|
|
599
573
|
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
600
|
-
}
|
|
574
|
+
});
|
|
601
575
|
}
|
|
602
576
|
|
|
603
577
|
// src/sql/sql.controller.ts
|
|
@@ -608,6 +582,9 @@ function sqlController(app) {
|
|
|
608
582
|
app.post("/sql/tables", postSqlTables);
|
|
609
583
|
}
|
|
610
584
|
|
|
585
|
+
// src/approvals/approvals.model.ts
|
|
586
|
+
import { getSqlite as getDb4 } from "@heyhru/server-util-sqlite";
|
|
587
|
+
|
|
611
588
|
// src/approvals/approvals.sql.ts
|
|
612
589
|
var FIND_BY_ID3 = `
|
|
613
590
|
SELECT *
|
|
@@ -617,22 +594,22 @@ var CREATE3 = `
|
|
|
617
594
|
INSERT INTO approvals (data_source_id, sql_text, submitted_by)
|
|
618
595
|
VALUES (?, ?, ?)
|
|
619
596
|
RETURNING *`;
|
|
620
|
-
var UPDATE_REVIEW =
|
|
597
|
+
var UPDATE_REVIEW = `
|
|
621
598
|
UPDATE approvals
|
|
622
|
-
SET status = ?, reviewed_by = ?, reject_reason = ?, updated_at =
|
|
599
|
+
SET status = ?, reviewed_by = ?, reject_reason = ?, updated_at = datetime('now')
|
|
623
600
|
WHERE id = ?
|
|
624
601
|
RETURNING *`;
|
|
625
|
-
var UPDATE_EXECUTING =
|
|
602
|
+
var UPDATE_EXECUTING = `
|
|
626
603
|
UPDATE approvals
|
|
627
|
-
SET status = 'executing', updated_at =
|
|
604
|
+
SET status = 'executing', updated_at = datetime('now')
|
|
628
605
|
WHERE id = ?`;
|
|
629
|
-
var UPDATE_RESULT =
|
|
606
|
+
var UPDATE_RESULT = `
|
|
630
607
|
UPDATE approvals
|
|
631
|
-
SET status = ?, execute_result = ?, updated_at =
|
|
608
|
+
SET status = ?, execute_result = ?, updated_at = datetime('now')
|
|
632
609
|
WHERE id = ?`;
|
|
633
610
|
|
|
634
611
|
// src/approvals/approvals.model.ts
|
|
635
|
-
|
|
612
|
+
function listApprovals(filters) {
|
|
636
613
|
let query = "SELECT * FROM approvals WHERE 1=1";
|
|
637
614
|
const params = [];
|
|
638
615
|
if (filters?.status) {
|
|
@@ -644,51 +621,52 @@ async function listApprovals(filters) {
|
|
|
644
621
|
params.push(filters.submittedBy);
|
|
645
622
|
}
|
|
646
623
|
query += " ORDER BY created_at DESC";
|
|
647
|
-
|
|
624
|
+
const bound = params;
|
|
625
|
+
return getDb4().prepare(query).all(...bound);
|
|
648
626
|
}
|
|
649
627
|
function getApprovalById(id) {
|
|
650
|
-
return
|
|
628
|
+
return getDb4().prepare(FIND_BY_ID3).get(id);
|
|
651
629
|
}
|
|
652
630
|
function insertApproval(dataSourceId, sqlText, submittedBy) {
|
|
653
|
-
return
|
|
631
|
+
return getDb4().prepare(CREATE3).get(dataSourceId, sqlText, submittedBy);
|
|
654
632
|
}
|
|
655
633
|
function updateReview(id, status, reviewedBy, rejectReason) {
|
|
656
|
-
return
|
|
634
|
+
return getDb4().prepare(UPDATE_REVIEW).get(status, reviewedBy, rejectReason, id);
|
|
657
635
|
}
|
|
658
636
|
function setExecuting(id) {
|
|
659
|
-
return
|
|
637
|
+
return getDb4().prepare(UPDATE_EXECUTING).run(id);
|
|
660
638
|
}
|
|
661
639
|
function setExecuteResult(id, status, result) {
|
|
662
|
-
return
|
|
640
|
+
return getDb4().prepare(UPDATE_RESULT).run(status, result, id);
|
|
663
641
|
}
|
|
664
642
|
|
|
665
643
|
// src/approvals/approvals.service.ts
|
|
666
|
-
|
|
644
|
+
function approvalList(req, reply) {
|
|
667
645
|
const { status, mine } = req.body ?? {};
|
|
668
646
|
return reply.send(
|
|
669
|
-
|
|
647
|
+
listApprovals({
|
|
670
648
|
status,
|
|
671
649
|
submittedBy: mine === "true" ? req.user.id : void 0
|
|
672
650
|
})
|
|
673
651
|
);
|
|
674
652
|
}
|
|
675
|
-
|
|
653
|
+
function approvalGet(req, reply) {
|
|
676
654
|
const { id } = req.body ?? {};
|
|
677
|
-
const approval =
|
|
655
|
+
const approval = getApprovalById(id);
|
|
678
656
|
if (!approval) return reply.code(404).send({ error: "\u672A\u627E\u5230" });
|
|
679
657
|
return reply.send(approval);
|
|
680
658
|
}
|
|
681
|
-
|
|
659
|
+
function approvalCreate(req, reply) {
|
|
682
660
|
const { dataSourceId, sql } = req.body ?? {};
|
|
683
661
|
if (!dataSourceId || !sql) {
|
|
684
662
|
return reply.code(400).send({ error: "\u6570\u636E\u6E90 ID \u548C SQL \u4E0D\u80FD\u4E3A\u7A7A" });
|
|
685
663
|
}
|
|
686
|
-
const approval =
|
|
664
|
+
const approval = insertApproval(dataSourceId, sql, req.user.id);
|
|
687
665
|
logger.info("Approval submitted (user=%s)", req.user.id);
|
|
688
666
|
return reply.code(201).send(approval);
|
|
689
667
|
}
|
|
690
|
-
|
|
691
|
-
const approval =
|
|
668
|
+
function reviewApproval(id, reviewerId, decision, rejectReason) {
|
|
669
|
+
const approval = getApprovalById(id);
|
|
692
670
|
if (!approval || approval["status"] !== "pending") {
|
|
693
671
|
throw new Error("\u5BA1\u6279\u8BB0\u5F55\u4E0D\u5B58\u5728\u6216\u4E0D\u5728\u5F85\u5BA1\u6279\u72B6\u6001");
|
|
694
672
|
}
|
|
@@ -697,10 +675,10 @@ async function reviewApproval(id, reviewerId, decision, rejectReason) {
|
|
|
697
675
|
}
|
|
698
676
|
return updateReview(id, decision, reviewerId, rejectReason ?? null);
|
|
699
677
|
}
|
|
700
|
-
|
|
678
|
+
function approvalApprove(req, reply) {
|
|
701
679
|
const { id } = req.body ?? {};
|
|
702
680
|
try {
|
|
703
|
-
const result =
|
|
681
|
+
const result = reviewApproval(id, req.user.id, "approved");
|
|
704
682
|
logger.info("Approval approved (id=%s, reviewer=%s)", id, req.user.id);
|
|
705
683
|
return reply.send(result);
|
|
706
684
|
} catch (err) {
|
|
@@ -708,10 +686,10 @@ async function approvalApprove(req, reply) {
|
|
|
708
686
|
return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
|
|
709
687
|
}
|
|
710
688
|
}
|
|
711
|
-
|
|
689
|
+
function approvalReject(req, reply) {
|
|
712
690
|
const { id, reason } = req.body ?? {};
|
|
713
691
|
try {
|
|
714
|
-
const result =
|
|
692
|
+
const result = reviewApproval(id, req.user.id, "rejected", reason);
|
|
715
693
|
logger.info("Approval rejected (id=%s, reviewer=%s)", id, req.user.id);
|
|
716
694
|
return reply.send(result);
|
|
717
695
|
} catch (err) {
|
|
@@ -731,18 +709,18 @@ async function approvalExecute(req, reply) {
|
|
|
731
709
|
}
|
|
732
710
|
}
|
|
733
711
|
async function doExecuteApproval(id, userId, ip) {
|
|
734
|
-
const approval =
|
|
712
|
+
const approval = getApprovalById(id);
|
|
735
713
|
if (!approval || approval["status"] !== "approved") {
|
|
736
714
|
throw new Error("\u5BA1\u6279\u8BB0\u5F55\u4E0D\u5B58\u5728\u6216\u672A\u901A\u8FC7\u5BA1\u6279");
|
|
737
715
|
}
|
|
738
|
-
|
|
716
|
+
setExecuting(id);
|
|
739
717
|
try {
|
|
740
|
-
const ds =
|
|
718
|
+
const ds = getDataSourceWithPassword(approval["data_source_id"]);
|
|
741
719
|
if (!ds) throw new Error("\u6570\u636E\u6E90\u672A\u627E\u5230");
|
|
742
720
|
const pool = getPool(ds);
|
|
743
721
|
const rows = await pool.execute(approval["sql_text"]);
|
|
744
722
|
const result = `${rows.length} rows affected`;
|
|
745
|
-
|
|
723
|
+
setExecuteResult(id, "executed", result);
|
|
746
724
|
logger.info("Approval executed (id=%s, user=%s)", id, userId);
|
|
747
725
|
await writeAuditLog({
|
|
748
726
|
userId,
|
|
@@ -755,7 +733,7 @@ async function doExecuteApproval(id, userId, ip) {
|
|
|
755
733
|
return getApprovalById(id);
|
|
756
734
|
} catch (err) {
|
|
757
735
|
const message = err instanceof Error ? err.message : String(err);
|
|
758
|
-
|
|
736
|
+
setExecuteResult(id, "execute_failed", message);
|
|
759
737
|
logger.error(err, "Approval execute_failed (id=%s)", id);
|
|
760
738
|
throw err;
|
|
761
739
|
}
|
|
@@ -796,39 +774,38 @@ async function buildApp() {
|
|
|
796
774
|
return app;
|
|
797
775
|
}
|
|
798
776
|
|
|
777
|
+
// src/migrate/runner.ts
|
|
778
|
+
import { getSqlite as getDb5 } from "@heyhru/server-util-sqlite";
|
|
779
|
+
|
|
799
780
|
// src/migrate/migrations.ts
|
|
800
|
-
var idCol = (driver) => driver === "postgres" ? "id UUID PRIMARY KEY DEFAULT gen_random_uuid()" : "id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16))))";
|
|
801
|
-
var tsCol = (name, driver) => driver === "postgres" ? `${name} TIMESTAMPTZ NOT NULL DEFAULT NOW()` : `${name} TEXT NOT NULL DEFAULT (datetime('now'))`;
|
|
802
|
-
var fkNotNull = (name, ref, driver) => driver === "postgres" ? `${name} UUID NOT NULL REFERENCES ${ref}(id)` : `${name} TEXT NOT NULL REFERENCES ${ref}(id)`;
|
|
803
|
-
var fkNullable = (name, ref, driver) => driver === "postgres" ? `${name} UUID REFERENCES ${ref}(id)` : `${name} TEXT REFERENCES ${ref}(id)`;
|
|
804
781
|
var migrations = [
|
|
805
782
|
{
|
|
806
783
|
name: "001_users.sql",
|
|
807
|
-
sql:
|
|
784
|
+
sql: `
|
|
808
785
|
CREATE TABLE IF NOT EXISTS users (
|
|
809
|
-
|
|
786
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
810
787
|
username TEXT NOT NULL UNIQUE,
|
|
811
788
|
email TEXT NOT NULL UNIQUE,
|
|
812
789
|
password_hash TEXT NOT NULL,
|
|
813
790
|
role TEXT NOT NULL CHECK (role IN ('admin', 'maintainer', 'developer', 'viewer')),
|
|
814
|
-
|
|
815
|
-
|
|
791
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
792
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
816
793
|
);
|
|
817
794
|
|
|
818
|
-
INSERT
|
|
795
|
+
INSERT OR IGNORE INTO users (id, username, email, password_hash, role)
|
|
819
796
|
VALUES (
|
|
820
797
|
'admin-seed-id-0000000000000000',
|
|
821
798
|
'admin',
|
|
822
799
|
'admin@example.com',
|
|
823
800
|
'178c20236d9629bffcb301f57d1b8383:40c49d6500a8f322754ac0cd2d5c9dea019a4c14feef60e436d767cc2dd44bc1eeee7ef16f1bd768260aeec4025e06c479c4c367537899002ec89962382a3104',
|
|
824
801
|
'admin'
|
|
825
|
-
)
|
|
802
|
+
);`
|
|
826
803
|
},
|
|
827
804
|
{
|
|
828
805
|
name: "002_data_sources.sql",
|
|
829
|
-
sql:
|
|
806
|
+
sql: `
|
|
830
807
|
CREATE TABLE IF NOT EXISTS data_sources (
|
|
831
|
-
|
|
808
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
832
809
|
name TEXT NOT NULL UNIQUE,
|
|
833
810
|
type TEXT NOT NULL CHECK (type IN ('mysql', 'postgres')),
|
|
834
811
|
host TEXT NOT NULL,
|
|
@@ -836,47 +813,45 @@ CREATE TABLE IF NOT EXISTS data_sources (
|
|
|
836
813
|
database TEXT,
|
|
837
814
|
username TEXT NOT NULL,
|
|
838
815
|
password_encrypted TEXT NOT NULL,
|
|
839
|
-
ssl BOOLEAN NOT NULL DEFAULT FALSE,
|
|
840
816
|
pool_min INTEGER NOT NULL DEFAULT 1,
|
|
841
817
|
pool_max INTEGER NOT NULL DEFAULT 10,
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
818
|
+
created_by TEXT NOT NULL REFERENCES users(id),
|
|
819
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
820
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
845
821
|
);`
|
|
846
822
|
},
|
|
847
823
|
{
|
|
848
824
|
name: "003_audit_logs.sql",
|
|
849
|
-
sql:
|
|
825
|
+
sql: `
|
|
850
826
|
CREATE TABLE IF NOT EXISTS audit_logs (
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
827
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
828
|
+
user_id TEXT NOT NULL REFERENCES users(id),
|
|
829
|
+
data_source_id TEXT REFERENCES data_sources(id),
|
|
854
830
|
action TEXT NOT NULL,
|
|
855
831
|
sql_text TEXT,
|
|
856
832
|
result_summary TEXT,
|
|
857
833
|
ip_address TEXT,
|
|
858
|
-
|
|
834
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
859
835
|
);
|
|
860
836
|
|
|
861
837
|
CREATE INDEX IF NOT EXISTS idx_audit_logs_user_id ON audit_logs(user_id);
|
|
862
|
-
CREATE INDEX IF NOT EXISTS idx_audit_logs_data_source_id ON audit_logs(data_source_id);
|
|
863
838
|
CREATE INDEX IF NOT EXISTS idx_audit_logs_created_at ON audit_logs(created_at);`
|
|
864
839
|
},
|
|
865
840
|
{
|
|
866
841
|
name: "004_approvals.sql",
|
|
867
|
-
sql:
|
|
842
|
+
sql: `
|
|
868
843
|
CREATE TABLE IF NOT EXISTS approvals (
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
844
|
+
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(16)))),
|
|
845
|
+
data_source_id TEXT NOT NULL REFERENCES data_sources(id),
|
|
846
|
+
submitted_by TEXT NOT NULL REFERENCES users(id),
|
|
847
|
+
reviewed_by TEXT REFERENCES users(id),
|
|
873
848
|
sql_text TEXT NOT NULL,
|
|
874
849
|
status TEXT NOT NULL DEFAULT 'pending'
|
|
875
850
|
CHECK (status IN ('pending', 'approved', 'rejected', 'executing', 'executed', 'execute_failed')),
|
|
876
851
|
reject_reason TEXT,
|
|
877
852
|
execute_result TEXT,
|
|
878
|
-
|
|
879
|
-
|
|
853
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
854
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
880
855
|
);
|
|
881
856
|
|
|
882
857
|
CREATE INDEX IF NOT EXISTS idx_approvals_status ON approvals(status);
|
|
@@ -885,30 +860,30 @@ CREATE INDEX IF NOT EXISTS idx_approvals_submitted_by ON approvals(submitted_by)
|
|
|
885
860
|
];
|
|
886
861
|
|
|
887
862
|
// src/migrate/runner.ts
|
|
888
|
-
|
|
889
|
-
const db =
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
const rows = await db.query("SELECT name FROM _migrations");
|
|
900
|
-
const applied = new Set(rows.map((r) => r.name));
|
|
863
|
+
function runMigrations() {
|
|
864
|
+
const db = getDb5();
|
|
865
|
+
db.exec(`
|
|
866
|
+
CREATE TABLE IF NOT EXISTS _migrations (
|
|
867
|
+
name TEXT PRIMARY KEY,
|
|
868
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
869
|
+
)
|
|
870
|
+
`);
|
|
871
|
+
const applied = new Set(
|
|
872
|
+
db.prepare("SELECT name FROM _migrations").all().map((r) => r.name)
|
|
873
|
+
);
|
|
901
874
|
for (const migration of migrations) {
|
|
902
875
|
if (applied.has(migration.name)) continue;
|
|
903
|
-
|
|
904
|
-
|
|
876
|
+
db.exec(migration.sql);
|
|
877
|
+
db.prepare("INSERT INTO _migrations (name) VALUES (?)").run(
|
|
878
|
+
migration.name
|
|
879
|
+
);
|
|
905
880
|
logger.info("Migration applied: %s", migration.name);
|
|
906
881
|
}
|
|
907
882
|
}
|
|
908
883
|
|
|
909
884
|
// src/index.ts
|
|
910
885
|
async function main() {
|
|
911
|
-
|
|
886
|
+
createSqlite({ path: config.dbUrl.replace("sqlite://", "") });
|
|
912
887
|
await runMigrations();
|
|
913
888
|
const app = await buildApp();
|
|
914
889
|
await app.listen({ port: config.port, host: "0.0.0.0" });
|