web_plsql 0.5.0 → 0.6.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 +123 -110
- package/examples/server_apex.js +28 -0
- package/examples/server_sample.js +30 -0
- package/examples/sql/{doc_table.sql → doctable.sql} +1 -1
- package/examples/sql/install.sql +6 -9
- package/examples/sql/sample_package.sql +27 -0
- package/examples/sql/sample_package_body.sql +245 -0
- package/examples/static/sample.css +11 -10
- package/package.json +90 -83
- package/src/cgi.js +127 -0
- package/src/error.js +24 -0
- package/src/errorPage.js +296 -0
- package/src/file.js +77 -0
- package/src/handlerLogger.js +21 -0
- package/src/handlerMetrics.js +46 -0
- package/src/handlerPlSql.js +65 -0
- package/src/handlerUpload.js +32 -0
- package/src/index.js +17 -0
- package/src/oracle.js +80 -0
- package/src/parsePage.js +193 -0
- package/src/procedure.js +260 -0
- package/src/procedureError.js +40 -0
- package/src/procedureNamed.js +233 -0
- package/src/procedureSanitize.js +215 -0
- package/src/procedureVariable.js +57 -0
- package/src/request.js +103 -0
- package/src/{requestError.ts → requestError.js} +8 -4
- package/src/sendResponse.js +104 -0
- package/src/server.js +106 -0
- package/src/shutdown.js +53 -0
- package/src/stream.js +28 -0
- package/src/trace.js +74 -0
- package/src/tty.js +48 -0
- package/src/types.js +146 -0
- package/src/upload.js +92 -0
- package/src/version.js +23 -0
- package/types/cgi.d.ts +4 -0
- package/types/error.d.ts +1 -0
- package/types/errorPage.d.ts +10 -0
- package/types/file.d.ts +5 -0
- package/types/handlerLogger.d.ts +2 -0
- package/types/handlerMetrics.d.ts +4 -0
- package/types/handlerPlSql.d.ts +8 -0
- package/types/handlerUpload.d.ts +7 -0
- package/types/index.d.ts +10 -0
- package/types/oracle.d.ts +5 -0
- package/types/parsePage.d.ts +3 -0
- package/types/procedure.d.ts +10 -0
- package/types/procedureError.d.ts +23 -0
- package/types/procedureNamed.d.ts +14 -0
- package/types/procedureSanitize.d.ts +14 -0
- package/types/procedureVariable.d.ts +9 -0
- package/types/request.d.ts +7 -0
- package/types/requestError.d.ts +8 -0
- package/types/sendResponse.d.ts +4 -0
- package/types/server.d.ts +7 -0
- package/types/shutdown.d.ts +2 -0
- package/types/stream.d.ts +1 -0
- package/types/trace.d.ts +5 -0
- package/types/tty.d.ts +4 -0
- package/types/types.d.ts +251 -0
- package/types/upload.d.ts +5 -0
- package/types/version.d.ts +7 -0
- package/.editorconfig +0 -8
- package/.eslintignore +0 -3
- package/.eslintrc.js +0 -347
- package/CHANGELOG.md +0 -127
- package/examples/apex.js +0 -70
- package/examples/credentials.js +0 -22
- package/examples/oracledb_example.js +0 -30
- package/examples/sample.js +0 -101
- package/examples/sql/sample.pkb +0 -223
- package/examples/sql/sample.pks +0 -24
- package/jest.config.js +0 -204
- package/src/cgi.ts +0 -95
- package/src/config.ts +0 -97
- package/src/errorPage.ts +0 -286
- package/src/fileUpload.ts +0 -126
- package/src/index.ts +0 -65
- package/src/page.ts +0 -275
- package/src/procedure.ts +0 -360
- package/src/procedureError.ts +0 -27
- package/src/request.ts +0 -139
- package/src/stream.ts +0 -26
- package/src/trace.ts +0 -194
- package/test/.eslintrc.json +0 -5
- package/test/__tests__/cgi.ts +0 -96
- package/test/__tests__/config.ts +0 -41
- package/test/__tests__/errorPage.ts +0 -101
- package/test/__tests__/oracledb_mock.ts +0 -98
- package/test/__tests__/server.ts +0 -495
- package/test/__tests__/stream.ts +0 -21
- package/test/mock/oracledb.ts +0 -85
- package/test/static/static.html +0 -1
- package/tsconfig.json +0 -24
- package/tsconfig.src.json +0 -23
package/src/procedure.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Invoke the Oracle procedure and return the raw content of the page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import debugModule from 'debug';
|
|
6
|
+
const debug = debugModule('webplsql:procedure');
|
|
7
|
+
|
|
8
|
+
import oracledb from 'oracledb';
|
|
9
|
+
import stream from 'node:stream';
|
|
10
|
+
import z from 'zod';
|
|
11
|
+
|
|
12
|
+
import {streamToBuffer} from './stream.js';
|
|
13
|
+
import {uploadFile} from './upload.js';
|
|
14
|
+
import {getProcedureVariable} from './procedureVariable.js';
|
|
15
|
+
import {getProcedureNamed} from './procedureNamed.js';
|
|
16
|
+
import {parsePage} from './parsePage.js';
|
|
17
|
+
import {sendResponse} from './sendResponse.js';
|
|
18
|
+
import {ProcedureError} from './procedureError.js';
|
|
19
|
+
import {inspect, getBlock} from './trace.js';
|
|
20
|
+
import {errorToString} from './error.js';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {import('express').Request} Request
|
|
24
|
+
* @typedef {import('express').Response} Response
|
|
25
|
+
* @typedef {import('oracledb').Connection} Connection
|
|
26
|
+
* @typedef {import('oracledb').Result<unknown>} Result
|
|
27
|
+
* @typedef {import('./types.js').argObjType} argObjType
|
|
28
|
+
* @typedef {import('./types.js').fileUploadType} fileUploadType
|
|
29
|
+
* @typedef {import('./types.js').environmentType} environmentType
|
|
30
|
+
* @typedef {import('./types.js').configPlSqlHandlerType} configPlSqlHandlerType
|
|
31
|
+
* @typedef {import('./types.js').BindParameterConfig} BindParameterConfig
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get the SQL statement to execute when a new procedure is invoked
|
|
36
|
+
* @param {string} procedure - The procedure
|
|
37
|
+
* @returns {string} - The SQL statement to execute
|
|
38
|
+
*/
|
|
39
|
+
const getProcedureSQL = (procedure) => `DECLARE
|
|
40
|
+
fileType VARCHAR2(32767);
|
|
41
|
+
fileSize INTEGER;
|
|
42
|
+
fileBlob BLOB;
|
|
43
|
+
fileExist INTEGER := 0;
|
|
44
|
+
BEGIN
|
|
45
|
+
-- Ensure a stateless environment by resetting package state (dbms_session.reset_package)
|
|
46
|
+
dbms_session.modify_package_state(dbms_session.reinitialize);
|
|
47
|
+
|
|
48
|
+
-- initialize the cgi
|
|
49
|
+
owa.init_cgi_env(:cgicount, :cginames, :cgivalues);
|
|
50
|
+
|
|
51
|
+
-- initialize the htp package
|
|
52
|
+
htp.init;
|
|
53
|
+
|
|
54
|
+
-- set the HTBUF_LEN
|
|
55
|
+
htp.HTBUF_LEN := :htbuflen;
|
|
56
|
+
|
|
57
|
+
-- execute the procedure
|
|
58
|
+
BEGIN
|
|
59
|
+
${procedure}
|
|
60
|
+
EXCEPTION WHEN OTHERS THEN
|
|
61
|
+
raise_application_error(-20000, 'Error executing ${procedure}'||CHR(10)||SUBSTR(dbms_utility.format_error_stack()||CHR(10)||dbms_utility.format_error_backtrace(), 1, 2000));
|
|
62
|
+
END;
|
|
63
|
+
|
|
64
|
+
-- Check for file download
|
|
65
|
+
IF (wpg_docload.is_file_download()) THEN
|
|
66
|
+
wpg_docload.get_download_file(fileType);
|
|
67
|
+
IF (filetype = 'B') THEN
|
|
68
|
+
fileExist := 1;
|
|
69
|
+
wpg_docload.get_download_blob(:fileBlob);
|
|
70
|
+
fileSize := dbms_lob.getlength(:fileBlob);
|
|
71
|
+
--dbms_lob.copy(dest_lob=>:fileBlob, src_lob=>fileBlob, amount=>fileSize);
|
|
72
|
+
END IF;
|
|
73
|
+
END IF;
|
|
74
|
+
:fileExist := fileExist;
|
|
75
|
+
:fileType := fileType;
|
|
76
|
+
:fileSize := fileSize;
|
|
77
|
+
|
|
78
|
+
-- retrieve the page
|
|
79
|
+
owa.get_page(thepage=>:lines, irows=>:irows);
|
|
80
|
+
END;`;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Invoke the Oracle procedure and return the page content
|
|
84
|
+
*
|
|
85
|
+
* @param {Request} req - The req object represents the HTTP request.
|
|
86
|
+
* @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
|
|
87
|
+
* @param {argObjType} argObj - - The arguments of the procedure to invoke.
|
|
88
|
+
* @param {environmentType} cgiObj - The cgi of the procedure to invoke.
|
|
89
|
+
* @param {fileUploadType[]} filesToUpload - Array of files to be uploaded
|
|
90
|
+
* @param {configPlSqlHandlerType} options - the options for the middleware.
|
|
91
|
+
* @param {Connection} databaseConnection - Database connection.
|
|
92
|
+
* @returns {Promise<void>} Promise resolving to the page content generated by the executed procedure
|
|
93
|
+
*/
|
|
94
|
+
export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options, databaseConnection) => {
|
|
95
|
+
debug('invokeProcedure: ENTER');
|
|
96
|
+
|
|
97
|
+
const procName = req.params.name;
|
|
98
|
+
|
|
99
|
+
//
|
|
100
|
+
// 1) UPLOAD FILES
|
|
101
|
+
//
|
|
102
|
+
|
|
103
|
+
debug(`invokeProcedure: upload "${filesToUpload.length}" files`);
|
|
104
|
+
if (filesToUpload.length > 0) {
|
|
105
|
+
if (typeof options.documentTable === 'string' && options.documentTable.length > 0) {
|
|
106
|
+
const {documentTable} = options;
|
|
107
|
+
|
|
108
|
+
await Promise.all(filesToUpload.map((file) => uploadFile(file, documentTable, databaseConnection)));
|
|
109
|
+
} else {
|
|
110
|
+
console.warn(`Unable to upload "${filesToUpload.length}" files because the option ""doctable" has not been defined`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//
|
|
115
|
+
// 2) GET SQL STATEMENT AND ARGUMENTS
|
|
116
|
+
//
|
|
117
|
+
|
|
118
|
+
const para = await getProcedure(procName, argObj, options, databaseConnection);
|
|
119
|
+
|
|
120
|
+
//
|
|
121
|
+
// 3) EXECUTE PROCEDURE
|
|
122
|
+
//
|
|
123
|
+
|
|
124
|
+
const HTBUF_LEN = 63;
|
|
125
|
+
const MAX_IROWS = 100000;
|
|
126
|
+
|
|
127
|
+
const cgi = {
|
|
128
|
+
keys: Object.keys(cgiObj),
|
|
129
|
+
values: Object.values(cgiObj),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const fileBlob = await databaseConnection.createLob(oracledb.BLOB);
|
|
133
|
+
|
|
134
|
+
/** @type {BindParameterConfig} */
|
|
135
|
+
const bind = {
|
|
136
|
+
cgicount: {dir: oracledb.BIND_IN, type: oracledb.NUMBER, val: cgi.keys.length},
|
|
137
|
+
cginames: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: cgi.keys},
|
|
138
|
+
cgivalues: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: cgi.values},
|
|
139
|
+
htbuflen: {dir: oracledb.BIND_IN, type: oracledb.NUMBER, val: HTBUF_LEN},
|
|
140
|
+
fileExist: {dir: oracledb.BIND_OUT, type: oracledb.NUMBER},
|
|
141
|
+
fileType: {dir: oracledb.BIND_OUT, type: oracledb.STRING},
|
|
142
|
+
fileSize: {dir: oracledb.BIND_OUT, type: oracledb.NUMBER},
|
|
143
|
+
fileBlob: {dir: oracledb.BIND_INOUT, type: oracledb.BLOB, val: fileBlob},
|
|
144
|
+
lines: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxSize: HTBUF_LEN * 2, maxArraySize: MAX_IROWS},
|
|
145
|
+
irows: {dir: oracledb.BIND_INOUT, type: oracledb.NUMBER, val: MAX_IROWS},
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
// execute procedure and retrieve page
|
|
149
|
+
const sqlStatement = getProcedureSQL(para.sql);
|
|
150
|
+
/** @type {Result | null} */
|
|
151
|
+
let result = null;
|
|
152
|
+
const bindParams = Object.assign({}, bind, para.bind);
|
|
153
|
+
try {
|
|
154
|
+
if (debug.enabled) {
|
|
155
|
+
if (debug.enabled) {
|
|
156
|
+
debug(getBlock('execute', sqlStatement));
|
|
157
|
+
// NOTE: Because inspecting a BLOB value generates a craxy amount of text, we somply remove it.
|
|
158
|
+
const temp = Object.assign({}, bindParams);
|
|
159
|
+
delete temp.fileBlob.val;
|
|
160
|
+
debug(getBlock('bindParams', inspect(temp)));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
result = await databaseConnection.execute(sqlStatement, bindParams);
|
|
164
|
+
} catch (err) {
|
|
165
|
+
if (debug.enabled) {
|
|
166
|
+
debug(getBlock('results', inspect(result)));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
throw new ProcedureError(`Error when executing procedure\n${sqlStatement}\n${errorToString(err)}`, cgiObj, para.sql, para.bind);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
//
|
|
173
|
+
// 4) PROCESS RESULTS
|
|
174
|
+
//
|
|
175
|
+
|
|
176
|
+
// validate results
|
|
177
|
+
const data = z
|
|
178
|
+
.object({
|
|
179
|
+
irows: z.number(),
|
|
180
|
+
lines: z.array(z.string()),
|
|
181
|
+
fileExist: z.number(),
|
|
182
|
+
fileType: z.string().nullable(),
|
|
183
|
+
fileSize: z.number().nullable(),
|
|
184
|
+
fileBlob: z.instanceof(stream.Readable).nullable(),
|
|
185
|
+
})
|
|
186
|
+
.parse(result.outBinds);
|
|
187
|
+
|
|
188
|
+
if (debug.enabled) {
|
|
189
|
+
debug(getBlock('data', inspect(data)));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Make sure that we have retrieved all the rows
|
|
193
|
+
if (data.irows > MAX_IROWS) {
|
|
194
|
+
/* istanbul ignore next */
|
|
195
|
+
throw new ProcedureError(`Error when retrieving rows. irows="${data.irows}"`, cgiObj, para.sql, para.bind);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// combine page
|
|
199
|
+
const pageContent = data.lines.join('');
|
|
200
|
+
if (debug.enabled && pageContent.length > 0) {
|
|
201
|
+
debug(getBlock('PLAIN CONTENT', pageContent));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
//
|
|
205
|
+
// 6) PARSE PAGE
|
|
206
|
+
//
|
|
207
|
+
|
|
208
|
+
// parse what we received from PL/SQL
|
|
209
|
+
const pageComponents = parsePage(pageContent);
|
|
210
|
+
|
|
211
|
+
// add "Server" header
|
|
212
|
+
pageComponents.head.server = cgiObj.SERVER_SOFTWARE;
|
|
213
|
+
|
|
214
|
+
// add file download information
|
|
215
|
+
if (data.fileExist === 1) {
|
|
216
|
+
pageComponents.file.fileType = data.fileType;
|
|
217
|
+
pageComponents.file.fileSize = data.fileSize;
|
|
218
|
+
if (data.fileBlob) {
|
|
219
|
+
pageComponents.file.fileBlob = await streamToBuffer(data.fileBlob);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
//
|
|
224
|
+
// 5) SEND THE RESPONSE
|
|
225
|
+
//
|
|
226
|
+
|
|
227
|
+
sendResponse(req, res, pageComponents);
|
|
228
|
+
|
|
229
|
+
//
|
|
230
|
+
// 6) CLEANUP
|
|
231
|
+
//
|
|
232
|
+
|
|
233
|
+
fileBlob.destroy();
|
|
234
|
+
|
|
235
|
+
debug('invokeProcedure: EXIT');
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Get the procedure and arguments to execute
|
|
240
|
+
* @param {string} procName - The procedure to execute
|
|
241
|
+
* @param {argObjType} argObj - The arguments to pass to the procedure
|
|
242
|
+
* @param {configPlSqlHandlerType} options - The options for the middleware
|
|
243
|
+
* @param {Connection} databaseConnection - The database connection
|
|
244
|
+
* @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
|
|
245
|
+
*/
|
|
246
|
+
const getProcedure = async (procName, argObj, options, databaseConnection) => {
|
|
247
|
+
if (options.pathAlias && options.pathAlias.toLowerCase() === procName.toLowerCase()) {
|
|
248
|
+
debug(`getProcedure: path alias "${options.pathAlias}" redirects to "${options.pathAliasProcedure}"`);
|
|
249
|
+
return {
|
|
250
|
+
sql: `${options.pathAliasProcedure}(p_path=>:p_path);`,
|
|
251
|
+
bind: {
|
|
252
|
+
p_path: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: procName},
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
} else if (procName.startsWith('!')) {
|
|
256
|
+
return await getProcedureVariable(procName.substring(1), argObj, databaseConnection, options);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return await getProcedureNamed(procName, argObj, databaseConnection, options);
|
|
260
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* RequestError
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @typedef {import('./types.js').environmentType} environmentType
|
|
7
|
+
* @typedef {import('./types.js').BindParameterConfig} BindParameterConfig
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export class ProcedureError extends Error {
|
|
11
|
+
/** @type {Date} */
|
|
12
|
+
timestamp;
|
|
13
|
+
/** @type {environmentType} */
|
|
14
|
+
environment;
|
|
15
|
+
/** @type {string} */
|
|
16
|
+
sql;
|
|
17
|
+
/** @type {BindParameterConfig} */
|
|
18
|
+
bind;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {string} message - The error message.
|
|
22
|
+
* @param {environmentType} environment - The environment.
|
|
23
|
+
* @param {string} sql - The SQL to execute.
|
|
24
|
+
* @param {BindParameterConfig} bind - The bind parameters.
|
|
25
|
+
*/
|
|
26
|
+
constructor(message, environment, sql, bind) {
|
|
27
|
+
super(message);
|
|
28
|
+
|
|
29
|
+
// Maintains proper stack trace for where our error was thrown (only available on V8)
|
|
30
|
+
if (Error.captureStackTrace) {
|
|
31
|
+
Error.captureStackTrace(this, ProcedureError);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Custom debugging information
|
|
35
|
+
this.timestamp = new Date();
|
|
36
|
+
this.environment = environment;
|
|
37
|
+
this.sql = sql;
|
|
38
|
+
this.bind = bind;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Invoke the Oracle procedure and return the raw content of the page
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import debugModule from 'debug';
|
|
6
|
+
const debug = debugModule('webplsql:procedureNamed');
|
|
7
|
+
|
|
8
|
+
import oracledb from 'oracledb';
|
|
9
|
+
import z from 'zod';
|
|
10
|
+
import {sanitizeProcName} from './procedureSanitize.js';
|
|
11
|
+
import {RequestError} from './requestError.js';
|
|
12
|
+
import {errorToString} from './error.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Record<string, string>} argsType
|
|
16
|
+
* @typedef {{hitCount: number, args: argsType}} cacheEntryType
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @typedef {import('oracledb').Connection} Connection
|
|
21
|
+
* @typedef {import('oracledb').Result<unknown>} Result
|
|
22
|
+
* @typedef {import('./types.js').configPlSqlHandlerType} configPlSqlHandlerType
|
|
23
|
+
* @typedef {import('./types.js').argObjType} argObjType
|
|
24
|
+
* @typedef {import('./types.js').BindParameterConfig} BindParameterConfig
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
const SQL_GET_ARGUMENT = [
|
|
28
|
+
'DECLARE',
|
|
29
|
+
' schemaName VARCHAR2(32767);',
|
|
30
|
+
' part1 VARCHAR2(32767);',
|
|
31
|
+
' part2 VARCHAR2(32767);',
|
|
32
|
+
' dblink VARCHAR2(32767);',
|
|
33
|
+
' objectType NUMBER;',
|
|
34
|
+
' objectID NUMBER;',
|
|
35
|
+
'BEGIN',
|
|
36
|
+
' dbms_utility.name_resolve(name=>UPPER(:name), context=>1, schema=>schemaName, part1=>part1, part2=>part2, dblink=>dblink, part1_type=>objectType, object_number=>objectID);',
|
|
37
|
+
' IF (part1 IS NOT NULL) THEN',
|
|
38
|
+
' SELECT argument_name, data_type BULK COLLECT INTO :names, :types FROM all_arguments WHERE owner = schemaName AND package_name = part1 AND object_name = part2 AND argument_name IS NOT NULL ORDER BY overload, sequence;',
|
|
39
|
+
' ELSE',
|
|
40
|
+
' SELECT argument_name, data_type BULK COLLECT INTO :names, :types FROM all_arguments WHERE owner = schemaName AND package_name IS NULL AND object_name = part2 AND argument_name IS NOT NULL ORDER BY overload, sequence;',
|
|
41
|
+
' END IF;',
|
|
42
|
+
'END;',
|
|
43
|
+
].join('\n');
|
|
44
|
+
|
|
45
|
+
// NOTE: Consider using a separate cache for each database pool to avoid possible conflicts.
|
|
46
|
+
/** @type {Map<string, cacheEntryType>} */
|
|
47
|
+
const ARGS_CACHE = new Map();
|
|
48
|
+
const ARGS_CACHE_MAX_COUNT = 10000;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Retrieve the argument types for a given procedure to be executed.
|
|
52
|
+
* This is important because if the procedure is defined to take a PL/SQL indexed table,
|
|
53
|
+
* we must provise a table, even if there is only one argument to be submitted.
|
|
54
|
+
* @param {string} procedure - The procedure
|
|
55
|
+
* @param {Connection} databaseConnection - The database connection
|
|
56
|
+
* @returns {Promise<argsType>} - The argument types
|
|
57
|
+
*/
|
|
58
|
+
const loadArguments = async (procedure, databaseConnection) => {
|
|
59
|
+
const MAX_PARAMETER_NUMBER = 1000;
|
|
60
|
+
|
|
61
|
+
/** @type {BindParameterConfig} */
|
|
62
|
+
const bind = {
|
|
63
|
+
name: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: procedure},
|
|
64
|
+
names: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxSize: 60, maxArraySize: MAX_PARAMETER_NUMBER},
|
|
65
|
+
types: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxSize: 60, maxArraySize: MAX_PARAMETER_NUMBER},
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** @type {Result} */
|
|
69
|
+
let result = {};
|
|
70
|
+
try {
|
|
71
|
+
result = await databaseConnection.execute(SQL_GET_ARGUMENT, bind);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
debug('result', result);
|
|
74
|
+
/* istanbul ignore next */
|
|
75
|
+
const message = `Error when retrieving arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`;
|
|
76
|
+
/* istanbul ignore next */
|
|
77
|
+
throw new RequestError(message);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** @type {{names: string[], types: string[]}} */
|
|
81
|
+
let data;
|
|
82
|
+
try {
|
|
83
|
+
data = z
|
|
84
|
+
.object({
|
|
85
|
+
names: z.array(z.string()),
|
|
86
|
+
types: z.array(z.string()),
|
|
87
|
+
})
|
|
88
|
+
.parse(result.outBinds);
|
|
89
|
+
} catch (err) {
|
|
90
|
+
debug('result.outBinds', result.outBinds);
|
|
91
|
+
/* istanbul ignore next */
|
|
92
|
+
const message = `Error when decoding arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`;
|
|
93
|
+
/* istanbul ignore next */
|
|
94
|
+
throw new RequestError(message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (data.names.length !== data.types.length) {
|
|
98
|
+
/* istanbul ignore next */
|
|
99
|
+
throw new RequestError('Error when decoding arguments. The number of names and types does not match');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** @type {Record<string, string>} */
|
|
103
|
+
const argTypes = {};
|
|
104
|
+
for (let i = 0; i < data.names.length; i++) {
|
|
105
|
+
argTypes[data.names[i].toLowerCase()] = data.types[i];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return argTypes;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Remove the cache entries with the lowest hitCount.
|
|
113
|
+
* @param {number} count - Number of entries to remove
|
|
114
|
+
* @returns {void}
|
|
115
|
+
*/
|
|
116
|
+
const removeLowestHitCountEntries = (count) => {
|
|
117
|
+
// Convert cache entries to an array
|
|
118
|
+
const entries = Array.from(ARGS_CACHE.entries());
|
|
119
|
+
|
|
120
|
+
// Sort entries by hitCount in ascending order
|
|
121
|
+
entries.sort((a, b) => a[1].hitCount - b[1].hitCount);
|
|
122
|
+
|
|
123
|
+
// Get the keys of the `count` entries with the lowest hitCount
|
|
124
|
+
const keysToRemove = entries.slice(0, count).map(([key]) => key);
|
|
125
|
+
|
|
126
|
+
// Remove these entries from the cache
|
|
127
|
+
for (const key of keysToRemove) {
|
|
128
|
+
ARGS_CACHE.delete(key);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Find the argument types for a given procedure to be executed.
|
|
134
|
+
* As the arguments are cached, we first look up the cache and only if not yet available we load them.
|
|
135
|
+
* @param {string} procedure - The procedure
|
|
136
|
+
* @param {Connection} databaseConnection - The database connection
|
|
137
|
+
* @returns {Promise<argsType>} - The argument types
|
|
138
|
+
*/
|
|
139
|
+
const findArguments = async (procedure, databaseConnection) => {
|
|
140
|
+
// calculate the key
|
|
141
|
+
//const key = `${databaseConnection.connectString}_${databaseConnection.user}_${procedure.toUpperCase()}`;
|
|
142
|
+
const key = procedure.toUpperCase();
|
|
143
|
+
|
|
144
|
+
// lookup in the cache
|
|
145
|
+
const cacheEntry = ARGS_CACHE.get(key);
|
|
146
|
+
|
|
147
|
+
// if we fount the procedure in the cache, we increase the hit cound and return
|
|
148
|
+
if (cacheEntry) {
|
|
149
|
+
cacheEntry.hitCount++;
|
|
150
|
+
if (debug.enabled) {
|
|
151
|
+
debug(`findArguments: procedure "${procedure}" found in cache with "${cacheEntry.hitCount}" hits`);
|
|
152
|
+
}
|
|
153
|
+
return cacheEntry.args;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// if the cache is full, we remove the 1000 least used cache entries
|
|
157
|
+
if (ARGS_CACHE.size > ARGS_CACHE_MAX_COUNT) {
|
|
158
|
+
if (debug.enabled) {
|
|
159
|
+
debug(`findArguments: cache is full. size=${ARGS_CACHE.size} max=${ARGS_CACHE_MAX_COUNT}`);
|
|
160
|
+
}
|
|
161
|
+
removeLowestHitCountEntries(1000);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// load from database
|
|
165
|
+
if (debug.enabled) {
|
|
166
|
+
debug(`findArguments: procedure "${procedure}" not found in cache and must be loaded`);
|
|
167
|
+
}
|
|
168
|
+
const args = await loadArguments(procedure, databaseConnection);
|
|
169
|
+
|
|
170
|
+
// add to the cache
|
|
171
|
+
ARGS_CACHE.set(key, {hitCount: 0, args});
|
|
172
|
+
|
|
173
|
+
return args;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get the sql statement and bindings for the procedure to execute for a fixed number of arguments
|
|
178
|
+
* @param {string} procName - The procedure to execute
|
|
179
|
+
* @param {argObjType} argObj - The arguments to pass to the procedure
|
|
180
|
+
* @param {Connection} databaseConnection - The database connection
|
|
181
|
+
* @param {configPlSqlHandlerType} options - the options for the middleware.
|
|
182
|
+
* @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
|
|
183
|
+
*/
|
|
184
|
+
export const getProcedureNamed = async (procName, argObj, databaseConnection, options) => {
|
|
185
|
+
if (debug.enabled) {
|
|
186
|
+
debug(`getProcedureNamed: ${procName} arguments=`, argObj);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** @type {BindParameterConfig} */
|
|
190
|
+
const bind = {};
|
|
191
|
+
let index = 0;
|
|
192
|
+
|
|
193
|
+
const sanitizedProcName = await sanitizeProcName(procName, databaseConnection, options);
|
|
194
|
+
const argTypes = await findArguments(sanitizedProcName, databaseConnection);
|
|
195
|
+
|
|
196
|
+
// bindings for the statement
|
|
197
|
+
let sql = `${sanitizedProcName}(`;
|
|
198
|
+
for (const key in argObj) {
|
|
199
|
+
const value = argObj[key];
|
|
200
|
+
const parameterName = `p_${key}`;
|
|
201
|
+
|
|
202
|
+
// prepend the separator, if this is not the first argument
|
|
203
|
+
if (index > 0) {
|
|
204
|
+
sql += ',';
|
|
205
|
+
}
|
|
206
|
+
index++;
|
|
207
|
+
|
|
208
|
+
// add the argument
|
|
209
|
+
sql += `${key}=>:${parameterName}`;
|
|
210
|
+
|
|
211
|
+
// add the binding
|
|
212
|
+
bind[parameterName] = {dir: oracledb.BIND_IN, type: oracledb.STRING};
|
|
213
|
+
|
|
214
|
+
// set the value or array of values
|
|
215
|
+
if (Array.isArray(value) || argTypes[key] === 'PL/SQL TABLE') {
|
|
216
|
+
/** @type {string[]} */
|
|
217
|
+
const val = [];
|
|
218
|
+
if (typeof value === 'string') {
|
|
219
|
+
val.push(value);
|
|
220
|
+
} else {
|
|
221
|
+
value.forEach((element) => {
|
|
222
|
+
val.push(element);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
bind[parameterName].val = val;
|
|
226
|
+
} else if (typeof value === 'string') {
|
|
227
|
+
bind[parameterName].val = value;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
sql += ');';
|
|
231
|
+
|
|
232
|
+
return {sql, bind};
|
|
233
|
+
};
|