@heyhru/app-dms-server 0.6.0 → 0.8.0

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.
Files changed (2) hide show
  1. package/dist/index.js +75 -7
  2. package/package.json +10 -9
package/dist/index.js CHANGED
@@ -4,6 +4,42 @@ import { createPgDb } from "@heyhru/server-plugin-pg";
4
4
  // src/app.ts
5
5
  import Fastify from "fastify";
6
6
  import cors from "@fastify/cors";
7
+ import { metricsPlugin } from "@heyhru/server-plugin-metrics";
8
+
9
+ // src/metrics.ts
10
+ import { Histogram, Counter, Gauge, register } from "@heyhru/server-plugin-metrics";
11
+ import { getPoolStats } from "@heyhru/business-dms-datasource";
12
+ var sqlDuration = new Histogram({
13
+ name: "dms_sql_duration_seconds",
14
+ help: "SQL execution duration in seconds",
15
+ labelNames: ["data_source_id", "sql_type", "status"],
16
+ buckets: [0.1, 0.5, 1, 2, 5, 10, 30],
17
+ registers: [register]
18
+ });
19
+ var authLoginTotal = new Counter({
20
+ name: "dms_auth_login_total",
21
+ help: "Total login attempts",
22
+ labelNames: ["status"],
23
+ registers: [register]
24
+ });
25
+ var dbPoolActive = new Gauge({
26
+ name: "dms_db_pool_active",
27
+ help: "Active connections per data source pool",
28
+ labelNames: ["data_source_id"],
29
+ registers: [register]
30
+ });
31
+ var dbPoolWaiting = new Gauge({
32
+ name: "dms_db_pool_waiting",
33
+ help: "Waiting requests per data source pool",
34
+ labelNames: ["data_source_id"],
35
+ registers: [register]
36
+ });
37
+ function refreshPoolMetrics() {
38
+ for (const { dataSourceId, active, waiting } of getPoolStats()) {
39
+ if (active !== null) dbPoolActive.set({ data_source_id: dataSourceId }, active);
40
+ if (waiting !== null) dbPoolWaiting.set({ data_source_id: dataSourceId }, waiting);
41
+ }
42
+ }
7
43
 
8
44
  // src/auth/auth.middleware.ts
9
45
  import { verifyToken as jwtVerify } from "@heyhru/server-plugin-jwt";
@@ -122,10 +158,12 @@ async function authLogin(req, reply) {
122
158
  const user = await getUserByUsername(username);
123
159
  if (!user || !await verifyPassword(password, user.password_hash)) {
124
160
  req.log.warn("Login failed for user: %s", username);
161
+ authLoginTotal.inc({ status: "fail" });
125
162
  return reply.code(401).send({ error: "\u7528\u6237\u540D\u6216\u5BC6\u7801\u9519\u8BEF" });
126
163
  }
127
164
  const payload = { id: user.id, username: user.username, role: user.role };
128
165
  const token = signToken(payload, config.jwtSecret, config.jwtExpiresIn);
166
+ authLoginTotal.inc({ status: "success" });
129
167
  req.log.info("User logged in: %s", username);
130
168
  return reply.send({ ...payload, token });
131
169
  }
@@ -214,20 +252,28 @@ function postSqlParse(req, reply) {
214
252
  return reply.send({ category: sqlParse(sql) });
215
253
  }
216
254
  async function postSqlExecute(req, reply) {
217
- const { dataSourceId, database, sql } = req.body ?? {};
255
+ const body = req.body ?? {};
256
+ const { dataSourceId, database, sql } = body;
218
257
  if (!dataSourceId || !sql) {
219
258
  return reply.code(400).send({ error: "\u6570\u636E\u6E90 ID \u548C SQL \u4E0D\u80FD\u4E3A\u7A7A" });
220
259
  }
260
+ const page = typeof body.page === "number" ? body.page : 1;
261
+ const pageSize = typeof body.pageSize === "number" ? body.pageSize : 50;
221
262
  const ip = req.headers["x-forwarded-for"] ?? req.headers["x-real-ip"] ?? "unknown";
222
263
  try {
223
- const rows = await sqlExecute({ dataSourceId, database, sql, userId: req.user.id, ip });
224
- return reply.send({ rows });
264
+ const result = await sqlExecute({ dataSourceId, database, sql, userId: req.user.id, ip, page, pageSize });
265
+ return reply.send(result);
225
266
  } catch (err) {
226
267
  req.log.error(err, "SQL execute failed (user=%s, ds=%s)", req.user.id, dataSourceId);
227
268
  return reply.code(400).send({ error: err instanceof Error ? err.message : String(err) });
228
269
  }
229
270
  }
