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.
- package/README.md +29 -9
- package/package.json +17 -7
- package/src/admin/client/charts.ts +598 -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 +529 -0
- package/src/admin/js/api.ts +170 -0
- package/src/admin/js/app.ts +614 -0
- package/src/admin/js/eslint.config.js +74 -0
- package/src/admin/js/schemas.ts +215 -0
- package/src/admin/js/templates/config.ts +147 -0
- package/src/admin/js/templates/errorRow.ts +21 -0
- package/src/admin/js/templates/index.ts +4 -0
- package/src/admin/js/templates/poolCard.ts +61 -0
- package/src/admin/js/templates/traceRow.ts +24 -0
- package/src/admin/js/tsconfig.json +24 -0
- package/src/admin/js/types.ts +336 -0
- package/src/admin/js/ui/components.ts +44 -0
- package/src/admin/js/ui/table.ts +173 -0
- package/src/admin/js/ui/theme.ts +93 -0
- package/src/admin/js/ui/views.ts +427 -0
- package/src/admin/js/util/format.ts +46 -0
- package/src/admin/js/util/metrics.ts +24 -0
- package/src/admin/lib/chart.bundle.css +1 -0
- package/src/admin/lib/chart.bundle.js +146 -0
- package/src/admin/style.css +1437 -0
- package/src/bin/load-test.js +202 -0
- package/src/handler/handlerAdmin.js +293 -0
- package/src/handler/plsql/cgi.js +1 -1
- package/src/handler/plsql/errorPage.js +31 -6
- package/src/handler/plsql/handlerPlSql.js +32 -6
- package/src/handler/plsql/owaPageStream.js +93 -0
- package/src/handler/plsql/parsePage.js +7 -3
- package/src/handler/plsql/procedure.js +221 -105
- package/src/handler/plsql/procedureNamed.js +34 -49
- package/src/handler/plsql/procedureSanitize.js +120 -45
- package/src/handler/plsql/procedureVariable.js +4 -2
- package/src/handler/plsql/request.js +8 -3
- package/src/handler/plsql/sendResponse.js +43 -5
- package/src/index.js +0 -1
- package/src/server/adminContext.js +23 -0
- package/src/server/config.js +1 -0
- package/src/server/server.js +130 -9
- package/src/types.js +7 -1
- package/src/util/cache.js +123 -0
- package/src/util/jsonLogger.js +47 -0
- package/src/util/shutdown.js +6 -2
- package/src/util/statsManager.js +368 -0
- package/src/util/trace.js +7 -6
- package/src/util/traceManager.js +68 -0
- package/src/version.js +1 -1
- package/types/admin/js/schemas.d.ts +384 -0
- package/types/admin/js/types.d.ts +312 -0
- package/types/handler/handlerAdmin.d.ts +79 -0
- package/types/handler/plsql/errorPage.d.ts +1 -0
- package/types/handler/plsql/handlerPlSql.d.ts +6 -1
- package/types/handler/plsql/owaPageStream.d.ts +28 -0
- package/types/handler/plsql/procedure.d.ts +4 -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/index.d.ts +0 -1
- package/types/server/adminContext.d.ts +17 -0
- package/types/server/server.d.ts +5 -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/util/statsManager.d.ts +395 -0
- package/types/util/traceManager.d.ts +35 -0
- package/src/handler/handlerMetrics.js +0 -68
- 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 {
|
|
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
|
-
|
|
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
|
|
141
|
+
const cachedArgs = argumentCache.get(key);
|
|
157
142
|
|
|
158
|
-
// if we
|
|
159
|
-
if (
|
|
160
|
-
|
|
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
|
|
147
|
+
debug(`findArguments: procedure "${procedure}" found in cache`);
|
|
163
148
|
}
|
|
164
|
-
|
|
165
|
-
}
|
|
149
|
+
/* v8 ignore stop */
|
|
166
150
|
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
/* 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
|
-
|
|
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 =
|
|
260
|
+
const cacheEntry = validationFunctionCache.get(key);
|
|
183
261
|
|
|
184
|
-
// if we
|
|
185
|
-
if (cacheEntry) {
|
|
186
|
-
|
|
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(`
|
|
266
|
+
debug(`requestValidationFunction: procedure "${procName}" found in cache`);
|
|
189
267
|
}
|
|
190
|
-
|
|
191
|
-
}
|
|
268
|
+
/* v8 ignore stop */
|
|
192
269
|
|
|
193
|
-
|
|
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(`
|
|
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
|
-
|
|
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}
|
|
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
|
+
/* 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
|
|
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}
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
};
|