web_plsql 0.16.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web_plsql",
3
- "version": "0.16.1",
3
+ "version": "0.17.0",
4
4
  "author": "Dieter Oberkofler <dieter.oberkofler@gmail.com>",
5
5
  "license": "MIT",
6
6
  "description": "The Express Middleware for Oracle PL/SQL",
@@ -61,30 +61,28 @@
61
61
  "cookie-parser": "1.4.7",
62
62
  "debug": "4.4.3",
63
63
  "express": "5.2.1",
64
- "fs-extra": "11.3.2",
65
64
  "http-parser-js": "0.5.10",
66
65
  "morgan": "1.10.1",
67
66
  "multer": "2.0.2",
68
67
  "rotating-file-stream": "3.2.7",
69
- "zod": "4.1.13"
68
+ "zod": "4.3.6"
70
69
  },
71
70
  "devDependencies": {
72
71
  "@types/basic-auth": "1.1.8",
73
72
  "@types/compression": "1.8.1",
74
73
  "@types/cookie-parser": "1.4.10",
75
74
  "@types/debug": "4.1.12",
76
- "@types/fs-extra": "11.0.4",
77
75
  "@types/morgan": "1.9.10",
78
76
  "@types/multer": "2.0.0",
79
- "@types/node": "24.10.2",
80
- "@types/oracledb": "6.10.0",
77
+ "@types/node": "25.0.10",
78
+ "@types/oracledb": "6.10.1",
81
79
  "@types/supertest": "6.0.3",
82
- "eslint": "9.39.1",
83
- "eslint-plugin-jsdoc": "61.5.0",
84
- "prettier": "3.7.4",
80
+ "eslint": "9.39.2",
81
+ "eslint-plugin-jsdoc": "62.4.1",
82
+ "prettier": "3.8.1",
85
83
  "shx": "0.4.0",
86
- "supertest": "7.1.4",
84
+ "supertest": "7.2.2",
87
85
  "typescript": "5.9.3",
88
- "typescript-eslint": "8.49.0"
86
+ "typescript-eslint": "8.53.1"
89
87
  }
90
88
  }
@@ -32,48 +32,6 @@ import {sanitizeProcName} from './procedureSanitize.js';
32
32
  * @typedef {import('../../types.js').BindParameterConfig} BindParameterConfig
33
33
  */
34
34
 
35
- /**
36
- * Get the SQL statement to execute when a new procedure is invoked
37
- *
38
- * NOTE:
39
- * 1) dbms_session.modify_package_state(dbms_session.reinitialize) is used to ensure a stateless environment by resetting package state (dbms_session.reset_package)
40
- *
41
- * @param {string} procedure - The procedure
42
- * @returns {string} - The SQL statement to execute
43
- */
44
- const getProcedureSQL = (procedure) => `
45
- DECLARE
46
- l_file_type VARCHAR2(32767);
47
- l_file_size INTEGER;
48
- l_file_exists INTEGER := 0;
49
- BEGIN
50
- dbms_session.modify_package_state(dbms_session.reinitialize);
51
- owa.init_cgi_env(:cgicount, :cginames, :cgivalues);
52
- htp.init;
53
- htp.HTBUF_LEN := :htbuflen;
54
-
55
- BEGIN
56
- ${procedure}
57
- EXCEPTION WHEN OTHERS THEN
58
- raise_application_error(-20000, 'Error executing "${procedure}"', TRUE);
59
- END;
60
-
61
- IF (wpg_docload.is_file_download()) THEN
62
- wpg_docload.get_download_file(l_file_type);
63
- IF (l_file_type = 'B') THEN
64
- l_file_exists := 1;
65
- wpg_docload.get_download_blob(:fileBlob);
66
- l_file_size := dbms_lob.getlength(:fileBlob);
67
- END IF;
68
- END IF;
69
- :fileExist := l_file_exists;
70
- :fileType := l_file_type;
71
- :fileSize := l_file_size;
72
-
73
- owa.get_page(thepage=>:lines, irows=>:irows);
74
- END;
75
- `;
76
-
77
35
  /**
78
36
  * Get the procedure and arguments to execute
79
37
  * @param {Request} req - The req object represents the HTTP request. (only used for debugging)
@@ -85,10 +43,10 @@ END;
85
43
  */
