web_plsql 0.18.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 (74) hide show
  1. package/README.md +29 -9
  2. package/package.json +17 -7
  3. package/src/admin/client/charts.ts +598 -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 +529 -0
  8. package/src/admin/js/api.ts +170 -0
  9. package/src/admin/js/app.ts +614 -0
  10. package/src/admin/js/eslint.config.js +74 -0
  11. package/src/admin/js/schemas.ts +215 -0
  12. package/src/admin/js/templates/config.ts +147 -0
  13. package/src/admin/js/templates/errorRow.ts +21 -0
  14. package/src/admin/js/templates/index.ts +4 -0
  15. package/src/admin/js/templates/poolCard.ts +61 -0
  16. package/src/admin/js/templates/traceRow.ts +24 -0
  17. package/src/admin/js/tsconfig.json +24 -0
  18. package/src/admin/js/types.ts +336 -0
  19. package/src/admin/js/ui/components.ts +44 -0
  20. package/src/admin/js/ui/table.ts +173 -0
  21. package/src/admin/js/ui/theme.ts +93 -0
  22. package/src/admin/js/ui/views.ts +427 -0
  23. package/src/admin/js/util/format.ts +46 -0
  24. package/src/admin/js/util/metrics.ts +24 -0
  25. package/src/admin/lib/chart.bundle.css +1 -0
  26. package/src/admin/lib/chart.bundle.js +146 -0
  27. package/src/admin/style.css +1437 -0
  28. package/src/bin/load-test.js +202 -0
  29. package/src/handler/handlerAdmin.js +293 -0
  30. package/src/handler/plsql/cgi.js +1 -1
  31. package/src/handler/plsql/errorPage.js +31 -6
  32. package/src/handler/plsql/handlerPlSql.js +32 -6
  33. package/src/handler/plsql/owaPageStream.js +93 -0
  34. package/src/handler/plsql/parsePage.js +7 -3
  35. package/src/handler/plsql/procedure.js +221 -105
  36. package/src/handler/plsql/procedureNamed.js +34 -49
  37. package/src/handler/plsql/procedureSanitize.js +120 -45
  38. package/src/handler/plsql/procedureVariable.js +4 -2
  39. package/src/handler/plsql/request.js +8 -3
  40. package/src/handler/plsql/sendResponse.js +43 -5
  41. package/src/index.js +0 -1
  42. package/src/server/adminContext.js +23 -0
  43. package/src/server/config.js +1 -0
  44. package/src/server/server.js +130 -9
  45. package/src/types.js +7 -1
  46. package/src/util/cache.js +123 -0
  47. package/src/util/jsonLogger.js +47 -0
  48. package/src/util/shutdown.js +6 -2
  49. package/src/util/statsManager.js +368 -0
  50. package/src/util/trace.js +7 -6
  51. package/src/util/traceManager.js +68 -0
  52. package/src/version.js +1 -1
  53. package/types/admin/js/schemas.d.ts +384 -0
  54. package/types/admin/js/types.d.ts +312 -0
  55. package/types/handler/handlerAdmin.d.ts +79 -0
  56. package/types/handler/plsql/errorPage.d.ts +1 -0
  57. package/types/handler/plsql/handlerPlSql.d.ts +6 -1
  58. package/types/handler/plsql/owaPageStream.d.ts +28 -0
  59. package/types/handler/plsql/procedure.d.ts +4 -1
  60. package/types/handler/plsql/procedureNamed.d.ts +2 -5
  61. package/types/handler/plsql/procedureSanitize.d.ts +2 -5
  62. package/types/handler/plsql/procedureVariable.d.ts +1 -1
  63. package/types/handler/plsql/request.d.ts +3 -1
  64. package/types/handler/plsql/sendResponse.d.ts +1 -1
  65. package/types/index.d.ts +0 -1
  66. package/types/server/adminContext.d.ts +17 -0
  67. package/types/server/server.d.ts +5 -0
  68. package/types/types.d.ts +19 -1
  69. package/types/util/cache.d.ts +69 -0
  70. package/types/util/jsonLogger.d.ts +45 -0
  71. package/types/util/statsManager.d.ts +395 -0
  72. package/types/util/traceManager.d.ts +35 -0
  73. package/src/handler/handlerMetrics.js +0 -68
  74. package/types/handler/handlerMetrics.d.ts +0 -25
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Cache statistics for a cache type.
3
+ */
4
+ export type CacheStats = {
5
+ hits: number;
6
+ misses: number;
7
+ };
8
+
9
+ /**
10
+ * Cache information for a specific cache type.
11
+ */
12
+ export type CacheInfo = {
13
+ size: number;
14
+ stats: CacheStats;
15
+ };
16
+
17
+ /**
18
+ * Cache data for a connection pool.
19
+ */
20
+ export type CacheData = {
21
+ poolName: string;
22
+ procedureNameCache: CacheInfo;
23
+ argumentCache: CacheInfo;
24
+ };
25
+
26
+ /**
27
+ * Pool statistics.
28
+ */
29
+ export type PoolStats = {
30
+ totalRequests: number;
31
+ totalTimeouts: number;
32
+ totalRequestsEnqueued: number | undefined;
33
+ totalRequestsDequeued: number | undefined;
34
+ totalRequestsFailed: number | undefined;
35
+ };
36
+
37
+ /**
38
+ * Pool information.
39
+ */
40
+ export type PoolInfo = {
41
+ name: string;
42
+ connectionsInUse: number;
43
+ connectionsOpen: number;
44
+ stats: PoolStats | null;
45
+ };
46
+
47
+ /**
48
+ * Server metrics.
49
+ */
50
+ export type Metrics = {
51
+ requestCount: number;
52
+ errorCount: number;
53
+ avgResponseTime: number;
54
+ minResponseTime: number;
55
+ maxResponseTime: number;
56
+ maxRequestsPerSecond: number;
57
+ };
58
+
59
+ /**
60
+ * Server configuration - route.
61
+ */
62
+ export type RouteConfig = {
63
+ route: string;
64
+ // PL/SQL specific
65
+ user?: string;
66
+ password?: string;
67
+ connectString?: string;
68
+ defaultPage?: string;
69
+ pathAlias?: string;
70
+ pathAliasProcedure?: string;
71
+ documentTable?: string;
72
+ exclusionList?: string[];
73
+ requestValidationFunction?: string;
74
+ transactionMode?: unknown;
75
+ errorStyle?: string;
76
+ // Static specific
77
+ directoryPath?: string;
78
+ };
79
+
80
+ /**
81
+ * Server configuration.
82
+ */
83
+ export type ServerConfig = {
84
+ port: number;
85
+ adminRoute?: string;
86
+ adminUser?: string;
87
+ adminPassword?: string;
88
+ loggerFilename: string;
89
+ uploadFileSizeLimit?: number;
90
+ routePlSql: RouteConfig[];
91
+ routeStatic: RouteConfig[];
92
+ };
93
+
94
+ /**
95
+ * Server status response.
96
+ */
97
+ export type Status = {
98
+ version: string;
99
+ status: 'running' | 'paused' | 'stopped';
100
+ uptime: number;
101
+ startTime: string;
102
+ intervalMs?: number;
103
+ metrics: Metrics;
104
+ pools: PoolInfo[];
105
+ config: Partial<ServerConfig>;
106
+ };
107
+
108
+ /**
109
+ * Resident Set Size - total memory used by the process
110
+ */
111
+ export type HistoryBucket = {
112
+ timestamp: number;
113
+ requests: number;
114
+ errors: number;
115
+ durationMin: number;
116
+ durationMax: number;
117
+ durationAvg: number;
118
+ durationP95: number;
119
+ durationP99: number;
120
+ system: {
121
+ cpu: number;
122
+ heapUsed: number;
123
+ heapTotal: number;
124
+ rss: number;
125
+ external: number;
126
+ };
127
+ pools: {
128
+ name: string;
129
+ connectionsInUse: number;
130
+ connectionsOpen: number;
131
+ }[];
132
+ };
133
+
134
+ /**
135
+ * Historical data for charts.
136
+ */
137
+ export type HistoryData = {
138
+ labels: string[];
139
+ requests: number[];
140
+ avgResponseTimes: number[];
141
+ p95ResponseTimes?: number[];
142
+ p99ResponseTimes?: number[];
143
+ poolUsage: Record<string, number[]>;
144
+ };
145
+
146
+ /**
147
+ * Chart grid options.
148
+ */
149
+ export type ChartGridOptions = {
150
+ color?: string;
151
+ display?: boolean;
152
+ drawBorder?: boolean;
153
+ drawOnChartArea?: boolean;
154
+ };
155
+
156
+ /**
157
+ * Chart ticks options.
158
+ */
159
+ export type ChartTicksOptions = {
160
+ color?: string;
161
+ display?: boolean;
162
+ stepSize?: number;
163
+ maxRotation?: number;
164
+ autoSkip?: boolean;
165
+ maxTicksLimit?: number;
166
+ callback?: (value: number | string) => string;
167
+ };
168
+
169
+ /**
170
+ * Chart scale options.
171
+ */
172
+ export type ChartScaleOptions = {
173
+ display?: boolean;
174
+ grid?: ChartGridOptions;
175
+ ticks?: ChartTicksOptions;
176
+ border?: {
177
+ display?: boolean;
178
+ color?: string;
179
+ };
180
+ title?: {
181
+ display: boolean;
182
+ text: string;
183
+ color: string;
184
+ };
185
+ type?: string;
186
+ position?: string;
187
+ beginAtZero?: boolean;
188
+ max?: number;
189
+ };
190
+
191
+ /**
192
+ * Chart options.
193
+ */
194
+ export type ChartOptions = {
195
+ responsive?: boolean;
196
+ maintainAspectRatio?: boolean;
197
+ scales?: {
198
+ x?: ChartScaleOptions;
199
+ y?: ChartScaleOptions;
200
+ y1?: ChartScaleOptions;
201
+ };
202
+ layout?: {
203
+ padding?: number;
204
+ };
205
+ plugins?: {
206
+ legend?: {
207
+ display?: boolean;
208
+ labels?: {
209
+ color?: string;
210
+ };
211
+ };
212
+ tooltip?: {
213
+ enabled?: boolean;
214
+ position?: string;
215
+ intersect?: boolean;
216
+ callbacks?: Record<string, unknown>;
217
+ };
218
+ };
219
+ elements?: {
220
+ point?: {
221
+ radius?: number;
222
+ };
223
+ line?: {
224
+ borderWidth?: number;
225
+ };
226
+ bar?: {
227
+ borderWidth?: number;
228
+ };
229
+ };
230
+ };
231
+
232
+ /**
233
+ * Chart dataset.
234
+ */
235
+ export type ChartDataset = {
236
+ label: string;
237
+ data: number[];
238
+ borderColor: string;
239
+ backgroundColor: string | undefined;
240
+ fill?: boolean | undefined;
241
+ tension?: number | undefined;
242
+ barThickness?: number;
243
+ categoryPercentage?: number;
244
+ barPercentage?: number;
245
+ borderWidth?: number;
246
+ yAxisID?: string;
247
+ };
248
+
249
+ /**
250
+ * Chart data.
251
+ */
252
+ export type ChartData = {
253
+ labels: string[];
254
+ datasets: ChartDataset[];
255
+ };
256
+
257
+ /**
258
+ * Chart instance interface.
259
+ */
260
+ export type ChartInstance = {
261
+ data: ChartData;
262
+ options: ChartOptions;
263
+ update: () => void;
264
+ };
265
+
266
+ /**
267
+ * Trace entry.
268
+ */
269
+ export type TraceEntry = {
270
+ id: string;
271
+ timestamp: string;
272
+ source: string;
273
+ url: string;
274
+ method: string;
275
+ status: string;
276
+ duration: number;
277
+ procedure?: string | undefined;
278
+ parameters?: Record<string, unknown> | undefined;
279
+ uploads?: {originalname: string; mimetype: string; size: number}[] | undefined;
280
+ downloads?:
281
+ | {
282
+ fileType: string;
283
+ fileSize: number;
284
+ }
285
+ | undefined;
286
+ html?: string | undefined;
287
+ cookies?: Record<string, string> | undefined;
288
+ headers?: Record<string, string> | undefined;
289
+ cgi?: Record<string, string> | undefined;
290
+ error?: string | undefined;
291
+ };
292
+
293
+ import type {StatusResponse, ErrorLogResponse, AccessLogResponse} from './schemas.js';
294
+
295
+ // Re-export types from schemas
296
+ export type {StatusResponse, ErrorLogResponse, AccessLogResponse};
297
+
298
+ /**
299
+ * System metrics for tracking min/max.
300
+ */
301
+ export type SystemMetrics = {
302
+ heapUsed: number;
303
+ heapTotal: number;
304
+ rss: number;
305
+ external: number;
306
+ cpuUser: number;
307
+ cpuSystem: number;
308
+ };
309
+
310
+ /**
311
+ * Application state.
312
+ */
313
+ export type State = {
314
+ currentView: string;
315
+ status: Partial<StatusResponse>;
316
+ maxHistoryPoints: number;
317
+ lastRequestCount: number;
318
+ lastErrorCount: number;
319
+ lastUpdateTime: number;
320
+ lastBucketTimestamp: number;
321
+ nextRefreshTimeout: ReturnType<typeof setTimeout> | null;
322
+ nextRefreshTime: number;
323
+ countdownInterval: ReturnType<typeof setInterval> | null;
324
+ history: HistoryData & {
325
+ cpuUsage: number[];
326
+ memoryUsage: number[];
327
+ };
328
+ charts: Record<string, ChartInstance>;
329
+ metricsMin: Partial<SystemMetrics>;
330
+ metricsMax: Partial<SystemMetrics>;
331
+ };
332
+
333
+ declare global {
334
+ /** Injected by Vite during build */
335
+ const __BUILD_TIME__: string;
336
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Enhances a button with loading state logic.
3
+ *
4
+ * @param btn - The button element to enhance.
5
+ * @param action - The async function to execute when the button is clicked.
6
+ * @returns The result of the action.
7
+ */
8
+ export async function withLoading<T>(btn: HTMLButtonElement, action: () => Promise<T>): Promise<T> {
9
+ const iconSpan = btn.querySelector('.material-symbols-rounded');
10
+ const originalIcon = iconSpan?.textContent;
11
+
12
+ btn.disabled = true;
13
+ btn.classList.add('loading');
14
+ if (iconSpan) {
15
+ iconSpan.textContent = 'sync';
16
+ iconSpan.classList.add('animate-spin');
17
+ }
18
+
19
+ try {
20
+ return await action();
21
+ } finally {
22
+ btn.disabled = false;
23
+ btn.classList.remove('loading');
24
+ if (iconSpan) {
25
+ iconSpan.textContent = originalIcon ?? '';
26
+ iconSpan.classList.remove('animate-spin');
27
+ }
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Binds an async action to a button with standardized loading feedback.
33
+ *
34
+ * @param id - The ID of the button element.
35
+ * @param action - The async action to perform.
36
+ * @returns The button element or null if not found.
37
+ */
38
+ export function bindLoadingButton(id: string, action: () => Promise<void>): HTMLButtonElement | null {
39
+ const btn = document.getElementById(id) as HTMLButtonElement | null;
40
+ if (btn) {
41
+ btn.onclick = () => void withLoading(btn, action);
42
+ }
43
+ return btn;
44
+ }
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Column configuration for the DataTable.
3
+ */
4
+ export type TableColumn<T> = {
5
+ /** Unique identifier for the column */
6
+ id: string;
7
+ /** Header text to display */
8
+ header: string;
9
+ /** Tooltip for the column header */
10
+ headerTitle?: string;
11
+ /** Optional fixed width (e.g., '80px') */
12
+ width?: string;
13
+ /** Text alignment */
14
+ align?: 'left' | 'center' | 'right';
15
+ /** Custom CSS classes for the cell */
16
+ className?: string | ((row: T) => string);
17
+ /** Function to get the cell content (HTML string) */
18
+ render?: (row: T) => string;
19
+ /** Function to get the raw value (text content) */
20
+ accessor?: (row: T) => string | number | undefined | null;
21
+ /** Tooltip for the data cell */
22
+ cellTitle?: (row: T) => string;
23
+ };
24
+
25
+ /**
26
+ * Options for the DataTable.
27
+ */
28
+ export type TableOptions<T> = {
29
+ /** Array of column definitions */
30
+ columns: TableColumn<T>[];
31
+ /** Enable compact/dense layout */
32
+ dense?: boolean;
33
+ /** Callback when a row is clicked */
34
+ onRowClick?: (row: T) => void;
35
+ };
36
+
37
+ /**
38
+ * A reusable, configuration-driven data table component.
39
+ */
40
+ export class DataTable<T> {
41
+ private element: HTMLTableElement | null = null;
42
+
43
+ /**
44
+ * Creates a new DataTable instance.
45
+ *
46
+ * @param elementId - The ID of the table element in the DOM.
47
+ * @param options - Configuration options for the table.
48
+ */
49
+ constructor(
50
+ private elementId: string,
51
+ private options: TableOptions<T>,
52
+ ) {}
53
+
54
+ /**
55
+ * Mounts the table to the DOM and renders the provided data.
56
+ *
57
+ * @param data - The array of data to display.
58
+ */
59
+ render(data: T[]): void {
60
+ this.element = document.getElementById(this.elementId) as HTMLTableElement;
61
+ if (!this.element) return;
62
+
63
+ // Apply dense class if enabled
64
+ if (this.options.dense) {
65
+ this.element.classList.add('dense-table');
66
+ } else {
67
+ this.element.classList.remove('dense-table');
68
+ }
69
+
70
+ this.renderHeader();
71
+ this.renderBody(data);
72
+ }
73
+
74
+ /**
75
+ * Renders the table header.
76
+ */
77
+ private renderHeader(): void {
78
+ if (!this.element) return;
79
+
80
+ let thead = this.element.querySelector('thead');
81
+ if (!thead) {
82
+ thead = document.createElement('thead');
83
+ this.element.appendChild(thead);
84
+ }
85
+
86
+ thead.innerHTML = '';
87
+ const tr = document.createElement('tr');
88
+
89
+ this.options.columns.forEach((col) => {
90
+ const th = document.createElement('th');
91
+ th.textContent = col.header;
92
+ if (col.headerTitle) {
93
+ th.title = col.headerTitle;
94
+ }
95
+ if (col.width) {
96
+ th.style.width = col.width;
97
+ }
98
+ if (col.align) {
99
+ th.style.textAlign = col.align;
100
+ }
101
+ tr.appendChild(th);
102
+ });
103
+
104
+ thead.appendChild(tr);
105
+ }
106
+
107
+ /**
108
+ * Renders the table body.
109
+ *
110
+ * @param data - The data to render.
111
+ */
112
+ private renderBody(data: T[]): void {
113
+ if (!this.element) return;
114
+
115
+ let tbody = this.element.querySelector('tbody');
116
+ if (!tbody) {
117
+ tbody = document.createElement('tbody');
118
+ this.element.appendChild(tbody);
119
+ }
120
+
121
+ tbody.innerHTML = '';
122
+
123
+ data.forEach((row) => {
124
+ const tr = document.createElement('tr');
125
+
126
+ // Add row click handler if provided
127
+ if (this.options.onRowClick) {
128
+ tr.style.cursor = 'pointer';
129
+ tr.addEventListener('click', () => {
130
+ if (this.options.onRowClick) {
131
+ this.options.onRowClick(row);
132
+ }
133
+ });
134
+ // Add hover effect class usually handled by CSS, but good to ensure
135
+ tr.classList.add('table-row-hover');
136
+ }
137
+
138
+ this.options.columns.forEach((col) => {
139
+ const td = document.createElement('td');
140
+
141
+ // Content
142
+ if (col.render) {
143
+ td.innerHTML = col.render(row);
144
+ } else if (col.accessor) {
145
+ const val = col.accessor(row);
146
+ td.textContent = val !== undefined && val !== null ? String(val) : '';
147
+ }
148
+
149
+ // Styling
150
+ if (col.className) {
151
+ const className = typeof col.className === 'function' ? col.className(row) : col.className;
152
+ if (className) {
153
+ td.className = className;
154
+ }
155
+ }
156
+
157
+ // Alignment
158
+ if (col.align) {
159
+ td.style.textAlign = col.align;
160
+ }
161
+
162
+ // Tooltip
163
+ if (col.cellTitle) {
164
+ td.title = col.cellTitle(row);
165
+ }
166
+
167
+ tr.appendChild(td);
168
+ });
169
+
170
+ tbody.appendChild(tr);
171
+ });
172
+ }
173
+ }
@@ -0,0 +1,93 @@
1
+ import type {State, ChartInstance} from '../types.js';
2
+
3
+ /**
4
+ * Theme colors for charts based on theme mode.
5
+ */
6
+ interface ChartColors {
7
+ gridColor: string;
8
+ textColor: string;
9
+ }
10
+
11
+ /**
12
+ * Get chart colors for the current theme.
13
+ * @param isDark - Whether dark mode is active.
14
+ * @returns Chart color configuration.
15
+ */
16
+ function getChartColors(isDark: boolean): ChartColors {
17
+ return {
18
+ gridColor: isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
19
+ textColor: isDark ? '#94a3b8' : '#475569',
20
+ };
21
+ }
22
+
23
+ /**
24
+ * Update chart options for theme change.
25
+ * @param chart - Chart instance to update.
26
+ * @param colors - Chart colors for the current theme.
27
+ */
28
+ function updateChartForTheme(chart: ChartInstance, colors: ChartColors): void {
29
+ if (!chart.options.scales) return;
30
+
31
+ const scales = chart.options.scales;
32
+
33
+ if (scales.x) {
34
+ if (scales.x.grid) scales.x.grid.color = colors.gridColor;
35
+ if (scales.x.ticks) scales.x.ticks.color = colors.textColor;
36
+ }
37
+
38
+ if (scales.y?.grid) {
39
+ scales.y.grid.color = colors.gridColor;
40
+ }
41
+ if (scales.y?.ticks) {
42
+ scales.y.ticks.color = colors.textColor;
43
+ }
44
+
45
+ if (scales.y1?.grid) {
46
+ scales.y1.grid.color = colors.gridColor;
47
+ }
48
+ if (scales.y1?.ticks) {
49
+ scales.y1.ticks.color = colors.textColor;
50
+ }
51
+
52
+ if (scales.y?.title) {
53
+ scales.y.title.color = colors.textColor;
54
+ }
55
+
56
+ if (scales.y1?.title) {
57
+ scales.y1.title.color = colors.textColor;
58
+ }
59
+
60
+ if (chart.options.plugins?.legend?.labels) {
61
+ chart.options.plugins.legend.labels.color = colors.textColor;
62
+ }
63
+
64
+ chart.update();
65
+ }
66
+
67
+ /**
68
+ * Initialize theme management.
69
+ * @param state - Application state containing chart instances.
70
+ */
71
+ export function initTheme(state: State): void {
72
+ const theme = localStorage.getItem('theme') ?? 'dark';
73
+ const btn = document.getElementById('theme-toggle-btn');
74
+ if (!btn) return;
75
+
76
+ document.body.className = theme;
77
+
78
+ btn.onclick = (): void => {
79
+ const isDark = document.body.classList.contains('dark');
80
+ const newTheme = isDark ? 'light' : 'dark';
81
+ document.body.className = newTheme;
82
+ localStorage.setItem('theme', newTheme);
83
+
84
+ const colors = getChartColors(newTheme === 'dark');
85
+
86
+ Object.values(state.charts).forEach((chartInstance): void => {
87
+ const chart = chartInstance;
88
+ if (chart?.options) {
89
+ updateChartForTheme(chart, colors);
90
+ }
91
+ });
92
+ };
93
+ }