web_plsql 0.17.1 → 0.18.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.17.1",
3
+ "version": "0.18.0",
4
4
  "author": "Dieter Oberkofler <dieter.oberkofler@gmail.com>",
5
5
  "license": "MIT",
6
6
  "description": "The Express Middleware for Oracle PL/SQL",
@@ -44,10 +44,11 @@
44
44
  "type": "module",
45
45
  "scripts": {
46
46
  "lint": "prettier --check --experimental-cli . && eslint . && tsc --noEmit",
47
- "test": "node --test --test-concurrency=1 \"./tests/**/*.test.js\"",
47
+ "test": "vitest run",
48
+ "test:coverage": "vitest run --coverage",
48
49
  "types": "rm -rf types && tsc --noEmit false --declaration true --declarationDir types --emitDeclarationOnly true --allowJs true --target es2020 --module es2020 --moduleResolution node --allowSyntheticDefaultImports true src/*.js",
49
50
  "clean": "shx rm -f *.tgz && shx rm -f *.log",
50
- "ci": "npm run clean && npm run version && npm run lint && npm run test && npm run types",
51
+ "ci": "npm run clean && npm run version && npm run types && npm run lint && npm run test:coverage",
51
52
  "version": "node update-version.js",
52
53
  "create-package": "shx rm -f *.tgz && npm pack",
53
54
  "image-build": "docker build --no-cache --progress=plain --tag=web_plsql .",
@@ -73,16 +74,18 @@
73
74
  "@types/debug": "4.1.12",
74
75
  "@types/morgan": "1.9.10",
75
76
  "@types/multer": "2.0.0",
76
- "@types/node": "25.2.0",
77
+ "@types/node": "25.2.1",
77
78
  "@types/oracledb": "6.10.1",
78
79
  "@types/supertest": "6.0.3",
80
+ "@vitest/coverage-v8": "^4.0.18",
79
81
  "eslint": "9.39.2",
80
- "eslint-plugin-jsdoc": "62.5.1",
82
+ "eslint-plugin-jsdoc": "62.5.2",
81
83
  "oracledb": "6.10.0",
82
84
  "prettier": "3.8.1",
83
85
  "shx": "0.4.0",
84
86
  "supertest": "7.2.2",
85
87
  "typescript": "5.9.3",
86
- "typescript-eslint": "8.54.0"
88
+ "typescript-eslint": "8.54.0",
89
+ "vitest": "^4.0.18"
87
90
  }
88
91
  }
