web_plsql 0.18.0 → 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 (56) hide show
  1. package/README.md +24 -4
  2. package/package.json +15 -5
  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 +35 -6
  28. package/src/handler/plsql/handlerPlSql.js +32 -6
  29. package/src/handler/plsql/parsePage.js +7 -3
  30. package/src/handler/plsql/procedure.js +57 -11
  31. package/src/handler/plsql/procedureNamed.js +18 -47
  32. package/src/handler/plsql/procedureSanitize.js +96 -45
  33. package/src/handler/plsql/procedureVariable.js +2 -2
  34. package/src/handler/plsql/request.js +8 -3
  35. package/src/handler/plsql/sendResponse.js +2 -2
  36. package/src/server/config.js +1 -0
  37. package/src/server/server.js +108 -2
  38. package/src/types.js +6 -0
  39. package/src/util/cache.js +123 -0
  40. package/src/util/jsonLogger.js +47 -0
  41. package/src/util/shutdown.js +6 -2
  42. package/src/util/trace.js +5 -5
  43. package/src/version.js +1 -1
  44. package/types/handler/handlerAdmin.d.ts +9 -0
  45. package/types/handler/plsql/errorPage.d.ts +1 -0
  46. package/types/handler/plsql/handlerPlSql.d.ts +6 -1
  47. package/types/handler/plsql/procedure.d.ts +3 -1
  48. package/types/handler/plsql/procedureNamed.d.ts +2 -5
  49. package/types/handler/plsql/procedureSanitize.d.ts +2 -5
  50. package/types/handler/plsql/procedureVariable.d.ts +1 -1
  51. package/types/handler/plsql/request.d.ts +3 -1
  52. package/types/handler/plsql/sendResponse.d.ts +1 -1
  53. package/types/server/server.d.ts +22 -0
  54. package/types/types.d.ts +18 -0
  55. package/types/util/cache.d.ts +69 -0
  56. package/types/util/jsonLogger.d.ts +45 -0
@@ -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} */
@@ -58,7 +61,7 @@ const getError = (req, error) => {
58
61
  /* v8 ignore stop */
59
62
  }
60
63
 
61
- return getFormattedMessage({type: 'error', timestamp, message, req, environment, sql, bind});
64
+ return {type: 'error', timestamp, message, req, environment, sql, bind};
62
65
  };
63
66
 
64
67
  /**
@@ -70,12 +73,38 @@ const getError = (req, error) => {
70
73
  * @param {unknown} error - The error.
71
74
  */
72
75
  export const errorPage = (req, res, options, error) => {
73
- // get error message
74
- 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);
75
84
 
76
85
  // trace to file
77
86
  logToFile(text);
78
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
+
79
108
  // console
80
109
  console.error(text);
81
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
  };
@@ -155,7 +155,11 @@ 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('=');
158
+ const firstElement = cookieElements[0];
159
+ if (!firstElement) {
160
+ return null;
161
+ }
162
+ const index = firstElement.indexOf('=');
159
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
@@ -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