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