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.
- package/README.md +24 -4
- package/package.json +21 -8
- package/src/admin/client/charts.ts +299 -0
- package/src/admin/client/main.js +3 -0
- package/src/admin/client/tailwind.css +41 -0
- package/src/admin/favicon.svg +3 -0
- package/src/admin/index.html +315 -0
- package/src/admin/js/api.ts +95 -0
- package/src/admin/js/app.ts +306 -0
- package/src/admin/js/eslint.config.js +74 -0
- package/src/admin/js/schemas.ts +153 -0
- package/src/admin/js/templates/config.ts +146 -0
- package/src/admin/js/templates/errorRow.ts +18 -0
- package/src/admin/js/templates/index.ts +3 -0
- package/src/admin/js/templates/poolCard.ts +61 -0
- package/src/admin/js/tsconfig.json +24 -0
- package/src/admin/js/types.ts +223 -0
- package/src/admin/js/ui/theme.ts +93 -0
- package/src/admin/js/ui/views.ts +164 -0
- package/src/admin/js/util/format.ts +27 -0
- package/src/admin/lib/assets/main-zpdhQ1gD.css +1 -0
- package/src/admin/lib/chart.bundle.js +139 -0
- package/src/admin/style.css +1321 -0
- package/src/bin/load-test.js +202 -0
- package/src/handler/handlerAdmin.js +198 -0
- package/src/handler/plsql/cgi.js +1 -1
- package/src/handler/plsql/errorPage.js +37 -12
- package/src/handler/plsql/handlerPlSql.js +32 -6
- package/src/handler/plsql/parsePage.js +15 -11
- package/src/handler/plsql/procedure.js +81 -33
- package/src/handler/plsql/procedureNamed.js +18 -52
- package/src/handler/plsql/procedureSanitize.js +96 -49
- package/src/handler/plsql/procedureVariable.js +2 -2
- package/src/handler/plsql/request.js +11 -6
- package/src/handler/plsql/sendResponse.js +43 -11
- package/src/handler/plsql/stream.js +1 -1
- package/src/handler/plsql/upload.js +1 -1
- package/src/server/config.js +1 -0
- package/src/server/server.js +108 -2
- package/src/types.js +7 -1
- package/src/util/cache.js +123 -0
- package/src/util/file.js +5 -9
- package/src/util/jsonLogger.js +47 -0
- package/src/util/shutdown.js +6 -2
- package/src/util/trace.js +5 -5
- package/src/version.js +1 -1
- package/types/handler/handlerAdmin.d.ts +9 -0
- package/types/handler/plsql/errorPage.d.ts +1 -0
- package/types/handler/plsql/handlerPlSql.d.ts +6 -1
- package/types/handler/plsql/procedure.d.ts +3 -1
- package/types/handler/plsql/procedureNamed.d.ts +2 -5
- package/types/handler/plsql/procedureSanitize.d.ts +2 -5
- package/types/handler/plsql/procedureVariable.d.ts +1 -1
- package/types/handler/plsql/request.d.ts +3 -1
- package/types/handler/plsql/sendResponse.d.ts +1 -1
- package/types/server/server.d.ts +22 -0
- package/types/types.d.ts +19 -1
- package/types/util/cache.d.ts +69 -0
- package/types/util/jsonLogger.d.ts +45 -0
- 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
|
-
* @
|
|
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
|
|
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
|
|
69
|
+
return {
|
|
70
|
+
...getProcedureVariable(req, sanitizedProcName, argObj),
|
|
71
|
+
resolvedName: sanitizedProcName,
|
|
72
|
+
};
|
|
65
73
|
} else {
|
|
66
|
-
return
|
|
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}
|
|
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 (
|
|
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
|
-
/*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
285
|
-
const pageComponents = parsePage(lines);
|
|
329
|
+
// 7) parse the page
|
|
286
330
|
|
|
287
|
-
|
|
288
|
-
|
|
331
|
+
debug('invokeProcedure: parse the page');
|
|
332
|
+
const pageComponents = parsePage(lines);
|
|
289
333
|
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
300
|
-
sendResponse(req, res, pageComponents);
|
|
344
|
+
// 8) send the page to browser
|
|
301
345
|
|
|
302
|
-
|
|
346
|
+
debug('invokeProcedure: send the page to browser');
|
|
347
|
+
await sendResponse(req, res, pageComponents);
|
|
348
|
+
} finally {
|
|
349
|
+
// 9) cleanup
|
|
303
350
|
|
|
304
|
-
|
|
305
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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
|
|
173
|
-
if (
|
|
137
|
+
// if we found the procedure in the cache, we return it
|
|
138
|
+
if (cachedArgs) {
|
|
174
139
|
if (debug.enabled) {
|
|
175
|
-
debug(`findArguments:
|
|
140
|
+
debug(`findArguments: procedure "${procedure}" found in cache`);
|
|
176
141
|
}
|
|
177
|
-
|
|
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
|
-
|
|
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
|
-
|
|
24
|
+
import {Cache} from '../../util/cache.js';
|
|
25
|
+
/** @type {Cache<{valid: boolean}>} */
|
|
26
|
+
const validationFunctionCache = new Cache();
|
|
27
|
+
|
|
24
28
|
/**
|
|
25
|
-
*
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
const
|
|
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
|
-
|
|
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 =
|
|
242
|
+
const cacheEntry = validationFunctionCache.get(key);
|
|
187
243
|
|
|
188
|
-
// if we
|
|
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(`
|
|
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(`
|
|
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
|
-
|
|
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}
|
|
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 = (
|
|
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
|
|
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
|
-
/*
|
|
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
|
-
/*
|
|
111
|
+
/* v8 ignore else - type validation */
|
|
107
112
|
if (isStringOrArrayOfString(value)) {
|
|
108
113
|
args[key] = value;
|
|
109
114
|
} else {
|
|
110
|
-
/*
|
|
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
|
);
|