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/frontend/assets/{main-0oxRTIkL.js → main-D2PlS0b8.js} +4 -4
- package/dist/frontend/assets/main-D2PlS0b8.js.br +0 -0
- package/dist/frontend/assets/{main-0oxRTIkL.js.gz → main-D2PlS0b8.js.gz} +0 -0
- package/dist/frontend/assets/{main-TYgLak6O.css → main-bU4x_yEW.css} +1 -1
- package/dist/frontend/assets/main-bU4x_yEW.css.br +0 -0
- package/dist/frontend/assets/main-bU4x_yEW.css.gz +0 -0
- package/dist/frontend/index.html +2 -2
- package/dist/frontend/index.html.br +0 -0
- package/dist/frontend/index.html.gz +0 -0
- package/dist/index.mjs +166 -139
- package/dist/index.mjs.map +1 -1
- package/package.json +22 -11
- package/dist/frontend/assets/main-0oxRTIkL.js.br +0 -0
- package/dist/frontend/assets/main-TYgLak6O.css.br +0 -0
- package/dist/frontend/assets/main-TYgLak6O.css.gz +0 -0
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
|
-
|
|
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.
|
|
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 =
|
|
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
|
|
928
|
-
durationMax: 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.
|
|
1109
|
+
const escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
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, "&").replace(/</g, "<")
|
|
|
1114
1114
|
*/
|
|
1115
1115
|
const convertAsciiToHtml = (text) => {
|
|
1116
1116
|
let html = escapeHtml(text);
|
|
1117
|
-
html = html.
|
|
1118
|
-
html = html.
|
|
1117
|
+
html = html.replaceAll(/\r\n|\r|\n/g, "<br />");
|
|
1118
|
+
html = html.replaceAll(" ", " ");
|
|
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 (
|
|
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
|
|
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:
|
|
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:
|
|
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.
|
|
1838
|
-
page.body = text.
|
|
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
|
|
1849
|
-
else
|
|
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 (
|
|
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
|
-
}
|
|
1874
|
+
}
|
|
1863
1875
|
}
|
|
1864
1876
|
break;
|
|
1865
1877
|
case "status":
|
|
1866
1878
|
{
|
|
1867
|
-
const statusCode = parseInt(header.value, 10);
|
|
1868
|
-
if (
|
|
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.
|
|
1873
|
-
}
|
|
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.
|
|
1897
|
-
value: line.
|
|
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.
|
|
1916
|
-
value: firstElement.
|
|
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.
|
|
1922
|
-
else if (element.toLowerCase().startsWith("domain=")) cookie.options.domain = element.
|
|
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.
|
|
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.
|
|
2537
|
-
|
|
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 ?? "").
|
|
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).
|
|
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.
|
|
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.
|
|
2868
|
+
const tmp = trimPath(pathname.slice(0, Math.max(0, pathname.lastIndexOf("/") + 1)));
|
|
2862
2869
|
return {
|
|
2863
2870
|
script: `/${tmp}`,
|
|
2864
|
-
prefix: `/${tmp.
|
|
2865
|
-
dad: tmp.
|
|
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.
|
|
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
|
-
|
|
2966
|
-
|
|
2967
|
-
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
aggregator
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
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 = (
|
|
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
|
|
3234
|
-
const argStats = cache?.argumentCache
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
3371
|
-
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
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.
|
|
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
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
3455
|
-
const
|
|
3456
|
-
|
|
3457
|
-
|
|
3458
|
-
|
|
3459
|
-
|
|
3460
|
-
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
3464
|
-
|
|
3465
|
-
|
|
3466
|
-
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
|
|
3470
|
-
|
|
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)) {
|