web_plsql 0.17.1 → 1.0.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.
Files changed (60) hide show
  1. package/README.md +24 -4
  2. package/package.json +21 -8
  3. package/src/admin/client/charts.ts +299 -0
  4. package/src/admin/client/main.js +3 -0
  5. package/src/admin/client/tailwind.css +41 -0
  6. package/src/admin/favicon.svg +3 -0
  7. package/src/admin/index.html +315 -0
  8. package/src/admin/js/api.ts +95 -0
  9. package/src/admin/js/app.ts +306 -0
  10. package/src/admin/js/eslint.config.js +74 -0
  11. package/src/admin/js/schemas.ts +153 -0
  12. package/src/admin/js/templates/config.ts +146 -0
  13. package/src/admin/js/templates/errorRow.ts +18 -0
  14. package/src/admin/js/templates/index.ts +3 -0
  15. package/src/admin/js/templates/poolCard.ts +61 -0
  16. package/src/admin/js/tsconfig.json +24 -0
  17. package/src/admin/js/types.ts +223 -0
  18. package/src/admin/js/ui/theme.ts +93 -0
  19. package/src/admin/js/ui/views.ts +164 -0
  20. package/src/admin/js/util/format.ts +27 -0
  21. package/src/admin/lib/assets/main-zpdhQ1gD.css +1 -0
  22. package/src/admin/lib/chart.bundle.js +139 -0
  23. package/src/admin/style.css +1321 -0
  24. package/src/bin/load-test.js +202 -0
  25. package/src/handler/handlerAdmin.js +198 -0
  26. package/src/handler/plsql/cgi.js +1 -1
  27. package/src/handler/plsql/errorPage.js +37 -12
  28. package/src/handler/plsql/handlerPlSql.js +32 -6
  29. package/src/handler/plsql/parsePage.js +15 -11
  30. package/src/handler/plsql/procedure.js +81 -33
  31. package/src/handler/plsql/procedureNamed.js +18 -52
  32. package/src/handler/plsql/procedureSanitize.js +96 -49
  33. package/src/handler/plsql/procedureVariable.js +2 -2
  34. package/src/handler/plsql/request.js +11 -6
  35. package/src/handler/plsql/sendResponse.js +43 -11
  36. package/src/handler/plsql/stream.js +1 -1
  37. package/src/handler/plsql/upload.js +1 -1
  38. package/src/server/config.js +1 -0
  39. package/src/server/server.js +108 -2
  40. package/src/types.js +7 -1
  41. package/src/util/cache.js +123 -0
  42. package/src/util/file.js +5 -9
  43. package/src/util/jsonLogger.js +47 -0
  44. package/src/util/shutdown.js +6 -2
  45. package/src/util/trace.js +5 -5
  46. package/src/version.js +1 -1
  47. package/types/handler/handlerAdmin.d.ts +9 -0
  48. package/types/handler/plsql/errorPage.d.ts +1 -0
  49. package/types/handler/plsql/handlerPlSql.d.ts +6 -1
  50. package/types/handler/plsql/procedure.d.ts +3 -1
  51. package/types/handler/plsql/procedureNamed.d.ts +2 -5
  52. package/types/handler/plsql/procedureSanitize.d.ts +2 -5
  53. package/types/handler/plsql/procedureVariable.d.ts +1 -1
  54. package/types/handler/plsql/request.d.ts +3 -1
  55. package/types/handler/plsql/sendResponse.d.ts +1 -1
  56. package/types/server/server.d.ts +22 -0
  57. package/types/types.d.ts +19 -1
  58. package/types/util/cache.d.ts +69 -0
  59. package/types/util/jsonLogger.d.ts +45 -0
  60. package/types/handler/plsql/stream.d.ts +0 -2
