web_plsql 0.15.1 → 0.17.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.
@@ -18,6 +18,7 @@ import {sendResponse} from './sendResponse.js';
18
18
  import {ProcedureError} from './procedureError.js';
19
19
  import {inspect, getBlock} from '../../util/trace.js';
20
20
  import {errorToString} from '../../util/errorToString.js';
21
+ import {sanitizeProcName} from './procedureSanitize.js';
21
22
 
22
23
  /**
23
24
  * @typedef {import('express').Request} Request
@@ -32,69 +33,191 @@ import {errorToString} from '../../util/errorToString.js';
32
33
  */
33
34
 
34
35
  /**
35
- * Get the SQL statement to execute when a new procedure is invoked
36
+ * Get the procedure and arguments to execute
37
+ * @param {Request} req - The req object represents the HTTP request. (only used for debugging)
38
+ * @param {string} procName - The procedure to execute
39
+ * @param {argObjType} argObj - The arguments to pass to the procedure
40
+ * @param {configPlSqlHandlerType} options - The options for the middleware
41
+ * @param {Connection} databaseConnection - The database connection
42
+ * @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
43
+ */
44
+ const getProcedure = async (req, procName, argObj, options, databaseConnection) => {
45
+ // path alias
46
+ if (options.pathAlias?.toLowerCase() === procName.toLowerCase()) {
47
+ debug(`getProcedure: path alias "${options.pathAlias}" redirects to "${options.pathAliasProcedure}"`);
48
+ return {
49
+ sql: `${options.pathAliasProcedure}(p_path=>:p_path)`,
50
+ bind: {
51
+ p_path: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: procName},
52
+ },
53
+ };
54
+ }
55
+
56
+ // check if we use variable arguments
57
+ const useVariableArguments = procName.startsWith('!');
58
+
59
+ // sanitize procedure name
60
+ const sanitizedProcName = await sanitizeProcName(useVariableArguments ? procName.substring(1) : procName, databaseConnection, options);
61
+
62
+ // run procedure
63
+ if (useVariableArguments) {
64
+ return getProcedureVariable(req, sanitizedProcName, argObj);
65
+ } else {
66
+ return await getProcedureNamed(req, sanitizedProcName, argObj, databaseConnection);
67
+ }
68
+ };
69
+
70
+ /**
71
+ * Prepare procedure
36
72
  *
37
73
  * NOTE:
38
74
  * 1) dbms_session.modify_package_state(dbms_session.reinitialize) is used to ensure a stateless environment by resetting package state (dbms_session.reset_package)
39
75
  *
40
- * @param {string} procedure - The procedure
41
- * @returns {string} - The SQL statement to execute
76
+ * @param {environmentType} cgiObj - The cgi of the procedure to invoke.
77
+ * @param {Connection} databaseConnection - Database connection.
78
+ * @returns {Promise<void>} Promise resolving to void.
42
79
  */
