web_plsql 0.15.1 → 0.16.1

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.15.1",
3
+ "version": "0.16.1",
4
4
  "author": "Dieter Oberkofler <dieter.oberkofler@gmail.com>",
5
5
  "license": "MIT",
6
6
  "description": "The Express Middleware for Oracle PL/SQL",
@@ -60,8 +60,7 @@
60
60
  "compression": "1.8.1",
61
61
  "cookie-parser": "1.4.7",
62
62
  "debug": "4.4.3",
63
- "escape-html": "1.0.3",
64
- "express": "5.1.0",
63
+ "express": "5.2.1",
65
64
  "fs-extra": "11.3.2",
66
65
  "http-parser-js": "0.5.10",
67
66
  "morgan": "1.10.1",
@@ -74,19 +73,18 @@
74
73
  "@types/compression": "1.8.1",
75
74
  "@types/cookie-parser": "1.4.10",
76
75
  "@types/debug": "4.1.12",
77
- "@types/escape-html": "1.0.4",
78
76
  "@types/fs-extra": "11.0.4",
79
77
  "@types/morgan": "1.9.10",
80
78
  "@types/multer": "2.0.0",
81
- "@types/node": "24.10.1",
79
+ "@types/node": "24.10.2",
82
80
  "@types/oracledb": "6.10.0",
83
81
  "@types/supertest": "6.0.3",
84
82
  "eslint": "9.39.1",
85
- "eslint-plugin-jsdoc": "61.4.1",
86
- "prettier": "3.7.1",
83
+ "eslint-plugin-jsdoc": "61.5.0",
84
+ "prettier": "3.7.4",
87
85
  "shx": "0.4.0",
88
86
  "supertest": "7.1.4",
89
87
  "typescript": "5.9.3",
90
- "typescript-eslint": "8.48.0"
88
+ "typescript-eslint": "8.49.0"
91
89
  }
92
90
  }
@@ -2,12 +2,11 @@
2
2
  * Error handling
3
3
  */
4
4
 
5
- import util from 'node:util';
6
- import escape from 'escape-html';
7
5
  import {ProcedureError} from './procedureError.js';
8
6
  import {RequestError} from './requestError.js';
9
- import {inspectRequest, logToFile} from '../../util/trace.js';
7
+ import {getFormattedMessage, logToFile} from '../../util/trace.js';
10
8
  import {errorToString} from '../../util/errorToString.js';
9
+ import {getHtmlPage} from '../../util/html.js';
11
10
 
12
11
  /**
13
12
  * @typedef {import('express').Request} Request
@@ -18,86 +17,6 @@ import {errorToString} from '../../util/errorToString.js';
18
17
  * @typedef {{html: string; text: string}} outputType
19
18
  */
20
19
 
21
- /**
22
- * Convert value to string
23
- * @param {unknown} value - The value to convert.
24
- * @returns {string} - The string representation of the value.
25
- */
26
- const inspect = (value) => util.inspect(value, {showHidden: false, depth: null, colors: false});
27
-
28
- /**
29
- * Convert LF and/or CR to <br>
30
- * @param {string} text - The text to convert.
31
- * @returns {string} - The converted text.
32
- */
33
- const convertToHtml = (text) => {
34
- let html = escape(text);
35
-
36
- html = html.replace(/(?:\r\n|\r|\n)/g, '<br />');
37
- html = html.replace(/\t/g, '&nbsp;&nbsp;&nbsp;');
38
-
39
- return html;
40
- };
41
-
42
- /**
43
- * Get html
44
- * @param {string} text - The text to convert.
45
- * @returns {string} - The converted text.
46
- */
47
- const getHtml = (text) => `<p>${convertToHtml(text)}</p>`;
48
-
49
- /**
50
- * Get text header
51
- * @param {string} text - The text to convert.
52
- * @returns {string} - The converted text.
53
- */
54
- const getHeaderText = (text) => `\n${text}\n${'='.repeat(text.length)}\n`;
55
-
56
- /**
57
- * Get html header
58
- * @param {string} text - The text to convert.
59
- * @returns {string} - The converted text.
60
- */
61
- const getHeaderHtml = (text) => `<h1>${text}</h1>`;
62
-
63
- /**
64
- * Get text
65
- * @param {string} text - The text to convert.
66
- * @returns {string} - The converted text.
67
- */
68
- const getText = (text) => `${text}\n`;
69
-
70
- /**
71
- * Get evnironment
72
- * @param {outputType} output - The output.
73
- * @param {environmentType} environment - The environment.
74
- */
75
- const getEnvironment = (output, environment) => {
76
- let html = '<table>';
77
- let text = '';
78
-
79
- try {
80
- for (const key in environment) {
81
- html += `<tr><td>${key}:</td><td>${environment[key]}</td></tr>`;
82
- text += `${key}=${environment[key]}\n`;
83
- }
84
- } catch (err) {
85
- /* istanbul ignore next */
86
- output.html += errorToString(err);
87
-
88
- /* istanbul ignore next */
89
- output.text += errorToString(err);
90
-
91
- /* istanbul ignore next */
92
- return;
93
- }
94
-
95
- html += '</table>';
96
-
97
- output.html += html;
98
- output.text += text;
99
- };
100
-
101
20
  /**
102
21
  * Show an error page
103
22
  * @param {Request} req - The req object represents the HTTP request.
@@ -111,8 +30,8 @@ const getError = (req, error) => {
111
30
  let environment = null;
112
31
  /** @type {string | null} */
