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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import {Readable} from 'node:stream';
|
|
2
|
+
import oracledb from 'oracledb';
|
|
3
|
+
import z from 'zod';
|
|
4
|
+
import debugModule from 'debug';
|
|
5
|
+
import {ProcedureError} from './procedureError.js';
|
|
6
|
+
import {errorToString} from '../../util/errorToString.js';
|
|
7
|
+
|
|
8
|
+
const debug = debugModule('webplsql:owaPageStream');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('oracledb').Connection} Connection
|
|
12
|
+
* @typedef {import('../../types.js').BindParameterConfig} BindParameterConfig
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export class OWAPageStream extends Readable {
|
|
16
|
+
/**
|
|
17
|
+
* @param {Connection} databaseConnection - The database connection.
|
|
18
|
+
*/
|
|
19
|
+
constructor(databaseConnection) {
|
|
20
|
+
super({highWaterMark: 16384}); // 16KB buffer
|
|
21
|
+
this.databaseConnection = databaseConnection;
|
|
22
|
+
this.chunkSize = 1000;
|
|
23
|
+
this.isDone = false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Fetch a chunk of the page from the database.
|
|
28
|
+
* @returns {Promise<string[]>} The array of lines fetched.
|
|
29
|
+
*/
|
|
30
|
+
async fetchChunk() {
|
|
31
|
+
if (this.isDone) return [];
|
|
32
|
+
|
|
33
|
+
/** @type {BindParameterConfig} */
|
|
34
|
+
const bindParameter = {
|
|
35
|
+
lines: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxArraySize: this.chunkSize},
|
|
36
|
+
irows: {dir: oracledb.BIND_INOUT, type: oracledb.NUMBER, val: this.chunkSize},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const sqlStatement = 'BEGIN owa.get_page(thepage=>:lines, irows=>:irows); END;';
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const result = await this.databaseConnection.execute(sqlStatement, bindParameter);
|
|
43
|
+
const {lines, irows} = z.object({irows: z.number(), lines: z.array(z.string())}).parse(result.outBinds);
|
|
44
|
+
|
|
45
|
+
debug(`fetched ${lines.length} lines (irows=${irows})`);
|
|
46
|
+
|
|
47
|
+
// If we got fewer lines than requested, OR if we got 0 lines, we are done
|
|
48
|
+
if (lines.length < this.chunkSize) {
|
|
49
|
+
this.isDone = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return lines;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
if (err instanceof ProcedureError) {
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
throw new ProcedureError(`OWAPageStream: error when getting page\n${errorToString(err)}`, {}, sqlStatement, bindParameter);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @override
|
|
63
|
+
* @param {number} _size - The size hint (unused).
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
_read(_size) {
|
|
67
|
+
this.fetchChunk()
|
|
68
|
+
.then((lines) => {
|
|
69
|
+
if (lines.length > 0) {
|
|
70
|
+
this.push(lines.join(''));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// After fetching, check if we're done
|
|
74
|
+
if (this.isDone) {
|
|
75
|
+
this.push(null);
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
.catch((/** @type {unknown} */ err) => {
|
|
79
|
+
this.destroy(err instanceof Error ? err : new Error(String(err)));
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Add initial body content to the stream.
|
|
85
|
+
* @param {string} content - The initial content to prepend.
|
|
86
|
+
* @returns {void}
|
|
87
|
+
*/
|
|
88
|
+
addBody(content) {
|
|
89
|
+
if (content && content.length > 0) {
|
|
90
|
+
this.push(content);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -155,7 +155,11 @@ const parseCookie = (text) => {
|
|
|
155
155
|
cookieElements = cookieElements.map((element) => element.trim());
|
|
156
156
|
|
|
157
157
|
// get name and value
|
|
158
|
-
const
|
|
158
|
+
const firstElement = cookieElements[0];
|
|
159
|
+
if (!firstElement) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
const index = firstElement.indexOf('=');
|
|
159
163
|
/* v8 ignore next - cookie format validation */
|
|
160
164
|
if (index <= 0) {
|
|
161
165
|
// if the index is -1, there is no equal sign and if it's 0 the name is empty
|
|
@@ -164,8 +168,8 @@ const parseCookie = (text) => {
|
|
|
164
168
|
|
|
165
169
|
/** @type {cookieType} */
|
|
166
170
|
const cookie = {
|
|
167
|
-
name:
|
|
168
|
-
value:
|
|
171
|
+
name: firstElement.substring(0, index).trim(),
|
|
172
|
+
value: firstElement.substring(index + 1).trim(),
|
|
169
173
|
options: {},
|
|
170
174
|
};
|
|
171
175
|
|
|
@@ -15,9 +15,16 @@ import {getProcedureNamed} from './procedureNamed.js';
|
|
|
15
15
|
import {parsePage} from './parsePage.js';
|
|
16
16
|
import {sendResponse} from './sendResponse.js';
|
|
17
17
|
import {ProcedureError} from './procedureError.js';
|
|
18
|
+
import {RequestError} from './requestError.js';
|
|
18
19
|
import {inspect, getBlock} from '../../util/trace.js';
|
|
19
20
|
import {errorToString} from '../../util/errorToString.js';
|
|
20
21
|
import {sanitizeProcName} from './procedureSanitize.js';
|
|
22
|
+
import {OWAPageStream} from './owaPageStream.js';
|
|
23
|
+
import {traceManager} from '../../util/traceManager.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {import('../../admin/js/types.js').TraceEntry} TraceEntry
|
|
27
|
+
*/
|
|
21
28
|
|
|
22
29
|
/**
|
|
23
30
|
* @typedef {import('express').Request} Request
|
|
@@ -29,6 +36,8 @@ import {sanitizeProcName} from './procedureSanitize.js';
|
|
|
29
36
|
* @typedef {import('../../types.js').environmentType} environmentType
|
|
30
37
|
* @typedef {import('../../types.js').configPlSqlHandlerType} configPlSqlHandlerType
|
|
31
38
|
* @typedef {import('../../types.js').BindParameterConfig} BindParameterConfig
|
|
39
|
+
* @typedef {import('../../util/cache.js').Cache<string>} ProcedureNameCache
|
|
40
|
+
* @typedef {import('../../util/cache.js').Cache<import('./procedureNamed.js').argsType>} ArgumentCache
|
|
32
41
|
*/
|
|
33
42
|
|
|
34
43
|
/**
|
|
@@ -38,12 +47,16 @@ import {sanitizeProcName} from './procedureSanitize.js';
|
|
|
38
47
|
* @param {argObjType} argObj - The arguments to pass to the procedure
|
|
39
48
|
* @param {configPlSqlHandlerType} options - The options for the middleware
|
|
40
49
|
* @param {Connection} databaseConnection - The database connection
|
|
41
|
-
* @
|
|
50
|
+
* @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
|
|
51
|
+
* @param {ArgumentCache} argumentCache - The argument cache.
|
|
52
|
+
* @returns {Promise<{sql: string; bind: BindParameterConfig; resolvedName?: string}>} - The SQL statement and bindings for the procedure to execute
|
|
42
53
|
*/
|
|
43
|
-
const getProcedure = async (req, procName, argObj, options, databaseConnection) => {
|
|
54
|
+
const getProcedure = async (req, procName, argObj, options, databaseConnection, procedureNameCache, argumentCache) => {
|
|
44
55
|
// path alias
|
|
45
56
|
if (options.pathAlias?.toLowerCase() === procName.toLowerCase()) {
|
|
57
|
+
/* v8 ignore start */
|
|
46
58
|
debug(`getProcedure: path alias "${options.pathAlias}" redirects to "${options.pathAliasProcedure}"`);
|
|
59
|
+
/* v8 ignore stop */
|
|
47
60
|
return {
|
|
48
61
|
sql: `${options.pathAliasProcedure}(p_path=>:p_path)`,
|
|
49
62
|
bind: {
|
|
@@ -56,13 +69,20 @@ const getProcedure = async (req, procName, argObj, options, databaseConnection)
|
|
|
56
69
|
const useVariableArguments = procName.startsWith('!');
|
|
57
70
|
|
|
58
71
|
// sanitize procedure name
|
|
59
|
-
const
|
|
72
|
+
const rawName = useVariableArguments ? procName.substring(1) : procName;
|
|
73
|
+
const sanitizedProcName = await sanitizeProcName(rawName, databaseConnection, options, procedureNameCache);
|
|
60
74
|
|
|
61
75
|
// run procedure
|
|
62
76
|
if (useVariableArguments) {
|
|
63
|
-
return
|
|
77
|
+
return {
|
|
78
|
+
...getProcedureVariable(req, sanitizedProcName, argObj),
|
|
79
|
+
resolvedName: sanitizedProcName,
|
|
80
|
+
};
|
|
64
81
|
} else {
|
|
65
|
-
return
|
|
82
|
+
return {
|
|
83
|
+
...(await getProcedureNamed(req, sanitizedProcName, argObj, databaseConnection, argumentCache)),
|
|
84
|
+
resolvedName: sanitizedProcName,
|
|
85
|
+
};
|
|
66
86
|
}
|
|
67
87
|
};
|
|
68
88
|
|
|
@@ -119,47 +139,6 @@ const procedureExecute = async (para, databaseConnection) => {
|
|
|
119
139
|
}
|
|
120
140
|
};
|
|
121
141
|
|
|
122
|
-
/**
|
|
123
|
-
* Get page from procedure
|
|
124
|
-
*
|
|
125
|
-
* @param {boolean} test - Test.
|
|
126
|
-
* @param {Connection} databaseConnection - Database connection.
|
|
127
|
-
* @returns {Promise<string>} Promise resolving to the returned page content.
|
|
128
|
-
*/
|
|
129
|
-
const procedureGetPage = async (test, databaseConnection) => {
|
|
130
|
-
const MAX_IROWS = 100000;
|
|
131
|
-
|
|
132
|
-
/** @type {BindParameterConfig} */
|
|
133
|
-
const bindParameter = {
|
|
134
|
-
lines: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxArraySize: MAX_IROWS},
|
|
135
|
-
irows: {dir: oracledb.BIND_INOUT, type: oracledb.NUMBER, val: MAX_IROWS},
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
const sqlStatement = 'BEGIN owa.get_page(thepage=>:lines, irows=>:irows); END;';
|
|
139
|
-
|
|
140
|
-
/** @type {Result} */
|
|
141
|
-
let result = {};
|
|
142
|
-
try {
|
|
143
|
-
result = await databaseConnection.execute(sqlStatement, bindParameter);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
if (debug.enabled) {
|
|
146
|
-
debug(getBlock('procedureGetPage: results', inspect(result)));
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
throw new ProcedureError(`procedureGetPage: error when getting page returned by procedure\n${errorToString(err)}`, {}, sqlStatement, bindParameter);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const {lines, irows} = z.object({irows: z.number(), lines: z.array(z.string())}).parse(result.outBinds);
|
|
153
|
-
|
|
154
|
-
// Make sure that we have retrieved all the rows
|
|
155
|
-
if (irows > MAX_IROWS) {
|
|
156
|
-
/* v8 ignore next - defensive check for row limit */
|
|
157
|
-
throw new ProcedureError(`procedureGetPage: error when retrieving rows. irows="${irows}"`, {}, sqlStatement, bindParameter);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return lines.join('');
|
|
161
|
-
};
|
|
162
|
-
|
|
163
142
|
/**
|
|
164
143
|
* Download files from procedure
|
|
165
144
|
*
|
|
@@ -197,9 +176,11 @@ END;
|
|
|
197
176
|
try {
|
|
198
177
|
result = await databaseConnection.execute(sqlStatement, bindParameter);
|
|
199
178
|
} catch (err) {
|
|
179
|
+
/* v8 ignore start */
|
|
200
180
|
if (debug.enabled) {
|
|
201
181
|
debug(getBlock('procedureDownloadFiles: results', inspect(result)));
|
|
202
182
|
}
|
|
183
|
+
/* v8 ignore stop */
|
|
203
184
|
|
|
204
185
|
throw new ProcedureError(`procedureDownloadFiles: error when downloading files\n${errorToString(err)}`, {}, sqlStatement, bindParameter);
|
|
205
186
|
}
|
|
@@ -229,81 +210,216 @@ END;
|
|
|
229
210
|
* @param {fileUploadType[]} filesToUpload - Array of files to be uploaded
|
|
230
211
|
* @param {configPlSqlHandlerType} options - the options for the middleware.
|
|
231
212
|
* @param {Connection} databaseConnection - Database connection.
|
|
213
|
+
* @param {ProcedureNameCache} procedureNameCache - The procedure name cache.
|
|
214
|
+
* @param {ArgumentCache} argumentCache - The argument cache.
|
|
232
215
|
* @returns {Promise<void>} Promise resolving to the page content generated by the executed procedure
|
|
233
216
|
*/
|
|
234
|
-
export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options, databaseConnection) => {
|
|
217
|
+
export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options, databaseConnection, procedureNameCache, argumentCache) => {
|
|
235
218
|
debug('invokeProcedure: begin');
|
|
236
219
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
// 4) execute the procedure
|
|
260
|
-
|
|
261
|
-
debug('invokeProcedure: execute the session');
|
|
262
|
-
await procedureExecute(para, databaseConnection);
|
|
263
|
-
|
|
264
|
-
// 5) get the page returned from the procedure
|
|
265
|
-
|
|
266
|
-
debug('invokeProcedure: get the page returned from the procedure');
|
|
267
|
-
const lines = await procedureGetPage(true, databaseConnection);
|
|
268
|
-
if (debug.enabled) {
|
|
269
|
-
debug(getBlock('data', lines));
|
|
220
|
+
const startTime = Date.now();
|
|
221
|
+
/** @type {TraceEntry | null} */
|
|
222
|
+
let traceData = null;
|
|
223
|
+
|
|
224
|
+
if (traceManager.isEnabled()) {
|
|
225
|
+
traceData = {
|
|
226
|
+
id: Math.random().toString(36).substring(2, 15),
|
|
227
|
+
timestamp: new Date().toISOString(),
|
|
228
|
+
source: cgiObj.REMOTE_ADDR ?? '',
|
|
229
|
+
url: req.originalUrl,
|
|
230
|
+
method: req.method,
|
|
231
|
+
status: 'pending',
|
|
232
|
+
duration: 0,
|
|
233
|
+
cgi: cgiObj,
|
|
234
|
+
headers: /** @type {Record<string, string>} */ (req.headers),
|
|
235
|
+
cookies: req.cookies,
|
|
236
|
+
uploads: filesToUpload.map((f) => ({
|
|
237
|
+
originalname: f.originalname,
|
|
238
|
+
mimetype: f.mimetype,
|
|
239
|
+
size: f.size,
|
|
240
|
+
})),
|
|
241
|
+
};
|
|
270
242
|
}
|
|
271
243
|
|
|
272
|
-
// 6) download files
|
|
273
|
-
|
|
274
|
-
debug('invokeProcedure: download files');
|
|
275
|
-
const fileBlob = await databaseConnection.createLob(oracledb.BLOB);
|
|
276
|
-
|
|
277
244
|
try {
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
245
|
+
// 1) upload files
|
|
246
|
+
debug(`invokeProcedure: upload "${filesToUpload.length}" files`);
|
|
247
|
+
if (filesToUpload.length > 0) {
|
|
248
|
+
if (typeof options.documentTable === 'string' && options.documentTable.length > 0) {
|
|
249
|
+
const {documentTable} = options;
|
|
250
|
+
|
|
251
|
+
await Promise.all(filesToUpload.map((file) => uploadFile(file, documentTable, databaseConnection)));
|
|
252
|
+
} else {
|
|
253
|
+
console.warn(`Unable to upload "${filesToUpload.length}" files because the option ""doctable" has not been defined`);
|
|
254
|
+
}
|
|
281
255
|
}
|
|
282
256
|
|
|
283
|
-
//
|
|
257
|
+
// 2) get procedure to execute and the arguments
|
|
284
258
|
|
|
285
|
-
debug('invokeProcedure:
|
|
286
|
-
|
|
259
|
+
debug('invokeProcedure: get procedure to execute and the arguments');
|
|
260
|
+
// Extract the raw procedure name from params
|
|
261
|
+
const rawProcName = Array.isArray(req.params.name) ? req.params.name[0] : req.params.name;
|
|
262
|
+
if (!rawProcName) {
|
|
263
|
+
throw new RequestError('No procedure name provided');
|
|
264
|
+
}
|
|
265
|
+
const para = await getProcedure(req, rawProcName, argObj, options, databaseConnection, procedureNameCache, argumentCache);
|
|
287
266
|
|
|
288
|
-
|
|
289
|
-
|
|
267
|
+
if (traceData) {
|
|
268
|
+
traceData.procedure = para.resolvedName;
|
|
269
|
+
traceData.parameters = para.bind;
|
|
270
|
+
}
|
|
290
271
|
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
272
|
+
// 3) prepare the session
|
|
273
|
+
|
|
274
|
+
debug('invokeProcedure: prepare the session');
|
|
275
|
+
await procedurePrepare(cgiObj, databaseConnection);
|
|
276
|
+
|
|
277
|
+
// 4) execute the procedure
|
|
278
|
+
|
|
279
|
+
debug('invokeProcedure: execute the session');
|
|
280
|
+
try {
|
|
281
|
+
await procedureExecute(para, databaseConnection);
|
|
282
|
+
} catch (err) {
|
|
283
|
+
// Invalidation Logic
|
|
284
|
+
if (err instanceof ProcedureError) {
|
|
285
|
+
const errorString = err.toString();
|
|
286
|
+
// Check for ORA-04068, ORA-04061, ORA-04065, ORA-06550
|
|
287
|
+
if (
|
|
288
|
+
errorString.includes('ORA-04068') ||
|
|
289
|
+
errorString.includes('ORA-04061') ||
|
|
290
|
+
errorString.includes('ORA-04065') ||
|
|
291
|
+
errorString.includes('ORA-06550')
|
|
292
|
+
) {
|
|
293
|
+
debug(`invokeProcedure: detected invalidation error (${errorString}). Clearing caches.`);
|
|
294
|
+
|
|
295
|
+
// Clear name resolution cache for the input name
|
|
296
|
+
if (rawProcName) {
|
|
297
|
+
procedureNameCache.delete(rawProcName);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Clear argument cache for the resolved name
|
|
301
|
+
if (para.resolvedName) {
|
|
302
|
+
argumentCache.delete(para.resolvedName.toUpperCase());
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
throw err;
|
|
296
307
|
}
|
|
297
308
|
|
|
298
|
-
//
|
|
309
|
+
// 5) get the page returned from the procedure
|
|
310
|
+
debug('invokeProcedure: get the page returned from the procedure');
|
|
299
311
|
|
|
300
|
-
|
|
301
|
-
await
|
|
302
|
-
} finally {
|
|
303
|
-
// 9) cleanup
|
|
312
|
+
const streamInstance = new OWAPageStream(databaseConnection);
|
|
313
|
+
const lines = await streamInstance.fetchChunk();
|
|
304
314
|
|
|
305
|
-
|
|
306
|
-
|
|
315
|
+
/* v8 ignore start */
|
|
316
|
+
if (debug.enabled) {
|
|
317
|
+
debug(getBlock('data', lines.join('')));
|
|
318
|
+
}
|
|
319
|
+
/* v8 ignore stop */
|
|
320
|
+
|
|
321
|
+
// 6) download files
|
|
322
|
+
|
|
323
|
+
debug('invokeProcedure: download files');
|
|
324
|
+
const fileBlob = await databaseConnection.createLob(oracledb.BLOB);
|
|
325
|
+
|
|
326
|
+
try {
|
|
327
|
+
const fileDownload = await procedureDownloadFiles(fileBlob, databaseConnection);
|
|
328
|
+
/* v8 ignore start */
|
|
329
|
+
if (debug.enabled) {
|
|
330
|
+
debug(getBlock('fileDownload', inspect({fileType: fileDownload.fileType, fileSize: fileDownload.fileSize})));
|
|
331
|
+
}
|
|
332
|
+
/* v8 ignore stop */
|
|
333
|
+
|
|
334
|
+
// 7) parse the page
|
|
335
|
+
|
|
336
|
+
debug('invokeProcedure: parse the page');
|
|
337
|
+
// We parse the headers from the first chunk
|
|
338
|
+
const pageComponents = parsePage(lines.join(''));
|
|
339
|
+
|
|
340
|
+
// add "Server" header
|
|
341
|
+
pageComponents.head.server = cgiObj.SERVER_SOFTWARE ?? '';
|
|
342
|
+
|
|
343
|
+
// add file download information
|
|
344
|
+
if (fileDownload.fileType !== '' && fileDownload.fileSize > 0 && fileDownload.fileBlob !== null) {
|
|
345
|
+
pageComponents.file.fileType = fileDownload.fileType;
|
|
346
|
+
pageComponents.file.fileSize = fileDownload.fileSize;
|
|
347
|
+
pageComponents.file.fileBlob = fileDownload.fileBlob;
|
|
348
|
+
|
|
349
|
+
if (traceData) {
|
|
350
|
+
traceData.downloads = {
|
|
351
|
+
fileType: fileDownload.fileType,
|
|
352
|
+
fileSize: fileDownload.fileSize,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
} else {
|
|
356
|
+
// For normal pages, we use the stream.
|
|
357
|
+
// Prepend the initial body (parsed by parsePage) to the stream
|
|
358
|
+
if (typeof pageComponents.body === 'string' && pageComponents.body.length > 0) {
|
|
359
|
+
streamInstance.addBody(pageComponents.body);
|
|
360
|
+
}
|
|
361
|
+
// Use the stream as the body
|
|
362
|
+
pageComponents.body = streamInstance;
|
|
363
|
+
|
|
364
|
+
if (traceData) {
|
|
365
|
+
// Buffer HTML if tracing enabled
|
|
366
|
+
let htmlBuffer = typeof pageComponents.body === 'string' ? pageComponents.body : lines.join('');
|
|
367
|
+
const MAX_HTML_SIZE = 1024 * 1024; // 1MB
|
|
368
|
+
|
|
369
|
+
const originalPush = streamInstance.push.bind(streamInstance);
|
|
370
|
+
streamInstance.push = (chunk) => {
|
|
371
|
+
if (chunk !== null && htmlBuffer.length < MAX_HTML_SIZE) {
|
|
372
|
+
const str = String(chunk);
|
|
373
|
+
htmlBuffer += str;
|
|
374
|
+
if (htmlBuffer.length > MAX_HTML_SIZE) {
|
|
375
|
+
htmlBuffer = htmlBuffer.substring(0, MAX_HTML_SIZE) + '... [truncated]';
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return originalPush(chunk);
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Since we can't easily wait for the stream to finish here without blocking,
|
|
382
|
+
// we'll have to finalize traceData when the stream ends.
|
|
383
|
+
const currentTrace = traceData;
|
|
384
|
+
streamInstance.on('end', () => {
|
|
385
|
+
currentTrace.html = htmlBuffer;
|
|
386
|
+
currentTrace.status = 'success';
|
|
387
|
+
currentTrace.duration = Date.now() - startTime;
|
|
388
|
+
traceManager.addTrace(currentTrace);
|
|
389
|
+
});
|
|
390
|
+
streamInstance.on('error', (err) => {
|
|
391
|
+
currentTrace.status = 'fail';
|
|
392
|
+
currentTrace.error = err instanceof Error ? err.message : String(err);
|
|
393
|
+
currentTrace.duration = Date.now() - startTime;
|
|
394
|
+
traceManager.addTrace(currentTrace);
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// 8) send the page to browser
|
|
400
|
+
|
|
401
|
+
debug('invokeProcedure: send the page to browser');
|
|
402
|
+
await sendResponse(req, res, pageComponents);
|
|
403
|
+
|
|
404
|
+
if (traceData?.downloads) {
|
|
405
|
+
traceData.status = 'success';
|
|
406
|
+
traceData.duration = Date.now() - startTime;
|
|
407
|
+
traceManager.addTrace(traceData);
|
|
408
|
+
}
|
|
409
|
+
} finally {
|
|
410
|
+
// 9) cleanup
|
|
411
|
+
|
|
412
|
+
debug('invokeProcedure: cleanup');
|
|
413
|
+
fileBlob.destroy();
|
|
414
|
+
}
|
|
415
|
+
} catch (err) {
|
|
416
|
+
if (traceData) {
|
|
417
|
+
traceData.status = err instanceof ProcedureError ? 'error' : 'fail';
|
|
418
|
+
traceData.error = err instanceof Error ? err.message : String(err);
|
|
419
|
+
traceData.duration = Date.now() - startTime;
|
|
420
|
+
traceManager.addTrace(traceData);
|
|
421
|
+
}
|
|
422
|
+
throw err;
|
|
307
423
|
}
|
|
308
424
|
|
|
309
425
|
debug('invokeProcedure: end');
|