web_plsql 0.18.0 → 1.2.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 (74) hide show
  1. package/README.md +29 -9
  2. package/package.json +17 -7
  3. package/src/admin/client/charts.ts +598 -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 +529 -0
  8. package/src/admin/js/api.ts +170 -0
  9. package/src/admin/js/app.ts +614 -0
  10. package/src/admin/js/eslint.config.js +74 -0
  11. package/src/admin/js/schemas.ts +215 -0
  12. package/src/admin/js/templates/config.ts +147 -0
  13. package/src/admin/js/templates/errorRow.ts +21 -0
  14. package/src/admin/js/templates/index.ts +4 -0
  15. package/src/admin/js/templates/poolCard.ts +61 -0
  16. package/src/admin/js/templates/traceRow.ts +24 -0
  17. package/src/admin/js/tsconfig.json +24 -0
  18. package/src/admin/js/types.ts +336 -0
  19. package/src/admin/js/ui/components.ts +44 -0
  20. package/src/admin/js/ui/table.ts +173 -0
  21. package/src/admin/js/ui/theme.ts +93 -0
  22. package/src/admin/js/ui/views.ts +427 -0
  23. package/src/admin/js/util/format.ts +46 -0
  24. package/src/admin/js/util/metrics.ts +24 -0
  25. package/src/admin/lib/chart.bundle.css +1 -0
  26. package/src/admin/lib/chart.bundle.js +146 -0
  27. package/src/admin/style.css +1437 -0
  28. package/src/bin/load-test.js +202 -0
  29. package/src/handler/handlerAdmin.js +293 -0
  30. package/src/handler/plsql/cgi.js +1 -1
  31. package/src/handler/plsql/errorPage.js +31 -6
  32. package/src/handler/plsql/handlerPlSql.js +32 -6
  33. package/src/handler/plsql/owaPageStream.js +93 -0
  34. package/src/handler/plsql/parsePage.js +7 -3
  35. package/src/handler/plsql/procedure.js +221 -105
  36. package/src/handler/plsql/procedureNamed.js +34 -49
  37. package/src/handler/plsql/procedureSanitize.js +120 -45
  38. package/src/handler/plsql/procedureVariable.js +4 -2
  39. package/src/handler/plsql/request.js +8 -3
  40. package/src/handler/plsql/sendResponse.js +43 -5
  41. package/src/index.js +0 -1
  42. package/src/server/adminContext.js +23 -0
  43. package/src/server/config.js +1 -0
  44. package/src/server/server.js +130 -9
  45. package/src/types.js +7 -1
  46. package/src/util/cache.js +123 -0
  47. package/src/util/jsonLogger.js +47 -0
  48. package/src/util/shutdown.js +6 -2
  49. package/src/util/statsManager.js +368 -0
  50. package/src/util/trace.js +7 -6
  51. package/src/util/traceManager.js +68 -0
  52. package/src/version.js +1 -1
  53. package/types/admin/js/schemas.d.ts +384 -0
  54. package/types/admin/js/types.d.ts +312 -0
  55. package/types/handler/handlerAdmin.d.ts +79 -0
  56. package/types/handler/plsql/errorPage.d.ts +1 -0
  57. package/types/handler/plsql/handlerPlSql.d.ts +6 -1
  58. package/types/handler/plsql/owaPageStream.d.ts +28 -0
  59. package/types/handler/plsql/procedure.d.ts +4 -1
  60. package/types/handler/plsql/procedureNamed.d.ts +2 -5
  61. package/types/handler/plsql/procedureSanitize.d.ts +2 -5
  62. package/types/handler/plsql/procedureVariable.d.ts +1 -1
  63. package/types/handler/plsql/request.d.ts +3 -1
  64. package/types/handler/plsql/sendResponse.d.ts +1 -1
  65. package/types/index.d.ts +0 -1
  66. package/types/server/adminContext.d.ts +17 -0
  67. package/types/server/server.d.ts +5 -0
  68. package/types/types.d.ts +19 -1
  69. package/types/util/cache.d.ts +69 -0
  70. package/types/util/jsonLogger.d.ts +45 -0
  71. package/types/util/statsManager.d.ts +395 -0
  72. package/types/util/traceManager.d.ts +35 -0
  73. package/src/handler/handlerMetrics.js +0 -68
  74. package/types/handler/handlerMetrics.d.ts +0 -25
