web_plsql 0.17.1 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +24 -4
  2. package/package.json +21 -8
  3. package/src/admin/client/charts.ts +299 -0
  4. package/src/admin/client/main.js +3 -0
  5. package/src/admin/client/tailwind.css +41 -0
  6. package/src/admin/favicon.svg +3 -0
  7. package/src/admin/index.html +315 -0
  8. package/src/admin/js/api.ts +95 -0
  9. package/src/admin/js/app.ts +306 -0
  10. package/src/admin/js/eslint.config.js +74 -0
  11. package/src/admin/js/schemas.ts +153 -0
  12. package/src/admin/js/templates/config.ts +146 -0
  13. package/src/admin/js/templates/errorRow.ts +18 -0
  14. package/src/admin/js/templates/index.ts +3 -0
  15. package/src/admin/js/templates/poolCard.ts +61 -0
  16. package/src/admin/js/tsconfig.json +24 -0
  17. package/src/admin/js/types.ts +223 -0
  18. package/src/admin/js/ui/theme.ts +93 -0
  19. package/src/admin/js/ui/views.ts +164 -0
  20. package/src/admin/js/util/format.ts +27 -0
  21. package/src/admin/lib/assets/main-zpdhQ1gD.css +1 -0
  22. package/src/admin/lib/chart.bundle.js +139 -0
  23. package/src/admin/style.css +1321 -0
  24. package/src/bin/load-test.js +202 -0
  25. package/src/handler/handlerAdmin.js +198 -0
  26. package/src/handler/plsql/cgi.js +1 -1
  27. package/src/handler/plsql/errorPage.js +37 -12
  28. package/src/handler/plsql/handlerPlSql.js +32 -6
  29. package/src/handler/plsql/parsePage.js +15 -11
  30. package/src/handler/plsql/procedure.js +81 -33
  31. package/src/handler/plsql/procedureNamed.js +18 -52
  32. package/src/handler/plsql/procedureSanitize.js +96 -49
  33. package/src/handler/plsql/procedureVariable.js +2 -2
  34. package/src/handler/plsql/request.js +11 -6
  35. package/src/handler/plsql/sendResponse.js +43 -11
  36. package/src/handler/plsql/stream.js +1 -1
  37. package/src/handler/plsql/upload.js +1 -1
  38. package/src/server/config.js +1 -0
  39. package/src/server/server.js +108 -2
  40. package/src/types.js +7 -1
  41. package/src/util/cache.js +123 -0
  42. package/src/util/file.js +5 -9
  43. package/src/util/jsonLogger.js +47 -0
  44. package/src/util/shutdown.js +6 -2
  45. package/src/util/trace.js +5 -5
  46. package/src/version.js +1 -1
  47. package/types/handler/handlerAdmin.d.ts +9 -0
  48. package/types/handler/plsql/errorPage.d.ts +1 -0
  49. package/types/handler/plsql/handlerPlSql.d.ts +6 -1
  50. package/types/handler/plsql/procedure.d.ts +3 -1
  51. package/types/handler/plsql/procedureNamed.d.ts +2 -5
  52. package/types/handler/plsql/procedureSanitize.d.ts +2 -5
  53. package/types/handler/plsql/procedureVariable.d.ts +1 -1
  54. package/types/handler/plsql/request.d.ts +3 -1
  55. package/types/handler/plsql/sendResponse.d.ts +1 -1
  56. package/types/server/server.d.ts +22 -0
  57. package/types/types.d.ts +19 -1
  58. package/types/util/cache.d.ts +69 -0
  59. package/types/util/jsonLogger.d.ts +45 -0
  60. package/types/handler/plsql/stream.d.ts +0 -2
