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
@@ -15,6 +15,7 @@ import {getProcedureNamed} from './procedureNamed.js';
15
15
  import {parsePage} from './parsePage.js';
16
16
  import {sendResponse} from './sendResponse.js';
17
17
  import {ProcedureError} from './procedureError.js';
18
+ import {RequestError} from './requestError.js';
18
19
  import {inspect, getBlock} from '../../util/trace.js';
19
20
  import {errorToString} from '../../util/errorToString.js';
20
21
  import {sanitizeProcName} from './procedureSanitize.js';
@@ -29,6 +30,8 @@ import {sanitizeProcName} from './procedureSanitize.js';
29
30
  * @typedef {import('../../types.js').environmentType} environmentType
30
31
  * @typedef {import('../../types.js').configPlSqlHandlerType} configPlSqlHandlerType
31
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
32
35
  */
33
36
 
34
37
  /**
@@ -38,9 +41,11 @@ import {sanitizeProcName} from './procedureSanitize.js';
38
41
  * @param {argObjType} argObj - The arguments to pass to the procedure
39
42
  * @param {configPlSqlHandlerType} options - The options for the middleware
40
43
  * @param {Connection} databaseConnection - The database connection
41
- * @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
42
47
  */
43
- const getProcedure = async (req, procName, argObj, options, databaseConnection) => {
48
+ const getProcedure = async (req, procName, argObj, options, databaseConnection, procedureNameCache, argumentCache) => {
44
49
  // path alias
45
50
  if (options.pathAlias?.toLowerCase() === procName.toLowerCase()) {
46
51
  debug(`getProcedure: path alias "${options.pathAlias}" redirects to "${options.pathAliasProcedure}"`);
@@ -56,13 +61,20 @@ const getProcedure = async (req, procName, argObj, options, databaseConnection)
56
61
  const useVariableArguments = procName.startsWith('!');
57
62
 
58
63
  // sanitize procedure name
59
- 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);
60
66
 
61
67
  // run procedure
62
68
  if (useVariableArguments) {
63
- return getProcedureVariable(req, sanitizedProcName, argObj);
69
+ return {
70
+ ...getProcedureVariable(req, sanitizedProcName, argObj),
71
+ resolvedName: sanitizedProcName,
72
+ };
64
73
  } else {
65
- return await getProcedureNamed(req, sanitizedProcName, argObj, databaseConnection);
74
+ return {
75
+ ...(await getProcedureNamed(req, sanitizedProcName, argObj, databaseConnection, argumentCache)),
76
+ resolvedName: sanitizedProcName,
77
+ };
66
78
  }
67
79
  };
68
80
 
@@ -122,11 +134,11 @@ const procedureExecute = async (para, databaseConnection) => {
122
134
  /**
123
135
  * Get page from procedure
124
136
  *
125
- * @param {boolean} test - Test.
137
+ * @param {boolean} _test - Test.
126
138
  * @param {Connection} databaseConnection - Database connection.
127
139
  * @returns {Promise<string>} Promise resolving to the returned page content.
128
140
  */
129
- const procedureGetPage = async (test, databaseConnection) => {
141
+ const procedureGetPage = async (_test, databaseConnection) => {
130
142
  const MAX_IROWS = 100000;
131
143
 
132
144
  /** @type {BindParameterConfig} */
@@ -229,9 +241,11 @@ END;
229
241
  * @param {fileUploadType[]} filesToUpload - Array of files to be uploaded
230
242
  * @param {configPlSqlHandlerType} options - the options for the middleware.
231
243
  * @param {Connection} databaseConnection - Database connection.
244
+ * @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
245
+ * @param {ArgumentCache} argumentCache - The argument cache.
232
246
  * @returns {Promise<void>} Promise resolving to the page content generated by the executed procedure
233
247
  */
234
- 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) => {
235
249
  debug('invokeProcedure: begin');
236
250
 
237
251
  // 1) upload files
@@ -249,7 +263,12 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
249
263
  // 2) get procedure to execute and the arguments
250
264
 
251
265
  debug('invokeProcedure: get procedure to execute and the arguments');
252
- 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);
253
272
 
254
273
  // 3) prepare the session
255
274
 
@@ -259,7 +278,34 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
259
278
  // 4) execute the procedure
260
279
 
261
280
  debug('invokeProcedure: execute the session');
262
- 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
+ }
263
309
 
264
310
  // 5) get the page returned from the procedure
265
311
 
@@ -286,7 +332,7 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
286
332
  const pageComponents = parsePage(lines);
287
333
 
288
334
  // add "Server" header
289
- pageComponents.head.server = cgiObj.SERVER_SOFTWARE;
335
+ pageComponents.head.server = cgiObj.SERVER_SOFTWARE ?? '';
290
336
 
291
337
  // add file download information
292
338
  if (fileDownload.fileType !== '' && fileDownload.fileSize > 0 && fileDownload.fileBlob !== null) {
@@ -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,
@@ -115,61 +110,36 @@ const loadArguments = async (procedure, databaseConnection) => {
115
110
  /** @type {Record<string, string>} */
116
111
  const argTypes = {};
117
112
  for (let i = 0; i < data.names.length; i++) {
118
- 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
+ }
119
118
  }
120
119
 
121
120
  return argTypes;
122
121
  };
123
122
 
124
- /**
125
- * Remove the cache entries with the lowest hitCount.
126
- * @param {number} count - Number of entries to remove
127
- * @returns {void}
128
- */
129
- const removeLowestHitCountEntries = (count) => {
130
- // Convert cache entries to an array
131
- const entries = Array.from(ARGS_CACHE.entries());
132
-
133
- // Sort entries by hitCount in ascending order
134
- entries.sort((a, b) => a[1].hitCount - b[1].hitCount);
135
-
136
- // Get the keys of the `count` entries with the lowest hitCount
137
- const keysToRemove = entries.slice(0, count).map(([key]) => key);
138
-
139
- // Remove these entries from the cache
140
- for (const key of keysToRemove) {
141
- ARGS_CACHE.delete(key);
142
- }
143
- };
144
-
145
123
  /**
146
124
  * Find the argument types for a given procedure to be executed.
147
125
  * As the arguments are cached, we first look up the cache and only if not yet available we load them.
148
126
  * @param {string} procedure - The procedure
149
127
  * @param {Connection} databaseConnection - The database connection
128
+ * @param {ArgumentCache} argumentCache - The argument cache.
150
129
  * @returns {Promise<argsType>} - The argument types
151
130
  */
152
- const findArguments = async (procedure, databaseConnection) => {
131
+ const findArguments = async (procedure, databaseConnection, argumentCache) => {
153
132
  const key = procedure.toUpperCase();
154
133
 
155
134
  // lookup in the cache
156
- const cacheEntry = ARGS_CACHE.get(key);
157
-
158
- // if we fount the procedure in the cache, we increase the hit cound and return
159
- if (cacheEntry) {
160
- cacheEntry.hitCount++;
161
- if (debug.enabled) {
162
- debug(`findArguments: procedure "${procedure}" found in cache with "${cacheEntry.hitCount}" hits`);
163
- }
164
- return cacheEntry.args;
165
- }
135
+ const cachedArgs = argumentCache.get(key);
166
136
 
167
- // if the cache is full, we remove the 1000 least used cache entries
168
- if (ARGS_CACHE.size > ARGS_CACHE_MAX_COUNT) {
137
+ // if we found the procedure in the cache, we return it
138
+ if (cachedArgs) {
169
139
  if (debug.enabled) {
170
- debug(`findArguments: cache is full. size=${ARGS_CACHE.size} max=${ARGS_CACHE_MAX_COUNT}`);
140
+ debug(`findArguments: procedure "${procedure}" found in cache`);
171
141
  }
172
- removeLowestHitCountEntries(1000);
142
+ return cachedArgs;
173
143
  }
174
144
 
175
145
  // load from database
@@ -179,7 +149,7 @@ const findArguments = async (procedure, databaseConnection) => {
179
149
  const args = await loadArguments(procedure, databaseConnection);
180
150
 
181
151
  // add to the cache
182
- ARGS_CACHE.set(key, {hitCount: 0, args});
152
+ argumentCache.set(key, args);
183
153
 
184
154
  return args;
185
155
  };
@@ -238,7 +208,7 @@ export const getBinding = (argName, argValue, argType) => {
238
208
  */
239
209
  const inspectBindings = (argObj, argTypes) => {
240
210
  const rows = Object.entries(argObj).map(([key, value]) => {
241
- return [key, value.toString(), typeof value, argTypes[key.toLowerCase()]];
211
+ return [key, value.toString(), typeof value, argTypes[key.toLowerCase()] ?? 'unknown'];
242
212
  });
243
213
 
244
214
  const {text} = toTable(['id', 'value', 'value type', 'argument type'], rows);
@@ -252,13 +222,14 @@ const inspectBindings = (argObj, argTypes) => {
252
222
  * @param {string} procName - The procedure to execute
253
223
  * @param {argObjType} argObj - The arguments to pass to the procedure
254
224
  * @param {Connection} databaseConnection - The database connection
225
+ * @param {ArgumentCache} argumentCache - The argument cache.
255
226
  * @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
256
227
  */
257
- export const getProcedureNamed = async (req, procName, argObj, databaseConnection) => {
228
+ export const getProcedureNamed = async (req, procName, argObj, databaseConnection, argumentCache) => {
258
229
  debug(`getProcedureNamed: ${procName} arguments=`, argObj);
259
230
 
260
231
  // get the types of the arguments
261
- const argTypes = await findArguments(procName, databaseConnection);
232
+ const argTypes = await findArguments(procName, databaseConnection, argumentCache);
262
233
 
263
234
  /** @type {string[]} */
264
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
  /**
@@ -142,27 +224,6 @@ const loadRequestValid = async (procName, requestValidationFunction, databaseCon
142
224
  }
143
225
  };
144
226
 
145
- /**
146
- * Remove the cache entries with the lowest hitCount.
147
- * @param {number} count - Number of entries to remove
148
- * @returns {void}
149
- */
150
- const removeLowestHitCountEntries = (count) => {
151
- // Convert cache entries to an array
152
- const entries = Array.from(REQUEST_VALIDATION_FUNCTION_CACHE.entries());
153
-
154
- // Sort entries by hitCount in ascending order
155
- entries.sort((a, b) => a[1].hitCount - b[1].hitCount);
156
-
157
- // Get the keys of the `count` entries with the lowest hitCount
158
- const keysToRemove = entries.slice(0, count).map(([key]) => key);
159
-
160
- // Remove these entries from the cache
161
- for (const key of keysToRemove) {
162
- REQUEST_VALIDATION_FUNCTION_CACHE.delete(key);
163
- }
164
- };
165
-
166
227
  /**
167
228
  * Request validation function.
168
229
  *
@@ -175,37 +236,27 @@ const requestValidationFunction = async (procName, requestValidationFunction, da
175
236
  debug('requestValidationFunction', procName, requestValidationFunction);
176
237
 
177
238
  // calculate the key
178
- //const key = `${databaseConnection.connectString}_${databaseConnection.user}_${procName.toLowerCase()}`;
179
239
  const key = procName.toLowerCase();
180
240
 
181
241
  // lookup in the cache
182
- const cacheEntry = REQUEST_VALIDATION_FUNCTION_CACHE.get(key);
242
+ const cacheEntry = validationFunctionCache.get(key);
183
243
 
184
- // if we fount the procedure in the cache, we increase the hit cound and return
185
- if (cacheEntry) {
186
- cacheEntry.hitCount++;
244
+ // if we found the procedure in the cache, we return it (hitCount already incremented by get)
245
+ if (cacheEntry !== undefined) {
187
246
  if (debug.enabled) {
188
- debug(`findArguments: procedure "${procName}" found in cache with "${cacheEntry.hitCount}" hits`);
247
+ debug(`requestValidationFunction: procedure "${procName}" found in cache`);
189
248
  }
190
249
  return cacheEntry.valid;
191
250
  }
192
251
 
193
- // if the cache is full, we remove the 1000 least used cache entries
194
- if (REQUEST_VALIDATION_FUNCTION_CACHE.size > REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT) {
195
- if (debug.enabled) {
196
- debug(`findArguments: cache is full. size=${REQUEST_VALIDATION_FUNCTION_CACHE.size} max=${REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT}`);
197
- }
198
- removeLowestHitCountEntries(1000);
199
- }
200
-
201
252
  // load from database
202
253
  if (debug.enabled) {
203
- 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`);
204
255
  }
205
256
  const valid = await loadRequestValid(procName, requestValidationFunction, databaseConnection);
206
257
 
207
258
  // add to the cache
208
- REQUEST_VALIDATION_FUNCTION_CACHE.set(key, {hitCount: 0, valid});
259
+ validationFunctionCache.set(key, {valid});
209
260
 
210
261
  return valid;
211
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;
@@ -18,12 +18,12 @@ import {getBlock} from '../../util/trace.js';
18
18
 
19
19
  /**
20
20
  * Send "default" response to the browser
21
- * @param {Request} req - The req object represents the HTTP request.
21
+ * @param {Request} _req - The req object represents the HTTP request.
22
22
  * @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
23
23
  * @param {pageType} page - The page to send.
24
24
  * @returns {Promise<void>}
25
25
  */
26
- export const sendResponse = async (req, res, page) => {
26
+ export const sendResponse = async (_req, res, page) => {
27
27
  /** @type {string[]} */
28
28
  const debugText = [];
29
29
 
@@ -17,6 +17,7 @@ export const showConfig = (config) => {
17
17
  console.log(LINE);
18
18
 
19
19
  console.log(`Server port: ${config.port}`);
20
+ console.log(`Admin route: ${config.adminRoute ?? '/admin'}${config.adminUser ? ' (authenticated)' : ''}`);
20
21
  console.log(`Access log: ${config.loggerFilename.length > 0 ? config.loggerFilename : ''}`);
21
22
  console.log(`Upload file size limit: ${typeof config.uploadFileSizeLimit === 'number' ? `${config.uploadFileSizeLimit} bytes` : 'any'}`);
22
23