113
32
  let sql = null;
114
- /** @type {BindParameterConfig} */
115
- let bind = {};
33
+ /** @type {BindParameterConfig | null} */
34
+ let bind = null;
116
35
 
117
36
  // what type of Error did we receive
118
37
  if (error instanceof ProcedureError) {
@@ -143,113 +62,9 @@ const getError = (req, error) => {
143
62
  }
144
63
  }
145
64
 
146
- /** @type {outputType} */
147
- const output = {
148
- html: '',
149
- text: '',
150
- };
151
-
152
- // timestamp
153
- let header = 'TIMESTAMP';
154
- output.html += getHeaderHtml(header);
155
- output.html += getHtml(timestamp.toUTCString());
156
-
157
- // error
158
- header = 'ERROR';
159
- output.html += getHeaderHtml(header);
160
- output.html += getHtml(message);
161
- output.text += getHeaderText(header);
162
- output.text += getText(message);
163
-
164
- // request
165
- header = 'REQUEST';
166
- output.text += getHeaderText(header);
167
- output.text += getText(inspectRequest(req));
168
-
169
- // parameters
170
- if (typeof sql === 'string' && bind) {
171
- header = 'PROCEDURE';
172
- output.html += getHeaderHtml(header);
173
- output.text += getHeaderText(header);
174
- getProcedure(output, sql, bind);
175
- }
176
-
177
- // environment
178
- if (environment) {
179
- header = 'ENVIRONMENT';
180
- output.html += getHeaderHtml(header);
181
- output.text += getHeaderText(header);
182
- getEnvironment(output, environment);
183
- }
184
-
185
- return output;
65
+ return getFormattedMessage({type: 'error', timestamp, message, req, environment, sql, bind});
186
66
  };
187
67
 
188
- /**
189
- * Get procedure
190
- * @param {outputType} output - The output.
191
- * @param {string} sql - The SQL to execute.
192
- * @param {BindParameterConfig} bind - The bind parameters.
193
- */
194
- const getProcedure = (output, sql, bind) => {
195
- let html = '<table>';
196
- let text = '';
197
-
198
- text += `PROCEDURE: ${sql}\n`;
199
- html += `<tr><td>PROCEDURE:</td><td>${sql}</td></tr>`;
200
-
201
- try {
202
- /* istanbul ignore else */
203
- for (const key in bind) {
204
- const value = inspect(bind[key].val);
205
-
206
- html += `<tr><td>${key}:</td><td>${value}</td></tr>`;
207
- text += `${key}: ${value}\n`;
208
- }
209
- } catch (err) {
210
- /* istanbul ignore next */
211
- output.html += err instanceof Error ? err.toString() : 'ERROR';
212
-
213
- /* istanbul ignore next */
214
- output.text += err instanceof Error ? err.toString() : 'ERROR';
215
- /* istanbul ignore next */
216
- return;
217
- }
218
-
219
- html += '</table>';
220
-
221
- output.html += html;
222
- output.text += text;
223
- };
224
-
225
- /**
226
- * getHtmlPage
227
- * @param {string} body - The body.
228
- * @returns {string} - The html page.
229
- */
230
- const getHtmlPage = (body) => `<!DOCTYPE html>
231
- <html lang="en">
232
- <head>
233
- <meta charset="utf-8">
234
- <title>web_plsql error page</title>
235
- <style type="text/css">
236
- html {
237
- font-family: monospace, sans-serif;
238
- font-size: 12px;
239
- }
240
- h1 {
241
- font-size: 16px;
242
- padding: 2px;
243
- background-color: #cc0000;
244
- }
245
- </style>
246
- </head>
247
- <body>
248
- ${body}
249
- </body>
250
- </html>
251
- `;
252
-
253
68
  /**
254
69
  * Show an error page
255
70
  *
@@ -259,38 +74,19 @@ ${body}
259
74
  * @param {unknown} error - The error.
260
75
  */
