web_plsql 1.0.0 → 1.2.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 (51) hide show
  1. package/README.md +5 -5
  2. package/package.json +5 -5
  3. package/src/admin/client/charts.ts +303 -4
  4. package/src/admin/index.html +283 -69
  5. package/src/admin/js/api.ts +95 -20
  6. package/src/admin/js/app.ts +460 -152
  7. package/src/admin/js/schemas.ts +76 -14
  8. package/src/admin/js/templates/config.ts +10 -9
  9. package/src/admin/js/templates/errorRow.ts +8 -5
  10. package/src/admin/js/templates/index.ts +1 -0
  11. package/src/admin/js/templates/poolCard.ts +6 -6
  12. package/src/admin/js/templates/traceRow.ts +24 -0
  13. package/src/admin/js/types.ts +132 -19
  14. package/src/admin/js/ui/components.ts +44 -0
  15. package/src/admin/js/ui/table.ts +173 -0
  16. package/src/admin/js/ui/views.ts +311 -48
  17. package/src/admin/js/util/format.ts +22 -3
  18. package/src/admin/js/util/metrics.ts +24 -0
  19. package/src/admin/lib/chart.bundle.css +1 -0
  20. package/src/admin/lib/chart.bundle.js +54 -47
  21. package/src/admin/style.css +143 -27
  22. package/src/handler/handlerAdmin.js +121 -26
  23. package/src/handler/plsql/errorPage.js +0 -4
  24. package/src/handler/plsql/owaPageStream.js +93 -0
  25. package/src/handler/plsql/procedure.js +191 -121
  26. package/src/handler/plsql/procedureNamed.js +17 -3
  27. package/src/handler/plsql/procedureSanitize.js +24 -0
  28. package/src/handler/plsql/procedureVariable.js +2 -0
  29. package/src/handler/plsql/sendResponse.js +41 -3
  30. package/src/index.js +0 -1
  31. package/src/server/adminContext.js +23 -0
  32. package/src/server/server.js +83 -68
  33. package/src/types.js +1 -1
  34. package/src/util/statsManager.js +368 -0
  35. package/src/util/trace.js +2 -1
  36. package/src/util/traceManager.js +68 -0
  37. package/src/version.js +1 -1
  38. package/types/admin/js/schemas.d.ts +384 -0
  39. package/types/admin/js/types.d.ts +312 -0
  40. package/types/handler/handlerAdmin.d.ts +70 -0
  41. package/types/handler/plsql/owaPageStream.d.ts +28 -0
  42. package/types/handler/plsql/procedure.d.ts +1 -0
  43. package/types/index.d.ts +0 -1
  44. package/types/server/adminContext.d.ts +17 -0
  45. package/types/server/server.d.ts +2 -19
  46. package/types/types.d.ts +1 -1
  47. package/types/util/statsManager.d.ts +395 -0
  48. package/types/util/traceManager.d.ts +35 -0
  49. package/src/admin/lib/assets/main-zpdhQ1gD.css +0 -1
  50. package/src/handler/handlerMetrics.js +0 -68
  51. package/types/handler/handlerMetrics.d.ts +0 -25
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  [![NPM Version][npm-image]][npm-url]
2
2
  [![NPM Downloads][downloads-image]][downloads-url]
