web_plsql 0.5.1 → 0.8.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.
Files changed (96) hide show
  1. package/README.md +128 -111
  2. package/examples/server_apex.js +26 -0
  3. package/examples/server_sample.js +30 -0
  4. package/examples/sql/{doc_table.sql → doctable.sql} +1 -1
  5. package/examples/sql/install.sql +6 -9
  6. package/examples/sql/sample_package.sql +27 -0
  7. package/examples/sql/sample_package_body.sql +245 -0
  8. package/examples/static/sample.css +11 -10
  9. package/package.json +92 -83
  10. package/src/cgi.js +127 -0
  11. package/src/error.js +24 -0
  12. package/src/errorPage.js +296 -0
  13. package/src/file.js +95 -0
  14. package/src/handlerLogger.js +21 -0
  15. package/src/handlerMetrics.js +46 -0
  16. package/src/handlerPlSql.js +65 -0
  17. package/src/handlerUpload.js +32 -0
  18. package/src/index.js +17 -0
  19. package/src/oracle.js +80 -0
  20. package/src/parsePage.js +193 -0
  21. package/src/procedure.js +260 -0
  22. package/src/procedureError.js +40 -0
  23. package/src/procedureNamed.js +233 -0
  24. package/src/procedureSanitize.js +215 -0
  25. package/src/procedureVariable.js +57 -0
  26. package/src/request.js +103 -0
  27. package/src/{requestError.ts → requestError.js} +8 -4
  28. package/src/sendResponse.js +104 -0
  29. package/src/server.js +185 -0
  30. package/src/shutdown.js +53 -0
  31. package/src/stream.js +28 -0
  32. package/src/trace.js +74 -0
  33. package/src/tty.js +48 -0
  34. package/src/types.js +146 -0
  35. package/src/upload.js +92 -0
  36. package/src/version.js +38 -0
  37. package/types/cgi.d.ts +4 -0
  38. package/types/error.d.ts +1 -0
  39. package/types/errorPage.d.ts +10 -0
  40. package/types/file.d.ts +6 -0
  41. package/types/handlerLogger.d.ts +2 -0
  42. package/types/handlerMetrics.d.ts +4 -0
  43. package/types/handlerPlSql.d.ts +8 -0
  44. package/types/handlerUpload.d.ts +7 -0
  45. package/types/index.d.ts +10 -0
  46. package/types/oracle.d.ts +5 -0
  47. package/types/parsePage.d.ts +3 -0
  48. package/types/procedure.d.ts +10 -0
  49. package/types/procedureError.d.ts +23 -0
  50. package/types/procedureNamed.d.ts +14 -0
  51. package/types/procedureSanitize.d.ts +14 -0
  52. package/types/procedureVariable.d.ts +9 -0
  53. package/types/request.d.ts +7 -0
  54. package/types/requestError.d.ts +8 -0
  55. package/types/sendResponse.d.ts +4 -0
  56. package/types/server.d.ts +11 -0
  57. package/types/shutdown.d.ts +2 -0
  58. package/types/stream.d.ts +1 -0
  59. package/types/trace.d.ts +5 -0
  60. package/types/tty.d.ts +4 -0
  61. package/types/types.d.ts +251 -0
  62. package/types/upload.d.ts +5 -0
  63. package/types/version.d.ts +8 -0
  64. package/.editorconfig +0 -8
  65. package/.eslintignore +0 -3
  66. package/.eslintrc.js +0 -347
  67. package/CHANGELOG.md +0 -133
  68. package/examples/apex.js +0 -70
  69. package/examples/credentials.js +0 -22
  70. package/examples/oracledb_example.js +0 -30
  71. package/examples/sample.js +0 -101
  72. package/examples/sql/sample.pkb +0 -223
  73. package/examples/sql/sample.pks +0 -24
  74. package/jest.config.js +0 -207
  75. package/src/cgi.ts +0 -95
  76. package/src/config.ts +0 -97
  77. package/src/errorPage.ts +0 -286
  78. package/src/fileUpload.ts +0 -126
  79. package/src/index.ts +0 -65
  80. package/src/page.ts +0 -275
  81. package/src/procedure.ts +0 -360
  82. package/src/procedureError.ts +0 -27
  83. package/src/request.ts +0 -139
  84. package/src/stream.ts +0 -26
  85. package/src/trace.ts +0 -194
  86. package/test/.eslintrc.json +0 -5
  87. package/test/__tests__/cgi.ts +0 -96
  88. package/test/__tests__/config.ts +0 -41
  89. package/test/__tests__/errorPage.ts +0 -101
  90. package/test/__tests__/oracledb_mock.ts +0 -98
  91. package/test/__tests__/server.ts +0 -495
  92. package/test/__tests__/stream.ts +0 -21
  93. package/test/mock/oracledb.ts +0 -85
  94. package/test/static/static.html +0 -1
  95. package/tsconfig.json +0 -24
  96. package/tsconfig.src.json +0 -23
@@ -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
+ };