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.
- package/README.md +108 -13
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/frontend/assets/main-0oxRTIkL.js +147 -0
- package/dist/frontend/assets/main-0oxRTIkL.js.br +0 -0
- package/dist/frontend/assets/main-0oxRTIkL.js.gz +0 -0
- package/dist/frontend/assets/main-TYgLak6O.css +1 -0
- package/dist/frontend/assets/main-TYgLak6O.css.br +0 -0
- package/dist/frontend/assets/main-TYgLak6O.css.gz +0 -0
- package/dist/frontend/index.html +516 -0
- package/dist/frontend/index.html.br +0 -0
- package/dist/frontend/index.html.gz +0 -0
- package/dist/index.d.mts +646 -0
- package/dist/index.mjs +3548 -0
- package/dist/index.mjs.map +1 -0
- package/dist/oracledb-mock-Dn8aHtd7.mjs +68 -0
- package/dist/oracledb-mock-Dn8aHtd7.mjs.map +1 -0
- package/examples/config-apex.js +1 -1
- package/examples/config-docker.js +1 -2
- package/package.json +45 -32
- package/examples/config-native.js +0 -68
- package/src/admin/client/charts.ts +0 -299
- package/src/admin/client/main.js +0 -3
- package/src/admin/client/tailwind.css +0 -41
- package/src/admin/index.html +0 -315
- package/src/admin/js/api.ts +0 -95
- package/src/admin/js/app.ts +0 -306
- package/src/admin/js/eslint.config.js +0 -74
- package/src/admin/js/schemas.ts +0 -153
- package/src/admin/js/templates/config.ts +0 -146
- package/src/admin/js/templates/errorRow.ts +0 -18
- package/src/admin/js/templates/index.ts +0 -3
- package/src/admin/js/templates/poolCard.ts +0 -61
- package/src/admin/js/tsconfig.json +0 -24
- package/src/admin/js/types.ts +0 -223
- package/src/admin/js/ui/theme.ts +0 -93
- package/src/admin/js/ui/views.ts +0 -164
- package/src/admin/js/util/format.ts +0 -27
- package/src/admin/lib/assets/main-zpdhQ1gD.css +0 -1
- package/src/admin/lib/chart.bundle.js +0 -139
- package/src/admin/style.css +0 -1321
- package/src/bin/load-test.js +0 -202
- package/src/handler/handlerAdmin.js +0 -198
- package/src/handler/handlerLogger.js +0 -21
- package/src/handler/handlerMetrics.js +0 -68
- package/src/handler/handlerUpload.js +0 -26
- package/src/handler/plsql/cgi.js +0 -131
- package/src/handler/plsql/errorPage.js +0 -117
- package/src/handler/plsql/handlerPlSql.js +0 -90
- package/src/handler/plsql/parsePage.js +0 -213
- package/src/handler/plsql/procedure.js +0 -356
- package/src/handler/plsql/procedureError.js +0 -40
- package/src/handler/plsql/procedureNamed.js +0 -268
- package/src/handler/plsql/procedureSanitize.js +0 -262
- package/src/handler/plsql/procedureVariable.js +0 -54
- package/src/handler/plsql/request.js +0 -124
- package/src/handler/plsql/requestError.js +0 -23
- package/src/handler/plsql/sendResponse.js +0 -136
- package/src/handler/plsql/stream.js +0 -28
- package/src/handler/plsql/upload.js +0 -109
- package/src/index.js +0 -20
- package/src/server/config.js +0 -57
- package/src/server/connections.js +0 -40
- package/src/server/server.js +0 -289
- package/src/types.js +0 -150
- package/src/util/cache.js +0 -123
- package/src/util/errorToString.js +0 -24
- package/src/util/file.js +0 -91
- package/src/util/html.js +0 -53
- package/src/util/jsonLogger.js +0 -47
- package/src/util/oracle.js +0 -80
- package/src/util/shutdown.js +0 -44
- package/src/util/trace.js +0 -325
- package/src/util/type.js +0 -6
- package/src/util/util.js +0 -66
- package/src/version.js +0 -5
- package/types/handler/handlerAdmin.d.ts +0 -9
- package/types/handler/handlerLogger.d.ts +0 -2
- package/types/handler/handlerMetrics.d.ts +0 -25
- package/types/handler/handlerUpload.d.ts +0 -7
- package/types/handler/plsql/cgi.d.ts +0 -4
- package/types/handler/plsql/errorPage.d.ts +0 -11
- package/types/handler/plsql/handlerPlSql.d.ts +0 -13
- package/types/handler/plsql/parsePage.d.ts +0 -3
- package/types/handler/plsql/procedure.d.ts +0 -12
- package/types/handler/plsql/procedureError.d.ts +0 -23
- package/types/handler/plsql/procedureNamed.d.ts +0 -14
- package/types/handler/plsql/procedureSanitize.d.ts +0 -11
- package/types/handler/plsql/procedureVariable.d.ts +0 -10
- package/types/handler/plsql/request.d.ts +0 -9
- package/types/handler/plsql/requestError.d.ts +0 -8
- package/types/handler/plsql/sendResponse.d.ts +0 -6
- package/types/handler/plsql/upload.d.ts +0 -5
- package/types/index.d.ts +0 -11
- package/types/server/config.d.ts +0 -2
- package/types/server/server.d.ts +0 -74
- package/types/types.d.ts +0 -328
- package/types/util/cache.d.ts +0 -69
- package/types/util/errorToString.d.ts +0 -1
- package/types/util/file.d.ts +0 -6
- package/types/util/html.d.ts +0 -3
- package/types/util/jsonLogger.d.ts +0 -45
- package/types/util/oracle.d.ts +0 -5
- package/types/util/shutdown.d.ts +0 -2
- package/types/util/trace.d.ts +0 -22
- package/types/util/type.d.ts +0 -1
- package/types/util/util.d.ts +0 -3
- package/types/version.d.ts +0 -1
- /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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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, " ");
|
|
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
|