43
- const getProcedureSQL = (procedure) => `
44
- DECLARE
45
- l_file_type VARCHAR2(32767);
46
- l_file_size INTEGER;
47
- l_file_exists INTEGER := 0;
48
- BEGIN
49
- dbms_session.modify_package_state(dbms_session.reinitialize);
50
- owa.init_cgi_env(:cgicount, :cginames, :cgivalues);
51
- htp.init;
52
- htp.HTBUF_LEN := :htbuflen;
80
+ const procedurePrepare = async (cgiObj, databaseConnection) => {
81
+ let sqlStatement = 'BEGIN dbms_session.modify_package_state(dbms_session.reinitialize); END;';
82
+ try {
83
+ await databaseConnection.execute(sqlStatement);
84
+ } catch (err) {
85
+ throw new ProcedureError(`Error when preparing procedure\n${errorToString(err)}`, cgiObj, sqlStatement, {});
86
+ }
53
87
 
54
- BEGIN
55
- ${procedure}
56
- EXCEPTION WHEN OTHERS THEN
57
- raise_application_error(-20000, 'Error executing "${procedure}"', TRUE);
58
- END;
88
+ // htbuf_len: reduce this limit based on your worst-case character size.
89
+ // For most character sets, this will be 2 bytes per character, so the limit would be 127.
90
+ // For UTF8 Unicode, it's 3 bytes per character, meaning the limit should be 85.
91
+ // For the newer AL32UTF8 Unicode, it's 4 bytes per character, and the limit should be 63.
92
+ sqlStatement = 'BEGIN owa.init_cgi_env(:cgicount, :cginames, :cgivalues); htp.init; htp.htbuf_len := 63; END;';
93
+ /** @type {BindParameterConfig} */
94
+ const bindParameter = {
95
+ cgicount: {dir: oracledb.BIND_IN, type: oracledb.NUMBER, val: Object.keys(cgiObj).length},
96
+ cginames: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: Object.keys(cgiObj)},
97
+ cgivalues: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: Object.values(cgiObj)},
98
+ };
99
+ try {
100
+ await databaseConnection.execute(sqlStatement, bindParameter);
101
+ } catch (err) {
102
+ throw new ProcedureError(`Error when preparing procedure\n${errorToString(err)}`, cgiObj, sqlStatement, bindParameter);
103
+ }
104
+ };
105
+
106
+ /**
107
+ * Execute procedure
108
+ *
109
+ * @param {{sql: string; bind: BindParameterConfig}} para - The statement and binding to use when executing the procedure.
110
+ * @param {Connection} databaseConnection - Database connection.
111
+ * @returns {Promise<void>} Promise resolving to void.
112
+ */
113
+ const procedureExecute = async (para, databaseConnection) => {
114
+ const sqlStatement = `BEGIN ${para.sql}; END;`;
115
+
116
+ try {
117
+ await databaseConnection.execute(sqlStatement, para.bind);
118
+ } catch (err) {
119
+ throw new ProcedureError(`Error when executing procedure:\n${sqlStatement}\n${errorToString(err)}`, {}, para.sql, para.bind);
120
+ }
121
+ };
122
+
123
+ /**
124
+ * Get page from procedure
125
+ *
126
+ * @param {boolean} test - Test.
127
+ * @param {Connection} databaseConnection - Database connection.
128
+ * @returns {Promise<string>} Promise resolving to the returned page content.
129
+ */
130
+ const procedureGetPage = async (test, databaseConnection) => {
131
+ const MAX_IROWS = 100000;
132
+
133
+ /** @type {BindParameterConfig} */
134
+ const bindParameter = {
135
+ lines: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxArraySize: MAX_IROWS},
136
+ irows: {dir: oracledb.BIND_INOUT, type: oracledb.NUMBER, val: MAX_IROWS},
137
+ };
138
+
139
+ const sqlStatement = 'BEGIN owa.get_page(thepage=>:lines, irows=>:irows); END;';
140
+
141
+ /** @type {Result} */
142
+ let result = {};
143
+ try {
144
+ result = await databaseConnection.execute(sqlStatement, bindParameter);
145
+ } catch (err) {
146
+ if (debug.enabled) {
147
+ debug(getBlock('results', inspect(result)));
148
+ }
149
+
150
+ throw new ProcedureError(`Error when getting page returned by procedure\n${errorToString(err)}`, {}, sqlStatement, bindParameter);
151
+ }
152
+
153
+ const {lines, irows} = z.object({irows: z.number(), lines: z.array(z.string())}).parse(result.outBinds);
154
+
155
+ // Make sure that we have retrieved all the rows
156
+ if (irows > MAX_IROWS) {
157
+ /* istanbul ignore next */
158
+ throw new ProcedureError(`Error when retrieving rows. irows="${irows}"`, {}, sqlStatement, bindParameter);
159
+ }
160
+
161
+ return lines.join('');
162
+ };
163
+
164
+ /**
165
+ * Download files from procedure
166
+ *
167
+ * @param {oracledb.Lob} fileBlob - The blob eventually containing the file.
168
+ * @param {Connection} databaseConnection - Database connection.
169
+ * @returns {Promise<{fileType: string, fileSize: number, fileBlob: stream.Readable | null}>} Promise resolving to the result.
170
+ */
171
+ const procedureDownloadFiles = async (fileBlob, databaseConnection) => {
172
+ /** @type {BindParameterConfig} */
173
+ const bindParameter = {
174
+ fileType: {dir: oracledb.BIND_OUT, type: oracledb.STRING},
175
+ fileSize: {dir: oracledb.BIND_OUT, type: oracledb.NUMBER},
176
+ fileBlob: {dir: oracledb.BIND_INOUT, type: oracledb.BLOB, val: fileBlob},
177
+ };
59
178
 
179
+ const sqlStatement = `
180
+ DECLARE
181
+ l_file_type VARCHAR2(32767) := '';
182
+ l_file_size INTEGER := 0;
183
+ BEGIN
60
184
  IF (wpg_docload.is_file_download()) THEN
