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
@@ -9,13 +9,13 @@ import oracledb from 'oracledb';
9
9
  import stream from 'node:stream';
10
10
  import z from 'zod';
11
11
 
12
- import {streamToBuffer} from './stream.js';
13
12
  import {uploadFile} from './upload.js';
14
13
  import {getProcedureVariable} from './procedureVariable.js';
15
14
  import {getProcedureNamed} from './procedureNamed.js';
16
15
  import {parsePage} from './parsePage.js';
17
16
  import {sendResponse} from './sendResponse.js';
18
17
  import {ProcedureError} from './procedureError.js';
18
+ import {RequestError} from './requestError.js';
19
19
  import {inspect, getBlock} from '../../util/trace.js';
20
20
  import {errorToString} from '../../util/errorToString.js';
21
21
  import {sanitizeProcName} from './procedureSanitize.js';
@@ -30,6 +30,8 @@ import {sanitizeProcName} from './procedureSanitize.js';
30
30
  * @typedef {import('../../types.js').environmentType} environmentType
31
31
  * @typedef {import('../../types.js').configPlSqlHandlerType} configPlSqlHandlerType
32
32
  * @typedef {import('../../types.js').BindParameterConfig} BindParameterConfig
33
+ * @typedef {import('../../util/cache.js').Cache<string>} ProcedureNameCache
34
+ * @typedef {import('../../util/cache.js').Cache<import('./procedureNamed.js').argsType>} ArgumentCache
33
35
  */
34
36
 
35
37
  /**
@@ -39,9 +41,11 @@ import {sanitizeProcName} from './procedureSanitize.js';
39
41
  * @param {argObjType} argObj - The arguments to pass to the procedure
40
42
  * @param {configPlSqlHandlerType} options - The options for the middleware
41
43
  * @param {Connection} databaseConnection - The database connection
42
- * @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
44
+ * @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
45
+ * @param {ArgumentCache} argumentCache - The argument cache.
46
+ * @returns {Promise<{sql: string; bind: BindParameterConfig; resolvedName?: string}>} - The SQL statement and bindings for the procedure to execute
43
47
  */