86
44
  const getProcedure = async (req, procName, argObj, options, databaseConnection) => {
87
45
  // path alias
88
- if (options.pathAlias && options.pathAlias.toLowerCase() === procName.toLowerCase()) {
46
+ if (options.pathAlias?.toLowerCase() === procName.toLowerCase()) {
89
47
  debug(`getProcedure: path alias "${options.pathAlias}" redirects to "${options.pathAliasProcedure}"`);
90
48
  return {
91
- sql: `${options.pathAliasProcedure}(p_path=>:p_path);`,
49
+ sql: `${options.pathAliasProcedure}(p_path=>:p_path)`,
92
50
  bind: {
93
51
  p_path: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: procName},
94
52
  },
@@ -109,6 +67,159 @@ const getProcedure = async (req, procName, argObj, options, databaseConnection)
109
67
  }
110
68
  };
111
69
 
70
+ /**
71
+ * Prepare procedure
72
+ *
73
+ * NOTE:
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)
75
+ *
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.
79
+ */
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
+ }
87
+
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
+ };
178
+
179
+ const sqlStatement = `
180
+ DECLARE
181
+ l_file_type VARCHAR2(32767) := '';
182
+ l_file_size INTEGER := 0;
183
+ BEGIN
184
+ IF (wpg_docload.is_file_download()) THEN
185
+ wpg_docload.get_download_file(l_file_type);
186
+ IF (l_file_type = 'B') THEN
187
+ wpg_docload.get_download_blob(:fileBlob);
188
+ l_file_size := dbms_lob.getlength(:fileBlob);
189
+ END IF;
190
+ END IF;
191
+ :fileType := l_file_type;
192
+ :fileSize := l_file_size;
193
+ END;
194
+ `;
195
+
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);
206
+ }
207
+
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);
221
+ };
222
+
112
223
  /**
113
224
  * Invoke the Oracle procedure and return the page content
114
225
  *
@@ -124,10 +235,8 @@ const getProcedure = async (req, procName, argObj, options, databaseConnection)
124
235
  export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, options, databaseConnection) => {
125
236
  debug('invokeProcedure: ENTER');
126
237
 
127
- const procName = req.params.name;
128
-
129
238
  //
130
- // 1) UPLOAD FILES
239
+ // UPLOAD FILES
131
240
  //
132
241
 
133
242
  debug(`invokeProcedure: upload "${filesToUpload.length}" files`);
@@ -142,119 +251,67 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
142
251
  }
143
252
 
144
253
  //
145
- // 2) GET SQL STATEMENT AND ARGUMENTS
254
+ // GET SQL STATEMENT AND ARGUMENTS
146
255
  //
147
256
 
148
- const para = await getProcedure(req, procName, argObj, options, databaseConnection);
257
+ const para = await getProcedure(req, req.params.name, argObj, options, databaseConnection);
149
258
 
150
259
  //
151
- // 3) EXECUTE PROCEDURE
260
+ // PROCEDURE PREPARE
152
261
  //
153
262
 
154
- const HTBUF_LEN = 63;
155
- const MAX_IROWS = 100000;
263
+ await procedurePrepare(cgiObj, databaseConnection);
156
264
 
157
- const cgi = {
158
- keys: Object.keys(cgiObj),
159
- values: Object.values(cgiObj),
160
- };
265
+ //
266
+ // PROCEDURE EXECUTE
267
+ //
161
268
 
162
- const fileBlob = await databaseConnection.createLob(oracledb.BLOB);
269
+ await procedureExecute(para, databaseConnection);
163
270
 
164
- /** @type {BindParameterConfig} */
165
- const bind = {
166
- cgicount: {dir: oracledb.BIND_IN, type: oracledb.NUMBER, val: cgi.keys.length},
167
- cginames: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: cgi.keys},
168
- cgivalues: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: cgi.values},
169
- htbuflen: {dir: oracledb.BIND_IN, type: oracledb.NUMBER, val: HTBUF_LEN},
170
- fileExist: {dir: oracledb.BIND_OUT, type: oracledb.NUMBER},
171
- fileType: {dir: oracledb.BIND_OUT, type: oracledb.STRING},
172
- fileSize: {dir: oracledb.BIND_OUT, type: oracledb.NUMBER},
173
- fileBlob: {dir: oracledb.BIND_INOUT, type: oracledb.BLOB, val: fileBlob},
174
- lines: {dir: oracledb.BIND_OUT, type: oracledb.STRING, maxSize: HTBUF_LEN * 2, maxArraySize: MAX_IROWS},
175
- irows: {dir: oracledb.BIND_INOUT, type: oracledb.NUMBER, val: MAX_IROWS},
176
- };
177
-
178
- // execute procedure and retrieve page
179
- const sqlStatement = getProcedureSQL(para.sql);
180
- /** @type {Result | null} */
181
- let result = null;
182
- const bindParams = Object.assign({}, bind, para.bind);
183
- try {
184
- if (debug.enabled) {
185
- if (debug.enabled) {
186
- debug(getBlock('execute', sqlStatement));
187
- // NOTE: Because inspecting a BLOB value generates a craxy amount of text, we somply remove it.
188
- const temp = Object.assign({}, bindParams);
189
- delete temp.fileBlob.val;
190
- debug(getBlock('bindParams', inspect(temp)));
191
- }
192
- }
193
- result = await databaseConnection.execute(sqlStatement, bindParams);
194
- } catch (err) {
195
- if (debug.enabled) {
196
- debug(getBlock('results', inspect(result)));
197
- }
271
+ //
272
+ // PROCEDURE GET PAGE
273
+ //
198
274
 
199
- 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));
200
278
  }
