web_plsql 1.0.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 +5 -5
- package/package.json +5 -5
- package/src/admin/client/charts.ts +303 -4
- package/src/admin/index.html +283 -69
- package/src/admin/js/api.ts +95 -20
- package/src/admin/js/app.ts +460 -152
- package/src/admin/js/schemas.ts +76 -14
- package/src/admin/js/templates/config.ts +10 -9
- package/src/admin/js/templates/errorRow.ts +8 -5
- package/src/admin/js/templates/index.ts +1 -0
- package/src/admin/js/templates/poolCard.ts +6 -6
- package/src/admin/js/templates/traceRow.ts +24 -0
- package/src/admin/js/types.ts +132 -19
- package/src/admin/js/ui/components.ts +44 -0
- package/src/admin/js/ui/table.ts +173 -0
- package/src/admin/js/ui/views.ts +311 -48
- package/src/admin/js/util/format.ts +22 -3
- 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 +54 -47
- package/src/admin/style.css +143 -27
- package/src/handler/handlerAdmin.js +121 -26
- package/src/handler/plsql/errorPage.js +0 -4
- package/src/handler/plsql/owaPageStream.js +93 -0
- package/src/handler/plsql/procedure.js +191 -121
- package/src/handler/plsql/procedureNamed.js +17 -3
- package/src/handler/plsql/procedureSanitize.js +24 -0
- package/src/handler/plsql/procedureVariable.js +2 -0
- package/src/handler/plsql/sendResponse.js +41 -3
- package/src/index.js +0 -1
- package/src/server/adminContext.js +23 -0
- package/src/server/server.js +83 -68
- package/src/types.js +1 -1
- package/src/util/statsManager.js +368 -0
- package/src/util/trace.js +2 -1
- 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 +70 -0
- package/types/handler/plsql/owaPageStream.d.ts +28 -0
- package/types/handler/plsql/procedure.d.ts +1 -0
- package/types/index.d.ts +0 -1
- package/types/server/adminContext.d.ts +17 -0
- package/types/server/server.d.ts +2 -19
- package/types/types.d.ts +1 -1
- package/types/util/statsManager.d.ts +395 -0
- package/types/util/traceManager.d.ts +35 -0
- package/src/admin/lib/assets/main-zpdhQ1gD.css +0 -1
- package/src/handler/handlerMetrics.js +0 -68
- package/types/handler/handlerMetrics.d.ts +0 -25
|
@@ -19,6 +19,12 @@ 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';
|
|
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
|
+
*/
|
|
22
28
|
|
|
23
29
|
/**
|
|
24
30
|
* @typedef {import('express').Request} Request
|
|
@@ -48,7 +54,9 @@ import {sanitizeProcName} from './procedureSanitize.js';
|
|
|
48
54
|
const getProcedure = async (req, procName, argObj, options, databaseConnection, procedureNameCache, argumentCache) => {
|
|
49
55
|
// path alias
|
|
50
56
|
if (options.pathAlias?.toLowerCase() === procName.toLowerCase()) {
|
|
57
|
+
/* v8 ignore start */
|
|
51
58
|
debug(`getProcedure: path alias "${options.pathAlias}" redirects to "${options.pathAliasProcedure}"`);
|
|
59
|
+
/* v8 ignore stop */
|
|
52
60
|
return {
|
|
53
61
|
sql: `${options.pathAliasProcedure}(p_path=>:p_path)`,
|
|
54
62
|
bind: {
|
|
@@ -131,47 +139,6 @@ const procedureExecute = async (para, databaseConnection) => {
|
|
|
131
139
|
}
|
|
132
140
|
};
|
|
133
141
|
|
|
134
|
-
/**
|
|
135
|
-
* Get page from procedure
|
|
136
|
-
*
|
|
137
|
-
* @param {boolean} _test - Test.
|
|
138
|
-
* @param {Connection} databaseConnection - Database connection.
|
|
139
|
-
* @returns {Promise<string>} Promise resolving to the returned page content.
|
|
140
|
-
*/
|
|
141
|
-
const procedureGetPage = async (_test, databaseConnection) => {
|
|
142
|
-
const MAX_IROWS = 100000;
|
|
143
|
-
|
|
144
|
-
/** @type {BindParameterConfig} */
|
|
145
|
-
const bindParameter = {
|
|
146
|
-
lines: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxArraySize: MAX_IROWS},
|
|
147
|
-
irows: {dir: oracledb.BIND_INOUT, type: oracledb.NUMBER, val: MAX_IROWS},
|
|
148
|
-
};
|
|
149
|
-
|
|
150
|
-
const sqlStatement = 'BEGIN owa.get_page(thepage=>:lines, irows=>:irows); END;';
|
|
151
|
-
|
|
152
|
-
/** @type {Result} */
|
|
153
|
-
let result = {};
|
|
154
|
-
try {
|
|
155
|
-
result = await databaseConnection.execute(sqlStatement, bindParameter);
|
|
156
|
-
} catch (err) {
|
|
157
|
-
if (debug.enabled) {
|
|
158
|
-
debug(getBlock('procedureGetPage: results', inspect(result)));
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
throw new ProcedureError(`procedureGetPage: error when getting page returned by procedure\n${errorToString(err)}`, {}, sqlStatement, bindParameter);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const {lines, irows} = z.object({irows: z.number(), lines: z.array(z.string())}).parse(result.outBinds);
|
|
165
|
-
|
|
166
|
-
// Make sure that we have retrieved all the rows
|
|
167
|
-
if (irows > MAX_IROWS) {
|
|
168
|
-
/* v8 ignore next - defensive check for row limit */
|
|
169
|
-
throw new ProcedureError(`procedureGetPage: error when retrieving rows. irows="${irows}"`, {}, sqlStatement, bindParameter);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return lines.join('');
|
|
173
|
-
};
|
|
174
|
-
|
|
175
142
|
/**
|
|
176
143
|
* Download files from procedure
|
|
177
144
|
*
|
|
@@ -209,9 +176,11 @@ END;
|
|
|
209
176
|
try {
|
|
210
177
|
result = await databaseConnection.execute(sqlStatement, bindParameter);
|
|
211
178
|
} catch (err) {
|
|
179
|
+
/* v8 ignore start */
|
|
212
180
|
if (debug.enabled) {
|
|
213
181
|
debug(getBlock('procedureDownloadFiles: results', inspect(result)));
|
|
214
182
|
}
|
|
183
|
+
/* v8 ignore stop */
|
|
215
184
|
|
|
216
185
|
throw new ProcedureError(`procedureDownloadFiles: error when downloading files\n${errorToString(err)}`, {}, sqlStatement, bindParameter);
|
|
217
186
|
}
|
|
@@ -248,108 +217,209 @@ END;
|
|
|
248
217
|
export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options, databaseConnection, procedureNameCache, argumentCache) => {
|
|
249
218
|
debug('invokeProcedure: begin');
|
|
250
219
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
+
};
|
|
242
|
+
}
|
|
256
243
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
244
|
+
try {
|
|
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
|
+
}
|
|
260
255
|
}
|
|
261
|
-
}
|
|
262
256
|
|
|
263
|
-
|
|
257
|
+
// 2) get procedure to execute and the arguments
|
|
264
258
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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);
|
|
272
266
|
|
|
273
|
-
|
|
267
|
+
if (traceData) {
|
|
268
|
+
traceData.procedure = para.resolvedName;
|
|
269
|
+
traceData.parameters = para.bind;
|
|
270
|
+
}
|
|
274
271
|
|
|
275
|
-
|
|
276
|
-
|
|
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;
|
|
307
|
+
}
|
|
277
308
|
|
|
278
|
-
|
|
309
|
+
// 5) get the page returned from the procedure
|
|
310
|
+
debug('invokeProcedure: get the page returned from the procedure');
|
|
279
311
|
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
}
|
|
312
|
+
const streamInstance = new OWAPageStream(databaseConnection);
|
|
313
|
+
const lines = await streamInstance.fetchChunk();
|
|
300
314
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
}
|
|
315
|
+
/* v8 ignore start */
|
|
316
|
+
if (debug.enabled) {
|
|
317
|
+
debug(getBlock('data', lines.join('')));
|
|
306
318
|
}
|
|
307
|
-
|
|
308
|
-
}
|
|
319
|
+
/* v8 ignore stop */
|
|
309
320
|
|
|
310
|
-
|
|
321
|
+
// 6) download files
|
|
311
322
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
if (debug.enabled) {
|
|
315
|
-
debug(getBlock('data', lines));
|
|
316
|
-
}
|
|
323
|
+
debug('invokeProcedure: download files');
|
|
324
|
+
const fileBlob = await databaseConnection.createLob(oracledb.BLOB);
|
|
317
325
|
|
|
318
|
-
|
|
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 */
|
|
319
333
|
|
|
320
|
-
|
|
321
|
-
const fileBlob = await databaseConnection.createLob(oracledb.BLOB);
|
|
334
|
+
// 7) parse the page
|
|
322
335
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
debug(getBlock('fileDownload', inspect({fileType: fileDownload.fileType, fileSize: fileDownload.fileSize})));
|
|
327
|
-
}
|
|
336
|
+
debug('invokeProcedure: parse the page');
|
|
337
|
+
// We parse the headers from the first chunk
|
|
338
|
+
const pageComponents = parsePage(lines.join(''));
|
|
328
339
|
|
|
329
|
-
|
|
340
|
+
// add "Server" header
|
|
341
|
+
pageComponents.head.server = cgiObj.SERVER_SOFTWARE ?? '';
|
|
330
342
|
|
|
331
|
-
|
|
332
|
-
|
|
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;
|
|
333
348
|
|
|
334
|
-
|
|
335
|
-
|
|
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
|
+
}
|
|
336
398
|
|
|
337
|
-
|
|
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
|
-
}
|
|
399
|
+
// 8) send the page to browser
|
|
343
400
|
|
|
344
|
-
|
|
401
|
+
debug('invokeProcedure: send the page to browser');
|
|
402
|
+
await sendResponse(req, res, pageComponents);
|
|
345
403
|
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
404
|
+
if (traceData?.downloads) {
|
|
405
|
+
traceData.status = 'success';
|
|
406
|
+
traceData.duration = Date.now() - startTime;
|
|
407
|
+
traceManager.addTrace(traceData);
|
|
408
|
+
}
|
|
409
|
+
} finally {
|
|
410
|
+
// 9) cleanup
|
|
350
411
|
|
|
351
|
-
|
|
352
|
-
|
|
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;
|
|
353
423
|
}
|
|
354
424
|
|
|
355
425
|
debug('invokeProcedure: end');
|
|
@@ -83,22 +83,28 @@ const loadArguments = async (procedure, databaseConnection) => {
|
|
|
83
83
|
try {
|
|
84
84
|
result = await databaseConnection.execute(SQL_GET_ARGUMENT, bind);
|
|
85
85
|
} catch (err) {
|
|
86
|
+
/* v8 ignore start */
|
|
86
87
|
debug('result', result);
|
|
88
|
+
/* v8 ignore stop */
|
|
89
|
+
|
|
87
90
|
const message = `Error when retrieving arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`;
|
|
88
91
|
throw new RequestError(message);
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
/** @type {{names: string[], types: string[]}} */
|
|
94
|
+
/** @type {{names: (string | null)[], types: (string | null)[]}} */
|
|
92
95
|
let data;
|
|
93
96
|
try {
|
|
94
97
|
data = z
|
|
95
98
|
.object({
|
|
96
|
-
names: z.array(z.string()),
|
|
97
|
-
types: z.array(z.string()),
|
|
99
|
+
names: z.array(z.string().nullable()),
|
|
100
|
+
types: z.array(z.string().nullable()),
|
|
98
101
|
})
|
|
99
102
|
.parse(result.outBinds);
|
|
100
103
|
} catch (err) {
|
|
104
|
+
/* v8 ignore start */
|
|
101
105
|
debug('result.outBinds', result.outBinds);
|
|
106
|
+
/* v8 ignore stop */
|
|
107
|
+
|
|
102
108
|
const message = `Error when decoding arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`;
|
|
103
109
|
throw new RequestError(message);
|
|
104
110
|
}
|
|
@@ -136,16 +142,22 @@ const findArguments = async (procedure, databaseConnection, argumentCache) => {
|
|
|
136
142
|
|
|
137
143
|
// if we found the procedure in the cache, we return it
|
|
138
144
|
if (cachedArgs) {
|
|
145
|
+
/* v8 ignore start */
|
|
139
146
|
if (debug.enabled) {
|
|
140
147
|
debug(`findArguments: procedure "${procedure}" found in cache`);
|
|
141
148
|
}
|
|
149
|
+
/* v8 ignore stop */
|
|
150
|
+
|
|
142
151
|
return cachedArgs;
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
// load from database
|
|
155
|
+
/* v8 ignore start */
|
|
146
156
|
if (debug.enabled) {
|
|
147
157
|
debug(`findArguments: procedure "${procedure}" not found in cache and must be loaded`);
|
|
148
158
|
}
|
|
159
|
+
/* v8 ignore stop */
|
|
160
|
+
|
|
149
161
|
const args = await loadArguments(procedure, databaseConnection);
|
|
150
162
|
|
|
151
163
|
// add to the cache
|
|
@@ -259,10 +271,12 @@ export const getProcedureNamed = async (req, procName, argObj, databaseConnectio
|
|
|
259
271
|
// select statement
|
|
260
272
|
const sql = `${procName}(${sqlParameter.join(', ')})`;
|
|
261
273
|
|
|
274
|
+
/* v8 ignore start */
|
|
262
275
|
if (debug.enabled) {
|
|
263
276
|
debug(sql);
|
|
264
277
|
debug(inspectBindings(argObj, argTypes));
|
|
265
278
|
}
|
|
279
|
+
/* v8 ignore stop */
|
|
266
280
|
|
|
267
281
|
return {sql, bind: bindings};
|
|
268
282
|
};
|
|
@@ -95,7 +95,10 @@ const resolveProcedureName = async (procName, databaseConnection, procedureNameC
|
|
|
95
95
|
|
|
96
96
|
return resolved;
|
|
97
97
|
} catch (err) {
|
|
98
|
+
/* v8 ignore start */
|
|
98
99
|
debug(`resolveProcedureName: Error resolving "${procName}"`, err);
|
|
100
|
+
/* v8 ignore stop */
|
|
101
|
+
|
|
99
102
|
// Rethrow as RequestError to indicate 404/403
|
|
100
103
|
throw new RequestError(`Procedure "${procName}" not found or not accessible.\n${errorToString(err)}`);
|
|
101
104
|
}
|
|
@@ -123,7 +126,10 @@ export const sanitizeProcName = async (procName, databaseConnection, options, pr
|
|
|
123
126
|
for (const i of DEFAULT_EXCLUSION_LIST) {
|
|
124
127
|
if (finalProcName.startsWith(i)) {
|
|
125
128
|
const error = `Procedure name "${procName}" is in default exclusion list "${DEFAULT_EXCLUSION_LIST.join(',')}"`;
|
|
129
|
+
/* v8 ignore start */
|
|
126
130
|
debug(error);
|
|
131
|
+
/* v8 ignore stop */
|
|
132
|
+
|
|
127
133
|
throw new RequestError(error);
|
|
128
134
|
}
|
|
129
135
|
}
|
|
@@ -133,7 +139,10 @@ export const sanitizeProcName = async (procName, databaseConnection, options, pr
|
|
|
133
139
|
for (const i of options.exclusionList) {
|
|
134
140
|
if (finalProcName.startsWith(i)) {
|
|
135
141
|
const error = `Procedure name "${procName}" is in custom exclusion list "${options.exclusionList.join(',')}"`;
|
|
142
|
+
/* v8 ignore start */
|
|
136
143
|
debug(error);
|
|
144
|
+
/* v8 ignore stop */
|
|
145
|
+
|
|
137
146
|
throw new RequestError(error);
|
|
138
147
|
}
|
|
139
148
|
}
|
|
@@ -148,7 +157,10 @@ export const sanitizeProcName = async (procName, databaseConnection, options, pr
|
|
|
148
157
|
const valid = await requestValidationFunction(finalProcName, options.requestValidationFunction, databaseConnection);
|
|
149
158
|
if (!valid) {
|
|
150
159
|
const error = `Procedure name "${procName}" is not valid according to the request validation function "${options.requestValidationFunction}"`;
|
|
160
|
+
/* v8 ignore start */
|
|
151
161
|
debug(error);
|
|
162
|
+
/* v8 ignore stop */
|
|
163
|
+
|
|
152
164
|
throw new RequestError(error);
|
|
153
165
|
}
|
|
154
166
|
}
|
|
@@ -209,7 +221,10 @@ const loadRequestValid = async (procName, requestValidationFunction, databaseCon
|
|
|
209
221
|
try {
|
|
210
222
|
result = await databaseConnection.execute(SQL, bind);
|
|
211
223
|
} catch (err) {
|
|
224
|
+
/* v8 ignore start */
|
|
212
225
|
debug('result', result);
|
|
226
|
+
/* v8 ignore stop */
|
|
227
|
+
|
|
213
228
|
const message = `Error when validating procedure name "${procName}"\n${SQL}\n${errorToString(err)}`;
|
|
214
229
|
throw new RequestError(message);
|
|
215
230
|
}
|
|
@@ -218,7 +233,10 @@ const loadRequestValid = async (procName, requestValidationFunction, databaseCon
|
|
|
218
233
|
const data = z.strictObject({valid: z.number()}).parse(result.outBinds);
|
|
219
234
|
return data.valid === 1;
|
|
220
235
|
} catch (err) {
|
|
236
|
+
/* v8 ignore start */
|
|
221
237
|
debug('result', result.outBinds);
|
|
238
|
+
/* v8 ignore stop */
|
|
239
|
+
|
|
222
240
|
const message = `Internal error when parsing ${result.outBinds}\n${errorToString(err)}`;
|
|
223
241
|
throw new Error(message);
|
|
224
242
|
}
|
|
@@ -243,16 +261,22 @@ const requestValidationFunction = async (procName, requestValidationFunction, da
|
|
|
243
261
|
|
|
244
262
|
// if we found the procedure in the cache, we return it (hitCount already incremented by get)
|
|
245
263
|
if (cacheEntry !== undefined) {
|
|
264
|
+
/* v8 ignore start */
|
|
246
265
|
if (debug.enabled) {
|
|
247
266
|
debug(`requestValidationFunction: procedure "${procName}" found in cache`);
|
|
248
267
|
}
|
|
268
|
+
/* v8 ignore stop */
|
|
269
|
+
|
|
249
270
|
return cacheEntry.valid;
|
|
250
271
|
}
|
|
251
272
|
|
|
252
273
|
// load from database
|
|
274
|
+
/* v8 ignore start */
|
|
253
275
|
if (debug.enabled) {
|
|
254
276
|
debug(`requestValidationFunction: procedure "${procName}" not found in cache and must be loaded`);
|
|
255
277
|
}
|
|
278
|
+
/* v8 ignore stop */
|
|
279
|
+
|
|
256
280
|
const valid = await loadRequestValid(procName, requestValidationFunction, databaseConnection);
|
|
257
281
|
|
|
258
282
|
// add to the cache
|
|
@@ -24,9 +24,11 @@ import oracledb from 'oracledb';
|
|
|
24
24
|
* @returns {{sql: string; bind: BindParameterConfig}} - The SQL statement and bindings for the procedure to execute
|
|
25
25
|
*/
|
|
26
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 = [];
|
|
@@ -29,19 +29,23 @@ export const sendResponse = async (_req, res, page) => {
|
|
|
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';
|