UNPKG

web_plsql

Version:

The Express Middleware for Oracle PL/SQL

1 lines 213 kB
{"version":3,"file":"index.mjs","names":["z","z","debug","oracledb.createPool","debug","debug","fs","debug","z","debug","debug","z","debug","debug","debug","z","debug","z","debug","z","debug","debug","debug"],"sources":["../src/common/configStaticSchema.ts","../src/common/procedureTraceEntry.ts","../src/common/logEntrySchema.ts","../src/backend/types.ts","../src/backend/util/oracledb-provider.ts","../src/backend/version.ts","../src/backend/server/config.ts","../src/backend/server/server.ts","../src/backend/util/shutdown.ts","../src/common/constants.ts","../src/backend/util/statsManager.ts","../src/backend/server/adminContext.ts","../src/backend/util/file.ts","../src/backend/util/html.ts","../src/backend/util/trace.ts","../src/backend/util/errorToString.ts","../src/backend/handler/plsql/upload.ts","../src/backend/handler/plsql/procedureVariable.ts","../src/backend/handler/plsql/requestError.ts","../src/backend/util/util.ts","../src/backend/handler/plsql/procedureNamed.ts","../src/backend/handler/plsql/parsePage.ts","../src/backend/handler/plsql/sendResponse.ts","../src/backend/handler/plsql/procedureError.ts","../src/backend/util/cache.ts","../src/backend/handler/plsql/procedureSanitize.ts","../src/backend/handler/plsql/owaPageStream.ts","../src/backend/util/traceManager.ts","../src/backend/handler/plsql/procedure.ts","../src/backend/handler/plsql/cgi.ts","../src/backend/util/type.ts","../src/backend/handler/plsql/request.ts","../src/backend/util/jsonLogger.ts","../src/backend/handler/plsql/errorPage.ts","../src/backend/handler/plsql/handlerPlSql.ts","../src/backend/handler/handlerLogger.ts","../src/backend/handler/handlerUpload.ts","../src/backend/handler/handlerAdmin.ts","../src/backend/handler/handlerAdminConsole.ts","../src/backend/handler/handlerSpaFallback.ts"],"sourcesContent":["import z from 'zod';\n\n/**\n * Configuration for serving static files\n */\nexport const configStaticSchema = z.strictObject({\n\t/** URL route prefix for static assets */\n\troute: z.string(),\n\t/** Local filesystem path to the static assets directory */\n\tdirectoryPath: z.string(),\n\t/**\n\t * Enable SPA fallback mode.\n\t * When true, serves index.html for unmatched routes (for React Router, Vue Router, etc.)\n\t * Requires: Application uses HTML5 History Mode routing\n\t * Default: false\n\t */\n\tspaFallback: z.boolean().optional(),\n});\nexport type configStaticType = z.infer<typeof configStaticSchema>;\n","import {z} from 'zod';\n\nexport const procedureTraceEntrySchema = z.strictObject({\n\tid: z.string(),\n\ttimestamp: z.string(),\n\tsource: z.string(),\n\turl: z.string(),\n\tmethod: z.string(),\n\tstatus: z.string(),\n\tduration: z.number(),\n\tprocedure: z.string().optional(),\n\tparameters: z.union([z.record(z.string(), z.unknown()), z.array(z.unknown())]).optional(),\n\tuploads: z\n\t\t.array(\n\t\t\tz.strictObject({\n\t\t\t\toriginalname: z.string(),\n\t\t\t\tmimetype: z.string(),\n\t\t\t\tsize: z.number(),\n\t\t\t}),\n\t\t)\n\t\t.optional(),\n\tdownloads: z\n\t\t.strictObject({\n\t\t\tfileType: z.string(),\n\t\t\tfileSize: z.number(),\n\t\t})\n\t\t.optional(),\n\thtml: z.string().optional(),\n\tcookies: z.record(z.string(), z.string()).optional(),\n\theaders: z.record(z.string(), z.string()).optional(),\n\tcgi: z.record(z.string(), z.string()).optional(),\n\terror: z.string().optional(),\n});\n\nexport type procedureTraceEntry = z.infer<typeof procedureTraceEntrySchema>;\n","import {z} from 'zod';\n\n/**\n * Error log entry schema.\n */\nconst logEntryTypeSchema = z.union([z.literal('error'), z.literal('info'), z.literal('warning')]);\nexport const logEntrySchema = z.strictObject({\n\ttimestamp: z.string(),\n\ttype: logEntryTypeSchema,\n\tmessage: z.string(),\n\treq: z\n\t\t.strictObject({\n\t\t\tmethod: z.string().optional(),\n\t\t\turl: z.string().optional(),\n\t\t\tip: z.string().optional(),\n\t\t\tuserAgent: z.string().optional(),\n\t\t})\n\t\t.optional(),\n\tdetails: z\n\t\t.strictObject({\n\t\t\tfullMessage: z.string().optional(),\n\t\t\tsql: z.string().optional(),\n\t\t\tbind: z.unknown().optional(),\n\t\t\tenvironment: z.record(z.string(), z.string()).optional(),\n\t\t})\n\t\t.optional(),\n});\nexport type logEntryType = z.infer<typeof logEntrySchema>;\n","import z from 'zod';\nimport {configStaticSchema} from '../common/configStaticSchema.ts';\nimport type {Connection, Pool} from 'oracledb';\nimport type {CookieOptions} from 'express';\nimport type {Readable} from 'node:stream';\nimport type {Cache} from './util/cache.ts';\n\nexport {procedureTraceEntrySchema, type procedureTraceEntry} from '../common/procedureTraceEntry.ts';\nexport {logEntrySchema, type logEntryType} from '../common/logEntrySchema.ts';\n\n/**\n * Defines the style of error reporting\n * 'basic': standard error messages\n * 'debug': detailed error messages including database context\n */\nconst z$errorStyleType = z.enum(['basic', 'debug']);\n\n/**\n * Custom callback signature for manual transaction handling\n */\ntype transactionCallbackType = (connection: Connection, procedure: string) => void | Promise<void>;\n\n/**\n * Defines how transactions are handled after procedure execution\n * 'commit': automatically commit\n * 'rollback': automatically rollback\n * callback: custom function for manual handling\n */\nconst transactionModeSchema = z.union([\n\tz.custom<transactionCallbackType>((val) => typeof val === 'function', {\n\t\tmessage: 'Invalid transaction callback',\n\t}),\n\tz.literal('commit'),\n\tz.literal('rollback'),\n\tz.undefined(),\n\tz.null(),\n]);\nexport type transactionModeType = z.infer<typeof transactionModeSchema>;\n\n/**\n * Authentication callback signature.\n * Returns the identity string on success, or null on failure.\n * @public\n */\nexport type AuthCallback = (connectionPool: Pool, credentials: {username: string; password?: string | undefined}) => Promise<string | null>;\n\n/**\n * Authentication configuration for a PL/SQL route\n */\nconst z$authSchema = z.strictObject({\n\t/** Authentication type */\n\ttype: z.literal('basic'),\n\t/** Callback function to validate credentials */\n\tcallback: z.custom<AuthCallback>((val) => typeof val === 'function', {\n\t\tmessage: 'Invalid auth callback',\n\t}),\n\t/** Authentication realm */\n\trealm: z.string().optional(),\n});\n\n/**\n * PL/SQL handler behavior configuration\n */\nexport const z$configPlSqlHandlerType = z.strictObject({\n\t/** Default procedure to execute if none specified */\n\tdefaultPage: z.string(),\n\t/** Virtual path alias for procedures */\n\tpathAlias: z.string().optional(),\n\t/** Procedure name associated with the path alias */\n\tpathAliasProcedure: z.string().optional(),\n\t/** Database table used for file uploads/downloads */\n\tdocumentTable: z.string(),\n\t/** List of pattern/procedure names excluded from execution */\n\texclusionList: z.array(z.string()).optional(),\n\t/** PL/SQL function called to validate requests */\n\trequestValidationFunction: z.string().optional(),\n\t/** Post-execution transaction behavior */\n\ttransactionMode: transactionModeSchema.optional(),\n\t/** Error reporting style */\n\terrorStyle: z$errorStyleType,\n\t/** Static CGI environment variables to be passed to the session */\n\tcgi: z.record(z.string(), z.string()).optional(),\n\t/** Authentication settings */\n\tauth: z$authSchema.optional(),\n});\nexport type configPlSqlHandlerType = z.infer<typeof z$configPlSqlHandlerType>;\n\n/**\n * Database connection configuration for a PL/SQL route\n */\nconst z$configPlSqlConfigType = z.strictObject({\n\t/** URL route prefix for this database connection */\n\troute: z.string(),\n\t/** Database username */\n\tuser: z.string(),\n\t/** Database password */\n\tpassword: z.string(),\n\t/** Oracle connection string (TNS or EZConnect) */\n\tconnectString: z.string(),\n});\nexport type configPlSqlConfigType = z.infer<typeof z$configPlSqlConfigType>;\n\n/**\n * Complete PL/SQL route configuration combining handler and connection settings\n */\nexport type configPlSqlType = configPlSqlHandlerType & configPlSqlConfigType;\nconst z$configPlSqlType = z.strictObject({\n\t...z$configPlSqlHandlerType.shape,\n\t...z$configPlSqlConfigType.shape,\n});\n\n/**\n * Root application configuration\n */\nexport const z$configType = z.strictObject({\n\t/** Server listening port */\n\tport: z.number(),\n\t/** Array of static file routes */\n\trouteStatic: z.array(configStaticSchema),\n\t/** Array of PL/SQL routes */\n\troutePlSql: z.array(z$configPlSqlType),\n\t/** Maximum allowed size for file uploads (bytes) */\n\tuploadFileSizeLimit: z.number().optional(),\n\t/** Path to the log file */\n\tloggerFilename: z.string(),\n\t/** URL route prefix for the admin console */\n\tadminRoute: z.string().optional(),\n\t/** Username for admin console authentication */\n\tadminUser: z.string().optional(),\n\t/** Password for admin console authentication */\n\tadminPassword: z.string().optional(),\n\t/** Developer mode (skips frontend build check, enables CORS) */\n\tdevMode: z.boolean().optional(),\n});\nexport type configType = z.infer<typeof z$configType>;\n\n/**\n * Environment variables as string key-value pairs\n */\nexport type environmentType = Record<string, string>;\n\n/**\n * HTTP arguments mapping with support for multi-value parameters\n */\nexport type argObjType = Record<string, string | string[]>;\n\n/**\n * Mapping of PL/SQL procedure argument names to their database types\n */\nexport type argsType = Record<string, string>;\n\n/**\n * Metadata for uploaded files\n */\nexport type fileUploadType = {\n\t/** HTML form field name */\n\tfieldname: string;\n\t/** Original filename as uploaded by the client */\n\toriginalname: string;\n\t/** Content encoding */\n\tencoding: string;\n\t/** MIME type */\n\tmimetype: string;\n\t/** Local temporary filename */\n\tfilename: string;\n\t/** Absolute path to the temporary file */\n\tpath: string;\n\t/** File size in bytes */\n\tsize: number;\n};\n\n/**\n * HTTP cookie definition\n */\nexport type cookieType = {\n\t/** Cookie name */\n\tname: string;\n\t/** Cookie value */\n\tvalue: string;\n\t/** Express cookie options (domain, path, expires, etc.) */\n\toptions: CookieOptions;\n};\n\n/**\n * Internal representation of a generated web page or file response\n */\nexport type pageType = {\n\t/** Response body content as string or stream */\n\tbody: string | Readable;\n\t/** HTTP response headers and status */\n\thead: {\n\t\t/** Array of cookies to be set */\n\t\tcookies: cookieType[];\n\t\t/** Content-Type header value */\n\t\tcontentType?: string;\n\t\t/** Content-Length header value */\n\t\tcontentLength?: number;\n\t\t/** HTTP status code */\n\t\tstatusCode?: number;\n\t\t/** HTTP status reason phrase */\n\t\tstatusDescription?: string;\n\t\t/** Location header for redirects */\n\t\tredirectLocation?: string;\n\t\t/** Additional custom HTTP headers */\n\t\totherHeaders: Record<string, string>;\n\t\t/** Server header value */\n\t\tserver?: string;\n\t};\n\t/** Metadata for file downloads if applicable */\n\tfile: {\n\t\t/** MIME type of the file */\n\t\tfileType: string | null;\n\t\t/** Size of the file in bytes */\n\t\tfileSize: number | null;\n\t\t/** File content as stream or buffer */\n\t\tfileBlob: Readable | Buffer | null;\n\t};\n};\n\nexport type ProcedureNameCache = Cache<string>;\nexport type ArgumentCache = Cache<argsType>;\n","import oracledb from 'oracledb';\n\nconst USE_MOCK = process.env.MOCK_ORACLE === 'true';\n\n/**\n * Create a database pool.\n * @param config - The pool attributes.\n * @returns The pool.\n */\n// Runtime switch for createPool\nexport async function createPool(config: oracledb.PoolAttributes): Promise<oracledb.Pool> {\n\tif (USE_MOCK) {\n\t\tconst mock = await import('./oracledb-mock.ts');\n\t\treturn mock.createPool(config);\n\t}\n\treturn await oracledb.createPool(config);\n}\n\n// Always export real oracledb constants (they're identical in mock)\nexport const BIND_IN = oracledb.BIND_IN;\nexport const BIND_OUT = oracledb.BIND_OUT;\nexport const BIND_INOUT = oracledb.BIND_INOUT;\nexport const STRING = oracledb.STRING;\nexport const NUMBER = oracledb.NUMBER;\nexport const DATE = oracledb.DATE;\nexport const CURSOR = oracledb.CURSOR;\nexport const BUFFER = oracledb.BUFFER;\nexport const CLOB = oracledb.CLOB;\nexport const BLOB = oracledb.BLOB;\nexport const DB_TYPE_VARCHAR = oracledb.DB_TYPE_VARCHAR;\nexport const DB_TYPE_CLOB = oracledb.DB_TYPE_CLOB;\nexport const DB_TYPE_NUMBER = oracledb.DB_TYPE_NUMBER;\nexport const DB_TYPE_DATE = oracledb.DB_TYPE_DATE;\n\n// Re-export types from real oracledb for convenience\nexport type {Connection, Pool, Lob, Result, BindParameter, BindParameters, ExecuteOptions, DbType} from 'oracledb';\n\n// Export mock-specific utilities\nexport {setExecuteCallback, type ExecuteCallback} from './oracledb-mock.ts';\n","declare global {\n\tvar __VERSION__: string;\n}\n\nglobalThis.__VERSION__ ??= '**development**';\n\ndeclare const __VERSION__: string;\n\n/**\n * Returns the current library version\n * @returns {string} - Version.\n */\nexport const getVersion = () => __VERSION__;\n","import {getVersion} from '../version.ts';\nimport type {configType} from '../types.ts';\n\nconst paddedLine = (title: string, value: string | number) => {\n\tconsole.log(`${(title + ':').padEnd(30)} ${value}`);\n};\n\n/**\n * Show configuration.\n * @param config - The config.\n */\nexport const showConfig = (config: configType): void => {\n\tconst LINE = '-'.repeat(80);\n\n\tconsole.log(LINE);\n\tconsole.log(`NODE PL/SQL SERVER version ${getVersion()}`);\n\tconsole.log(LINE);\n\n\tpaddedLine('Server port', config.port);\n\tpaddedLine('Admin route', `${config.adminRoute ?? '/admin'}${config.adminUser ? ' (authenticated)' : ''}`);\n\tpaddedLine('Access log', config.loggerFilename.length > 0 ? config.loggerFilename : '-');\n\tpaddedLine('Upload file size limit', typeof config.uploadFileSizeLimit === 'number' ? `${config.uploadFileSizeLimit} bytes` : '-');\n\n\tif (config.routeStatic.length > 0) {\n\t\tconfig.routeStatic.forEach((e) => {\n\t\t\tpaddedLine('Static route', e.route);\n\t\t\tpaddedLine('Directory path', e.directoryPath);\n\t\t});\n\t}\n\n\tif (config.routePlSql.length > 0) {\n\t\tconfig.routePlSql.forEach((e) => {\n\t\t\tlet transactionMode = '';\n\t\t\tif (typeof e.transactionMode === 'string') {\n\t\t\t\ttransactionMode = e.transactionMode;\n\t\t\t} else if (typeof e.transactionMode === 'function') {\n\t\t\t\ttransactionMode = 'custom callback';\n\t\t\t}\n\n\t\t\tpaddedLine('Route', `http://localhost:${config.port}${e.route}`);\n\t\t\tpaddedLine('Oracle user', e.user);\n\t\t\tpaddedLine('Oracle server', e.connectString);\n\t\t\tpaddedLine('Oracle document table', e.documentTable);\n\t\t\tpaddedLine('Default page', e.defaultPage.length > 0 ? e.defaultPage : '-');\n\t\t\tpaddedLine('Path alias', e.pathAlias ?? '-');\n\t\t\tpaddedLine('Path alias procedure', e.pathAliasProcedure ?? '-');\n\t\t\tpaddedLine('Exclution list', e.exclusionList ? e.exclusionList.join(', ') : '-');\n\t\t\tpaddedLine('Validation function', e.requestValidationFunction ?? '-');\n\t\t\tpaddedLine('After request handler', transactionMode.length > 0 ? transactionMode : '-');\n\t\t\tpaddedLine('Error style', e.errorStyle);\n\t\t});\n\t}\n\n\tconsole.log(LINE);\n\n\tconst baseUrl = `http://localhost:${config.port}`;\n\tpaddedLine('🏠 Admin Console', `${baseUrl}${config.adminRoute ?? '/admin'}`);\n\tif (config.routePlSql.length > 0) {\n\t\tconsole.log('');\n\t\tconsole.log('⚙️ PL/SQL Gateways:');\n\t\tconfig.routePlSql.forEach((e) => {\n\t\t\tconsole.log(' ' + `${e.route.padEnd(28)} ${baseUrl}${e.route}`);\n\t\t});\n\t}\n\n\tconsole.log(LINE);\n};\n","import debugModule from 'debug';\nconst debug = debugModule('webplsql:server');\n\nimport http from 'node:http';\nimport https from 'node:https';\nimport type {Socket} from 'node:net';\n\nimport express, {type Express, type Request, type Response, type NextFunction} from 'express';\nimport type {Pool} from 'oracledb';\nimport cors from 'cors';\nimport cookieParser from 'cookie-parser';\nimport compression from 'compression';\nimport expressStaticGzip from 'express-static-gzip';\n\n// NOTE: it is only allowed to import from the API './index.ts'\nimport {\n\thandlerWebPlSql,\n\thandlerAdminConsole,\n\thandlerUpload,\n\thandlerLogger,\n\tAdminContext,\n\tshowConfig,\n\treadFileSyncUtf8,\n\tgetJsonFile,\n\tinstallShutdown,\n\tz$configType,\n\ttype configType,\n\ttype configPlSqlType,\n\toracledb,\n\tcreateSpaFallback,\n} from '../index.ts';\n\n/**\n * Close multiple pools.\n * @param pools - The pools to close.\n */\nexport const poolsClose = async (pools: Pool[]): Promise<void> => {\n\tawait Promise.all(pools.map((pool) => pool.close(0)));\n};\n\nexport type webServer = {\n\tconfig: configType;\n\tconnectionPools: Pool[];\n\tapp: Express;\n\tserver: http.Server | https.Server;\n\tadminContext: AdminContext;\n\tshutdown: () => Promise<void>;\n};\n\nexport type sslConfig = {\n\tkeyFilename: string;\n\tcertFilename: string;\n};\n\n/**\n * Create HTTPS server.\n * @param app - express application\n * @param ssl - ssl configuration.\n * @returns server\n */\nexport const createServer = (app: Express, ssl?: sslConfig): http.Server | https.Server => {\n\tif (ssl) {\n\t\tconst key = readFileSyncUtf8(ssl.keyFilename);\n\t\tconst cert = readFileSyncUtf8(ssl.certFilename);\n\n\t\treturn https.createServer({key, cert}, app);\n\t} else {\n\t\treturn http.createServer(app);\n\t}\n};\n\n/**\n * Start server.\n * @param config - The config.\n * @param ssl - ssl configuration.\n * @returns Promise resolving to the web server object.\n */\nexport const startServer = async (config: configType, ssl?: sslConfig): Promise<webServer> => {\n\tdebug('startServer: BEGIN', config, ssl);\n\n\tconst internalConfig = z$configType.parse(config);\n\n\tshowConfig(internalConfig);\n\n\t// Create express app\n\tconst app = express();\n\n\t// Default middleware\n\tif (internalConfig.devMode) {\n\t\tapp.use(\n\t\t\tcors({\n\t\t\t\torigin: 'http://localhost:5173',\n\t\t\t\tcredentials: true,\n\t\t\t}),\n\t\t);\n\t}\n\tapp.use(handlerUpload(internalConfig.uploadFileSizeLimit));\n\tapp.use(express.json({limit: '50mb'}));\n\tapp.use(express.urlencoded({limit: '50mb', extended: true}));\n\tapp.use(cookieParser());\n\tapp.use(compression());\n\n\t// Create AdminContext\n\tconst adminContext = new AdminContext(internalConfig);\n\n\t// Mount Admin Console (includes Pause middleware)\n\tapp.use(\n\t\thandlerAdminConsole(\n\t\t\t{\n\t\t\t\tadminRoute: internalConfig.adminRoute,\n\t\t\t\tuser: internalConfig.adminUser,\n\t\t\t\tpassword: internalConfig.adminPassword,\n\t\t\t\tdevMode: internalConfig.devMode,\n\t\t\t},\n\t\t\tadminContext,\n\t\t),\n\t);\n\n\t// Oracle pl/sql express middleware\n\tfor (const i of internalConfig.routePlSql) {\n\t\t// Allocate the Oracle database pool\n\t\tconst pool = await oracledb.createPool({\n\t\t\tuser: i.user,\n\t\t\tpassword: i.password,\n\t\t\tconnectString: i.connectString,\n\t\t});\n\n\t\tconst handler = handlerWebPlSql(pool, i as configPlSqlType, adminContext);\n\n\t\tapp.use([`${i.route}/:name`, i.route], (req: Request, res: Response, next: NextFunction) => {\n\t\t\tconst start = process.hrtime();\n\t\t\tres.on('finish', () => {\n\t\t\t\tconst diff = process.hrtime(start);\n\t\t\t\tconst duration = diff[0] * 1000 + diff[1] / 1_000_000;\n\t\t\t\tadminContext.statsManager.recordRequest(duration, res.statusCode >= 400);\n\t\t\t});\n\t\t\thandler(req, res, next);\n\t\t});\n\t}\n\n\t// Access log\n\tif (internalConfig.loggerFilename.length > 0) {\n\t\tapp.use(handlerLogger(internalConfig.loggerFilename));\n\t}\n\n\t// Serving static files\n\tfor (const i of internalConfig.routeStatic) {\n\t\tapp.use(\n\t\t\ti.route,\n\t\t\texpressStaticGzip(i.directoryPath, {\n\t\t\t\tenableBrotli: true,\n\t\t\t\torderPreference: ['br'],\n\t\t\t}),\n\t\t);\n\n\t\t// Mount SPA fallback (serves index.html for unmatched routes)\n\t\t// IMPORTANT: Must come AFTER expressStaticGzip\n\t\tif (i.spaFallback) {\n\t\t\tapp.use(i.route, createSpaFallback(i.directoryPath, i.route));\n\t\t}\n\t}\n\n\t// Mount PL/SQL handlers with stats tracking\n\t// (already mounted in the loop above)\n\n\t// create server\n\tdebug('startServer: createServer');\n\tconst server = createServer(app, ssl);\n\n\t// Track open connections\n\tconst connections = new Set<Socket>();\n\tserver.on('connection', (socket: Socket) => {\n\t\tconnections.add(socket);\n\t\tsocket.on('close', () => {\n\t\t\tconnections.delete(socket);\n\t\t});\n\t});\n\n\tconst closeAllConnections = () => {\n\t\tfor (const socket of connections) {\n\t\t\tsocket.destroy(); // forcibly closes the connection\n\t\t\tconnections.delete(socket);\n\t\t}\n\t};\n\n\tconst shutdown = async () => {\n\t\tdebug('startServer: onShutdown');\n\n\t\tadminContext.statsManager.stop();\n\n\t\tawait poolsClose(adminContext.pools);\n\n\t\tserver.close(() => {\n\t\t\tconsole.log('Server has closed');\n\t\t\tprocess.exit(0);\n\t\t});\n\n\t\tcloseAllConnections();\n\t};\n\n\t// Install shutdown handler\n\tinstallShutdown(shutdown);\n\n\t// Listen\n\tdebug('startServer: start listener');\n\tawait new Promise<void>((resolve, reject) => {\n\t\tserver\n\t\t\t.listen(internalConfig.port)\n\t\t\t.on('listening', () => {\n\t\t\t\tdebug('startServer: listener running');\n\t\t\t\tresolve();\n\t\t\t})\n\t\t\t.on('error', (err: NodeJS.ErrnoException) => {\n\t\t\t\tif ('code' in err) {\n\t\t\t\t\tif (err.code === 'EADDRINUSE') {\n\t\t\t\t\t\terr.message = `Port ${internalConfig.port} is already in use`;\n\t\t\t\t\t} else if (err.code === 'EACCES') {\n\t\t\t\t\t\terr.message = `Port ${internalConfig.port} requires elevated privileges`;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\treject(err);\n\t\t\t});\n\t});\n\n\tdebug('startServer: END');\n\n\treturn {\n\t\tconfig: internalConfig,\n\t\tconnectionPools: adminContext.pools,\n\t\tapp,\n\t\tserver,\n\t\tadminContext,\n\t\tshutdown,\n\t};\n};\n\n/**\n * Load configuration.\n * @param filename - The configuration filename.\n * @returns Promise.\n */\nexport const loadConfig = (filename = 'config.json'): configType => z$configType.parse(getJsonFile(filename));\n\n/**\n * Start server from config file.\n * @param filename - The configuration filename.\n * @param ssl - ssl configuration.\n * @returns Promise resolving to the web server object.\n */\nexport const startServerConfig = async (filename = 'config.json', ssl?: sslConfig): Promise<webServer> => startServer(loadConfig(filename), ssl);\n","import debugModule from 'debug';\nconst debug = debugModule('webplsql:shutdown');\n\n/**\n * Install a shutdown handler.\n * @param handler - Shutdown handler\n */\nexport const installShutdown = (handler: () => Promise<void>): void => {\n\tdebug('installShutdown');\n\n\tlet isShuttingDown = false;\n\n\t/*\n\t *\tThe 'unhandledRejection' event is emitted whenever a Promise is rejected and no error handler is attached to the promise within a turn of the event loop.\n\t */\n\tprocess.on('unhandledRejection', (reason) => {\n\t\tif (isShuttingDown) {\n\t\t\treturn;\n\t\t}\n\t\tisShuttingDown = true;\n\n\t\tif (reason instanceof Error) {\n\t\t\tconsole.error(`\\n${reason.message}. Graceful shutdown...`);\n\t\t} else {\n\t\t\tconsole.error('\\nUnhandled promise rejection. Graceful shutdown...', reason);\n\t\t}\n\t\tvoid handler().catch((err: unknown) => {\n\t\t\tconsole.error('Error during shutdown:', err);\n\t\t\tprocess.exit(1);\n\t\t});\n\t});\n\n\t// install signal event handler\n\tprocess.on('SIGTERM', function sigterm() {\n\t\tif (isShuttingDown) {\n\t\t\treturn;\n\t\t}\n\t\tisShuttingDown = true;\n\n\t\tconsole.log('\\nGot SIGTERM (aka docker container stop). Graceful shutdown...');\n\t\tvoid handler().catch((err: unknown) => {\n\t\t\tconsole.error('Error during shutdown:', err);\n\t\t\tprocess.exit(1);\n\t\t});\n\t});\n\n\tprocess.on('SIGINT', function sigint() {\n\t\tif (isShuttingDown) {\n\t\t\treturn;\n\t\t}\n\t\tisShuttingDown = true;\n\n\t\tconsole.log('\\nGot SIGINT (aka ctrl-c in docker). Graceful shutdown...');\n\t\tvoid handler().catch((err: unknown) => {\n\t\t\tconsole.error('Error during shutdown:', err);\n\t\t\tprocess.exit(1);\n\t\t});\n\t});\n};\n\n/**\n * Force a shutdown.\n */\nexport const forceShutdown = (): void => {\n\tdebug('forceShutdown');\n\n\tprocess.kill(process.pid, 'SIGTERM');\n};\n","/**\n * Web PL/SQL Gateway - Common Shared Constants\n *\n * This file centralizes all hardcoded numeric and string constants used throughout\n * the application. Constants are organized by functional category.\n */\n\n// =============================================================================\n// CACHE CONFIGURATION\n// =============================================================================\n\n/**\n * DEFAULT_CACHE_MAX_SIZE = 10000\n *\n * Purpose: Maximum number of entries in the generic LFU (Least Frequently Used) cache.\n *\n * Used By:\n * - procedureNameCache: Caches resolved Oracle procedure names (e.g., \"HR.EMPLOYEES\")\n * - argumentCache: Caches procedure argument introspection results from all_arguments view\n *\n * Related Values:\n * - CACHE_PRUNE_PERCENT (0.1): When cache is full, removes 10% = 1000 entries\n * - Cache instantiation in src/handler/plsql/handlerPlSql.js creates new Cache() without params\n *\n * Implications:\n * - Memory footprint: ~1-2MB at max capacity (strings + hitCount metadata)\n * - Pruning: Removes least-frequently-used entries when full\n * - Higher values = better cache hit rates but more memory\n */\nexport const DEFAULT_CACHE_MAX_SIZE = 10_000;\n\n/**\n * CACHE_PRUNE_PERCENT = 0.1\n *\n * Purpose: Fraction of cache entries to remove during pruning (10%).\n *\n * Used By: Cache.prune() method only\n *\n * Related Values:\n * - DEFAULT_CACHE_MAX_SIZE (10000): Applied to this value to calculate removeCount = 1000\n *\n * Implications:\n * - Balances between removing too few entries (frequent pruning) vs too many (evicting useful data)\n * - 10% is a common pattern for cache eviction\n */\nexport const CACHE_PRUNE_PERCENT = 0.1;\n\n// =============================================================================\n// ORACLE LIMITS\n// =============================================================================\n\n/**\n * MAX_PROCEDURE_PARAMETERS = 1000\n *\n * Purpose: Maximum number of procedure arguments that can be introspected from Oracle's\n * all_arguments view using BULK COLLECT with dbms_utility.name_resolve.\n *\n * Used By:\n * - src/handler/plsql/procedureNamed.js\n * bind.names = {maxArraySize: MAX_PARAMETER_NUMBER}\n * bind.types = {maxArraySize: MAX_PARAMETER_NUMBER}\n *\n * Related Values:\n * - Procedure introspection SQL: SQL_GET_ARGUMENT block at procedureNamed.js:27-43\n * - oracledb.BIND_OUT direction for array fetches\n *\n * Implications:\n * - This is an Oracle driver limitation for array binding, not an arbitrary choice\n * - Procedures with >1000 arguments will have introspection truncated\n * - No error handling exists for this edge case\n * - Practical limit: most procedures have <50 arguments\n */\nexport const MAX_PROCEDURE_PARAMETERS = 1000;\n\n/**\n * OWA_STREAM_CHUNK_SIZE = 1000\n *\n * Purpose: Number of lines fetched per Oracle OWA call when streaming page content.\n *\n * Used By:\n * - owaPageStream.js: maxArraySize for :lines bind variable\n * - owaPageStream.js: :irows INOUT parameter value\n * - owaPageStream.js: Determines when streaming is complete (lines.length < chunkSize)\n *\n * Related Values:\n * - OWA_GET_PAGE_SQL: 'BEGIN owa.get_page(thepage=>:lines, irows=>:irows); END;'\n * - OWAPageStream class constructor at line 20\n *\n * Implications:\n * - Controls round-trip frequency to Oracle database\n * - Higher = fewer round-trips but larger memory buffers per fetch\n * - Lower = more responsive streaming but more database calls\n * - Each line is a PL/SQL varchar2; total data per chunk depends on htp.htbuf_len (63 chars)\n * - Estimated max data per chunk: 1000 lines × 63 chars = 63KB\n */\nexport const OWA_STREAM_CHUNK_SIZE = 1000;\n\n// =============================================================================\n// STREAMING\n// =============================================================================\n\n/**\n * OWA_STREAM_BUFFER_SIZE = 16384\n *\n * Purpose: Node.js Readable stream highWaterMark in bytes (16KB).\n *\n * Used By: OWAPageStream class extends Readable stream\n *\n * Related Values:\n * - OWA_STREAM_CHUNK_SIZE (1000): Lines per fetch\n * - Default Node.js highWaterMark is 64KB (Readable stream default)\n * - OWAPageStream.push() converts lines to string buffer\n *\n * Implications:\n * - Smaller than default (64KB) = more frequent _read() callbacks\n * - Reduces memory footprint for large responses\n * - Improves backpressure handling responsiveness\n * - Trade-off: More CPU for _read() calls vs memory efficiency\n */\nexport const OWA_STREAM_BUFFER_SIZE = 16_384;\n\n/**\n * OWA_RESOLVED_NAME_MAX_LEN = 400\n *\n * Purpose: Maximum string length for resolved Oracle procedure canonical names.\n * Canonical format: SCHEMA.PACKAGE.PROCEDURE or SCHEMA.PROCEDURE.\n *\n * Used By:\n * - resolveProcedureName() function for dbms_utility.name_resolve output\n * - Procedure name resolution SQL at procedureSanitize.js:46-76\n *\n * Related Values:\n * - dbms_utility.name_resolve context = 1 (procedure/function resolution)\n * - Oracle identifier limits: Schema (128) + Package (128) + Procedure (128) + 2 dots = ~386\n * - 400 provides comfortable headroom\n *\n * Implications:\n * - Oracle object names: 30 bytes for most, extended to 128 in some contexts\n * - Canonical name: schema.package.procedure (max ~128+1+128+1+128 = 386)\n * - 400 is safe upper bound with margin\n */\nexport const OWA_RESOLVED_NAME_MAX_LEN = 400;\n\n// =============================================================================\n// STATS COLLECTION\n// =============================================================================\n\n/**\n * STATS_INTERVAL_MS = 5000\n *\n * Purpose: Duration of each statistical bucket in milliseconds.\n *\n * Used By:\n * - src/util/statsManager.js:165: setInterval(this.rotateBucket, this.config.intervalMs)\n * - src/handler/handlerAdmin.js:123: Exposed as intervalMs in /api/status response\n * - src/frontend/main.ts\n * - src/frontend/charts.ts\n *\n * Related Values:\n * - MAX_HISTORY_BUCKETS (1000): At 5s per bucket = ~83 minutes of history\n * - MAX_PERCENTILE_SAMPLES (1000): Samples per bucket for P95/P99\n *\n * Implications:\n * - Bucket aggregation: request counts, durations, errors, system metrics\n * - Affects granularity of performance monitoring\n * - Lower values = more granular but more history entries\n * - Higher values = smoother averages but less detail\n */\nexport const STATS_INTERVAL_MS = 5000;\n\n/**\n * MAX_HISTORY_BUCKETS = 1000\n *\n * Purpose: Maximum number of statistical buckets retained in StatsManager ring buffer.\n *\n * Used By:\n * - src/util/statsManager.js: Ring buffer limit check\n * if (this.history.length > this.config.maxHistoryPoints) { this.history.shift(); }\n * - Exposed via /api/stats/history endpoint\n *\n * Related Values:\n * - STATS_INTERVAL_MS (5000): 5s per bucket = ~83 minutes total history\n * - Each bucket contains: timestamp, requestCount, errors, durations, system metrics\n * - Bucket memory estimate: ~100 bytes × 1000 = ~100KB\n *\n * Implications:\n * - Ring buffer: oldest bucket is removed when new one exceeds limit\n * - Affects admin console chart history display\n * - Higher = more historical context but more memory\n */\nexport const MAX_HISTORY_BUCKETS = 1000;\n\n/**\n * MAX_PERCENTILE_SAMPLES = 1000\n *\n * Purpose: Maximum number of request duration samples collected per bucket\n * for calculating P95/P99 percentiles.\n *\n * Used By:\n * - src/util/statsManager.js: Array length check\n * if (b.durations.length < this.config.percentilePrecision)\n * - src/util/statsManager.js: P95/P99 calculation\n *\n * Related Values:\n * - Percentile calculation: floor(length × 0.95) and floor(length × 0.99)\n * - FIFO replacement: When exceeded, new samples replace oldest\n *\n * Implications:\n * - With 1000 samples, P95/P99 are statistically meaningful\n * - Higher = more accurate percentiles but more memory per bucket\n * - With STATS_INTERVAL_MS = 5000, 1000 samples ≈ 5 req/sec sustained\n * - Under heavy load, older samples are discarded (FIFO)\n */\nexport const MAX_PERCENTILE_SAMPLES = 1000;\n\n// =============================================================================\n// SHUTDOWN\n// =============================================================================\n\n/**\n * SHUTDOWN_GRACE_DELAY_MS = 100\n *\n * Purpose: Delay between initiating server shutdown and forced termination.\n *\n * Used By: POST /api/server/stop handler only\n *\n * Related Values:\n * - forceShutdown() at src/util/shutdown.js\n * - SIGTERM/SIGINT handlers at shutdown.js\n *\n * Implications:\n * - Allows graceful completion of in-flight requests\n * - Gives Express middleware time to send final responses\n * - 100ms is short; may be insufficient under heavy load\n * - Consider making configurable for high-traffic deployments\n */\nexport const SHUTDOWN_GRACE_DELAY_MS = 100;\n\n// =============================================================================\n// LOG ROTATION - TRACE\n// =============================================================================\n\n/**\n * TRACE_LOG_ROTATION_SIZE = '10M'\n *\n * Purpose: Log file size threshold triggering trace log rotation (10 Megabytes).\n *\n * Used By: rotating-file-stream library for 'trace.log'\n *\n * Related Values:\n * - TRACE_LOG_ROTATION_INTERVAL ('1d'): Also triggers rotation\n * - TRACE_LOG_MAX_ROTATED_FILES (10): Maximum retained files\n *\n * Implications:\n * - When either size OR time threshold is reached, rotation occurs\n * - Combined with daily rotation: ~10MB/day minimum\n * - gzip compression reduces rotated file size by ~70-90%\n */\nexport const TRACE_LOG_ROTATION_SIZE = '10M';\n\n/**\n * TRACE_LOG_ROTATION_INTERVAL = '1d'\n *\n * Purpose: Time-based trace log rotation trigger (daily).\n *\n * Used By: rotating-file-stream library for 'trace.log'\n *\n * Implications:\n * - Guarantees at least one rotation per day\n * - Midnight-based or 24h from first write\n */\nexport const TRACE_LOG_ROTATION_INTERVAL = '1d';\n\n/**\n * TRACE_LOG_MAX_ROTATED_FILES = 10\n *\n * Purpose: Maximum number of rotated trace log files to retain.\n *\n * Used By: rotating-file-stream library for 'trace.log'\n *\n * Implications:\n * - When exceeded, oldest rotated file is deleted\n * - Maximum: ~10 files × 10MB = ~100MB (compressed: ~10-30MB)\n */\nexport const TRACE_LOG_MAX_ROTATED_FILES = 10;\n\n// =============================================================================\n// LOG ROTATION - JSON\n// =============================================================================\n\n/**\n * JSON_LOG_ROTATION_SIZE = '10M'\n *\n * Purpose: Log file size threshold triggering JSON error log rotation (10 Megabytes).\n *\n * Used By: rotating-file-stream library for 'error.json.log'\n *\n * Related Values:\n * - JSON_LOG_ROTATION_INTERVAL ('1d'): Also triggers rotation\n * - JSON_LOG_MAX_ROTATED_FILES (10): Maximum retained files\n *\n * Implications:\n * - When either size OR time threshold is reached, rotation occurs\n * - Combined with daily rotation: ~10MB/day minimum\n * - gzip compression reduces rotated file size by ~70-90%\n */\nexport const JSON_LOG_ROTATION_SIZE = '10M';\n\n/**\n * JSON_LOG_ROTATION_INTERVAL = '1d'\n *\n * Purpose: Time-based JSON error log rotation trigger (daily).\n *\n * Used By: rotating-file-stream library for 'error.json.log'\n *\n * Implications:\n * - Guarantees at least one rotation per day\n * - Midnight-based or 24h from first write\n */\nexport const JSON_LOG_ROTATION_INTERVAL = '1d';\n\n/**\n * JSON_LOG_MAX_ROTATED_FILES = 10\n *\n * Purpose: Maximum number of rotated JSON error log files to retain.\n *\n * Used By: rotating-file-stream library for 'error.json.log'\n *\n * Implications:\n * - When exceeded, oldest rotated file is deleted\n * - Maximum: ~10 files × 10MB = ~100MB (compressed: ~10-30MB)\n */\nexport const JSON_LOG_MAX_ROTATED_FILES = 10;\n","import debugModule from 'debug';\nimport os from 'node:os';\nimport {STATS_INTERVAL_MS, MAX_HISTORY_BUCKETS, MAX_PERCENTILE_SAMPLES} from '../../common/constants.ts';\n\nconst debug = debugModule('webplsql:statsManager');\n\ntype StatsConfig = {\n\tintervalMs: number;\n\tmaxHistoryPoints: number;\n\tsampleSystem: boolean;\n\tsamplePools: boolean;\n\tpercentilePrecision: number;\n};\n\ntype CacheStats = {\n\tsize: number;\n\thits: number;\n\tmisses: number;\n};\n\nexport type PoolCacheSnapshot = {\n\tprocedureName: CacheStats;\n\targument: CacheStats;\n};\n\nexport type PoolSnapshot = {\n\tname: string;\n\tconnectionsInUse: number;\n\tconnectionsOpen: number;\n\tcache?: PoolCacheSnapshot;\n};\n\ntype Bucket = {\n\ttimestamp: number;\n\trequests: number;\n\terrors: number;\n\tdurationMin: number;\n\tdurationMax: number;\n\tdurationAvg: number;\n\tdurationP95: number;\n\tdurationP99: number;\n\tsystem: {\n\t\tcpu: number;\n\t\theapUsed: number;\n\t\theapTotal: number;\n\t\trss: number;\n\t\texternal: number;\n\t};\n\tpools: PoolSnapshot[];\n};\n\ntype CurrentBucket = {\n\tcount: number;\n\terrors: number;\n\tdurationSum: number;\n\tdurationMin: number;\n\tdurationMax: number;\n\tdurations: number[];\n};\n\ntype MemoryLifetime = {\n\theapUsedMax: number;\n\theapTotalMax: number;\n\trssMax: number;\n\texternalMax: number;\n};\n\ntype LifetimeStats = {\n\ttotalRequests: number;\n\ttotalErrors: number;\n\tminDuration: number;\n\tmaxDuration: number;\n\ttotalDuration: number;\n\tmaxRequestsPerSecond: number;\n\tmemory: MemoryLifetime;\n\tcpu: {\n\t\tmax: number;\n\t\tuserMax: number;\n\t\tsystemMax: number;\n\t};\n};\n\ntype StatsSummary = {\n\tstartTime: Date;\n\ttotalRequests: number;\n\ttotalErrors: number;\n\tavgResponseTime: number;\n\tminResponseTime: number;\n\tmaxResponseTime: number;\n\tmaxRequestsPerSecond: number;\n\tmaxMemory: MemoryLifetime;\n\tcpu: {\n\t\tmax: number;\n\t\tuserMax: number;\n\t\tsystemMax: number;\n\t};\n};\n\n/**\n * Manager for statistical data collection and temporal bucketing.\n */\nexport class StatsManager {\n\tconfig: StatsConfig;\n\tstartTime: Date;\n\thistory: Bucket[];\n\tlifetime: LifetimeStats;\n\t_currentBucket: CurrentBucket;\n\t_lastCpuTimes: {user: number; nice: number; sys: number; idle: number; irq: number; total: number};\n\t_timer: NodeJS.Timeout | undefined;\n\n\t/**\n\t * @param config - Configuration.\n\t */\n\tconstructor(config: Partial<StatsConfig> = {}) {\n\t\tthis.config = {\n\t\t\tintervalMs: STATS_INTERVAL_MS,\n\t\t\tmaxHistoryPoints: MAX_HISTORY_BUCKETS,\n\t\t\tsampleSystem: true,\n\t\t\tsamplePools: true,\n\t\t\tpercentilePrecision: MAX_PERCENTILE_SAMPLES,\n\t\t\t...config,\n\t\t};\n\n\t\tthis.startTime = new Date();\n\t\tthis.history = [];\n\n\t\tthis.lifetime = {\n\t\t\ttotalRequests: 0,\n\t\t\ttotalErrors: 0,\n\t\t\tminDuration: -1,\n\t\t\tmaxDuration: -1,\n\t\t\ttotalDuration: 0,\n\t\t\tmaxRequestsPerSecond: 0,\n\t\t\tmemory: {\n\t\t\t\theapUsedMax: 0,\n\t\t\t\theapTotalMax: 0,\n\t\t\t\trssMax: 0,\n\t\t\t\texternalMax: 0,\n\t\t\t},\n\t\t\tcpu: {\n\t\t\t\tmax: 0,\n\t\t\t\tuserMax: 0,\n\t\t\t\tsystemMax: 0,\n\t\t\t},\n\t\t};\n\n\t\tthis._currentBucket = {\n\t\t\tcount: 0,\n\t\t\terrors: 0,\n\t\t\tdurations: [],\n\t\t\tdurationSum: 0,\n\t\t\tdurationMin: -1,\n\t\t\tdurationMax: -1,\n\t\t};\n\n\t\tthis._lastCpuTimes = this._getSystemCpuTimes();\n\n\t\tthis._timer = undefined;\n\t\tif (this.config.sampleSystem) {\n\t\t\tthis._timer = setInterval(() => {\n\t\t\t\tthis.rotateBucket();\n\t\t\t}, this.config.intervalMs);\n\t\t\tif (this._timer && typeof this._timer.unref === 'function') {\n\t\t\t\tthis._timer.unref();\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * Reset the current bucket accumulator.\n\t */\n\tprivate _resetBucket(): void {\n\t\tthis._currentBucket = {\n\t\t\tcount: 0,\n\t\t\terrors: 0,\n\t\t\tdurations: [],\n\t\t\tdurationSum: 0,\n\t\t\tdurationMin: -1,\n\t\t\tdurationMax: -1,\n\t\t};\n\t}\n\n\t/**\n\t * Record a request event.\n\t * @param duration - Duration in milliseconds.\n\t * @param isError - Whether the request was an error.\n\t */\n\trecordRequest(duration: number, isError = false): void {\n\t\tthis.lifetime.totalRequests++;\n\t\tif (isError) {\n\t\t\tthis.lifetime.totalErrors++;\n\t\t}\n\n\t\tthis.lifetime.totalDuration += duration;\n\t\tif (this.lifetime.minDuration < 0 || duration < this.lifetime.minDuration) {\n\t\t\tthis.lifetime.minDuration = duration;\n\t\t}\n\t\tif (this.lifetime.maxDuration < 0 || duration > this.lifetime.maxDuration) {\n\t\t\tthis.lifetime.maxDuration = duration;\n\t\t}\n\n\t\tconst b = this._currentBucket;\n\t\tb.count++;\n\t\tif (isError) {\n\t\t\tb.errors++;\n\t\t}\n\n\t\tb.durationSum += duration;\n\t\tif (b.durationMin < 0 || duration < b.durationMin) {\n\t\t\tb.durationMin = duration;\n\t\t}\n\t\tif (b.durationMax < 0 || duration > b.durationMax) {\n\t\t\tb.durationMax = duration;\n\t\t}\n\n\t\tif (b.durations.length < this.config.percentilePrecision) {\n\t\t\tb.durations.push(duration);\n\t\t}\n\t}\n\n\t/**\n\t * Get system CPU times.\n\t * @returns System CPU times.\n\t */\n\tprivate _getSystemCpuTimes(): {user: number; nice: number; sys: number; idle: number; irq: number; total: number} {\n\t\tconst cpus = os.cpus();\n\t\tlet user = 0;\n\t\tlet nice = 0;\n\t\tlet sys = 0;\n\t\tlet idle = 0;\n\t\tlet irq = 0;\n\n\t\tfor (const cpu of cpus) {\n\t\t\tuser += cpu.times.user;\n\t\t\tnice += cpu.times.nice;\n\t\t\tsys += cpu.times.sys;\n\t\t\tidle += cpu.times.idle;\n\t\t\tirq += cpu.times.irq;\n\t\t}\n\n\t\tconst total = user + nice + sys + idle + irq;\n\t\treturn {user, nice, sys, idle, irq, total};\n\t}\n\n\t/**\n\t * Calculate CPU usage percentage since last call.\n\t * @returns CPU usage percentage (0-100).\n\t */\n\tprivate _calculateCpuUsage(): number {\n\t\tconst current = this._getSystemCpuTimes();\n\t\tconst last = this._lastCpuTimes || {user: 0, nice: 0, sys: 0, idle: 0, irq: 0, total: 0};\n\n\t\tconst deltaTotal = current.total - last.total;\n\t\tconst deltaIdle = current.idle - last.idle;\n\n\t\tthis._lastCpuTimes = current;\n\n\t\tif (deltaTotal <= 0) return 0;\n\n\t\tconst percent = ((deltaTotal - deltaIdle) / deltaTotal) * 100;\n\t\treturn Math.min(100, Math.max(0, percent));\n\t}\n\n\t/**\n\t * Rotate the current bucket into history and start a new one.\n\t * @param poolSnapshots - Optional pool snapshots to include.\n\t */\n\trotateBucket(poolSnapshots: PoolSnapshot[] = []): void {\n\t\tconst b = this._currentBucket;\n\t\tconst memUsage = process.memoryUsage();\n\t\tconst systemMemoryUsed = os.totalmem() - os.freemem();\n\t\tconst cpuUsage = process.cpuUsage();\n\t\tconst cpu = this._calculateCpuUsage();\n\n\t\t// Update lifetime extremes\n\t\tconst reqPerSec = b.count / (this.config.intervalMs / 1000);\n\t\tthis.lifetime.maxRequestsPerSecond = Math.max(this.lifetime.maxRequestsPerSecond, reqPerSec);\n\t\tthis.lifetime.memory.heapUsedMax = Math.max(this.lifetime.memory.heapUsedMax, memUsage.heapUsed);\n\t\tthis.lifetime.memory.heapTotalMax = Math.max(this.lifetime.memory.heapTotalMax, memUsage.heapTotal);\n\t\tthis.lifetime.memory.rssMax = Math.max(this.lifetime.memory.rssMax, systemMemoryUsed);\n\t\tthis.lifetime.memory.externalMax = Math.max(this.lifetime.memory.externalMax, memUsage.external);\n\t\tthis.lifetime.cpu.max = Math.max(this.lifetime.cpu.max, cpu);\n\t\tthis.lifetime.cpu.userMax = Math.max(this.lifetime.cpu.userMax, cpuUsage.user);\n\t\tthis.lifetime.cpu.systemMax = Math.max(this.lifetime.cpu.systemMax, cpuUsage.system);\n\n\t\tlet p95 = 0;\n\t\tlet p99 = 0;\n\t\tif (b.durations.length > 0) {\n\t\t\tconst sorted = b.durations.toSorted((x, y) => x - y);\n\t\t\tconst p95Idx = Math.floor(sorted.length * 0.95);\n\t\t\tconst p99Idx = Math.floor(sorted.length * 0.99);\n\t\t\tconst lastIdx = sorted.length - 1;\n\n\t\t\tp95 = sorted[p95Idx] ?? sorted[lastIdx] ?? 0;\n\t\t\tp99 = sorted[p99Idx] ?? sorted[lastIdx] ?? 0;\n\t\t}\n\n\t\tconst bucket: Bucket = {\n\t\t\ttimestamp: Date.now(),\n\t\t\trequests: b.count,\n\t\t\terrors: b.errors,\n\t\t\tdurationMin: Math.max(b.durationMin, 0),\n\t\t\tdurationMax: Math.max(b.durationMax, 0),\n\t\t\tdurationAvg: b.count > 0 ? b.durationSum / b.count : 0,\n\t\t\tdurationP95: p95,\n\t\t\tdurationP99: p99,\n\t\t\tsystem: {\n\t\t\t\tcpu,\n\t\t\t\theapUsed: memUsage.heapUsed,\n\t\t\t\theapTotal: memUsage.heapTotal,\n\t\t\t\trss: systemMemoryUsed,\n\t\t\t\texternal: memUsage.external,\n\t\t\t},\n\t\t\tpools: poolSnapshots,\n\t\t};\n\n\t\tthis.history.push(bucket);\n\t\tif (this.history.length > this.config.maxHistoryPoints) {\n\t\t\tthis.history.shift();\n\t\t}\n\n\t\tthis._resetBucket();\n\t\tdebug('Bucket rotated: %j', bucket);\n\t}\n\n\t/**\n\t * Stop the background timer.\n\t */\n\tstop(): void {\n\t\tif (this._timer) {\n\t\t\tclearInterval(this._timer);\n\t\t\tthis._timer = undefined;\n\t\t}\n\t}\n\n\t/**\n\t * Get lifetime summary.\n\t * @returns Summary.\n\t */\n\tgetSummary(): StatsSummary {\n\t\treturn {\n\t\t\tstartTime: this.startTime,\n\t\t\ttotalRequests: this.lifetime.totalRequests,\n\t\t\ttotalErrors: this.lifetime.totalErrors,\n\t\t\tavgResponseTime: this.lifetime.totalRequests > 0 ? this.lifetime.totalDuration / this.lifetime.totalRequests : 0,\n\t\t\tminResponseTime: this.lifetime.minDuration,\n\t\t\tmaxResponseTime: this.lifetime.maxDuration,\n\t\t\tmaxRequestsPerSecond: this.lifetime.maxRequestsPerSecond,\n\t\t\tmaxMemory: this.lifetime.memory,\n\t\t\tcpu: this.lifetime.cpu,\n\t\t};\n\t}\n\n\t/**\n\t * Get history buffer.\n\t * @returns The history buffer.\n\t */\n\tgetHistory(): Bucket[] {\n\t\treturn this.history;\n\t}\n}\n","import {StatsManager} from '../util/statsManager.ts';\nimport type {Pool} from 'oracledb';\nimport type {configType, argsType} from '../types.ts';\nimport type {Cache} from '../util/cache.ts';\n\ntype PoolCacheEntry = {\n\tpoolName: string;\n\tprocedureNameCache: Cache<string>;\n\targumentCache: Cache<argsType>;\n};\n\n/**\n * Admin Context Class\n */\nexport class AdminContext {\n\treadonly startTime: Date;\n\treadonly config: configType;\n\treadonly pools: Pool[];\n\treadonly caches: PoolCacheEntry[];\n\treadonly statsManager: StatsManager;\n\tprivate _paused: boolean;\n\n\tconstructor(config: configType) {\n\t\tthis.startTime = new Date();\n\t\tthis.config = config;\n\t\tthis.pools = [];\n\t\tthis.caches = [];\n\t\tthis.statsManager = new StatsManager();\n\t\tthis._paused = false;\n\t}\n\n\t/**\n\t * Register a PL/SQL handler with the admin context.\n\t * @param route - The route for the handler.\n\t * @param pool - The connection pool.\n\t * @param procedureNameCache - The procedure name cache.\n\t * @param argumentCache - The argument cache.\n\t */\n\tregisterHandler(route: string, pool: Pool, procedureNameCache: Cache<string>, argumentCache: Cache<argsType>): void {\n\t\tthis.pools.push(pool);\n\t\tthis.caches.push({\n\t\t\tpoolName: route,\n\t\t\tprocedureNameCache,\n\t\t\targumentCache,\n\t\t});\n\t}\n\n\tget paused(): boolean {\n\t\treturn this._paused;\n\t}\n\n\tsetPaused(value: boolean): void {\n\t\tthis._paused = value;\n\t}\n}\n","import {promises as fs, readFileSync} from 'node:fs';\n\n/**\n * Read file.\n *\n * @param filePath - File name.\n * @returns The string.\n */\nexport const readFileSyncUtf8 = (filePath: string): string => {\n\ttry {\n\t\treturn readFileSync(filePath, 'utf8');\n\t} catch {\n\t\tthrow new Error(`Unable to read file \"${filePath}\"`);\n\t}\n};\n\n/**\n * Read file.\n *\n * @param filePath - File name.\n * @returns The buffer.\n */\nexport const readFile = async (filePath: string): Promise<Buffer> => {\n\ttry {\n\t\treturn await fs.readFile(filePath);\n\t} catch {\n\t\tthrow new Error(`Unable to read file \"${filePath}\"`);\n\t}\n};\n\n/**\n * Remove file.\n *\n * @param filePath - File name.\n */\nexport const removeFile = async (filePath: string): Promise<void> => {\n\ttry {\n\t\tawait fs.unlink(filePath);\n\t} catch {\n\t\tthrow new Error(`Unable to remove file \"${filePath}\"`);\n\t}\n};\n\n/**\n * Load a json file.\n *\n * @param filePath - File name.\n * @returns The json object.\n */\nexport const getJsonFile = (filePath: string): unknown => {\n\ttry {\n\t\tconst fileContent = readFileSync(filePath, 'utf8');\n\t\treturn JSON.parse(fileContent);\n\t} catch {\n\t\tthrow new Error(`Unable to load file \"${filePath}\"`);\n\t}\n};\n\n/**\n * Is this a directory.\n * @param directoryPath - Directory name.\n * @returns Return true if it is a directory.\n */\nexport const isDirectory = async (directoryPath: unknown): Promise<boolean> => {\n\tif (typeof directoryPath !== 'string') {\n\t\treturn false;\n\t}\n\n\tconst stats = await fs.stat(directoryPath);\n\n\treturn stats.isDirectory();\n};\n\n/**\n * Is this a file.\n * @param filePath - File name.\n * @returns Return true if it is a file.\n */\nexport const isFile = async (filePath: unknown): Promise<boolean> => {\n\tif (typeof filePath !== 'string') {\n\t\treturn false;\n\t}\n\n\ttry {\n\t\tconst stats = await fs.stat(filePath);\n\t\treturn stats.isFile();\n\t} catch {\n\t\treturn false;\n\t}\n};\n","/*\n *\tHtml utilities\n */\n\n/**\n * Escape html string.\n *\n * @param value - The value.\n * @returns The escaped value.\n */\nexport const escapeHtml = (value: string): string =>\n\tvalue.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('\"', '&quot;').replaceAll(\"'\", '&#39;');\n\n/**\n *\tConvert LF and/or CR to <br>\n *\t@param text - The text to convert.\n *\t@returns The converted text.\n */\nexport const convertAsciiToHtm