web_plsql
Version:
The Express Middleware for Oracle PL/SQL
1,684 lines (1,663 loc) • 113 kB
JavaScript
import { t as __exportAll } from "./chunk-DQk6qfdC.mjs";
import { n as setExecuteCallback } from "./oracledb-mock-Dn8aHtd7.mjs";
import z$1, { z } from "zod";
import oracledb from "oracledb";
import debugModule from "debug";
import http from "node:http";
import https from "node:https";
import express, { Router } from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import compression from "compression";
import expressStaticGzip from "express-static-gzip";
import os from "node:os";
import url, { URL, fileURLToPath } from "node:url";
import util from "node:util";
import stream, { Readable } from "node:stream";
import fs, { existsSync, promises, readFileSync } from "node:fs";
import * as rotatingFileStream from "rotating-file-stream";
import path from "node:path";
import morgan from "morgan";
import multer from "multer";
import readline from "node:readline";
//#region src/common/configStaticSchema.ts
/**
* Configuration for serving static files
*/
const configStaticSchema = z$1.strictObject({
route: z$1.string(),
directoryPath: z$1.string(),
spaFallback: z$1.boolean().optional()
});
//#endregion
//#region src/common/procedureTraceEntry.ts
const procedureTraceEntrySchema = z.strictObject({
id: z.string(),
timestamp: z.string(),
source: z.string(),
url: z.string(),
method: z.string(),
status: z.string(),
duration: z.number(),
procedure: z.string().optional(),
parameters: z.union([z.record(z.string(), z.unknown()), z.array(z.unknown())]).optional(),
uploads: z.array(z.strictObject({
originalname: z.string(),
mimetype: z.string(),
size: z.number()
})).optional(),
downloads: z.strictObject({
fileType: z.string(),
fileSize: z.number()
}).optional(),
html: z.string().optional(),
cookies: z.record(z.string(), z.string()).optional(),
headers: z.record(z.string(), z.string()).optional(),
cgi: z.record(z.string(), z.string()).optional(),
error: z.string().optional()
});
//#endregion
//#region src/common/logEntrySchema.ts
/**
* Error log entry schema.
*/
const logEntryTypeSchema = z.union([
z.literal("error"),
z.literal("info"),
z.literal("warning")
]);
const logEntrySchema = z.strictObject({
timestamp: z.string(),
type: logEntryTypeSchema,
message: z.string(),
req: z.strictObject({
method: z.string().optional(),
url: z.string().optional(),
ip: z.string().optional(),
userAgent: z.string().optional()
}).optional(),
details: z.strictObject({
fullMessage: z.string().optional(),
sql: z.string().optional(),
bind: z.unknown().optional(),
environment: z.record(z.string(), z.string()).optional()
}).optional()
});
//#endregion
//#region src/backend/types.ts
/**
* Defines the style of error reporting
* 'basic': standard error messages
* 'debug': detailed error messages including database context
*/
const z$errorStyleType = z$1.enum(["basic", "debug"]);
/**
* Defines how transactions are handled after procedure execution
* 'commit': automatically commit
* 'rollback': automatically rollback
* callback: custom function for manual handling
*/
const transactionModeSchema = z$1.union([
z$1.custom((val) => typeof val === "function", { message: "Invalid transaction callback" }),
z$1.literal("commit"),
z$1.literal("rollback"),
z$1.undefined(),
z$1.null()
]);
/**
* Authentication configuration for a PL/SQL route
*/
const z$authSchema = z$1.strictObject({
type: z$1.literal("basic"),
callback: z$1.custom((val) => typeof val === "function", { message: "Invalid auth callback" }),
realm: z$1.string().optional()
});
/**
* PL/SQL handler behavior configuration
*/
const z$configPlSqlHandlerType = z$1.strictObject({
defaultPage: z$1.string(),
pathAlias: z$1.string().optional(),
pathAliasProcedure: z$1.string().optional(),
documentTable: z$1.string(),
exclusionList: z$1.array(z$1.string()).optional(),
requestValidationFunction: z$1.string().optional(),
transactionMode: transactionModeSchema.optional(),
errorStyle: z$errorStyleType,
cgi: z$1.record(z$1.string(), z$1.string()).optional(),
auth: z$authSchema.optional()
});
/**
* Database connection configuration for a PL/SQL route
*/
const z$configPlSqlConfigType = z$1.strictObject({
route: z$1.string(),
user: z$1.string(),
password: z$1.string(),
connectString: z$1.string()
});
const z$configPlSqlType = z$1.strictObject({
...z$configPlSqlHandlerType.shape,
...z$configPlSqlConfigType.shape
});
/**
* Root application configuration
*/
const z$configType = z$1.strictObject({
port: z$1.number(),
routeStatic: z$1.array(configStaticSchema),
routePlSql: z$1.array(z$configPlSqlType),
uploadFileSizeLimit: z$1.number().optional(),
loggerFilename: z$1.string(),
adminRoute: z$1.string().optional(),
adminUser: z$1.string().optional(),
adminPassword: z$1.string().optional(),
devMode: z$1.boolean().optional()
});
//#endregion
//#region src/backend/util/oracledb-provider.ts
var oracledb_provider_exports = /* @__PURE__ */ __exportAll({
BIND_IN: () => BIND_IN,
BIND_INOUT: () => BIND_INOUT,
BIND_OUT: () => BIND_OUT,
BLOB: () => BLOB,
BUFFER: () => BUFFER,
CLOB: () => CLOB,
CURSOR: () => CURSOR,
DATE: () => DATE,
DB_TYPE_CLOB: () => DB_TYPE_CLOB,
DB_TYPE_DATE: () => DB_TYPE_DATE,
DB_TYPE_NUMBER: () => DB_TYPE_NUMBER,
DB_TYPE_VARCHAR: () => DB_TYPE_VARCHAR,
NUMBER: () => NUMBER,
STRING: () => STRING,
createPool: () => createPool,
setExecuteCallback: () => setExecuteCallback
});
const USE_MOCK = process.env.MOCK_ORACLE === "true";
/**
* Create a database pool.
* @param config - The pool attributes.
* @returns The pool.
*/
async function createPool(config) {
if (USE_MOCK) return (await import("./oracledb-mock-Dn8aHtd7.mjs").then((n) => n.t)).createPool(config);
return await oracledb.createPool(config);
}
const BIND_IN = oracledb.BIND_IN;
const BIND_OUT = oracledb.BIND_OUT;
const BIND_INOUT = oracledb.BIND_INOUT;
const STRING = oracledb.STRING;
const NUMBER = oracledb.NUMBER;
const DATE = oracledb.DATE;
const CURSOR = oracledb.CURSOR;
const BUFFER = oracledb.BUFFER;
const CLOB = oracledb.CLOB;
const BLOB = oracledb.BLOB;
const DB_TYPE_VARCHAR = oracledb.DB_TYPE_VARCHAR;
const DB_TYPE_CLOB = oracledb.DB_TYPE_CLOB;
const DB_TYPE_NUMBER = oracledb.DB_TYPE_NUMBER;
const DB_TYPE_DATE = oracledb.DB_TYPE_DATE;
//#endregion
//#region src/backend/version.ts
globalThis.__VERSION__ ??= "**development**";
/**
* Returns the current library version
* @returns {string} - Version.
*/
const getVersion = () => "1.3.1";
//#endregion
//#region src/backend/server/config.ts
const paddedLine = (title, value) => {
console.log(`${(title + ":").padEnd(30)} ${value}`);
};
/**
* Show configuration.
* @param config - The config.
*/
const showConfig = (config) => {
const LINE = "-".repeat(80);
console.log(LINE);
console.log(`NODE PL/SQL SERVER version ${getVersion()}`);
console.log(LINE);
paddedLine("Server port", config.port);
paddedLine("Admin route", `${config.adminRoute ?? "/admin"}${config.adminUser ? " (authenticated)" : ""}`);
paddedLine("Access log", config.loggerFilename.length > 0 ? config.loggerFilename : "-");
paddedLine("Upload file size limit", typeof config.uploadFileSizeLimit === "number" ? `${config.uploadFileSizeLimit} bytes` : "-");
if (config.routeStatic.length > 0) config.routeStatic.forEach((e) => {
paddedLine("Static route", e.route);
paddedLine("Directory path", e.directoryPath);
});
if (config.routePlSql.length > 0) config.routePlSql.forEach((e) => {
let transactionMode = "";
if (typeof e.transactionMode === "string") transactionMode = e.transactionMode;
else if (typeof e.transactionMode === "function") transactionMode = "custom callback";
paddedLine("Route", `http://localhost:${config.port}${e.route}`);
paddedLine("Oracle user", e.user);
paddedLine("Oracle server", e.connectString);
paddedLine("Oracle document table", e.documentTable);
paddedLine("Default page", e.defaultPage.length > 0 ? e.defaultPage : "-");
paddedLine("Path alias", e.pathAlias ?? "-");
paddedLine("Path alias procedure", e.pathAliasProcedure ?? "-");
paddedLine("Exclution list", e.exclusionList ? e.exclusionList.join(", ") : "-");
paddedLine("Validation function", e.requestValidationFunction ?? "-");
paddedLine("After request handler", transactionMode.length > 0 ? transactionMode : "-");
paddedLine("Error style", e.errorStyle);
});
console.log(LINE);
const baseUrl = `http://localhost:${config.port}`;
paddedLine("🏠 Admin Console", `${baseUrl}${config.adminRoute ?? "/admin"}`);
if (config.routePlSql.length > 0) {
console.log("");
console.log("⚙️ PL/SQL Gateways:");
config.routePlSql.forEach((e) => {
console.log(` ${e.route.padEnd(28)} ${baseUrl}${e.route}`);
});
}
console.log(LINE);
};
//#endregion
//#region src/backend/server/server.ts
const debug$14 = debugModule("webplsql:server");
/**
* Close multiple pools.
* @param pools - The pools to close.
*/
const poolsClose = async (pools) => {
await Promise.all(pools.map((pool) => pool.close(0)));
};
/**
* Create HTTPS server.
* @param app - express application
* @param ssl - ssl configuration.
* @returns server
*/
const createServer = (app, ssl) => {
if (ssl) {
const key = readFileSyncUtf8(ssl.keyFilename);
const cert = readFileSyncUtf8(ssl.certFilename);
return https.createServer({
key,
cert
}, app);
} else return http.createServer(app);
};
/**
* Start server.
* @param config - The config.
* @param ssl - ssl configuration.
* @returns Promise resolving to the web server object.
*/
const startServer = async (config, ssl) => {
debug$14("startServer: BEGIN", config, ssl);
const internalConfig = z$configType.parse(config);
showConfig(internalConfig);
const app = express();
if (internalConfig.devMode) app.use(cors({
origin: "http://localhost:5173",
credentials: true
}));
app.use(handlerUpload(internalConfig.uploadFileSizeLimit));
app.use(express.json({ limit: "50mb" }));
app.use(express.urlencoded({
limit: "50mb",
extended: true
}));
app.use(cookieParser());
app.use(compression());
const adminContext = new AdminContext(internalConfig);
app.use(handlerAdminConsole({
adminRoute: internalConfig.adminRoute,
user: internalConfig.adminUser,
password: internalConfig.adminPassword,
devMode: internalConfig.devMode
}, adminContext));
for (const i of internalConfig.routePlSql) {
const handler = handlerWebPlSql(await createPool({
user: i.user,
password: i.password,
connectString: i.connectString
}), i, adminContext);
app.use([`${i.route}/:name`, i.route], (req, res, next) => {
const start = process.hrtime();
res.on("finish", () => {
const diff = process.hrtime(start);
const duration = diff[0] * 1e3 + diff[1] / 1e6;
adminContext.statsManager.recordRequest(duration, res.statusCode >= 400);
});
handler(req, res, next);
});
}
if (internalConfig.loggerFilename.length > 0) app.use(handlerLogger(internalConfig.loggerFilename));
for (const i of internalConfig.routeStatic) {
app.use(i.route, expressStaticGzip(i.directoryPath, {
enableBrotli: true,
orderPreference: ["br"]
}));
if (i.spaFallback) app.use(i.route, createSpaFallback(i.directoryPath, i.route));
}
debug$14("startServer: createServer");
const server = createServer(app, ssl);
const connections = /* @__PURE__ */ new Set();
server.on("connection", (socket) => {
connections.add(socket);
socket.on("close", () => {
connections.delete(socket);
});
});
const closeAllConnections = () => {
for (const socket of connections) {
socket.destroy();
connections.delete(socket);
}
};
const shutdown = async () => {
debug$14("startServer: onShutdown");
adminContext.statsManager.stop();
await poolsClose(adminContext.pools);
server.close(() => {
console.log("Server has closed");
process.exit(0);
});
closeAllConnections();
};
installShutdown(shutdown);
debug$14("startServer: start listener");
await new Promise((resolve, reject) => {
server.listen(internalConfig.port).on("listening", () => {
debug$14("startServer: listener running");
resolve();
}).on("error", (err) => {
if ("code" in err) {
if (err.code === "EADDRINUSE") err.message = `Port ${internalConfig.port} is already in use`;
else if (err.code === "EACCES") err.message = `Port ${internalConfig.port} requires elevated privileges`;
}
reject(err);
});
});
debug$14("startServer: END");
return {
config: internalConfig,
connectionPools: adminContext.pools,
app,
server,
adminContext,
shutdown
};
};
/**
* Load configuration.
* @param filename - The configuration filename.
* @returns Promise.
*/
const loadConfig = (filename = "config.json") => z$configType.parse(getJsonFile(filename));
/**
* Start server from config file.
* @param filename - The configuration filename.
* @param ssl - ssl configuration.
* @returns Promise resolving to the web server object.
*/
const startServerConfig = async (filename = "config.json", ssl) => startServer(loadConfig(filename), ssl);
//#endregion
//#region src/backend/util/shutdown.ts
const debug$13 = debugModule("webplsql:shutdown");
/**
* Install a shutdown handler.
* @param handler - Shutdown handler
*/
const installShutdown = (handler) => {
debug$13("installShutdown");
let isShuttingDown = false;
process.on("unhandledRejection", (reason) => {
if (isShuttingDown) return;
isShuttingDown = true;
if (reason instanceof Error) console.error(`\n${reason.message}. Graceful shutdown...`);
else console.error("\nUnhandled promise rejection. Graceful shutdown...", reason);
handler().catch((err) => {
console.error("Error during shutdown:", err);
process.exit(1);
});
});
process.on("SIGTERM", function sigterm() {
if (isShuttingDown) return;
isShuttingDown = true;
console.log("\nGot SIGTERM (aka docker container stop). Graceful shutdown...");
handler().catch((err) => {
console.error("Error during shutdown:", err);
process.exit(1);
});
});
process.on("SIGINT", function sigint() {
if (isShuttingDown) return;
isShuttingDown = true;
console.log("\nGot SIGINT (aka ctrl-c in docker). Graceful shutdown...");
handler().catch((err) => {
console.error("Error during shutdown:", err);
process.exit(1);
});
});
};
/**
* Force a shutdown.
*/
const forceShutdown = () => {
debug$13("forceShutdown");
process.kill(process.pid, "SIGTERM");
};
//#endregion
//#region src/common/constants.ts
/**
* Web PL/SQL Gateway - Common Shared Constants
*
* This file centralizes all hardcoded numeric and string constants used throughout
* the application. Constants are organized by functional category.
*/
/**
* DEFAULT_CACHE_MAX_SIZE = 10000
*
* Purpose: Maximum number of entries in the generic LFU (Least Frequently Used) cache.
*
* Used By:
* - procedureNameCache: Caches resolved Oracle procedure names (e.g., "HR.EMPLOYEES")
* - argumentCache: Caches procedure argument introspection results from all_arguments view
*
* Related Values:
* - CACHE_PRUNE_PERCENT (0.1): When cache is full, removes 10% = 1000 entries
* - Cache instantiation in src/handler/plsql/handlerPlSql.js creates new Cache() without params
*
* Implications:
* - Memory footprint: ~1-2MB at max capacity (strings + hitCount metadata)
* - Pruning: Removes least-frequently-used entries when full
* - Higher values = better cache hit rates but more memory
*/
const DEFAULT_CACHE_MAX_SIZE = 1e4;
/**
* CACHE_PRUNE_PERCENT = 0.1
*
* Purpose: Fraction of cache entries to remove during pruning (10%).
*
* Used By: Cache.prune() method only
*
* Related Values:
* - DEFAULT_CACHE_MAX_SIZE (10000): Applied to this value to calculate removeCount = 1000
*
* Implications:
* - Balances between removing too few entries (frequent pruning) vs too many (evicting useful data)
* - 10% is a common pattern for cache eviction
*/
const CACHE_PRUNE_PERCENT = .1;
/**
* MAX_PROCEDURE_PARAMETERS = 1000
*
* Purpose: Maximum number of procedure arguments that can be introspected from Oracle's
* all_arguments view using BULK COLLECT with dbms_utility.name_resolve.
*
* Used By:
* - src/handler/plsql/procedureNamed.js
* bind.names = {maxArraySize: MAX_PARAMETER_NUMBER}
* bind.types = {maxArraySize: MAX_PARAMETER_NUMBER}
*
* Related Values:
* - Procedure introspection SQL: SQL_GET_ARGUMENT block at procedureNamed.js:27-43
* - oracledb.BIND_OUT direction for array fetches
*
* Implications:
* - This is an Oracle driver limitation for array binding, not an arbitrary choice
* - Procedures with >1000 arguments will have introspection truncated
* - No error handling exists for this edge case
* - Practical limit: most procedures have <50 arguments
*/
const MAX_PROCEDURE_PARAMETERS = 1e3;
/**
* OWA_STREAM_CHUNK_SIZE = 1000
*
* Purpose: Number of lines fetched per Oracle OWA call when streaming page content.
*
* Used By:
* - owaPageStream.js: maxArraySize for :lines bind variable
* - owaPageStream.js: :irows INOUT parameter value
* - owaPageStream.js: Determines when streaming is complete (lines.length < chunkSize)
*
* Related Values:
* - OWA_GET_PAGE_SQL: 'BEGIN owa.get_page(thepage=>:lines, irows=>:irows); END;'
* - OWAPageStream class constructor at line 20
*
* Implications:
* - Controls round-trip frequency to Oracle database
* - Higher = fewer round-trips but larger memory buffers per fetch
* - Lower = more responsive streaming but more database calls
* - Each line is a PL/SQL varchar2; total data per chunk depends on htp.htbuf_len (63 chars)
* - Estimated max data per chunk: 1000 lines × 63 chars = 63KB
*/
const OWA_STREAM_CHUNK_SIZE = 1e3;
/**
* OWA_STREAM_BUFFER_SIZE = 16384
*
* Purpose: Node.js Readable stream highWaterMark in bytes (16KB).
*
* Used By: OWAPageStream class extends Readable stream
*
* Related Values:
* - OWA_STREAM_CHUNK_SIZE (1000): Lines per fetch
* - Default Node.js highWaterMark is 64KB (Readable stream default)
* - OWAPageStream.push() converts lines to string buffer
*
* Implications:
* - Smaller than default (64KB) = more frequent _read() callbacks
* - Reduces memory footprint for large responses
* - Improves backpressure handling responsiveness
* - Trade-off: More CPU for _read() calls vs memory efficiency
*/
const OWA_STREAM_BUFFER_SIZE = 16384;
/**
* OWA_RESOLVED_NAME_MAX_LEN = 400
*
* Purpose: Maximum string length for resolved Oracle procedure canonical names.
* Canonical format: SCHEMA.PACKAGE.PROCEDURE or SCHEMA.PROCEDURE.
*
* Used By:
* - resolveProcedureName() function for dbms_utility.name_resolve output
* - Procedure name resolution SQL at procedureSanitize.js:46-76
*
* Related Values:
* - dbms_utility.name_resolve context = 1 (procedure/function resolution)
* - Oracle identifier limits: Schema (128) + Package (128) + Procedure (128) + 2 dots = ~386
* - 400 provides comfortable headroom
*
* Implications:
* - Oracle object names: 30 bytes for most, extended to 128 in some contexts
* - Canonical name: schema.package.procedure (max ~128+1+128+1+128 = 386)
* - 400 is safe upper bound with margin
*/
const OWA_RESOLVED_NAME_MAX_LEN = 400;
/**
* STATS_INTERVAL_MS = 5000
*
* Purpose: Duration of each statistical bucket in milliseconds.
*
* Used By:
* - src/util/statsManager.js:165: setInterval(this.rotateBucket, this.config.intervalMs)
* - src/handler/handlerAdmin.js:123: Exposed as intervalMs in /api/status response
* - src/frontend/main.ts
* - src/frontend/charts.ts
*
* Related Values:
* - MAX_HISTORY_BUCKETS (1000): At 5s per bucket = ~83 minutes of history
* - MAX_PERCENTILE_SAMPLES (1000): Samples per bucket for P95/P99
*
* Implications:
* - Bucket aggregation: request counts, durations, errors, system metrics
* - Affects granularity of performance monitoring
* - Lower values = more granular but more history entries
* - Higher values = smoother averages but less detail
*/
const STATS_INTERVAL_MS = 5e3;
/**
* MAX_HISTORY_BUCKETS = 1000
*
* Purpose: Maximum number of statistical buckets retained in StatsManager ring buffer.
*
* Used By:
* - src/util/statsManager.js: Ring buffer limit check
* if (this.history.length > this.config.maxHistoryPoints) { this.history.shift(); }
* - Exposed via /api/stats/history endpoint
*
* Related Values:
* - STATS_INTERVAL_MS (5000): 5s per bucket = ~83 minutes total history
* - Each bucket contains: timestamp, requestCount, errors, durations, system metrics
* - Bucket memory estimate: ~100 bytes × 1000 = ~100KB
*
* Implications:
* - Ring buffer: oldest bucket is removed when new one exceeds limit
* - Affects admin console chart history display
* - Higher = more historical context but more memory
*/
const MAX_HISTORY_BUCKETS = 1e3;
/**
* MAX_PERCENTILE_SAMPLES = 1000
*
* Purpose: Maximum number of request duration samples collected per bucket
* for calculating P95/P99 percentiles.
*
* Used By:
* - src/util/statsManager.js: Array length check
* if (b.durations.length < this.config.percentilePrecision)
* - src/util/statsManager.js: P95/P99 calculation
*
* Related Values:
* - Percentile calculation: floor(length × 0.95) and floor(length × 0.99)
* - FIFO replacement: When exceeded, new samples replace oldest
*
* Implications:
* - With 1000 samples, P95/P99 are statistically meaningful
* - Higher = more accurate percentiles but more memory per bucket
* - With STATS_INTERVAL_MS = 5000, 1000 samples ≈ 5 req/sec sustained
* - Under heavy load, older samples are discarded (FIFO)
*/
const MAX_PERCENTILE_SAMPLES = 1e3;
/**
* SHUTDOWN_GRACE_DELAY_MS = 100
*
* Purpose: Delay between initiating server shutdown and forced termination.
*
* Used By: POST /api/server/stop handler only
*
* Related Values:
* - forceShutdown() at src/util/shutdown.js
* - SIGTERM/SIGINT handlers at shutdown.js
*
* Implications:
* - Allows graceful completion of in-flight requests
* - Gives Express middleware time to send final responses
* - 100ms is short; may be insufficient under heavy load
* - Consider making configurable for high-traffic deployments
*/
const SHUTDOWN_GRACE_DELAY_MS = 100;
/**
* TRACE_LOG_ROTATION_SIZE = '10M'
*
* Purpose: Log file size threshold triggering trace log rotation (10 Megabytes).
*
* Used By: rotating-file-stream library for 'trace.log'
*
* Related Values:
* - TRACE_LOG_ROTATION_INTERVAL ('1d'): Also triggers rotation
* - TRACE_LOG_MAX_ROTATED_FILES (10): Maximum retained files
*
* Implications:
* - When either size OR time threshold is reached, rotation occurs
* - Combined with daily rotation: ~10MB/day minimum
* - gzip compression reduces rotated file size by ~70-90%
*/
const TRACE_LOG_ROTATION_SIZE = "10M";
/**
* TRACE_LOG_ROTATION_INTERVAL = '1d'
*
* Purpose: Time-based trace log rotation trigger (daily).
*
* Used By: rotating-file-stream library for 'trace.log'
*
* Implications:
* - Guarantees at least one rotation per day
* - Midnight-based or 24h from first write
*/
const TRACE_LOG_ROTATION_INTERVAL = "1d";
/**
* TRACE_LOG_MAX_ROTATED_FILES = 10
*
* Purpose: Maximum number of rotated trace log files to retain.
*
* Used By: rotating-file-stream library for 'trace.log'
*
* Implications:
* - When exceeded, oldest rotated file is deleted
* - Maximum: ~10 files × 10MB = ~100MB (compressed: ~10-30MB)
*/
const TRACE_LOG_MAX_ROTATED_FILES = 10;
/**
* JSON_LOG_ROTATION_SIZE = '10M'
*
* Purpose: Log file size threshold triggering JSON error log rotation (10 Megabytes).
*
* Used By: rotating-file-stream library for 'error.json.log'
*
* Related Values:
* - JSON_LOG_ROTATION_INTERVAL ('1d'): Also triggers rotation
* - JSON_LOG_MAX_ROTATED_FILES (10): Maximum retained files
*
* Implications:
* - When either size OR time threshold is reached, rotation occurs
* - Combined with daily rotation: ~10MB/day minimum
* - gzip compression reduces rotated file size by ~70-90%
*/
const JSON_LOG_ROTATION_SIZE = "10M";
/**
* JSON_LOG_ROTATION_INTERVAL = '1d'
*
* Purpose: Time-based JSON error log rotation trigger (daily).
*
* Used By: rotating-file-stream library for 'error.json.log'
*
* Implications:
* - Guarantees at least one rotation per day
* - Midnight-based or 24h from first write
*/
const JSON_LOG_ROTATION_INTERVAL = "1d";
/**
* JSON_LOG_MAX_ROTATED_FILES = 10
*
* Purpose: Maximum number of rotated JSON error log files to retain.
*
* Used By: rotating-file-stream library for 'error.json.log'
*
* Implications:
* - When exceeded, oldest rotated file is deleted
* - Maximum: ~10 files × 10MB = ~100MB (compressed: ~10-30MB)
*/
const JSON_LOG_MAX_ROTATED_FILES = 10;
//#endregion
//#region src/backend/util/statsManager.ts
const debug$12 = debugModule("webplsql:statsManager");
/**
* Manager for statistical data collection and temporal bucketing.
*/
var StatsManager = class {
config;
startTime;
history;
lifetime;
_currentBucket;
_lastCpuTimes;
_timer;
/**
* @param config - Configuration.
*/
constructor(config = {}) {
this.config = {
intervalMs: STATS_INTERVAL_MS,
maxHistoryPoints: MAX_HISTORY_BUCKETS,
sampleSystem: true,
samplePools: true,
percentilePrecision: MAX_PERCENTILE_SAMPLES,
...config
};
this.startTime = /* @__PURE__ */ new Date();
this.history = [];
this.lifetime = {
totalRequests: 0,
totalErrors: 0,
minDuration: -1,
maxDuration: -1,
totalDuration: 0,
maxRequestsPerSecond: 0,
memory: {
heapUsedMax: 0,
heapTotalMax: 0,
rssMax: 0,
externalMax: 0
},
cpu: {
max: 0,
userMax: 0,
systemMax: 0
}
};
this._currentBucket = {
count: 0,
errors: 0,
durations: [],
durationSum: 0,
durationMin: -1,
durationMax: -1
};
this._lastCpuTimes = this._getSystemCpuTimes();
this._timer = void 0;
if (this.config.sampleSystem) {
this._timer = setInterval(() => {
this.rotateBucket();
}, this.config.intervalMs);
if (this._timer && typeof this._timer.unref === "function") this._timer.unref();
}
}
/**
* Reset the current bucket accumulator.
*/
_resetBucket() {
this._currentBucket = {
count: 0,
errors: 0,
durations: [],
durationSum: 0,
durationMin: -1,
durationMax: -1
};
}
/**
* Record a request event.
* @param duration - Duration in milliseconds.
* @param isError - Whether the request was an error.
*/
recordRequest(duration, isError = false) {
this.lifetime.totalRequests++;
if (isError) this.lifetime.totalErrors++;
this.lifetime.totalDuration += duration;
if (this.lifetime.minDuration < 0 || duration < this.lifetime.minDuration) this.lifetime.minDuration = duration;
if (this.lifetime.maxDuration < 0 || duration > this.lifetime.maxDuration) this.lifetime.maxDuration = duration;
const b = this._currentBucket;
b.count++;
if (isError) b.errors++;
b.durationSum += duration;
if (b.durationMin < 0 || duration < b.durationMin) b.durationMin = duration;
if (b.durationMax < 0 || duration > b.durationMax) b.durationMax = duration;
if (b.durations.length < this.config.percentilePrecision) b.durations.push(duration);
}
/**
* Get system CPU times.
* @returns System CPU times.
*/
_getSystemCpuTimes() {
const cpus = os.cpus();
let user = 0;
let nice = 0;
let sys = 0;
let idle = 0;
let irq = 0;
for (const cpu of cpus) {
user += cpu.times.user;
nice += cpu.times.nice;
sys += cpu.times.sys;
idle += cpu.times.idle;
irq += cpu.times.irq;
}
const total = user + nice + sys + idle + irq;
return {
user,
nice,
sys,
idle,
irq,
total
};
}
/**
* Calculate CPU usage percentage since last call.
* @returns CPU usage percentage (0-100).
*/
_calculateCpuUsage() {
const current = this._getSystemCpuTimes();
const last = this._lastCpuTimes || {
user: 0,
nice: 0,
sys: 0,
idle: 0,
irq: 0,
total: 0
};
const deltaTotal = current.total - last.total;
const deltaIdle = current.idle - last.idle;
this._lastCpuTimes = current;
if (deltaTotal <= 0) return 0;
const percent = (deltaTotal - deltaIdle) / deltaTotal * 100;
return Math.min(100, Math.max(0, percent));
}
/**
* Rotate the current bucket into history and start a new one.
* @param poolSnapshots - Optional pool snapshots to include.
*/
rotateBucket(poolSnapshots = []) {
const b = this._currentBucket;
const memUsage = process.memoryUsage();
const systemMemoryUsed = os.totalmem() - os.freemem();
const cpuUsage = process.cpuUsage();
const cpu = this._calculateCpuUsage();
const reqPerSec = b.count / (this.config.intervalMs / 1e3);
this.lifetime.maxRequestsPerSecond = Math.max(this.lifetime.maxRequestsPerSecond, reqPerSec);
this.lifetime.memory.heapUsedMax = Math.max(this.lifetime.memory.heapUsedMax, memUsage.heapUsed);
this.lifetime.memory.heapTotalMax = Math.max(this.lifetime.memory.heapTotalMax, memUsage.heapTotal);
this.lifetime.memory.rssMax = Math.max(this.lifetime.memory.rssMax, systemMemoryUsed);
this.lifetime.memory.externalMax = Math.max(this.lifetime.memory.externalMax, memUsage.external);
this.lifetime.cpu.max = Math.max(this.lifetime.cpu.max, cpu);
this.lifetime.cpu.userMax = Math.max(this.lifetime.cpu.userMax, cpuUsage.user);
this.lifetime.cpu.systemMax = Math.max(this.lifetime.cpu.systemMax, cpuUsage.system);
let p95 = 0;
let p99 = 0;
if (b.durations.length > 0) {
const sorted = b.durations.toSorted((x, y) => x - y);
const p95Idx = Math.floor(sorted.length * .95);
const p99Idx = Math.floor(sorted.length * .99);
const lastIdx = sorted.length - 1;
p95 = sorted[p95Idx] ?? sorted[lastIdx] ?? 0;
p99 = sorted[p99Idx] ?? sorted[lastIdx] ?? 0;
}
const bucket = {
timestamp: Date.now(),
requests: b.count,
errors: b.errors,
durationMin: Math.max(b.durationMin, 0),
durationMax: Math.max(b.durationMax, 0),
durationAvg: b.count > 0 ? b.durationSum / b.count : 0,
durationP95: p95,
durationP99: p99,
system: {
cpu,
heapUsed: memUsage.heapUsed,
heapTotal: memUsage.heapTotal,
rss: systemMemoryUsed,
external: memUsage.external
},
pools: poolSnapshots
};
this.history.push(bucket);
if (this.history.length > this.config.maxHistoryPoints) this.history.shift();
this._resetBucket();
debug$12("Bucket rotated: %j", bucket);
}
/**
* Stop the background timer.
*/
stop() {
if (this._timer) {
clearInterval(this._timer);
this._timer = void 0;
}
}
/**
* Get lifetime summary.
* @returns Summary.
*/
getSummary() {
return {
startTime: this.startTime,
totalRequests: this.lifetime.totalRequests,
totalErrors: this.lifetime.totalErrors,
avgResponseTime: this.lifetime.totalRequests > 0 ? this.lifetime.totalDuration / this.lifetime.totalRequests : 0,
minResponseTime: this.lifetime.minDuration,
maxResponseTime: this.lifetime.maxDuration,
maxRequestsPerSecond: this.lifetime.maxRequestsPerSecond,
maxMemory: this.lifetime.memory,
cpu: this.lifetime.cpu
};
}
/**
* Get history buffer.
* @returns The history buffer.
*/
getHistory() {
return this.history;
}
};
//#endregion
//#region src/backend/server/adminContext.ts
/**
* Admin Context Class
*/
var AdminContext = class {
startTime;
config;
pools;
caches;
statsManager;
_paused;
constructor(config) {
this.startTime = /* @__PURE__ */ new Date();
this.config = config;
this.pools = [];
this.caches = [];
this.statsManager = new StatsManager();
this._paused = false;
}
/**
* Register a PL/SQL handler with the admin context.
* @param route - The route for the handler.
* @param pool - The connection pool.
* @param procedureNameCache - The procedure name cache.
* @param argumentCache - The argument cache.
*/
registerHandler(route, pool, procedureNameCache, argumentCache) {
this.pools.push(pool);
this.caches.push({
poolName: route,
procedureNameCache,
argumentCache
});
}
get paused() {
return this._paused;
}
setPaused(value) {
this._paused = value;
}
};
//#endregion
//#region src/backend/util/file.ts
/**
* Read file.
*
* @param filePath - File name.
* @returns The string.
*/
const readFileSyncUtf8 = (filePath) => {
try {
return readFileSync(filePath, "utf8");
} catch {
throw new Error(`Unable to read file "${filePath}"`);
}
};
/**
* Read file.
*
* @param filePath - File name.
* @returns The buffer.
*/
const readFile = async (filePath) => {
try {
return await promises.readFile(filePath);
} catch {
throw new Error(`Unable to read file "${filePath}"`);
}
};
/**
* Remove file.
*
* @param filePath - File name.
*/
const removeFile = async (filePath) => {
try {
await promises.unlink(filePath);
} catch {
throw new Error(`Unable to remove file "${filePath}"`);
}
};
/**
* Load a json file.
*
* @param filePath - File name.
* @returns The json object.
*/
const getJsonFile = (filePath) => {
try {
const fileContent = readFileSync(filePath, "utf8");
return JSON.parse(fileContent);
} catch {
throw new Error(`Unable to load file "${filePath}"`);
}
};
/**
* Is this a directory.
* @param directoryPath - Directory name.
* @returns Return true if it is a directory.
*/
const isDirectory = async (directoryPath) => {
if (typeof directoryPath !== "string") return false;
return (await promises.stat(directoryPath)).isDirectory();
};
/**
* Is this a file.
* @param filePath - File name.
* @returns Return true if it is a file.
*/
const isFile = async (filePath) => {
if (typeof filePath !== "string") return false;
try {
return (await promises.stat(filePath)).isFile();
} catch {
return false;
}
};
//#endregion
//#region src/backend/util/html.ts
/**
* Escape html string.
*
* @param value - The value.
* @returns The escaped value.
*/
const escapeHtml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
/**
* Convert LF and/or CR to <br>
* @param text - The text to convert.
* @returns The converted text.
*/
const convertAsciiToHtml = (text) => {
let html = escapeHtml(text);
html = html.replaceAll(/\r\n|\r|\n/g, "<br />");
html = html.replaceAll(" ", " ");
return html;
};
/**
* get a minimal html page.
* @param body - The body.
* @returns The html page.
*/
const getHtmlPage = (body) => `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>web_plsql error page</title>
<style type="text/css">
html {
font-family: monospace, sans-serif;
font-size: 12px;
}
h1 {
font-size: 16px;
padding: 2px;
background-color: #cc0000;
}
</style>
</head>
<body>
${body}
</body>
</html>
`;
//#endregion
//#region src/backend/util/trace.ts
/**
* Type guard for BindParameter
* @param row - The row to check
* @returns True if row is a BindParameter
*/
const isBindParameter = (row) => {
if (typeof row !== "object" || row === null) return false;
return "dir" in row || "type" in row || "val" in row || "maxSize" in row || "maxArraySize" in row;
};
const SEPARATOR_H1 = "=".repeat(100);
const SEPARATOR_H2 = "-".repeat(30);
/**
* Return a string representation of the value.
*
* @param value - Any value.
* @param depth - Specifies the number of times to recurse while formatting object.
* @returns The string representation.
*/
const inspect = (value, depth = null) => {
try {
return util.inspect(value, {
showHidden: false,
depth,
colors: false
});
} catch {}
try {
return JSON.stringify(value);
} catch {}
return "Unable to convert value to string";
};
/**
* @param cell - The string
* @param width - The width
* @returns The result
*/
const padCell = (cell, width) => cell.padEnd(width, " ");
/**
* Return a tabular representation of the values.
*
* @param head - The header values.
* @param body - The row values.
* @returns The output.
*/
const toTable = (head, body) => {
if (head.length === 0) throw new Error("head cannot be empty");
const widths = head.map((h, i) => {
const bodyMax = Math.max(0, ...body.map((row) => (row[i] ?? "").length));
return Math.max(h.length, bodyMax);
});
/**
* @param i - The index
* @returns The width
*/
const getWidth = (i) => widths[i] ?? 0;
return {
text: [
head.map((h, i) => padCell(h, getWidth(i))).join(" | "),
widths.map((w) => "-".repeat(w ?? 0)).join("-+-"),
...body.map((row) => head.map((_, i) => padCell(row[i] ?? "", getWidth(i))).join(" | "))
].join("\n"),
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>`
};
};
/**
* Log text to the console and to a file.
*
* @param text - Text to log.
*/
const logToFile = (text) => {
const fs = rotatingFileStream.createStream("trace.log", {
size: TRACE_LOG_ROTATION_SIZE,
interval: TRACE_LOG_ROTATION_INTERVAL,
maxFiles: TRACE_LOG_MAX_ROTATED_FILES,
compress: "gzip"
});
fs.write(text);
fs.end();
};
/**
* Return a string representation of the request.
*
* @param req - express.Request.
* @returns The string representation.
*/
const inspectRequest = (req) => {
const requestData = {};
Object.keys(req).filter((prop) => [
"originalUrl",
"params",
"query",
"url",
"method",
"body",
"files",
"secret",
"cookies"
].includes(prop)).forEach((prop) => {
requestData[prop] = req[prop];
});
return inspect(requestData);
};
/**
* Return a string representation of the bind parameter.
* @param dir - The direction.
* @returns The string.
*/
const dirToString = (dir) => {
switch (dir) {
case oracledb.BIND_IN: return "IN";
case oracledb.BIND_OUT: return "OUT";
case oracledb.BIND_INOUT: return "INOUT";
default: return "";
}
};
/**
* Return a string representation of the bind type.
* @param type - The type.
* @returns The string.
*/
const bindTypeToString = (type) => {
if (typeof type === "object" && type !== null && "name" in type) return type.name;
if (typeof type === "string") return type;
if (typeof type === "number") return type.toString();
return "";
};
/**
* Return a string representation of the bind parameter.
* @param output - The output.
* @param bind - The bind parameters.
*/
const inspectBindParameter = (output, bind) => {
const rows = Object.entries(bind);
if (rows.length === 0) return;
const { html, text } = toTable([
"id",
"dir",
"maxArraySize",
"maxSize",
"bind type",
"value",
"value type"
], rows.map(([id, row]) => {
let dir = "";
let maxArraySize = "";
let maxSize = "";
let bindType = "";
let value = "";
let valueType = "";
if (isBindParameter(row)) {
dir = dirToString(row.dir);
maxArraySize = row.maxArraySize ? row.maxArraySize.toString() : "";
maxSize = row.maxSize ? row.maxSize.toString() : "";
bindType = bindTypeToString(row.type);
value = inspect(row.val);
valueType = typeof row.val;
} else {
value = inspect(row);
valueType = typeof row;
}
return [
id,
dir,
maxArraySize,
maxSize,
bindType,
value,
valueType
];
}));
output.html += html;
output.text += text;
};
/**
* Add environment
* @param output - The output.
* @param environment - The environment.
*/
const inspectEnvironment = (output, environment) => {
const rows = Object.entries(environment);
if (rows.length === 0) return;
const { html, text } = toTable(["key", "value"], rows);
output.html += html;
output.text += text;
};
/**
* Get a block.
* @param title - The name.
* @param body - The name.
* @returns The text.
*/
const getBlock = (title, body) => `\n${SEPARATOR_H2}${title.toUpperCase()}${SEPARATOR_H2}\n${body}`;
/**
* Get line html
* @param text - The text.
* @returns The line.
*/
const getLineHtml = (text) => `<p>${convertAsciiToHtml(text)}</p>`;
/**
* Get line text
* @param text - The text.
* @returns The line.
*/
const getLineText = (text) => `${text}\n`;
/**
* Add line
* @param output - The output.
* @param text - The text to convert.
*/
const addLine = (output, text) => {
output.html += getLineHtml(text);
output.text += getLineText(text);
};
/**
* Add header
* @param output - The output.
* @param text - The text to convert.
*/
const addHeader = (output, text) => {
output.html += `<h2>${text}</h2>`;
output.text += `\n${text}\n${"-".repeat(text.length)}\n`;
};
/**
* Add procedure
* @param output - The output.
* @param sql - The SQL to execute.
* @param bind - The bind parameters.
*/
const addProcedure = (output, sql, bind) => {
output.html += `${sql}<br><br>`;
output.text += `${sql}\n\n`;
try {
inspectBindParameter(output, bind);
} catch (err) {
addLine(output, `Unable to inspect bind parameter: ${errorToString(err)}`);
}
output.html += `<br>`;
output.text += `\n`;
};
/**
* Get a formatted message.
* @param para - The req object represents the HTTP request.
* @returns The output.
*/
const getFormattedMessage = (para) => {
const timestamp = para.timestamp ?? /* @__PURE__ */ new Date();
const url = typeof para.req?.originalUrl === "string" && para.req.originalUrl.length > 0 ? ` on ${para.req.originalUrl}` : "";
const header = `${(para.type ?? "trace").toUpperCase()} at ${timestamp.toUTCString()}${url}`;
const output = {
html: `<h1>${header}</h1>`,
text: `\n\n${SEPARATOR_H1}\n== ${header}\n${SEPARATOR_H1}\n`
};
addHeader(output, "ERROR");
addLine(output, para.message);
if (para.req) {
addHeader(output, "REQUEST");
addLine(output, inspectRequest(para.req));
}
if (para.sql && para.bind) {
addHeader(output, "PROCEDURE");
addProcedure(output, para.sql, para.bind);
}
if (para.environment) {
addHeader(output, "ENVIRONMENT");
inspectEnvironment(output, para.environment);
}
return output;
};
/**
* Log a warning message.
* @param para - The req object represents the HTTP request.
*/
const warningMessage = (para) => {
const { text } = getFormattedMessage(para);
logToFile(text);
console.warn(text);
};
//#endregion
//#region src/backend/util/errorToString.ts
/**
* Convert Error to a string.
*
* @param error - The error.
* @returns The string representation.
*/
const errorToString = (error) => {
if (typeof error === "string") return error;
else if (error instanceof Error) {
const parts = [error.name];
if (typeof error.message === "string" && error.message.length > 0) parts.push(error.message);
if (typeof error.stack === "string" && error.stack.length > 0) parts.push(error.stack);
return parts.join("\n");
} else return inspect(error);
};
//#endregion
//#region src/backend/handler/plsql/upload.ts
const debug$11 = debugModule("webplsql:fileUpload");
const z$reqFiles = z$1.array(z$1.strictObject({
fieldname: z$1.string(),
originalname: z$1.string(),
encoding: z$1.string(),
mimetype: z$1.string(),
destination: z$1.string(),
filename: z$1.string(),
path: z$1.string(),
size: z$1.number()
}));
/**
* Get the files
*
* @param req - The req object represents the HTTP request.
* @returns Promise that resolves with an array of files to be uploaded.
*/
const getFiles = (req) => {
if (!("files" in req)) {
debug$11("getFiles: no files");
return [];
}
if (typeof req.files === "object" && req.files !== null && Object.keys(req.files).length === 0) {
debug$11("getFiles: no files");
return [];
}
const files = z$reqFiles.parse(req.files);
for (const file of files) file.filename += `/${file.originalname}`;
debug$11("getFiles", files);
return files;
};
/**
* Upload the given file and return a promise.
*
* @param file - The file to upload.
* @param doctable - The file to upload.
* @param databaseConnection - The file to upload.
* @returns Promise that resolves when uploaded.
*/
const uploadFile = async (file, doctable, databaseConnection) => {
debug$11(`uploadFile`, file, doctable);
/* v8 ignore next - defensive validation */
if (typeof doctable !== "string" || doctable.length === 0) throw new Error(`Unable to upload file "${file.filename}" because the option ""doctable" has not been defined`);
let blobContent;
try {
blobContent = await readFile(file.path);
} catch (err) {
throw new Error(`Unable to load file "${file.path}".\n${errorToString(err)}`);
}
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)`;
const bind = {
name: file.filename,
mime_type: file.mimetype,
doc_size: file.size,
blob_content: {
val: blobContent,
type: BUFFER
}
};
try {
await databaseConnection.execute(sql, bind, { autoCommit: true });
} catch (err) {
throw new Error(`Unable to insert file "${file.filename}".\n${errorToString(err)}`);
}
try {
await removeFile(file.path);
} catch (err) {
throw new Error(`Unable to remove file "${file.filename}".\n${errorToString(err)}`);
}
};
//#endregion
//#region src/backend/handler/plsql/procedureVariable.ts
const debug$10 = debugModule("webplsql:procedureVariable");
/**
* Get the sql statement and bindings for the procedure to execute for a variable number of arguments
* @param _req - The req object represents the HTTP request. (only used for debugging)
* @param procName - The procedure to execute
* @param argObj - The arguments to pass to the procedure
* @returns The SQL statement and bindings for the procedure to execute
*/
const getProcedureVariable = (_req, procName, argObj) => {
/* v8 ignore start */
if (debug$10.enabled) debug$10(`getProcedureVariable: ${procName} arguments=`, argObj);
/* v8 ignore stop */
const names = [];
const values = [];
for (const key in argObj) {
const value = argObj[key];
if (typeof value === "string") {
names.push(key);
values.push(value);
} else if (Array.isArray(value)) value.forEach((item) => {
names.push(key);
values.push(item);
});
}
return {
sql: `${procName}(:argnames, :argvalues)`,
bind: {
argnames: {
dir: BIND_IN,
type: STRING,
val: names
},
argvalues: {
dir: BIND_IN,
type: STRING,
val: values
}
}
};
};
//#endregion
//#region src/backend/handler/plsql/requestError.ts
var RequestError = class RequestError extends Error {
timestamp;
/**
* @param message - The error message.
*/
constructor(message) {
super(message);
if (Error.captureStackTrace) Error.captureStackTrace(this, RequestError);
this.timestamp = /* @__PURE__ */ new Date();
}
};
//#endregion
//#region src/backend/util/util.ts
/**
* Convert a string to a number
*
* @param value - The string to convert
* @returns The number or null if the string could not be converted
*/
const stringToNumber = (value) => {
if (typeof value === "number") return !Number.isNaN(value) && Number.isFinite(value) ? value : null;
if (typeof value !== "string" || !/^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:E[+-]?\d+)?$/i.test(value)) return null;
return Number(value);
};
//#endregion
//#region src/backend/handler/plsql/procedureNamed.ts
const debug$9 = debugModule("webplsql:procedureNamed");
const SQL_GET_ARGUMENT = [
"DECLARE",
" schemaName VARCHAR2(32767);",
" part1 VARCHAR2(32767);",
" part2 VARCHAR2(32767);",
" dblink VARCHAR2(32767);",
" objectType NUMBER;",
" objectID NUMBER;",
"BEGIN",
" dbms_utility.name_resolve(name=>UPPER(:name), context=>1, schema=>schemaName, part1=>part1, part2=>part2, dblink=>dblink, part1_type=>objectType, object_number=>objectID);",
" IF (part1 IS NOT NULL) THEN",
" 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;",
" ELSE",
" 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;",
" END IF;",
"END;"
].join("\n");
const DATA_TYPES = Object.freeze({
VARCHAR2: "VARCHAR2",
CHAR: "CHAR",
BINARY_INTEGER: "BINARY_INTEGER",
NUMBER: "NUMBER",
DATE: "DATE",
CLOB: "CLOB",
PL_SQL_TABLE: "PL/SQL TABLE"
});
/**
* Retrieve the argument types for a given procedure to be executed.
* This is important because if the procedure is defined to take a PL/SQL indexed table,
* we must provise a table, even if there is only one argument to be submitted.
* @param procedure - The procedure
* @param databaseConnection - The database connection
* @returns The argument types
*/
const loadArguments = async (procedure, databaseConnection) => {
const bind = {
name: {
dir: BIND_IN,
type: STRING,
val: procedure
},
names: {
dir: BIND_OUT,
type: STRING,
maxSize: 60,
maxArraySize: MAX_PROCEDURE_PARAMETERS
},
types: {
dir: BIND_OUT,
type: STRING,
maxSize: 60,
maxArraySize: MAX_PROCEDURE_PARAMETERS
}
};
let result = {};
try {
result = await databaseConnection.execute(SQL_GET_ARGUMENT, bind);
} catch (err) {
/* v8 ignore start */
debug$9("result", result);
throw new RequestError(`Error when retrieving arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`);
}
let data;
try {
data = z$1.object({
names: z$1.array(z$1.string().nullable()),
types: z$1.array(z$1.string().nullable())
}).parse(result.outBinds);
} catch (err) {
/* v8 ignore start */
debug$9("result.outBinds", result.outBinds);
throw new RequestError(`Error when decoding arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`);
}
if (data.names.length !== data.types.length) throw new RequestError("Error when decoding arguments. The number of names and types does not match");
const argTypes = {};
for (let i = 0; i < data.names.length; i++) {
const name = data.names[i];
const type = data.types[i];
if (name && type) argTypes[name.toLowerCase()] = type;
}
return argTypes;
};
/**
* Find the argument types for a given procedure to be executed.
* As the arguments are cached, we first look up the cache and only if not yet available we load them.
* @param procedure - The procedure
* @param databaseConnection - The database connection
* @param argumentCache - The argument cache.
* @returns The argument types
*/
const findArguments = async (procedure, databaseConnection, argu