@@ -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
  /**
@@ -17,12 +18,12 @@ import {getBlock} from '../../util/trace.js';
17
18
 
18
19
  /**
19
20
  * Send "default" response to the browser
20
- * @param {Request} req - The req object represents the HTTP request.
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
  }
@@ -17,6 +17,7 @@ export const showConfig = (config) => {
17
17
  console.log(LINE);
18
18
 
19
19
  console.log(`Server port: ${config.port}`);
20
+ console.log(`Admin route: ${config.adminRoute ?? '/admin'}${config.adminUser ? ' (authenticated)' : ''}`);
20
21
  console.log(`Access log: ${config.loggerFilename.length > 0 ? config.loggerFilename : ''}`);
21
22
  console.log(`Upload file size limit: ${typeof config.uploadFileSizeLimit === 'number' ? `${config.uploadFileSizeLimit} bytes` : 'any'}`);
22
23
 
@@ -2,6 +2,8 @@ import debugModule from 'debug';
2
2
  const debug = debugModule('webplsql:server');
3
3
  import http from 'node:http';
4
4
  import https from 'node:https';
5
+ import path from 'node:path';
6
+ import {fileURLToPath} from 'node:url';
5
7
  import express from 'express';
6
8
  import cookieParser from 'cookie-parser';
7
9
  import compression from 'compression';
@@ -11,8 +13,13 @@ import {poolCreate, poolsClose} from '../util/oracle.js';
11
13
  import {handlerUpload} from '../handler/handlerUpload.js';
12
14
  import {handlerLogger} from '../handler/handlerLogger.js';
13
15
  import {handlerWebPlSql} from '../handler/plsql/handlerPlSql.js';
16
+ import {handlerAdmin} from '../handler/handlerAdmin.js';
14
17
  import {readFileSyncUtf8, getJsonFile} from '../util/file.js';
15
18
  import {showConfig} from './config.js';
19
+ import {Cache} from '../util/cache.js';
20
+
21
+ const __filename = fileURLToPath(import.meta.url);
22
+ const __dirname = path.dirname(__filename);
16
23
 
17
24
  /**
18
25
  * @typedef {import('node:net').Socket} Socket
@@ -23,7 +30,34 @@ import {showConfig} from './config.js';
23
30
  * @typedef {import('oracledb').Pool} Pool
24
31
  * @typedef {import('../types.js').environmentType} environmentType
25
32
  * @typedef {import('../types.js').configType} configType
33
+ * @typedef {import('../handler/plsql/procedureNamed.js').argsType} argsType
34
+ */
35
+
36
+ /**
37
+ * @typedef {import('express').RequestHandler & {
38
+ * procedureNameCache: Cache<string>;
39
+ * argumentCache: Cache<argsType>;
40
+ * }} ExtendedRequestHandler
41
+ */
42
+
43
+ /**
44
+ * Global Admin Context
26
45
  */
46
+ export const AdminContext = {
47
+ startTime: new Date(),
48
+ /** @type {configType | null} */
49
+ config: null,
50
+ /** @type {Pool[]} */
51
+ pools: [],
52
+ /** @type {Array<{poolName: string, procedureNameCache: Cache<string>, argumentCache: Cache<argsType>}>} */
53
+ caches: [],
54
+ paused: false,
55
+ metrics: {
56
+ requestCount: 0,
57
+ errorCount: 0,
58
+ totalDuration: 0,
59
+ },
60
+ };
27
61
 
28
62
  /**
29
63
  * @typedef {object} webServer - Web server interface.
@@ -40,6 +74,42 @@ import {showConfig} from './config.js';
40
74
  * @property {string} certFilename - cert filename.
41
75
  */
42
76
 
77
+ /**
78
+ * Admin basic auth middleware
79
+ * @param {Request} req - The request.
80
+ * @param {Response} res - The response.
81
+ * @param {NextFunction} next - The next function.
82
+ */
83
+ const adminAuth = (req, res, next) => {
84
+ const adminRoute = AdminContext.config?.adminRoute ?? '/admin';
85
+
86
+ // Simple pause check for all PL/SQL routes (not admin)
87
+ if (AdminContext.paused && !req.path.startsWith(adminRoute)) {
88
+ res.status(503).send('Server Paused');
89
+ return;
90
+ }
91
+
92
+ // Basic Auth for Admin Route
93
+ if (req.path.startsWith(adminRoute)) {
94
+ const user = AdminContext.config?.adminUser;
95
+ const pass = AdminContext.config?.adminPassword;
96
+
97
+ if (user && pass) {
98
+ const auth = {login: user, password: pass};
99
+ const b64auth = (req.headers.authorization ?? '').split(' ')[1] ?? '';
100
+ const [login, password] = Buffer.from(b64auth, 'base64').toString().split(':');
101
+
102
+ if (login !== auth.login || password !== auth.password) {
103
+ res.set('WWW-Authenticate', 'Basic realm="Admin Console"');
104
+ res.status(401).send('Authentication required.');
105
+ return;
106
+ }
107
+ }
108
+ }
109
+
110
+ next();
111
+ };
112
+
43
113
  /**
44
114
  * Create HTTPS server.
45
115
  * @param {Express} app - express application
@@ -67,17 +137,27 @@ export const startServer = async (config, ssl) => {
67
137
  debug('startServer: BEGIN', config, ssl);
68
138
 
69
139
  const internalConfig = /** @type {configType} */ (z$configType.parse(config));
