web_plsql
Version:
The Express Middleware for Oracle PL/SQL
261 lines (219 loc) • 8.57 kB
JavaScript
/*
* Invoke the Oracle procedure and return the raw content of the page
*/
import debugModule from 'debug';
const debug = debugModule('webplsql:procedure');
import oracledb from 'oracledb';
import stream from 'node:stream';
import z from 'zod';
import {streamToBuffer} from './stream.js';
import {uploadFile} from './upload.js';
import {getProcedureVariable} from './procedureVariable.js';
import {getProcedureNamed} from './procedureNamed.js';
import {parsePage} from './parsePage.js';
import {sendResponse} from './sendResponse.js';
import {ProcedureError} from './procedureError.js';
import {inspect, getBlock} from './trace.js';
import {errorToString} from './error.js';
/**
* @typedef {import('express').Request} Request
* @typedef {import('express').Response} Response
* @typedef {import('oracledb').Connection} Connection
* @typedef {import('oracledb').Result<unknown>} Result
* @typedef {import('./types.js').argObjType} argObjType
* @typedef {import('./types.js').fileUploadType} fileUploadType
* @typedef {import('./types.js').environmentType} environmentType
* @typedef {import('./types.js').configPlSqlHandlerType} configPlSqlHandlerType
* @typedef {import('./types.js').BindParameterConfig} BindParameterConfig
*/
/**
* Get the SQL statement to execute when a new procedure is invoked
* @param {string} procedure - The procedure
* @returns {string} - The SQL statement to execute
*/
const getProcedureSQL = (procedure) => `DECLARE
fileType VARCHAR2(32767);
fileSize INTEGER;
fileBlob BLOB;
fileExist INTEGER := 0;
BEGIN
-- Ensure a stateless environment by resetting package state (dbms_session.reset_package)
dbms_session.modify_package_state(dbms_session.reinitialize);
-- initialize the cgi
owa.init_cgi_env(:cgicount, :cginames, :cgivalues);
-- initialize the htp package
htp.init;
-- set the HTBUF_LEN
htp.HTBUF_LEN := :htbuflen;
-- execute the procedure
BEGIN
${procedure}
EXCEPTION WHEN OTHERS THEN
raise_application_error(-20000, 'Error executing ${procedure}'||CHR(10)||SUBSTR(dbms_utility.format_error_stack()||CHR(10)||dbms_utility.format_error_backtrace(), 1, 2000));
END;
-- Check for file download
IF (wpg_docload.is_file_download()) THEN
wpg_docload.get_download_file(fileType);
IF (filetype = 'B') THEN
fileExist := 1;
wpg_docload.get_download_blob(:fileBlob);
fileSize := dbms_lob.getlength(:fileBlob);
--dbms_lob.copy(dest_lob=>:fileBlob, src_lob=>fileBlob, amount=>fileSize);
END IF;
END IF;
:fileExist := fileExist;
:fileType := fileType;
:fileSize := fileSize;
-- retrieve the page
owa.get_page(thepage=>:lines, irows=>:irows);
END;`;
/**
* Invoke the Oracle procedure and return the page content
*
* @param {Request} req - The req object represents the HTTP request.
* @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
* @param {argObjType} argObj - - The arguments of the procedure to invoke.
* @param {environmentType} cgiObj - The cgi of the procedure to invoke.
* @param {fileUploadType[]} filesToUpload - Array of files to be uploaded
* @param {configPlSqlHandlerType} options - the options for the middleware.
* @param {Connection} databaseConnection - Database connection.
* @returns {Promise<void>} Promise resolving to the page content generated by the executed procedure
*/
export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options, databaseConnection) => {
debug('invokeProcedure: ENTER');
const procName = req.params.name;
//
// 1) UPLOAD FILES
//
debug(`invokeProcedure: upload "${filesToUpload.length}" files`);
if (filesToUpload.length > 0) {
if (typeof options.documentTable === 'string' && options.documentTable.length > 0) {
const {documentTable} = options;
await Promise.all(filesToUpload.map((file) => uploadFile(file, documentTable, databaseConnection)));
} else {
console.warn(`Unable to upload "${filesToUpload.length}" files because the option ""doctable" has not been defined`);
}
}
//
// 2) GET SQL STATEMENT AND ARGUMENTS
//
const para = await getProcedure(procName, argObj, options, databaseConnection);
//
// 3) EXECUTE PROCEDURE
//
const HTBUF_LEN = 63;
const MAX_IROWS = 100000;
const cgi = {
keys: Object.keys(cgiObj),
values: Object.values(cgiObj),
};
const fileBlob = await databaseConnection.createLob(oracledb.BLOB);
/** @type {BindParameterConfig} */
const bind = {
cgicount: {dir: oracledb.BIND_IN, type: oracledb.NUMBER, val: cgi.keys.length},
cginames: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: cgi.keys},
cgivalues: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: cgi.values},
htbuflen: {dir: oracledb.BIND_IN, type: oracledb.NUMBER, val: HTBUF_LEN},
fileExist: {dir: oracledb.BIND_OUT, type: oracledb.NUMBER},
fileType: {dir: oracledb.BIND_OUT, type: oracledb.STRING},
fileSize: {dir: oracledb.BIND_OUT, type: oracledb.NUMBER},
fileBlob: {dir: oracledb.BIND_INOUT, type: oracledb.BLOB, val: fileBlob},
lines: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxSize: HTBUF_LEN * 2, maxArraySize: MAX_IROWS},
irows: {dir: oracledb.BIND_INOUT, type: oracledb.NUMBER, val: MAX_IROWS},
};
// execute procedure and retrieve page
const sqlStatement = getProcedureSQL(para.sql);
/** @type {Result | null} */
let result = null;
const bindParams = Object.assign({}, bind, para.bind);
try {
if (debug.enabled) {
if (debug.enabled) {
debug(getBlock('execute', sqlStatement));
// NOTE: Because inspecting a BLOB value generates a craxy amount of text, we somply remove it.
const temp = Object.assign({}, bindParams);
delete temp.fileBlob.val;
debug(getBlock('bindParams', inspect(temp)));
}
}
result = await databaseConnection.execute(sqlStatement, bindParams);
} catch (err) {
if (debug.enabled) {
debug(getBlock('results', inspect(result)));
}
throw new ProcedureError(`Error when executing procedure\n${sqlStatement}\n${errorToString(err)}`, cgiObj, para.sql, para.bind);
}
//
// 4) PROCESS RESULTS
//
// validate results
const data = z
.object({
irows: z.number(),
lines: z.array(z.string()),
fileExist: z.number(),
fileType: z.string().nullable(),
fileSize: z.number().nullable(),
fileBlob: z.instanceof(stream.Readable).nullable(),
})
.parse(result.outBinds);
if (debug.enabled) {
debug(getBlock('data', inspect(data)));
}
// Make sure that we have retrieved all the rows
if (data.irows > MAX_IROWS) {
/* istanbul ignore next */
throw new ProcedureError(`Error when retrieving rows. irows="${data.irows}"`, cgiObj, para.sql, para.bind);
}
// combine page
const pageContent = data.lines.join('');
if (debug.enabled && pageContent.length > 0) {
debug(getBlock('PLAIN CONTENT', pageContent));
}
//
// 6) PARSE PAGE
//
// parse what we received from PL/SQL
const pageComponents = parsePage(pageContent);
// add "Server" header
pageComponents.head.server = cgiObj.SERVER_SOFTWARE;
// add file download information
if (data.fileExist === 1) {
pageComponents.file.fileType = data.fileType;
pageComponents.file.fileSize = data.fileSize;
if (data.fileBlob) {
pageComponents.file.fileBlob = await streamToBuffer(data.fileBlob);
}
}
//
// 5) SEND THE RESPONSE
//
sendResponse(req, res, pageComponents);
//
// 6) CLEANUP
//
fileBlob.destroy();
debug('invokeProcedure: EXIT');
};
/**
* Get the procedure and arguments to execute
* @param {string} procName - The procedure to execute
* @param {argObjType} argObj - The arguments to pass to the procedure
* @param {configPlSqlHandlerType} options - The options for the middleware
* @param {Connection} databaseConnection - The database connection
* @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
*/
const getProcedure = async (procName, argObj, options, databaseConnection) => {
if (options.pathAlias && options.pathAlias.toLowerCase() === procName.toLowerCase()) {
debug(`getProcedure: path alias "${options.pathAlias}" redirects to "${options.pathAliasProcedure}"`);
return {
sql: `${options.pathAliasProcedure}(p_path=>:p_path);`,
bind: {
p_path: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: procName},
},
};
} else if (procName.startsWith('!')) {
return await getProcedureVariable(procName.substring(1), argObj, databaseConnection, options);
}
return await getProcedureNamed(procName, argObj, databaseConnection, options);
};