@@ -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,
@@ -88,22 +83,28 @@ const loadArguments = async (procedure, databaseConnection) => {
88
83
  try {
89
84
  result = await databaseConnection.execute(SQL_GET_ARGUMENT, bind);
90
85
  } catch (err) {
86
+ /* v8 ignore start */
91
87
  debug('result', result);
88
+ /* v8 ignore stop */
89
+
92
90
  const message = `Error when retrieving arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`;
93
91
  throw new RequestError(message);
94
92
  }
95
93
 
96
- /** @type {{names: string[], types: string[]}} */
94
+ /** @type {{names: (string | null)[], types: (string | null)[]}} */
97
95
  let data;
98
96
  try {
99
97
  data = z
100
98
  .object({
101
- names: z.array(z.string()),
102
- types: z.array(z.string()),
99
+ names: z.array(z.string().nullable()),
100
+ types: z.array(z.string().nullable()),
103
101
  })
104
102
  .parse(result.outBinds);
105
103
  } catch (err) {
104
+ /* v8 ignore start */
106
105
  debug('result.outBinds', result.outBinds);
106
+ /* v8 ignore stop */
107
+
107
108
  const message = `Error when decoding arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`;
108
109
  throw new RequestError(message);
109
110
  }
@@ -115,71 +116,52 @@ const loadArguments = async (procedure, databaseConnection) => {
115
116
  /** @type {Record<string, string>} */
116
117
  const argTypes = {};
117
118
  for (let i = 0; i < data.names.length; i++) {
118
- argTypes[data.names[i].toLowerCase()] = data.types[i];
119
+ const name = data.names[i];
120
+ const type = data.types[i];
121
+ if (name && type) {
122
+ argTypes[name.toLowerCase()] = type;
123
+ }
119
124
  }
120
125
 
121
126
  return argTypes;
122
127
  };
123
128
 
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
129
  /**
146
130
  * Find the argument types for a given procedure to be executed.
147
131
  * As the arguments are cached, we first look up the cache and only if not yet available we load them.
148
132
  * @param {string} procedure - The procedure
149
133
  * @param {Connection} databaseConnection - The database connection
134
+ * @param {ArgumentCache} argumentCache - The argument cache.
150
135
  * @returns {Promise<argsType>} - The argument types
151
136
  */
152
- const findArguments = async (procedure, databaseConnection) => {
137
+ const findArguments = async (procedure, databaseConnection, argumentCache) => {
153
138
  const key = procedure.toUpperCase();
154
139
 
155
140
  // lookup in the cache
156
- const cacheEntry = ARGS_CACHE.get(key);
141
+ const cachedArgs = argumentCache.get(key);
157
142
 
158
- // if we fount the procedure in the cache, we increase the hit cound and return
159
- if (cacheEntry) {
160
- cacheEntry.hitCount++;
143
+ // if we found the procedure in the cache, we return it
144
+ if (cachedArgs) {
145
+ /* v8 ignore start */
161
146
  if (debug.enabled) {
162
- debug(`findArguments: procedure "${procedure}" found in cache with "${cacheEntry.hitCount}" hits`);
147
+ debug(`findArguments: procedure "${procedure}" found in cache`);
163
148
  }
164
- return cacheEntry.args;
165
- }
149
+ /* v8 ignore stop */
166
150
 
167
- // if the cache is full, we remove the 1000 least used cache entries
168
- if (ARGS_CACHE.size > ARGS_CACHE_MAX_COUNT) {
169
- if (debug.enabled) {
170
- debug(`findArguments: cache is full. size=${ARGS_CACHE.size} max=${ARGS_CACHE_MAX_COUNT}`);
171
- }
172
- removeLowestHitCountEntries(1000);
151
+ return cachedArgs;
173
152
  }
174
153
 
175
154
  // load from database
155
+ /* v8 ignore start */
176
156
  if (debug.enabled) {
177
157
  debug(`findArguments: procedure "${procedure}" not found in cache and must be loaded`);
178
158
  }
159
+ /* v8 ignore stop */
160
+
179
161
  const args = await loadArguments(procedure, databaseConnection);
180
162
 
181
163
  // add to the cache
182
- ARGS_CACHE.set(key, {hitCount: 0, args});
164
+ argumentCache.set(key, args);
183
165
 
184
166
  return args;
185
167
  };
@@ -238,7 +220,7 @@ export const getBinding = (argName, argValue, argType) => {
238
220
  */
239
221
  const inspectBindings = (argObj, argTypes) => {
240
222
  const rows = Object.entries(argObj).map(([key, value]) => {
241
- return [key, value.toString(), typeof value, argTypes[key.toLowerCase()]];
223
+ return [key, value.toString(), typeof value, argTypes[key.toLowerCase()] ?? 'unknown'];
242
224
  });
243
225
 
244
226
  const {text} = toTable(['id', 'value', 'value type', 'argument type'], rows);
@@ -252,13 +234,14 @@ const inspectBindings = (argObj, argTypes) => {
252
234
  * @param {string} procName - The procedure to execute
253
235
  * @param {argObjType} argObj - The arguments to pass to the procedure
254
236
  * @param {Connection} databaseConnection - The database connection
237
+ * @param {ArgumentCache} argumentCache - The argument cache.
255
238
  * @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
256
239
  */
257
- export const getProcedureNamed = async (req, procName, argObj, databaseConnection) => {
240
+ export const getProcedureNamed = async (req, procName, argObj, databaseConnection, argumentCache) => {
258
241
  debug(`getProcedureNamed: ${procName} arguments=`, argObj);
259
242
 
260
243
  // get the types of the arguments
261
- const argTypes = await findArguments(procName, databaseConnection);
244
+ const argTypes = await findArguments(procName, databaseConnection, argumentCache);
262
245
 
263
246
  /** @type {string[]} */
264
247
  const sqlParameter = [];
@@ -288,10 +271,12 @@ export const getProcedureNamed = async (req, procName, argObj, databaseConnectio
288
271
  // select statement
289
272
  const sql = `${procName}(${sqlParameter.join(', ')})`;
290
273
 
274
+ /* v8 ignore start */
291
275
  if (debug.enabled) {
292
276
  debug(sql);
293
277
  debug(inspectBindings(argObj, argTypes));
294
278
  }
279
+ /* v8 ignore stop */
295
280
 
296
281
  return {sql, bind: bindings};
297
282
  };
@@ -16,17 +16,93 @@ 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
+ /* v8 ignore start */
99
+ debug(`resolveProcedureName: Error resolving "${procName}"`, err);
100
+ /* v8 ignore stop */
101
+
102
+ // Rethrow as RequestError to indicate 404/403
103
+ throw new RequestError(`Procedure "${procName}" not found or not accessible.\n${errorToString(err)}`);
104
+ }
105
+ };
30
106
 
31
107
  /**
32
108
  * Sanitize the procedure name.
@@ -34,22 +110,26 @@ const REQUEST_VALIDATION_FUNCTION_CACHE_MAX_COUNT = 10000;
34
110
  * @param {string} procName - The procedure name.
35
111
  * @param {Connection} databaseConnection - The database connection
36
112
  * @param {configPlSqlHandlerType} options - the options for the middleware.
113
+ * @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
37
114
  * @returns {Promise<string>} Promise resolving to final procedure name.
38
115
  */
39
- export const sanitizeProcName = async (procName, databaseConnection, options) => {
116
+ export const sanitizeProcName = async (procName, databaseConnection, options, procedureNameCache) => {
40
117
  debug('sanitizeProcName', procName);
41
118
 
42
119
  // make lowercase and trim
43
120
  let finalProcName = procName.toLowerCase().trim();
44
121
 
45
- // remove special characters
122
+ // remove special characters (basic sanity check before DB call)
46
123
  finalProcName = removeSpecialCharacters(finalProcName);
47
124
 
48
125
  // check for default exclusions
49
126
  for (const i of DEFAULT_EXCLUSION_LIST) {
50
127
  if (finalProcName.startsWith(i)) {
51
128
  const error = `Procedure name "${procName}" is in default exclusion list "${DEFAULT_EXCLUSION_LIST.join(',')}"`;
129
+ /* v8 ignore start */
52
130
  debug(error);
131
+ /* v8 ignore stop */
132
+
53
133
  throw new RequestError(error);
54
134
  }
55
135
  }
@@ -59,7 +139,10 @@ export const sanitizeProcName = async (procName, databaseConnection, options) =>
59
139
  for (const i of options.exclusionList) {
60
140
  if (finalProcName.startsWith(i)) {
61
141
  const error = `Procedure name "${procName}" is in custom exclusion list "${options.exclusionList.join(',')}"`;
142
+ /* v8 ignore start */
62
143
  debug(error);
144
+ /* v8 ignore stop */
145
+
63
146
  throw new RequestError(error);
64
147
  }
65
148
  }
@@ -67,15 +150,26 @@ export const sanitizeProcName = async (procName, databaseConnection, options) =>
67
150
 
68
151
  // Check request validation function
69
152
  if (options.requestValidationFunction && options.requestValidationFunction.length > 0) {
153
+ // Note: We might want to scope this cache too, but for now let's focus on the procedure name cache.
154
+ // The original code used a global cache for this.
155
+ // For strict correctness, we should probably verify the *resolved* name,
156
+ // but legacy behavior checks the input name.
70
157
  const valid = await requestValidationFunction(finalProcName, options.requestValidationFunction, databaseConnection);
71
158
  if (!valid) {
72
159
  const error = `Procedure name "${procName}" is not valid according to the request validation function "${options.requestValidationFunction}"`;
160
+ /* v8 ignore start */
73
161
  debug(error);
162
+ /* v8 ignore stop */
163
+
74
164
  throw new RequestError(error);
75
165
  }
76
166
  }
77
167
 
78
- return finalProcName;
168
+ // NEW: Resolve the procedure name against the database
169
+ // This prevents SQL injection and ensures the procedure exists
170
+ const resolvedName = await resolveProcedureName(finalProcName, databaseConnection, procedureNameCache);
171
+
172
+ return resolvedName;
79
173
  };
80
174
 
81
175
  /**
@@ -127,7 +221,10 @@ const loadRequestValid = async (procName, requestValidationFunction, databaseCon
127
221
  try {
128
222
  result = await databaseConnection.execute(SQL, bind);
129
223
  } catch (err) {
224
+ /* v8 ignore start */
130
225
  debug('result', result);
226
+ /* v8 ignore stop */
227
+
131
228
  const message = `Error when validating procedure name "${procName}"\n${SQL}\n${errorToString(err)}`;
132
229
  throw new RequestError(message);
133
230
  }
@@ -136,33 +233,15 @@ const loadRequestValid = async (procName, requestValidationFunction, databaseCon
136
233
  const data = z.strictObject({valid: z.number()}).parse(result.outBinds);
137
234
  return data.valid === 1;
138
235
  } catch (err) {
236
+ /* v8 ignore start */
139
237
  debug('result', result.outBinds);
238
+ /* v8 ignore stop */
239
+
140
240
  const message = `Internal error when parsing ${result.outBinds}\n${errorToString(err)}`;
141
241
  throw new Error(message);
142
242
  }
143
243
  };
144
244
 
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
245
  /**
167
246
  * Request validation function.
168
247
  *
@@ -175,37 +254,33 @@ const requestValidationFunction = async (procName, requestValidationFunction, da
175
254
  debug('requestValidationFunction', procName, requestValidationFunction);
176
255
 
177
256
  // calculate the key
178
- //const key = `${databaseConnection.connectString}_${databaseConnection.user}_${procName.toLowerCase()}`;
179
257
  const key = procName.toLowerCase();
180
258
 
181
259
  // lookup in the cache
182
- const cacheEntry = REQUEST_VALIDATION_FUNCTION_CACHE.get(key);
260
+ const cacheEntry = validationFunctionCache.get(key);
183
261
 
184
- // if we fount the procedure in the cache, we increase the hit cound and return
185
- if (cacheEntry) {
186
- cacheEntry.hitCount++;
262
+ // if we found the procedure in the cache, we return it (hitCount already incremented by get)
263
+ if (cacheEntry !== undefined) {
264
+ /* v8 ignore start */
187
265
  if (debug.enabled) {
188
- debug(`findArguments: procedure "${procName}" found in cache with "${cacheEntry.hitCount}" hits`);
266
+ debug(`requestValidationFunction: procedure "${procName}" found in cache`);
189
267
  }
190
- return cacheEntry.valid;
191
- }
268
+ /* v8 ignore stop */
192
269
 
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);
270
+ return cacheEntry.valid;
199
271
  }
200
272
 
201
273
  // load from database
274
+ /* v8 ignore start */
202
275
  if (debug.enabled) {
203
- debug(`findArguments: procedure "${procName}" not found in cache and must be loaded`);
276
+ debug(`requestValidationFunction: procedure "${procName}" not found in cache and must be loaded`);
204
277
  }
278
+ /* v8 ignore stop */
279
+
205
280
  const valid = await loadRequestValid(procName, requestValidationFunction, databaseConnection);
206
281
 
207
282
  // add to the cache
208
- REQUEST_VALIDATION_FUNCTION_CACHE.set(key, {hitCount: 0, valid});
283
+ validationFunctionCache.set(key, {valid});
209
284
 
210
285
  return valid;
211
286
  };
@@ -18,15 +18,17 @@ 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
+ /* v8 ignore start */
27
28
  if (debug.enabled) {
28
29
  debug(`getProcedureVariable: ${procName} arguments=`, argObj);
29
30
  }
31
+ /* v8 ignore stop */
30
32
 
31
33
  const names = [];
32
34
  const values = [];
@@ -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,30 +18,34 @@ 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
 
30
30
  // Send the "cookies"
31
31
  page.head.cookies.forEach((cookie) => {
32
+ /* v8 ignore start */
32
33
  if (debug.enabled) {
33
34
  debugText.push(`res.cookie: name="${cookie.name}" value="${cookie.value}" options=${JSON.stringify(cookie.options)}`);
34
35
  }
36
+ /* v8 ignore stop */
35
37
 
36
38
  res.cookie(cookie.name, cookie.value, cookie.options);
37
39
  });
38
40
 
39
41
  // If there is a "redirectLocation" header, we immediately redirect and return
40
42
  if (typeof page.head.redirectLocation === 'string' && page.head.redirectLocation.length > 0) {
43
+ /* v8 ignore start */
41
44
  if (debug.enabled) {
42
45
  debugText.push(`res.redirect(302, "${page.head.redirectLocation}")`);
43
46
  debug(getBlock('RESPONSE', debugText.join('\n')));
44
47
  }
48
+ /* v8 ignore stop */
45
49
 
46
50
  res.redirect(302, page.head.redirectLocation);
47
51
  return;
@@ -49,9 +53,11 @@ export const sendResponse = async (req, res, page) => {
49
53
 
50
54
  // Send all the "otherHeaders"
51
55
  for (const key in page.head.otherHeaders) {
56
+ /* v8 ignore start */
52
57
  if (debug.enabled) {
53
58
  debugText.push(`res.set("${key}", "${page.head.otherHeaders[key]}")`);
54
59
  }
60
+ /* v8 ignore stop */
55
61
 
56
62
  res.set(key, page.head.otherHeaders[key]);
57
63
  }
@@ -70,18 +76,23 @@ export const sendResponse = async (req, res, page) => {
70
76
  }
71
77
 
72
78
  if (Object.keys(headers).length > 0) {
79
+ /* v8 ignore start */
73
80
  if (debug.enabled) {
74
81
  debugText.push(`res.writeHead(200, ${JSON.stringify(headers)})`);
75
82
  }
83
+ /* v8 ignore stop */
84
+
76
85
  res.writeHead(200, headers);
77
86
  }
78
87
 
79
88
  // Check if fileBlob is a stream
80
89
  if (page.file.fileBlob instanceof stream.Readable) {
90
+ /* v8 ignore start */
81
91
  if (debug.enabled) {
82
92
  debugText.push(`res.pipe("${page.file.fileType}") - streaming`);
83
93
  debug(getBlock('RESPONSE', debugText.join('\n')));
84
94
  }
95
+ /* v8 ignore stop */
85
96
 
86
97
  /** @type {Promise<void>} */
87
98
  const streamComplete = new Promise((resolve, reject) => {
@@ -96,10 +107,12 @@ export const sendResponse = async (req, res, page) => {
96
107
  });
97
108
  await streamComplete;
98
109
  } else {
110
+ /* v8 ignore start */
99
111
  if (debug.enabled) {
100
112
  debugText.push(`res.end("${page.file.fileType}") - buffer`);
101
113
  debug(getBlock('RESPONSE', debugText.join('\n')));
102
114
  }
115
+ /* v8 ignore stop */
103
116
 
104
117
  res.end(page.file.fileBlob, 'binary');
105
118
  }
@@ -108,29 +121,54 @@ export const sendResponse = async (req, res, page) => {
108
121
 
109
122
  // Is the a "contentType" header
110
123
  if (typeof page.head.contentType === 'string' && page.head.contentType.length > 0) {
124
+ /* v8 ignore start */
111
125
  if (debug.enabled) {
112
126
  debugText.push(`res.set("Content-Type", "${page.head.contentType}")`);
113
127
  }
128
+ /* v8 ignore stop */
114
129
 
115
130
  res.set('Content-Type', page.head.contentType);
116
131
  }
117
132
 
118
133
  // If we have a "Status" header, we send the header and then return.
119
134
  if (typeof page.head.statusCode === 'number') {
135
+ /* v8 ignore start */
120
136
  if (debug.enabled) {
121
137
  debugText.push(`res.status(page.head.statusCode).send("${page.head.statusDescription}")`);
122
138
  debug(getBlock('RESPONSE', debugText.join('\n')));
123
139
  }
140
+ /* v8 ignore stop */
124
141
 
125
142
  res.status(page.head.statusCode).send(page.head.statusDescription);
126
143
  return;
127
144
  }
128
145
 
129
146
  // Send the body
147
+ /* v8 ignore start */
130
148
  if (debug.enabled) {
131
- debugText.push(`${'-'.repeat(60)}\n${page.body}`);
149
+ if (page.body instanceof stream.Readable) {
150
+ debugText.push(`${'-'.repeat(60)}\n[Stream Body]`);
151
+ } else {
152
+ debugText.push(`${'-'.repeat(60)}\n${page.body}`);
153
+ }
132
154
  debug(getBlock('RESPONSE', debugText.join('\n')));
133
155
  }
134
-
135
- res.send(page.body);
156
+ /* v8 ignore stop */
157
+
158
+ if (page.body instanceof stream.Readable) {
159
+ /** @type {Promise<void>} */
160
+ const streamComplete = new Promise((resolve, reject) => {
161
+ if (page.body instanceof stream.Readable) {
162
+ page.body.pipe(res);
163
+ page.body.on('end', () => resolve());
164
+ /* v8 ignore next - error handler */
165
+ page.body.on('error', (/** @type {Error} */ err) => reject(err));
166
+ /* v8 ignore next - error handler */
167
+ res.on('close', () => resolve());
168
+ }
169
+ });
170
+ await streamComplete;
171
+ } else {
172
+ res.send(page.body);
173
+ }
136
174
  };
package/src/index.js CHANGED
@@ -11,7 +11,6 @@ export * from './util/shutdown.js';
11
11
  export {handlerWebPlSql} from './handler/plsql/handlerPlSql.js';
12
12
  export {handlerLogger} from './handler/handlerLogger.js';
13
13
  export {handlerUpload} from './handler/handlerUpload.js';
14
- export {handlerMetrics} from './handler/handlerMetrics.js';
15
14
 
16
15
  // oracle
17
16
  export * from './util/oracle.js';
@@ -0,0 +1,23 @@
1
+ import {StatsManager} from '../util/statsManager.js';
2
+
3
+ /**
4
+ * @typedef {import('oracledb').Pool} Pool
5
+ * @typedef {import('../types.js').configType} configType
6
+ * @typedef {import('../handler/plsql/procedureNamed.js').argsType} argsType
7
+ * @typedef {import('../util/cache.js').Cache<unknown>} Cache
8
+ */
9
+
10
+ /**
11
+ * Global Admin Context
12
+ */
13
+ export const AdminContext = {
14
+ startTime: new Date(),
15
+ /** @type {configType | null} */
16
+ config: null,
17
+ /** @type {Pool[]} */
18
+ pools: [],
19
+ /** @type {Array<{poolName: string, procedureNameCache: Cache, argumentCache: Cache}>} */
20
+ caches: [],
21
+ paused: false,
22
+ statsManager: new StatsManager(),
23
+ };