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
package/README.md CHANGED
@@ -31,10 +31,29 @@ Please visit the [node-oracledb](https://node-oracledb.readthedocs.io/en/latest/
31
31
  * Start server using `node examples/config-native.js` after having set the WEB_PLSQL_ORACLE_SERVER environment variable to the database where you just installed the sample schema.
32
32
  * Invoke a browser and open the page `http://localhost/sample`.
33
33
 
34
- ## Container
35
- * Build the image using `npm run image-build`.
36
- * Adapt the `examples/config-docker.json` configuration file. Typically only the `connectString` property must be changed.
37
- * Start the container using `docker compose up -d`.
34
+ # Admin Console
35
+
36
+ `web_plsql` includes a built-in Admin Console for real-time monitoring and management of your gateway.
37
+
38
+ ## Features
39
+ - **Dashboard**: Real-time charts showing requests per second and error rates.
40
+ - **Pool Monitoring**: Visual representation of database connection pool usage.
41
+ - **Cache Management**: View and manage the metadata cache.
42
+ - **Access & Error Logs**: Interactive viewers for the server logs.
43
+ - **System Info**: Overview of the server environment and configuration.
44
+
45
+ ## Configuration
46
+ The admin console is enabled by default in the `startServer` API. You can configure it via the `admin` property in the main configuration object.
47
+
48
+ ```typescript
49
+ /**
50
+ * @typedef {object} configAdminType
51
+ * @property {boolean} [enabled=true] - Whether the admin console is enabled.
52
+ * @property {string} [route='/admin'] - The route path for the admin console.
53
+ */
54
+ ```
55
+
56
+ Access the console at `http://localhost:<port>/admin` (e.g., `http://localhost:8080/admin`).
38
57
 
39
58
  # Configuration
40
59
 
@@ -93,6 +112,7 @@ The `startServer` api uses the following configuration object:
93
112
  /**
94
113
  * @typedef {object} configType
95
114
  * @property {number} port - The server port number.
115
+ * @property {configAdminType} [admin] - The admin console configuration.
96
116
  * @property {configStaticType[]} routeStatic - The static routes.
97
117
  * @property {configPlSqlType[]} routePlSql - The PL/SQL routes.
98
118
  * @property {number} [uploadFileSizeLimit] - Maximum size of each uploaded file in bytes or no limit if omitted.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web_plsql",
3
- "version": "0.17.1",
3
+ "version": "1.0.0",
4
4
  "author": "Dieter Oberkofler <dieter.oberkofler@gmail.com>",
5
5
  "license": "MIT",
6
6
  "description": "The Express Middleware for Oracle PL/SQL",
@@ -43,12 +43,17 @@
43
43
  "types": "./types/index.d.ts",
44
44
  "type": "module",
45
45
  "scripts": {
46
- "lint": "prettier --check --experimental-cli . && eslint . && tsc --noEmit",
47
- "test": "node --test --test-concurrency=1 \"./tests/**/*.test.js\"",
46
+ "build": "npm run build:admin",
47
+ "build:admin": "vite build",
48
+ "lint": "prettier --check --experimental-cli . && eslint . && tsc --noEmit && eslint src/admin --config src/admin/js/eslint.config.js",
49
+ "test": "vitest run",
50
+ "test:coverage": "vitest run --coverage",
51
+ "stylelint": "stylelint \"src/admin/**/*.css\"",
48
52
  "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
53
  "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
54
  "version": "node update-version.js",
55
+ "prepack": "npm run ci",
56
+ "ci": "npm run clean && npm run version && npm run types && npm run build && npm run lint && npm run stylelint && npm run test:coverage",
52
57
  "create-package": "shx rm -f *.tgz && npm pack",
53
58
  "image-build": "docker build --no-cache --progress=plain --tag=web_plsql .",
54
59
  "image-save": "docker save web_plsql | gzip > web_plsql.tar.gz"
@@ -57,6 +62,7 @@
57
62
  "oracledb": "6.10.0"
58
63
  },
59
64
  "dependencies": {
65
+ "chart.js": "^4.5.1",
60
66
  "compression": "1.8.1",
61
67
  "cookie-parser": "1.4.7",
62
68
  "debug": "4.4.3",
@@ -65,24 +71,31 @@
65
71
  "morgan": "1.10.1",
66
72
  "multer": "2.0.2",
67
73
  "rotating-file-stream": "3.2.8",
68
- "zod": "4.3.6"
74
+ "zod": "^4.3.6"
69
75
  },