61
185
  wpg_docload.get_download_file(l_file_type);
62
186
  IF (l_file_type = 'B') THEN
63
- l_file_exists := 1;
64
187
  wpg_docload.get_download_blob(:fileBlob);
65
188
  l_file_size := dbms_lob.getlength(:fileBlob);
66
189
  END IF;
67
190
  END IF;
68
- :fileExist := l_file_exists;
69
191
  :fileType := l_file_type;
70
192
  :fileSize := l_file_size;
71
-
72
- owa.get_page(thepage=>:lines, irows=>:irows);
73
193
  END;
74
194
  `;
75
195
 
76
- /**
77
- * Get the procedure and arguments to execute
78
- * @param {string} procName - The procedure to execute
79
- * @param {argObjType} argObj - The arguments to pass to the procedure
80
- * @param {configPlSqlHandlerType} options - The options for the middleware
81
- * @param {Connection} databaseConnection - The database connection
82
- * @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
83
- */
84
- const getProcedure = async (procName, argObj, options, databaseConnection) => {
85
- if (options.pathAlias && options.pathAlias.toLowerCase() === procName.toLowerCase()) {
86
- debug(`getProcedure: path alias "${options.pathAlias}" redirects to "${options.pathAliasProcedure}"`);
87
- return {
88
- sql: `${options.pathAliasProcedure}(p_path=>:p_path);`,
89
- bind: {
90
- p_path: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: procName},
91
- },
92
- };
93
- } else if (procName.startsWith('!')) {
94
- return await getProcedureVariable(procName.substring(1), argObj, databaseConnection, options);
196
+ /** @type {Result | null} */
197
+ let result = null;
198
+ try {
199
+ result = await databaseConnection.execute(sqlStatement, bindParameter);
200
+ } catch (err) {
201
+ if (debug.enabled) {
202
+ debug(getBlock('results', inspect(result)));
203
+ }
204
+
205
+ throw new ProcedureError(`Error when downloading files of procedure\n${errorToString(err)}`, {}, sqlStatement, bindParameter);
95
206
  }
96
207
 
97
- return await getProcedureNamed(procName, argObj, databaseConnection, options);
208
+ return z
209
+ .object({
210
+ fileType: z
211
+ .string()
212
+ .nullable()
213
+ .transform((val) => val ?? ''),
214
+ fileSize: z
215
+ .number()
216
+ .nullable()
217
+ .transform((val) => val ?? 0),
218
+ fileBlob: z.instanceof(stream.Readable).nullable(),
219
+ })
220
+ .parse(result.outBinds);
98
221
  };
99
222
 
100
223
  /**
@@ -112,10 +235,8 @@ const getProcedure = async (procName, argObj, options, databaseConnection) => {
112
235
  export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options, databaseConnection) => {
113
236
  debug('invokeProcedure: ENTER');
114
237
 
115
- const procName = req.params.name;
116
-
117
238
  //
118
- // 1) UPLOAD FILES
239
+ // UPLOAD FILES
119
240
  //
120
241
 
121
242
  debug(`invokeProcedure: upload "${filesToUpload.length}" files`);
@@ -130,119 +251,67 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
130
251
  }
131
252
 
132
253
  //
133
- // 2) GET SQL STATEMENT AND ARGUMENTS
254
+ // GET SQL STATEMENT AND ARGUMENTS
134
255
  //
135
256
 
136
- const para = await getProcedure(procName, argObj, options, databaseConnection);
257
+ const para = await getProcedure(req, req.params.name, argObj, options, databaseConnection);
137
258
 
138
259
  //
139
- // 3) EXECUTE PROCEDURE
260
+ // PROCEDURE PREPARE
140
261
  //
141
262
 
142
- const HTBUF_LEN = 63;
143
- const MAX_IROWS = 100000;
144
-
145
- const cgi = {
146
- keys: Object.keys(cgiObj),
147
- values: Object.values(cgiObj),
148
- };
263
+ await procedurePrepare(cgiObj, databaseConnection);
149
264
 
150
- const fileBlob = await databaseConnection.createLob(oracledb.BLOB);
265
+ //
266
+ // PROCEDURE EXECUTE
267
+ //
151
268
 
152
- /** @type {BindParameterConfig} */
153
- const bind = {
154
- cgicount: {dir: oracledb.BIND_IN, type: oracledb.NUMBER, val: cgi.keys.length},
155
- cginames: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: cgi.keys},
156
- cgivalues: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: cgi.values},
157
- htbuflen: {dir: oracledb.BIND_IN, type: oracledb.NUMBER, val: HTBUF_LEN},
158
- fileExist: {dir: oracledb.BIND_OUT, type: oracledb.NUMBER},
159
- fileType: {dir: oracledb.BIND_OUT, type: oracledb.STRING},
160
- fileSize: {dir: oracledb.BIND_OUT, type: oracledb.NUMBER},
161
- fileBlob: {dir: oracledb.BIND_INOUT, type: oracledb.BLOB, val: fileBlob},
162
- lines: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxSize: HTBUF_LEN * 2, maxArraySize: MAX_IROWS},
163
- irows: {dir: oracledb.BIND_INOUT, type: oracledb.NUMBER, val: MAX_IROWS},
164
- };
269
+ await procedureExecute(para, databaseConnection);
165
270
 
166
- // execute procedure and retrieve page
167
- const sqlStatement = getProcedureSQL(para.sql);
168
- /** @type {Result | null} */
169
- let result = null;
170
- const bindParams = Object.assign({}, bind, para.bind);
171
- try {
172
- if (debug.enabled) {
173
- if (debug.enabled) {
174
- debug(getBlock('execute', sqlStatement));
175
- // NOTE: Because inspecting a BLOB value generates a craxy amount of text, we somply remove it.
176
- const temp = Object.assign({}, bindParams);
177
- delete temp.fileBlob.val;
178
- debug(getBlock('bindParams', inspect(temp)));
179
- }
180
- }
181
- result = await databaseConnection.execute(sqlStatement, bindParams);
182
- } catch (err) {
183
- if (debug.enabled) {
184
- debug(getBlock('results', inspect(result)));
185
- }
271
+ //
272
+ // PROCEDURE GET PAGE
273
+ //
186
274
 
187
- throw new ProcedureError(`Error when executing procedure\n${sqlStatement}\n${errorToString(err)}`, cgiObj, para.sql, para.bind);
275
+ const lines = await procedureGetPage(true, databaseConnection);
276
+ if (debug.enabled) {
277
+ debug(getBlock('data', lines));
188
278
  }
189
279
 
190
280
  //
191
- // 4) PROCESS RESULTS
281
+ // PROCEDURE DOWNLOAD FILE
192
282
  //
193
283
 
194
- // validate results
195
- const data = z
196
- .object({
197
- irows: z.number(),
198
- lines: z.array(z.string()),
199
- fileExist: z.number(),
200
- fileType: z.string().nullable(),
201
- fileSize: z.number().nullable(),
202
- fileBlob: z.instanceof(stream.Readable).nullable(),
203
- })
204
- .parse(result.outBinds);
205
-
284
+ const fileBlob = await databaseConnection.createLob(oracledb.BLOB);
285
+ const fileDownload = await procedureDownloadFiles(fileBlob, databaseConnection);
206
286
  if (debug.enabled) {
207
- debug(getBlock('data', inspect(data)));
287
+ debug(getBlock('data', inspect(fileDownload)));
208
288
  }
209
289
 
210
- // Make sure that we have retrieved all the rows
211
- if (data.irows > MAX_IROWS) {
212
- /* istanbul ignore next */
213
- throw new ProcedureError(`Error when retrieving rows. irows="${data.irows}"`, cgiObj, para.sql, para.bind);
214
- }
215
-
216
- // combine page
217
- const pageContent = data.lines.join('');
218
-
219
290
  //
220
- // 6) PARSE PAGE
291
+ // PARSE PAGE
221
292
  //
222
293
 
223
294
  // parse what we received from PL/SQL
224
- const pageComponents = parsePage(pageContent);
295
+ const pageComponents = parsePage(lines);
225
296
 
226
297
  // add "Server" header
227
298
  pageComponents.head.server = cgiObj.SERVER_SOFTWARE;
228
299
 
229
300
  // add file download information
230
- if (data.fileExist === 1) {
231
- pageComponents.file.fileType = data.fileType;
232
- pageComponents.file.fileSize = data.fileSize;
233
- if (data.fileBlob) {
234
- pageComponents.file.fileBlob = await streamToBuffer(data.fileBlob);
235
- }
301
+ if (fileDownload.fileType !== '' && fileDownload.fileSize > 0 && fileDownload.fileBlob !== null) {
302
+ pageComponents.file.fileType = fileDownload.fileType;
303
+ pageComponents.file.fileSize = fileDownload.fileSize;
304
+ pageComponents.file.fileBlob = await streamToBuffer(fileDownload.fileBlob);
236
305
  }
237
306
 
238
307
  //
239
- // 5) SEND THE RESPONSE
308
+ // SEND THE RESPONSE
240
309
  //
241
310
 
242
311
  sendResponse(req, res, pageComponents);
243
312
 
244
313
  //
245
- // 6) CLEANUP
314
+ // CLEANUP
246
315
  //
247
316
 
248
317
  fileBlob.destroy();
@@ -7,21 +7,21 @@ const debug = debugModule('webplsql:procedureNamed');
7
7
 
8
8
  import oracledb from 'oracledb';
9
9
  import z from 'zod';
10
- import {sanitizeProcName} from './procedureSanitize.js';
11
10
  import {RequestError} from './requestError.js';
12
11
  import {errorToString} from '../../util/errorToString.js';
12
+ import {stringToNumber} from '../../util/util.js';
13
+ import {toTable, warningMessage} from '../../util/trace.js';
13
14
 
14
15
  /**
15
- * @typedef {Record<string, string>} argsType
16
- * @typedef {{hitCount: number, args: argsType}} cacheEntryType
17
- */
18
-
19
- /**
16
+ * @typedef {import('express').Request} Request
20
17
  * @typedef {import('oracledb').Connection} Connection
21
18
  * @typedef {import('oracledb').Result<unknown>} Result
22
19
  * @typedef {import('../../types.js').configPlSqlHandlerType} configPlSqlHandlerType
23
20
  * @typedef {import('../../types.js').argObjType} argObjType
24
21
  * @typedef {import('../../types.js').BindParameterConfig} BindParameterConfig
22
+ * @typedef {import('../../types.js').BindParameter} BindParameter
23
+ * @typedef {Record<string, string>} argsType
24
+ * @typedef {{hitCount: number, args: argsType}} cacheEntryType
25
25
  */
26
26
 
27
27
  const SQL_GET_ARGUMENT = [
@@ -42,6 +42,24 @@ const SQL_GET_ARGUMENT = [
42
42
  'END;',
43
43
  ].join('\n');
44
44
 
45
+ const DATA_TYPES = Object.freeze({
46
+ VARCHAR2: 'VARCHAR2',
47
+ CHAR: 'CHAR',
48
+ BINARY_INTEGER: 'BINARY_INTEGER',
49
+ NUMBER: 'NUMBER',
50
+ DATE: 'DATE',
51
+ CLOB: 'CLOB',
52
+ PL_SQL_TABLE: 'PL/SQL TABLE',
53
+ // PL/SQL BOOLEAN
54
+ // PL/SQL RECORD
55
+ // OBJECT
56
+ // TABLE
57
+ // BLOB
58
+ // RAW
59
+ // VARRAY
60
+ // REF CURSOR
61
+ });
62
+
45
63
  // NOTE: Consider using a separate cache for each database pool to avoid possible conflicts.
46
64
  /** @type {Map<string, cacheEntryType>} */
47
65
  const ARGS_CACHE = new Map();
@@ -137,8 +155,6 @@ const removeLowestHitCountEntries = (count) => {
137
155
  * @returns {Promise<argsType>} - The argument types
138
156
  */
139
157
  const findArguments = async (procedure, databaseConnection) => {
140
- // calculate the key
141
- //const key = `${databaseConnection.connectString}_${databaseConnection.user}_${procedure.toUpperCase()}`;
142
158
  const key = procedure.toUpperCase();
143
159
 
144
160
  // lookup in the cache
@@ -174,60 +190,113 @@ const findArguments = async (procedure, databaseConnection) => {
174
190
  };
175
191
 
176
192
  /**
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
193
+ * Get the bindling for an argument.
194
+ * @param {string} argName - The argument name.
195
+ * @param {unknown} argValue - The argument value.
196
+ * @param {string} argType - The argument type.
197
+ * @returns {BindParameter} - The binding.
183
198
  */
184
- export const getProcedureNamed = async (procName, argObj, databaseConnection, options) => {
185
- if (debug.enabled) {
186
- debug(`getProcedureNamed: ${procName} arguments=`, argObj);
199
+ export const getBinding = (argName, argValue, argType) => {
200
+ if (argType === DATA_TYPES.VARCHAR2 || argType === DATA_TYPES.CHAR) {
201
+ return {dir: oracledb.BIND_IN, type: oracledb.DB_TYPE_VARCHAR, val: argValue};
187
202
  }
188
203
 
189
- /** @type {BindParameterConfig} */
190
- const bind = {};
191
- let index = 0;
204
+ if (argType === DATA_TYPES.CLOB) {
205
+ return {dir: oracledb.BIND_IN, type: oracledb.DB_TYPE_CLOB, val: argValue};
206
+ }
192
207
 
193
- const sanitizedProcName = await sanitizeProcName(procName, databaseConnection, options);
194
- const argTypes = await findArguments(sanitizedProcName, databaseConnection);
208
+ if (argType === DATA_TYPES.NUMBER || argType === DATA_TYPES.BINARY_INTEGER) {
209
+ const value = stringToNumber(argValue);
210
+ if (value === null) {
211
+ throw new Error(`Error in named parameter "${argName}": invalid value "${argValue}" for type "${argType}"`);
212
+ }
213
+
214
+ return {dir: oracledb.BIND_IN, type: oracledb.DB_TYPE_NUMBER, val: value};
215
+ }
216
+
217
+ if (argType === DATA_TYPES.DATE) {
218
+ if (typeof argValue !== 'string') {
219
+ throw new Error(`Error in named parameter "${argName}": invalid value "${argValue}" for type "${argType}"`);
220
+ }
221
+ const value = new Date(argValue);
222
+ if (Number.isNaN(value.getTime())) {
223
+ throw new Error(`Error in named parameter "${argName}": invalid value "${argValue}" for type "${argType}"`);
224
+ }
225
+
226
+ return {dir: oracledb.BIND_IN, type: oracledb.DB_TYPE_VARCHAR, val: value};
227
+ }
228
+
229
+ if (argType === DATA_TYPES.PL_SQL_TABLE || Array.isArray(argValue)) {
230
+ const value = typeof argValue === 'string' ? [argValue] : argValue;
231
+
232
+ return {dir: oracledb.BIND_IN, type: oracledb.DB_TYPE_DATE, val: value};
233
+ }
234
+
235
+ throw new Error(`Error in named parameter "${argName}": invalid binding type "${argType}"`);
236
+ };
237
+
238
+ /**
239
+ * Get binding table for tracing.
240
+ * @param {argObjType} argObj - The arguments to pass to the procedure
241
+ * @param {argsType} argTypes - The argument types.
242
+ * @returns {string} - The text.
243
+ */
244
+ const inspectBindings = (argObj, argTypes) => {
245
+ const rows = Object.entries(argObj).map(([key, value]) => {
246
+ return [key, value.toString(), typeof value, argTypes[key.toLowerCase()]];
247
+ });
248
+
249
+ const {text} = toTable(['id', 'value', 'value type', 'argument type'], rows);
250
+
251
+ return text;
252
+ };
253
+
254
+ /**
255
+ * Get the sql statement and bindings for the procedure to execute for a fixed number of arguments
256
+ * @param {Request} req - The req object represents the HTTP request. (only used for debugging)
257
+ * @param {string} procName - The procedure to execute
258
+ * @param {argObjType} argObj - The arguments to pass to the procedure
259
+ * @param {Connection} databaseConnection - The database connection
260
+ * @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
261
+ */
262
+ export const getProcedureNamed = async (req, procName, argObj, databaseConnection) => {
263
+ debug(`getProcedureNamed: ${procName} arguments=`, argObj);
264
+
265
+ // get the types of the arguments
266
+ const argTypes = await findArguments(procName, databaseConnection);
267
+
268
+ /** @type {string[]} */
269
+ const sqlParameter = [];
270
+
271
+ /** @type {BindParameterConfig} */
272
+ const bindings = {};
195
273
 
196
274
  // bindings for the statement
197
- let sql = `${sanitizedProcName}(`;
198
275
  for (const key in argObj) {
199
- const value = argObj[key];
200
276
  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;
277
+ const argValue = argObj[key];
278
+ const argType = argTypes[key.toLowerCase()];
279
+ /** @type {BindParameter} */
280
+ let bind = {dir: oracledb.BIND_IN, type: oracledb.DB_TYPE_VARCHAR, val: argValue};
281
+
282
+ if (argType) {
283
+ bind = getBinding(key, argValue, argType);
284
+ } else {
285
+ const text = inspectBindings(argObj, argTypes);
286
+ warningMessage({type: 'warning', message: `Error in named parameter "${key}": invalid binding type "${argType}"\n\n${text}`, req});
228
287
  }
288
+
289
+ sqlParameter.push(`${key}=>:${parameterName}`);
290
+ bindings[parameterName] = bind;
291
+ }
292
+
293
+ // select statement
294
+ const sql = `${procName}(${sqlParameter.join(', ')})`;
295
+
296
+ if (debug.enabled) {
297
+ debug(sql);
298
+ debug(inspectBindings(argObj, argTypes));
229
299
  }
230
- sql += ');';
231
300
 
232
- return {sql, bind};
301
+ return {sql, bind: bindings};
233
302
  };
@@ -6,9 +6,9 @@ import debugModule from 'debug';
6
6
  const debug = debugModule('webplsql:procedureVariable');
7
7
 
8
8
  import oracledb from 'oracledb';
9
- import {sanitizeProcName} from './procedureSanitize.js';
10
9
 
11
10
  /**
11
+ * @typedef {import('express').Request} Request
12
12
  * @typedef {import('oracledb').Connection} Connection
13
13
  * @typedef {import('oracledb').Result<unknown>} Result
14
14
  * @typedef {import('../../types.js').configPlSqlHandlerType} configPlSqlHandlerType
@@ -17,14 +17,13 @@ import {sanitizeProcName} from './procedureSanitize.js';
17
17
  */
18
18
 
19
19
  /**
20
- * Get the sql statement and bindings for the procedure to execute for a variable number of arguments
21
- * @param {string} procName - The procedure to execute
22
- * @param {argObjType} argObj - The arguments to pass to the procedure
23
- * @param {Connection} databaseConnection - The database connection
24
- * @param {configPlSqlHandlerType} options - the options for the middleware.
25
- * @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
20
+ * Get the sql statement and bindings for the procedure to execute for a variable number of arguments
21
+ * @param {Request} req - The req object represents the HTTP request. (only used for debugging)
22
+ * @param {string} procName - The procedure to execute
23
+ * @param {argObjType} argObj - The arguments to pass to the procedure
24
+ * @returns {{sql: string; bind: BindParameterConfig}} - The SQL statement and bindings for the procedure to execute
26
25
  */
27
- export const getProcedureVariable = async (procName, argObj, databaseConnection, options) => {
26
+ export const getProcedureVariable = (req, procName, argObj) => {
28
27
  if (debug.enabled) {
29
28
  debug(`getProcedureVariable: ${procName} arguments=`, argObj);
30
29
  }
@@ -45,10 +44,8 @@ export const getProcedureVariable = async (procName, argObj, databaseConnection,
45
44
  }
46
45
  }
47
46
 
48
- const sanitizedProcName = await sanitizeProcName(procName, databaseConnection, options);
49
-
50
47
  return {
51
- sql: `${sanitizedProcName}(:argnames, :argvalues);`,
48
+ sql: `${procName}(:argnames, :argvalues)`,
52
49
  bind: {
53
50
  argnames: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: names},
54
51
  argvalues: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: values},
@@ -2,4 +2,4 @@
2
2
  * Returns the current library version
3
3
  * @returns {string} - Version.
4
4
  */
5
- export const getVersion = () => '0.15.1';
5
+ export const getVersion = () => '0.17.0';