3
- [![Linux Build](https://travis-ci.org/doberkofler/web_plsql.svg?branch=master)](https://travis-ci.org/doberkofler/web_plsql)
4
- [![Build status][appveyor-image]][appveyor-url]
3
+ [![Node.js CI](https://github.com/doberkofler/web_plsql/actions/workflows/node.js.yml/badge.svg)](https://github.com/doberkofler/web_plsql/actions/workflows/node.js.yml)
5
4
  [![Coverage Status](https://coveralls.io/repos/github/doberkofler/web_plsql/badge.svg?branch=master)](https://coveralls.io/github/doberkofler/web_plsql?branch=master)
6
5
 
7
6
  # Oracle PL/SQL Gateway Middleware for the Express web framework for Node.js
@@ -12,6 +11,8 @@ and serve the content using the Express web framework for Node.js.
12
11
 
13
12
  Please feel free to try and suggest any improvements. Your thoughts and ideas are most welcome.
14
13
 
14
+ ![Dashboard](docs/images/admin_console.png)
15
+
15
16
  # Release History
16
17
  See the [changelog](https://github.com/doberkofler/web_plsql/blob/master/CHANGELOG.md).
17
18
 
@@ -200,6 +201,8 @@ The following mod_plsql DAD configuration translates to the configuration option
200
201
  - PlsqlRequestValidationFunction -> routePlSql[].pathAliasProcedure
201
202
  - PlsqlExclusionList
202
203
  - Basic and custom authentication methods, based on the OWA_SEC package and custom packages.
204
+ - Caching of procedure metadata and validation results for high performance.
205
+ - Real-time monitoring and management via a built-in Admin Console.
203
206
 
204
207
  ## Features that are only available in web_plsql
205
208
  - The option `transactionModeType` specifies an optional transaction mode.
@@ -233,6 +236,3 @@ The following mod_plsql DAD configuration translates to the configuration option
233
236
 
234
237
  [downloads-image]: https://img.shields.io/npm/dm/web_plsql.svg
235
238
  [downloads-url]: https://npmjs.org/package/web_plsql
236
-
237
- [appveyor-image]: https://ci.appveyor.com/api/projects/status/github/doberkofler/web_plsql?branch=master&svg=true
238
- [appveyor-url]: https://ci.appveyor.com/project/doberkofler/web-plsql
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "web_plsql",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "author": "Dieter Oberkofler <dieter.oberkofler@gmail.com>",
5
5
  "license": "MIT",
6
6
  "description": "The Express Middleware for Oracle PL/SQL",
@@ -47,14 +47,14 @@
47
47
  "build:admin": "vite build",
48
48
  "lint": "prettier --check --experimental-cli . && eslint . && tsc --noEmit && eslint src/admin --config src/admin/js/eslint.config.js",
49
49
  "test": "vitest run",
50
+ "test:perf": "vitest run tests/performance.test.js",
50
51
  "test:coverage": "vitest run --coverage",
51
52
  "stylelint": "stylelint \"src/admin/**/*.css\"",
52
53
  "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",
53
54
  "clean": "shx rm -f *.tgz && shx rm -f *.log",
54
55
  "version": "node update-version.js",
55
- "prepack": "npm run ci",
56
56
  "ci": "npm run clean && npm run version && npm run types && npm run build && npm run lint && npm run stylelint && npm run test:coverage",
57
- "create-package": "shx rm -f *.tgz && npm pack",
57
+ "create-package": "npm run ci && shx rm -f *.tgz && npm pack",
58
58
  "image-build": "docker build --no-cache --progress=plain --tag=web_plsql .",
59
59
  "image-save": "docker save web_plsql | gzip > web_plsql.tar.gz"
60
60
  },
@@ -80,12 +80,12 @@
80
80
  "@types/debug": "4.1.12",
81
81
  "@types/morgan": "1.9.10",
82
82
  "@types/multer": "2.0.0",
83
- "@types/node": "25.2.1",
83
+ "@types/node": "25.2.2",
84
84
  "@types/oracledb": "6.10.1",
85
85
  "@types/supertest": "6.0.3",
86
86
  "@vitest/coverage-v8": "^4.0.18",
87
87
  "eslint": "9.39.2",
88
- "eslint-plugin-jsdoc": "62.5.3",
88
+ "eslint-plugin-jsdoc": "62.5.4",
89
89
  "oracledb": "6.10.0",
90
90
  "prettier": "3.8.1",
91
91
  "shx": "0.4.0",
@@ -1,5 +1,5 @@
1
1
  import {Chart, registerables} from 'chart.js';
2
- import type {State, ChartInstance} from '../js/types.js';
2
+ import type {State, ChartInstance, HistoryBucket} from '../js/types.js';
3
3
  import type {StatusResponse} from '../js/schemas.js';
4
4
 
5
5
  // Register Chart.js components
@@ -40,6 +40,182 @@ export function initCharts(state: State): void {
40
40
  const isDark = document.body.classList.contains('dark');
41
41
  const colors = getChartColors(isDark);
42
42
 
43
+ const memoryEl = document.getElementById('memory-chart') as HTMLCanvasElement | null;
44
+ if (memoryEl) {
45
+ const chartData = {
46
+ labels: [],
47
+ datasets: [
48
+ {
49
+ label: 'Memory Usage (%)',
50
+ data: [],
51
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
52
+ borderColor: '#10b981',
53
+ borderWidth: 2,
54
+ fill: true,
55
+ tension: 0.4,
56
+ },
57
+ ],
58
+ };
59
+
60
+ const chartOptions = {
61
+ responsive: true,
62
+ maintainAspectRatio: false,
63
+ scales: {
64
+ x: {
65
+ grid: {color: colors.gridColor},
66
+ ticks: {
67
+ display: false,
68
+ color: colors.textColor,
69
+ maxRotation: 0,
70
+ autoSkip: true,
71
+ maxTicksLimit: 10,
72
+ },
73
+ title: {
74
+ display: false,
75
+ text: 'Time',
76
+ color: colors.textColor,
77
+ },
78
+ },
79
+ y: {
80
+ display: true,
81
+ beginAtZero: true,
82
+ max: 100,
83
+ grid: {
84
+ color: colors.gridColor,
85
+ },
86
+ border: {
87
+ display: true,
88
+ color: colors.gridColor,
89
+ },
90
+ ticks: {
91
+ display: true,
92
+ color: colors.textColor,
93
+ stepSize: 25,
94
+ callback: function (value: number | string) {
95
+ return String(value) + '%';
96
+ },
97
+ },
98
+ title: {
99
+ display: false,
100
+ text: 'Memory %',
101
+ color: colors.textColor,
102
+ },
103
+ },
104
+ },
105
+ layout: {
106
+ padding: 0,
107
+ },
108
+ plugins: {
109
+ legend: {display: false},
110
+ tooltip: {
111
+ enabled: true,
112
+ position: 'nearest' as const,
113
+ intersect: false,
114
+ },
115
+ },
116
+ };
117
+
118
+ const chart = new Chart(memoryEl, {
119
+ type: 'line',
120
+ data: chartData,
121
+ options: chartOptions,
122
+ });
123
+
124
+ state.charts.memory = {
125
+ data: chartData,
126
+ options: chartOptions,
127
+ update: () => chart.update(),
128
+ } as ChartInstance;
129
+ }
130
+
131
+ const cpuEl = document.getElementById('cpu-chart') as HTMLCanvasElement | null;
132
+ if (cpuEl) {
133
+ const chartData = {
134
+ labels: [],
135
+ datasets: [
136
+ {
137
+ label: 'CPU Usage (%)',
138
+ data: [],
139
+ backgroundColor: 'rgba(16, 185, 129, 0.1)',
140
+ borderColor: '#10b981',
141
+ borderWidth: 2,
142
+ fill: true,
143
+ tension: 0.4,
144
+ },
145
+ ],
146
+ };
147
+
148
+ const chartOptions = {
149
+ responsive: true,
150
+ maintainAspectRatio: false,
151
+ scales: {
152
+ x: {
153
+ grid: {color: colors.gridColor},
154
+ ticks: {
155
+ display: false,
156
+ color: colors.textColor,
157
+ maxRotation: 0,
158
+ autoSkip: true,
159
+ maxTicksLimit: 10,
160
+ },
161
+ title: {
162
+ display: false,
163
+ text: 'Time',
164
+ color: colors.textColor,
165
+ },
166
+ },
167
+ y: {
168
+ display: true,
169
+ beginAtZero: true,
170
+ max: 100,
171
+ grid: {
172
+ color: colors.gridColor,
173
+ },
174
+ border: {
175
+ display: true,
176
+ color: colors.gridColor,
177
+ },
178
+ ticks: {
179
+ display: true,
180
+ color: colors.textColor,
181
+ stepSize: 25,
182
+ callback: function (value: number | string) {
183
+ return String(value) + '%';
184
+ },
185
+ },
186
+ title: {
187
+ display: false,
188
+ text: 'Cpu %',
189
+ color: colors.textColor,
190
+ },
191
+ },
192
+ },
193
+ layout: {
194
+ padding: 0,
195
+ },
196
+ plugins: {
197
+ legend: {display: false},
198
+ tooltip: {
199
+ enabled: true,
200
+ position: 'nearest' as const,
201
+ intersect: false,
202
+ },
203
+ },
204
+ };
205
+
206
+ const chart = new Chart(cpuEl, {
207
+ type: 'line',
208
+ data: chartData,
209
+ options: chartOptions,
210
+ });
211
+
212
+ state.charts.cpu = {
213
+ data: chartData,
214
+ options: chartOptions,
215
+ update: () => chart.update(),
216
+ } as ChartInstance;
217
+ }
218
+
43
219
  const trafficEl = document.getElementById('traffic-chart') as HTMLCanvasElement | null;
44
220
  if (trafficEl) {
45
221
  const chartData = {
@@ -73,6 +249,7 @@ export function initCharts(state: State): void {
73
249
  x: {
74
250
  grid: {color: colors.gridColor},
75
251
  ticks: {
252
+ display: false,
76
253
  color: colors.textColor,
77
254
  maxRotation: 0,
78
255
  autoSkip: true,
@@ -145,6 +322,7 @@ export function initCharts(state: State): void {
145
322
  x: {
146
323
  grid: {color: colors.gridColor},
147
324
  ticks: {
325
+ display: false,
148
326
  color: colors.textColor,
149
327
  maxRotation: 0,
150
328
  autoSkip: true,
@@ -188,6 +366,99 @@ export function initCharts(state: State): void {
188
366
  }
189
367
  }
190
368
 
369
+ /**
370
+ * Hydrate charts and state from server history.
371
+ * @param state - Application state.
372
+ * @param history - Server history buffer.
373
+ */
374
+ export function hydrateHistory(state: State, history: HistoryBucket[]): void {
375
+ state.history.labels = [];
376
+ state.history.requests = [];
377
+ state.history.avgResponseTimes = [];
378
+ state.history.p95ResponseTimes = [];
379
+ state.history.p99ResponseTimes = [];
380
+ state.history.cpuUsage = [];
381
+ state.history.memoryUsage = [];
382
+ state.history.poolUsage = {};
383
+
384
+ const maxPoints = state.maxHistoryPoints;
385
+ const recentHistory = history.slice(-maxPoints);
386
+ const intervalSec = (state.status.intervalMs ?? 5000) / 1000;
387
+
388
+ recentHistory.forEach((b) => {
389
+ const timeLabel = new Date(b.timestamp).toLocaleTimeString();
390
+ state.history.labels.push(timeLabel);
391
+ state.history.requests.push(b.requests / intervalSec); // Requests per second
392
+ state.history.avgResponseTimes.push(b.durationAvg);
393
+ state.history.p95ResponseTimes ??= [];
394
+ state.history.p95ResponseTimes.push(b.durationP95);
395
+ state.history.p99ResponseTimes ??= [];
396
+ state.history.p99ResponseTimes.push(b.durationP99);
397
+
398
+ // Resource usage percentage
399
+ const totalMem = state.status.system?.memory.totalMemory ?? 0;
400
+ state.history.cpuUsage.push(b.system.cpu);
401
+ state.history.memoryUsage.push(totalMem > 0 ? (b.system.rss / totalMem) * 100 : 0);
402
+
403
+ b.pools.forEach((p) => {
404
+ state.history.poolUsage[p.name] ??= [];
405
+ state.history.poolUsage[p.name]?.push(p.connectionsInUse);
406
+ });
407
+ });
408
+
409
+ const trafficChart = state.charts.traffic;
410
+ if (trafficChart) {
411
+ trafficChart.data.labels = state.history.labels;
412
+ const ds0 = trafficChart.data.datasets[0];
413
+ const ds1 = trafficChart.data.datasets[1];
414
+ if (ds0) ds0.data = state.history.requests;
415
+ if (ds1) ds1.data = state.history.avgResponseTimes;
416
+ trafficChart.update();
417
+ }
418
+
419
+ const poolChart = state.charts.pool;
420
+ if (poolChart) {
421
+ poolChart.data.labels = state.history.labels;
422
+ Object.entries(state.history.poolUsage).forEach(([name, usage], i) => {
423
+ let dataset = poolChart.data.datasets.find((ds) => ds.label === name);
424
+ if (!dataset) {
425
+ const colors = ['#10b981', '#f59e0b', '#3b82f6', '#8b5cf6', '#ec4899'];
426
+ const color = colors[i % colors.length] ?? '#3b82f6';
427
+ dataset = {
428
+ label: name,
429
+ data: usage,
430
+ borderColor: color,
431
+ backgroundColor: undefined,
432
+ fill: undefined,
433
+ tension: 0.4,
434
+ };
435
+ poolChart.data.datasets.push(dataset);
436
+ } else {
437
+ dataset.data = usage;
438
+ }
439
+ });
440
+ poolChart.update();
441
+ }
442
+
443
+ const memoryChart = state.charts.memory;
444
+ if (memoryChart) {
445
+ memoryChart.data.labels = state.history.labels;
446
+ if (memoryChart.data.datasets[0]) {
447
+ memoryChart.data.datasets[0].data = state.history.memoryUsage;
448
+ }
449
+ memoryChart.update();
450
+ }
451
+
452
+ const cpuChart = state.charts.cpu;
453
+ if (cpuChart) {
454
+ cpuChart.data.labels = state.history.labels;
455
+ if (cpuChart.data.datasets[0]) {
456
+ cpuChart.data.datasets[0].data = state.history.cpuUsage;
457
+ }
458
+ cpuChart.update();
459
+ }
460
+ }
461
+
191
462
  /**
192
463
  * Update charts with new data.
193
464
  * @param state - Application state.
@@ -203,10 +474,23 @@ export function updateCharts(state: State, timeLabel: string, reqPerSec: number,
203
474
  state.history.requests.push(reqPerSec);
204
475
  state.history.avgResponseTimes.push(avgResponseTime);
205
476
 
477
+ // Resource usage percentage
478
+ const totalMem = state.status.system?.memory.totalMemory ?? 0;
479
+ const latestBucket = state.status.history?.[state.status.history.length - 1];
480
+ if (latestBucket) {
481
+ state.history.cpuUsage.push(latestBucket.system.cpu);
482
+ state.history.memoryUsage.push(totalMem > 0 ? (latestBucket.system.rss / totalMem) * 100 : 0);
483
+ } else {
484
+ state.history.cpuUsage.push(0);
485
+ state.history.memoryUsage.push(0);
486
+ }
487
+
206
488
  if (state.history.labels.length > maxPoints) {
207
489
  state.history.labels.shift();
208
490
  state.history.requests.shift();
209
491
  state.history.avgResponseTimes.shift();
492
+ state.history.cpuUsage.shift();
493
+ state.history.memoryUsage.shift();
210
494
  }
211
495
 
212
496
  const trafficChart = state.charts.traffic;
@@ -217,6 +501,20 @@ export function updateCharts(state: State, timeLabel: string, reqPerSec: number,
217
501
  trafficChart.update();
218
502
  }
219
503
 
504
+ const memoryChart = state.charts.memory;
505
+ if (memoryChart?.data.datasets[0]) {
506
+ memoryChart.data.labels = state.history.labels;
507
+ memoryChart.data.datasets[0].data = state.history.memoryUsage;
508
+ memoryChart.update();
509
+ }
510
+
511
+ const cpuChart = state.charts.cpu;
512
+ if (cpuChart?.data.datasets[0]) {
513
+ cpuChart.data.labels = state.history.labels;
514
+ cpuChart.data.datasets[0].data = state.history.cpuUsage;
515
+ cpuChart.update();
516
+ }
517
+
220
518
  const poolChart = state.charts.pool;
221
519
  if (poolChart) {
222
520
  poolChart.data.labels = state.history.labels;
@@ -228,7 +526,7 @@ export function updateCharts(state: State, timeLabel: string, reqPerSec: number,
228
526
  usage = [];
229
527
  history.poolUsage[poolName] = usage;
230
528
  const colors = ['#10b981', '#f59e0b', '#3b82f6', '#8b5cf6', '#ec4899'];
231
- const color = colors[i % colors.length];
529
+ const color = colors[i % colors.length] ?? '#3b82f6';
232
530
  if (color) {
233
531
  poolChart.data.datasets.push({
234
532
  label: poolName,
@@ -250,7 +548,8 @@ export function updateCharts(state: State, timeLabel: string, reqPerSec: number,
250
548
  }
251
549
 
252
550
  /**
253
- * Create or update a pie chart for cache statistics.
551
+ * Render a cache statistics pie chart.
552
+ *
254
553
  * @param canvas - The canvas element.
255
554
  */
256
555
  export function renderCachePie(canvas: HTMLCanvasElement): void {
@@ -287,7 +586,7 @@ export function renderCachePie(canvas: HTMLCanvasElement): void {
287
586
  const value = context.raw as number;
288
587
  const total = hits + misses;
289
588
  const pct = total > 0 ? ((value / total) * 100).toFixed(1) : '0';
290
- return `${context.label}: ${value} (${pct}%)`;
589
+ return `${context.label ?? ''}: ${value} (${pct}%)`;
291
590
  },
292
591
  },
293
592
  },