70
76
  "devDependencies": {
77
+ "@tailwindcss/vite": "^4.1.18",
71
78
  "@types/compression": "1.8.1",
72
79
  "@types/cookie-parser": "1.4.10",
73
80
  "@types/debug": "4.1.12",
74
81
  "@types/morgan": "1.9.10",
75
82
  "@types/multer": "2.0.0",
76
- "@types/node": "25.2.0",
83
+ "@types/node": "25.2.1",
77
84
  "@types/oracledb": "6.10.1",
78
85
  "@types/supertest": "6.0.3",
86
+ "@vitest/coverage-v8": "^4.0.18",
79
87
  "eslint": "9.39.2",
80
- "eslint-plugin-jsdoc": "62.5.1",
88
+ "eslint-plugin-jsdoc": "62.5.3",
81
89
  "oracledb": "6.10.0",
82
90
  "prettier": "3.8.1",
83
91
  "shx": "0.4.0",
92
+ "stylelint": "^17.1.1",
93
+ "stylelint-config-standard": "^40.0.0",
84
94
  "supertest": "7.2.2",
95
+ "tailwindcss": "^4.1.18",
85
96
  "typescript": "5.9.3",
86
- "typescript-eslint": "8.54.0"
97
+ "typescript-eslint": "8.54.0",
98
+ "vite": "^7.3.1",
99
+ "vitest": "^4.0.18"
87
100
  }
88
101
  }