@@ -0,0 +1,202 @@
1
+ /**
2
+ * @file Load testing CLI tool for web_plsql.
3
+ * This tool simulates parallel traffic to the middleware and provides real-time statistics.
4
+ */
5
+
6
+ import {performance} from 'node:perf_hooks';
7
+
8
+ /**
9
+ * @typedef {object} Metrics
10
+ * @property {number} total Total requests made.
11
+ * @property {number} success Number of successful requests (2xx).
12
+ * @property {number} error Number of failed requests.
13
+ * @property {number} minLatency Minimum latency in milliseconds.
14
+ * @property {number} maxLatency Maximum latency in milliseconds.
15
+ * @property {number} sumLatency Sum of all latencies for average calculation.
16
+ * @property {Map<string|number, number>} statusCodes Frequency of status codes.
17
+ */
18
+
19
+ /** @type {Metrics} */
20
+ const metrics = {
21
+ total: 0,
22
+ success: 0,
23
+ error: 0,
24
+ minLatency: Number.MAX_SAFE_INTEGER,
25
+ maxLatency: 0,
26
+ sumLatency: 0,
27
+ statusCodes: new Map(),
28
+ };
29
+
30
+ let stopped = false;
31
+
32
+ /**
33
+ * Parse CLI arguments.
34
+ * @returns {{ url: string, concurrency: number, duration: number }} Options object.
35
+ */
36
+ function parseArgs() {
37
+ const args = process.argv.slice(2);
38
+ const options = {
39
+ url: '',
40
+ concurrency: 10,
41
+ duration: 30,
42
+ };
43
+
44
+ for (let i = 0; i < args.length; i++) {
45
+ const arg = args[i];
46
+ const nextArg = args[i + 1] ?? '';
47
+
48
+ if (arg === '--url' || arg === '-u') {
49
+ options.url = nextArg;
50
+ i++;
51
+ } else if (arg === '--concurrency' || arg === '-c') {
52
+ options.concurrency = parseInt(nextArg, 10);
53
+ i++;
54
+ } else if (arg === '--duration' || arg === '-d') {
55
+ options.duration = parseInt(nextArg, 10);
56
+ i++;
57
+ }
58
+ }
59
+
60
+ if (!options.url) {
61
+ console.error('Usage: node src/bin/load-test.js --url <url> [-c concurrency] [-d duration]');
62
+ process.exit(1);
63
+ }
64
+
65
+ return options;
66
+ }
67
+
68
+ /**
69
+ * Record a single request's result.
70
+ * @param {boolean} ok Whether the request succeeded.
71
+ * @param {number} latency Latency in milliseconds.
72
+ * @param {number|string} status Status code or error string.
73
+ */
74
+ function recordMetrics(ok, latency, status) {
75
+ metrics.total++;
76
+ if (ok) {
77
+ metrics.success++;
78
+ } else {
79
+ metrics.error++;
80
+ }
81
+
82
+ metrics.minLatency = Math.min(metrics.minLatency, latency);
83
+ metrics.maxLatency = Math.max(metrics.maxLatency, latency);
84
+ metrics.sumLatency += latency;
85
+
86
+ const count = metrics.statusCodes.get(status) ?? 0;
87
+ metrics.statusCodes.set(status, count + 1);
88
+ }
89
+
90
+ /**
91
+ * Clear the console and move cursor to the top.
92
+ */
93
+ function clearConsole() {
94
+ process.stdout.write('\x1b[2J\x1b[0;0H');
95
+ }
96
+
97
+ /**
98
+ * Render the dashboard.
99
+ * @param {number} elapsed Elapsed time in seconds.
100
+ * @param {number} duration Total duration in seconds.
101
+ * @param {string} url Target URL.
102
+ * @param {number} concurrency Concurrency level.
103
+ */
104
+ function renderDashboard(elapsed, duration, url, concurrency) {
105
+ clearConsole();
106
+ const rps = elapsed > 0 ? (metrics.total / elapsed).toFixed(1) : '0.0';
107
+ const avgLatency = metrics.total > 0 ? (metrics.sumLatency / metrics.total).toFixed(1) : '0.0';
108
+ const progress = Math.min(100, (elapsed / duration) * 100).toFixed(1);
109
+ const barWidth = 30;
110
+ const filledWidth = Math.floor((barWidth * parseFloat(progress)) / 100);
111
+ const bar = '='.repeat(filledWidth) + '>'.padEnd(barWidth - filledWidth, ' ');
112
+
113
+ console.log('\x1b[1m\x1b[36mWeb PL/SQL Load Test\x1b[0m');
114
+ console.log('='.repeat(40));
115
+ console.log(`\x1b[33mURL:\x1b[0m ${url}`);
116
+ console.log(`\x1b[33mConcurrency:\x1b[0m ${concurrency}`);
117
+ console.log(`\x1b[33mDuration:\x1b[0m ${duration}s`);
118
+ console.log('='.repeat(40));
119
+ console.log(`\x1b[32mProgress:\x1b[0m [${bar}] ${progress}% (${elapsed.toFixed(1)}s)`);
120
+ console.log(`\x1b[32mRPS:\x1b[0m ${rps}`);
121
+ console.log(`\x1b[32mTotal:\x1b[0m ${metrics.total}`);
122
+ console.log(`\x1b[32mSuccess:\x1b[0m ${metrics.success}`);
123
+ console.log(`\x1b[31mErrors:\x1b[0m ${metrics.error}`);
124
+ console.log('='.repeat(40));
125
+ console.log('\x1b[1mLatency (ms)\x1b[0m');
126
+ console.log(`\x1b[34mAvg:\x1b[0m ${avgLatency.padStart(8)}`);
127
+ console.log(`\x1b[34mMin:\x1b[0m ${(metrics.minLatency === Number.MAX_SAFE_INTEGER ? 0 : metrics.minLatency).toFixed(1).padStart(8)}`);
128
+ console.log(`\x1b[34mMax:\x1b[0m ${metrics.maxLatency.toFixed(1).padStart(8)}`);
129
+ console.log('='.repeat(40));
130
+
131
+ if (metrics.statusCodes.size > 0) {
132
+ console.log('\x1b[1mStatus Codes\x1b[0m');
133
+ for (const [code, count] of metrics.statusCodes.entries()) {
134
+ const numericCode = typeof code === 'number' ? code : 0;
135
+ const color = numericCode >= 200 && numericCode < 300 ? '\x1b[32m' : '\x1b[31m';
136
+ console.log(`${color}${code}\x1b[0m: ${count}`);
137
+ }
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Worker function to maintain concurrency.
143
+ * @param {string} url The target URL.
144
+ * @param {number} endTime The end time in milliseconds.
145
+ */
146
+ async function runWorker(url, endTime) {
147
+ while (performance.now() < endTime && !stopped) {
148
+ const start = performance.now();
149
+ try {
150
+ const res = await fetch(url, {
151
+ signal: AbortSignal.timeout(10000), // 10s timeout
152
+ });
153
+ const latency = performance.now() - start;
154
+ recordMetrics(res.ok, latency, res.status);
155
+ // Consume body to free up connection
156
+ await res.arrayBuffer();
157
+ } catch (err) {
158
+ const latency = performance.now() - start;
159
+ const errorName = err instanceof Error ? err.name : 'UnknownError';
160
+ recordMetrics(false, latency, errorName === 'TimeoutError' ? 'Timeout' : 'NetworkErr');
161
+ }
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Main function.
167
+ */
168
+ async function main() {
169
+ const options = parseArgs();
170
+ const startTime = performance.now();
171
+ const endTime = startTime + options.duration * 1000;
172
+
173
+ console.log(`Starting load test on ${options.url}...`);
174
+
175
+ // Start workers
176
+ const workers = Array.from({length: options.concurrency}, () => runWorker(options.url, endTime));
177
+
178
+ // Dashboard refresh interval
179
+ const uiInterval = setInterval(() => {
180
+ const elapsed = (performance.now() - startTime) / 1000;
181
+ renderDashboard(elapsed, options.duration, options.url, options.concurrency);
182
+ }, 200);
183
+
184
+ // Handle Ctrl+C
185
+ process.on('SIGINT', () => {
186
+ stopped = true;
187
+ console.log('\nStopping...');
188
+ });
189
+
190
+ await Promise.all(workers);
191
+ clearInterval(uiInterval);
192
+
193
+ // Final render
194
+ const finalElapsed = (performance.now() - startTime) / 1000;
195
+ renderDashboard(finalElapsed, options.duration, options.url, options.concurrency);
196
+ console.log('\n\x1b[1m\x1b[32mTest Complete!\x1b[0m\n');
197
+ }
198
+
199
+ main().catch((/** @type {unknown} */ err) => {
200
+ console.error('Fatal error:', err);
201
+ process.exit(1);
202
+ });
@@ -0,0 +1,198 @@
1
+ import express from 'express';
2
+ import fs from 'node:fs';
3
+ import readline from 'node:readline';
4
+ import {AdminContext} from '../server/server.js';
5
+ import {getVersion} from '../version.js';
6
+
7
+ const version = getVersion();
8
+ import {forceShutdown} from '../util/shutdown.js';
9
+
10
+ /**
11
+ * @typedef {import('express').Request} Request
12
+ * @typedef {import('express').Response} Response
13
+ * @typedef {import('express').NextFunction} NextFunction
14
+ */
15
+
16
+ export const handlerAdmin = express.Router();
17
+
18
+ /**
19
+ * Helper to read last N lines of a file
20
+ * @param {string} filePath - Path to file
21
+ * @param {number} n - Number of lines
22
+ * @returns {Promise<string[]>} - The lines
23
+ */
24
+ const readLastLines = async (filePath, n = 100) => {
25
+ if (!fs.existsSync(filePath)) {
26
+ return [];
27
+ }
28
+
29
+ const fileStream = fs.createReadStream(filePath);
30
+ const rl = readline.createInterface({
31
+ input: fileStream,
32
+ crlfDelay: Infinity,
33
+ });
34
+
35
+ /** @type {string[]} */
36
+ const lines = [];
37
+ for await (const line of rl) {
38
+ lines.push(line);
39
+ if (lines.length > n) {
40
+ lines.shift();
41
+ }
42
+ }
43
+ return lines;
44
+ };
45
+
46
+ // GET /api/status
47
+ handlerAdmin.get('/api/status', (_req, res) => {
48
+ const uptime = (new Date().getTime() - AdminContext.startTime.getTime()) / 1000;
49
+
50
+ const poolStats = AdminContext.pools.map((pool, index) => {
51
+ const name = AdminContext.caches[index]?.poolName ?? `pool-${index}`;
52
+ const p = /** @type {import('oracledb').Pool & {getStatistics?: () => Record<string, unknown>}} */ (pool);
53
+ return {
54
+ name,
55
+ stats: typeof p.getStatistics === 'function' ? p.getStatistics() : null,
56
+ connectionsOpen: pool.connectionsOpen,
57
+ connectionsInUse: pool.connectionsInUse,
58
+ };
59
+ });
60
+
61
+ const memUsage = process.memoryUsage();
62
+ const cpuUsage = process.cpuUsage();
63
+
64
+ res.json({
65
+ version,
66
+ status: AdminContext.paused ? 'paused' : 'running',
67
+ uptime,
68
+ startTime: AdminContext.startTime,
69
+ metrics: {
70
+ ...AdminContext.metrics,
71
+ avgResponseTime: AdminContext.metrics.requestCount > 0 ? AdminContext.metrics.totalDuration / AdminContext.metrics.requestCount : 0,
72
+ },
73
+ pools: poolStats,
74
+ system: {
75
+ nodeVersion: process.version,
76
+ platform: process.platform,
77
+ arch: process.arch,
78
+ memory: {
79
+ rss: memUsage.rss,
80
+ heapTotal: memUsage.heapTotal,
81
+ heapUsed: memUsage.heapUsed,
82
+ external: memUsage.external,
83
+ },
84
+ cpu: {
85
+ user: cpuUsage.user,
86
+ system: cpuUsage.system,
87
+ },
88
+ },
89
+ config: AdminContext.config
90
+ ? {
91
+ ...AdminContext.config,
92
+ adminPassword: AdminContext.config.adminPassword ? '********' : undefined,
93
+ routePlSql: AdminContext.config.routePlSql.map((p) => ({
94
+ ...p,
95
+ password: '********',
96
+ })),
97
+ }
98
+ : null,
99
+ });
100
+ });
101
+
102
+ // GET /api/logs/error
103
+ handlerAdmin.get('/api/logs/error', async (req, res) => {
104
+ try {
105
+ const limit = Number(req.query.limit) || 100;
106
+ const logFile = 'error.json.log';
107
+ const lines = await readLastLines(logFile, limit);
108
+
109
+ /** @type {Record<string, unknown>[]} */
110
+ const logs = [];
111
+ for (const line of lines) {
112
+ try {
113
+ const parsed = /** @type {unknown} */ (JSON.parse(line));
114
+ if (parsed && typeof parsed === 'object') {
115
+ logs.push(/** @type {Record<string, unknown>} */ (parsed));
116
+ }
117
+ } catch (e) {
118
+ // ignore
119
+ }
120
+ }
121
+
122
+ res.json(logs.reverse());
123
+ } catch (err) {
124
+ res.status(500).json({error: String(err)});
125
+ }
126
+ });
127
+
128
+ // GET /api/logs/access
129
+ handlerAdmin.get('/api/logs/access', async (req, res) => {
130
+ try {
131
+ const limit = Number(req.query.limit) || 100;
132
+ const logFile = AdminContext.config?.loggerFilename ?? 'access.log';
133
+
134
+ if (!AdminContext.config?.loggerFilename) {
135
+ res.json({message: 'Access logging not enabled'});
136
+ return;
137
+ }
138
+
139
+ const lines = await readLastLines(logFile, limit);
140
+ res.json(lines.reverse());
141
+ } catch (err) {
142
+ res.status(500).json({error: String(err)});
143
+ }
144
+ });
145
+
146
+ // GET /api/cache
147
+ handlerAdmin.get('/api/cache', (_req, res) => {
148
+ const caches = AdminContext.caches.map((c) => ({
149
+ poolName: c.poolName,
150
+ procedureNameCache: {
151
+ size: c.procedureNameCache.keys().length,
152
+ stats: c.procedureNameCache.getStats(),
153
+ },
154
+ argumentCache: {
155
+ size: c.argumentCache.keys().length,
156
+ stats: c.argumentCache.getStats(),
157
+ },
158
+ }));
159
+ res.json(caches);
160
+ });
161
+
162
+ // POST /api/cache/clear
163
+ handlerAdmin.post('/api/cache/clear', (req, res) => {
164
+ const body = /** @type {unknown} */ (req.body);
165
+ const poolName = body && typeof body === 'object' && 'poolName' in body && typeof body.poolName === 'string' ? body.poolName : undefined;
166
+
167
+ let cleared = 0;
168
+ AdminContext.caches.forEach((c) => {
169
+ if (!poolName || c.poolName === poolName) {
170
+ c.procedureNameCache.clear();
171
+ cleared++;
172
+ c.argumentCache.clear();
173
+ cleared++;
174
+ }
175
+ });
176
+
177
+ res.json({message: `Cleared ${cleared} caches`});
178
+ });
179
+
180
+ // POST /api/server/:action
181
+ handlerAdmin.post('/api/server/:action', (req, res) => {
182
+ const action = req.params.action;
183
+
184
+ if (action === 'stop') {
185
+ res.json({message: 'Server shutting down...'});
186
+ setTimeout(() => {
187
+ forceShutdown();
188
+ }, 100);
189
+ } else if (action === 'pause') {
190
+ AdminContext.paused = true;
191
+ res.json({message: 'Server paused', status: 'paused'});
192
+ } else if (action === 'resume') {
193
+ AdminContext.paused = false;
194
+ res.json({message: 'Server resumed', status: 'running'});
195
+ } else {
196
+ res.status(400).json({error: 'Invalid action'});
197
+ }
198
+ });
@@ -109,7 +109,7 @@ export const getCGI = (req, doctable, cgi) => {
109
109
  const CGI = {
110
110
  SERVER_PORT: typeof req.socket.localPort === 'number' ? req.socket.localPort.toString() : '',
111
111
  REQUEST_METHOD: req.method,
112
- PATH_INFO: Array.isArray(req.params.name) ? req.params.name[0] : req.params.name,
112
+ PATH_INFO: Array.isArray(req.params.name) ? (req.params.name[0] ?? '') : (req.params.name ?? ''),
113
113
  SCRIPT_NAME: PATH.script,
114
114
  REMOTE_ADDR: (req.ip ?? '').replace('::ffff:', ''),
115
115
  SERVER_PROTOCOL: `${PROTOCOL}/${req.httpVersion}`,
@@ -7,6 +7,8 @@ import {RequestError} from './requestError.js';
7
7
  import {getFormattedMessage, logToFile} from '../../util/trace.js';
8
8
  import {errorToString} from '../../util/errorToString.js';
9
9
  import {getHtmlPage} from '../../util/html.js';
10
+ import {jsonLogger} from '../../util/jsonLogger.js';
11
+ import {AdminContext} from '../../server/server.js';
10
12
 
11
13
  /**
12
14
  * @typedef {import('express').Request} Request
@@ -15,15 +17,16 @@ import {getHtmlPage} from '../../util/html.js';
15
17
  * @typedef {import('../../types.js').environmentType} environmentType
16
18
  * @typedef {import('../../types.js').configPlSqlHandlerType} configPlSqlHandlerType
17
19
  * @typedef {{html: string; text: string}} outputType
20
+ * @typedef {import('../../util/trace.js').messageType} messageType
18
21
  */
19
22
 
20
23
  /**
21
- * Show an error page
24
+ * Get error data
22
25
  * @param {Request} req - The req object represents the HTTP request.
23
26
  * @param {unknown} error - The error.
24
- * @returns {outputType} - The output.
27
+ * @returns {messageType} - The output.
25
28
  */
26
- const getError = (req, error) => {
29
+ const getErrorData = (req, error) => {
27
30
  let timestamp = new Date();
28
31
  let message = '';
29
32
  /** @type {environmentType | null} */
@@ -36,33 +39,29 @@ const getError = (req, error) => {
36
39
  // what type of Error did we receive
37
40
  if (error instanceof ProcedureError) {
38
41
  timestamp = error.timestamp;
39
- /* istanbul ignore next */
40
42
  message = error.stack ?? '';
41
43
  environment = error.environment;
42
44
  sql = error.sql;
43
45
  bind = error.bind;
44
46
  } else if (error instanceof RequestError) {
45
47
  timestamp = error.timestamp;
46
- /* istanbul ignore next */
47
48
  message = error.stack ?? '';
48
49
  } else if (error instanceof Error) {
49
- /* istanbul ignore next */
50
50
  message = errorToString(error);
51
51
  } else {
52
52
  if (typeof error === 'string') {
53
- /* istanbul ignore next */
54
53
  message = `${error}\n`;
55
54
  }
55
+ /* v8 ignore start - unreachable code: creating Error without throwing */
56
56
  try {
57
- /* istanbul ignore next */
58
57
  new Error();
59
58
  } catch (err) {
60
- /* istanbul ignore next */
61
59
  message += errorToString(err);
62
60
  }
61
+ /* v8 ignore stop */
63
62
  }
64
63
 
65
- return getFormattedMessage({type: 'error', timestamp, message, req, environment, sql, bind});
64
+ return {type: 'error', timestamp, message, req, environment, sql, bind};
66
65
  };
67
66
 
68
67
  /**
@@ -74,12 +73,38 @@ const getError = (req, error) => {
74
73
  * @param {unknown} error - The error.
75
74
  */
76
75
  export const errorPage = (req, res, options, error) => {
77
- // get error message
78
- const {html, text} = getError(req, error);
76
+ // get error data
77
+ const errorData = getErrorData(req, error);
78
+
79
+ // Update metrics
80
+ AdminContext.metrics.errorCount++;
81
+
82
+ // get formatted message
83
+ const {html, text} = getFormattedMessage(errorData);
79
84
 
80
85
  // trace to file
81
86
  logToFile(text);
82
87
 
88
+ // json log
89
+ const firstLine = errorData.message.split('\n')[0];
90
+ jsonLogger.log({
91
+ timestamp: errorData.timestamp?.toISOString() ?? new Date().toISOString(),
92
+ type: 'error',
93
+ message: firstLine ?? '',
94
+ req: {
95
+ method: req.method,
96
+ url: req.originalUrl,
97
+ ip: req.ip ?? '',
98
+ userAgent: req.get('user-agent') ?? '',
99
+ },
100
+ details: {
101
+ fullMessage: errorData.message,
102
+ sql: errorData.sql,
103
+ bind: errorData.bind,
104
+ environment: errorData.environment,
105
+ },
106
+ });
107
+
83
108
  // console
84
109
  console.error(text);
85
110
 
@@ -9,6 +9,7 @@ import url from 'node:url';
9
9
  import {processRequest} from './request.js';
10
10
  import {RequestError} from './requestError.js';
11
11
  import {errorPage} from './errorPage.js';
12
+ import {Cache} from '../../util/cache.js';
12
13
 
13
14
  /**
14
15
  * @typedef {import('express').RequestHandler} RequestHandler
@@ -24,11 +25,13 @@ import {errorPage} from './errorPage.js';
24
25
  * express.Request handler
25
26
  * @param {Request} req - The req object represents the HTTP request.
26
27
  * @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
27
- * @param {NextFunction} next - The next function.
28
+ * @param {NextFunction} _next - The next function.
28
29
  * @param {Pool} connectionPool - The connection pool.
29
30
  * @param {configPlSqlHandlerType} options - the options for the middleware.
31
+ * @param {Cache<string>} procedureNameCache - The procedure name cache.
32
+ * @param {Cache<import('./procedureNamed.js').argsType>} argumentCache - The argument cache.
30
33
  */
31
- const requestHandler = async (req, res, next, connectionPool, options) => {
34
+ const requestHandler = async (req, res, _next, connectionPool, options, procedureNameCache, argumentCache) => {
32
35
  try {
33
36
  // should we switch to the default page if there is one defined
34
37
  if (typeof req.params.name !== 'string' || req.params.name.length === 0) {
@@ -41,24 +44,47 @@ const requestHandler = async (req, res, next, connectionPool, options) => {
41
44
  }
42
45
  } else {
43
46
  // request handler
44
- await processRequest(req, res, options, connectionPool);
47
+ await processRequest(req, res, options, connectionPool, procedureNameCache, argumentCache);
45
48
  }
46
49
  } catch (err) {
47
50
  errorPage(req, res, options, err);
48
51
  }
49
52
  };
50
53
 
54
+ /**
55
+ * @typedef {import('express').RequestHandler & {
56
+ * procedureNameCache: Cache<string>;
57
+ * argumentCache: Cache<import('./procedureNamed.js').argsType>;
58
+ * }} WebPlSqlRequestHandler
59
+ */
60
+
51
61
  /**
52
62
  * Express middleware.
53
63
  *
54
64
  * @param {Pool} connectionPool - The connection pool.
55
65
  * @param {configPlSqlHandlerType} config - The configuration options.
56
- * @returns {RequestHandler} - The handler.
66
+ * @returns {WebPlSqlRequestHandler} - The handler.
57
67
  */
58
68
  export const handlerWebPlSql = (connectionPool, config) => {
59
69
  debug('options', config);
60
70
 
61
- return (req, res, next) => {
62
- void requestHandler(req, res, next, connectionPool, config);
71
+ /** @type {Cache<string>} */
72
+ const procedureNameCache = new Cache();
73
+ /** @type {Cache<import('./procedureNamed.js').argsType>} */
74
+ const argumentCache = new Cache();
75
+
76
+ /**
77
+ * @param {Request} req - The request.
78
+ * @param {Response} res - The response.
79
+ * @param {NextFunction} next - The next function.
80
+ */
81
+ const handler = (req, res, next) => {
82
+ void requestHandler(req, res, next, connectionPool, config, procedureNameCache, argumentCache);
63
83
  };
84
+
85
+ // Expose caches for Admin Console
86
+ handler.procedureNameCache = procedureNameCache;
87
+ handler.argumentCache = argumentCache;
88
+
89
+ return handler;
64
90
  };
@@ -55,7 +55,7 @@ export const parsePage = (text) => {
55
55
  {
56
56
  const cookie = parseCookie(header.value);
57
57
  debug(`oracle header "set-cookie" with value "${header.value}" was received has been parsed to ${JSON.stringify(cookie)}`);
58
- /* istanbul ignore else */
58
+ /* v8 ignore else - parseCookie validation ensures non-null */
59
59
  if (cookie !== null) {
60
60
  page.head.cookies.push(cookie);
61
61
  } else {
@@ -72,7 +72,7 @@ export const parsePage = (text) => {
72
72
  case 'x-db-content-length':
73
73
  {
74
74
  const contentLength = parseInt(header.value, 10);
75
- /* istanbul ignore else */
75
+ /* v8 ignore else - parseInt validation */
76
76
  if (!Number.isNaN(contentLength)) {
77
77
  page.head.contentLength = contentLength;
78
78
  debug(`oracle header "x-db-content-length" with value "${page.head.contentLength}" was parsed`);
@@ -85,12 +85,12 @@ export const parsePage = (text) => {
85
85
  case 'status':
86
86
  {
87
87
  const statusCode = parseInt(header.value, 10);
88
- /* istanbul ignore else */
88
+ /* v8 ignore else - parseInt validation */
89
89
  if (!Number.isNaN(statusCode)) {
90
90
  page.head.statusCode = statusCode;
91
91
  debug(`oracle header "status" with value "${page.head.statusCode}" was parsed`);
92
92
  const index = header.value.indexOf(' ');
93
- /* istanbul ignore else */
93
+ /* v8 ignore else - status code may not have description */
94
94
  if (index !== -1) {
95
95
  page.head.statusDescription = header.value.substring(index + 1);
96
96
  }
@@ -143,7 +143,7 @@ const getHeader = (line) => {
143
143
  */
144
144
  const parseCookie = (text) => {
145
145
  // validate
146
- /* istanbul ignore next */
146
+ /* v8 ignore next - input validation */
147
147
  if (typeof text !== 'string' || text.trim().length === 0) {
148
148
  return null;
149
149
  }
@@ -155,8 +155,12 @@ const parseCookie = (text) => {
155
155
  cookieElements = cookieElements.map((element) => element.trim());
156
156
 
157
157
  // get name and value
158
- const index = cookieElements[0].indexOf('=');
159
- /* istanbul ignore next */
158
+ const firstElement = cookieElements[0];
159
+ if (!firstElement) {
160
+ return null;
161
+ }
162
+ const index = firstElement.indexOf('=');
163
+ /* v8 ignore next - cookie format validation */
160
164
  if (index <= 0) {
161
165
  // if the index is -1, there is no equal sign and if it's 0 the name is empty
162
166
  return null;
@@ -164,8 +168,8 @@ const parseCookie = (text) => {
164
168
 
165
169
  /** @type {cookieType} */
166
170
  const cookie = {
167
- name: cookieElements[0].substring(0, index).trim(),
168
- value: cookieElements[0].substring(index + 1).trim(),
171
+ name: firstElement.substring(0, index).trim(),
172
+ value: firstElement.substring(index + 1).trim(),
169
173
  options: {},
170
174
  };
171
175
 
@@ -179,7 +183,7 @@ const parseCookie = (text) => {
179
183
  } else if (element.toLowerCase().startsWith('domain=')) {
180
184
  cookie.options.domain = element.substring(7);
181
185
  } else if (element.toLowerCase().startsWith('secure=')) {
182
- /* istanbul ignore next */
186
+ /* v8 ignore next - secure cookie attribute */
183
187
  cookie.options.secure = true;
184
188
  } else if (element.toLowerCase().startsWith('expires=')) {
185
189
  const date = tryDecodeDate(element.substring(8));
@@ -203,7 +207,7 @@ const tryDecodeDate = (value) => {
203
207
  try {
204
208
  return new Date(value);
205
209
  } catch (err) {
206
- /* istanbul ignore next */
210
+ /* v8 ignore next - invalid date format */
207
211
  return null;
208
212
  }
209
213
  };