web_plsql 1.0.0 → 1.3.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 (108) hide show
  1. package/README.md +108 -13
  2. package/dist/chunk-DQk6qfdC.mjs +18 -0
  3. package/dist/frontend/assets/main-0oxRTIkL.js +147 -0
  4. package/dist/frontend/assets/main-0oxRTIkL.js.br +0 -0
  5. package/dist/frontend/assets/main-0oxRTIkL.js.gz +0 -0
  6. package/dist/frontend/assets/main-TYgLak6O.css +1 -0
  7. package/dist/frontend/assets/main-TYgLak6O.css.br +0 -0
  8. package/dist/frontend/assets/main-TYgLak6O.css.gz +0 -0
  9. package/dist/frontend/index.html +516 -0
  10. package/dist/frontend/index.html.br +0 -0
  11. package/dist/frontend/index.html.gz +0 -0
  12. package/dist/index.d.mts +646 -0
  13. package/dist/index.mjs +3548 -0
  14. package/dist/index.mjs.map +1 -0
  15. package/dist/oracledb-mock-Dn8aHtd7.mjs +68 -0
  16. package/dist/oracledb-mock-Dn8aHtd7.mjs.map +1 -0
  17. package/examples/config-apex.js +1 -1
  18. package/examples/config-docker.js +1 -2
  19. package/package.json +45 -32
  20. package/examples/config-native.js +0 -68
  21. package/src/admin/client/charts.ts +0 -299
  22. package/src/admin/client/main.js +0 -3
  23. package/src/admin/client/tailwind.css +0 -41
  24. package/src/admin/index.html +0 -315
  25. package/src/admin/js/api.ts +0 -95
  26. package/src/admin/js/app.ts +0 -306
  27. package/src/admin/js/eslint.config.js +0 -74
  28. package/src/admin/js/schemas.ts +0 -153
  29. package/src/admin/js/templates/config.ts +0 -146
  30. package/src/admin/js/templates/errorRow.ts +0 -18
  31. package/src/admin/js/templates/index.ts +0 -3
  32. package/src/admin/js/templates/poolCard.ts +0 -61
  33. package/src/admin/js/tsconfig.json +0 -24
  34. package/src/admin/js/types.ts +0 -223
  35. package/src/admin/js/ui/theme.ts +0 -93
  36. package/src/admin/js/ui/views.ts +0 -164
  37. package/src/admin/js/util/format.ts +0 -27
  38. package/src/admin/lib/assets/main-zpdhQ1gD.css +0 -1
  39. package/src/admin/lib/chart.bundle.js +0 -139
  40. package/src/admin/style.css +0 -1321
  41. package/src/bin/load-test.js +0 -202
  42. package/src/handler/handlerAdmin.js +0 -198
  43. package/src/handler/handlerLogger.js +0 -21
  44. package/src/handler/handlerMetrics.js +0 -68
  45. package/src/handler/handlerUpload.js +0 -26
  46. package/src/handler/plsql/cgi.js +0 -131
  47. package/src/handler/plsql/errorPage.js +0 -117
  48. package/src/handler/plsql/handlerPlSql.js +0 -90
  49. package/src/handler/plsql/parsePage.js +0 -213
  50. package/src/handler/plsql/procedure.js +0 -356
  51. package/src/handler/plsql/procedureError.js +0 -40
  52. package/src/handler/plsql/procedureNamed.js +0 -268
  53. package/src/handler/plsql/procedureSanitize.js +0 -262
  54. package/src/handler/plsql/procedureVariable.js +0 -54
  55. package/src/handler/plsql/request.js +0 -124
  56. package/src/handler/plsql/requestError.js +0 -23
  57. package/src/handler/plsql/sendResponse.js +0 -136
  58. package/src/handler/plsql/stream.js +0 -28
  59. package/src/handler/plsql/upload.js +0 -109
  60. package/src/index.js +0 -20
  61. package/src/server/config.js +0 -57
  62. package/src/server/connections.js +0 -40
  63. package/src/server/server.js +0 -289
  64. package/src/types.js +0 -150
  65. package/src/util/cache.js +0 -123
  66. package/src/util/errorToString.js +0 -24
  67. package/src/util/file.js +0 -91
  68. package/src/util/html.js +0 -53
  69. package/src/util/jsonLogger.js +0 -47
  70. package/src/util/oracle.js +0 -80
  71. package/src/util/shutdown.js +0 -44
  72. package/src/util/trace.js +0 -325
  73. package/src/util/type.js +0 -6
  74. package/src/util/util.js +0 -66
  75. package/src/version.js +0 -5
  76. package/types/handler/handlerAdmin.d.ts +0 -9
  77. package/types/handler/handlerLogger.d.ts +0 -2
  78. package/types/handler/handlerMetrics.d.ts +0 -25
  79. package/types/handler/handlerUpload.d.ts +0 -7
  80. package/types/handler/plsql/cgi.d.ts +0 -4
  81. package/types/handler/plsql/errorPage.d.ts +0 -11
  82. package/types/handler/plsql/handlerPlSql.d.ts +0 -13
  83. package/types/handler/plsql/parsePage.d.ts +0 -3
  84. package/types/handler/plsql/procedure.d.ts +0 -12
  85. package/types/handler/plsql/procedureError.d.ts +0 -23
  86. package/types/handler/plsql/procedureNamed.d.ts +0 -14
  87. package/types/handler/plsql/procedureSanitize.d.ts +0 -11
  88. package/types/handler/plsql/procedureVariable.d.ts +0 -10
  89. package/types/handler/plsql/request.d.ts +0 -9
  90. package/types/handler/plsql/requestError.d.ts +0 -8
  91. package/types/handler/plsql/sendResponse.d.ts +0 -6
  92. package/types/handler/plsql/upload.d.ts +0 -5
  93. package/types/index.d.ts +0 -11
  94. package/types/server/config.d.ts +0 -2
  95. package/types/server/server.d.ts +0 -74
  96. package/types/types.d.ts +0 -328
  97. package/types/util/cache.d.ts +0 -69
  98. package/types/util/errorToString.d.ts +0 -1
  99. package/types/util/file.d.ts +0 -6
  100. package/types/util/html.d.ts +0 -3
  101. package/types/util/jsonLogger.d.ts +0 -45
  102. package/types/util/oracle.d.ts +0 -5
  103. package/types/util/shutdown.d.ts +0 -2
  104. package/types/util/trace.d.ts +0 -22
  105. package/types/util/type.d.ts +0 -1
  106. package/types/util/util.d.ts +0 -3
  107. package/types/version.d.ts +0 -1
  108. /package/{src/admin/favicon.svg → dist/frontend/assets/favicon-mQAM4tVu.svg} +0 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,3548 @@