140
+ AdminContext.config = internalConfig;
70
141
 
71
142
  showConfig(internalConfig);
72
143
 
73
144
  // Create express app
74
145
  const app = express();
75
146
 
147
+ // Pause & Admin Auth middleware
148
+ app.use(adminAuth);
149
+
76
150
  // Access log
77
151
  if (internalConfig.loggerFilename.length > 0) {
78
152
  app.use(handlerLogger(internalConfig.loggerFilename));
79
153
  }
80
154
 
155
+ // Admin console
156
+ const adminRoute = internalConfig.adminRoute ?? '/admin';
157
+ const adminDirectory = path.resolve(__dirname, '../admin');
158
+ app.use(adminRoute, handlerAdmin);
159
+ app.use(adminRoute, express.static(adminDirectory));
160
+
81
161
  // Serving static files
82
162
  for (const i of internalConfig.routeStatic) {
83
163
  app.use(i.route, express.static(i.directoryPath));
@@ -92,6 +172,8 @@ export const startServer = async (config, ssl) => {
92
172
 
93
173
  /** @type {Pool[]} */
94
174
  const connectionPools = [];
175
+ AdminContext.pools = connectionPools;
176
+ AdminContext.caches = [];
95
177
 
96
178
  // Oracle pl/sql express middleware
97
179
  for (const i of internalConfig.routePlSql) {
@@ -99,7 +181,25 @@ export const startServer = async (config, ssl) => {
99
181
  const pool = await poolCreate(i.user, i.password, i.connectString);
100
182
  connectionPools.push(pool);
101
183
 
102
- app.use([`${i.route}/:name`, i.route], handlerWebPlSql(pool, i));
184
+ const handler = handlerWebPlSql(pool, i);
185
+
186
+ // Capture caches for admin console
187
+ AdminContext.caches.push({
188
+ poolName: i.route,
189
+ procedureNameCache: handler.procedureNameCache,
190
+ argumentCache: handler.argumentCache,
191
+ });
192
+
193
+ app.use([`${i.route}/:name`, i.route], (req, res, next) => {
194
+ AdminContext.metrics.requestCount++;
195
+ const start = process.hrtime();
196
+ res.on('finish', () => {
197
+ const diff = process.hrtime(start);
198
+ const duration = diff[0] * 1000 + diff[1] / 1_000_000;
199
+ AdminContext.metrics.totalDuration += duration;
200
+ });
201
+ handler(req, res, next);
202
+ });
103
203
  }
104
204
 
105
205
  // create server
@@ -150,7 +250,13 @@ export const startServer = async (config, ssl) => {
150
250
  resolve();
151
251
  })
152
252
  .on('error', (err) => {
153
- console.error(err);
253
+ if ('code' in err) {
254
+ if (err.code === 'EADDRINUSE') {
255
+ err.message = `Port ${internalConfig.port} is already in use`;
256
+ } else if (err.code === 'EACCES') {
257
+ err.message = `Port ${internalConfig.port} requires elevated privileges`;
258
+ }
259
+ }
154
260
  reject(err);
155
261
  });
156
262
  })
