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.
- package/package.json +10 -14
- package/src/handler/plsql/errorPage.js +10 -214
- package/src/handler/plsql/handlerPlSql.js +0 -2
- package/src/handler/plsql/procedure.js +192 -123
- package/src/handler/plsql/procedureNamed.js +122 -53
- package/src/handler/plsql/procedureVariable.js +8 -11
- package/src/server/version.js +1 -1
- package/src/util/html.js +53 -0
- package/src/util/trace.js +259 -8
- package/src/util/util.js +66 -0
- package/types/handler/plsql/procedureNamed.d.ts +9 -6
- package/types/handler/plsql/procedureVariable.d.ts +3 -2
- package/types/util/html.d.ts +3 -0
- package/types/util/trace.d.ts +19 -3
- package/types/util/util.d.ts +3 -0
- package/src/util/date.js +0 -23
|
@@ -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
|
-
*
|
|
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 {
|
|
41
|
-
* @
|
|
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
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
-
//
|
|
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
|
-
//
|
|
254
|
+
// GET SQL STATEMENT AND ARGUMENTS
|
|
134
255
|
//
|
|
135
256
|
|
|
136
|
-
const para = await getProcedure(
|
|
257
|
+
const para = await getProcedure(req, req.params.name, argObj, options, databaseConnection);
|
|
137
258
|
|
|
138
259
|
//
|
|
139
|
-
//
|
|
260
|
+
// PROCEDURE PREPARE
|
|
140
261
|
//
|
|
141
262
|
|
|
142
|
-
|
|
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
|
-
|
|
265
|
+
//
|
|
266
|
+
// PROCEDURE EXECUTE
|
|
267
|
+
//
|
|
151
268
|
|
|
152
|
-
|
|
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
|
-
//
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
275
|
+
const lines = await procedureGetPage(true, databaseConnection);
|
|
276
|
+
if (debug.enabled) {
|
|
277
|
+
debug(getBlock('data', lines));
|
|
188
278
|
}
|
|
189
279
|
|
|
190
280
|
//
|
|
191
|
-
//
|
|
281
|
+
// PROCEDURE DOWNLOAD FILE
|
|
192
282
|
//
|
|
193
283
|
|
|
194
|
-
|
|
195
|
-
const
|
|
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(
|
|
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
|
-
//
|
|
291
|
+
// PARSE PAGE
|
|
221
292
|
//
|
|
222
293
|
|
|
223
294
|
// parse what we received from PL/SQL
|
|
224
|
-
const pageComponents = parsePage(
|
|
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 (
|
|
231
|
-
pageComponents.file.fileType =
|
|
232
|
-
pageComponents.file.fileSize =
|
|
233
|
-
|
|
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
|
-
//
|
|
308
|
+
// SEND THE RESPONSE
|
|
240
309
|
//
|
|
241
310
|
|
|
242
311
|
sendResponse(req, res, pageComponents);
|
|
243
312
|
|
|
244
313
|
//
|
|
245
|
-
//
|
|
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 {
|
|
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
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
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
|
|
185
|
-
if (
|
|
186
|
-
|
|
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
|
-
|
|
190
|
-
|
|
191
|
-
|
|
204
|
+
if (argType === DATA_TYPES.CLOB) {
|
|
205
|
+
return {dir: oracledb.BIND_IN, type: oracledb.DB_TYPE_CLOB, val: argValue};
|
|
206
|
+
}
|
|
192
207
|
|
|
193
|
-
|
|
194
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
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 =
|
|
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: `${
|
|
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},
|
package/src/server/version.js
CHANGED