261
76
  export const errorPage = (req, res, options, error) => {
262
- let output = {
263
- html: '',
264
- text: '',
265
- };
266
-
267
- // get the error description
268
- try {
269
- output = getError(req, error);
270
- } catch (err) {
271
- /* istanbul ignore next */
272
- const header = 'ERROR';
273
-
274
- /* istanbul ignore next */
275
- const message = errorToString(err);
276
-
277
- /* istanbul ignore next */
278
- output.html += getHeaderHtml(header) + getHtml(message);
279
-
280
- /* istanbul ignore next */
281
- output.text += getHeaderHtml(header) + getHtml(message);
282
- }
77
+ // get error message
78
+ const {html, text} = getError(req, error);
283
79
 
284
80
  // trace to file
285
- logToFile(output.text);
81
+ logToFile(text);
286
82
 
287
83
  // console
288
- console.error(output.text);
84
+ console.error(text);
289
85
 
290
86
  // show page
291
87
  if (options.errorStyle === 'basic') {
292
88
  res.status(404).send('Page not found');
293
89
  } else {
294
- res.status(404).send(getHtmlPage(output.html));
90
+ res.status(404).send(getHtmlPage(html));
295
91
  }
296
92
  };
@@ -37,7 +37,6 @@ const requestHandler = async (req, res, next, connectionPool, options) => {
37
37
  debug(`Redirect to the url "${newUrl}"`);
38
38
  res.redirect(newUrl);
39
39
  } else {
40
- /* istanbul ignore next */
41
40
  errorPage(req, res, options, new RequestError('No procedure name given and no default page has been specified'));
42
41
  }
43
42
  } else {
@@ -45,7 +44,6 @@ const requestHandler = async (req, res, next, connectionPool, options) => {
45
44
  await processRequest(req, res, options, connectionPool);
46
45
  }
47
46
  } catch (err) {
48
- /* istanbul ignore next */
49
47
  errorPage(req, res, options, err);
50
48
  }
51
49
  };
@@ -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
@@ -75,13 +76,15 @@ END;
75
76
 
76
77
  /**
77
78
  * Get the procedure and arguments to execute
79
+ * @param {Request} req - The req object represents the HTTP request. (only used for debugging)
78
80
  * @param {string} procName - The procedure to execute
79
81
  * @param {argObjType} argObj - The arguments to pass to the procedure
80
82
  * @param {configPlSqlHandlerType} options - The options for the middleware
81
83
  * @param {Connection} databaseConnection - The database connection
82
84
  * @returns {Promise<{sql: string; bind: BindParameterConfig}>} - The SQL statement and bindings for the procedure to execute
83
85
  */