1
+ import { t as __exportAll } from "./chunk-DQk6qfdC.mjs";
2
+ import { n as setExecuteCallback } from "./oracledb-mock-Dn8aHtd7.mjs";
3
+ import z$1, { z } from "zod";
4
+ import oracledb from "oracledb";
5
+ import debugModule from "debug";
6
+ import http from "node:http";
7
+ import https from "node:https";
8
+ import express, { Router } from "express";
9
+ import cors from "cors";
10
+ import cookieParser from "cookie-parser";
11
+ import compression from "compression";
12
+ import expressStaticGzip from "express-static-gzip";
13
+ import os from "node:os";
14
+ import url, { URL, fileURLToPath } from "node:url";
15
+ import util from "node:util";
16
+ import stream, { Readable } from "node:stream";
17
+ import fs, { existsSync, promises, readFileSync } from "node:fs";
18
+ import * as rotatingFileStream from "rotating-file-stream";
19
+ import path from "node:path";
20
+ import morgan from "morgan";
21
+ import multer from "multer";
22
+ import readline from "node:readline";
23
+
24
+ //#region src/common/configStaticSchema.ts
25
+ /**
26
+ * Configuration for serving static files
27
+ */
28
+ const configStaticSchema = z$1.strictObject({
29
+ route: z$1.string(),
30
+ directoryPath: z$1.string(),
31
+ spaFallback: z$1.boolean().optional()
32
+ });
33
+
34
+ //#endregion
35
+ //#region src/common/procedureTraceEntry.ts
36
+ const procedureTraceEntrySchema = z.strictObject({
37
+ id: z.string(),
38
+ timestamp: z.string(),
39
+ source: z.string(),
40
+ url: z.string(),
41
+ method: z.string(),
42
+ status: z.string(),
43
+ duration: z.number(),
44
+ procedure: z.string().optional(),
45
+ parameters: z.union([z.record(z.string(), z.unknown()), z.array(z.unknown())]).optional(),
46
+ uploads: z.array(z.strictObject({
47
+ originalname: z.string(),
48
+ mimetype: z.string(),
49
+ size: z.number()
50
+ })).optional(),
51
+ downloads: z.strictObject({
52
+ fileType: z.string(),
53
+ fileSize: z.number()
54
+ }).optional(),
55
+ html: z.string().optional(),
56
+ cookies: z.record(z.string(), z.string()).optional(),
57
+ headers: z.record(z.string(), z.string()).optional(),
58
+ cgi: z.record(z.string(), z.string()).optional(),
59
+ error: z.string().optional()
60
+ });
61
+
62
+ //#endregion
63
+ //#region src/common/logEntrySchema.ts
64
+ /**
65
+ * Error log entry schema.
66
+ */
67
+ const logEntryTypeSchema = z.union([
68
+ z.literal("error"),
69
+ z.literal("info"),
70
+ z.literal("warning")
71
+ ]);
72
+ const logEntrySchema = z.strictObject({
73
+ timestamp: z.string(),
74
+ type: logEntryTypeSchema,
75
+ message: z.string(),
76
+ req: z.strictObject({
77
+ method: z.string().optional(),
78
+ url: z.string().optional(),
79
+ ip: z.string().optional(),
80
+ userAgent: z.string().optional()
81
+ }).optional(),
82
+ details: z.strictObject({
83
+ fullMessage: z.string().optional(),
84
+ sql: z.string().optional(),
85
+ bind: z.unknown().optional(),
86
+ environment: z.record(z.string(), z.string()).optional()
87
+ }).optional()
88
+ });
89
+
90
+ //#endregion
91
+ //#region src/backend/types.ts
92
+ /**
93
+ * Defines the style of error reporting
94
+ * 'basic': standard error messages
95
+ * 'debug': detailed error messages including database context
96
+ */
97
+ const z$errorStyleType = z$1.enum(["basic", "debug"]);
98
+ /**
99
+ * Defines how transactions are handled after procedure execution
100
+ * 'commit': automatically commit
101
+ * 'rollback': automatically rollback
102
+ * callback: custom function for manual handling
103
+ */
104
+ const transactionModeSchema = z$1.union([
105
+ z$1.custom((val) => typeof val === "function", { message: "Invalid transaction callback" }),
106
+ z$1.literal("commit"),
107
+ z$1.literal("rollback"),
108
+ z$1.undefined(),
109
+ z$1.null()
110
+ ]);
111
+ /**
112
+ * Authentication configuration for a PL/SQL route
113
+ */
114
+ const z$authSchema = z$1.strictObject({
115
+ type: z$1.literal("basic"),
116
+ callback: z$1.custom((val) => typeof val === "function", { message: "Invalid auth callback" }),
117
+ realm: z$1.string().optional()
118
+ });
119
+ /**
120
+ * PL/SQL handler behavior configuration
121
+ */
122
+ const z$configPlSqlHandlerType = z$1.strictObject({
123
+ defaultPage: z$1.string(),
124
+ pathAlias: z$1.string().optional(),
125
+ pathAliasProcedure: z$1.string().optional(),
126
+ documentTable: z$1.string(),
127
+ exclusionList: z$1.array(z$1.string()).optional(),
128
+ requestValidationFunction: z$1.string().optional(),
129
+ transactionMode: transactionModeSchema.optional(),
130
+ errorStyle: z$errorStyleType,
131
+ cgi: z$1.record(z$1.string(), z$1.string()).optional(),
132
+ auth: z$authSchema.optional()
133
+ });
134
+ /**
135
+ * Database connection configuration for a PL/SQL route
136
+ */
137
+ const z$configPlSqlConfigType = z$1.strictObject({
138
+ route: z$1.string(),
139
+ user: z$1.string(),
140
+ password: z$1.string(),
141
+ connectString: z$1.string()
142
+ });
143
+ const z$configPlSqlType = z$1.strictObject({
144
+ ...z$configPlSqlHandlerType.shape,
145
+ ...z$configPlSqlConfigType.shape
146
+ });
147
+ /**
148
+ * Root application configuration
149
+ */
150
+ const z$configType = z$1.strictObject({
151
+ port: z$1.number(),
152
+ routeStatic: z$1.array(configStaticSchema),
153
+ routePlSql: z$1.array(z$configPlSqlType),
154
+ uploadFileSizeLimit: z$1.number().optional(),
155
+ loggerFilename: z$1.string(),
156
+ adminRoute: z$1.string().optional(),
157
+ adminUser: z$1.string().optional(),
158
+ adminPassword: z$1.string().optional(),
159
+ devMode: z$1.boolean().optional()
160
+ });
161
+
162
+ //#endregion
163
+ //#region src/backend/util/oracledb-provider.ts
164
+ var oracledb_provider_exports = /* @__PURE__ */ __exportAll({
165
+ BIND_IN: () => BIND_IN,
166
+ BIND_INOUT: () => BIND_INOUT,
167
+ BIND_OUT: () => BIND_OUT,
168
+ BLOB: () => BLOB,
169
+ BUFFER: () => BUFFER,
170
+ CLOB: () => CLOB,
171
+ CURSOR: () => CURSOR,
172
+ DATE: () => DATE,
173
+ DB_TYPE_CLOB: () => DB_TYPE_CLOB,
174
+ DB_TYPE_DATE: () => DB_TYPE_DATE,
175
+ DB_TYPE_NUMBER: () => DB_TYPE_NUMBER,
176
+ DB_TYPE_VARCHAR: () => DB_TYPE_VARCHAR,
177
+ NUMBER: () => NUMBER,
178
+ STRING: () => STRING,
179
+ createPool: () => createPool,
180
+ setExecuteCallback: () => setExecuteCallback
181
+ });
182
+ const USE_MOCK = process.env.MOCK_ORACLE === "true";
183
+ /**
184
+ * Create a database pool.
185
+ * @param config - The pool attributes.
186
+ * @returns The pool.
187
+ */
188
+ async function createPool(config) {
189
+ if (USE_MOCK) return (await import("./oracledb-mock-Dn8aHtd7.mjs").then((n) => n.t)).createPool(config);
190
+ return await oracledb.createPool(config);
191
+ }
192
+ const BIND_IN = oracledb.BIND_IN;
193
+ const BIND_OUT = oracledb.BIND_OUT;
194
+ const BIND_INOUT = oracledb.BIND_INOUT;
195
+ const STRING = oracledb.STRING;
196
+ const NUMBER = oracledb.NUMBER;
197
+ const DATE = oracledb.DATE;
198
+ const CURSOR = oracledb.CURSOR;
199
+ const BUFFER = oracledb.BUFFER;
200
+ const CLOB = oracledb.CLOB;
201
+ const BLOB = oracledb.BLOB;
202
+ const DB_TYPE_VARCHAR = oracledb.DB_TYPE_VARCHAR;
203
+ const DB_TYPE_CLOB = oracledb.DB_TYPE_CLOB;
204
+ const DB_TYPE_NUMBER = oracledb.DB_TYPE_NUMBER;
205
+ const DB_TYPE_DATE = oracledb.DB_TYPE_DATE;
206
+
207
+ //#endregion
208
+ //#region src/backend/version.ts
209
+ if (typeof globalThis.__VERSION__ === "undefined") globalThis.__VERSION__ = "**development**";
210
+ /**
211
+ * Returns the current library version
212
+ * @returns {string} - Version.
213
+ */
214
+ const getVersion = () => "1.3.0";
215
+
216
+ //#endregion
217
+ //#region src/backend/server/config.ts
218
+ const paddedLine = (title, value) => {
219
+ console.log(`${(title + ":").padEnd(30)} ${value}`);
220
+ };
221
+ /**
222
+ * Show configuration.
223
+ * @param config - The config.
224
+ */
225
+ const showConfig = (config) => {
226
+ const LINE = "-".repeat(80);
227
+ console.log(LINE);
228
+ console.log(`NODE PL/SQL SERVER version ${getVersion()}`);
229
+ console.log(LINE);
230
+ paddedLine("Server port", config.port);
231
+ paddedLine("Admin route", `${config.adminRoute ?? "/admin"}${config.adminUser ? " (authenticated)" : ""}`);
232
+ paddedLine("Access log", config.loggerFilename.length > 0 ? config.loggerFilename : "-");
233
+ paddedLine("Upload file size limit", typeof config.uploadFileSizeLimit === "number" ? `${config.uploadFileSizeLimit} bytes` : "-");
234
+ if (config.routeStatic.length > 0) config.routeStatic.forEach((e) => {
235
+ paddedLine("Static route", e.route);
236
+ paddedLine("Directory path", e.directoryPath);
237
+ });
238
+ if (config.routePlSql.length > 0) config.routePlSql.forEach((e) => {
239
+ let transactionMode = "";
240
+ if (typeof e.transactionMode === "string") transactionMode = e.transactionMode;
241
+ else if (typeof e.transactionMode === "function") transactionMode = "custom callback";
242
+ paddedLine("Route", `http://localhost:${config.port}${e.route}`);
243
+ paddedLine("Oracle user", e.user);
244
+ paddedLine("Oracle server", e.connectString);
245
+ paddedLine("Oracle document table", e.documentTable);
246
+ paddedLine("Default page", e.defaultPage.length > 0 ? e.defaultPage : "-");
247
+ paddedLine("Path alias", e.pathAlias ?? "-");
248
+ paddedLine("Path alias procedure", e.pathAliasProcedure ?? "-");
249
+ paddedLine("Exclution list", e.exclusionList ? e.exclusionList.join(", ") : "-");
250
+ paddedLine("Validation function", e.requestValidationFunction ?? "-");
251
+ paddedLine("After request handler", transactionMode.length > 0 ? transactionMode : "-");
252
+ paddedLine("Error style", e.errorStyle);
253
+ });
254
+ console.log(LINE);
255
+ const baseUrl = `http://localhost:${config.port}`;
256
+ paddedLine("🏠 Admin Console", `${baseUrl}${config.adminRoute ?? "/admin"}`);
257
+ if (config.routePlSql.length > 0) {
258
+ console.log("");
259
+ console.log("⚙️ PL/SQL Gateways:");
260
+ config.routePlSql.forEach((e) => {
261
+ console.log(` ${e.route.padEnd(28)} ${baseUrl}${e.route}`);
262
+ });
263
+ }
264
+ console.log(LINE);
265
+ };
266
+
267
+ //#endregion
268
+ //#region src/backend/server/server.ts
269
+ const debug$14 = debugModule("webplsql:server");
270
+ /**
271
+ * Close multiple pools.
272
+ * @param pools - The pools to close.
273
+ */
274
+ const poolsClose = async (pools) => {
275
+ await Promise.all(pools.map((pool) => pool.close(0)));
276
+ };
277
+ /**
278
+ * Create HTTPS server.
279
+ * @param app - express application
280
+ * @param ssl - ssl configuration.
281
+ * @returns server
282
+ */
283
+ const createServer = (app, ssl) => {
284
+ if (ssl) {
285
+ const key = readFileSyncUtf8(ssl.keyFilename);
286
+ const cert = readFileSyncUtf8(ssl.certFilename);
287
+ return https.createServer({
288
+ key,
289
+ cert
290
+ }, app);
291
+ } else return http.createServer(app);
292
+ };
293
+ /**
294
+ * Start server.
295
+ * @param config - The config.
296
+ * @param ssl - ssl configuration.
297
+ * @returns Promise resolving to the web server object.
298
+ */
299
+ const startServer = async (config, ssl) => {
300
+ debug$14("startServer: BEGIN", config, ssl);
301
+ const internalConfig = z$configType.parse(config);
302
+ showConfig(internalConfig);
303
+ const app = express();
304
+ if (internalConfig.devMode) app.use(cors({
305
+ origin: "http://localhost:5173",
306
+ credentials: true
307
+ }));
308
+ app.use(handlerUpload(internalConfig.uploadFileSizeLimit));
309
+ app.use(express.json({ limit: "50mb" }));
310
+ app.use(express.urlencoded({
311
+ limit: "50mb",
312
+ extended: true
313
+ }));
314
+ app.use(cookieParser());
315
+ app.use(compression());
316
+ const adminContext = new AdminContext(internalConfig);
317
+ app.use(handlerAdminConsole({
318
+ adminRoute: internalConfig.adminRoute,
319
+ user: internalConfig.adminUser,
320
+ password: internalConfig.adminPassword,
321
+ devMode: internalConfig.devMode
322
+ }, adminContext));
323
+ for (const i of internalConfig.routePlSql) {
324
+ const handler = handlerWebPlSql(await createPool({
325
+ user: i.user,
326
+ password: i.password,
327
+ connectString: i.connectString
328
+ }), i, adminContext);
329
+ app.use([`${i.route}/:name`, i.route], (req, res, next) => {
330
+ const start = process.hrtime();
331
+ res.on("finish", () => {
332
+ const diff = process.hrtime(start);
333
+ const duration = diff[0] * 1e3 + diff[1] / 1e6;
334
+ adminContext.statsManager.recordRequest(duration, res.statusCode >= 400);
335
+ });
336
+ handler(req, res, next);
337
+ });
338
+ }
339
+ if (internalConfig.loggerFilename.length > 0) app.use(handlerLogger(internalConfig.loggerFilename));
340
+ for (const i of internalConfig.routeStatic) {
341
+ app.use(i.route, expressStaticGzip(i.directoryPath, {
342
+ enableBrotli: true,
343
+ orderPreference: ["br"]
344
+ }));
345
+ if (i.spaFallback) app.use(i.route, createSpaFallback(i.directoryPath, i.route));
346
+ }
347
+ debug$14("startServer: createServer");
348
+ const server = createServer(app, ssl);
349
+ const connections = /* @__PURE__ */ new Set();
350
+ server.on("connection", (socket) => {
351
+ connections.add(socket);
352
+ socket.on("close", () => {
353
+ connections.delete(socket);
354
+ });
355
+ });
356
+ const closeAllConnections = () => {
357
+ for (const socket of connections) {
358
+ socket.destroy();
359
+ connections.delete(socket);
360
+ }
361
+ };
362
+ const shutdown = async () => {
363
+ debug$14("startServer: onShutdown");
364
+ adminContext.statsManager.stop();
365
+ await poolsClose(adminContext.pools);
366
+ server.close(() => {
367
+ console.log("Server has closed");
368
+ process.exit(0);
369
+ });
370
+ closeAllConnections();
371
+ };
372
+ installShutdown(shutdown);
373
+ debug$14("startServer: start listener");
374
+ await new Promise((resolve, reject) => {
375
+ server.listen(internalConfig.port).on("listening", () => {
376
+ debug$14("startServer: listener running");
377
+ resolve();
378
+ }).on("error", (err) => {
379
+ if ("code" in err) {
380
+ if (err.code === "EADDRINUSE") err.message = `Port ${internalConfig.port} is already in use`;
381
+ else if (err.code === "EACCES") err.message = `Port ${internalConfig.port} requires elevated privileges`;
382
+ }
383
+ reject(err);
384
+ });
385
+ });
386
+ debug$14("startServer: END");
387
+ return {
388
+ config: internalConfig,
389
+ connectionPools: adminContext.pools,
390
+ app,
391
+ server,
392
+ adminContext,
393
+ shutdown
394
+ };
395
+ };
396
+ /**
397
+ * Load configuration.
398
+ * @param filename - The configuration filename.
399
+ * @returns Promise.
400
+ */
401
+ const loadConfig = (filename = "config.json") => z$configType.parse(getJsonFile(filename));
402
+ /**
403
+ * Start server from config file.
404
+ * @param filename - The configuration filename.
405
+ * @param ssl - ssl configuration.
406
+ * @returns Promise resolving to the web server object.
407
+ */
408
+ const startServerConfig = async (filename = "config.json", ssl) => startServer(loadConfig(filename), ssl);
409
+
410
+ //#endregion
411
+ //#region src/backend/util/shutdown.ts
412
+ const debug$13 = debugModule("webplsql:shutdown");
413
+ /**
414
+ * Install a shutdown handler.
415
+ * @param handler - Shutdown handler
416
+ */
417
+ const installShutdown = (handler) => {
418
+ debug$13("installShutdown");
419
+ let isShuttingDown = false;
420
+ process.on("unhandledRejection", (reason) => {
421
+ if (isShuttingDown) return;
422
+ isShuttingDown = true;
423
+ if (reason instanceof Error) console.error(`\n${reason.message}. Graceful shutdown...`);
424
+ else console.error("\nUnhandled promise rejection. Graceful shutdown...", reason);
425
+ handler().catch((err) => {
426
+ console.error("Error during shutdown:", err);
427
+ process.exit(1);
428
+ });
429
+ });
430
+ process.on("SIGTERM", function sigterm() {
431
+ if (isShuttingDown) return;
432
+ isShuttingDown = true;
433
+ console.log("\nGot SIGTERM (aka docker container stop). Graceful shutdown...");
434
+ handler().catch((err) => {
435
+ console.error("Error during shutdown:", err);
436
+ process.exit(1);
437
+ });
438
+ });
439
+ process.on("SIGINT", function sigint() {
440
+ if (isShuttingDown) return;
441
+ isShuttingDown = true;
442
+ console.log("\nGot SIGINT (aka ctrl-c in docker). Graceful shutdown...");
443
+ handler().catch((err) => {
444
+ console.error("Error during shutdown:", err);
445
+ process.exit(1);
446
+ });
447
+ });
448
+ };
449
+ /**
450
+ * Force a shutdown.
451
+ */
452
+ const forceShutdown = () => {
453
+ debug$13("forceShutdown");
454
+ process.kill(process.pid, "SIGTERM");
455
+ };
456
+
457
+ //#endregion
458
+ //#region src/common/constants.ts
459
+ /**
460
+ * Web PL/SQL Gateway - Common Shared Constants
461
+ *
462
+ * This file centralizes all hardcoded numeric and string constants used throughout
463
+ * the application. Constants are organized by functional category.
464
+ */
465
+ /**
466
+ * DEFAULT_CACHE_MAX_SIZE = 10000
467
+ *
468
+ * Purpose: Maximum number of entries in the generic LFU (Least Frequently Used) cache.
469
+ *
470
+ * Used By:
471
+ * - procedureNameCache: Caches resolved Oracle procedure names (e.g., "HR.EMPLOYEES")
472
+ * - argumentCache: Caches procedure argument introspection results from all_arguments view
473
+ *
474
+ * Related Values:
475
+ * - CACHE_PRUNE_PERCENT (0.1): When cache is full, removes 10% = 1000 entries
476
+ * - Cache instantiation in src/handler/plsql/handlerPlSql.js creates new Cache() without params
477
+ *
478
+ * Implications:
479
+ * - Memory footprint: ~1-2MB at max capacity (strings + hitCount metadata)
480
+ * - Pruning: Removes least-frequently-used entries when full
481
+ * - Higher values = better cache hit rates but more memory
482
+ */
483
+ const DEFAULT_CACHE_MAX_SIZE = 1e4;
484
+ /**
485
+ * CACHE_PRUNE_PERCENT = 0.1
486
+ *
487
+ * Purpose: Fraction of cache entries to remove during pruning (10%).
488
+ *
489
+ * Used By: Cache.prune() method only
490
+ *
491
+ * Related Values:
492
+ * - DEFAULT_CACHE_MAX_SIZE (10000): Applied to this value to calculate removeCount = 1000
493
+ *
494
+ * Implications:
495
+ * - Balances between removing too few entries (frequent pruning) vs too many (evicting useful data)
496
+ * - 10% is a common pattern for cache eviction
497
+ */
498
+ const CACHE_PRUNE_PERCENT = .1;
499
+ /**
500
+ * MAX_PROCEDURE_PARAMETERS = 1000
501
+ *
502
+ * Purpose: Maximum number of procedure arguments that can be introspected from Oracle's
503
+ * all_arguments view using BULK COLLECT with dbms_utility.name_resolve.
504
+ *
505
+ * Used By:
506
+ * - src/handler/plsql/procedureNamed.js
507
+ * bind.names = {maxArraySize: MAX_PARAMETER_NUMBER}
508
+ * bind.types = {maxArraySize: MAX_PARAMETER_NUMBER}
509
+ *
510
+ * Related Values:
511
+ * - Procedure introspection SQL: SQL_GET_ARGUMENT block at procedureNamed.js:27-43
512
+ * - oracledb.BIND_OUT direction for array fetches
513
+ *
514
+ * Implications:
515
+ * - This is an Oracle driver limitation for array binding, not an arbitrary choice
516
+ * - Procedures with >1000 arguments will have introspection truncated
517
+ * - No error handling exists for this edge case
518
+ * - Practical limit: most procedures have <50 arguments
519
+ */
520
+ const MAX_PROCEDURE_PARAMETERS = 1e3;
521
+ /**
522
+ * OWA_STREAM_CHUNK_SIZE = 1000
523
+ *
524
+ * Purpose: Number of lines fetched per Oracle OWA call when streaming page content.
525
+ *
526
+ * Used By:
527
+ * - owaPageStream.js: maxArraySize for :lines bind variable
528
+ * - owaPageStream.js: :irows INOUT parameter value
529
+ * - owaPageStream.js: Determines when streaming is complete (lines.length < chunkSize)
530
+ *
531
+ * Related Values:
532
+ * - OWA_GET_PAGE_SQL: 'BEGIN owa.get_page(thepage=>:lines, irows=>:irows); END;'
533
+ * - OWAPageStream class constructor at line 20
534
+ *
535
+ * Implications:
536
+ * - Controls round-trip frequency to Oracle database
537
+ * - Higher = fewer round-trips but larger memory buffers per fetch
538
+ * - Lower = more responsive streaming but more database calls
539
+ * - Each line is a PL/SQL varchar2; total data per chunk depends on htp.htbuf_len (63 chars)
540
+ * - Estimated max data per chunk: 1000 lines × 63 chars = 63KB
541
+ */
542
+ const OWA_STREAM_CHUNK_SIZE = 1e3;
543
+ /**
544
+ * OWA_STREAM_BUFFER_SIZE = 16384
545
+ *
546
+ * Purpose: Node.js Readable stream highWaterMark in bytes (16KB).
547
+ *
548
+ * Used By: OWAPageStream class extends Readable stream
549
+ *
550
+ * Related Values:
551
+ * - OWA_STREAM_CHUNK_SIZE (1000): Lines per fetch
552
+ * - Default Node.js highWaterMark is 64KB (Readable stream default)
553
+ * - OWAPageStream.push() converts lines to string buffer
554
+ *
555
+ * Implications:
556
+ * - Smaller than default (64KB) = more frequent _read() callbacks
557
+ * - Reduces memory footprint for large responses
558
+ * - Improves backpressure handling responsiveness
559
+ * - Trade-off: More CPU for _read() calls vs memory efficiency
560
+ */
561
+ const OWA_STREAM_BUFFER_SIZE = 16384;
562
+ /**
563
+ * OWA_RESOLVED_NAME_MAX_LEN = 400
564
+ *
565
+ * Purpose: Maximum string length for resolved Oracle procedure canonical names.
566
+ * Canonical format: SCHEMA.PACKAGE.PROCEDURE or SCHEMA.PROCEDURE.
567
+ *
568
+ * Used By:
569
+ * - resolveProcedureName() function for dbms_utility.name_resolve output
570
+ * - Procedure name resolution SQL at procedureSanitize.js:46-76
571
+ *
572
+ * Related Values:
573
+ * - dbms_utility.name_resolve context = 1 (procedure/function resolution)
574
+ * - Oracle identifier limits: Schema (128) + Package (128) + Procedure (128) + 2 dots = ~386
575
+ * - 400 provides comfortable headroom
576
+ *
577
+ * Implications:
578
+ * - Oracle object names: 30 bytes for most, extended to 128 in some contexts
579
+ * - Canonical name: schema.package.procedure (max ~128+1+128+1+128 = 386)
580
+ * - 400 is safe upper bound with margin
581
+ */
582
+ const OWA_RESOLVED_NAME_MAX_LEN = 400;
583
+ /**
584
+ * STATS_INTERVAL_MS = 5000
585
+ *
586
+ * Purpose: Duration of each statistical bucket in milliseconds.
587
+ *
588
+ * Used By:
589
+ * - src/util/statsManager.js:165: setInterval(this.rotateBucket, this.config.intervalMs)
590
+ * - src/handler/handlerAdmin.js:123: Exposed as intervalMs in /api/status response
591
+ * - src/frontend/main.ts
592
+ * - src/frontend/charts.ts
593
+ *
594
+ * Related Values:
595
+ * - MAX_HISTORY_BUCKETS (1000): At 5s per bucket = ~83 minutes of history
596
+ * - MAX_PERCENTILE_SAMPLES (1000): Samples per bucket for P95/P99
597
+ *
598
+ * Implications:
599
+ * - Bucket aggregation: request counts, durations, errors, system metrics
600
+ * - Affects granularity of performance monitoring
601
+ * - Lower values = more granular but more history entries
602
+ * - Higher values = smoother averages but less detail
603
+ */
604
+ const STATS_INTERVAL_MS = 5e3;
605
+ /**
606
+ * MAX_HISTORY_BUCKETS = 1000
607
+ *
608
+ * Purpose: Maximum number of statistical buckets retained in StatsManager ring buffer.
609
+ *
610
+ * Used By:
611
+ * - src/util/statsManager.js: Ring buffer limit check
612
+ * if (this.history.length > this.config.maxHistoryPoints) { this.history.shift(); }
613
+ * - Exposed via /api/stats/history endpoint
614
+ *
615
+ * Related Values:
616
+ * - STATS_INTERVAL_MS (5000): 5s per bucket = ~83 minutes total history
617
+ * - Each bucket contains: timestamp, requestCount, errors, durations, system metrics
618
+ * - Bucket memory estimate: ~100 bytes × 1000 = ~100KB
619
+ *
620
+ * Implications:
621
+ * - Ring buffer: oldest bucket is removed when new one exceeds limit
622
+ * - Affects admin console chart history display
623
+ * - Higher = more historical context but more memory
624
+ */
625
+ const MAX_HISTORY_BUCKETS = 1e3;
626
+ /**
627
+ * MAX_PERCENTILE_SAMPLES = 1000
628
+ *
629
+ * Purpose: Maximum number of request duration samples collected per bucket
630
+ * for calculating P95/P99 percentiles.
631
+ *
632
+ * Used By:
633
+ * - src/util/statsManager.js: Array length check
634
+ * if (b.durations.length < this.config.percentilePrecision)
635
+ * - src/util/statsManager.js: P95/P99 calculation
636
+ *
637
+ * Related Values:
638
+ * - Percentile calculation: floor(length × 0.95) and floor(length × 0.99)
639
+ * - FIFO replacement: When exceeded, new samples replace oldest
640
+ *
641
+ * Implications:
642
+ * - With 1000 samples, P95/P99 are statistically meaningful
643
+ * - Higher = more accurate percentiles but more memory per bucket
644
+ * - With STATS_INTERVAL_MS = 5000, 1000 samples ≈ 5 req/sec sustained
645
+ * - Under heavy load, older samples are discarded (FIFO)
646
+ */
647
+ const MAX_PERCENTILE_SAMPLES = 1e3;
648
+ /**
649
+ * SHUTDOWN_GRACE_DELAY_MS = 100
650
+ *
651
+ * Purpose: Delay between initiating server shutdown and forced termination.
652
+ *
653
+ * Used By: POST /api/server/stop handler only
654
+ *
655
+ * Related Values:
656
+ * - forceShutdown() at src/util/shutdown.js
657
+ * - SIGTERM/SIGINT handlers at shutdown.js
658
+ *
659
+ * Implications:
660
+ * - Allows graceful completion of in-flight requests
661
+ * - Gives Express middleware time to send final responses
662
+ * - 100ms is short; may be insufficient under heavy load
663
+ * - Consider making configurable for high-traffic deployments
664
+ */
665
+ const SHUTDOWN_GRACE_DELAY_MS = 100;
666
+ /**
667
+ * TRACE_LOG_ROTATION_SIZE = '10M'
668
+ *
669
+ * Purpose: Log file size threshold triggering trace log rotation (10 Megabytes).
670
+ *
671
+ * Used By: rotating-file-stream library for 'trace.log'
672
+ *
673
+ * Related Values:
674
+ * - TRACE_LOG_ROTATION_INTERVAL ('1d'): Also triggers rotation
675
+ * - TRACE_LOG_MAX_ROTATED_FILES (10): Maximum retained files
676
+ *
677
+ * Implications:
678
+ * - When either size OR time threshold is reached, rotation occurs
679
+ * - Combined with daily rotation: ~10MB/day minimum
680
+ * - gzip compression reduces rotated file size by ~70-90%
681
+ */
682
+ const TRACE_LOG_ROTATION_SIZE = "10M";
683
+ /**
684
+ * TRACE_LOG_ROTATION_INTERVAL = '1d'
685
+ *
686
+ * Purpose: Time-based trace log rotation trigger (daily).
687
+ *
688
+ * Used By: rotating-file-stream library for 'trace.log'
689
+ *
690
+ * Implications:
691
+ * - Guarantees at least one rotation per day
692
+ * - Midnight-based or 24h from first write
693
+ */
694
+ const TRACE_LOG_ROTATION_INTERVAL = "1d";
695
+ /**
696
+ * TRACE_LOG_MAX_ROTATED_FILES = 10
697
+ *
698
+ * Purpose: Maximum number of rotated trace log files to retain.
699
+ *
700
+ * Used By: rotating-file-stream library for 'trace.log'
701
+ *
702
+ * Implications:
703
+ * - When exceeded, oldest rotated file is deleted
704
+ * - Maximum: ~10 files × 10MB = ~100MB (compressed: ~10-30MB)
705
+ */
706
+ const TRACE_LOG_MAX_ROTATED_FILES = 10;
707
+ /**
708
+ * JSON_LOG_ROTATION_SIZE = '10M'
709
+ *
710
+ * Purpose: Log file size threshold triggering JSON error log rotation (10 Megabytes).
711
+ *
712
+ * Used By: rotating-file-stream library for 'error.json.log'
713
+ *
714
+ * Related Values:
715
+ * - JSON_LOG_ROTATION_INTERVAL ('1d'): Also triggers rotation
716
+ * - JSON_LOG_MAX_ROTATED_FILES (10): Maximum retained files
717
+ *
718
+ * Implications:
719
+ * - When either size OR time threshold is reached, rotation occurs
720
+ * - Combined with daily rotation: ~10MB/day minimum
721
+ * - gzip compression reduces rotated file size by ~70-90%
722
+ */
723
+ const JSON_LOG_ROTATION_SIZE = "10M";
724
+ /**
725
+ * JSON_LOG_ROTATION_INTERVAL = '1d'
726
+ *
727
+ * Purpose: Time-based JSON error log rotation trigger (daily).
728
+ *
729
+ * Used By: rotating-file-stream library for 'error.json.log'
730
+ *
731
+ * Implications:
732
+ * - Guarantees at least one rotation per day
733
+ * - Midnight-based or 24h from first write
734
+ */
735
+ const JSON_LOG_ROTATION_INTERVAL = "1d";
736
+ /**
737
+ * JSON_LOG_MAX_ROTATED_FILES = 10
738
+ *
739
+ * Purpose: Maximum number of rotated JSON error log files to retain.
740
+ *
741
+ * Used By: rotating-file-stream library for 'error.json.log'
742
+ *
743
+ * Implications:
744
+ * - When exceeded, oldest rotated file is deleted
745
+ * - Maximum: ~10 files × 10MB = ~100MB (compressed: ~10-30MB)
746
+ */
747
+ const JSON_LOG_MAX_ROTATED_FILES = 10;
748
+
749
+ //#endregion
750
+ //#region src/backend/util/statsManager.ts
751
+ const debug$12 = debugModule("webplsql:statsManager");
752
+ /**
753
+ * Manager for statistical data collection and temporal bucketing.
754
+ */
755
+ var StatsManager = class {
756
+ config;
757
+ startTime;
758
+ history;
759
+ lifetime;
760
+ _currentBucket;
761
+ _lastCpuTimes;
762
+ _timer;
763
+ /**
764
+ * @param config - Configuration.
765
+ */
766
+ constructor(config = {}) {
767
+ this.config = {
768
+ intervalMs: STATS_INTERVAL_MS,
769
+ maxHistoryPoints: MAX_HISTORY_BUCKETS,
770
+ sampleSystem: true,
771
+ samplePools: true,
772
+ percentilePrecision: MAX_PERCENTILE_SAMPLES,
773
+ ...config
774
+ };
775
+ this.startTime = /* @__PURE__ */ new Date();
776
+ this.history = [];
777
+ this.lifetime = {
778
+ totalRequests: 0,
779
+ totalErrors: 0,
780
+ minDuration: -1,
781
+ maxDuration: -1,
782
+ totalDuration: 0,
783
+ maxRequestsPerSecond: 0,
784
+ memory: {
785
+ heapUsedMax: 0,
786
+ heapTotalMax: 0,
787
+ rssMax: 0,
788
+ externalMax: 0
789
+ },
790
+ cpu: {
791
+ max: 0,
792
+ userMax: 0,
793
+ systemMax: 0
794
+ }
795
+ };
796
+ this._currentBucket = {
797
+ count: 0,
798
+ errors: 0,
799
+ durations: [],
800
+ durationSum: 0,
801
+ durationMin: -1,
802
+ durationMax: -1
803
+ };
804
+ this._lastCpuTimes = this._getSystemCpuTimes();
805
+ this._timer = void 0;
806
+ if (this.config.sampleSystem) {
807
+ this._timer = setInterval(() => {
808
+ this.rotateBucket();
809
+ }, this.config.intervalMs);
810
+ if (this._timer && typeof this._timer.unref === "function") this._timer.unref();
811
+ }
812
+ }
813
+ /**
814
+ * Reset the current bucket accumulator.
815
+ */
816
+ _resetBucket() {
817
+ this._currentBucket = {
818
+ count: 0,
819
+ errors: 0,
820
+ durations: [],
821
+ durationSum: 0,
822
+ durationMin: -1,
823
+ durationMax: -1
824
+ };
825
+ }
826
+ /**
827
+ * Record a request event.
828
+ * @param duration - Duration in milliseconds.
829
+ * @param isError - Whether the request was an error.
830
+ */
831
+ recordRequest(duration, isError = false) {
832
+ this.lifetime.totalRequests++;
833
+ if (isError) this.lifetime.totalErrors++;
834
+ this.lifetime.totalDuration += duration;
835
+ if (this.lifetime.minDuration < 0 || duration < this.lifetime.minDuration) this.lifetime.minDuration = duration;
836
+ if (this.lifetime.maxDuration < 0 || duration > this.lifetime.maxDuration) this.lifetime.maxDuration = duration;
837
+ const b = this._currentBucket;
838
+ b.count++;
839
+ if (isError) b.errors++;
840
+ b.durationSum += duration;
841
+ if (b.durationMin < 0 || duration < b.durationMin) b.durationMin = duration;
842
+ if (b.durationMax < 0 || duration > b.durationMax) b.durationMax = duration;
843
+ if (b.durations.length < this.config.percentilePrecision) b.durations.push(duration);
844
+ }
845
+ /**
846
+ * Get system CPU times.
847
+ * @returns System CPU times.
848
+ */
849
+ _getSystemCpuTimes() {
850
+ const cpus = os.cpus();
851
+ let user = 0;
852
+ let nice = 0;
853
+ let sys = 0;
854
+ let idle = 0;
855
+ let irq = 0;
856
+ for (const cpu of cpus) {
857
+ user += cpu.times.user;
858
+ nice += cpu.times.nice;
859
+ sys += cpu.times.sys;
860
+ idle += cpu.times.idle;
861
+ irq += cpu.times.irq;
862
+ }
863
+ const total = user + nice + sys + idle + irq;
864
+ return {
865
+ user,
866
+ nice,
867
+ sys,
868
+ idle,
869
+ irq,
870
+ total
871
+ };
872
+ }
873
+ /**
874
+ * Calculate CPU usage percentage since last call.
875
+ * @returns CPU usage percentage (0-100).
876
+ */
877
+ _calculateCpuUsage() {
878
+ const current = this._getSystemCpuTimes();
879
+ const last = this._lastCpuTimes || {
880
+ user: 0,
881
+ nice: 0,
882
+ sys: 0,
883
+ idle: 0,
884
+ irq: 0,
885
+ total: 0
886
+ };
887
+ const deltaTotal = current.total - last.total;
888
+ const deltaIdle = current.idle - last.idle;
889
+ this._lastCpuTimes = current;
890
+ if (deltaTotal <= 0) return 0;
891
+ const percent = (deltaTotal - deltaIdle) / deltaTotal * 100;
892
+ return Math.min(100, Math.max(0, percent));
893
+ }
894
+ /**
895
+ * Rotate the current bucket into history and start a new one.
896
+ * @param poolSnapshots - Optional pool snapshots to include.
897
+ */
898
+ rotateBucket(poolSnapshots = []) {
899
+ const b = this._currentBucket;
900
+ const memUsage = process.memoryUsage();
901
+ const systemMemoryUsed = os.totalmem() - os.freemem();
902
+ const cpuUsage = process.cpuUsage();
903
+ const cpu = this._calculateCpuUsage();
904
+ const reqPerSec = b.count / (this.config.intervalMs / 1e3);
905
+ this.lifetime.maxRequestsPerSecond = Math.max(this.lifetime.maxRequestsPerSecond, reqPerSec);
906
+ this.lifetime.memory.heapUsedMax = Math.max(this.lifetime.memory.heapUsedMax, memUsage.heapUsed);
907
+ this.lifetime.memory.heapTotalMax = Math.max(this.lifetime.memory.heapTotalMax, memUsage.heapTotal);
908
+ this.lifetime.memory.rssMax = Math.max(this.lifetime.memory.rssMax, systemMemoryUsed);
909
+ this.lifetime.memory.externalMax = Math.max(this.lifetime.memory.externalMax, memUsage.external);
910
+ this.lifetime.cpu.max = Math.max(this.lifetime.cpu.max, cpu);
911
+ this.lifetime.cpu.userMax = Math.max(this.lifetime.cpu.userMax, cpuUsage.user);
912
+ this.lifetime.cpu.systemMax = Math.max(this.lifetime.cpu.systemMax, cpuUsage.system);
913
+ let p95 = 0;
914
+ let p99 = 0;
915
+ if (b.durations.length > 0) {
916
+ const sorted = [...b.durations].sort((x, y) => x - y);
917
+ const p95Idx = Math.floor(sorted.length * .95);
918
+ const p99Idx = Math.floor(sorted.length * .99);
919
+ const lastIdx = sorted.length - 1;
920
+ p95 = sorted[p95Idx] ?? sorted[lastIdx] ?? 0;
921
+ p99 = sorted[p99Idx] ?? sorted[lastIdx] ?? 0;
922
+ }
923
+ const bucket = {
924
+ timestamp: Date.now(),
925
+ requests: b.count,
926
+ errors: b.errors,
927
+ durationMin: b.durationMin < 0 ? 0 : b.durationMin,
928
+ durationMax: b.durationMax < 0 ? 0 : b.durationMax,
929
+ durationAvg: b.count > 0 ? b.durationSum / b.count : 0,
930
+ durationP95: p95,
931
+ durationP99: p99,
932
+ system: {
933
+ cpu,
934
+ heapUsed: memUsage.heapUsed,
935
+ heapTotal: memUsage.heapTotal,
936
+ rss: systemMemoryUsed,
937
+ external: memUsage.external
938
+ },
939
+ pools: poolSnapshots
940
+ };
941
+ this.history.push(bucket);
942
+ if (this.history.length > this.config.maxHistoryPoints) this.history.shift();
943
+ this._resetBucket();
944
+ debug$12("Bucket rotated: %j", bucket);
945
+ }
946
+ /**
947
+ * Stop the background timer.
948
+ */
949
+ stop() {
950
+ if (this._timer) {
951
+ clearInterval(this._timer);
952
+ this._timer = void 0;
953
+ }
954
+ }
955
+ /**
956
+ * Get lifetime summary.
957
+ * @returns Summary.
958
+ */
959
+ getSummary() {
960
+ return {
961
+ startTime: this.startTime,
962
+ totalRequests: this.lifetime.totalRequests,
963
+ totalErrors: this.lifetime.totalErrors,
964
+ avgResponseTime: this.lifetime.totalRequests > 0 ? this.lifetime.totalDuration / this.lifetime.totalRequests : 0,
965
+ minResponseTime: this.lifetime.minDuration,
966
+ maxResponseTime: this.lifetime.maxDuration,
967
+ maxRequestsPerSecond: this.lifetime.maxRequestsPerSecond,
968
+ maxMemory: this.lifetime.memory,
969
+ cpu: this.lifetime.cpu
970
+ };
971
+ }
972
+ /**
973
+ * Get history buffer.
974
+ * @returns The history buffer.
975
+ */
976
+ getHistory() {
977
+ return this.history;
978
+ }
979
+ };
980
+
981
+ //#endregion
982
+ //#region src/backend/server/adminContext.ts
983
+ /**
984
+ * Admin Context Class
985
+ */
986
+ var AdminContext = class {
987
+ startTime;
988
+ config;
989
+ pools;
990
+ caches;
991
+ statsManager;
992
+ _paused;
993
+ constructor(config) {
994
+ this.startTime = /* @__PURE__ */ new Date();
995
+ this.config = config;
996
+ this.pools = [];
997
+ this.caches = [];
998
+ this.statsManager = new StatsManager();
999
+ this._paused = false;
1000
+ }
1001
+ /**
1002
+ * Register a PL/SQL handler with the admin context.
1003
+ * @param route - The route for the handler.
1004
+ * @param pool - The connection pool.
1005
+ * @param procedureNameCache - The procedure name cache.
1006
+ * @param argumentCache - The argument cache.
1007
+ */
1008
+ registerHandler(route, pool, procedureNameCache, argumentCache) {
1009
+ this.pools.push(pool);
1010
+ this.caches.push({
1011
+ poolName: route,
1012
+ procedureNameCache,
1013
+ argumentCache
1014
+ });
1015
+ }
1016
+ get paused() {
1017
+ return this._paused;
1018
+ }
1019
+ setPaused(value) {
1020
+ this._paused = value;
1021
+ }
1022
+ };
1023
+
1024
+ //#endregion
1025
+ //#region src/backend/util/file.ts
1026
+ /**
1027
+ * Read file.
1028
+ *
1029
+ * @param filePath - File name.
1030
+ * @returns The string.
1031
+ */
1032
+ const readFileSyncUtf8 = (filePath) => {
1033
+ try {
1034
+ return readFileSync(filePath, "utf8");
1035
+ } catch {
1036
+ throw new Error(`Unable to read file "${filePath}"`);
1037
+ }
1038
+ };
1039
+ /**
1040
+ * Read file.
1041
+ *
1042
+ * @param filePath - File name.
1043
+ * @returns The buffer.
1044
+ */
1045
+ const readFile = async (filePath) => {
1046
+ try {
1047
+ return await promises.readFile(filePath);
1048
+ } catch {
1049
+ throw new Error(`Unable to read file "${filePath}"`);
1050
+ }
1051
+ };
1052
+ /**
1053
+ * Remove file.
1054
+ *
1055
+ * @param filePath - File name.
1056
+ */
1057
+ const removeFile = async (filePath) => {
1058
+ try {
1059
+ await promises.unlink(filePath);
1060
+ } catch {
1061
+ throw new Error(`Unable to remove file "${filePath}"`);
1062
+ }
1063
+ };
1064
+ /**
1065
+ * Load a json file.
1066
+ *
1067
+ * @param filePath - File name.
1068
+ * @returns The json object.
1069
+ */
1070
+ const getJsonFile = (filePath) => {
1071
+ try {
1072
+ const fileContent = readFileSync(filePath, "utf8");
1073
+ return JSON.parse(fileContent);
1074
+ } catch {
1075
+ throw new Error(`Unable to load file "${filePath}"`);
1076
+ }
1077
+ };
1078
+ /**
1079
+ * Is this a directory.
1080
+ * @param directoryPath - Directory name.
1081
+ * @returns Return true if it is a directory.
1082
+ */
1083
+ const isDirectory = async (directoryPath) => {
1084
+ if (typeof directoryPath !== "string") return false;
1085
+ return (await promises.stat(directoryPath)).isDirectory();
1086
+ };
1087
+ /**
1088
+ * Is this a file.
1089
+ * @param filePath - File name.
1090
+ * @returns Return true if it is a file.
1091
+ */
1092
+ const isFile = async (filePath) => {
1093
+ if (typeof filePath !== "string") return false;
1094
+ try {
1095
+ return (await promises.stat(filePath)).isFile();
1096
+ } catch {
1097
+ return false;
1098
+ }
1099
+ };
1100
+
1101
+ //#endregion
1102
+ //#region src/backend/util/html.ts
1103
+ /**
1104
+ * Escape html string.
1105
+ *
1106
+ * @param value - The value.
1107
+ * @returns The escaped value.
1108
+ */
1109
+ const escapeHtml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1110
+ /**
1111
+ * Convert LF and/or CR to <br>
1112
+ * @param text - The text to convert.
1113
+ * @returns The converted text.
1114
+ */
1115
+ const convertAsciiToHtml = (text) => {
1116
+ let html = escapeHtml(text);
1117
+ html = html.replace(/(?:\r\n|\r|\n)/g, "<br />");
1118
+ html = html.replace(/\t/g, "&nbsp;&nbsp;&nbsp;");
1119
+ return html;
1120
+ };
1121
+ /**
1122
+ * get a minimal html page.
1123
+ * @param body - The body.
1124
+ * @returns The html page.
1125
+ */
1126
+ const getHtmlPage = (body) => `<!DOCTYPE html>
1127
+ <html lang="en">
1128
+ <head>
1129
+ <meta charset="utf-8">
1130
+ <title>web_plsql error page</title>
1131
+ <style type="text/css">
1132
+ html {
1133
+ font-family: monospace, sans-serif;
1134
+ font-size: 12px;
1135
+ }
1136
+ h1 {
1137
+ font-size: 16px;
1138
+ padding: 2px;
1139
+ background-color: #cc0000;
1140
+ }
1141
+ </style>
1142
+ </head>
1143
+ <body>
1144
+ ${body}
1145
+ </body>
1146
+ </html>
1147
+ `;
1148
+
1149
+ //#endregion
1150
+ //#region src/backend/util/trace.ts
1151
+ /**
1152
+ * Type guard for BindParameter
1153
+ * @param row - The row to check
1154
+ * @returns True if row is a BindParameter
1155
+ */
1156
+ const isBindParameter = (row) => {
1157
+ if (typeof row !== "object" || row === null) return false;
1158
+ return "dir" in row || "type" in row || "val" in row || "maxSize" in row || "maxArraySize" in row;
1159
+ };
1160
+ const SEPARATOR_H1 = "=".repeat(100);
1161
+ const SEPARATOR_H2 = "-".repeat(30);
1162
+ /**
1163
+ * Return a string representation of the value.
1164
+ *
1165
+ * @param value - Any value.
1166
+ * @param depth - Specifies the number of times to recurse while formatting object.
1167
+ * @returns The string representation.
1168
+ */
1169
+ const inspect = (value, depth = null) => {
1170
+ try {
1171
+ return util.inspect(value, {
1172
+ showHidden: false,
1173
+ depth,
1174
+ colors: false
1175
+ });
1176
+ } catch {}
1177
+ try {
1178
+ return JSON.stringify(value);
1179
+ } catch {}
1180
+ return "Unable to convert value to string";
1181
+ };
1182
+ /**
1183
+ * Return a tabular representation of the values.
1184
+ *
1185
+ * @param head - The header values.
1186
+ * @param body - The row values.
1187
+ * @returns The output.
1188
+ */
1189
+ const toTable = (head, body) => {
1190
+ if (head.length === 0) throw new Error("head cannot be empty");
1191
+ const widths = head.map((h, i) => {
1192
+ const bodyMax = Math.max(0, ...body.map((row) => (row[i] ?? "").length));
1193
+ return Math.max(h.length, bodyMax);
1194
+ });
1195
+ /**
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
+ * @param i - The index
1203
+ * @returns The width
1204
+ */
1205
+ const getWidth = (i) => widths[i] ?? 0;
1206
+ return {
1207
+ text: [
1208
+ head.map((h, i) => padCell(h, getWidth(i))).join(" | "),
1209
+ widths.map((w) => "-".repeat(w ?? 0)).join("-+-"),
1210
+ ...body.map((row) => head.map((_, i) => padCell(row[i] ?? "", getWidth(i))).join(" | "))
1211
+ ].join("\n"),
1212
+ html: `<table><thead><tr>${head.map((h) => `<th>${escapeHtml(h)}</th>`).join("")}</tr></thead><tbody>${body.map((row) => `<tr>${head.map((_, i) => `<td>${escapeHtml(row[i] ?? "")}</td>`).join("")}</tr>`).join("")}</tbody></table>`
1213
+ };
1214
+ };
1215
+ /**
1216
+ * Log text to the console and to a file.
1217
+ *
1218
+ * @param text - Text to log.
1219
+ */
1220
+ const logToFile = (text) => {
1221
+ const fs = rotatingFileStream.createStream("trace.log", {
1222
+ size: TRACE_LOG_ROTATION_SIZE,
1223
+ interval: TRACE_LOG_ROTATION_INTERVAL,
1224
+ maxFiles: TRACE_LOG_MAX_ROTATED_FILES,
1225
+ compress: "gzip"
1226
+ });
1227
+ fs.write(text);
1228
+ fs.end();
1229
+ };
1230
+ /**
1231
+ * Return a string representation of the request.
1232
+ *
1233
+ * @param req - express.Request.
1234
+ * @returns The string representation.
1235
+ */
1236
+ const inspectRequest = (req) => {
1237
+ const requestData = {};
1238
+ Object.keys(req).filter((prop) => [
1239
+ "originalUrl",
1240
+ "params",
1241
+ "query",
1242
+ "url",
1243
+ "method",
1244
+ "body",
1245
+ "files",
1246
+ "secret",
1247
+ "cookies"
1248
+ ].includes(prop)).forEach((prop) => {
1249
+ requestData[prop] = req[prop];
1250
+ });
1251
+ return inspect(requestData);
1252
+ };
1253
+ /**
1254
+ * Return a string representation of the bind parameter.
1255
+ * @param dir - The direction.
1256
+ * @returns The string.
1257
+ */
1258
+ const dirToString = (dir) => {
1259
+ switch (dir) {
1260
+ case oracledb.BIND_IN: return "IN";
1261
+ case oracledb.BIND_OUT: return "OUT";
1262
+ case oracledb.BIND_INOUT: return "INOUT";
1263
+ default: return "";
1264
+ }
1265
+ };
1266
+ /**
1267
+ * Return a string representation of the bind type.
1268
+ * @param type - The type.
1269
+ * @returns The string.
1270
+ */
1271
+ const bindTypeToString = (type) => {
1272
+ if (typeof type === "object" && type !== null && "name" in type) return type.name;
1273
+ if (typeof type === "string") return type;
1274
+ if (typeof type === "number") return type.toString();
1275
+ return "";
1276
+ };
1277
+ /**
1278
+ * Return a string representation of the bind parameter.
1279
+ * @param output - The output.
1280
+ * @param bind - The bind parameters.
1281
+ */
1282
+ const inspectBindParameter = (output, bind) => {
1283
+ const rows = Object.entries(bind);
1284
+ if (rows.length === 0) return;
1285
+ const { html, text } = toTable([
1286
+ "id",
1287
+ "dir",
1288
+ "maxArraySize",
1289
+ "maxSize",
1290
+ "bind type",
1291
+ "value",
1292
+ "value type"
1293
+ ], rows.map(([id, row]) => {
1294
+ let dir = "";
1295
+ let maxArraySize = "";
1296
+ let maxSize = "";
1297
+ let bindType = "";
1298
+ let value = "";
1299
+ let valueType = "";
1300
+ if (isBindParameter(row)) {
1301
+ dir = dirToString(row.dir);
1302
+ maxArraySize = row.maxArraySize ? row.maxArraySize.toString() : "";
1303
+ maxSize = row.maxSize ? row.maxSize.toString() : "";
1304
+ bindType = bindTypeToString(row.type);
1305
+ value = inspect(row.val);
1306
+ valueType = typeof row.val;
1307
+ } else {
1308
+ value = inspect(row);
1309
+ valueType = typeof row;
1310
+ }
1311
+ return [
1312
+ id,
1313
+ dir,
1314
+ maxArraySize,
1315
+ maxSize,
1316
+ bindType,
1317
+ value,
1318
+ valueType
1319
+ ];
1320
+ }));
1321
+ output.html += html;
1322
+ output.text += text;
1323
+ };
1324
+ /**
1325
+ * Add environment
1326
+ * @param output - The output.
1327
+ * @param environment - The environment.
1328
+ */
1329
+ const inspectEnvironment = (output, environment) => {
1330
+ const rows = Object.entries(environment);
1331
+ if (rows.length === 0) return;
1332
+ const { html, text } = toTable(["key", "value"], rows);
1333
+ output.html += html;
1334
+ output.text += text;
1335
+ };
1336
+ /**
1337
+ * Get a block.
1338
+ * @param title - The name.
1339
+ * @param body - The name.
1340
+ * @returns The text.
1341
+ */
1342
+ const getBlock = (title, body) => `\n${SEPARATOR_H2}${title.toUpperCase()}${SEPARATOR_H2}\n${body}`;
1343
+ /**
1344
+ * Get line html
1345
+ * @param text - The text.
1346
+ * @returns The line.
1347
+ */
1348
+ const getLineHtml = (text) => `<p>${convertAsciiToHtml(text)}</p>`;
1349
+ /**
1350
+ * Get line text
1351
+ * @param text - The text.
1352
+ * @returns The line.
1353
+ */
1354
+ const getLineText = (text) => `${text}\n`;
1355
+ /**
1356
+ * Add line
1357
+ * @param output - The output.
1358
+ * @param text - The text to convert.
1359
+ */
1360
+ const addLine = (output, text) => {
1361
+ output.html += getLineHtml(text);
1362
+ output.text += getLineText(text);
1363
+ };
1364
+ /**
1365
+ * Add header
1366
+ * @param output - The output.
1367
+ * @param text - The text to convert.
1368
+ */
1369
+ const addHeader = (output, text) => {
1370
+ output.html += `<h2>${text}</h2>`;
1371
+ output.text += `\n${text}\n${"-".repeat(text.length)}\n`;
1372
+ };
1373
+ /**
1374
+ * Add procedure
1375
+ * @param output - The output.
1376
+ * @param sql - The SQL to execute.
1377
+ * @param bind - The bind parameters.
1378
+ */
1379
+ const addProcedure = (output, sql, bind) => {
1380
+ output.html += `${sql}<br><br>`;
1381
+ output.text += `${sql}\n\n`;
1382
+ try {
1383
+ inspectBindParameter(output, bind);
1384
+ } catch (err) {
1385
+ addLine(output, `Unable to inspect bind parameter: ${errorToString(err)}`);
1386
+ }
1387
+ output.html += `<br>`;
1388
+ output.text += `\n`;
1389
+ };
1390
+ /**
1391
+ * Get a formatted message.
1392
+ * @param para - The req object represents the HTTP request.
1393
+ * @returns The output.
1394
+ */
1395
+ const getFormattedMessage = (para) => {
1396
+ const timestamp = para.timestamp ?? /* @__PURE__ */ new Date();
1397
+ const url = typeof para.req?.originalUrl === "string" && para.req.originalUrl.length > 0 ? ` on ${para.req.originalUrl}` : "";
1398
+ const header = `${(para.type ?? "trace").toUpperCase()} at ${timestamp.toUTCString()}${url}`;
1399
+ const output = {
1400
+ html: `<h1>${header}</h1>`,
1401
+ text: `\n\n${SEPARATOR_H1}\n== ${header}\n${SEPARATOR_H1}\n`
1402
+ };
1403
+ addHeader(output, "ERROR");
1404
+ addLine(output, para.message);
1405
+ if (para.req) {
1406
+ addHeader(output, "REQUEST");
1407
+ addLine(output, inspectRequest(para.req));
1408
+ }
1409
+ if (para.sql && para.bind) {
1410
+ addHeader(output, "PROCEDURE");
1411
+ addProcedure(output, para.sql, para.bind);
1412
+ }
1413
+ if (para.environment) {
1414
+ addHeader(output, "ENVIRONMENT");
1415
+ inspectEnvironment(output, para.environment);
1416
+ }
1417
+ return output;
1418
+ };
1419
+ /**
1420
+ * Log a warning message.
1421
+ * @param para - The req object represents the HTTP request.
1422
+ */
1423
+ const warningMessage = (para) => {
1424
+ const { text } = getFormattedMessage(para);
1425
+ logToFile(text);
1426
+ console.warn(text);
1427
+ };
1428
+
1429
+ //#endregion
1430
+ //#region src/backend/util/errorToString.ts
1431
+ /**
1432
+ * Convert Error to a string.
1433
+ *
1434
+ * @param error - The error.
1435
+ * @returns The string representation.
1436
+ */
1437
+ const errorToString = (error) => {
1438
+ if (typeof error === "string") return error;
1439
+ else if (error instanceof Error) {
1440
+ const parts = [error.name];
1441
+ if (typeof error.message === "string" && error.message.length > 0) parts.push(error.message);
1442
+ if (typeof error.stack === "string" && error.stack.length > 0) parts.push(error.stack);
1443
+ return parts.join("\n");
1444
+ } else return inspect(error);
1445
+ };
1446
+
1447
+ //#endregion
1448
+ //#region src/backend/handler/plsql/upload.ts
1449
+ const debug$11 = debugModule("webplsql:fileUpload");
1450
+ const z$reqFiles = z$1.array(z$1.strictObject({
1451
+ fieldname: z$1.string(),
1452
+ originalname: z$1.string(),
1453
+ encoding: z$1.string(),
1454
+ mimetype: z$1.string(),
1455
+ destination: z$1.string(),
1456
+ filename: z$1.string(),
1457
+ path: z$1.string(),
1458
+ size: z$1.number()
1459
+ }));
1460
+ /**
1461
+ * Get the files
1462
+ *
1463
+ * @param req - The req object represents the HTTP request.
1464
+ * @returns Promise that resolves with an array of files to be uploaded.
1465
+ */
1466
+ const getFiles = (req) => {
1467
+ if (!("files" in req)) {
1468
+ debug$11("getFiles: no files");
1469
+ return [];
1470
+ }
1471
+ if (typeof req.files === "object" && req.files !== null && Object.keys(req.files).length === 0) {
1472
+ debug$11("getFiles: no files");
1473
+ return [];
1474
+ }
1475
+ const files = z$reqFiles.parse(req.files);
1476
+ for (const file of files) file.filename += `/${file.originalname}`;
1477
+ debug$11("getFiles", files);
1478
+ return files;
1479
+ };
1480
+ /**
1481
+ * Upload the given file and return a promise.
1482
+ *
1483
+ * @param file - The file to upload.
1484
+ * @param doctable - The file to upload.
1485
+ * @param databaseConnection - The file to upload.
1486
+ * @returns Promise that resolves when uploaded.
1487
+ */
1488
+ const uploadFile = async (file, doctable, databaseConnection) => {
1489
+ debug$11(`uploadFile`, file, doctable);
1490
+ /* v8 ignore next - defensive validation */
1491
+ if (typeof doctable !== "string" || doctable.length === 0) throw new Error(`Unable to upload file "${file.filename}" because the option ""doctable" has not been defined`);
1492
+ let blobContent;
1493
+ try {
1494
+ blobContent = await readFile(file.path);
1495
+ } catch (err) {
1496
+ throw new Error(`Unable to load file "${file.path}".\n${errorToString(err)}`);
1497
+ }
1498
+ const sql = `INSERT INTO ${doctable} (name, mime_type, doc_size, dad_charset, last_updated, content_type, blob_content) VALUES (:name, :mime_type, :doc_size, 'ascii', SYSDATE, 'BLOB', :blob_content)`;
1499
+ const bind = {
1500
+ name: file.filename,
1501
+ mime_type: file.mimetype,
1502
+ doc_size: file.size,
1503
+ blob_content: {
1504
+ val: blobContent,
1505
+ type: BUFFER
1506
+ }
1507
+ };
1508
+ try {
1509
+ await databaseConnection.execute(sql, bind, { autoCommit: true });
1510
+ } catch (err) {
1511
+ throw new Error(`Unable to insert file "${file.filename}".\n${errorToString(err)}`);
1512
+ }
1513
+ try {
1514
+ await removeFile(file.path);
1515
+ } catch (err) {
1516
+ throw new Error(`Unable to remove file "${file.filename}".\n${errorToString(err)}`);
1517
+ }
1518
+ };
1519
+
1520
+ //#endregion
1521
+ //#region src/backend/handler/plsql/procedureVariable.ts
1522
+ const debug$10 = debugModule("webplsql:procedureVariable");
1523
+ /**
1524
+ * Get the sql statement and bindings for the procedure to execute for a variable number of arguments
1525
+ * @param _req - The req object represents the HTTP request. (only used for debugging)
1526
+ * @param procName - The procedure to execute
1527
+ * @param argObj - The arguments to pass to the procedure
1528
+ * @returns The SQL statement and bindings for the procedure to execute
1529
+ */
1530
+ const getProcedureVariable = (_req, procName, argObj) => {
1531
+ /* v8 ignore start */
1532
+ if (debug$10.enabled) debug$10(`getProcedureVariable: ${procName} arguments=`, argObj);
1533
+ /* v8 ignore stop */
1534
+ const names = [];
1535
+ const values = [];
1536
+ for (const key in argObj) {
1537
+ const value = argObj[key];
1538
+ if (typeof value === "string") {
1539
+ names.push(key);
1540
+ values.push(value);
1541
+ } else if (Array.isArray(value)) value.forEach((item) => {
1542
+ names.push(key);
1543
+ values.push(item);
1544
+ });
1545
+ }
1546
+ return {
1547
+ sql: `${procName}(:argnames, :argvalues)`,
1548
+ bind: {
1549
+ argnames: {
1550
+ dir: BIND_IN,
1551
+ type: STRING,
1552
+ val: names
1553
+ },
1554
+ argvalues: {
1555
+ dir: BIND_IN,
1556
+ type: STRING,
1557
+ val: values
1558
+ }
1559
+ }
1560
+ };
1561
+ };
1562
+
1563
+ //#endregion
1564
+ //#region src/backend/handler/plsql/requestError.ts
1565
+ var RequestError = class RequestError extends Error {
1566
+ timestamp;
1567
+ /**
1568
+ * @param message - The error message.
1569
+ */
1570
+ constructor(message) {
1571
+ super(message);
1572
+ if (Error.captureStackTrace) Error.captureStackTrace(this, RequestError);
1573
+ this.timestamp = /* @__PURE__ */ new Date();
1574
+ }
1575
+ };
1576
+
1577
+ //#endregion
1578
+ //#region src/backend/util/util.ts
1579
+ /**
1580
+ * Convert a string to a number
1581
+ *
1582
+ * @param value - The string to convert
1583
+ * @returns The number or null if the string could not be converted
1584
+ */
1585
+ const stringToNumber = (value) => {
1586
+ if (typeof value === "number") return !Number.isNaN(value) && Number.isFinite(value) ? value : null;
1587
+ if (typeof value !== "string" || !/^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:E[+-]?\d+)?$/i.test(value)) return null;
1588
+ return Number(value);
1589
+ };
1590
+
1591
+ //#endregion
1592
+ //#region src/backend/handler/plsql/procedureNamed.ts
1593
+ const debug$9 = debugModule("webplsql:procedureNamed");
1594
+ const SQL_GET_ARGUMENT = [
1595
+ "DECLARE",
1596
+ " schemaName VARCHAR2(32767);",
1597
+ " part1 VARCHAR2(32767);",
1598
+ " part2 VARCHAR2(32767);",
1599
+ " dblink VARCHAR2(32767);",
1600
+ " objectType NUMBER;",
1601
+ " objectID NUMBER;",
1602
+ "BEGIN",
1603
+ " dbms_utility.name_resolve(name=>UPPER(:name), context=>1, schema=>schemaName, part1=>part1, part2=>part2, dblink=>dblink, part1_type=>objectType, object_number=>objectID);",
1604
+ " IF (part1 IS NOT NULL) THEN",
1605
+ " SELECT argument_name, data_type BULK COLLECT INTO :names, :types FROM all_arguments WHERE owner = schemaName AND package_name = part1 AND object_name = part2 AND argument_name IS NOT NULL ORDER BY overload, sequence;",
1606
+ " ELSE",
1607
+ " SELECT argument_name, data_type BULK COLLECT INTO :names, :types FROM all_arguments WHERE owner = schemaName AND package_name IS NULL AND object_name = part2 AND argument_name IS NOT NULL ORDER BY overload, sequence;",
1608
+ " END IF;",
1609
+ "END;"
1610
+ ].join("\n");
1611
+ const DATA_TYPES = Object.freeze({
1612
+ VARCHAR2: "VARCHAR2",
1613
+ CHAR: "CHAR",
1614
+ BINARY_INTEGER: "BINARY_INTEGER",
1615
+ NUMBER: "NUMBER",
1616
+ DATE: "DATE",
1617
+ CLOB: "CLOB",
1618
+ PL_SQL_TABLE: "PL/SQL TABLE"
1619
+ });
1620
+ /**
1621
+ * Retrieve the argument types for a given procedure to be executed.
1622
+ * This is important because if the procedure is defined to take a PL/SQL indexed table,
1623
+ * we must provise a table, even if there is only one argument to be submitted.
1624
+ * @param procedure - The procedure
1625
+ * @param databaseConnection - The database connection
1626
+ * @returns The argument types
1627
+ */
1628
+ const loadArguments = async (procedure, databaseConnection) => {
1629
+ const bind = {
1630
+ name: {
1631
+ dir: BIND_IN,
1632
+ type: STRING,
1633
+ val: procedure
1634
+ },
1635
+ names: {
1636
+ dir: BIND_OUT,
1637
+ type: STRING,
1638
+ maxSize: 60,
1639
+ maxArraySize: MAX_PROCEDURE_PARAMETERS
1640
+ },
1641
+ types: {
1642
+ dir: BIND_OUT,
1643
+ type: STRING,
1644
+ maxSize: 60,
1645
+ maxArraySize: MAX_PROCEDURE_PARAMETERS
1646
+ }
1647
+ };
1648
+ let result = {};
1649
+ try {
1650
+ result = await databaseConnection.execute(SQL_GET_ARGUMENT, bind);
1651
+ } catch (err) {
1652
+ /* v8 ignore start */
1653
+ debug$9("result", result);
1654
+ throw new RequestError(`Error when retrieving arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`);
1655
+ }
1656
+ let data;
1657
+ try {
1658
+ data = z$1.object({
1659
+ names: z$1.array(z$1.string().nullable()),
1660
+ types: z$1.array(z$1.string().nullable())
1661
+ }).parse(result.outBinds);
1662
+ } catch (err) {
1663
+ /* v8 ignore start */
1664
+ debug$9("result.outBinds", result.outBinds);
1665
+ throw new RequestError(`Error when decoding arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`);
1666
+ }
1667
+ if (data.names.length !== data.types.length) throw new RequestError("Error when decoding arguments. The number of names and types does not match");
1668
+ const argTypes = {};
1669
+ for (let i = 0; i < data.names.length; i++) {
1670
+ const name = data.names[i];
1671
+ const type = data.types[i];
1672
+ if (name && type) argTypes[name.toLowerCase()] = type;
1673
+ }
1674
+ return argTypes;
1675
+ };
1676
+ /**
1677
+ * Find the argument types for a given procedure to be executed.
1678
+ * As the arguments are cached, we first look up the cache and only if not yet available we load them.
1679
+ * @param procedure - The procedure
1680
+ * @param databaseConnection - The database connection
1681
+ * @param argumentCache - The argument cache.
1682
+ * @returns The argument types
1683
+ */
1684
+ const findArguments = async (procedure, databaseConnection, argumentCache) => {
1685
+ const key = procedure.toUpperCase();
1686
+ const cachedArgs = argumentCache.get(key);
1687
+ if (cachedArgs) {
1688
+ /* v8 ignore start */
1689
+ if (debug$9.enabled) debug$9(`findArguments: procedure "${procedure}" found in cache`);
1690
+ /* v8 ignore stop */
1691
+ return cachedArgs;
1692
+ }
1693
+ /* v8 ignore start */
1694
+ if (debug$9.enabled) debug$9(`findArguments: procedure "${procedure}" not found in cache and must be loaded`);
1695
+ /* v8 ignore stop */
1696
+ const args = await loadArguments(procedure, databaseConnection);
1697
+ argumentCache.set(key, args);
1698
+ return args;
1699
+ };
1700
+ /**
1701
+ * Get the bindling for an argument.
1702
+ * @param argName - The argument name.
1703
+ * @param argValue - The argument value.
1704
+ * @param argType - The argument type.
1705
+ * @returns The binding.
1706
+ */
1707
+ const getBinding = (argName, argValue, argType) => {
1708
+ if (argType === DATA_TYPES.VARCHAR2 || argType === DATA_TYPES.CHAR) return {
1709
+ dir: BIND_IN,
1710
+ type: DB_TYPE_VARCHAR,
1711
+ val: argValue
1712
+ };
1713
+ if (argType === DATA_TYPES.CLOB) return {
1714
+ dir: BIND_IN,
1715
+ type: DB_TYPE_CLOB,
1716
+ val: argValue
1717
+ };
1718
+ if (argType === DATA_TYPES.NUMBER || argType === DATA_TYPES.BINARY_INTEGER) {
1719
+ const value = stringToNumber(argValue);
1720
+ if (value === null) throw new Error(`Error in named parameter "${argName}": invalid value "${argValue}" for type "${argType}"`);
1721
+ return {
1722
+ dir: BIND_IN,
1723
+ type: DB_TYPE_NUMBER,
1724
+ val: value
1725
+ };
1726
+ }
1727
+ if (argType === DATA_TYPES.DATE) {
1728
+ if (typeof argValue !== "string") throw new Error(`Error in named parameter "${argName}": invalid value "${argValue}" for type "${argType}"`);
1729
+ 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}"`);
1731
+ return {
1732
+ dir: BIND_IN,
1733
+ type: DB_TYPE_VARCHAR,
1734
+ val: value
1735
+ };
1736
+ }
1737
+ if (argType === DATA_TYPES.PL_SQL_TABLE || Array.isArray(argValue)) return {
1738
+ dir: BIND_IN,
1739
+ type: DB_TYPE_DATE,
1740
+ val: typeof argValue === "string" ? [argValue] : argValue
1741
+ };
1742
+ throw new Error(`Error in named parameter "${argName}": invalid binding type "${argType}"`);
1743
+ };
1744
+ /**
1745
+ * Get binding table for tracing.
1746
+ * @param argObj - The arguments to pass to the procedure
1747
+ * @param argTypes - The argument types.
1748
+ * @returns The text.
1749
+ */
1750
+ const inspectBindings = (argObj, argTypes) => {
1751
+ const { text } = toTable([
1752
+ "id",
1753
+ "value",
1754
+ "value type",
1755
+ "argument type"
1756
+ ], Object.entries(argObj).map(([key, value]) => {
1757
+ return [
1758
+ key,
1759
+ value.toString(),
1760
+ typeof value,
1761
+ argTypes[key.toLowerCase()] ?? "unknown"
1762
+ ];
1763
+ }));
1764
+ return text;
1765
+ };
1766
+ /**
1767
+ * Get the sql statement and bindings for the procedure to execute for a fixed number of arguments
1768
+ * @param req - The req object represents the HTTP request. (only used for debugging)
1769
+ * @param procName - The procedure to execute
1770
+ * @param argObj - The arguments to pass to the procedure
1771
+ * @param databaseConnection - The database connection
1772
+ * @param argumentCache - The argument cache.
1773
+ * @returns The SQL statement and bindings for the procedure to execute
1774
+ */
1775
+ const getProcedureNamed = async (req, procName, argObj, databaseConnection, argumentCache) => {
1776
+ debug$9(`getProcedureNamed: ${procName} arguments=`, argObj);
1777
+ const argTypes = await findArguments(procName, databaseConnection, argumentCache);
1778
+ const sqlParameter = [];
1779
+ const bindings = {};
1780
+ for (const key in argObj) {
1781
+ const parameterName = `p_${key}`;
1782
+ const argValue = argObj[key];
1783
+ const argType = argTypes[key.toLowerCase()];
1784
+ let bind = {
1785
+ dir: BIND_IN,
1786
+ type: DB_TYPE_VARCHAR,
1787
+ val: argValue
1788
+ };
1789
+ if (argType) bind = getBinding(key, argValue, argType);
1790
+ else warningMessage({
1791
+ type: "warning",
1792
+ message: `Error in named parameter "${key}": invalid binding type "${argType}"\n\n${inspectBindings(argObj, argTypes)}`,
1793
+ req
1794
+ });
1795
+ sqlParameter.push(`${key}=>:${parameterName}`);
1796
+ bindings[parameterName] = bind;
1797
+ }
1798
+ const sql = `${procName}(${sqlParameter.join(", ")})`;
1799
+ /* v8 ignore start */
1800
+ if (debug$9.enabled) {
1801
+ debug$9(sql);
1802
+ debug$9(inspectBindings(argObj, argTypes));
1803
+ }
1804
+ /* v8 ignore stop */
1805
+ return {
1806
+ sql,
1807
+ bind: bindings
1808
+ };
1809
+ };
1810
+
1811
+ //#endregion
1812
+ //#region src/backend/handler/plsql/parsePage.ts
1813
+ const debug$8 = debugModule("webplsql:parsePage");
1814
+ /**
1815
+ * Parse the header and split it up into the individual components
1816
+ *
1817
+ * @param text - The text returned from the PL/SQL procedure.
1818
+ * @returns The parsed page.
1819
+ */
1820
+ const parsePage = (text) => {
1821
+ const page = {
1822
+ body: "",
1823
+ head: {
1824
+ cookies: [],
1825
+ otherHeaders: {}
1826
+ },
1827
+ file: {
1828
+ fileType: null,
1829
+ fileSize: null,
1830
+ fileBlob: null
1831
+ }
1832
+ };
1833
+ let head = "";
1834
+ const headerEndPosition = text.indexOf("\n\n");
1835
+ if (headerEndPosition === -1) head = text;
1836
+ else {
1837
+ head = text.substring(0, headerEndPosition + 2);
1838
+ page.body = text.substring(headerEndPosition + 2);
1839
+ }
1840
+ debug$8("parsing the headers received from PL/SQL");
1841
+ head.split("\n").forEach((line) => {
1842
+ const header = getHeader(line);
1843
+ if (header) switch (header.name.toLowerCase()) {
1844
+ case "set-cookie":
1845
+ {
1846
+ const cookie = parseCookie(header.value);
1847
+ 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`);
1850
+ }
1851
+ break;
1852
+ case "content-type":
1853
+ page.head.contentType = header.value;
1854
+ debug$8(`oracle header "content-type" with value "${page.head.contentType}" was parsed`);
1855
+ break;
1856
+ case "x-db-content-length":
1857
+ {
1858
+ const contentLength = parseInt(header.value, 10);
1859
+ if (!Number.isNaN(contentLength)) {
1860
+ page.head.contentLength = contentLength;
1861
+ 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`);
1863
+ }
1864
+ break;
1865
+ case "status":
1866
+ {
1867
+ const statusCode = parseInt(header.value, 10);
1868
+ if (!Number.isNaN(statusCode)) {
1869
+ page.head.statusCode = statusCode;
1870
+ debug$8(`oracle header "status" with value "${page.head.statusCode}" was parsed`);
1871
+ 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`);
1874
+ }
1875
+ break;
1876
+ case "location":
1877
+ page.head.redirectLocation = header.value;
1878
+ debug$8(`oracle header "location" with value "${page.head.statusCode}" was parsed`);
1879
+ break;
1880
+ case "x-oracle-ignore": break;
1881
+ default:
1882
+ page.head.otherHeaders[header.name] = header.value;
1883
+ break;
1884
+ }
1885
+ });
1886
+ return page;
1887
+ };
1888
+ /**
1889
+ * Get a header line
1890
+ * @param line - The line
1891
+ * @returns The header.
1892
+ */
1893
+ const getHeader = (line) => {
1894
+ const index = line.indexOf(":");
1895
+ if (index !== -1) return {
1896
+ name: line.substring(0, index).trim(),
1897
+ value: line.substring(index + 1).trim()
1898
+ };
1899
+ return null;
1900
+ };
1901
+ /**
1902
+ * Parses a cookie string
1903
+ * @param text - The cookie string.
1904
+ * @returns The parsed cookie.
1905
+ */
1906
+ const parseCookie = (text) => {
1907
+ if (typeof text !== "string" || text.trim().length === 0) return null;
1908
+ let cookieElements = text.split(";");
1909
+ cookieElements = cookieElements.map((element) => element.trim());
1910
+ const firstElement = cookieElements[0];
1911
+ if (!firstElement) return null;
1912
+ const index = firstElement.indexOf("=");
1913
+ if (index <= 0) return null;
1914
+ const cookie = {
1915
+ name: firstElement.substring(0, index).trim(),
1916
+ value: firstElement.substring(index + 1).trim(),
1917
+ options: {}
1918
+ };
1919
+ cookieElements.shift();
1920
+ 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);
1923
+ else if (element.toLowerCase().startsWith("secure")) cookie.options.secure = true;
1924
+ else if (element.toLowerCase().startsWith("expires=")) {
1925
+ const date = tryDecodeDate(element.substring(8));
1926
+ if (date) cookie.options.expires = date;
1927
+ } else if (element.toLowerCase().startsWith("httponly")) cookie.options.httpOnly = true;
1928
+ });
1929
+ return cookie;
1930
+ };
1931
+ /**
1932
+ * Try to decode a date
1933
+ * @param value - The value to decode.
1934
+ * @returns The decoded date or null.
1935
+ */
1936
+ const tryDecodeDate = (value) => {
1937
+ const result = new Date(value);
1938
+ if (Number.isNaN(result.getTime())) return null;
1939
+ return result;
1940
+ };
1941
+
1942
+ //#endregion
1943
+ //#region src/backend/handler/plsql/sendResponse.ts
1944
+ const debug$7 = debugModule("webplsql:sendResponse");
1945
+ /**
1946
+ * Send "default" response to the browser
1947
+ * @param _req - The req object represents the HTTP request.
1948
+ * @param res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
1949
+ * @param page - The page to send.
1950
+ */
1951
+ const sendResponse = async (_req, res, page) => {
1952
+ const debugText = [];
1953
+ page.head.cookies.forEach((cookie) => {
1954
+ /* v8 ignore start */
1955
+ if (debug$7.enabled) debugText.push(`res.cookie: name="${cookie.name}" value="${cookie.value}" options=${JSON.stringify(cookie.options)}`);
1956
+ /* v8 ignore stop */
1957
+ res.cookie(cookie.name, cookie.value, cookie.options);
1958
+ });
1959
+ if (typeof page.head.redirectLocation === "string" && page.head.redirectLocation.length > 0) {
1960
+ /* v8 ignore start */
1961
+ if (debug$7.enabled) {
1962
+ debugText.push(`res.redirect(302, "${page.head.redirectLocation}")`);
1963
+ debug$7(getBlock("RESPONSE", debugText.join("\n")));
1964
+ }
1965
+ /* v8 ignore stop */
1966
+ res.redirect(302, page.head.redirectLocation);
1967
+ return;
1968
+ }
1969
+ for (const key in page.head.otherHeaders) {
1970
+ /* v8 ignore start */
1971
+ if (debug$7.enabled) debugText.push(`res.set("${key}", "${page.head.otherHeaders[key]}")`);
1972
+ /* v8 ignore stop */
1973
+ res.set(key, page.head.otherHeaders[key]);
1974
+ }
1975
+ if (page.file.fileType === "B" || page.file.fileType === "F") {
1976
+ const headers = {};
1977
+ if (typeof page.head.contentType === "string" && page.head.contentType.length > 0) headers["Content-Type"] = page.head.contentType;
1978
+ if (typeof page.file.fileSize === "number" && page.file.fileSize > 0) headers["Content-Length"] = page.file.fileSize.toString();
1979
+ if (Object.keys(headers).length > 0) {
1980
+ /* v8 ignore start */
1981
+ if (debug$7.enabled) debugText.push(`res.writeHead(200, ${JSON.stringify(headers)})`);
1982
+ /* v8 ignore stop */
1983
+ res.writeHead(200, headers);
1984
+ }
1985
+ if (page.file.fileBlob instanceof stream.Readable) {
1986
+ /* v8 ignore start */
1987
+ if (debug$7.enabled) {
1988
+ debugText.push(`res.pipe("${page.file.fileType ?? ""}") - streaming`);
1989
+ debug$7(getBlock("RESPONSE", debugText.join("\n")));
1990
+ }
1991
+ await new Promise((resolve, reject) => {
1992
+ if (page.file.fileBlob instanceof stream.Readable) {
1993
+ page.file.fileBlob.pipe(res);
1994
+ page.file.fileBlob.on("end", () => resolve());
1995
+ /* v8 ignore next - error handler */
1996
+ page.file.fileBlob.on("error", (err) => reject(err));
1997
+ /* v8 ignore next - error handler */
1998
+ res.on("close", () => resolve());
1999
+ }
2000
+ });
2001
+ } else {
2002
+ /* v8 ignore start */
2003
+ if (debug$7.enabled) {
2004
+ debugText.push(`res.end("${page.file.fileType ?? ""}") - buffer`);
2005
+ debug$7(getBlock("RESPONSE", debugText.join("\n")));
2006
+ }
2007
+ /* v8 ignore stop */
2008
+ res.end(page.file.fileBlob, "binary");
2009
+ }
2010
+ return;
2011
+ }
2012
+ if (typeof page.head.contentType === "string" && page.head.contentType.length > 0) {
2013
+ /* v8 ignore start */
2014
+ if (debug$7.enabled) debugText.push(`res.set("Content-Type", "${page.head.contentType}")`);
2015
+ /* v8 ignore stop */
2016
+ res.set("Content-Type", page.head.contentType);
2017
+ }
2018
+ if (typeof page.head.statusCode === "number") {
2019
+ /* v8 ignore start */
2020
+ if (debug$7.enabled) {
2021
+ debugText.push(`res.status(page.head.statusCode).send("${page.head.statusDescription ?? ""}")`);
2022
+ debug$7(getBlock("RESPONSE", debugText.join("\n")));
2023
+ }
2024
+ /* v8 ignore stop */
2025
+ res.status(page.head.statusCode).send(page.head.statusDescription);
2026
+ return;
2027
+ }
2028
+ /* v8 ignore start */
2029
+ if (debug$7.enabled) {
2030
+ if (page.body instanceof stream.Readable) debugText.push(`${"-".repeat(60)}\n[Stream Body]`);
2031
+ else debugText.push(`${"-".repeat(60)}\n${page.body}`);
2032
+ debug$7(getBlock("RESPONSE", debugText.join("\n")));
2033
+ }
2034
+ /* v8 ignore stop */
2035
+ if (page.body instanceof stream.Readable) await new Promise((resolve, reject) => {
2036
+ if (page.body instanceof stream.Readable) {
2037
+ page.body.pipe(res);
2038
+ page.body.on("end", () => resolve());
2039
+ /* v8 ignore next - error handler */
2040
+ page.body.on("error", (err) => reject(err));
2041
+ /* v8 ignore next - error handler */
2042
+ res.on("close", () => resolve());
2043
+ }
2044
+ });
2045
+ else res.send(page.body);
2046
+ };
2047
+
2048
+ //#endregion
2049
+ //#region src/backend/handler/plsql/procedureError.ts
2050
+ var ProcedureError = class ProcedureError extends Error {
2051
+ timestamp;
2052
+ environment;
2053
+ sql;
2054
+ bind;
2055
+ /**
2056
+ * @param message - The error message.
2057
+ * @param environment - The environment.
2058
+ * @param sql - The SQL to execute.
2059
+ * @param bind - The bind parameters.
2060
+ */
2061
+ constructor(message, environment, sql, bind) {
2062
+ super(message);
2063
+ if (Error.captureStackTrace) Error.captureStackTrace(this, ProcedureError);
2064
+ this.timestamp = /* @__PURE__ */ new Date();
2065
+ this.environment = environment;
2066
+ this.sql = sql;
2067
+ this.bind = bind;
2068
+ }
2069
+ };
2070
+
2071
+ //#endregion
2072
+ //#region src/backend/util/cache.ts
2073
+ /**
2074
+ * Generic Cache class with LFU (Least Frequently Used) eviction policy.
2075
+ */
2076
+ var Cache = class {
2077
+ cache;
2078
+ maxSize;
2079
+ hits;
2080
+ misses;
2081
+ /**
2082
+ * @param maxSize - Maximum number of entries in the cache.
2083
+ */
2084
+ constructor(maxSize = DEFAULT_CACHE_MAX_SIZE) {
2085
+ this.cache = /* @__PURE__ */ new Map();
2086
+ this.maxSize = maxSize;
2087
+ this.hits = 0;
2088
+ this.misses = 0;
2089
+ }
2090
+ /**
2091
+ * Get an entry from the cache.
2092
+ * @param key - The key.
2093
+ * @returns The value or undefined if not found.
2094
+ */
2095
+ get(key) {
2096
+ const entry = this.cache.get(key);
2097
+ if (entry) {
2098
+ entry.hitCount++;
2099
+ this.hits++;
2100
+ return entry.value;
2101
+ }
2102
+ this.misses++;
2103
+ }
2104
+ /**
2105
+ * Set an entry in the cache.
2106
+ * @param key - The key.
2107
+ * @param value - The value.
2108
+ */
2109
+ set(key, value) {
2110
+ if (this.cache.size >= this.maxSize && !this.cache.has(key)) this.prune();
2111
+ this.cache.set(key, {
2112
+ hitCount: 0,
2113
+ value
2114
+ });
2115
+ }
2116
+ /**
2117
+ * Delete an entry from the cache.
2118
+ * @param key - The key.
2119
+ */
2120
+ delete(key) {
2121
+ this.cache.delete(key);
2122
+ }
2123
+ /**
2124
+ * Clear the cache.
2125
+ */
2126
+ clear() {
2127
+ this.cache.clear();
2128
+ this.hits = 0;
2129
+ this.misses = 0;
2130
+ }
2131
+ /**
2132
+ * Prune the cache by removing the least frequently used entries.
2133
+ * Removes 10% of the cache size.
2134
+ */
2135
+ prune() {
2136
+ const entries = Array.from(this.cache.entries());
2137
+ entries.sort((a, b) => a[1].hitCount - b[1].hitCount);
2138
+ const removeCount = Math.max(1, Math.floor(this.maxSize * CACHE_PRUNE_PERCENT));
2139
+ const keysToRemove = entries.slice(0, removeCount).map(([key]) => key);
2140
+ for (const key of keysToRemove) this.cache.delete(key);
2141
+ }
2142
+ /**
2143
+ * Get the size of the cache.
2144
+ * @returns The size.
2145
+ */
2146
+ get size() {
2147
+ return this.cache.size;
2148
+ }
2149
+ /**
2150
+ * Get all keys in the cache.
2151
+ * @returns The keys.
2152
+ */
2153
+ keys() {
2154
+ return Array.from(this.cache.keys());
2155
+ }
2156
+ /**
2157
+ * Get cache statistics.
2158
+ * @returns The statistics.
2159
+ */
2160
+ getStats() {
2161
+ return {
2162
+ size: this.cache.size,
2163
+ maxSize: this.maxSize,
2164
+ hits: this.hits,
2165
+ misses: this.misses
2166
+ };
2167
+ }
2168
+ };
2169
+
2170
+ //#endregion
2171
+ //#region src/backend/handler/plsql/procedureSanitize.ts
2172
+ const debug$6 = debugModule("webplsql:procedureSanitize");
2173
+ const DEFAULT_EXCLUSION_LIST = [
2174
+ "sys.",
2175
+ "dbms_",
2176
+ "utl_",
2177
+ "owa_",
2178
+ "htp.",
2179
+ "htf.",
2180
+ "wpg_docload.",
2181
+ "ctxsys.",
2182
+ "mdsys."
2183
+ ];
2184
+ const validationFunctionCache = new Cache();
2185
+ /**
2186
+ * Resolve the procedure name using dbms_utility.name_resolve.
2187
+ *
2188
+ * @param procName - The procedure name to resolve.
2189
+ * @param databaseConnection - The database connection.
2190
+ * @param procedureNameCache - The procedure name cache.
2191
+ * @returns The resolved canonical procedure name (SCHEMA.NAME).
2192
+ */
2193
+ const resolveProcedureName = async (procName, databaseConnection, procedureNameCache) => {
2194
+ const cachedName = procedureNameCache.get(procName);
2195
+ if (cachedName) {
2196
+ debug$6(`resolveProcedureName: Cache hit for "${procName}" -> "${cachedName}"`);
2197
+ return cachedName;
2198
+ }
2199
+ debug$6(`resolveProcedureName: Cache miss for "${procName}". Resolving in DB...`);
2200
+ const sql = `
2201
+ DECLARE
2202
+ l_schema VARCHAR2(128);
2203
+ l_part1 VARCHAR2(128);
2204
+ l_part2 VARCHAR2(128);
2205
+ l_dblink VARCHAR2(128);
2206
+ l_part1_type NUMBER;
2207
+ l_object_number NUMBER;
2208
+ BEGIN
2209
+ dbms_utility.name_resolve(
2210
+ name => :name,
2211
+ context => 1,
2212
+ schema => l_schema,
2213
+ part1 => l_part1,
2214
+ part2 => l_part2,
2215
+ dblink => l_dblink,
2216
+ part1_type => l_part1_type,
2217
+ object_number => l_object_number
2218
+ );
2219
+
2220
+ -- Reconstruct the canonical name
2221
+ -- If it's a package procedure: schema.package.procedure
2222
+ -- If it's a standalone procedure: schema.procedure
2223
+
2224
+ IF l_part1 IS NOT NULL THEN
2225
+ :resolved := l_schema || '.' || l_part1 || '.' || l_part2;
2226
+ ELSE
2227
+ :resolved := l_schema || '.' || l_part2;
2228
+ END IF;
2229
+ END;
2230
+ `;
2231
+ const bind = {
2232
+ name: {
2233
+ dir: BIND_IN,
2234
+ type: STRING,
2235
+ val: procName
2236
+ },
2237
+ resolved: {
2238
+ dir: BIND_OUT,
2239
+ type: STRING,
2240
+ maxSize: OWA_RESOLVED_NAME_MAX_LEN
2241
+ }
2242
+ };
2243
+ try {
2244
+ const result = await databaseConnection.execute(sql, bind);
2245
+ const { resolved } = z$1.strictObject({ resolved: z$1.string() }).parse(result.outBinds);
2246
+ if (!resolved) throw new RequestError(`Could not resolve procedure name "${procName}"`);
2247
+ debug$6(`resolveProcedureName: Resolved "${procName}" -> "${resolved}"`);
2248
+ procedureNameCache.set(procName, resolved);
2249
+ return resolved;
2250
+ } catch (err) {
2251
+ /* v8 ignore start */
2252
+ debug$6(`resolveProcedureName: Error resolving "${procName}"`, err);
2253
+ /* v8 ignore stop */
2254
+ throw new RequestError(`Procedure "${procName}" not found or not accessible.\n${errorToString(err)}`);
2255
+ }
2256
+ };
2257
+ /**
2258
+ * Sanitize the procedure name.
2259
+ *
2260
+ * @param procName - The procedure name.
2261
+ * @param databaseConnection - The database connection
2262
+ * @param options - the options for the middleware.
2263
+ * @param procedureNameCache - The procedure name cache.
2264
+ * @returns Promise resolving to final procedure name.
2265
+ */
2266
+ const sanitizeProcName = async (procName, databaseConnection, options, procedureNameCache) => {
2267
+ debug$6("sanitizeProcName", procName);
2268
+ let finalProcName = procName.toLowerCase().trim();
2269
+ finalProcName = removeSpecialCharacters(finalProcName);
2270
+ for (const i of DEFAULT_EXCLUSION_LIST) if (finalProcName.startsWith(i)) {
2271
+ const error = `Procedure name "${procName}" is in default exclusion list "${DEFAULT_EXCLUSION_LIST.join(",")}"`;
2272
+ /* v8 ignore start */
2273
+ debug$6(error);
2274
+ /* v8 ignore stop */
2275
+ throw new RequestError(error);
2276
+ }
2277
+ if (options.exclusionList && options.exclusionList.length > 0) {
2278
+ for (const i of options.exclusionList) if (finalProcName.startsWith(i)) {
2279
+ const error = `Procedure name "${procName}" is in custom exclusion list "${options.exclusionList.join(",")}"`;
2280
+ /* v8 ignore start */
2281
+ debug$6(error);
2282
+ /* v8 ignore stop */
2283
+ throw new RequestError(error);
2284
+ }
2285
+ }
2286
+ if (options.requestValidationFunction && options.requestValidationFunction.length > 0) {
2287
+ if (!await requestValidationFunction(finalProcName, options.requestValidationFunction, databaseConnection)) {
2288
+ const error = `Procedure name "${procName}" is not valid according to the request validation function "${options.requestValidationFunction}"`;
2289
+ /* v8 ignore start */
2290
+ debug$6(error);
2291
+ /* v8 ignore stop */
2292
+ throw new RequestError(error);
2293
+ }
2294
+ }
2295
+ return await resolveProcedureName(finalProcName, databaseConnection, procedureNameCache);
2296
+ };
2297
+ /**
2298
+ * @param str - string
2299
+ * @returns string
2300
+ */
2301
+ const removeSpecialCharacters = (str) => {
2302
+ if (str === null || str === void 0) return "";
2303
+ const chars = [];
2304
+ for (const c of str) if (c >= "0" && c <= "9" || c >= "A" && c <= "Z" || c >= "a" && c <= "z" || c === "." || c === "_" || c === "#" || c === "$") chars.push(c);
2305
+ return chars.join("");
2306
+ };
2307
+ /**
2308
+ * @param procName - The procedure name.
2309
+ * @param requestValidationFunction - The request validation function.
2310
+ * @param databaseConnection - The database connection
2311
+ * @returns Promise resolving to final procedure name.
2312
+ */
2313
+ const loadRequestValid = async (procName, requestValidationFunction, databaseConnection) => {
2314
+ const bind = {
2315
+ proc: {
2316
+ dir: BIND_IN,
2317
+ type: STRING,
2318
+ val: procName
2319
+ },
2320
+ valid: {
2321
+ dir: BIND_OUT,
2322
+ type: NUMBER
2323
+ }
2324
+ };
2325
+ const SQL = [
2326
+ "DECLARE",
2327
+ " l_valid NUMBER := 0;",
2328
+ "BEGIN",
2329
+ ` IF (${requestValidationFunction}(:proc)) THEN`,
2330
+ " l_valid := 1;",
2331
+ " END IF;",
2332
+ " :valid := l_valid;",
2333
+ "END;"
2334
+ ].join("\n");
2335
+ let result = {};
2336
+ try {
2337
+ result = await databaseConnection.execute(SQL, bind);
2338
+ } catch (err) {
2339
+ /* v8 ignore start */
2340
+ debug$6("result", result);
2341
+ throw new RequestError(`Error when validating procedure name "${procName}"\n${SQL}\n${errorToString(err)}`);
2342
+ }
2343
+ try {
2344
+ return z$1.strictObject({ valid: z$1.number() }).parse(result.outBinds).valid === 1;
2345
+ } catch (err) {
2346
+ /* v8 ignore start */
2347
+ debug$6("result", result.outBinds);
2348
+ /* v8 ignore stop */
2349
+ const message = `Internal error when parsing ${String(result.outBinds)}\n${errorToString(err)}`;
2350
+ throw new Error(message);
2351
+ }
2352
+ };
2353
+ /**
2354
+ * Request validation function.
2355
+ *
2356
+ * @param procName - The procedure name.
2357
+ * @param validationFunction - The request validation function.
2358
+ * @param databaseConnection - The database connection
2359
+ * @returns Promise resolving to final procedure name.
2360
+ */
2361
+ const requestValidationFunction = async (procName, validationFunction, databaseConnection) => {
2362
+ debug$6("requestValidationFunction", procName, validationFunction);
2363
+ const key = procName.toLowerCase();
2364
+ const cacheEntry = validationFunctionCache.get(key);
2365
+ if (cacheEntry !== void 0) {
2366
+ /* v8 ignore start */
2367
+ if (debug$6.enabled) debug$6(`requestValidationFunction: procedure "${procName}" found in cache`);
2368
+ /* v8 ignore stop */
2369
+ return cacheEntry.valid;
2370
+ }
2371
+ /* v8 ignore start */
2372
+ if (debug$6.enabled) debug$6(`requestValidationFunction: procedure "${procName}" not found in cache and must be loaded`);
2373
+ /* v8 ignore stop */
2374
+ const valid = await loadRequestValid(procName, validationFunction, databaseConnection);
2375
+ validationFunctionCache.set(key, { valid });
2376
+ return valid;
2377
+ };
2378
+
2379
+ //#endregion
2380
+ //#region src/backend/handler/plsql/owaPageStream.ts
2381
+ const debug$5 = debugModule("webplsql:owaPageStream");
2382
+ var OWAPageStream = class extends Readable {
2383
+ databaseConnection;
2384
+ chunkSize;
2385
+ isDone;
2386
+ /**
2387
+ * @param databaseConnection - The database connection.
2388
+ */
2389
+ constructor(databaseConnection) {
2390
+ super({ highWaterMark: OWA_STREAM_BUFFER_SIZE });
2391
+ this.databaseConnection = databaseConnection;
2392
+ this.chunkSize = OWA_STREAM_CHUNK_SIZE;
2393
+ this.isDone = false;
2394
+ }
2395
+ /**
2396
+ * Fetch a chunk of the page from the database.
2397
+ * @returns The array of lines fetched.
2398
+ */
2399
+ async fetchChunk() {
2400
+ if (this.isDone) return [];
2401
+ const bindParameter = {
2402
+ lines: {
2403
+ dir: BIND_OUT,
2404
+ type: STRING,
2405
+ maxArraySize: this.chunkSize
2406
+ },
2407
+ irows: {
2408
+ dir: BIND_INOUT,
2409
+ type: NUMBER,
2410
+ val: this.chunkSize
2411
+ }
2412
+ };
2413
+ const sqlStatement = "BEGIN owa.get_page(thepage=>:lines, irows=>:irows); END;";
2414
+ try {
2415
+ const result = await this.databaseConnection.execute(sqlStatement, bindParameter);
2416
+ const { lines, irows } = z$1.strictObject({
2417
+ irows: z$1.number(),
2418
+ lines: z$1.array(z$1.string())
2419
+ }).parse(result.outBinds);
2420
+ debug$5(`fetched ${lines.length} lines (irows=${irows})`);
2421
+ if (lines.length < this.chunkSize) this.isDone = true;
2422
+ return lines;
2423
+ } catch (err) {
2424
+ if (err instanceof ProcedureError) throw err;
2425
+ throw new ProcedureError(`OWAPageStream: error when getting page\n${errorToString(err)}`, {}, sqlStatement, bindParameter);
2426
+ }
2427
+ }
2428
+ /**
2429
+ * @override
2430
+ * @param _size - The size hint (unused).
2431
+ */
2432
+ _read(_size) {
2433
+ this.fetchChunk().then((lines) => {
2434
+ if (lines.length > 0) this.push(lines.join(""));
2435
+ if (this.isDone) this.push(null);
2436
+ }).catch((err) => {
2437
+ this.destroy(err instanceof Error ? err : new Error(String(err)));
2438
+ });
2439
+ }
2440
+ /**
2441
+ * Add initial body content to the stream.
2442
+ * @param content - The initial content to prepend.
2443
+ */
2444
+ addBody(content) {
2445
+ if (content && content.length > 0) this.push(content);
2446
+ }
2447
+ };
2448
+
2449
+ //#endregion
2450
+ //#region src/backend/util/traceManager.ts
2451
+ 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
+ }
2460
+ /**
2461
+ * Toggle tracing
2462
+ * @param enabled - New state
2463
+ */
2464
+ setEnabled(enabled) {
2465
+ this.enabled = enabled;
2466
+ }
2467
+ /**
2468
+ * Is tracing enabled?
2469
+ * @returns The state
2470
+ */
2471
+ isEnabled() {
2472
+ return this.enabled;
2473
+ }
2474
+ /**
2475
+ * Add a trace entry
2476
+ * @param entry - The trace entry
2477
+ */
2478
+ addTrace(entry) {
2479
+ if (!this.enabled) return;
2480
+ try {
2481
+ const line = JSON.stringify(entry) + "\n";
2482
+ fs.appendFileSync(this.filename, line);
2483
+ } catch (err) {
2484
+ console.error("TraceManager: error writing trace", err);
2485
+ }
2486
+ }
2487
+ /**
2488
+ * Clear all traces
2489
+ */
2490
+ clear() {
2491
+ try {
2492
+ if (fs.existsSync(this.filename)) fs.truncateSync(this.filename, 0);
2493
+ } catch (err) {
2494
+ console.error("TraceManager: error clearing traces", err);
2495
+ }
2496
+ }
2497
+ /**
2498
+ * Get the full path to the trace file
2499
+ * @returns The path
2500
+ */
2501
+ getFilePath() {
2502
+ return path.resolve(this.filename);
2503
+ }
2504
+ };
2505
+ const traceManager = new TraceManager();
2506
+
2507
+ //#endregion
2508
+ //#region src/backend/handler/plsql/procedure.ts
2509
+ const debug$4 = debugModule("webplsql:procedure");
2510
+ /**
2511
+ * Get the procedure and arguments to execute
2512
+ * @param req - The req object represents the HTTP request. (only used for debugging)
2513
+ * @param procName - The procedure to execute
2514
+ * @param argObj - The arguments to pass to the procedure
2515
+ * @param options - The options for the middleware
2516
+ * @param databaseConnection - The database connection
2517
+ * @param procedureNameCache - The procedure name cache.
2518
+ * @param argumentCache - The argument cache.
2519
+ * @returns The SQL statement and bindings for the procedure to execute
2520
+ */
2521
+ const getProcedure = async (req, procName, argObj, options, databaseConnection, procedureNameCache, argumentCache) => {
2522
+ if (options.pathAlias?.toLowerCase() === procName.toLowerCase()) {
2523
+ /* v8 ignore start */
2524
+ debug$4(`getProcedure: path alias "${options.pathAlias}" redirects to "${options.pathAliasProcedure}"`);
2525
+ /* v8 ignore stop */
2526
+ return {
2527
+ sql: `${options.pathAliasProcedure}(p_path=>:p_path)`,
2528
+ bind: { p_path: {
2529
+ dir: BIND_IN,
2530
+ type: STRING,
2531
+ val: procName
2532
+ } }
2533
+ };
2534
+ }
2535
+ const useVariableArguments = procName.startsWith("!");
2536
+ const sanitizedProcName = await sanitizeProcName(useVariableArguments ? procName.substring(1) : procName, databaseConnection, options, procedureNameCache);
2537
+ if (useVariableArguments) return {
2538
+ ...getProcedureVariable(req, sanitizedProcName, argObj),
2539
+ resolvedName: sanitizedProcName
2540
+ };
2541
+ else return {
2542
+ ...await getProcedureNamed(req, sanitizedProcName, argObj, databaseConnection, argumentCache),
2543
+ resolvedName: sanitizedProcName
2544
+ };
2545
+ };
2546
+ /**
2547
+ * Prepare procedure
2548
+ *
2549
+ * NOTE:
2550
+ * 1) dbms_session.modify_package_state(dbms_session.reinitialize) is used to ensure a stateless environment by resetting package state (dbms_session.reset_package)
2551
+ *
2552
+ * @param cgiObj - The cgi of the procedure to invoke.
2553
+ * @param databaseConnection - Database connection.
2554
+ * @returns Promise resolving to void.
2555
+ */
2556
+ const procedurePrepare = async (cgiObj, databaseConnection) => {
2557
+ let sqlStatement = "BEGIN dbms_session.modify_package_state(dbms_session.reinitialize); END;";
2558
+ try {
2559
+ await databaseConnection.execute(sqlStatement);
2560
+ } catch (err) {
2561
+ throw new ProcedureError(`procedurePrepare: error when preparing procedure\n${errorToString(err)}`, cgiObj, sqlStatement, {});
2562
+ }
2563
+ sqlStatement = "BEGIN owa.init_cgi_env(:cgicount, :cginames, :cgivalues); owa.user_id := :remote_user; htp.init; htp.htbuf_len := 63; END;";
2564
+ const bindParameter = {
2565
+ cgicount: {
2566
+ dir: BIND_IN,
2567
+ type: NUMBER,
2568
+ val: Object.keys(cgiObj).length
2569
+ },
2570
+ cginames: {
2571
+ dir: BIND_IN,
2572
+ type: STRING,
2573
+ val: Object.keys(cgiObj)
2574
+ },
2575
+ cgivalues: {
2576
+ dir: BIND_IN,
2577
+ type: STRING,
2578
+ val: Object.values(cgiObj)
2579
+ },
2580
+ remote_user: {
2581
+ dir: BIND_IN,
2582
+ type: STRING,
2583
+ val: (cgiObj.REMOTE_USER ?? "").substring(0, 30)
2584
+ }
2585
+ };
2586
+ try {
2587
+ await databaseConnection.execute(sqlStatement, bindParameter);
2588
+ } catch (err) {
2589
+ throw new ProcedureError(`procedurePrepare: error when preparing procedure\n${errorToString(err)}`, cgiObj, sqlStatement, bindParameter);
2590
+ }
2591
+ };
2592
+ /**
2593
+ * Execute procedure
2594
+ *
2595
+ * @param para - The statement and binding to use when executing the procedure.
2596
+ * @param para.sql - The SQL statement.
2597
+ * @param para.bind - The bind parameters.
2598
+ * @param databaseConnection - Database connection.
2599
+ * @returns Promise resolving to void.
2600
+ */
2601
+ const procedureExecute = async (para, databaseConnection) => {
2602
+ const sqlStatement = `BEGIN ${para.sql}; END;`;
2603
+ try {
2604
+ await databaseConnection.execute(sqlStatement, para.bind);
2605
+ } catch (err) {
2606
+ throw new ProcedureError(`procedureExecute: error when executing procedure:\n${sqlStatement}\n${errorToString(err)}`, {}, para.sql, para.bind);
2607
+ }
2608
+ };
2609
+ /**
2610
+ * Download files from procedure
2611
+ *
2612
+ * @param fileBlob - The blob eventually containing the file.
2613
+ * @param databaseConnection - Database connection.
2614
+ * @returns Promise resolving to the result.
2615
+ */
2616
+ const procedureDownloadFiles = async (fileBlob, databaseConnection) => {
2617
+ const bindParameter = {
2618
+ fileType: {
2619
+ dir: BIND_OUT,
2620
+ type: STRING
2621
+ },
2622
+ fileSize: {
2623
+ dir: BIND_OUT,
2624
+ type: NUMBER
2625
+ },
2626
+ fileBlob: {
2627
+ dir: BIND_INOUT,
2628
+ type: BLOB,
2629
+ val: fileBlob
2630
+ }
2631
+ };
2632
+ const sqlStatement = `
2633
+ DECLARE
2634
+ l_file_type VARCHAR2(32767) := '';
2635
+ l_file_size INTEGER := 0;
2636
+ BEGIN
2637
+ IF (wpg_docload.is_file_download()) THEN
2638
+ wpg_docload.get_download_file(l_file_type);
2639
+ IF (l_file_type = 'B') THEN
2640
+ wpg_docload.get_download_blob(:fileBlob);
2641
+ l_file_size := dbms_lob.getlength(:fileBlob);
2642
+ END IF;
2643
+ END IF;
2644
+ :fileType := l_file_type;
2645
+ :fileSize := l_file_size;
2646
+ END;
2647
+ `;
2648
+ let result = null;
2649
+ try {
2650
+ result = await databaseConnection.execute(sqlStatement, bindParameter);
2651
+ } catch (err) {
2652
+ /* v8 ignore start */
2653
+ if (debug$4.enabled) debug$4(getBlock("procedureDownloadFiles: results", inspect(result)));
2654
+ /* v8 ignore stop */
2655
+ throw new ProcedureError(`procedureDownloadFiles: error when downloading files\n${errorToString(err)}`, {}, sqlStatement, bindParameter);
2656
+ }
2657
+ return z$1.object({
2658
+ fileType: z$1.string().nullable().transform((val) => val ?? ""),
2659
+ fileSize: z$1.number().nullable().transform((val) => val ?? 0),
2660
+ fileBlob: z$1.instanceof(stream.Readable).nullable()
2661
+ }).parse(result.outBinds);
2662
+ };
2663
+ /**
2664
+ * Invoke the Oracle procedure and return the page content
2665
+ *
2666
+ * @param req - The req object represents the HTTP request.
2667
+ * @param res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
2668
+ * @param argObj - - The arguments of the procedure to invoke.
2669
+ * @param cgiObj - The cgi of the procedure to invoke.
2670
+ * @param filesToUpload - Array of files to be uploaded
2671
+ * @param options - the options for the middleware.
2672
+ * @param databaseConnection - Database connection.
2673
+ * @param procedureNameCache - The procedure name cache.
2674
+ * @param argumentCache - The argument cache.
2675
+ * @returns Promise resolving to the page content generated by the executed procedure
2676
+ */
2677
+ const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options, databaseConnection, procedureNameCache, argumentCache) => {
2678
+ debug$4("invokeProcedure: begin");
2679
+ const startTime = Date.now();
2680
+ let traceData = null;
2681
+ if (traceManager.isEnabled()) traceData = {
2682
+ id: Math.random().toString(36).substring(2, 15),
2683
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2684
+ source: cgiObj.REMOTE_ADDR ?? "",
2685
+ url: req.originalUrl,
2686
+ method: req.method,
2687
+ status: "pending",
2688
+ duration: 0,
2689
+ cgi: cgiObj,
2690
+ headers: req.headers,
2691
+ cookies: req.cookies,
2692
+ uploads: filesToUpload.map((f) => ({
2693
+ originalname: f.originalname,
2694
+ mimetype: f.mimetype,
2695
+ size: f.size
2696
+ }))
2697
+ };
2698
+ try {
2699
+ debug$4(`invokeProcedure: upload "${filesToUpload.length}" files`);
2700
+ if (filesToUpload.length > 0) if (typeof options.documentTable === "string" && options.documentTable.length > 0) {
2701
+ const { documentTable } = options;
2702
+ await Promise.all(filesToUpload.map((file) => uploadFile(file, documentTable, databaseConnection)));
2703
+ } else console.warn(`Unable to upload "${filesToUpload.length}" files because the option ""doctable" has not been defined`);
2704
+ debug$4("invokeProcedure: get procedure to execute and the arguments");
2705
+ const rawProcName = Array.isArray(req.params.name) ? req.params.name[0] : req.params.name;
2706
+ if (!rawProcName) throw new RequestError("No procedure name provided");
2707
+ const para = await getProcedure(req, rawProcName, argObj, options, databaseConnection, procedureNameCache, argumentCache);
2708
+ if (traceData) {
2709
+ traceData.procedure = para.resolvedName;
2710
+ traceData.parameters = para.bind;
2711
+ }
2712
+ debug$4("invokeProcedure: prepare the session");
2713
+ await procedurePrepare(cgiObj, databaseConnection);
2714
+ debug$4("invokeProcedure: execute the session");
2715
+ try {
2716
+ await procedureExecute(para, databaseConnection);
2717
+ } catch (err) {
2718
+ if (err instanceof ProcedureError) {
2719
+ const errorString = err.toString();
2720
+ if (errorString.includes("ORA-04068") || errorString.includes("ORA-04061") || errorString.includes("ORA-04065") || errorString.includes("ORA-06550")) {
2721
+ debug$4(`invokeProcedure: detected invalidation error (${errorString}). Clearing caches.`);
2722
+ if (rawProcName) procedureNameCache.delete(rawProcName);
2723
+ if (para.resolvedName) argumentCache.delete(para.resolvedName.toUpperCase());
2724
+ }
2725
+ }
2726
+ throw err;
2727
+ }
2728
+ debug$4("invokeProcedure: get the page returned from the procedure");
2729
+ const streamInstance = new OWAPageStream(databaseConnection);
2730
+ const lines = await streamInstance.fetchChunk();
2731
+ /* v8 ignore start */
2732
+ if (debug$4.enabled) debug$4(getBlock("data", lines.join("")));
2733
+ /* v8 ignore stop */
2734
+ debug$4("invokeProcedure: download files");
2735
+ const fileBlob = await databaseConnection.createLob(BLOB);
2736
+ try {
2737
+ const fileDownload = await procedureDownloadFiles(fileBlob, databaseConnection);
2738
+ /* v8 ignore start */
2739
+ if (debug$4.enabled) debug$4(getBlock("fileDownload", inspect({
2740
+ fileType: fileDownload.fileType,
2741
+ fileSize: fileDownload.fileSize
2742
+ })));
2743
+ /* v8 ignore stop */
2744
+ debug$4("invokeProcedure: parse the page");
2745
+ const pageComponents = parsePage(lines.join(""));
2746
+ pageComponents.head.server = cgiObj.SERVER_SOFTWARE ?? "";
2747
+ if (fileDownload.fileType !== "" && fileDownload.fileSize > 0 && fileDownload.fileBlob !== null) {
2748
+ pageComponents.file.fileType = fileDownload.fileType;
2749
+ pageComponents.file.fileSize = fileDownload.fileSize;
2750
+ pageComponents.file.fileBlob = fileDownload.fileBlob;
2751
+ if (traceData) traceData.downloads = {
2752
+ fileType: fileDownload.fileType,
2753
+ fileSize: fileDownload.fileSize
2754
+ };
2755
+ } else {
2756
+ if (typeof pageComponents.body === "string" && pageComponents.body.length > 0) streamInstance.addBody(pageComponents.body);
2757
+ pageComponents.body = streamInstance;
2758
+ if (traceData) {
2759
+ let htmlBuffer = typeof pageComponents.body === "string" ? pageComponents.body : lines.join("");
2760
+ const MAX_HTML_SIZE = 1024 * 1024;
2761
+ const originalPush = streamInstance.push.bind(streamInstance);
2762
+ streamInstance.push = (chunk) => {
2763
+ if (chunk !== null && htmlBuffer.length < MAX_HTML_SIZE) {
2764
+ htmlBuffer += String(chunk);
2765
+ if (htmlBuffer.length > MAX_HTML_SIZE) htmlBuffer = htmlBuffer.substring(0, MAX_HTML_SIZE) + "... [truncated]";
2766
+ }
2767
+ return originalPush(chunk);
2768
+ };
2769
+ const currentTrace = traceData;
2770
+ streamInstance.on("end", () => {
2771
+ currentTrace.html = htmlBuffer;
2772
+ currentTrace.status = "success";
2773
+ currentTrace.duration = Date.now() - startTime;
2774
+ traceManager.addTrace(currentTrace);
2775
+ });
2776
+ streamInstance.on("error", (err) => {
2777
+ currentTrace.status = "fail";
2778
+ currentTrace.error = err instanceof Error ? err.message : String(err);
2779
+ currentTrace.duration = Date.now() - startTime;
2780
+ traceManager.addTrace(currentTrace);
2781
+ });
2782
+ }
2783
+ }
2784
+ debug$4("invokeProcedure: send the page to browser");
2785
+ await sendResponse(req, res, pageComponents);
2786
+ if (traceData?.downloads) {
2787
+ traceData.status = "success";
2788
+ traceData.duration = Date.now() - startTime;
2789
+ traceManager.addTrace(traceData);
2790
+ }
2791
+ } finally {
2792
+ debug$4("invokeProcedure: cleanup");
2793
+ fileBlob.destroy();
2794
+ }
2795
+ } catch (err) {
2796
+ if (traceData) {
2797
+ traceData.status = err instanceof ProcedureError ? "error" : "fail";
2798
+ traceData.error = err instanceof Error ? err.message : String(err);
2799
+ traceData.duration = Date.now() - startTime;
2800
+ traceManager.addTrace(traceData);
2801
+ }
2802
+ throw err;
2803
+ }
2804
+ debug$4("invokeProcedure: end");
2805
+ };
2806
+
2807
+ //#endregion
2808
+ //#region src/backend/handler/plsql/cgi.ts
2809
+ const debug$3 = debugModule("webplsql:cgi");
2810
+ const DEFAULT_CGI = {
2811
+ PLSQL_GATEWAY: "WebDb",
2812
+ GATEWAY_IVERSION: "2",
2813
+ SERVER_SOFTWARE: "web_plsql",
2814
+ GATEWAY_INTERFACE: "CGI/1.1",
2815
+ SERVER_PORT: "",
2816
+ SERVER_NAME: os.hostname(),
2817
+ REQUEST_METHOD: "",
2818
+ PATH_INFO: "",
2819
+ SCRIPT_NAME: "",
2820
+ REMOTE_ADDR: "",
2821
+ SERVER_PROTOCOL: "",
2822
+ REQUEST_PROTOCOL: "",
2823
+ REMOTE_USER: "",
2824
+ HTTP_COOKIE: "",
2825
+ HTTP_USER_AGENT: "",
2826
+ HTTP_HOST: "",
2827
+ HTTP_ACCEPT: "",
2828
+ HTTP_ACCEPT_ENCODING: "",
2829
+ HTTP_ACCEPT_LANGUAGE: "",
2830
+ HTTP_REFERER: "",
2831
+ HTTP_X_FORWARDED_FOR: "",
2832
+ WEB_AUTHENT_PREFIX: "",
2833
+ DAD_NAME: "",
2834
+ DOC_ACCESS_PATH: "doc",
2835
+ DOCUMENT_TABLE: "",
2836
+ PATH_ALIAS: "",
2837
+ REQUEST_CHARSET: "UTF8",
2838
+ REQUEST_IANA_CHARSET: "UTF-8",
2839
+ SCRIPT_PREFIX: ""
2840
+ };
2841
+ /**
2842
+ * Create the HTTP_COOKIE string
2843
+ *
2844
+ * @param req - The req object represents the HTTP request.
2845
+ * @returns The string representation of the cookies.
2846
+ */
2847
+ const getCookieString = (req) => {
2848
+ let cookieString = "";
2849
+ for (const propName in req.cookies) cookieString += `${propName}=${req.cookies[propName]};`;
2850
+ debug$3("getCookieString", req.cookies, cookieString);
2851
+ return cookieString;
2852
+ };
2853
+ /**
2854
+ * Get the script name and DAD name from a URL
2855
+ *
2856
+ * @param req - The req object represents the HTTP request.
2857
+ * @returns The DAD structure.
2858
+ */
2859
+ const getPath = (req) => {
2860
+ const pathname = new URL(`${req.protocol}://${os.hostname()}${req.originalUrl}`).pathname;
2861
+ const tmp = trimPath(pathname.substring(0, pathname.lastIndexOf("/") + 1));
2862
+ return {
2863
+ script: `/${tmp}`,
2864
+ prefix: `/${tmp.substring(0, tmp.lastIndexOf("/"))}`,
2865
+ dad: tmp.substring(tmp.indexOf("/") + 1)
2866
+ };
2867
+ };
2868
+ /**
2869
+ * Get a path that is not enclodes in slashes
2870
+ *
2871
+ * @param value - The value to trim.
2872
+ * @returns The trimmed value.
2873
+ */
2874
+ const trimPath = (value) => value.replace(/^\/+|\/+$/g, "");
2875
+ /**
2876
+ * Create a CGI object
2877
+ *
2878
+ * @param req - The req object represents the HTTP request.
2879
+ * @param doctable - The document table.
2880
+ * @param cgi - The additional cgi.
2881
+ * @param authenticatedUser - The authenticated user.
2882
+ * @returns CGI object
2883
+ */
2884
+ const getCGI = (req, doctable, cgi, authenticatedUser = null) => {
2885
+ const PROTOCOL = req.protocol ? req.protocol.toUpperCase() : "";
2886
+ const PATH = getPath(req);
2887
+ const CGI = {
2888
+ SERVER_PORT: typeof req.socket.localPort === "number" ? req.socket.localPort.toString() : "",
2889
+ REQUEST_METHOD: req.method,
2890
+ PATH_INFO: Array.isArray(req.params.name) ? req.params.name[0] ?? "" : req.params.name ?? "",
2891
+ SCRIPT_NAME: PATH.script,
2892
+ REMOTE_ADDR: (req.ip ?? "").replace("::ffff:", ""),
2893
+ SERVER_PROTOCOL: `${PROTOCOL}/${req.httpVersion}`,
2894
+ REQUEST_PROTOCOL: PROTOCOL,
2895
+ REMOTE_USER: authenticatedUser ?? "",
2896
+ AUTH_TYPE: authenticatedUser ? "Basic" : "",
2897
+ HTTP_COOKIE: getCookieString(req),
2898
+ HTTP_USER_AGENT: req.get("user-agent") ?? "",
2899
+ HTTP_HOST: req.get("host") ?? "",
2900
+ HTTP_ACCEPT: req.get("accept") ?? "",
2901
+ HTTP_ACCEPT_ENCODING: req.get("accept-encoding") ?? "",
2902
+ HTTP_ACCEPT_LANGUAGE: req.get("accept-language") ?? "",
2903
+ HTTP_REFERER: req.get("referer") ?? "",
2904
+ HTTP_X_FORWARDED_FOR: req.get("x-forwarded-for") ?? "",
2905
+ DAD_NAME: PATH.dad,
2906
+ DOCUMENT_TABLE: doctable,
2907
+ SCRIPT_PREFIX: PATH.prefix
2908
+ };
2909
+ return Object.assign({}, DEFAULT_CGI, CGI, cgi);
2910
+ };
2911
+
2912
+ //#endregion
2913
+ //#region src/backend/util/type.ts
2914
+ /**
2915
+ * Is the given value a string or an array of strings
2916
+ * @param value - The value to check.
2917
+ * @returns True if the value is a string or an array of strings
2918
+ */
2919
+ const isStringOrArrayOfString = (value) => typeof value === "string" || Array.isArray(value) && value.every((element) => typeof element === "string");
2920
+
2921
+ //#endregion
2922
+ //#region src/backend/handler/plsql/request.ts
2923
+ const debug$2 = debugModule("webplsql:request");
2924
+ /**
2925
+ * Normalize the body by making sure that only "simple" parameters and no nested objects are submitted
2926
+ * @param req - The req object represents the HTTP request.
2927
+ * @returns The normalized body.
2928
+ */
2929
+ const normalizeBody = (req) => {
2930
+ const args = {};
2931
+ /* v8 ignore else - body validation */
2932
+ if (typeof req.body === "object" && req.body !== null) {
2933
+ const body = req.body;
2934
+ for (const key in body) {
2935
+ const value = body[key];
2936
+ /* v8 ignore else - type validation */
2937
+ if (isStringOrArrayOfString(value)) args[key] = value;
2938
+ else
2939
+ /* v8 ignore next - invalid body type */
2940
+ throw new RequestError(`The element "${key}" in the body is not a string or an array of strings!\n${util.inspect(req.body, {
2941
+ showHidden: false,
2942
+ depth: null,
2943
+ colors: false
2944
+ })}`);
2945
+ }
2946
+ }
2947
+ return args;
2948
+ };
2949
+ /**
2950
+ * Execute the request
2951
+ *
2952
+ * @param req - The req object represents the HTTP request.
2953
+ * @param res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
2954
+ * @param options - the options for the middleware.
2955
+ * @param connectionPool - The connection pool.
2956
+ * @param procedureNameCache - The procedure name cache.
2957
+ * @param argumentCache - The argument cache.
2958
+ * @param authenticatedUser - The authenticated user identity.
2959
+ * @returns Promise resolving to th page
2960
+ */
2961
+ const processRequest = async (req, res, options, connectionPool, procedureNameCache, argumentCache, authenticatedUser = null) => {
2962
+ debug$2("processRequest: ENTER");
2963
+ 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
+ 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();
2992
+ debug$2("processRequest: EXIT");
2993
+ };
2994
+
2995
+ //#endregion
2996
+ //#region src/backend/util/jsonLogger.ts
2997
+ var JsonLogger = class {
2998
+ stream;
2999
+ constructor(filename = "error.json.log") {
3000
+ this.stream = rotatingFileStream.createStream(filename, {
3001
+ size: JSON_LOG_ROTATION_SIZE,
3002
+ interval: JSON_LOG_ROTATION_INTERVAL,
3003
+ maxFiles: JSON_LOG_MAX_ROTATED_FILES,
3004
+ compress: "gzip"
3005
+ });
3006
+ }
3007
+ /**
3008
+ * Log an entry as NDJSON.
3009
+ * @param entry - The entry to log.
3010
+ */
3011
+ log(entry) {
3012
+ try {
3013
+ entry.timestamp ??= (/* @__PURE__ */ new Date()).toISOString();
3014
+ const line = JSON.stringify(entry);
3015
+ this.stream.write(line + "\n");
3016
+ } catch (err) {
3017
+ console.error("JsonLogger: Failed to write log", err);
3018
+ }
3019
+ }
3020
+ /**
3021
+ * Close the stream.
3022
+ */
3023
+ close() {
3024
+ this.stream.end();
3025
+ }
3026
+ };
3027
+ const jsonLogger = new JsonLogger();
3028
+
3029
+ //#endregion
3030
+ //#region src/backend/handler/plsql/errorPage.ts
3031
+ /**
3032
+ * Get error data
3033
+ * @param req - The req object represents the HTTP request.
3034
+ * @param error - The error.
3035
+ * @returns The output.
3036
+ */
3037
+ const getErrorData = (req, error) => {
3038
+ let timestamp = /* @__PURE__ */ new Date();
3039
+ let message = "";
3040
+ let environment = null;
3041
+ let sql = null;
3042
+ let bind = null;
3043
+ if (error instanceof ProcedureError) {
3044
+ timestamp = error.timestamp;
3045
+ message = error.stack ?? "";
3046
+ environment = error.environment;
3047
+ sql = error.sql;
3048
+ bind = error.bind;
3049
+ } else if (error instanceof RequestError) {
3050
+ timestamp = error.timestamp;
3051
+ message = error.stack ?? "";
3052
+ } else if (error instanceof Error) message = errorToString(error);
3053
+ else if (typeof error === "string") message = `${error}\n`;
3054
+ return {
3055
+ type: "error",
3056
+ timestamp,
3057
+ message,
3058
+ req,
3059
+ environment,
3060
+ sql,
3061
+ bind
3062
+ };
3063
+ };
3064
+ /**
3065
+ * Show an error page
3066
+ *
3067
+ * @param req - The req object represents the HTTP request.
3068
+ * @param res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
3069
+ * @param options - The configuration options.
3070
+ * @param error - The error.
3071
+ */
3072
+ const errorPage = (req, res, options, error) => {
3073
+ const errorData = getErrorData(req, error);
3074
+ const { html, text } = getFormattedMessage(errorData);
3075
+ logToFile(text);
3076
+ const firstLine = errorData.message.split("\n")[0];
3077
+ jsonLogger.log({
3078
+ timestamp: errorData.timestamp?.toISOString() ?? (/* @__PURE__ */ new Date()).toISOString(),
3079
+ type: "error",
3080
+ message: firstLine ?? "",
3081
+ req: {
3082
+ method: req.method,
3083
+ url: req.originalUrl,
3084
+ ip: req.ip ?? "",
3085
+ userAgent: req.get("user-agent") ?? ""
3086
+ },
3087
+ details: {
3088
+ fullMessage: errorData.message,
3089
+ sql: errorData.sql ?? "",
3090
+ bind: errorData.bind,
3091
+ environment: errorData.environment ?? {}
3092
+ }
3093
+ });
3094
+ console.error(text);
3095
+ if (options.errorStyle === "basic") res.status(404).send("Page not found");
3096
+ else res.status(404).send(getHtmlPage(html));
3097
+ };
3098
+
3099
+ //#endregion
3100
+ //#region src/backend/handler/plsql/handlerPlSql.ts
3101
+ const debug$1 = debugModule("webplsql:handlerPlSql");
3102
+ /**
3103
+ * express.Request handler
3104
+ * @param req - The req object represents the HTTP request.
3105
+ * @param res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
3106
+ * @param _next - The next function.
3107
+ * @param connectionPool - The connection pool.
3108
+ * @param options - the options for the middleware.
3109
+ * @param procedureNameCache - The procedure name cache.
3110
+ * @param argumentCache - The argument cache.
3111
+ */
3112
+ const requestHandler = async (req, res, _next, connectionPool, options, procedureNameCache, argumentCache) => {
3113
+ try {
3114
+ let authenticatedUser = null;
3115
+ if (options.auth?.type === "basic") {
3116
+ const b64auth = (req.headers.authorization ?? "").split(" ")[1] ?? "";
3117
+ const [login, password] = Buffer.from(b64auth, "base64").toString().split(":");
3118
+ if (login) authenticatedUser = await options.auth.callback(connectionPool, {
3119
+ username: login,
3120
+ password
3121
+ });
3122
+ if (authenticatedUser === null) {
3123
+ const realm = options.auth.realm ?? "PL/SQL Gateway";
3124
+ res.set("WWW-Authenticate", `Basic realm="${realm}"`);
3125
+ res.status(401).send("Authentication required.");
3126
+ return;
3127
+ }
3128
+ }
3129
+ if (typeof req.params.name !== "string" || req.params.name.length === 0) if (typeof options.defaultPage === "string" && options.defaultPage.length > 0) {
3130
+ const newUrl = url.resolve(`${req.originalUrl}/${options.defaultPage}`, "");
3131
+ debug$1(`Redirect to the url "${newUrl}"`);
3132
+ res.redirect(newUrl);
3133
+ } else errorPage(req, res, options, new RequestError("No procedure name given and no default page has been specified"));
3134
+ else await processRequest(req, res, options, connectionPool, procedureNameCache, argumentCache, authenticatedUser);
3135
+ } catch (err) {
3136
+ errorPage(req, res, options, err);
3137
+ }
3138
+ };
3139
+ /**
3140
+ * Express middleware.
3141
+ *
3142
+ * @param connectionPool - The connection pool.
3143
+ * @param config - The configuration options.
3144
+ * @param adminContext - Optional admin context for self-registration and stats tracking.
3145
+ * @returns The handler.
3146
+ */
3147
+ const handlerWebPlSql = (connectionPool, config, adminContext) => {
3148
+ debug$1("options", config);
3149
+ const procedureNameCache = new Cache();
3150
+ const argumentCache = new Cache();
3151
+ if (adminContext) adminContext.registerHandler(config.route, connectionPool, procedureNameCache, argumentCache);
3152
+ /**
3153
+ * @param req - The request.
3154
+ * @param res - The response.
3155
+ * @param next - The next function.
3156
+ */
3157
+ const handler = ((req, res, next) => {
3158
+ requestHandler(req, res, next, connectionPool, config, procedureNameCache, argumentCache);
3159
+ });
3160
+ handler.procedureNameCache = procedureNameCache;
3161
+ handler.argumentCache = argumentCache;
3162
+ return handler;
3163
+ };
3164
+
3165
+ //#endregion
3166
+ //#region src/backend/handler/handlerLogger.ts
3167
+ const debug = debugModule("webplsql:handlerLogger");
3168
+ /**
3169
+ * Create the upload middleware.
3170
+ * @param filename - Output filename.
3171
+ * @returns Request handler.
3172
+ */
3173
+ const handlerLogger = (filename) => {
3174
+ debug("register");
3175
+ return morgan("combined", { stream: fs.createWriteStream(path.join(process.cwd(), filename), { flags: "a" }) });
3176
+ };
3177
+
3178
+ //#endregion
3179
+ //#region src/backend/handler/handlerUpload.ts
3180
+ /**
3181
+ * Create the upload middleware.
3182
+ * @param uploadFileSizeLimit - Maximum size of each uploaded file in bytes or no limit if omitted.
3183
+ * @returns Request handler.
3184
+ */
3185
+ const handlerUpload = (uploadFileSizeLimit) => {
3186
+ return multer({
3187
+ storage: multer.diskStorage({}),
3188
+ limits: { fileSize: uploadFileSizeLimit }
3189
+ }).any();
3190
+ };
3191
+
3192
+ //#endregion
3193
+ //#region src/backend/handler/handlerAdmin.ts
3194
+ const version = () => getVersion();
3195
+ /**
3196
+ * Helper to read last N lines of a file
3197
+ * @param filePath - Path to file
3198
+ * @param n - Number of lines
3199
+ * @param filter - Optional filter string
3200
+ * @returns The lines
3201
+ */
3202
+ const readLastLines = async (filePath, n = 100, filter = "") => {
3203
+ if (!fs.existsSync(filePath)) return [];
3204
+ const fileStream = fs.createReadStream(filePath);
3205
+ const rl = readline.createInterface({
3206
+ input: fileStream,
3207
+ crlfDelay: Infinity
3208
+ });
3209
+ const filterLower = filter.toLowerCase();
3210
+ const lines = [];
3211
+ for await (const line of rl) if (!filter || line.toLowerCase().includes(filterLower)) {
3212
+ lines.push(line);
3213
+ if (lines.length > n) lines.shift();
3214
+ }
3215
+ return lines;
3216
+ };
3217
+ /**
3218
+ * Create admin API router
3219
+ * @param adminContext - The admin context
3220
+ * @returns Express router
3221
+ */
3222
+ const createAdminRouter = (adminContext) => {
3223
+ const router = express.Router();
3224
+ router.get("/api/status", (req, res) => {
3225
+ const uptime = ((/* @__PURE__ */ new Date()).getTime() - adminContext.startTime.getTime()) / 1e3;
3226
+ const includeHistory = req.query.history === "true";
3227
+ const includeConfig = req.query.config === "true";
3228
+ const poolStats = adminContext.pools.map((pool, index) => {
3229
+ const cache = adminContext.caches[index];
3230
+ const name = cache?.poolName ?? `pool-${index}`;
3231
+ const p = pool;
3232
+ const stats = typeof p.getStatistics === "function" ? p.getStatistics() : null;
3233
+ const procStats = cache?.procedureNameCache.getStats();
3234
+ const argStats = cache?.argumentCache.getStats();
3235
+ return {
3236
+ name,
3237
+ stats,
3238
+ connectionsOpen: pool.connectionsOpen,
3239
+ connectionsInUse: pool.connectionsInUse,
3240
+ cache: {
3241
+ procedureName: {
3242
+ size: cache?.procedureNameCache.keys().length ?? 0,
3243
+ hits: procStats?.hits ?? 0,
3244
+ misses: procStats?.misses ?? 0
3245
+ },
3246
+ argument: {
3247
+ size: cache?.argumentCache.keys().length ?? 0,
3248
+ hits: argStats?.hits ?? 0,
3249
+ misses: argStats?.misses ?? 0
3250
+ }
3251
+ }
3252
+ };
3253
+ });
3254
+ const memUsage = process.memoryUsage();
3255
+ const systemMemoryUsed = os.totalmem() - os.freemem();
3256
+ const cpuUsage = process.cpuUsage();
3257
+ const summary = adminContext.statsManager.getSummary();
3258
+ const history = adminContext.statsManager.getHistory();
3259
+ const historyData = includeHistory ? history : history.slice(-1);
3260
+ res.json({
3261
+ version: version(),
3262
+ status: adminContext.paused ? "paused" : "running",
3263
+ uptime,
3264
+ startTime: adminContext.startTime,
3265
+ intervalMs: adminContext.statsManager.config.intervalMs,
3266
+ metrics: {
3267
+ requestCount: summary.totalRequests,
3268
+ errorCount: summary.totalErrors,
3269
+ avgResponseTime: summary.avgResponseTime,
3270
+ minResponseTime: summary.minResponseTime,
3271
+ maxResponseTime: summary.maxResponseTime,
3272
+ maxRequestsPerSecond: summary.maxRequestsPerSecond
3273
+ },
3274
+ history: historyData,
3275
+ pools: poolStats,
3276
+ system: {
3277
+ nodeVersion: process.version,
3278
+ platform: process.platform,
3279
+ arch: process.arch,
3280
+ cpuCores: os.cpus().length,
3281
+ memory: {
3282
+ rss: systemMemoryUsed,
3283
+ heapTotal: memUsage.heapTotal,
3284
+ heapUsed: memUsage.heapUsed,
3285
+ external: memUsage.external,
3286
+ totalMemory: os.totalmem(),
3287
+ ...summary.maxMemory
3288
+ },
3289
+ cpu: {
3290
+ user: cpuUsage.user,
3291
+ system: cpuUsage.system,
3292
+ max: summary.cpu.max,
3293
+ userMax: summary.cpu.userMax,
3294
+ systemMax: summary.cpu.systemMax
3295
+ }
3296
+ },
3297
+ config: includeConfig && adminContext.config ? {
3298
+ ...adminContext.config,
3299
+ adminPassword: adminContext.config.adminPassword ? "********" : void 0,
3300
+ routePlSql: adminContext.config.routePlSql.map((p) => {
3301
+ const { auth, transactionMode, cgi, ...rest } = p;
3302
+ return {
3303
+ ...rest,
3304
+ password: "********",
3305
+ hasAuth: !!auth,
3306
+ hasTransactionMode: !!transactionMode,
3307
+ hasCgi: !!cgi
3308
+ };
3309
+ })
3310
+ } : void 0
3311
+ });
3312
+ });
3313
+ router.get("/api/stats/history", (req, res) => {
3314
+ const limitQuery = req.query.limit;
3315
+ let limit = 100;
3316
+ if (typeof limitQuery === "string") {
3317
+ const parsed = Number(limitQuery);
3318
+ if (!isNaN(parsed)) limit = parsed;
3319
+ }
3320
+ const history = adminContext.statsManager.getHistory();
3321
+ const slice = limit > 0 ? history.slice(-limit) : [...history];
3322
+ res.json(slice.reverse());
3323
+ });
3324
+ router.get("/api/logs/error", async (req, res) => {
3325
+ try {
3326
+ const parsedLines = (await readLastLines("error.json.log", Number(req.query.limit) || 100, typeof req.query.filter === "string" ? req.query.filter : "")).map((line) => {
3327
+ try {
3328
+ return JSON.parse(line);
3329
+ } catch {
3330
+ return null;
3331
+ }
3332
+ }).filter((l) => l !== null);
3333
+ const logs = z.array(logEntrySchema).safeParse(parsedLines);
3334
+ if (!logs.success) throw new Error(`Validation failed: ${logs.error.message}`);
3335
+ return res.json(logs.data.reverse());
3336
+ } catch (err) {
3337
+ return res.status(500).json({ error: String(err) });
3338
+ }
3339
+ });
3340
+ router.get("/api/logs/access", async (req, res) => {
3341
+ try {
3342
+ const limit = Number(req.query.limit) || 100;
3343
+ const filter = typeof req.query.filter === "string" ? req.query.filter : "";
3344
+ const logFile = adminContext.config?.loggerFilename ?? "access.log";
3345
+ if (!adminContext.config?.loggerFilename) {
3346
+ res.json({ message: "Access logging not enabled" });
3347
+ return;
3348
+ }
3349
+ const lines = await readLastLines(logFile, limit, filter);
3350
+ res.json(lines.reverse());
3351
+ } catch (err) {
3352
+ res.status(500).json({ error: String(err) });
3353
+ }
3354
+ });
3355
+ router.post("/api/cache/clear", (req, res) => {
3356
+ const body = req.body;
3357
+ const poolName = typeof body?.poolName === "string" ? body.poolName : void 0;
3358
+ let cleared = 0;
3359
+ adminContext.caches.forEach((c) => {
3360
+ if (!poolName || c.poolName === poolName) {
3361
+ c.procedureNameCache.clear();
3362
+ cleared++;
3363
+ c.argumentCache.clear();
3364
+ cleared++;
3365
+ }
3366
+ });
3367
+ res.json({ message: `Cleared ${cleared} caches` });
3368
+ });
3369
+ 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" });
3389
+ });
3390
+ router.get("/api/trace/status", (_req, res) => {
3391
+ res.json({ enabled: traceManager.isEnabled() });
3392
+ });
3393
+ router.post("/api/trace/toggle", (req, res) => {
3394
+ const body = req.body;
3395
+ const enabled = typeof body?.enabled === "boolean" ? body.enabled : false;
3396
+ traceManager.setEnabled(enabled);
3397
+ res.json({ enabled: traceManager.isEnabled() });
3398
+ });
3399
+ router.get("/api/trace/logs", async (req, res) => {
3400
+ try {
3401
+ const limit = Number(req.query.limit) || 100;
3402
+ const filter = typeof req.query.filter === "string" ? req.query.filter : "";
3403
+ const parsedLines = (await readLastLines(traceManager.getFilePath(), limit, filter)).map((line) => {
3404
+ try {
3405
+ return JSON.parse(line);
3406
+ } catch {
3407
+ return null;
3408
+ }
3409
+ }).filter((l) => l !== null);
3410
+ const logs = z.array(procedureTraceEntrySchema).safeParse(parsedLines);
3411
+ if (!logs.success) throw new Error(`Validation failed: ${logs.error.message}`);
3412
+ return res.json(logs.data.reverse());
3413
+ } catch (err) {
3414
+ return res.status(500).json({ error: String(err) });
3415
+ }
3416
+ });
3417
+ router.post("/api/trace/clear", (_req, res) => {
3418
+ traceManager.clear();
3419
+ res.json({ message: "Traces cleared" });
3420
+ });
3421
+ return router;
3422
+ };
3423
+
3424
+ //#endregion
3425
+ //#region src/backend/handler/handlerAdminConsole.ts
3426
+ /**
3427
+ * Resolves the admin console static directory by walking up from the current module
3428
+ * to find the project root (identified by package.json), then returns dist/frontend path.
3429
+ * @returns Path to dist/frontend directory
3430
+ * @throws {Error} if project root cannot be found
3431
+ */
3432
+ const resolveAdminStaticDir = () => {
3433
+ const __filename = fileURLToPath(import.meta.url);
3434
+ let projectRoot = path.dirname(__filename);
3435
+ while (!existsSync(path.join(projectRoot, "package.json")) && projectRoot !== "/") projectRoot = path.dirname(projectRoot);
3436
+ if (projectRoot === "/") throw new Error("Could not find project root (package.json). Please provide explicit staticDir in AdminConsoleConfig.");
3437
+ return path.join(projectRoot, "dist", "frontend");
3438
+ };
3439
+ /**
3440
+ * Creates the admin console middleware.
3441
+ * @param config - The admin console configuration.
3442
+ * @param adminContext - The admin context.
3443
+ * @returns The express request handler.
3444
+ */
3445
+ const handlerAdminConsole = (config, adminContext) => {
3446
+ const adminRoute = config.adminRoute ?? "/admin";
3447
+ const resolvedStaticDir = config.staticDir ?? resolveAdminStaticDir();
3448
+ if (adminRoute && !adminRoute.startsWith("/")) throw new Error("adminRoute must start with /");
3449
+ 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
3471
+ }
3472
+ }
3473
+ };
3474
+ }));
3475
+ };
3476
+ const router = Router();
3477
+ router.use((req, res, next) => {
3478
+ if (adminContext.paused && !req.path.startsWith(adminRoute)) {
3479
+ res.status(503).send("Server Paused");
3480
+ return;
3481
+ }
3482
+ next();
3483
+ });
3484
+ router.use(adminRoute, (req, res, next) => {
3485
+ const baseUrl = req.baseUrl || "";
3486
+ const [path] = req.originalUrl.split("?");
3487
+ if (path === baseUrl) {
3488
+ const query = req.originalUrl.split("?")[1];
3489
+ return res.redirect(baseUrl + "/" + (query ? "?" + query : ""));
3490
+ }
3491
+ next();
3492
+ });
3493
+ if (config.user && config.password) router.use(adminRoute, (req, res, next) => {
3494
+ const b64auth = (req.headers.authorization ?? "").split(" ")[1] ?? "";
3495
+ const [login, password] = Buffer.from(b64auth, "base64").toString().split(":");
3496
+ if (login !== config.user || password !== config.password) {
3497
+ res.set("WWW-Authenticate", "Basic realm=\"Admin Console\"");
3498
+ res.status(401).send("Authentication required.");
3499
+ return;
3500
+ }
3501
+ next();
3502
+ });
3503
+ router.use(adminRoute, createAdminRouter(adminContext));
3504
+ if (existsSync(resolvedStaticDir)) router.use(adminRoute, expressStaticGzip(resolvedStaticDir, {
3505
+ enableBrotli: true,
3506
+ orderPreference: ["br"]
3507
+ }));
3508
+ return router;
3509
+ };
3510
+
3511
+ //#endregion
3512
+ //#region src/backend/handler/handlerSpaFallback.ts
3513
+ /**
3514
+ * Creates middleware that serves index.html for SPA routes.
3515
+ *
3516
+ * Handles React Router (createBrowserRouter), Vue Router, Angular Router, etc.
3517
+ *
3518
+ * **CRITICAL**: This middleware MUST be mounted AFTER express-static-gzip.
3519
+ *
3520
+ * Middleware order:
3521
+ * 1. express-static-gzip → Serves actual files (CSS, JS, images)
3522
+ * 2. handlerSpaFallback → Serves index.html for navigation requests
3523
+ *
3524
+ * @param directoryPath - Path to the static directory containing index.html
3525
+ * @param _route - The route prefix (for debugging)
3526
+ * @returns Express request handler
3527
+ *
3528
+ * @example
3529
+ * ```typescript
3530
+ * app.use('/app', expressStaticGzip('./build'));
3531
+ * app.use('/app', createSpaFallback('./build', '/app'));
3532
+ * ```
3533
+ */
3534
+ const createSpaFallback = (directoryPath, _route) => {
3535
+ const indexPath = path.join(directoryPath, "index.html");
3536
+ return (req, res, next) => {
3537
+ if (req.method !== "GET" && req.method !== "HEAD") return next();
3538
+ const accept = req.headers.accept ?? "";
3539
+ if (accept && !accept.includes("text/html") && !accept.includes("*/*")) return next();
3540
+ res.sendFile(indexPath, (err) => {
3541
+ if (err) next(err);
3542
+ });
3543
+ };
3544
+ };
3545
+
3546
+ //#endregion
3547
+ export { AdminContext, createAdminRouter, createServer, createSpaFallback, forceShutdown, getJsonFile, getVersion, handlerAdminConsole, handlerLogger, handlerUpload, handlerWebPlSql, installShutdown, isDirectory, isFile, loadConfig, oracledb_provider_exports as oracledb, readFile, readFileSyncUtf8, removeFile, showConfig, startServer, startServerConfig, z$configType };
3548
+ //# sourceMappingURL=index.mjs.map