@@ -36,30 +36,26 @@ const getError = (req, error) => {
36
36
  // what type of Error did we receive
37
37
  if (error instanceof ProcedureError) {
38
38
  timestamp = error.timestamp;
39
- /* istanbul ignore next */
40
39
  message = error.stack ?? '';
41
40
  environment = error.environment;
42
41
  sql = error.sql;
43
42
  bind = error.bind;
44
43
  } else if (error instanceof RequestError) {
45
44
  timestamp = error.timestamp;
46
- /* istanbul ignore next */
47
45
  message = error.stack ?? '';
48
46
  } else if (error instanceof Error) {
49
- /* istanbul ignore next */
50
47
  message = errorToString(error);
51
48
  } else {
52
49
  if (typeof error === 'string') {
53
- /* istanbul ignore next */
54
50
  message = `${error}\n`;
55
51
  }
52
+ /* v8 ignore start - unreachable code: creating Error without throwing */
56
53
  try {
57
- /* istanbul ignore next */
58
54
  new Error();
59
55
  } catch (err) {
60
- /* istanbul ignore next */
61
56
  message += errorToString(err);
62
57
  }
58
+ /* v8 ignore stop */
63
59
  }
64
60
 
65
61
  return getFormattedMessage({type: 'error', timestamp, message, req, environment, sql, bind});
@@ -55,7 +55,7 @@ export const parsePage = (text) => {
55
55
  {
56
56
  const cookie = parseCookie(header.value);
57
57
  debug(`oracle header "set-cookie" with value "${header.value}" was received has been parsed to ${JSON.stringify(cookie)}`);
58
- /* istanbul ignore else */
58
+ /* v8 ignore else - parseCookie validation ensures non-null */
59
59
  if (cookie !== null) {
60
60
  page.head.cookies.push(cookie);
61
61
  } else {
@@ -72,7 +72,7 @@ export const parsePage = (text) => {
72
72
  case 'x-db-content-length':
73
73
  {
74
74
  const contentLength = parseInt(header.value, 10);
75
- /* istanbul ignore else */
75
+ /* v8 ignore else - parseInt validation */
76
76
  if (!Number.isNaN(contentLength)) {
77
77
  page.head.contentLength = contentLength;
78
78
  debug(`oracle header "x-db-content-length" with value "${page.head.contentLength}" was parsed`);
@@ -85,12 +85,12 @@ export const parsePage = (text) => {
85
85
  case 'status':
86
86
  {
87
87
  const statusCode = parseInt(header.value, 10);
88
- /* istanbul ignore else */
88
+ /* v8 ignore else - parseInt validation */
89
89
  if (!Number.isNaN(statusCode)) {
90
90
  page.head.statusCode = statusCode;
91
91
  debug(`oracle header "status" with value "${page.head.statusCode}" was parsed`);
92
92
  const index = header.value.indexOf(' ');
93
- /* istanbul ignore else */
93
+ /* v8 ignore else - status code may not have description */
94
94
  if (index !== -1) {
95
95
  page.head.statusDescription = header.value.substring(index + 1);
96
96
  }
@@ -143,7 +143,7 @@ const getHeader = (line) => {
143
143
  */
144
144
  const parseCookie = (text) => {
145
145
  // validate
146
- /* istanbul ignore next */
146
+ /* v8 ignore next - input validation */
147
147
  if (typeof text !== 'string' || text.trim().length === 0) {
148
148
  return null;
149
149
  }
@@ -156,7 +156,7 @@ const parseCookie = (text) => {
156
156
 
157
157
  // get name and value
158
158
  const index = cookieElements[0].indexOf('=');
159
- /* istanbul ignore next */
159
+ /* v8 ignore next - cookie format validation */
160
160
  if (index <= 0) {
161
161
  // if the index is -1, there is no equal sign and if it's 0 the name is empty
162
162
  return null;
@@ -179,7 +179,7 @@ const parseCookie = (text) => {
179
179
  } else if (element.toLowerCase().startsWith('domain=')) {
180
180
  cookie.options.domain = element.substring(7);
181
181
  } else if (element.toLowerCase().startsWith('secure=')) {
182
- /* istanbul ignore next */
182
+ /* v8 ignore next - secure cookie attribute */
183
183
  cookie.options.secure = true;
184
184
  } else if (element.toLowerCase().startsWith('expires=')) {
185
185
  const date = tryDecodeDate(element.substring(8));
@@ -203,7 +203,7 @@ const tryDecodeDate = (value) => {
203
203
  try {
204
204
  return new Date(value);
205
205
  } catch (err) {
206
- /* istanbul ignore next */
206
+ /* v8 ignore next - invalid date format */
207
207
  return null;
208
208
  }
209
209
  };
@@ -9,7 +9,6 @@ import oracledb from 'oracledb';
9
9
  import stream from 'node:stream';
10
10
  import z from 'zod';
11
11
 
12
- import {streamToBuffer} from './stream.js';
13
12
  import {uploadFile} from './upload.js';
14
13
  import {getProcedureVariable} from './procedureVariable.js';
15
14
  import {getProcedureNamed} from './procedureNamed.js';
@@ -154,7 +153,7 @@ const procedureGetPage = async (test, databaseConnection) => {
154
153
 
155
154
  // Make sure that we have retrieved all the rows
156
155
  if (irows > MAX_IROWS) {
157
- /* istanbul ignore next */
156
+ /* v8 ignore next - defensive check for row limit */
158
157
  throw new ProcedureError(`procedureGetPage: error when retrieving rows. irows="${irows}"`, {}, sqlStatement, bindParameter);
159
158
  }
160
159
 
@@ -274,35 +273,38 @@ export const invokeProcedure = async (req, res, argObj, cgiObj, filesToUpload, o
274
273
 
275
274
  debug('invokeProcedure: download files');
276
275
  const fileBlob = await databaseConnection.createLob(oracledb.BLOB);
277
- const fileDownload = await procedureDownloadFiles(fileBlob, databaseConnection);
278
- if (debug.enabled) {
279
- debug(getBlock('fileDownload', inspect({fileType: fileDownload.fileType, fileSize: fileDownload.fileSize})));
280
- }
281
276
 
282
- // 7) parse the page
277
+ try {
278
+ const fileDownload = await procedureDownloadFiles(fileBlob, databaseConnection);
279
+ if (debug.enabled) {
280
+ debug(getBlock('fileDownload', inspect({fileType: fileDownload.fileType, fileSize: fileDownload.fileSize})));
281
+ }
283
282
 
284
- debug('invokeProcedure: parse the page');
285
- const pageComponents = parsePage(lines);
283
+ // 7) parse the page
286
284
 
287
- // add "Server" header
288
- pageComponents.head.server = cgiObj.SERVER_SOFTWARE;
285
+ debug('invokeProcedure: parse the page');
286
+ const pageComponents = parsePage(lines);
289
287
 
290
- // add file download information
291
- if (fileDownload.fileType !== '' && fileDownload.fileSize > 0 && fileDownload.fileBlob !== null) {
292
- pageComponents.file.fileType = fileDownload.fileType;
293
- pageComponents.file.fileSize = fileDownload.fileSize;
294
- pageComponents.file.fileBlob = await streamToBuffer(fileDownload.fileBlob);
295
- }
288
+ // add "Server" header
289
+ pageComponents.head.server = cgiObj.SERVER_SOFTWARE;
296
290
 
297
- // 8) send the page to browser
291
+ // add file download information
292
+ if (fileDownload.fileType !== '' && fileDownload.fileSize > 0 && fileDownload.fileBlob !== null) {
293
+ pageComponents.file.fileType = fileDownload.fileType;
294
+ pageComponents.file.fileSize = fileDownload.fileSize;
295
+ pageComponents.file.fileBlob = fileDownload.fileBlob;
296
+ }
298
297
 
299
- debug('invokeProcedure: send the page to browser');
300
- sendResponse(req, res, pageComponents);
298
+ // 8) send the page to browser
301
299
 
302
- // 9) cleanup
300
+ debug('invokeProcedure: send the page to browser');
301
+ await sendResponse(req, res, pageComponents);
302
+ } finally {
303
+ // 9) cleanup
303
304
 
304
- debug('invokeProcedure: cleanup');
305
- fileBlob.destroy();
305
+ debug('invokeProcedure: cleanup');
306
+ fileBlob.destroy();
307
+ }
306
308
 
307
309
  debug('invokeProcedure: end');
308
310
  };
@@ -89,9 +89,7 @@ const loadArguments = async (procedure, databaseConnection) => {
89
89
  result = await databaseConnection.execute(SQL_GET_ARGUMENT, bind);
90
90
  } catch (err) {
91
91
  debug('result', result);
92
- /* istanbul ignore next */
93
92
  const message = `Error when retrieving arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`;
94
- /* istanbul ignore next */
95
93
  throw new RequestError(message);
96
94
  }
97
95
 
@@ -106,14 +104,11 @@ const loadArguments = async (procedure, databaseConnection) => {
106
104
  .parse(result.outBinds);
107
105
  } catch (err) {
108
106
  debug('result.outBinds', result.outBinds);
109
- /* istanbul ignore next */
110
107
  const message = `Error when decoding arguments\n${SQL_GET_ARGUMENT}\n${errorToString(err)}`;
111
- /* istanbul ignore next */
112
108
  throw new RequestError(message);
113
109
  }
114
110
 
115
111
  if (data.names.length !== data.types.length) {
116
- /* istanbul ignore next */
117
112
  throw new RequestError('Error when decoding arguments. The number of names and types does not match');
118
113
  }
119
114
 
@@ -128,9 +128,7 @@ const loadRequestValid = async (procName, requestValidationFunction, databaseCon
128
128
  result = await databaseConnection.execute(SQL, bind);
129
129
  } catch (err) {
130
130
  debug('result', result);
131
- /* istanbul ignore next */
132
131
  const message = `Error when validating procedure name "${procName}"\n${SQL}\n${errorToString(err)}`;
133
- /* istanbul ignore next */
134
132
  throw new RequestError(message);
135
133
  }
136
134
 
@@ -139,9 +137,7 @@ const loadRequestValid = async (procName, requestValidationFunction, databaseCon
139
137
  return data.valid === 1;
140
138
  } catch (err) {
141
139
  debug('result', result.outBinds);
142
- /* istanbul ignore next */
143
140
  const message = `Internal error when parsing ${result.outBinds}\n${errorToString(err)}`;
144
- /* istanbul ignore next */
145
141
  throw new Error(message);
146
142
  }
147
143
  };
@@ -98,16 +98,16 @@ const normalizeBody = (req) => {
98
98
  /** @type {Record<string, string | string[]>} */
99
99
  const args = {};
100
100
 
101
- /* istanbul ignore else */
101
+ /* v8 ignore else - body validation */
102
102
  if (typeof req.body === 'object' && req.body !== null) {
103
103
  for (const key in req.body) {
104
104
  // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
105
105
  const value = req.body[key];
106
- /* istanbul ignore else */
106
+ /* v8 ignore else - type validation */
107
107
  if (isStringOrArrayOfString(value)) {
108
108
  args[key] = value;
109
109
  } else {
110
- /* istanbul ignore next */
110
+ /* v8 ignore next - invalid body type */
111
111
  throw new RequestError(
112
112
  `The element "${key}" in the body is not a string or an array of strings!\n${util.inspect(req.body, {showHidden: false, depth: null, colors: false})}`,
113
113
  );
@@ -5,6 +5,7 @@
5
5
  import debugModule from 'debug';
6
6
  const debug = debugModule('webplsql:sendResponse');
7
7
 
8
+ import stream from 'node:stream';
8
9
  import {getBlock} from '../../util/trace.js';
9
10
 
10
11
  /**
@@ -20,9 +21,9 @@ import {getBlock} from '../../util/trace.js';
20
21
  * @param {Request} req - The req object represents the HTTP request.
21
22
  * @param {Response} res - The res object represents the HTTP response that an Express app sends when it gets an HTTP request.
22
23
  * @param {pageType} page - The page to send.
23
- * @returns {void}
24
+ * @returns {Promise<void>}
24
25
  */
25
- export const sendResponse = (req, res, page) => {
26
+ export const sendResponse = async (req, res, page) => {
26
27
  /** @type {string[]} */
27
28
  const debugText = [];
28
29
 
@@ -57,20 +58,51 @@ export const sendResponse = (req, res, page) => {
57
58
 
58
59
  // If this is a file download, we eventually set the "Content-Type" and the file content and then return.
59
60
  if (page.file.fileType === 'B' || page.file.fileType === 'F') {
61
+ /** @type {Record<string, string>} */
62
+ const headers = {};
63
+
60
64
  if (typeof page.head.contentType === 'string' && page.head.contentType.length > 0) {
61
- if (debug.enabled) {
62
- debugText.push(`res.writeHead("Content-Type", "${page.head.contentType}")`);
63
- }
65
+ headers['Content-Type'] = page.head.contentType;
66
+ }
64
67
 
65
- res.writeHead(200, {'Content-Type': page.head.contentType});
68
+ if (typeof page.file.fileSize === 'number' && page.file.fileSize > 0) {
69
+ headers['Content-Length'] = page.file.fileSize.toString();
66
70
  }
67
71
 
68
- if (debug.enabled) {
69
- debugText.push(`res.end("${page.file.fileType}")`);
70
- debug(getBlock('RESPONSE', debugText.join('\n')));
72
+ if (Object.keys(headers).length > 0) {
73
+ if (debug.enabled) {
74
+ debugText.push(`res.writeHead(200, ${JSON.stringify(headers)})`);
75
+ }
76
+ res.writeHead(200, headers);
71
77
  }
72
78
 
73
- res.end(page.file.fileBlob, 'binary');
79
+ // Check if fileBlob is a stream
80
+ if (page.file.fileBlob instanceof stream.Readable) {
81
+ if (debug.enabled) {
82
+ debugText.push(`res.pipe("${page.file.fileType}") - streaming`);
83
+ debug(getBlock('RESPONSE', debugText.join('\n')));
84
+ }
85
+
86
+ /** @type {Promise<void>} */
87
+ const streamComplete = new Promise((resolve, reject) => {
88
+ if (page.file.fileBlob instanceof stream.Readable) {
89
+ page.file.fileBlob.pipe(res);
90
+ page.file.fileBlob.on('end', () => resolve());
91
+ /* v8 ignore next - error handler */
92
+ page.file.fileBlob.on('error', (/** @type {Error} */ err) => reject(err));
93
+ /* v8 ignore next - error handler */
94
+ res.on('close', () => resolve());
95
+ }
96
+ });
97
+ await streamComplete;
98
+ } else {
99
+ if (debug.enabled) {
100
+ debugText.push(`res.end("${page.file.fileType}") - buffer`);
101
+ debug(getBlock('RESPONSE', debugText.join('\n')));
102
+ }
103
+
104
+ res.end(page.file.fileBlob, 'binary');
105
+ }
74
106
  return;
75
107
  }
76
108
 
@@ -21,7 +21,7 @@ export const streamToBuffer = async (readable) => {
21
21
  });
22
22
 
23
23
  readable.on('error', (err) => {
24
- // istanbul ignore next
24
+ /* v8 ignore next - error handler */
25
25
  reject(err);
26
26
  });
27
27
  });
@@ -70,7 +70,7 @@ export const getFiles = (req) => {
70
70
  export const uploadFile = async (file, doctable, databaseConnection) => {
71
71
  debug(`uploadFile`, file, doctable);
72
72
 
73
- /* istanbul ignore next */
73
+ /* v8 ignore next - defensive validation */
74
74
  if (typeof doctable !== 'string' || doctable.length === 0) {
75
75
  throw new Error(`Unable to upload file "${file.filename}" because the option ""doctable" has not been defined`);
76
76
  }
package/src/types.js CHANGED
@@ -140,5 +140,5 @@ export const z$configType = z.strictObject({
140
140
  * @property {object} file - The file.
141
141
  * @property {string | null} file.fileType - The file type.
142
142
  * @property {number | null} file.fileSize - The file size.
143
- * @property {Buffer | null} file.fileBlob - The file blob.
143
+ * @property {import('node:stream').Readable | Buffer | null} file.fileBlob - The file blob.
144
144
  */
package/src/util/file.js CHANGED
@@ -10,7 +10,6 @@ export const readFileSyncUtf8 = (filePath) => {
10
10
  try {
11
11
  return readFileSync(filePath, 'utf8');
12
12
  } catch (err) {
13
- /* istanbul ignore next */
14
13
  throw new Error(`Unable to read file "${filePath}"`);
15
14
  }
16
15
  };
@@ -21,11 +20,10 @@ export const readFileSyncUtf8 = (filePath) => {
21
20
  * @param {string} filePath - File name.
22
21
  * @returns {Promise<Buffer>} The buffer.
23
22
  */
24
- export const readFile = (filePath) => {
23
+ export const readFile = async (filePath) => {
25
24
  try {
26
- return fs.readFile(filePath);
25
+ return await fs.readFile(filePath);
27
26
  } catch (err) {
28
- /* istanbul ignore next */
29
27
  throw new Error(`Unable to read file "${filePath}"`);
30
28
  }
31
29
  };
@@ -36,11 +34,10 @@ export const readFile = (filePath) => {
36
34
  * @param {string} filePath - File name.
37
35
  * @returns {Promise<void>}.
38
36
  */
39
- export const removeFile = (filePath) => {
37
+ export const removeFile = async (filePath) => {
40
38
  try {
41
- return fs.unlink(filePath);
39
+ return await fs.unlink(filePath);
42
40
  } catch (err) {
43
- /* istanbul ignore next */
44
41
  throw new Error(`Unable to remove file "${filePath}"`);
45
42
  }
46
43
  };
@@ -52,9 +49,8 @@ export const removeFile = (filePath) => {
52
49
  * @returns {unknown} The json object.
53
50
  */
54
51
  export const getJsonFile = (filePath) => {
55
- const fileContent = readFileSync(filePath, 'utf8');
56
-
57
52
  try {
53
+ const fileContent = readFileSync(filePath, 'utf8');
58
54
  return JSON.parse(fileContent);
59
55
  } catch (err) {
60
56
  throw new Error(`Unable to load file "${filePath}"`);
package/src/version.js CHANGED
@@ -2,4 +2,4 @@
2
2
  * Returns the current library version
3
3
  * @returns {string} - Version.
4
4
  */
5
- export const getVersion = () => '0.17.1';
5
+ export const getVersion = () => '0.18.0';
@@ -1,4 +1,4 @@
1
- export function sendResponse(req: Request, res: Response, page: pageType): void;
1
+ export function sendResponse(req: Request, res: Response, page: pageType): Promise<void>;
2
2
  export type Request = import("express").Request;
3
3
  export type Response = import("express").Response;
4
4
  export type CookieOptions = import("express").CookieOptions;
package/types/types.d.ts CHANGED
@@ -304,7 +304,7 @@ export type pageType = {
304
304
  file: {
305
305
  fileType: string | null;
306
306
  fileSize: number | null;
307
- fileBlob: Buffer | null;
307
+ fileBlob: import("node:stream").Readable | Buffer | null;
308
308
  };
309
309
  };
310
310
  import z from 'zod';
@@ -1,2 +0,0 @@
1
- export function streamToBuffer(readable: stream.Readable): Promise<Buffer>;
2
- import stream from 'node:stream';