package/src/types.js CHANGED
@@ -82,6 +82,9 @@ export const z$configPlSqlType = z.strictObject({
82
82
  * @property {configPlSqlType[]} routePlSql - The PL/SQL routes.
83
83
  * @property {number} [uploadFileSizeLimit] - Maximum size of each uploaded file in bytes or no limit if omitted.
84
84
  * @property {string} loggerFilename - name of the request logger filename or '' if not required.
85
+ * @property {string} [adminRoute] - Optional route for the admin console (defaults to /admin).
86
+ * @property {string} [adminUser] - Optional username for admin console basic auth.
87
+ * @property {string} [adminPassword] - Optional password for admin console basic auth.
85
88
  */
86
89
  export const z$configType = z.strictObject({
87
90
  port: z.number(),
@@ -89,6 +92,9 @@ export const z$configType = z.strictObject({
89
92
  routePlSql: z.array(z$configPlSqlType),
90
93
  uploadFileSizeLimit: z.number().optional(),
91
94
  loggerFilename: z.string(),
95
+ adminRoute: z.string().optional(),
96
+ adminUser: z.string().optional(),
97
+ adminPassword: z.string().optional(),
92
98
  });
93
99
 
94
100
  /**
@@ -140,5 +146,5 @@ export const z$configType = z.strictObject({
140
146
  * @property {object} file - The file.
141
147
  * @property {string | null} file.fileType - The file type.
142
148
  * @property {number | null} file.fileSize - The file size.
143
- * @property {Buffer | null} file.fileBlob - The file blob.
149
+ * @property {import('node:stream').Readable | Buffer | null} file.fileBlob - The file blob.
144
150
  */
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @template T
3
+ * @typedef {{hitCount: number, value: T}} cacheEntryType
4
+ */
5
+
6
+ /**
7
+ * Generic Cache class with LFU (Least Frequently Used) eviction policy.
8
+ * @template T
9
+ */
10
+ export class Cache {
11
+ /**
12
+ * @param {number} maxSize - Maximum number of entries in the cache.
13
+ */
14
+ constructor(maxSize = 10000) {
15
+ /** @type {Map<string, cacheEntryType<T>>} */
16
+ this.cache = new Map();
17
+ this.maxSize = maxSize;
18
+ this.hits = 0;
19
+ this.misses = 0;
20
+ }
21
+
22
+ /**
23
+ * Get an entry from the cache.
24
+ * @param {string} key - The key.
25
+ * @returns {T | undefined} - The value or undefined if not found.
26
+ */
27
+ get(key) {
28
+ const entry = this.cache.get(key);
29
+ if (entry) {
30
+ entry.hitCount++;
31
+ this.hits++;
32
+ return entry.value;
33
+ }
34
+ this.misses++;
35
+ return undefined;
36
+ }
37
+
38
+ /**
39
+ * Set an entry in the cache.
40
+ * @param {string} key - The key.
41
+ * @param {T} value - The value.
42
+ */
43
+ set(key, value) {
44
+ // If updating an existing key, preserve its hitCount?
45
+ // Typically LFU implies resetting or keeping.
46
+ // For simplicity and avoiding complex aging, if we set it again, we reset hitCount or keep it?
47
+ // The requirement is "cache invalidation" (delete) or "cache loading" (set).
48
+ // If we overwrite, it's usually a new value. Let's reset hitCount to 0 for a fresh start or 1.
49
+
50
+ // Ensure we have space
51
+ if (this.cache.size >= this.maxSize && !this.cache.has(key)) {
52
+ this.prune();
53
+ }
54
+
55
+ this.cache.set(key, {hitCount: 0, value});
56
+ }
57
+
58
+ /**
59
+ * Delete an entry from the cache.
60
+ * @param {string} key - The key.
61
+ */
62
+ delete(key) {
63
+ this.cache.delete(key);
64
+ }
65
+
66
+ /**
67
+ * Clear the cache.
68
+ */
69
+ clear() {
70
+ this.cache.clear();
71
+ this.hits = 0;
72
+ this.misses = 0;
73
+ }
74
+
75
+ /**
76
+ * Prune the cache by removing the least frequently used entries.
77
+ * Removes 10% of the cache size.
78
+ */
79
+ prune() {
80
+ // Convert cache entries to an array
81
+ const entries = Array.from(this.cache.entries());
82
+
83
+ // Sort entries by hitCount in ascending order
84
+ entries.sort((a, b) => a[1].hitCount - b[1].hitCount);
85
+
86
+ // Remove the bottom 10%
87
+ const removeCount = Math.max(1, Math.floor(this.maxSize * 0.1));
88
+ const keysToRemove = entries.slice(0, removeCount).map(([key]) => key);
89
+
90
+ for (const key of keysToRemove) {
91
+ this.cache.delete(key);
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Get the size of the cache.
97
+ * @returns {number} - The size.
98
+ */
99
+ get size() {
100
+ return this.cache.size;
101
+ }
102
+
103
+ /**
104
+ * Get all keys in the cache.
105
+ * @returns {string[]} - The keys.
106
+ */
107
+ keys() {
108
+ return Array.from(this.cache.keys());
109
+ }
110
+
111
+ /**
112
+ * Get cache statistics.
113
+ * @returns {{size: number, maxSize: number, hits: number, misses: number}} - The statistics.
114
+ */
115
+ getStats() {
116
+ return {
117
+ size: this.cache.size,
118
+ maxSize: this.maxSize,
119
+ hits: this.hits,
120
+ misses: this.misses,
121
+ };
122
+ }
123
+ }
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}"`);
@@ -0,0 +1,47 @@
1
+ import * as rotatingFileStream from 'rotating-file-stream';
2
+
3
+ /**
4
+ * @typedef {object} LogEntry
5
+ * @property {string} timestamp - ISO string
6
+ * @property {'error'|'info'|'warning'} type - Log level
7
+ * @property {string} message - Error message
8
+ * @property {object} [req] - Request details
9
+ * @property {object} [details] - Additional details (stack, sql, etc)
10
+ */
11
+
12
+ export class JsonLogger {
13
+ constructor(filename = 'error.json.log') {
14
+ this.stream = rotatingFileStream.createStream(filename, {
15
+ size: '10M', // rotate every 10 MegaBytes written
16
+ interval: '1d', // rotate daily
17
+ maxFiles: 10, // maximum number of rotated files to keep
18
+ compress: 'gzip', // compress rotated files
19
+ });
20
+ }
21
+
22
+ /**
23
+ * Log an entry as NDJSON.
24
+ * @param {LogEntry} entry - The entry to log.
25
+ */
26
+ log(entry) {
27
+ try {
28
+ // Ensure timestamp exists
29
+ if (!entry.timestamp) {
30
+ entry.timestamp = new Date().toISOString();
31
+ }
32
+ const line = JSON.stringify(entry);
33
+ this.stream.write(line + '\n');
34
+ } catch (err) {
35
+ console.error('JsonLogger: Failed to write log', err);
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Close the stream.
41
+ */
42
+ close() {
43
+ this.stream.end();
44
+ }
45
+ }
46
+
47
+ export const jsonLogger = new JsonLogger();
@@ -12,8 +12,12 @@ export const installShutdown = (handler) => {
12
12
  /*
13
13
  * The 'unhandledRejection' event is emitted whenever a Promise is rejected and no error handler is attached to the promise within a turn of the event loop.
14
14
  */
15
- process.on('unhandledRejection', (reason, p) => {
16
- console.log('\nUnhandled promise rejection. Graceful shutdown...', reason, p);
15
+ process.on('unhandledRejection', (reason) => {
16
+ if (reason instanceof Error) {
17
+ console.error(`\n${reason.message}. Graceful shutdown...`);
18
+ } else {
19
+ console.error('\nUnhandled promise rejection. Graceful shutdown...', reason);
20
+ }
17
21
  void handler();
18
22
  });
19
23
 
package/src/util/trace.js CHANGED
@@ -57,7 +57,7 @@ export const toTable = (head, body) => {
57
57
 
58
58
  // Calculate column widths
59
59
  const widths = head.map((h, i) => {
60
- const bodyMax = Math.max(0, ...body.map((row) => (row[i] || '').length));
60
+ const bodyMax = Math.max(0, ...body.map((row) => (row[i] ?? '').length));
61
61
  return Math.max(h.length, bodyMax);
62
62
  });
63
63
 
@@ -68,13 +68,13 @@ export const toTable = (head, body) => {
68
68
  * @returns {string} - The result
69
69
  */
70
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(' | '));
71
+ const textHeader = head.map((h, i) => padCell(h, widths[i] ?? 0)).join(' | ');
72
+ const textSeparator = widths.map((w) => '-'.repeat(w ?? 0)).join('-+-');
73
+ const textRows = body.map((row) => head.map((_, i) => padCell(row[i] ?? '', widths[i] ?? 0)).join(' | '));
74
74
  const text = [textHeader, textSeparator, ...textRows].join('\n');
75
75
 
76
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('');
77
+ const htmlBody = body.map((row) => `<tr>${head.map((_, i) => `<td>${escapeHtml(row[i] ?? '')}</td>`).join('')}</tr>`).join('');
78
78
  const html = `<table><thead><tr>${htmlHead}</tr></thead><tbody>${htmlBody}</tbody></table>`;
79
79
 
80
80
  return {text, html};
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 = () => '1.0.0';
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @typedef {import('express').Request} Request
3
+ * @typedef {import('express').Response} Response
4
+ * @typedef {import('express').NextFunction} NextFunction
5
+ */
6
+ export const handlerAdmin: import("express-serve-static-core").Router;
7
+ export type Request = import("express").Request;
8
+ export type Response = import("express").Response;
9
+ export type NextFunction = import("express").NextFunction;
@@ -8,3 +8,4 @@ export type outputType = {
8
8
  html: string;
9
9
  text: string;
10
10
  };
11
+ export type messageType = import("../../util/trace.js").messageType;
@@ -1,4 +1,4 @@
1
- export function handlerWebPlSql(connectionPool: Pool, config: configPlSqlHandlerType): RequestHandler;
1
+ export function handlerWebPlSql(connectionPool: Pool, config: configPlSqlHandlerType): WebPlSqlRequestHandler;
2
2
  export type RequestHandler = import("express").RequestHandler;
3
3
  export type Request = import("express").Request;
4
4
  export type Response = import("express").Response;
@@ -6,3 +6,8 @@ export type NextFunction = import("express").NextFunction;
6
6
  export type Pool = import("oracledb").Pool;
7
7
  export type environmentType = import("../../types.js").environmentType;
8
8
  export type configPlSqlHandlerType = import("../../types.js").configPlSqlHandlerType;
9
+ export type WebPlSqlRequestHandler = import("express").RequestHandler & {
10
+ procedureNameCache: Cache<string>;
11
+ argumentCache: Cache<import("./procedureNamed.js").argsType>;
12
+ };
13
+ import { Cache } from '../../util/cache.js';
@@ -1,4 +1,4 @@
1
- export function invokeProcedure(req: Request, res: Response, argObj: argObjType, cgiObj: environmentType, filesToUpload: fileUploadType[], options: configPlSqlHandlerType, databaseConnection: Connection): Promise<void>;
1
+ export function invokeProcedure(req: Request, res: Response, argObj: argObjType, cgiObj: environmentType, filesToUpload: fileUploadType[], options: configPlSqlHandlerType, databaseConnection: Connection, procedureNameCache: ProcedureNameCache, argumentCache: ArgumentCache): Promise<void>;
2
2
  export type Request = import("express").Request;
3
3
  export type Response = import("express").Response;
4
4
  export type Connection = import("oracledb").Connection;
@@ -8,3 +8,5 @@ export type fileUploadType = import("../../types.js").fileUploadType;
8
8
  export type environmentType = import("../../types.js").environmentType;
9
9
  export type configPlSqlHandlerType = import("../../types.js").configPlSqlHandlerType;
10
10
  export type BindParameterConfig = import("../../types.js").BindParameterConfig;
11
+ export type ProcedureNameCache = import("../../util/cache.js").Cache<string>;
12
+ export type ArgumentCache = import("../../util/cache.js").Cache<import("./procedureNamed.js").argsType>;
@@ -1,5 +1,5 @@
1
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
+ export function getProcedureNamed(req: Request, procName: string, argObj: argObjType, databaseConnection: Connection, argumentCache: ArgumentCache): Promise<{
3
3
  sql: string;
4
4
  bind: BindParameterConfig;
5
5
  }>;
@@ -11,7 +11,4 @@ export type argObjType = import("../../types.js").argObjType;
11
11
  export type BindParameterConfig = import("../../types.js").BindParameterConfig;
12
12
  export type BindParameter = import("../../types.js").BindParameter;
13
13
  export type argsType = Record<string, string>;
14
- export type cacheEntryType = {
15
- hitCount: number;
16
- args: argsType;
17
- };
14
+ export type ArgumentCache = import("../../util/cache.js").Cache<argsType>;
@@ -1,4 +1,4 @@
1
- export function sanitizeProcName(procName: string, databaseConnection: Connection, options: configPlSqlHandlerType): Promise<string>;
1
+ export function sanitizeProcName(procName: string, databaseConnection: Connection, options: configPlSqlHandlerType, procedureNameCache: ProcedureNameCache): Promise<string>;
2
2
  export type Request = import("express").Request;
3
3
  export type Response = import("express").Response;
4
4
  export type Connection = import("oracledb").Connection;
@@ -8,7 +8,4 @@ export type fileUploadType = import("../../types.js").fileUploadType;
8
8
  export type environmentType = import("../../types.js").environmentType;
9
9
  export type configPlSqlHandlerType = import("../../types.js").configPlSqlHandlerType;
10
10
  export type BindParameterConfig = import("../../types.js").BindParameterConfig;
11
- export type cacheEntryType = {
12
- hitCount: number;
13
- valid: boolean;
14
- };
11
+ export type ProcedureNameCache = import("../../util/cache.js").Cache<string>;
@@ -1,4 +1,4 @@
1
- export function getProcedureVariable(req: Request, procName: string, argObj: argObjType): {
1
+ export function getProcedureVariable(_req: Request, procName: string, argObj: argObjType): {
2
2
  sql: string;
3
3
  bind: BindParameterConfig;
4
4
  };