201
279
 
202
280
  //
203
- // 4) PROCESS RESULTS
281
+ // PROCEDURE DOWNLOAD FILE
204
282
  //
205
283
 
206
- // validate results
207
- const data = z
208
- .object({
209
- irows: z.number(),
210
- lines: z.array(z.string()),
211
- fileExist: z.number(),
212
- fileType: z.string().nullable(),
213
- fileSize: z.number().nullable(),
214
- fileBlob: z.instanceof(stream.Readable).nullable(),
215
- })
216
- .parse(result.outBinds);
217
-
284
+ const fileBlob = await databaseConnection.createLob(oracledb.BLOB);
285
+ const fileDownload = await procedureDownloadFiles(fileBlob, databaseConnection);
218
286
  if (debug.enabled) {
219
- debug(getBlock('data', inspect(data)));
287
+ debug(getBlock('data', inspect(fileDownload)));
220
288
  }
221
289
 
222
- // Make sure that we have retrieved all the rows
223
- if (data.irows > MAX_IROWS) {
224
- /* istanbul ignore next */
225
- throw new ProcedureError(`Error when retrieving rows. irows="${data.irows}"`, cgiObj, para.sql, para.bind);
226
- }
227
-
228
- // combine page
229
- const pageContent = data.lines.join('');
230
-
231
290
  //
232
- // 6) PARSE PAGE
291
+ // PARSE PAGE
233
292
  //
234
293
 
235
294
  // parse what we received from PL/SQL
236
- const pageComponents = parsePage(pageContent);
295
+ const pageComponents = parsePage(lines);
237
296
 
238
297
  // add "Server" header
239
298
  pageComponents.head.server = cgiObj.SERVER_SOFTWARE;
240
299
 
241
300
  // add file download information
242
- if (data.fileExist === 1) {
243
- pageComponents.file.fileType = data.fileType;
244
- pageComponents.file.fileSize = data.fileSize;
245
- if (data.fileBlob) {
246
- pageComponents.file.fileBlob = await streamToBuffer(data.fileBlob);
247
- }
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);
248
305
  }
249
306
 
250
307
  //
251
- // 5) SEND THE RESPONSE
308
+ // SEND THE RESPONSE
252
309
  //
253
310
 
254
311
  sendResponse(req, res, pageComponents);
255
312
 
256
313
  //
257
- // 6) CLEANUP
314
+ // CLEANUP
258
315
  //
259
316
 
260
317
  fileBlob.destroy();
@@ -291,7 +291,7 @@ export const getProcedureNamed = async (req, procName, argObj, databaseConnectio
291
291
  }
292
292
 
293
293
  // select statement
294
- const sql = `${procName}(${sqlParameter.join(', ')});`;
294
+ const sql = `${procName}(${sqlParameter.join(', ')})`;
295
295
 
296
296
  if (debug.enabled) {
297
297
  debug(sql);
@@ -45,7 +45,7 @@ export const getProcedureVariable = (req, procName, argObj) => {
45
45
  }
46
46
 
47
47
  return {
48
- sql: `${procName}(:argnames, :argvalues);`,
48
+ sql: `${procName}(:argnames, :argvalues)`,
49
49
  bind: {
50
50
  argnames: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: names},
51
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.16.1';
5
+ export const getVersion = () => '0.17.0';