web_plsql 1.3.0 → 1.3.2

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.mjs CHANGED
@@ -206,12 +206,12 @@ const DB_TYPE_DATE = oracledb.DB_TYPE_DATE;
206
206
 
207
207
  //#endregion
208
208
  //#region src/backend/version.ts
209
- if (typeof globalThis.__VERSION__ === "undefined") globalThis.__VERSION__ = "**development**";
209
+ globalThis.__VERSION__ ??= "**development**";
210
210
  /**
211
211
  * Returns the current library version
212
212
  * @returns {string} - Version.
213
213
  */
214
- const getVersion = () => "1.3.0";
214
+ const getVersion = () => "1.3.2";
215
215
 
216
216
  //#endregion
217
217
  //#region src/backend/server/config.ts
@@ -913,7 +913,7 @@ var StatsManager = class {
913
913
  let p95 = 0;
914
914
  let p99 = 0;
915
915
  if (b.durations.length > 0) {
916
- const sorted = [...b.durations].sort((x, y) => x - y);
916
+ const sorted = b.durations.toSorted((x, y) => x - y);
917
917
  const p95Idx = Math.floor(sorted.length * .95);
918
918
  const p99Idx = Math.floor(sorted.length * .99);
919
919
  const lastIdx = sorted.length - 1;
@@ -924,8 +924,8 @@ var StatsManager = class {
924
924
  timestamp: Date.now(),
925
925
  requests: b.count,
926
926
  errors: b.errors,
927
- durationMin: b.durationMin < 0 ? 0 : b.durationMin,
928
- durationMax: b.durationMax < 0 ? 0 : b.durationMax,
927
+ durationMin: Math.max(b.durationMin, 0),
928
+ durationMax: Math.max(b.durationMax, 0),
929
929
  durationAvg: b.count > 0 ? b.durationSum / b.count : 0,
930
930
  durationP95: p95,
931
931
  durationP99: p99,
@@ -1106,7 +1106,7 @@ const isFile = async (filePath) => {
1106
1106
  * @param value - The value.
1107
1107
  * @returns The escaped value.
1108
1108
  */
1109
- const escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1109
+ const escapeHtml = (value) => value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#39;");
1110
1110
  /**
1111
1111
  * Convert LF and/or CR to <br>
1112
1112
  * @param text - The text to convert.
@@ -1114,8 +1114,8 @@ const escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;")
1114
1114
  */
1115
1115
  const convertAsciiToHtml = (text) => {
1116
1116
  let html = escapeHtml(text);
1117
- html = html.replace(/(?:\r\n|\r|\n)/g, "<br />");
1118
- html = html.replace(/\t/g, "&nbsp;&nbsp;&nbsp;");
1117
+ html = html.replaceAll(/\r\n|\r|\n/g, "<br />");
1118
+ html = html.replaceAll(" ", "&nbsp;&nbsp;&nbsp;");
1119
1119
  return html;
1120
1120
  };
1121
1121
  /**
@@ -1180,6 +1180,12 @@ const inspect = (value, depth = null) => {
1180
1180
  return "Unable to convert value to string";
1181
1181
  };
1182
1182
  /**
1183
+ * @param cell - The string
1184
+ * @param width - The width
1185
+ * @returns The result
1186
+ */
1187
+ const padCell = (cell, width) => cell.padEnd(width, " ");
1188
+ /**
1183
1189
  * Return a tabular representation of the values.
1184
1190
  *
1185
1191
  * @param head - The header values.
@@ -1193,12 +1199,6 @@ const toTable = (head, body) => {
1193
1199
  return Math.max(h.length, bodyMax);
1194
1200
  });
1195
1201
  /**
1196
- * @param cell - The string
1197
- * @param width - The width
1198
- * @returns The result
1199
- */
1200
- const padCell = (cell, width) => cell.padEnd(width, " ");
1201
- /**
1202
1202
  * @param i - The index
1203
1203
  * @returns The width
1204
1204
  */
@@ -1705,19 +1705,25 @@ const findArguments = async (procedure, databaseConnection, argumentCache) => {
1705
1705
  * @returns The binding.
1706
1706
  */
1707
1707
  const getBinding = (argName, argValue, argType) => {
1708
+ const isEmpty = argValue === "" || argValue === null || argValue === void 0;
1708
1709
  if (argType === DATA_TYPES.VARCHAR2 || argType === DATA_TYPES.CHAR) return {
1709
1710
  dir: BIND_IN,
1710
1711
  type: DB_TYPE_VARCHAR,
1711
- val: argValue
1712
+ val: isEmpty ? null : argValue
1712
1713
  };
1713
1714
  if (argType === DATA_TYPES.CLOB) return {
1714
1715
  dir: BIND_IN,
1715
1716
  type: DB_TYPE_CLOB,
1716
- val: argValue
1717
+ val: isEmpty ? null : argValue
1717
1718
  };
1718
1719
  if (argType === DATA_TYPES.NUMBER || argType === DATA_TYPES.BINARY_INTEGER) {
1720
+ if (isEmpty) return {
1721
+ dir: BIND_IN,
1722
+ type: DB_TYPE_NUMBER,
1723
+ val: null
1724
+ };
1719
1725
  const value = stringToNumber(argValue);
1720
- if (value === null) throw new Error(`Error in named parameter "${argName}": invalid value "${argValue}" for type "${argType}"`);
1726
+ if (value === null) throw new Error(`Error in named parameter "${argName}": invalid value "${inspect(argValue)}" for type "${argType}"`);
1721
1727
  return {
1722
1728
  dir: BIND_IN,
1723
1729
  type: DB_TYPE_NUMBER,
@@ -1725,18 +1731,23 @@ const getBinding = (argName, argValue, argType) => {
1725
1731
  };
1726
1732
  }
1727
1733
  if (argType === DATA_TYPES.DATE) {
1728
- if (typeof argValue !== "string") throw new Error(`Error in named parameter "${argName}": invalid value "${argValue}" for type "${argType}"`);
1734
+ if (isEmpty) return {
1735
+ dir: BIND_IN,
1736
+ type: DB_TYPE_DATE,
1737
+ val: null
1738
+ };
1739
+ if (typeof argValue !== "string") throw new TypeError(`Error in named parameter "${argName}": invalid value "${inspect(argValue)}" for type "${argType}"`);
1729
1740
  const value = new Date(argValue);
1730
- if (Number.isNaN(value.getTime())) throw new Error(`Error in named parameter "${argName}": invalid value "${argValue}" for type "${argType}"`);
1741
+ if (Number.isNaN(value.getTime())) throw new TypeError(`Error in named parameter "${argName}": invalid value "${inspect(argValue)}" for type "${argType}"`);
1731
1742
  return {
1732
1743
  dir: BIND_IN,
1733
- type: DB_TYPE_VARCHAR,
1744
+ type: DB_TYPE_DATE,
1734
1745
  val: value
1735
1746
  };
1736
1747
  }
1737
1748
  if (argType === DATA_TYPES.PL_SQL_TABLE || Array.isArray(argValue)) return {
1738
1749
  dir: BIND_IN,
1739
- type: DB_TYPE_DATE,
1750
+ type: DB_TYPE_VARCHAR,
1740
1751
  val: typeof argValue === "string" ? [argValue] : argValue
1741
1752
  };
1742
1753
  throw new Error(`Error in named parameter "${argName}": invalid binding type "${argType}"`);
@@ -1834,8 +1845,8 @@ const parsePage = (text) => {
1834
1845
  const headerEndPosition = text.indexOf("\n\n");
1835
1846
  if (headerEndPosition === -1) head = text;
1836
1847
  else {
1837
- head = text.substring(0, headerEndPosition + 2);
1838
- page.body = text.substring(headerEndPosition + 2);
1848
+ head = text.slice(0, Math.max(0, headerEndPosition + 2));
1849
+ page.body = text.slice(Math.max(0, headerEndPosition + 2));
1839
1850
  }
1840
1851
  debug$8("parsing the headers received from PL/SQL");
1841
1852
  head.split("\n").forEach((line) => {
@@ -1845,8 +1856,8 @@ const parsePage = (text) => {
1845
1856
  {
1846
1857
  const cookie = parseCookie(header.value);
1847
1858
  debug$8(`oracle header "set-cookie" with value "${header.value}" was received has been parsed to ${JSON.stringify(cookie)}`);
1848
- if (cookie !== null) page.head.cookies.push(cookie);
1849
- else throw new Error(`Unable to parse header "set-cookie" with value "${header.value}" received from PL/SQL`);
1859
+ if (cookie === null) throw new Error(`Unable to parse header "set-cookie" with value "${header.value}" received from PL/SQL`);
1860
+ else page.head.cookies.push(cookie);
1850
1861
  }
1851
1862
  break;
1852
1863
  case "content-type":
@@ -1855,22 +1866,24 @@ const parsePage = (text) => {
1855
1866
  break;
1856
1867
  case "x-db-content-length":
1857
1868
  {
1858
- const contentLength = parseInt(header.value, 10);
1859
- if (!Number.isNaN(contentLength)) {
1869
+ const contentLength = Number.parseInt(header.value, 10);
1870
+ if (Number.isNaN(contentLength)) throw new TypeError(`Unable to parse header "x-db-content-length" with value "${header.value}" received from PL/SQL`);
1871
+ else {
1860
1872
  page.head.contentLength = contentLength;
1861
1873
  debug$8(`oracle header "x-db-content-length" with value "${page.head.contentLength}" was parsed`);
1862
- } else throw new Error(`Unable to parse header "x-db-content-length" with value "${header.value}" received from PL/SQL`);
1874
+ }
1863
1875
  }
1864
1876
  break;
1865
1877
  case "status":
1866
1878
  {
1867
- const statusCode = parseInt(header.value, 10);
1868
- if (!Number.isNaN(statusCode)) {
1879
+ const statusCode = Number.parseInt(header.value, 10);
1880
+ if (Number.isNaN(statusCode)) throw new TypeError(`Unable to parse header "status" with value "${header.value}" received from PL/SQL`);
1881
+ else {
1869
1882
  page.head.statusCode = statusCode;
1870
1883
  debug$8(`oracle header "status" with value "${page.head.statusCode}" was parsed`);
1871
1884
  const index = header.value.indexOf(" ");
1872
- if (index !== -1) page.head.statusDescription = header.value.substring(index + 1);
1873
- } else throw new Error(`Unable to parse header "status" with value "${header.value}" received from PL/SQL`);
1885
+ if (index !== -1) page.head.statusDescription = header.value.slice(Math.max(0, index + 1));
1886
+ }
1874
1887
  }
1875
1888
  break;
1876
1889
  case "location":
@@ -1893,8 +1906,8 @@ const parsePage = (text) => {
1893
1906
  const getHeader = (line) => {
1894
1907
  const index = line.indexOf(":");
1895
1908
  if (index !== -1) return {
1896
- name: line.substring(0, index).trim(),
1897
- value: line.substring(index + 1).trim()
1909
+ name: line.slice(0, Math.max(0, index)).trim(),
1910
+ value: line.slice(Math.max(0, index + 1)).trim()
1898
1911
  };
1899
1912
  return null;
1900
1913
  };
@@ -1912,17 +1925,17 @@ const parseCookie = (text) => {
1912
1925
  const index = firstElement.indexOf("=");
1913
1926
  if (index <= 0) return null;
1914
1927
  const cookie = {
1915
- name: firstElement.substring(0, index).trim(),
1916
- value: firstElement.substring(index + 1).trim(),
1928
+ name: firstElement.slice(0, Math.max(0, index)).trim(),
1929
+ value: firstElement.slice(Math.max(0, index + 1)).trim(),
1917
1930
  options: {}
1918
1931
  };
1919
1932
  cookieElements.shift();
1920
1933
  cookieElements.forEach((element) => {
1921
- if (element.toLowerCase().startsWith("path=")) cookie.options.path = element.substring(5);
1922
- else if (element.toLowerCase().startsWith("domain=")) cookie.options.domain = element.substring(7);
1934
+ if (element.toLowerCase().startsWith("path=")) cookie.options.path = element.slice(5);
1935
+ else if (element.toLowerCase().startsWith("domain=")) cookie.options.domain = element.slice(7);
1923
1936
  else if (element.toLowerCase().startsWith("secure")) cookie.options.secure = true;
1924
1937
  else if (element.toLowerCase().startsWith("expires=")) {
1925
- const date = tryDecodeDate(element.substring(8));
1938
+ const date = tryDecodeDate(element.slice(8));
1926
1939
  if (date) cookie.options.expires = date;
1927
1940
  } else if (element.toLowerCase().startsWith("httponly")) cookie.options.httpOnly = true;
1928
1941
  });
@@ -2449,14 +2462,9 @@ var OWAPageStream = class extends Readable {
2449
2462
  //#endregion
2450
2463
  //#region src/backend/util/traceManager.ts
2451
2464
  var TraceManager = class {
2452
- enabled;
2453
- filename;
2454
- maxEntries;
2455
- constructor() {
2456
- this.enabled = false;
2457
- this.filename = "trace.json.log";
2458
- this.maxEntries = 1e3;
2459
- }
2465
+ enabled = false;
2466
+ filename = "trace.json.log";
2467
+ maxEntries = 1e3;
2460
2468
  /**
2461
2469
  * Toggle tracing
2462
2470
  * @param enabled - New state
@@ -2533,12 +2541,11 @@ const getProcedure = async (req, procName, argObj, options, databaseConnection,
2533
2541
  };
2534
2542
  }
2535
2543
  const useVariableArguments = procName.startsWith("!");
2536
- const sanitizedProcName = await sanitizeProcName(useVariableArguments ? procName.substring(1) : procName, databaseConnection, options, procedureNameCache);
2537
- if (useVariableArguments) return {
2544
+ const sanitizedProcName = await sanitizeProcName(useVariableArguments ? procName.slice(1) : procName, databaseConnection, options, procedureNameCache);
2545
+ return useVariableArguments ? {
2538
2546
  ...getProcedureVariable(req, sanitizedProcName, argObj),
2539
2547
  resolvedName: sanitizedProcName
2540
- };
2541
- else return {
2548
+ } : {
2542
2549
  ...await getProcedureNamed(req, sanitizedProcName, argObj, databaseConnection, argumentCache),
2543
2550
  resolvedName: sanitizedProcName
2544
2551
  };
@@ -2580,7 +2587,7 @@ const procedurePrepare = async (cgiObj, databaseConnection) => {
2580
2587
  remote_user: {
2581
2588
  dir: BIND_IN,
2582
2589
  type: STRING,
2583
- val: (cgiObj.REMOTE_USER ?? "").substring(0, 30)
2590
+ val: (cgiObj.REMOTE_USER ?? "").slice(0, 30)
2584
2591
  }
2585
2592
  };
2586
2593
  try {
@@ -2679,7 +2686,7 @@ const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options,
2679
2686
  const startTime = Date.now();
2680
2687
  let traceData = null;
2681
2688
  if (traceManager.isEnabled()) traceData = {
2682
- id: Math.random().toString(36).substring(2, 15),
2689
+ id: Math.random().toString(36).slice(2, 15),
2683
2690
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2684
2691
  source: cgiObj.REMOTE_ADDR ?? "",
2685
2692
  url: req.originalUrl,
@@ -2762,7 +2769,7 @@ const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options,
2762
2769
  streamInstance.push = (chunk) => {
2763
2770
  if (chunk !== null && htmlBuffer.length < MAX_HTML_SIZE) {
2764
2771
  htmlBuffer += String(chunk);
2765
- if (htmlBuffer.length > MAX_HTML_SIZE) htmlBuffer = htmlBuffer.substring(0, MAX_HTML_SIZE) + "... [truncated]";
2772
+ if (htmlBuffer.length > MAX_HTML_SIZE) htmlBuffer = htmlBuffer.slice(0, Math.max(0, MAX_HTML_SIZE)) + "... [truncated]";
2766
2773
  }
2767
2774
  return originalPush(chunk);
2768
2775
  };
@@ -2858,11 +2865,11 @@ const getCookieString = (req) => {
2858
2865
  */
2859
2866
  const getPath = (req) => {
2860
2867
  const pathname = new URL(`${req.protocol}://${os.hostname()}${req.originalUrl}`).pathname;
2861
- const tmp = trimPath(pathname.substring(0, pathname.lastIndexOf("/") + 1));
2868
+ const tmp = trimPath(pathname.slice(0, Math.max(0, pathname.lastIndexOf("/") + 1)));
2862
2869
  return {
2863
2870
  script: `/${tmp}`,
2864
- prefix: `/${tmp.substring(0, tmp.lastIndexOf("/"))}`,
2865
- dad: tmp.substring(tmp.indexOf("/") + 1)
2871
+ prefix: `/${tmp.slice(0, Math.max(0, tmp.lastIndexOf("/")))}`,
2872
+ dad: tmp.slice(Math.max(0, tmp.indexOf("/") + 1))
2866
2873
  };
2867
2874
  };
2868
2875
  /**
@@ -2871,7 +2878,7 @@ const getPath = (req) => {
2871
2878
  * @param value - The value to trim.
2872
2879
  * @returns The trimmed value.
2873
2880
  */
2874
- const trimPath = (value) => value.replace(/^\/+|\/+$/g, "");
2881
+ const trimPath = (value) => value.replaceAll(/^\/+|\/+$/g, "");
2875
2882
  /**
2876
2883
  * Create a CGI object
2877
2884
  *
@@ -2962,33 +2969,36 @@ const processRequest = async (req, res, options, connectionPool, procedureNameCa
2962
2969
  debug$2("processRequest: ENTER");
2963
2970
  if (typeof req.params.name !== "string") console.warn(`processRequest: WARNING: the req.params.name is not a string but an array of string: ${req.params.name}`);
2964
2971
  const connection = await connectionPool.getConnection();
2965
- const cgiObj = getCGI(req, options.documentTable, options.cgi ?? {}, authenticatedUser);
2966
- debug$2("processRequest: cgiObj=", cgiObj);
2967
- const filesToUpload = getFiles(req);
2968
- debug$2("processRequest: filesToUpload=", filesToUpload);
2969
- const argObj = {};
2970
- Object.assign(argObj, req.query);
2971
- filesToUpload.reduce((aggregator, file) => {
2972
- aggregator[file.fieldname] = file.filename;
2973
- return aggregator;
2974
- }, argObj);
2975
- Object.assign(argObj, normalizeBody(req));
2976
- debug$2("processRequest: argObj=", argObj);
2977
- await invokeProcedure(req, res, argObj, cgiObj, filesToUpload, options, connection, procedureNameCache, argumentCache);
2978
- if (options.transactionMode === "rollback") {
2979
- debug$2("transactionMode: rollback");
2980
- await connection.rollback();
2981
- } else if (typeof options.transactionMode === "function") {
2982
- debug$2("transactionMode: callback");
2983
- const procName = Array.isArray(req.params.name) ? req.params.name[0] : req.params.name;
2984
- const result = options.transactionMode(connection, procName ?? "");
2985
- debug$2("transactionMode: callback restult", result);
2986
- if (result && typeof result.then === "function") await result;
2987
- } else {
2988
- debug$2("transactionMode: commit");
2989
- await connection.commit();
2990
- }
2991
- await connection.release();
2972
+ try {
2973
+ const cgiObj = getCGI(req, options.documentTable, options.cgi ?? {}, authenticatedUser);
2974
+ debug$2("processRequest: cgiObj=", cgiObj);
2975
+ const filesToUpload = getFiles(req);
2976
+ debug$2("processRequest: filesToUpload=", filesToUpload);
2977
+ const argObj = {};
2978
+ Object.assign(argObj, req.query);
2979
+ filesToUpload.reduce((aggregator, file) => {
2980
+ aggregator[file.fieldname] = file.filename;
2981
+ return aggregator;
2982
+ }, argObj);
2983
+ Object.assign(argObj, normalizeBody(req));
2984
+ debug$2("processRequest: argObj=", argObj);
2985
+ await invokeProcedure(req, res, argObj, cgiObj, filesToUpload, options, connection, procedureNameCache, argumentCache);
2986
+ if (options.transactionMode === "rollback") {
2987
+ debug$2("transactionMode: rollback");
2988
+ await connection.rollback();
2989
+ } else if (typeof options.transactionMode === "function") {
2990
+ debug$2("transactionMode: callback");
2991
+ const procName = Array.isArray(req.params.name) ? req.params.name[0] : req.params.name;
2992
+ const result = options.transactionMode(connection, procName ?? "");
2993
+ debug$2("transactionMode: callback restult", result);
2994
+ if (result && typeof result.then === "function") await result;
2995
+ } else {
2996
+ debug$2("transactionMode: commit");
2997
+ await connection.commit();
2998
+ }
2999
+ } finally {
3000
+ await connection.release();
3001
+ }
2992
3002
  debug$2("processRequest: EXIT");
2993
3003
  };
2994
3004
 
@@ -3222,7 +3232,7 @@ const readLastLines = async (filePath, n = 100, filter = "") => {
3222
3232
  const createAdminRouter = (adminContext) => {
3223
3233
  const router = express.Router();
3224
3234
  router.get("/api/status", (req, res) => {
3225
- const uptime = ((/* @__PURE__ */ new Date()).getTime() - adminContext.startTime.getTime()) / 1e3;
3235
+ const uptime = (Date.now() - adminContext.startTime.getTime()) / 1e3;
3226
3236
  const includeHistory = req.query.history === "true";
3227
3237
  const includeConfig = req.query.config === "true";
3228
3238
  const poolStats = adminContext.pools.map((pool, index) => {
@@ -3230,8 +3240,8 @@ const createAdminRouter = (adminContext) => {
3230
3240
  const name = cache?.poolName ?? `pool-${index}`;
3231
3241
  const p = pool;
3232
3242
  const stats = typeof p.getStatistics === "function" ? p.getStatistics() : null;
3233
- const procStats = cache?.procedureNameCache.getStats();
3234
- const argStats = cache?.argumentCache.getStats();
3243
+ const procStats = cache?.procedureNameCache?.getStats();
3244
+ const argStats = cache?.argumentCache?.getStats();
3235
3245
  return {
3236
3246
  name,
3237
3247
  stats,
@@ -3239,12 +3249,12 @@ const createAdminRouter = (adminContext) => {
3239
3249
  connectionsInUse: pool.connectionsInUse,
3240
3250
  cache: {
3241
3251
  procedureName: {
3242
- size: cache?.procedureNameCache.keys().length ?? 0,
3252
+ size: cache?.procedureNameCache?.keys().length ?? 0,
3243
3253
  hits: procStats?.hits ?? 0,
3244
3254
  misses: procStats?.misses ?? 0
3245
3255
  },
3246
3256
  argument: {
3247
- size: cache?.argumentCache.keys().length ?? 0,
3257
+ size: cache?.argumentCache?.keys().length ?? 0,
3248
3258
  hits: argStats?.hits ?? 0,
3249
3259
  misses: argStats?.misses ?? 0
3250
3260
  }
@@ -3315,11 +3325,11 @@ const createAdminRouter = (adminContext) => {
3315
3325
  let limit = 100;
3316
3326
  if (typeof limitQuery === "string") {
3317
3327
  const parsed = Number(limitQuery);
3318
- if (!isNaN(parsed)) limit = parsed;
3328
+ if (!Number.isNaN(parsed)) limit = parsed;
3319
3329
  }
3320
3330
  const history = adminContext.statsManager.getHistory();
3321
3331
  const slice = limit > 0 ? history.slice(-limit) : [...history];
3322
- res.json(slice.reverse());
3332
+ res.json(slice.toReversed());
3323
3333
  });
3324
3334
  router.get("/api/logs/error", async (req, res) => {
3325
3335
  try {
@@ -3332,7 +3342,7 @@ const createAdminRouter = (adminContext) => {
3332
3342
  }).filter((l) => l !== null);
3333
3343
  const logs = z.array(logEntrySchema).safeParse(parsedLines);
3334
3344
  if (!logs.success) throw new Error(`Validation failed: ${logs.error.message}`);
3335
- return res.json(logs.data.reverse());
3345
+ return res.json(logs.data.toReversed());
3336
3346
  } catch (err) {
3337
3347
  return res.status(500).json({ error: String(err) });
3338
3348
  }
@@ -3347,7 +3357,7 @@ const createAdminRouter = (adminContext) => {
3347
3357
  return;
3348
3358
  }
3349
3359
  const lines = await readLastLines(logFile, limit, filter);
3350
- res.json(lines.reverse());
3360
+ res.json(lines.toReversed());
3351
3361
  } catch (err) {
3352
3362
  res.status(500).json({ error: String(err) });
3353
3363
  }
@@ -3367,25 +3377,29 @@ const createAdminRouter = (adminContext) => {
3367
3377
  res.json({ message: `Cleared ${cleared} caches` });
3368
3378
  });
3369
3379
  router.post("/api/server/:action", (req, res) => {
3370
- const action = req.params.action;
3371
- if (action === "stop") {
3372
- res.json({ message: "Server shutting down..." });
3373
- setTimeout(() => {
3374
- forceShutdown();
3375
- }, SHUTDOWN_GRACE_DELAY_MS);
3376
- } else if (action === "pause") {
3377
- adminContext.setPaused(true);
3378
- res.json({
3379
- message: "Server paused",
3380
- status: "paused"
3381
- });
3382
- } else if (action === "resume") {
3383
- adminContext.setPaused(false);
3384
- res.json({
3385
- message: "Server resumed",
3386
- status: "running"
3387
- });
3388
- } else res.status(400).json({ error: "Invalid action" });
3380
+ switch (req.params.action) {
3381
+ case "stop":
3382
+ res.json({ message: "Server shutting down..." });
3383
+ setTimeout(() => {
3384
+ forceShutdown();
3385
+ }, SHUTDOWN_GRACE_DELAY_MS);
3386
+ break;
3387
+ case "pause":
3388
+ adminContext.setPaused(true);
3389
+ res.json({
3390
+ message: "Server paused",
3391
+ status: "paused"
3392
+ });
3393
+ break;
3394
+ case "resume":
3395
+ adminContext.setPaused(false);
3396
+ res.json({
3397
+ message: "Server resumed",
3398
+ status: "running"
3399
+ });
3400
+ break;
3401
+ default: res.status(400).json({ error: "Invalid action" });
3402
+ }
3389
3403
  });
3390
3404
  router.get("/api/trace/status", (_req, res) => {
3391
3405
  res.json({ enabled: traceManager.isEnabled() });
@@ -3409,7 +3423,7 @@ const createAdminRouter = (adminContext) => {
3409
3423
  }).filter((l) => l !== null);
3410
3424
  const logs = z.array(procedureTraceEntrySchema).safeParse(parsedLines);
3411
3425
  if (!logs.success) throw new Error(`Validation failed: ${logs.error.message}`);
3412
- return res.json(logs.data.reverse());
3426
+ return res.json(logs.data.toReversed());
3413
3427
  } catch (err) {
3414
3428
  return res.status(500).json({ error: String(err) });
3415
3429
  }
@@ -3447,32 +3461,45 @@ const handlerAdminConsole = (config, adminContext) => {
3447
3461
  const resolvedStaticDir = config.staticDir ?? resolveAdminStaticDir();
3448
3462
  if (adminRoute && !adminRoute.startsWith("/")) throw new Error("adminRoute must start with /");
3449
3463
  if (!config.devMode && !existsSync(resolvedStaticDir)) throw new Error(`Admin console not built. Run 'npm run build:frontend' first.\nExpected: ${resolvedStaticDir}`);
3450
- const originalRotate = adminContext.statsManager.rotateBucket.bind(adminContext.statsManager);
3451
- adminContext.statsManager.rotateBucket = () => {
3452
- originalRotate(adminContext.pools.map((pool, index) => {
3453
- const cache = adminContext.caches[index];
3454
- const name = cache?.poolName ?? `pool-${index}`;
3455
- const procStats = cache?.procedureNameCache.getStats();
3456
- const argStats = cache?.argumentCache.getStats();
3457
- return {
3458
- name,
3459
- connectionsOpen: pool.connectionsOpen,
3460
- connectionsInUse: pool.connectionsInUse,
3461
- cache: {
3462
- procedureName: {
3463
- size: cache?.procedureNameCache.keys().length ?? 0,
3464
- hits: procStats?.hits ?? 0,
3465
- misses: procStats?.misses ?? 0
3466
- },
3467
- argument: {
3468
- size: cache?.argumentCache.keys().length ?? 0,
3469
- hits: argStats?.hits ?? 0,
3470
- misses: argStats?.misses ?? 0
3464
+ const statsManager = adminContext.statsManager;
3465
+ const currentRotate = statsManager.rotateBucket;
3466
+ if (!Object.prototype.hasOwnProperty.call(currentRotate, "_isWrapped")) {
3467
+ const originalRotate = statsManager.rotateBucket;
3468
+ const wrappedRotate = (poolSnapshots = []) => {
3469
+ const mergedSnapshots = [...adminContext.pools.map((pool, index) => {
3470
+ const cache = adminContext.caches[index];
3471
+ const name = cache?.poolName ?? `pool-${index}`;
3472
+ const procStats = cache?.procedureNameCache?.getStats();
3473
+ const argStats = cache?.argumentCache?.getStats();
3474
+ return {
3475
+ name,
3476
+ connectionsOpen: pool.connectionsOpen,
3477
+ connectionsInUse: pool.connectionsInUse,
3478
+ cache: {
3479
+ procedureName: {
3480
+ size: cache?.procedureNameCache?.keys().length ?? 0,
3481
+ hits: procStats?.hits ?? 0,
3482
+ misses: procStats?.misses ?? 0
3483
+ },
3484
+ argument: {
3485
+ size: cache?.argumentCache?.keys().length ?? 0,
3486
+ hits: argStats?.hits ?? 0,
3487
+ misses: argStats?.misses ?? 0
3488
+ }
3471
3489
  }
3472
- }
3473
- };
3474
- }));
3475
- };
3490
+ };
3491
+ })];
3492
+ if (Array.isArray(poolSnapshots)) {
3493
+ for (const ps of poolSnapshots) if (!mergedSnapshots.some((s) => s.name === ps.name)) mergedSnapshots.push(ps);
3494
+ }
3495
+ originalRotate.call(statsManager, mergedSnapshots);
3496
+ };
3497
+ Object.defineProperty(wrappedRotate, "_isWrapped", {
3498
+ value: true,
3499
+ writable: false
3500
+ });
3501
+ statsManager.rotateBucket = wrappedRotate;
3502
+ }
3476
3503
  const router = Router();
3477
3504
  router.use((req, res, next) => {
3478
3505
  if (adminContext.paused && !req.path.startsWith(adminRoute)) {