UNPKG

web_plsql

Version:

The Express Middleware for Oracle PL/SQL

261 lines (219 loc) 8.57 kB
/* * 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); };