84
- const getProcedure = async (procName, argObj, options, databaseConnection) => {
86
+ const getProcedure = async (req, procName, argObj, options, databaseConnection) => {
87
+ // path alias
85
88
  if (options.pathAlias && options.pathAlias.toLowerCase() === procName.toLowerCase()) {
86
89
  debug(`getProcedure: path alias "${options.pathAlias}" redirects to "${options.pathAliasProcedure}"`);
87
90
  return {
@@ -90,11 +93,20 @@ const getProcedure = async (procName, argObj, options, databaseConnection) => {
90
93
  p_path: {dir: oracledb.BIND_IN, type: oracledb.STRING, val: procName},
91
94
  },
92
95
  };
93
- } else if (procName.startsWith('!')) {
94
- return await getProcedureVariable(procName.substring(1), argObj, databaseConnection, options);
95
96
  }
96
97
 
97
- return await getProcedureNamed(procName, argObj, databaseConnection, options);
98
+ // check if we use variable arguments
99
+ const useVariableArguments = procName.startsWith('!');
100
+
101
+ // sanitize procedure name
102
+ const sanitizedProcName = await sanitizeProcName(useVariableArguments ? procName.substring(1) : procName, databaseConnection, options);
103
+
104
+ // run procedure
105
+ if (useVariableArguments) {
106
+ return getProcedureVariable(req, sanitizedProcName, argObj);
107
+ } else {
108
+ return await getProcedureNamed(req, sanitizedProcName, argObj, databaseConnection);
109
+ }
98
110
  };
99
111
 
100
112
  /**
@@ -133,7 +145,7 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
133
145
  // 2) GET SQL STATEMENT AND ARGUMENTS
134
146
  //
135
147
 
136
- const para = await getProcedure(procName, argObj, options, databaseConnection);
148
+ const para = await getProcedure(req, procName, argObj, options, databaseConnection);
137
149
 
138
150
  //
139
151
  // 3) EXECUTE PROCEDURE
@@ -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.16.1';
@@ -0,0 +1,53 @@
1
+ /*
2
+ * Html utilities
3
+ */
4
+
5
+ /**
6
+ * Escape html string.
7
+ *
8
+ * @param {string} value - The value.
9
+ * @returns {string} - The escaped value.
10
+ */
11
+ export const escapeHtml = (value) => value.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
12
+
13
+ /**
14
+ * Convert LF and/or CR to <br>
15
+ * @param {string} text - The text to convert.
16
+ * @returns {string} - The converted text.
17
+ */
18
+ export const convertAsciiToHtml = (text) => {
19
+ let html = escapeHtml(text);
20
+
21
+ html = html.replace(/(?:\r\n|\r|\n)/g, '<br />');
22
+ html = html.replace(/\t/g, '&nbsp;&nbsp;&nbsp;');
23
+
24
+ return html;
25
+ };
26
+
27
+ /**
28
+ * get a minimal html page.
29
+ * @param {string} body - The body.
30
+ * @returns {string} - The html page.
31
+ */
32
+ export const getHtmlPage = (body) => `<!DOCTYPE html>
33
+ <html lang="en">
34
+ <head>
35
+ <meta charset="utf-8">
36
+ <title>web_plsql error page</title>
37
+ <style type="text/css">
38
+ html {
39
+ font-family: monospace, sans-serif;
40
+ font-size: 12px;
41
+ }
42
+ h1 {
43
+ font-size: 16px;
44
+ padding: 2px;
45
+ background-color: #cc0000;
46
+ }
47
+ </style>
48
+ </head>
49
+ <body>
50
+ ${body}
51
+ </body>
52
+ </html>
53
+ `;
package/src/util/trace.js CHANGED
@@ -5,6 +5,20 @@
5
5
  import * as rotatingFileStream from 'rotating-file-stream';
6
6
  import express from 'express';
7
7
  import util from 'node:util';
8
+ import oracledb from 'oracledb';
9
+ import {escapeHtml, convertAsciiToHtml} from './html.js';
10
+ import {errorToString} from './errorToString.js';
11
+
12
+ /**
13
+ * @typedef {import('express').Request} Request
14
+ * @typedef {import('../types.js').BindParameterConfig} BindParameterConfig
15
+ * @typedef {import('../types.js').environmentType} environmentType
16
+ * @typedef {{html: string; text: string}} outputType
17
+ * @typedef {{type: 'error' | 'warning' | 'trace'; message: string; timestamp?: Date | null; req?: Request | null; environment?: environmentType | null, sql?: string | null; bind?: BindParameterConfig | null}} messageType
18
+ */
19
+
20
+ const SEPARATOR_H1 = '='.repeat(100);
21
+ const SEPARATOR_H2 = '-'.repeat(30);
8
22
 
9
23
  /**
10
24
  * Return a string representation of the value.
@@ -13,7 +27,58 @@ import util from 'node:util';
13
27
  * @param {number | null} depth - Specifies the number of times to recurse while formatting object.
14
28
  * @returns {string} - The string representation.
15
29
  */
16
- export const inspect = (value, depth = null) => util.inspect(value, {showHidden: false, depth, colors: false});
30
+ export const inspect = (value, depth = null) => {
31
+ try {
32
+ return util.inspect(value, {showHidden: false, depth, colors: false});
33
+ } catch (err) {
34
+ /* empty */
35
+ }
36
+
37
+ try {
38
+ return JSON.stringify(value);
39
+ } catch (err) {
40
+ /* empty */
41
+ }
42
+
43
+ return 'Unable to convert value to string';
44
+ };
45
+
46
+ /**
47
+ * Return a tabular representation of the values.
48
+ *
49
+ * @param {string[]} head - The header values.
50
+ * @param {string[][]} body - The row values.
51
+ * @returns {outputType} - The output.
52
+ */
53
+ export const toTable = (head, body) => {
54
+ if (head.length === 0) {
55
+ throw new Error('head cannot be empty');
56
+ }
57
+
58
+ // Calculate column widths
59
+ const widths = head.map((h, i) => {
60
+ const bodyMax = Math.max(0, ...body.map((row) => (row[i] || '').length));
61
+ return Math.max(h.length, bodyMax);
62
+ });
63
+
64
+ // Build text representation
65
+ /**
66
+ * @param {string} cell - The string
67
+ * @param {number} width - The width
68
+ * @returns {string} - The result
69
+ */
70
+ const padCell = (cell, width) => cell.padEnd(width, ' ');
71
+ const textHeader = head.map((h, i) => padCell(h, widths[i])).join(' | ');
72
+ const textSeparator = widths.map((w) => '-'.repeat(w)).join('-+-');
73
+ const textRows = body.map((row) => head.map((_, i) => padCell(row[i] || '', widths[i])).join(' | '));
74
+ const text = [textHeader, textSeparator, ...textRows].join('\n');
75
+
76
+ const htmlHead = head.map((h) => `<th>${escapeHtml(h)}</th>`).join('');
77
+ const htmlBody = body.map((row) => `<tr>${head.map((_, i) => `<td>${escapeHtml(row[i] || '')}</td>`).join('')}</tr>`).join('');
78
+ const html = `<table><thead><tr>${htmlHead}</tr></thead><tbody>${htmlBody}</tbody></table>`;
79
+
80
+ return {text, html};
81
+ };
17
82
 
18
83
  /**
19
84
  * Log text to the console and to a file.
@@ -40,7 +105,7 @@ export const logToFile = (text) => {
40
105
  * @param {boolean} simple - Set to false to see all public properties of the request.
41
106
  * @returns {string} - The string representation.
42
107
  */
43
- export const inspectRequest = (req, simple = true) => {
108
+ const inspectRequest = (req, simple = true) => {
44
109
  /** @type {Record<string, unknown>} */
45
110
  const requestData = {};
46
111
 
@@ -55,20 +120,206 @@ export const inspectRequest = (req, simple = true) => {
55
120
  return inspect(requestData);
56
121
  };
57
122
 
123
+ /**
124
+ * Return a string representation of the bind parameter.
125
+ * @param {number | undefined} dir - The direction.
126
+ * @returns {string} The string.
127
+ */
128
+ const dirToString = (dir) => {
129
+ switch (dir) {
130
+ case oracledb.BIND_IN:
131
+ return 'IN';
132
+ case oracledb.BIND_OUT:
133
+ return 'OUT';
134
+ case oracledb.BIND_INOUT:
135
+ return 'INOUT';
136
+ default:
137
+ return '';
138
+ }
139
+ };
140
+
141
+ /**
142
+ * Return a string representation of the bind type.
143
+ * @param {oracledb.DbType | string | number | undefined} type - The type.
144
+ * @returns {string} The string.
145
+ */
146
+ const bindTypeToString = (type) => {
147
+ if (typeof type === 'object' && 'name' in type) {
148
+ return type.name;
149
+ }
150
+
151
+ if (typeof type === 'string') {
152
+ return type;
153
+ }
154
+
155
+ if (typeof type === 'number') {
156
+ return type.toString();
157
+ }
158
+
159
+ return '';
160
+ };
161
+
162
+ /**
163
+ * Return a string representation of the bind parameter.
164
+ * @param {outputType} output - The output.
165
+ * @param {BindParameterConfig} bind - The bind parameters.
166
+ * @returns {undefined}
167
+ */
168
+ const inspectBindParameter = (output, bind) => {
169
+ const rows = Object.entries(bind);
170
+
171
+ if (rows.length === 0) {
172
+ return;
173
+ }
174
+
175
+ const body = rows.map(([id, row]) => {
176
+ const dir = dirToString(row.dir);
177
+ const maxArraySize = row.maxArraySize ? row.maxArraySize.toString() : '';
178
+ const maxSize = row.maxSize ? row.maxSize.toString() : '';
179
+ const bindType = bindTypeToString(row.type);
180
+ const value = inspect(row.val);
181
+ const valueType = typeof row.val;
182
+
183
+ return [id, dir, maxArraySize, maxSize, bindType, value, valueType];
184
+ });
185
+
186
+ const {html, text} = toTable(['id', 'dir', 'maxArraySize', 'maxSize', 'bind type', 'value', 'value type'], body);
187
+
188
+ output.html += html;
189
+ output.text += text;
190
+ };
191
+
192
+ /**
193
+ * Add environment
194
+ * @param {outputType} output - The output.
195
+ * @param {environmentType} environment - The environment.
196
+ */
197
+ const inspectEnvironment = (output, environment) => {
198
+ const rows = Object.entries(environment);
199
+
200
+ if (rows.length === 0) {
201
+ return;
202
+ }
203
+
204
+ const {html, text} = toTable(['key', 'value'], rows);
205
+
206
+ output.html += html;
207
+ output.text += text;
208
+ };
209
+
58
210
  /**
59
211
  * Get a block.
60
212
  * @param {string} title - The name.
61
213
  * @param {string} body - The name.
62
214
  * @returns {string} - The text.
63
215
  */
64
- export const getBlock = (title, body) => {
65
- const SEPARATOR = '-'.repeat(30);
216
+ export const getBlock = (title, body) => `\n${SEPARATOR_H2}${title.toUpperCase()}${SEPARATOR_H2}\n${body}`;
217
+
218
+ /**
219
+ * Get line html
220
+ * @param {string} text - The text.
221
+ * @returns {string} - The line.
222
+ */
223
+ const getLineHtml = (text) => `<p>${convertAsciiToHtml(text)}</p>`;
224
+
225
+ /**
226
+ * Get line text
227
+ * @param {string} text - The text.
228
+ * @returns {string} - The line.
229
+ */
230
+ const getLineText = (text) => `${text}\n`;
231
+
232
+ /**
233
+ * Add line
234
+ * @param {outputType} output - The output.
235
+ * @param {string} text - The text to convert.
236
+ */
237
+ const addLine = (output, text) => {
238
+ output.html += getLineHtml(text);
239
+ output.text += getLineText(text);
240
+ };
241
+
242
+ /**
243
+ * Add header
244
+ * @param {outputType} output - The output.
245
+ * @param {string} text - The text to convert.
246
+ */
247
+ const addHeader = (output, text) => {
248
+ output.html += `<h2>${text}</h2>`;
249
+ output.text += `\n${text}\n${'-'.repeat(text.length)}\n`;
250
+ };
251
+
252
+ /**
253
+ * Add procedure
254
+ * @param {outputType} output - The output.
255
+ * @param {string} sql - The SQL to execute.
256
+ * @param {BindParameterConfig} bind - The bind parameters.
257
+ */
258
+ const addProcedure = (output, sql, bind) => {
259
+ output.html += `${sql}<br><br>`;
260
+ output.text += `${sql}\n\n`;
66
261
 
67
- return `\n${SEPARATOR}${title.toUpperCase()}${SEPARATOR}\n${body}`;
262
+ try {
263
+ inspectBindParameter(output, bind);
264
+ } catch (err) {
265
+ addLine(output, `Unable to inspect bind parameter: ${errorToString(err)}`);
266
+ }
267
+
268
+ output.html += `<br>`;
269
+ output.text += `\n`;
68
270
  };
69
271
 
70
272
  /**
71
- * Get a timestamp
72
- * @returns {string} - The timestamp.
273
+ * Get a formatted message.
274
+ * @param {messageType} para - The req object represents the HTTP request.
275
+ * @returns {outputType} - The output.
73
276
  */
74
- export const getTimestamp = () => new Date().toISOString();
277
+ export const getFormattedMessage = (para) => {
278
+ const timestamp = para.timestamp ?? new Date();
279
+
280
+ // header
281
+ const url = typeof para.req?.originalUrl === 'string' && para.req.originalUrl.length > 0 ? ` on ${para.req.originalUrl}` : '';
282
+ const header = `${para.type.toUpperCase()} at ${timestamp.toUTCString()}${url}`;
283
+ const output = {
284
+ html: `<h1>${header}</h1>`,
285
+ text: `\n\n${SEPARATOR_H1}\n== ${header}\n${SEPARATOR_H1}\n`,
286
+ };
287
+
288
+ // error
289
+ addHeader(output, 'ERROR');
290
+ addLine(output, para.message);
291
+
292
+ // request
293
+ if (para.req) {
294
+ addHeader(output, 'REQUEST');
295
+ addLine(output, inspectRequest(para.req));
296
+ }
297
+
298
+ // parameters
299
+ if (para.sql && para.bind) {
300
+ addHeader(output, 'PROCEDURE');
301
+ addProcedure(output, para.sql, para.bind);
302
+ }
303
+
304
+ // environment
305
+ if (para.environment) {
306
+ addHeader(output, 'ENVIRONMENT');
307
+ inspectEnvironment(output, para.environment);
308
+ }
309
+
310
+ return output;
311
+ };
312
+
313
+ /**
314
+ * Log a warning message.
315
+ * @param {messageType} para - The req object represents the HTTP request.
316
+ */
317
+ export const warningMessage = (para) => {
318
+ const {text} = getFormattedMessage(para);
319
+
320
+ // trace to file
321
+ logToFile(text);
322
+
323
+ // console
324
+ console.warn(text);
325
+ };
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Get duration as human readable string.
3
+ * @param {number} duration - Milliseconds.
4
+ * @returns {string} String.
5
+ */
6
+ export const humanDuration = (duration) => {
7
+ if (!Number.isFinite(duration)) return 'invalid';
8
+
9
+ const ms = Math.floor(duration % 1000);
10
+ const s = Math.floor((duration / 1000) % 60);
11
+ const m = Math.floor((duration / (1000 * 60)) % 60);
12
+ const h = Math.floor((duration / (1000 * 60 * 60)) % 24);
13
+ const d = Math.floor(duration / (1000 * 60 * 60 * 24));
14
+
15
+ const parts = [];
16
+ if (d) parts.push(`${d}d`);
17
+ if (h) parts.push(`${h}h`);
18
+ if (m) parts.push(`${m}m`);
19
+ if (s) parts.push(`${s}s`);
20
+ if (ms || !parts.length) parts.push(`${ms}ms`);
21
+
22
+ return parts.join(' ');
23
+ };
24
+
25
+ /**
26
+ * Convert a string to a number
27
+ *
28
+ * @param {unknown} value - The string to convert
29
+ * @returns {number | null} The number or null if the string could not be converted
30
+ */
31
+ export const stringToNumber = (value) => {
32
+ // is the value already of type number?
33
+ if (typeof value === 'number') {
34
+ return !Number.isNaN(value) && Number.isFinite(value) ? value : null;
35
+ }
36
+
37
+ // Test for invalid characters
38
+ if (typeof value !== 'string' || !/^[+-]?(?:\d+(?:\.\d+)?|\.\d+)(?:E[+-]?\d+)?$/i.test(value)) {
39
+ return null;
40
+ }
41
+
42
+ // Convert value to a number
43
+ const num = Number(value);
44
+ return Number.isNaN(num) ? null : num;
45
+ };
46
+
47
+ /**
48
+ * Convert a string to a integer
49
+ *
50
+ * @param {unknown} value - The value to convert
51
+ * @returns {number | null} The integer or null if the string could not be converted
52
+ */
53
+ export const stringToInteger = (value) => {
54
+ // is the value already a "real" integer, we just return the value
55
+ if (typeof value === 'number' && Number.isInteger(value)) {
56
+ return value;
57
+ }
58
+
59
+ // try to convert value to a number
60
+ const num = stringToNumber(value);
61
+ if (num === null || !Number.isInteger(num)) {
62
+ return null;
63
+ }
64
+
65
+ return num;
66
+ };
@@ -1,14 +1,17 @@
1
- export function getProcedureNamed(procName: string, argObj: argObjType, databaseConnection: Connection, options: configPlSqlHandlerType): Promise<{
1
+ export function getBinding(argName: string, argValue: unknown, argType: string): BindParameter;
2
+ export function getProcedureNamed(req: Request, procName: string, argObj: argObjType, databaseConnection: Connection): Promise<{
2
3
  sql: string;
3
4
  bind: BindParameterConfig;
4
5
  }>;
5
- export type argsType = Record<string, string>;
6
- export type cacheEntryType = {
7
- hitCount: number;
8
- args: argsType;
9
- };
6
+ export type Request = import("express").Request;
10
7
  export type Connection = import("oracledb").Connection;
11
8
  export type Result = import("oracledb").Result<unknown>;
12
9
  export type configPlSqlHandlerType = import("../../types.js").configPlSqlHandlerType;
13
10
  export type argObjType = import("../../types.js").argObjType;
14
11
  export type BindParameterConfig = import("../../types.js").BindParameterConfig;
12
+ export type BindParameter = import("../../types.js").BindParameter;
13
+ export type argsType = Record<string, string>;
14
+ export type cacheEntryType = {
15
+ hitCount: number;
16
+ args: argsType;
17
+ };
@@ -1,7 +1,8 @@
1
- export function getProcedureVariable(procName: string, argObj: argObjType, databaseConnection: Connection, options: configPlSqlHandlerType): Promise<{
1
+ export function getProcedureVariable(req: Request, procName: string, argObj: argObjType): {
2
2
  sql: string;
3
3
  bind: BindParameterConfig;
4
- }>;
4
+ };
5
+ export type Request = import("express").Request;
5
6
  export type Connection = import("oracledb").Connection;
6
7
  export type Result = import("oracledb").Result<unknown>;
7
8
  export type configPlSqlHandlerType = import("../../types.js").configPlSqlHandlerType;
@@ -0,0 +1,3 @@
1
+ export function escapeHtml(value: string): string;
2
+ export function convertAsciiToHtml(text: string): string;
3
+ export function getHtmlPage(body: string): string;
@@ -1,6 +1,22 @@
1
1
  export function inspect(value: unknown, depth?: number | null): string;
2
+ export function toTable(head: string[], body: string[][]): outputType;
2
3
  export function logToFile(text: string): void;
3
- export function inspectRequest(req: express.Request, simple?: boolean): string;
4
4
  export function getBlock(title: string, body: string): string;
5
- export function getTimestamp(): string;
6
- import express from 'express';
5
+ export function getFormattedMessage(para: messageType): outputType;
6
+ export function warningMessage(para: messageType): void;
7
+ export type Request = import("express").Request;
8
+ export type BindParameterConfig = import("../types.js").BindParameterConfig;
9
+ export type environmentType = import("../types.js").environmentType;
10
+ export type outputType = {
11
+ html: string;
12
+ text: string;
13
+ };
14
+ export type messageType = {
15
+ type: "error" | "warning" | "trace";
16
+ message: string;
17
+ timestamp?: Date | null;
18
+ req?: Request | null;
19
+ environment?: environmentType | null;
20
+ sql?: string | null;
21
+ bind?: BindParameterConfig | null;
22
+ };
@@ -0,0 +1,3 @@
1
+ export function humanDuration(duration: number): string;
2
+ export function stringToNumber(value: unknown): number | null;
3
+ export function stringToInteger(value: unknown): number | null;
package/src/util/date.js DELETED
@@ -1,23 +0,0 @@
1
- /**
2
- * Get duration as human readable string.
3
- * @param {number} duration - Milliseconds.
4
- * @returns {string} String.
5
- */
6
- export const humanDuration = (duration) => {
7
- if (!Number.isFinite(duration)) return 'invalid';
8
-
9
- const ms = Math.floor(duration % 1000);
10
- const s = Math.floor((duration / 1000) % 60);
11
- const m = Math.floor((duration / (1000 * 60)) % 60);
12
- const h = Math.floor((duration / (1000 * 60 * 60)) % 24);
13
- const d = Math.floor(duration / (1000 * 60 * 60 * 24));
14
-
15
- const parts = [];
16
- if (d) parts.push(`${d}d`);
17
- if (h) parts.push(`${h}h`);
18
- if (m) parts.push(`${m}m`);
19
- if (s) parts.push(`${s}s`);
20
- if (ms || !parts.length) parts.push(`${ms}ms`);
21
-
22
- return parts.join(' ');
23
- };