230
- async function sqlExecute({ dataSourceId, database, sql, userId, ip }) {
271
+ async function fetchCountQuery(pool, sql) {
272
+ const countSql = `SELECT COUNT(*) AS total FROM (${sql}) AS _count_t`;
273
+ const rows = await pool.execute(countSql);
274
+ return Number(rows[0]?.total ?? 0);
275
+ }
276
+ async function sqlExecute({ dataSourceId, database, sql, userId, ip, page, pageSize }) {
231
277
  const category = sqlParse(sql);
232
278
  if (category !== "select") {
233
279
  throw new Error("\u4EC5\u5141\u8BB8\u76F4\u63A5\u6267\u884C SELECT \u8BED\u53E5");
@@ -237,16 +283,34 @@ async function sqlExecute({ dataSourceId, database, sql, userId, ip }) {
237
283
  return ds ? getPool(ds) : null;
238
284
  })();
239
285
  if (!pool) throw new Error("\u6570\u636E\u6E90\u672A\u627E\u5230");
240
- const rows = await pool.execute(sql);
286
+ const hasLimit = /\bLIMIT\b/i.test(sql);
287
+ const dataSql = hasLimit ? sql : `${sql} LIMIT ${pageSize} OFFSET ${(page - 1) * pageSize}`;
288
+ const end = sqlDuration.startTimer({ data_source_id: dataSourceId, sql_type: category });
289
+ let rows;
290
+ let total;
291
+ try {
292
+ if (hasLimit) {
293
+ rows = await pool.execute(dataSql);
294
+ total = rows.length;
295
+ } else {
296
+ const [rawRows, count] = await Promise.all([pool.execute(dataSql), fetchCountQuery(pool, sql)]);
297
+ rows = rawRows;
298
+ total = count;
299
+ }
300
+ end({ status: "success" });
301
+ } catch (err) {
302
+ end({ status: "failed" });
303
+ throw err;
304
+ }
241
305
  await writeAuditLog({
242
306
  userId,
243
307
  dataSourceId,
244
308
  action: "SELECT",
245
309
  sqlText: sql,
246
- resultSummary: `${rows.length} rows`,
310
+ resultSummary: `${total} total rows`,
247
311
  ipAddress: ip
248
312
  });
249
- return rows;
313
+ return { rows, total, page, pageSize };
250
314
  }
251
315
  async function postSqlDatabases(req, reply) {
252
316
  const { dataSourceId } = req.body ?? {};
@@ -345,6 +409,10 @@ function savedSqlController(app) {
345
409
  async function buildApp() {
346
410
  const app = Fastify({ loggerInstance: logger });
347
411
  await app.register(cors, { origin: true, credentials: true });
412
+ await app.register(metricsPlugin);
413
+ app.addHook("onResponse", () => {
414
+ refreshPoolMetrics();
415
+ });
348
416
  app.decorateRequest("user", null);
349
417
  app.addHook("preHandler", authHook);
350
418
  app.addHook("onError", (req, _reply, error, done) => {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.6.0",
6
+ "version": "0.8.0",
7
7
  "description": "DMS backend API server built on Fastify",
8
8
  "type": "module",
9
9
  "main": "./dist/index.mjs",
@@ -19,15 +19,16 @@
19
19
  },
20
20
  "dependencies": {
21
21
  "@fastify/cors": "^11.2.0",
22
- "@heyhru/business-dms-approval": "0.6.0",
23
- "@heyhru/business-dms-audit": "0.6.0",
24
- "@heyhru/business-dms-datasource": "0.6.0",
25
- "@heyhru/business-dms-saved-sql": "0.6.0",
26
- "@heyhru/business-dms-user": "0.6.0",
22
+ "@heyhru/business-dms-approval": "0.6.2",
23
+ "@heyhru/business-dms-audit": "0.6.1",
24
+ "@heyhru/business-dms-datasource": "0.7.1",
25
+ "@heyhru/business-dms-saved-sql": "0.6.1",
26
+ "@heyhru/business-dms-user": "0.6.1",
27
27
  "@heyhru/common-util-logger": "0.6.0",
28
28
  "@heyhru/server-plugin-jwt": "0.6.0",
29
- "@heyhru/server-plugin-mysql": "0.6.0",
30
- "@heyhru/server-plugin-pg": "0.6.0",
29
+ "@heyhru/server-plugin-metrics": "0.7.0",
30
+ "@heyhru/server-plugin-mysql": "0.7.0",
31
+ "@heyhru/server-plugin-pg": "0.7.0",
31
32
  "@heyhru/server-util-crypto": "0.6.0",
32
33
  "fastify": "^5.8.4",
33
34
  "node-sql-parser": "^5.4.0",
@@ -41,5 +42,5 @@
41
42
  "typescript": "^6.0.2",
42
43
  "vitest": "^4.1.4"
43
44
  },
44
- "gitHead": "c25e6fe17f0b15e07ba534e712cc723be02051b3"
45
+ "gitHead": "b20e6c3e571bd4939fd32ece8f5bc6d8302a4c83"
45
46
  }