@@ -0,0 +1,299 @@
1
+ import {Chart, registerables} from 'chart.js';
2
+ import type {State, ChartInstance} from '../js/types.js';
3
+ import type {StatusResponse} from '../js/schemas.js';
4
+
5
+ // Register Chart.js components
6
+ Chart.register(...registerables);
7
+
8
+ /**
9
+ * Cache pie chart instances.
10
+ */
11
+ const cachePieInstances = new Map<HTMLCanvasElement, Chart>();
12
+
13
+ type PoolInfo = StatusResponse['pools'][number];
14
+
15
+ /**
16
+ * Chart colors based on theme mode.
17
+ */
18
+ type ChartColors = {
19
+ gridColor: string;
20
+ textColor: string;
21
+ };
22
+
23
+ /**
24
+ * Get chart colors for the current theme.
25
+ * @param isDark - Whether dark mode is active.
26
+ * @returns Chart color configuration.
27
+ */
28
+ function getChartColors(isDark: boolean): ChartColors {
29
+ return {
30
+ gridColor: isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
31
+ textColor: isDark ? '#94a3b8' : '#475569',
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Initialize charts.
37
+ * @param state - Application state.
38
+ */
39
+ export function initCharts(state: State): void {
40
+ const isDark = document.body.classList.contains('dark');
41
+ const colors = getChartColors(isDark);
42
+
43
+ const trafficEl = document.getElementById('traffic-chart') as HTMLCanvasElement | null;
44
+ if (trafficEl) {
45
+ const chartData = {
46
+ labels: [],
47
+ datasets: [
48
+ {
49
+ label: 'Requests/s',
50
+ data: [],
51
+ borderColor: '#3b82f6',
52
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
53
+ fill: true,
54
+ tension: 0.4,
55
+ yAxisID: 'y',
56
+ },
57
+ {
58
+ label: 'Avg Response Time (ms)',
59
+ data: [],
60
+ borderColor: '#10b981',
61
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
62
+ fill: true,
63
+ tension: 0.4,
64
+ yAxisID: 'y1',
65
+ },
66
+ ],
67
+ };
68
+
69
+ const chartOptions = {
70
+ responsive: true,
71
+ maintainAspectRatio: false,
72
+ scales: {
73
+ x: {
74
+ grid: {color: colors.gridColor},
75
+ ticks: {
76
+ color: colors.textColor,
77
+ maxRotation: 0,
78
+ autoSkip: true,
79
+ maxTicksLimit: 10,
80
+ },
81
+ title: {
82
+ display: true,
83
+ text: 'Time',
84
+ color: colors.textColor,
85
+ },
86
+ },
87
+ y: {
88
+ type: 'linear' as const,
89
+ display: true,
90
+ position: 'left' as const,
91
+ grid: {color: colors.gridColor},
92
+ ticks: {color: colors.textColor},
93
+ beginAtZero: true,
94
+ title: {
95
+ display: true,
96
+ text: 'Requests/s',
97
+ color: colors.textColor,
98
+ },
99
+ },
100
+ y1: {
101
+ type: 'linear' as const,
102
+ display: true,
103
+ position: 'right' as const,
104
+ grid: {drawOnChartArea: false, color: colors.gridColor},
105
+ ticks: {color: '#10b981'},
106
+ beginAtZero: true,
107
+ title: {
108
+ display: true,
109
+ text: 'Avg Response Time (ms)',
110
+ color: '#10b981',
111
+ },
112
+ },
113
+ },
114
+ plugins: {
115
+ legend: {
116
+ labels: {color: colors.textColor},
117
+ },
118
+ },
119
+ };
120
+
121
+ const chart = new Chart(trafficEl, {
122
+ type: 'line',
123
+ data: chartData,
124
+ options: chartOptions,
125
+ });
126
+
127
+ state.charts.traffic = {
128
+ data: chartData,
129
+ options: chartOptions,
130
+ update: () => chart.update(),
131
+ } as ChartInstance;
132
+ }
133
+
134
+ const poolEl = document.getElementById('pool-chart') as HTMLCanvasElement | null;
135
+ if (poolEl) {
136
+ const chartData = {
137
+ labels: [],
138
+ datasets: [],
139
+ };
140
+
141
+ const chartOptions = {
142
+ responsive: true,
143
+ maintainAspectRatio: false,
144
+ scales: {
145
+ x: {
146
+ grid: {color: colors.gridColor},
147
+ ticks: {
148
+ color: colors.textColor,
149
+ maxRotation: 0,
150
+ autoSkip: true,
151
+ maxTicksLimit: 10,
152
+ },
153
+ title: {
154
+ display: true,
155
+ text: 'Time',
156
+ color: colors.textColor,
157
+ },
158
+ },
159
+ y: {
160
+ grid: {color: colors.gridColor},
161
+ ticks: {color: colors.textColor},
162
+ beginAtZero: true,
163
+ title: {
164
+ display: true,
165
+ text: 'Connections',
166
+ color: colors.textColor,
167
+ },
168
+ },
169
+ },
170
+ plugins: {
171
+ legend: {
172
+ labels: {color: colors.textColor},
173
+ },
174
+ },
175
+ };
176
+
177
+ const chart = new Chart(poolEl, {
178
+ type: 'line',
179
+ data: chartData,
180
+ options: chartOptions,
181
+ });
182
+
183
+ state.charts.pool = {
184
+ data: chartData,
185
+ options: chartOptions,
186
+ update: () => chart.update(),
187
+ } as ChartInstance;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Update charts with new data.
193
+ * @param state - Application state.
194
+ * @param timeLabel - Time label for the data point.
195
+ * @param reqPerSec - Requests per second.
196
+ * @param avgResponseTime - Average response time in ms.
197
+ * @param pools - Pool information.
198
+ */
199
+ export function updateCharts(state: State, timeLabel: string, reqPerSec: number, avgResponseTime: number, pools: PoolInfo[]): void {
200
+ const maxPoints = state.maxHistoryPoints;
201
+
202
+ state.history.labels.push(timeLabel);
203
+ state.history.requests.push(reqPerSec);
204
+ state.history.avgResponseTimes.push(avgResponseTime);
205
+
206
+ if (state.history.labels.length > maxPoints) {
207
+ state.history.labels.shift();
208
+ state.history.requests.shift();
209
+ state.history.avgResponseTimes.shift();
210
+ }
211
+
212
+ const trafficChart = state.charts.traffic;
213
+ if (trafficChart?.data.datasets[0] && trafficChart.data.datasets[1]) {
214
+ trafficChart.data.labels = state.history.labels;
215
+ trafficChart.data.datasets[0].data = state.history.requests;
216
+ trafficChart.data.datasets[1].data = state.history.avgResponseTimes;
217
+ trafficChart.update();
218
+ }
219
+
220
+ const poolChart = state.charts.pool;
221
+ if (poolChart) {
222
+ poolChart.data.labels = state.history.labels;
223
+ pools.forEach((p, i) => {
224
+ const poolName = p.name;
225
+ const history = state.history;
226
+ let usage = history.poolUsage[poolName];
227
+ if (!usage) {
228
+ usage = [];
229
+ history.poolUsage[poolName] = usage;
230
+ const colors = ['#10b981', '#f59e0b', '#3b82f6', '#8b5cf6', '#ec4899'];
231
+ const color = colors[i % colors.length];
232
+ if (color) {
233
+ poolChart.data.datasets.push({
234
+ label: poolName,
235
+ data: usage,
236
+ borderColor: color,
237
+ backgroundColor: undefined,
238
+ fill: undefined,
239
+ tension: 0.4,
240
+ });
241
+ }
242
+ }
243
+ usage.push(p.connectionsInUse);
244
+ if (usage.length > maxPoints) {
245
+ usage.shift();
246
+ }
247
+ });
248
+ poolChart.update();
249
+ }
250
+ }
251
+
252
+ /**
253
+ * Create or update a pie chart for cache statistics.
254
+ * @param canvas - The canvas element.
255
+ */
256
+ export function renderCachePie(canvas: HTMLCanvasElement): void {
257
+ const hits = parseInt(canvas.dataset.hits ?? '0');
258
+ const misses = parseInt(canvas.dataset.misses ?? '0');
259
+
260
+ const existingChart = cachePieInstances.get(canvas);
261
+ if (existingChart?.data.datasets[0]) {
262
+ existingChart.data.datasets[0].data = [hits, misses];
263
+ existingChart.update();
264
+ return;
265
+ }
266
+
267
+ const chart = new Chart(canvas, {
268
+ type: 'pie',
269
+ data: {
270
+ labels: ['Hits', 'Misses'],
271
+ datasets: [
272
+ {
273
+ data: [hits, misses],
274
+ backgroundColor: ['#10b981', '#ef4444'],
275
+ borderColor: 'transparent',
276
+ },
277
+ ],
278
+ },
279
+ options: {
280
+ responsive: true,
281
+ maintainAspectRatio: true,
282
+ plugins: {
283
+ legend: {display: false},
284
+ tooltip: {
285
+ callbacks: {
286
+ label: (context) => {
287
+ const value = context.raw as number;
288
+ const total = hits + misses;
289
+ const pct = total > 0 ? ((value / total) * 100).toFixed(1) : '0';
290
+ return `${context.label}: ${value} (${pct}%)`;
291
+ },
292
+ },
293
+ },
294
+ },
295
+ },
296
+ });
297
+
298
+ cachePieInstances.set(canvas, chart);
299
+ }
@@ -0,0 +1,3 @@
1
+ import '../js/api.js';
2
+ import '../js/app.js';
3
+ import './tailwind.css';
@@ -0,0 +1,41 @@
1
+ @import 'tailwindcss';
2
+
3
+ @theme {
4
+ --color-primary: #3b82f6;
5
+ --color-success: #10b981;
6
+ --color-warning: #f59e0b;
7
+ --color-danger: #ef4444;
8
+ }
9
+
10
+ body {
11
+ font-family:
12
+ 'Inter',
13
+ -apple-system,
14
+ BlinkMacSystemFont,
15
+ 'Segoe UI',
16
+ Roboto,
17
+ Helvetica,
18
+ Arial,
19
+ sans-serif;
20
+ -webkit-font-smoothing: antialiased;
21
+ }
22
+
23
+ body.dark {
24
+ --bg-main: #0f172a;
25
+ --bg-side: #1e293b;
26
+ --bg-card: #1e293b;
27
+ --bg-hover: #334155;
28
+ --text-main: #94a3b8;
29
+ --text-bright: #f8fafc;
30
+ --border: #334155;
31
+ }
32
+
33
+ body:not(.dark) {
34
+ --bg-main: #f8fafc;
35
+ --bg-side: #ffffff;
36
+ --bg-card: #ffffff;
37
+ --bg-hover: #f1f5f9;
38
+ --text-main: #475569;
39
+ --text-bright: #0f172a;
40
+ --border: #e2e8f0;
41
+ }
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#3b82f6">
2
+ <path d="M12 2C6.48 2 2 4.02 2 6.5s4.48 4.5 10 4.5 10-2.02 10-4.5S17.52 2 12 2zm0 13c-5.52 0-10-2.02-10-4.5V14c0 2.48 4.48 4.5 10 4.5s10-2.02 10-4.5v-3.5c0 2.48-4.48 4.5-10 4.5zm0 4.5c-5.52 0-10-2.02-10-4.5V19c0 2.48 4.48 4.5 10 4.5s10-2.02 10-4.5v-2.5c0 2.48-4.48 4.5-10 4.5z"/>
3
+ </svg>