44
- const getProcedure = async (req, procName, argObj, options, databaseConnection) => {
48
+ const getProcedure = async (req, procName, argObj, options, databaseConnection, procedureNameCache, argumentCache) => {
45
49
  // path alias
46
50
  if (options.pathAlias?.toLowerCase() === procName.toLowerCase()) {
47
51
  debug(`getProcedure: path alias "${options.pathAlias}" redirects to "${options.pathAliasProcedure}"`);
@@ -57,13 +61,20 @@ const getProcedure = async (req, procName, argObj, options, databaseConnection)
57
61
  const useVariableArguments = procName.startsWith('!');
58
62
 
59
63
  // sanitize procedure name
60
- const sanitizedProcName = await sanitizeProcName(useVariableArguments ? procName.substring(1) : procName, databaseConnection, options);
64
+ const rawName = useVariableArguments ? procName.substring(1) : procName;
65
+ const sanitizedProcName = await sanitizeProcName(rawName, databaseConnection, options, procedureNameCache);
61
66
 
62
67
  // run procedure
63
68
  if (useVariableArguments) {
64
- return getProcedureVariable(req, sanitizedProcName, argObj);
69
+ return {
70
+ ...getProcedureVariable(req, sanitizedProcName, argObj),
71
+ resolvedName: sanitizedProcName,
72
+ };
65
73
  } else {
66
- return await getProcedureNamed(req, sanitizedProcName, argObj, databaseConnection);
74
+ return {
75
+ ...(await getProcedureNamed(req, sanitizedProcName, argObj, databaseConnection, argumentCache)),
76
+ resolvedName: sanitizedProcName,
77
+ };
67
78
  }
68
79
  };
69
80
 
@@ -123,11 +134,11 @@ const procedureExecute = async (para, databaseConnection) => {
123
134
  /**
124
135
  * Get page from procedure
125
136
  *
126
- * @param {boolean} test - Test.
137
+ * @param {boolean} _test - Test.
127
138
  * @param {Connection} databaseConnection - Database connection.
128
139
  * @returns {Promise<string>} Promise resolving to the returned page content.
129
140
  */
130
- const procedureGetPage = async (test, databaseConnection) => {
141
+ const procedureGetPage = async (_test, databaseConnection) => {
131
142
  const MAX_IROWS = 100000;
132
143
 
133
144
  /** @type {BindParameterConfig} */
@@ -154,7 +165,7 @@ const procedureGetPage = async (test, databaseConnection) => {
154
165
 
155
166
  // Make sure that we have retrieved all the rows
156
167
  if (irows > MAX_IROWS) {
157
- /* istanbul ignore next */
168
+ /* v8 ignore next - defensive check for row limit */
158
169
  throw new ProcedureError(`procedureGetPage: error when retrieving rows. irows="${irows}"`, {}, sqlStatement, bindParameter);
159
170
  }
160
171
 
@@ -230,9 +241,11 @@ END;
230
241
  * @param {fileUploadType[]} filesToUpload - Array of files to be uploaded
231
242
  * @param {configPlSqlHandlerType} options - the options for the middleware.
232
243
  * @param {Connection} databaseConnection - Database connection.
244
+ * @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
245
+ * @param {ArgumentCache} argumentCache - The argument cache.
233
246
  * @returns {Promise<void>} Promise resolving to the page content generated by the executed procedure
234
247
  */
235
- export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options, databaseConnection) => {
248
+ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options, databaseConnection, procedureNameCache, argumentCache) => {
236
249
  debug('invokeProcedure: begin');
237
250
 
238
251
  // 1) upload files
@@ -250,7 +263,12 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
250
263
  // 2) get procedure to execute and the arguments
251
264
 
252
265
  debug('invokeProcedure: get procedure to execute and the arguments');
253
- const para = await getProcedure(req, Array.isArray(req.params.name) ? req.params.name[0] : req.params.name, argObj, options, databaseConnection);
266
+ // Extract the raw procedure name from params
267
+ const rawProcName = Array.isArray(req.params.name) ? req.params.name[0] : req.params.name;
268
+ if (!rawProcName) {
269
+ throw new RequestError('No procedure name provided');
270
+ }
271
+ const para = await getProcedure(req, rawProcName, argObj, options, databaseConnection, procedureNameCache, argumentCache);
254
272
 
255
273
  // 3) prepare the session
256
274
 
@@ -260,7 +278,34 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
260
278
  // 4) execute the procedure
261
279
 
262
280
  debug('invokeProcedure: execute the session');
263
- await procedureExecute(para, databaseConnection);
281
+ try {
282
+ await procedureExecute(para, databaseConnection);
283
+ } catch (err) {
284
+ // Invalidation Logic
285
+ if (err instanceof ProcedureError) {
286
+ const errorString = err.toString();
287
+ // Check for ORA-04068, ORA-04061, ORA-04065, ORA-06550
288
+ if (
289
+ errorString.includes('ORA-04068') ||
290
+ errorString.includes('ORA-04061') ||
291
+ errorString.includes('ORA-04065') ||
292
+ errorString.includes('ORA-06550')
293
+ ) {
294
+ debug(`invokeProcedure: detected invalidation error (${errorString}). Clearing caches.`);
295
+
296
+ // Clear name resolution cache for the input name
297
+ if (rawProcName) {
298
+ procedureNameCache.delete(rawProcName);
299
+ }
300
+
301
+ // Clear argument cache for the resolved name
302
+ if (para.resolvedName) {
303
+ argumentCache.delete(para.resolvedName.toUpperCase());
304
+ }
305
+ }
306
+ }
307
+ throw err;
308
+ }
264
309
 
265
310
  // 5) get the page returned from the procedure
266
311
 
@@ -274,35 +319,38 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
274
319
 
275
320
  debug('invokeProcedure: download files');
276
321
  const fileBlob = await databaseConnection.createLob(oracledb.BLOB);
277
- const fileDownload = await procedureDownloadFiles(fileBlob, databaseConnection);
278
- if (debug.enabled) {
279
- debug(getBlock('fileDownload', inspect({fileType: fileDownload.fileType, fileSize: fileDownload.fileSize})));
280
- }
281
322
 
282
- // 7) parse the page
323
+ try {
324
+ const fileDownload = await procedureDownloadFiles(fileBlob, databaseConnection);
325
+ if (debug.enabled) {
326
+ debug(getBlock('fileDownload', inspect({fileType: fileDownload.fileType, fileSize: fileDownload.fileSize})));
327
+ }
283
328
 
284
- debug('invokeProcedure: parse the page');
285
- const pageComponents = parsePage(lines);
329
+ // 7) parse the page
286
330
 
287
- // add "Server" header
288
- pageComponents.head.server = cgiObj.SERVER_SOFTWARE;
331
+ debug('invokeProcedure: parse the page');
332
+ const pageComponents = parsePage(lines);
289
333
 
290
- // add file download information
291
- if (fileDownload.fileType !== '' && fileDownload.fileSize > 0 && fileDownload.fileBlob !== null) {
292
- pageComponents.file.fileType = fileDownload.fileType;
293
- pageComponents.file.fileSize = fileDownload.fileSize;
294
- pageComponents.file.fileBlob = await streamToBuffer(fileDownload.fileBlob);
295
- }
334
+ // add "Server" header
335
+ pageComponents.head.server = cgiObj.SERVER_SOFTWARE ?? '';
296
336
 
297
- // 8) send the page to browser
337
+ // add file download information
338
+ if (fileDownload.fileType !== '' && fileDownload.fileSize > 0 && fileDownload.fileBlob !== null) {
339
+ pageComponents.file.fileType = fileDownload.fileType;
340
+ pageComponents.file.fileSize = fileDownload.fileSize;
341
+ pageComponents.file.fileBlob = fileDownload.fileBlob;
342
+ }
298
343
 
299
- debug('invokeProcedure: send the page to browser');
300
- sendResponse(req, res, pageComponents);
344
+ // 8) send the page to browser
301
345
 
302
- // 9) cleanup
346
+ debug('invokeProcedure: send the page to browser');
347
+ await sendResponse(req, res, pageComponents);
348
+ } finally {
349
+ // 9) cleanup
303
350
 
304
- debug('invokeProcedure: cleanup');
305
- fileBlob.destroy();
351
+ debug('invokeProcedure: cleanup');
352
+ fileBlob.destroy();
353
+ }
306
354
 
307
355
  debug('invokeProcedure: end');
308
356
  };
@@ -21,7 +21,7 @@ import {toTable, warningMessage} from '../../util/trace.js';
21
21
  * @typedef {import('../../types.js').BindParameterConfig} BindParameterConfig
22
22
  * @typedef {import('../../types.js').BindParameter} BindParameter
23
23
  * @typedef {Record<string, string>} argsType
24
- * @typedef {{hitCount: number, args: argsType}} cacheEntryType
24
+ * @typedef {import('../../util/cache.js').Cache<argsType>} ArgumentCache
25
25
  */
26
26
 
27
27
  const SQL_GET_ARGUMENT = [
@@ -60,11 +60,6 @@ const DATA_TYPES = Object.freeze({
60
60
  // REF CURSOR
61
61
  });
62
62
 
63
- // NOTE: Consider using a separate cache for each database pool to avoid possible conflicts.
64
- /** @type {Map<string, cacheEntryType>} */
65
- const ARGS_CACHE = new Map();
66
- const ARGS_CACHE_MAX_COUNT = 10000;
67
-
68
63
  /**
69
64
  * Retrieve the argument types for a given procedure to be executed.
70
65
  * This is important because if the procedure is defined to take a PL/SQL indexed table,
@@ -89,9 +84,7 @@ const loadArguments = async (procedure, databaseConnection) => {
89
84
  result = await databaseConnection.execute(SQL_GET_ARGUMENT, bind);
90
85
  } catch (err) {
91
86
  debug('result', result);
92
- /* istanbul ignore next */
93
87
  const message = `Error when retrieving arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`;
94
- /* istanbul ignore next */
95
88
  throw new RequestError(message);
96
89
  }
97
90
 
@@ -106,75 +99,47 @@ const loadArguments = async (procedure, databaseConnection) => {
106
99
  .parse(result.outBinds);
107
100
  } catch (err) {
108
101
  debug('result.outBinds', result.outBinds);
109
- /* istanbul ignore next */
110
102
  const message = `Error when decoding arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`;
111
- /* istanbul ignore next */
112
103
  throw new RequestError(message);
113
104
  }
114
105
 
115
106
  if (data.names.length !== data.types.length) {
116
- /* istanbul ignore next */
117
107
  throw new RequestError('Error when decoding arguments. The number of names and types does not match');
118
108
  }
119
109
 
120
110
  /** @type {Record<string, string>} */
121
111
  const argTypes = {};
122
112
  for (let i = 0; i < data.names.length; i++) {
123
- argTypes[data.names[i].toLowerCase()] = data.types[i];
113
+ const name = data.names[i];
114
+ const type = data.types[i];
115
+ if (name && type) {
116
+ argTypes[name.toLowerCase()] = type;
117
+ }
124
118
  }
125
119
 
126
120
  return argTypes;
127
121
  };
128
122
 
129
- /**
130
- * Remove the cache entries with the lowest hitCount.
131
- * @param {number} count - Number of entries to remove
132
- * @returns {void}
133
- */
134
- const removeLowestHitCountEntries = (count) => {
135
- // Convert cache entries to an array
136
- const entries = Array.from(ARGS_CACHE.entries());
137
-
138
- // Sort entries by hitCount in ascending order
139
- entries.sort((a, b) => a[1].hitCount - b[1].hitCount);
140
-
141
- // Get the keys of the `count` entries with the lowest hitCount
142
- const keysToRemove = entries.slice(0, count).map(([key]) => key);
143
-
144
- // Remove these entries from the cache
145
- for (const key of keysToRemove) {
146
- ARGS_CACHE.delete(key);
147
- }
148
- };
149
-
150
123
  /**
151
124
  * Find the argument types for a given procedure to be executed.
152
125
  * As the arguments are cached, we first look up the cache and only if not yet available we load them.
153
126
  * @param {string} procedure - The procedure
154
127
  * @param {Connection} databaseConnection - The database connection
128
+ * @param {ArgumentCache} argumentCache - The argument cache.
155
129
  * @returns {Promise<argsType>} - The argument types
156
130
  */
157
- const findArguments = async (procedure, databaseConnection) => {
131
+ const findArguments = async (procedure, databaseConnection, argumentCache) => {
158
132
  const key = procedure.toUpperCase();
159
133
 
160
134
  // lookup in the cache
161
- const cacheEntry = ARGS_CACHE.get(key);
162
-
163
- // if we fount the procedure in the cache, we increase the hit cound and return
164
- if (cacheEntry) {
165
- cacheEntry.hitCount++;
166
- if (debug.enabled) {
167
- debug(`findArguments: procedure "${procedure}" found in cache with "${cacheEntry.hitCount}" hits`);
168
- }
169
- return cacheEntry.args;
170
- }
135
+ const cachedArgs = argumentCache.get(key);
171
136
 
172
- // if the cache is full, we remove the 1000 least used cache entries
173
- if (ARGS_CACHE.size > ARGS_CACHE_MAX_COUNT) {
137
+ // if we found the procedure in the cache, we return it
138
+ if (cachedArgs) {
174
139
  if (debug.enabled) {
175
- debug(`findArguments: cache is full. size=${ARGS_CACHE.size} max=${ARGS_CACHE_MAX_COUNT}`);
140
+ debug(`findArguments: procedure "${procedure}" found in cache`);
176
141
  }
177
- removeLowestHitCountEntries(1000);
142
+ return cachedArgs;
178
143
  }
179
144
 
180
145
  // load from database
@@ -184,7 +149,7 @@ const findArguments = async (procedure, databaseConnection) => {
184
149
  const args = await loadArguments(procedure, databaseConnection);
185
150
 
186
151
  // add to the cache
187
- ARGS_CACHE.set(key, {hitCount: 0, args});
152
+ argumentCache.set(key, args);
188
153
 
189
154
  return args;
190
155
  };
@@ -243,7 +208,7 @@ export const getBinding = (argName, argValue, argType) => {
243
208
  */
244
209
  const inspectBindings = (argObj, argTypes) => {
245
210
  const rows = Object.entries(argObj).map(([key, value]) => {
246
- return [key, value.toString(), typeof value, argTypes[key.toLowerCase()]];
211
+ return [key, value.toString(), typeof value, argTypes[key.toLowerCase()] ?? 'unknown'];
247
212
  });
248
213
 
249
214
  const {text} = toTable(['id', 'value', 'value type', 'argument type'], rows);
@@ -257,13 +222,14 @@ const inspectBindings = (argObj, argTypes) => {
257
222
  * @param {string} procName - The procedure to execute
258
223
  * @param {argObjType} argObj - The arguments to pass to the procedure
259
224
  * @param {Connection} databaseConnection - The database connection
225
+ * @param {ArgumentCache} argumentCache - The argument cache.
260
226
  * @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
261
227
  */
262
- export const getProcedureNamed = async (req, procName, argObj, databaseConnection) => {
228
+ export const getProcedureNamed = async (req, procName, argObj, databaseConnection, argumentCache) => {
263
229
  debug(`getProcedureNamed: ${procName} arguments=`, argObj);
264
230
 
265
231
  // get the types of the arguments
266
- const argTypes = await findArguments(procName, databaseConnection);
232
+ const argTypes = await findArguments(procName, databaseConnection, argumentCache);
267
233
 
268
234
  /** @type {string[]} */
269
235
  const sqlParameter = [];
@@ -16,17 +16,90 @@ import {errorToString} from '../../util/errorToString.js';
16
16
  * @typedef {import('../../types.js').environmentType} environmentType
17
17
  * @typedef {import('../../types.js').configPlSqlHandlerType} configPlSqlHandlerType
18
18
  * @typedef {import('../../types.js').BindParameterConfig} BindParameterConfig
19
+ * @typedef {import('../../util/cache.js').Cache<string>} ProcedureNameCache
19
20
  */
20
21
 
21
22
  const DEFAULT_EXCLUSION_LIST = ['sys.', 'dbms_', 'utl_', 'owa_', 'htp.', 'htf.', 'wpg_docload.', 'ctxsys.', 'mdsys.'];
22
23
 
23
- // NOTE: Consider using a separate cache for each database pool to avoid possible conflicts.
24
+ import {Cache} from '../../util/cache.js';
25
+ /** @type {Cache<{valid: boolean}>} */
26
+ const validationFunctionCache = new Cache();
27
+
24
28
  /**
25
- * @typedef {{hitCount: number, valid: boolean}} cacheEntryType
29
+ * Resolve the procedure name using dbms_utility.name_resolve.
30
+ *
31
+ * @param {string} procName - The procedure name to resolve.
32
+ * @param {Connection} databaseConnection - The database connection.
33
+ * @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
34
+ * @returns {Promise<string>} - The resolved canonical procedure name (SCHEMA.NAME).
26
35
  */
27
- /** @type {Map<string, cacheEntryType>} */
28
- const REQUEST_VALIDATION_FUNCTION_CACHE = new Map();
29
- const REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT = 10000;
36
+ const resolveProcedureName = async (procName, databaseConnection, procedureNameCache) => {
37
+ // Check cache
38
+ const cachedName = procedureNameCache.get(procName);
39
+ if (cachedName) {
40
+ debug(`resolveProcedureName: Cache hit for "${procName}" -> "${cachedName}"`);
41
+ return cachedName;
42
+ }
43
+
44
+ debug(`resolveProcedureName: Cache miss for "${procName}". Resolving in DB...`);
45
+
46
+ const sql = `
47
+ DECLARE
48
+ l_schema VARCHAR2(128);
49
+ l_part1 VARCHAR2(128);
50
+ l_part2 VARCHAR2(128);
51
+ l_dblink VARCHAR2(128);
52
+ l_part1_type NUMBER;
53
+ l_object_number NUMBER;
54
+ BEGIN
55
+ dbms_utility.name_resolve(
56
+ name => :name,
57
+ context => 1,
58
+ schema => l_schema,
59
+ part1 => l_part1,
60
+ part2 => l_part2,
61
+ dblink => l_dblink,
62
+ part1_type => l_part1_type,
63
+ object_number => l_object_number
64
+ );
65
+
66
+ -- Reconstruct the canonical name
67
+ -- If it's a package procedure: schema.package.procedure
68
+ -- If it's a standalone procedure: schema.procedure
69
+
70
+ IF l_part1 IS NOT NULL THEN
71
+ :resolved := l_schema || '.' || l_part1 || '.' || l_part2;
72
+ ELSE
73
+ :resolved := l_schema || '.' || l_part2;
74
+ END IF;
75
+ END;
76
+ `;
77
+
78
+ const bind = {
79
+ name: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: procName},
80
+ resolved: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxSize: 400},
81
+ };
82
+
83
+ try {
84
+ const result = await databaseConnection.execute(sql, bind);
85
+ const {resolved} = z.object({resolved: z.string()}).parse(result.outBinds);
86
+
87
+ if (!resolved) {
88
+ throw new RequestError(`Could not resolve procedure name "${procName}"`);
89
+ }
90
+
91
+ debug(`resolveProcedureName: Resolved "${procName}" -> "${resolved}"`);
92
+
93
+ // Update cache
94
+ procedureNameCache.set(procName, resolved);
95
+
96
+ return resolved;
97
+ } catch (err) {
98
+ debug(`resolveProcedureName: Error resolving "${procName}"`, err);
99
+ // Rethrow as RequestError to indicate 404/403
100
+ throw new RequestError(`Procedure "${procName}" not found or not accessible.\n${errorToString(err)}`);
101
+ }
102
+ };
30
103
 
31
104
  /**
32
105
  * Sanitize the procedure name.
@@ -34,15 +107,16 @@ const REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT = 10000;
34
107
  * @param {string} procName - The procedure name.
35
108
  * @param {Connection} databaseConnection - The database connection
36
109
  * @param {configPlSqlHandlerType} options - the options for the middleware.
110
+ * @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
37
111
  * @returns {Promise<string>} Promise resolving to final procedure name.
38
112
  */
39
- export const sanitizeProcName = async (procName, databaseConnection, options) => {
113
+ export const sanitizeProcName = async (procName, databaseConnection, options, procedureNameCache) => {
40
114
  debug('sanitizeProcName', procName);
41
115
 
42
116
  // make lowercase and trim
43
117
  let finalProcName = procName.toLowerCase().trim();
44
118
 
45
- // remove special characters
119
+ // remove special characters (basic sanity check before DB call)
46
120
  finalProcName = removeSpecialCharacters(finalProcName);
47
121
 
48
122
  // check for default exclusions
@@ -67,6 +141,10 @@ export const sanitizeProcName = async (procName, databaseConnection, options) =>
67
141
 
68
142
  // Check request validation function
69
143
  if (options.requestValidationFunction && options.requestValidationFunction.length > 0) {
144
+ // Note: We might want to scope this cache too, but for now let's focus on the procedure name cache.
145
+ // The original code used a global cache for this.
146
+ // For strict correctness, we should probably verify the *resolved* name,
147
+ // but legacy behavior checks the input name.
70
148
  const valid = await requestValidationFunction(finalProcName, options.requestValidationFunction, databaseConnection);
71
149
  if (!valid) {
72
150
  const error = `Procedure name "${procName}" is not valid according to the request validation function "${options.requestValidationFunction}"`;
@@ -75,7 +153,11 @@ export const sanitizeProcName = async (procName, databaseConnection, options) =>
75
153
  }
76
154
  }
77
155
 
78
- return finalProcName;
156
+ // NEW: Resolve the procedure name against the database
157
+ // This prevents SQL injection and ensures the procedure exists
158
+ const resolvedName = await resolveProcedureName(finalProcName, databaseConnection, procedureNameCache);
159
+
160
+ return resolvedName;
79
161
  };
80
162
 
81
163
  /**
@@ -128,9 +210,7 @@ const loadRequestValid = async (procName, requestValidationFunction, databaseCon
128
210
  result = await databaseConnection.execute(SQL, bind);
129
211
  } catch (err) {
130
212
  debug('result', result);
131
- /* istanbul ignore next */
132
213
  const message = `Error when validating procedure name "${procName}"\n${SQL}\n${errorToString(err)}`;
133
- /* istanbul ignore next */
134
214
  throw new RequestError(message);
135
215
  }
136
216
 
@@ -139,34 +219,11 @@ const loadRequestValid = async (procName, requestValidationFunction, databaseCon
139
219
  return data.valid === 1;
140
220
  } catch (err) {
141
221
  debug('result', result.outBinds);
142
- /* istanbul ignore next */
143
222
  const message = `Internal error when parsing ${result.outBinds}\n${errorToString(err)}`;
144
- /* istanbul ignore next */
145
223
  throw new Error(message);
146
224
  }
147
225
  };
148
226
 
149
- /**
150
- * Remove the cache entries with the lowest hitCount.
151
- * @param {number} count - Number of entries to remove
152
- * @returns {void}
153
- */
154
- const removeLowestHitCountEntries = (count) => {
155
- // Convert cache entries to an array
156
- const entries = Array.from(REQUEST_VALIDATION_FUNCTION_CACHE.entries());
157
-
158
- // Sort entries by hitCount in ascending order
159
- entries.sort((a, b) => a[1].hitCount - b[1].hitCount);
160
-
161
- // Get the keys of the `count` entries with the lowest hitCount
162
- const keysToRemove = entries.slice(0, count).map(([key]) => key);
163
-
164
- // Remove these entries from the cache
165
- for (const key of keysToRemove) {
166
- REQUEST_VALIDATION_FUNCTION_CACHE.delete(key);
167
- }
168
- };
169
-
170
227
  /**
171
228
  * Request validation function.
172
229
  *
@@ -179,37 +236,27 @@ const requestValidationFunction = async (procName, requestValidationFunction, da
179
236
  debug('requestValidationFunction', procName, requestValidationFunction);
180
237
 
181
238
  // calculate the key
182
- //const key = `${databaseConnection.connectString}_${databaseConnection.user}_${procName.toLowerCase()}`;
183
239
  const key = procName.toLowerCase();
184
240
 
185
241
  // lookup in the cache
186
- const cacheEntry = REQUEST_VALIDATION_FUNCTION_CACHE.get(key);
242
+ const cacheEntry = validationFunctionCache.get(key);
187
243
 
188
- // if we fount the procedure in the cache, we increase the hit cound and return
189
- if (cacheEntry) {
190
- cacheEntry.hitCount++;
244
+ // if we found the procedure in the cache, we return it (hitCount already incremented by get)
245
+ if (cacheEntry !== undefined) {
191
246
  if (debug.enabled) {
192
- debug(`findArguments: procedure "${procName}" found in cache with "${cacheEntry.hitCount}" hits`);
247
+ debug(`requestValidationFunction: procedure "${procName}" found in cache`);
193
248
  }
194
249
  return cacheEntry.valid;
195
250
  }
196
251
 
197
- // if the cache is full, we remove the 1000 least used cache entries
198
- if (REQUEST_VALIDATION_FUNCTION_CACHE.size > REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT) {
199
- if (debug.enabled) {
200
- debug(`findArguments: cache is full. size=${REQUEST_VALIDATION_FUNCTION_CACHE.size} max=${REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT}`);
201
- }
202
- removeLowestHitCountEntries(1000);
203
- }
204
-
205
252
  // load from database
206
253
  if (debug.enabled) {
207
- debug(`findArguments: procedure "${procName}" not found in cache and must be loaded`);
254
+ debug(`requestValidationFunction: procedure "${procName}" not found in cache and must be loaded`);
208
255
  }
209
256
  const valid = await loadRequestValid(procName, requestValidationFunction, databaseConnection);
210
257
 
211
258
  // add to the cache
212
- REQUEST_VALIDATION_FUNCTION_CACHE.set(key, {hitCount: 0, valid});
259
+ validationFunctionCache.set(key, {valid});
213
260
 
214
261
  return valid;
215
262
  };
@@ -18,12 +18,12 @@ import oracledb from 'oracledb';
18
18
 
19
19
  /**
20
20
  * Get the sql statement and bindings for the procedure to execute for a variable number of arguments
21
- * @param {Request} req - The req object represents the HTTP request. (only used for debugging)
21
+ * @param {Request} _req - The req object represents the HTTP request. (only used for debugging)
22
22
  * @param {string} procName - The procedure to execute
23
23
  * @param {argObjType} argObj - The arguments to pass to the procedure
24
24
  * @returns {{sql: string; bind: BindParameterConfig}} - The SQL statement and bindings for the procedure to execute
25
25
  */
26
- export const getProcedureVariable = (req, procName, argObj) => {
26
+ export const getProcedureVariable = (_req, procName, argObj) => {
27
27
  if (debug.enabled) {
28
28
  debug(`getProcedureVariable: ${procName} arguments=`, argObj);
29
29
  }
@@ -19,6 +19,8 @@ import {isStringOrArrayOfString} from '../../util/type.js';
19
19
  * @typedef {import('oracledb').Connection} Connection
20
20
  * @typedef {import('../../types.js').argObjType} argObjType
21
21
  * @typedef {import('../../types.js').configPlSqlHandlerType} configPlSqlHandlerType
22
+ * @typedef {import('../../util/cache.js').Cache<string>} ProcedureNameCache
23
+ * @typedef {import('../../util/cache.js').Cache<import('./procedureNamed.js').argsType>} ArgumentCache
22
24
  */
23
25
 
24
26
  /**
@@ -28,9 +30,11 @@ import {isStringOrArrayOfString} from '../../util/type.js';
28
30
  * @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
29
31
  * @param {configPlSqlHandlerType} options - the options for the middleware.
30
32
  * @param {Pool} connectionPool - The connection pool.
33
+ * @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
34
+ * @param {ArgumentCache} argumentCache - The argument cache.
31
35
  * @returns {Promise<void>} - Promise resolving to th page
32
36
  */
33
- export const processRequest = async (req, res, options, connectionPool) => {
37
+ export const processRequest = async (req, res, options, connectionPool, procedureNameCache, argumentCache) => {
34
38
  debug('processRequest: ENTER');
35
39
 
36
40
  //
@@ -65,7 +69,7 @@ export const processRequest = async (req, res, options, connectionPool) => {
65
69
  debug('processRequest: argObj=', argObj);
66
70
 
67
71
  // invoke the Oracle procedure and get the page contenst
68
- await invokeProcedure(req, res, argObj, cgiObj, filesToUpload, options, connection);
72
+ await invokeProcedure(req, res, argObj, cgiObj, filesToUpload, options, connection, procedureNameCache, argumentCache);
69
73
 
70
74
  // transaction mode
71
75
  if (options.transactionMode === 'rollback') {
@@ -73,7 +77,8 @@ export const processRequest = async (req, res, options, connectionPool) => {
73
77
  await connection.rollback();
74
78
  } else if (typeof options.transactionMode === 'function') {
75
79
  debug('transactionMode: callback');
76
- const result = options.transactionMode(connection, Array.isArray(req.params.name) ? req.params.name[0] : req.params.name);
80
+ const procName = Array.isArray(req.params.name) ? req.params.name[0] : req.params.name;
81
+ const result = options.transactionMode(connection, procName ?? '');
77
82
  debug('transactionMode: callback restult', result);
78
83
  if (result && typeof result.then === 'function') {
79
84
  await result;
@@ -98,16 +103,16 @@ const normalizeBody = (req) => {
98
103
  /** @type {Record<string, string | string[]>} */
99
104
  const args = {};
100
105
 
101
- /* istanbul ignore else */
106
+ /* v8 ignore else - body validation */
102
107
  if (typeof req.body === 'object' && req.body !== null) {
103
108
  for (const key in req.body) {
104
109
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
105
110
  const value = req.body[key];
106
- /* istanbul ignore else */
111
+ /* v8 ignore else - type validation */
107
112
  if (isStringOrArrayOfString(value)) {
108
113
  args[key] = value;
109
114
  } else {
110
- /* istanbul ignore next */
115
+ /* v8 ignore next - invalid body type */
111
116
  throw new RequestError(
112
117
  `The element "${key}" in the body is not a string or an array of strings!\n${util.inspect(req.body, {showHidden: false, depth: null, colors: false})}`,
113
118
  );