web_plsql 0.18.0 → 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.
- package/README.md +24 -4
- package/package.json +15 -5
- package/src/admin/client/charts.ts +299 -0
- package/src/admin/client/main.js +3 -0
- package/src/admin/client/tailwind.css +41 -0
- package/src/admin/favicon.svg +3 -0
- package/src/admin/index.html +315 -0
- package/src/admin/js/api.ts +95 -0
- package/src/admin/js/app.ts +306 -0
- package/src/admin/js/eslint.config.js +74 -0
- package/src/admin/js/schemas.ts +153 -0
- package/src/admin/js/templates/config.ts +146 -0
- package/src/admin/js/templates/errorRow.ts +18 -0
- package/src/admin/js/templates/index.ts +3 -0
- package/src/admin/js/templates/poolCard.ts +61 -0
- package/src/admin/js/tsconfig.json +24 -0
- package/src/admin/js/types.ts +223 -0
- package/src/admin/js/ui/theme.ts +93 -0
- package/src/admin/js/ui/views.ts +164 -0
- package/src/admin/js/util/format.ts +27 -0
- package/src/admin/lib/assets/main-zpdhQ1gD.css +1 -0
- package/src/admin/lib/chart.bundle.js +139 -0
- package/src/admin/style.css +1321 -0
- package/src/bin/load-test.js +202 -0
- package/src/handler/handlerAdmin.js +198 -0
- package/src/handler/plsql/cgi.js +1 -1
- package/src/handler/plsql/errorPage.js +35 -6
- package/src/handler/plsql/handlerPlSql.js +32 -6
- package/src/handler/plsql/parsePage.js +7 -3
- package/src/handler/plsql/procedure.js +57 -11
- package/src/handler/plsql/procedureNamed.js +18 -47
- package/src/handler/plsql/procedureSanitize.js +96 -45
- package/src/handler/plsql/procedureVariable.js +2 -2
- package/src/handler/plsql/request.js +8 -3
- package/src/handler/plsql/sendResponse.js +2 -2
- package/src/server/config.js +1 -0
- package/src/server/server.js +108 -2
- package/src/types.js +6 -0
- package/src/util/cache.js +123 -0
- package/src/util/jsonLogger.js +47 -0
- package/src/util/shutdown.js +6 -2
- package/src/util/trace.js +5 -5
- package/src/version.js +1 -1
- package/types/handler/handlerAdmin.d.ts +9 -0
- package/types/handler/plsql/errorPage.d.ts +1 -0
- package/types/handler/plsql/handlerPlSql.d.ts +6 -1
- package/types/handler/plsql/procedure.d.ts +3 -1
- package/types/handler/plsql/procedureNamed.d.ts +2 -5
- package/types/handler/plsql/procedureSanitize.d.ts +2 -5
- package/types/handler/plsql/procedureVariable.d.ts +1 -1
- package/types/handler/plsql/request.d.ts +3 -1
- package/types/handler/plsql/sendResponse.d.ts +1 -1
- package/types/server/server.d.ts +22 -0
- package/types/types.d.ts +18 -0
- package/types/util/cache.d.ts +69 -0
- package/types/util/jsonLogger.d.ts +45 -0
package/src/server/server.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
/**
|
|
@@ -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
|
+
}
|
|
@@ -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();
|
package/src/util/shutdown.js
CHANGED
|
@@ -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
|
|
16
|
-
|
|
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]
|
|
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]
|
|
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]
|
|
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
|
@@ -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;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function handlerWebPlSql(connectionPool: Pool, config: configPlSqlHandlerType):
|
|
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
|
|
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
|
|
12
|
-
hitCount: number;
|
|
13
|
-
valid: boolean;
|
|
14
|
-
};
|
|
11
|
+
export type ProcedureNameCache = import("../../util/cache.js").Cache<string>;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
export function processRequest(req: Request, res: Response, options: configPlSqlHandlerType, connectionPool: Pool): Promise<void>;
|
|
1
|
+
export function processRequest(req: Request, res: Response, options: configPlSqlHandlerType, connectionPool: Pool, 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 Pool = import("oracledb").Pool;
|
|
5
5
|
export type Connection = import("oracledb").Connection;
|
|
6
6
|
export type argObjType = import("../../types.js").argObjType;
|
|
7
7
|
export type configPlSqlHandlerType = import("../../types.js").configPlSqlHandlerType;
|
|
8
|
+
export type ProcedureNameCache = import("../../util/cache.js").Cache<string>;
|
|
9
|
+
export type ArgumentCache = import("../../util/cache.js").Cache<import("./procedureNamed.js").argsType>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export function sendResponse(
|
|
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/server/server.d.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
export namespace AdminContext {
|
|
2
|
+
let startTime: Date;
|
|
3
|
+
let config: configType | null;
|
|
4
|
+
let pools: Pool[];
|
|
5
|
+
let caches: Array<{
|
|
6
|
+
poolName: string;
|
|
7
|
+
procedureNameCache: Cache<string>;
|
|
8
|
+
argumentCache: Cache<argsType>;
|
|
9
|
+
}>;
|
|
10
|
+
let paused: boolean;
|
|
11
|
+
namespace metrics {
|
|
12
|
+
let requestCount: number;
|
|
13
|
+
let errorCount: number;
|
|
14
|
+
let totalDuration: number;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
1
17
|
export function createServer(app: Express, ssl?: sslConfig): http.Server | https.Server;
|
|
2
18
|
export function startServer(config: configType, ssl?: sslConfig): Promise<webServer>;
|
|
3
19
|
export function loadConfig(filename?: string): configType;
|
|
@@ -10,6 +26,11 @@ export type NextFunction = import("express").NextFunction;
|
|
|
10
26
|
export type Pool = import("oracledb").Pool;
|
|
11
27
|
export type environmentType = import("../types.js").environmentType;
|
|
12
28
|
export type configType = import("../types.js").configType;
|
|
29
|
+
export type argsType = import("../handler/plsql/procedureNamed.js").argsType;
|
|
30
|
+
export type ExtendedRequestHandler = import("express").RequestHandler & {
|
|
31
|
+
procedureNameCache: Cache<string>;
|
|
32
|
+
argumentCache: Cache<argsType>;
|
|
33
|
+
};
|
|
13
34
|
/**
|
|
14
35
|
* - Web server interface.
|
|
15
36
|
*/
|
|
@@ -48,5 +69,6 @@ export type sslConfig = {
|
|
|
48
69
|
*/
|
|
49
70
|
certFilename: string;
|
|
50
71
|
};
|
|
72
|
+
import { Cache } from '../util/cache.js';
|
|
51
73
|
import http from 'node:http';
|
|
52
74
|
import https from 'node:https';
|
package/types/types.d.ts
CHANGED
|
@@ -92,6 +92,9 @@ export const z$configPlSqlType: z.ZodObject<{
|
|
|
92
92
|
* @property {configPlSqlType[]} routePlSql - The PL/SQL routes.
|
|
93
93
|
* @property {number} [uploadFileSizeLimit] - Maximum size of each uploaded file in bytes or no limit if omitted.
|
|
94
94
|
* @property {string} loggerFilename - name of the request logger filename or '' if not required.
|
|
95
|
+
* @property {string} [adminRoute] - Optional route for the admin console (defaults to /admin).
|
|
96
|
+
* @property {string} [adminUser] - Optional username for admin console basic auth.
|
|
97
|
+
* @property {string} [adminPassword] - Optional password for admin console basic auth.
|
|
95
98
|
*/
|
|
96
99
|
export const z$configType: z.ZodObject<{
|
|
97
100
|
port: z.ZodNumber;
|
|
@@ -118,6 +121,9 @@ export const z$configType: z.ZodObject<{
|
|
|
118
121
|
}, z.core.$strict>>;
|
|
119
122
|
uploadFileSizeLimit: z.ZodOptional<z.ZodNumber>;
|
|
120
123
|
loggerFilename: z.ZodString;
|
|
124
|
+
adminRoute: z.ZodOptional<z.ZodString>;
|
|
125
|
+
adminUser: z.ZodOptional<z.ZodString>;
|
|
126
|
+
adminPassword: z.ZodOptional<z.ZodString>;
|
|
121
127
|
}, z.core.$strict>;
|
|
122
128
|
export type BindParameter = import("oracledb").BindParameter;
|
|
123
129
|
export type Connection = import("oracledb").Connection;
|
|
@@ -217,6 +223,18 @@ export type configType = {
|
|
|
217
223
|
* - name of the request logger filename or '' if not required.
|
|
218
224
|
*/
|
|
219
225
|
loggerFilename: string;
|
|
226
|
+
/**
|
|
227
|
+
* - Optional route for the admin console (defaults to /admin).
|
|
228
|
+
*/
|
|
229
|
+
adminRoute?: string;
|
|
230
|
+
/**
|
|
231
|
+
* - Optional username for admin console basic auth.
|
|
232
|
+
*/
|
|
233
|
+
adminUser?: string;
|
|
234
|
+
/**
|
|
235
|
+
* - Optional password for admin console basic auth.
|
|
236
|
+
*/
|
|
237
|
+
adminPassword?: string;
|
|
220
238
|
};
|
|
221
239
|
/**
|
|
222
240
|
* Environment variables as string key-value pairs
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @template T
|
|
3
|
+
* @typedef {{hitCount: number, value: T}} cacheEntryType
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Generic Cache class with LFU (Least Frequently Used) eviction policy.
|
|
7
|
+
* @template T
|
|
8
|
+
*/
|
|
9
|
+
export class Cache<T> {
|
|
10
|
+
/**
|
|
11
|
+
* @param {number} maxSize - Maximum number of entries in the cache.
|
|
12
|
+
*/
|
|
13
|
+
constructor(maxSize?: number);
|
|
14
|
+
/** @type {Map<string, cacheEntryType<T>>} */
|
|
15
|
+
cache: Map<string, cacheEntryType<T>>;
|
|
16
|
+
maxSize: number;
|
|
17
|
+
hits: number;
|
|
18
|
+
misses: number;
|
|
19
|
+
/**
|
|
20
|
+
* Get an entry from the cache.
|
|
21
|
+
* @param {string} key - The key.
|
|
22
|
+
* @returns {T | undefined} - The value or undefined if not found.
|
|
23
|
+
*/
|
|
24
|
+
get(key: string): T | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Set an entry in the cache.
|
|
27
|
+
* @param {string} key - The key.
|
|
28
|
+
* @param {T} value - The value.
|
|
29
|
+
*/
|
|
30
|
+
set(key: string, value: T): void;
|
|
31
|
+
/**
|
|
32
|
+
* Delete an entry from the cache.
|
|
33
|
+
* @param {string} key - The key.
|
|
34
|
+
*/
|
|
35
|
+
delete(key: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Clear the cache.
|
|
38
|
+
*/
|
|
39
|
+
clear(): void;
|
|
40
|
+
/**
|
|
41
|
+
* Prune the cache by removing the least frequently used entries.
|
|
42
|
+
* Removes 10% of the cache size.
|
|
43
|
+
*/
|
|
44
|
+
prune(): void;
|
|
45
|
+
/**
|
|
46
|
+
* Get the size of the cache.
|
|
47
|
+
* @returns {number} - The size.
|
|
48
|
+
*/
|
|
49
|
+
get size(): number;
|
|
50
|
+
/**
|
|
51
|
+
* Get all keys in the cache.
|
|
52
|
+
* @returns {string[]} - The keys.
|
|
53
|
+
*/
|
|
54
|
+
keys(): string[];
|
|
55
|
+
/**
|
|
56
|
+
* Get cache statistics.
|
|
57
|
+
* @returns {{size: number, maxSize: number, hits: number, misses: number}} - The statistics.
|
|
58
|
+
*/
|
|
59
|
+
getStats(): {
|
|
60
|
+
size: number;
|
|
61
|
+
maxSize: number;
|
|
62
|
+
hits: number;
|
|
63
|
+
misses: number;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
export type cacheEntryType<T> = {
|
|
67
|
+
hitCount: number;
|
|
68
|
+
value: T;
|
|
69
|
+
};
|