@dangao/bun-server 1.9.0 → 1.12.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 +79 -6
- package/dist/cache/cache-module.d.ts +6 -0
- package/dist/cache/cache-module.d.ts.map +1 -1
- package/dist/client/generator.d.ts +16 -0
- package/dist/client/generator.d.ts.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/runtime.d.ts +15 -0
- package/dist/client/runtime.d.ts.map +1 -0
- package/dist/client/types.d.ts +36 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/config/config-module.d.ts +7 -0
- package/dist/config/config-module.d.ts.map +1 -1
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/service.d.ts +13 -0
- package/dist/config/service.d.ts.map +1 -1
- package/dist/config/types.d.ts +10 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/core/application.d.ts +7 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/core/apply-decorators.d.ts +6 -0
- package/dist/core/apply-decorators.d.ts.map +1 -0
- package/dist/core/cluster.d.ts +47 -0
- package/dist/core/cluster.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/server.d.ts +8 -0
- package/dist/core/server.d.ts.map +1 -1
- package/dist/dashboard/controller.d.ts +55 -0
- package/dist/dashboard/controller.d.ts.map +1 -0
- package/dist/dashboard/dashboard-extension.d.ts +20 -0
- package/dist/dashboard/dashboard-extension.d.ts.map +1 -0
- package/dist/dashboard/dashboard-module.d.ts +13 -0
- package/dist/dashboard/dashboard-module.d.ts.map +1 -0
- package/dist/dashboard/index.d.ts +4 -0
- package/dist/dashboard/index.d.ts.map +1 -0
- package/dist/dashboard/types.d.ts +16 -0
- package/dist/dashboard/types.d.ts.map +1 -0
- package/dist/dashboard/ui.d.ts +7 -0
- package/dist/dashboard/ui.d.ts.map +1 -0
- package/dist/database/database-module.d.ts +7 -0
- package/dist/database/database-module.d.ts.map +1 -1
- package/dist/debug/debug-module.d.ts +13 -0
- package/dist/debug/debug-module.d.ts.map +1 -0
- package/dist/debug/debug-ui-middleware.d.ts +8 -0
- package/dist/debug/debug-ui-middleware.d.ts.map +1 -0
- package/dist/debug/index.d.ts +5 -0
- package/dist/debug/index.d.ts.map +1 -0
- package/dist/debug/middleware.d.ts +12 -0
- package/dist/debug/middleware.d.ts.map +1 -0
- package/dist/debug/recorder.d.ts +61 -0
- package/dist/debug/recorder.d.ts.map +1 -0
- package/dist/debug/types.d.ts +48 -0
- package/dist/debug/types.d.ts.map +1 -0
- package/dist/debug/ui.d.ts +6 -0
- package/dist/debug/ui.d.ts.map +1 -0
- package/dist/di/async-module.d.ts +49 -0
- package/dist/di/async-module.d.ts.map +1 -0
- package/dist/di/lifecycle.d.ts +49 -0
- package/dist/di/lifecycle.d.ts.map +1 -0
- package/dist/di/module-registry.d.ts +24 -0
- package/dist/di/module-registry.d.ts.map +1 -1
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1887 -35
- package/dist/router/route.d.ts +5 -7
- package/dist/router/route.d.ts.map +1 -1
- package/dist/swagger/generator.d.ts +10 -0
- package/dist/swagger/generator.d.ts.map +1 -1
- package/dist/testing/test-client.d.ts +49 -0
- package/dist/testing/test-client.d.ts.map +1 -0
- package/dist/testing/testing-module.d.ts +90 -0
- package/dist/testing/testing-module.d.ts.map +1 -0
- package/dist/websocket/registry.d.ts +1 -6
- package/dist/websocket/registry.d.ts.map +1 -1
- package/docs/async-module.md +59 -0
- package/docs/client-generation.md +100 -0
- package/docs/cluster.md +81 -0
- package/docs/custom-decorators.md +1 -7
- package/docs/dashboard.md +54 -0
- package/docs/debug.md +58 -0
- package/docs/extensions.md +0 -2
- package/docs/guide.md +0 -1
- package/docs/lifecycle.md +72 -0
- package/docs/testing.md +110 -0
- package/docs/zh/async-module.md +98 -0
- package/docs/zh/client-generation.md +92 -0
- package/docs/zh/cluster.md +74 -0
- package/docs/zh/custom-decorators.md +1 -7
- package/docs/zh/dashboard.md +69 -0
- package/docs/zh/debug.md +81 -0
- package/docs/zh/extensions.md +0 -2
- package/docs/zh/guide.md +0 -1
- package/docs/zh/lifecycle.md +87 -0
- package/docs/zh/migration.md +0 -5
- package/docs/zh/testing.md +119 -0
- package/package.json +4 -4
- package/src/cache/cache-module.ts +25 -0
- package/src/client/generator.ts +36 -0
- package/src/client/index.ts +8 -0
- package/src/client/runtime.ts +101 -0
- package/src/client/types.ts +38 -0
- package/src/config/config-module.ts +44 -4
- package/src/config/index.ts +1 -0
- package/src/config/service.ts +50 -0
- package/src/config/types.ts +12 -0
- package/src/core/application.ts +37 -0
- package/src/core/apply-decorators.ts +31 -0
- package/src/core/cluster.ts +143 -0
- package/src/core/index.ts +1 -0
- package/src/core/server.ts +14 -1
- package/src/dashboard/controller.ts +227 -0
- package/src/dashboard/dashboard-extension.ts +26 -0
- package/src/dashboard/dashboard-module.ts +38 -0
- package/src/dashboard/index.ts +3 -0
- package/src/dashboard/types.ts +16 -0
- package/src/dashboard/ui.ts +219 -0
- package/src/database/database-module.ts +20 -0
- package/src/debug/debug-module.ts +70 -0
- package/src/debug/debug-ui-middleware.ts +110 -0
- package/src/debug/index.ts +9 -0
- package/src/debug/middleware.ts +126 -0
- package/src/debug/recorder.ts +141 -0
- package/src/debug/types.ts +49 -0
- package/src/debug/ui.ts +393 -0
- package/src/di/async-module.ts +141 -0
- package/src/di/lifecycle.ts +117 -0
- package/src/di/module-registry.ts +75 -0
- package/src/index.ts +35 -0
- package/src/router/route.ts +20 -20
- package/src/swagger/generator.ts +100 -0
- package/src/testing/test-client.ts +112 -0
- package/src/testing/testing-module.ts +238 -0
- package/src/websocket/registry.ts +3 -16
- package/tests/auth/auth-decorators.test.ts +0 -1
- package/tests/auth/oauth2-service.test.ts +0 -1
- package/tests/cache/cache-decorators-extended.test.ts +0 -1
- package/tests/cache/cache-decorators.test.ts +0 -1
- package/tests/cache/cache-interceptors.test.ts +0 -1
- package/tests/cache/cache-module.test.ts +0 -1
- package/tests/cache/cache-service-proxy.test.ts +0 -1
- package/tests/client/client-generator.test.ts +142 -0
- package/tests/config/config-center-integration.test.ts +0 -1
- package/tests/config/config-module-extended.test.ts +0 -1
- package/tests/config/config-module.test.ts +0 -1
- package/tests/controller/controller.test.ts +0 -1
- package/tests/controller/param-binder.test.ts +0 -1
- package/tests/controller/path-combination.test.ts +0 -1
- package/tests/core/application.test.ts +34 -0
- package/tests/core/apply-decorators.test.ts +109 -0
- package/tests/core/cluster.test.ts +32 -0
- package/tests/dashboard/dashboard-module.test.ts +85 -0
- package/tests/database/database-module.test.ts +0 -1
- package/tests/database/orm.test.ts +0 -1
- package/tests/database/postgres-mysql-integration.test.ts +0 -1
- package/tests/database/transaction.test.ts +0 -1
- package/tests/debug/debug-module.test.ts +141 -0
- package/tests/di/async-module.test.ts +125 -0
- package/tests/di/container.test.ts +0 -1
- package/tests/di/lifecycle.test.ts +140 -0
- package/tests/error/error-handler.test.ts +0 -1
- package/tests/events/event-decorators.test.ts +0 -1
- package/tests/events/event-listener-scanner.test.ts +0 -1
- package/tests/events/event-module.test.ts +0 -1
- package/tests/extensions/logger-module.test.ts +0 -1
- package/tests/health/health-module.test.ts +0 -1
- package/tests/integration/oauth2-e2e.test.ts +0 -1
- package/tests/integration/session-e2e.test.ts +0 -1
- package/tests/interceptor/base-interceptor.test.ts +0 -1
- package/tests/interceptor/builtin/cache-interceptor.test.ts +0 -1
- package/tests/interceptor/builtin/log-interceptor.test.ts +0 -1
- package/tests/interceptor/builtin/permission-interceptor.test.ts +0 -1
- package/tests/interceptor/interceptor-advanced-integration.test.ts +0 -1
- package/tests/interceptor/interceptor-chain.test.ts +0 -1
- package/tests/interceptor/interceptor-integration.test.ts +0 -1
- package/tests/interceptor/interceptor-metadata.test.ts +0 -1
- package/tests/interceptor/interceptor-registry.test.ts +0 -1
- package/tests/interceptor/perf/interceptor-performance.test.ts +0 -1
- package/tests/metrics/metrics-module.test.ts +0 -1
- package/tests/microservice/config-center.test.ts +0 -1
- package/tests/microservice/service-client-decorators.test.ts +0 -1
- package/tests/microservice/service-registry-decorators.test.ts +0 -1
- package/tests/microservice/service-registry.test.ts +0 -1
- package/tests/middleware/builtin/middleware-builtin-extended.test.ts +0 -1
- package/tests/middleware/builtin/rate-limit.test.ts +0 -1
- package/tests/middleware/middleware-decorators.test.ts +0 -1
- package/tests/middleware/middleware-pipeline.test.ts +0 -1
- package/tests/middleware/middleware.test.ts +0 -1
- package/tests/perf/optimization.test.ts +0 -1
- package/tests/queue/queue-decorators.test.ts +0 -1
- package/tests/queue/queue-module.test.ts +0 -1
- package/tests/queue/queue-service.test.ts +0 -1
- package/tests/router/router-decorators.test.ts +0 -1
- package/tests/router/router-extended.test.ts +0 -1
- package/tests/security/guards/guards-integration.test.ts +0 -1
- package/tests/security/guards/guards.test.ts +0 -1
- package/tests/security/guards/reflector.test.ts +0 -1
- package/tests/security/security-filter.test.ts +0 -1
- package/tests/security/security-module-extended.test.ts +0 -1
- package/tests/security/security-module.test.ts +0 -1
- package/tests/session/session-decorators.test.ts +0 -1
- package/tests/session/session-module.test.ts +0 -1
- package/tests/swagger/decorators.test.ts +0 -1
- package/tests/swagger/swagger-module.test.ts +0 -1
- package/tests/swagger/ui.test.ts +0 -1
- package/tests/testing/testing-module.test.ts +129 -0
- package/tests/validation/class-validator.test.ts +0 -1
- package/tests/validation/controller-validation.test.ts +0 -1
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 创建 Dashboard HTML 页面
|
|
3
|
+
* @param basePath - Dashboard 基础路径
|
|
4
|
+
* @returns 完整的 HTML 页面字符串
|
|
5
|
+
*/
|
|
6
|
+
export function createDashboardHTML(basePath: string): string {
|
|
7
|
+
const apiBase = basePath.endsWith('/') ? basePath.slice(0, -1) : basePath;
|
|
8
|
+
return `<!DOCTYPE html>
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="UTF-8">
|
|
12
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
13
|
+
<title>Bun Server Dashboard</title>
|
|
14
|
+
<style>
|
|
15
|
+
:root {
|
|
16
|
+
--bg-primary: #0f0f12;
|
|
17
|
+
--bg-secondary: #18181c;
|
|
18
|
+
--bg-card: #1e1e24;
|
|
19
|
+
--border: #2a2a32;
|
|
20
|
+
--text-primary: #e4e4e7;
|
|
21
|
+
--text-secondary: #a1a1aa;
|
|
22
|
+
--accent: #6366f1;
|
|
23
|
+
--accent-hover: #818cf8;
|
|
24
|
+
--success: #22c55e;
|
|
25
|
+
--warning: #eab308;
|
|
26
|
+
--error: #ef4444;
|
|
27
|
+
}
|
|
28
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
29
|
+
body {
|
|
30
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
31
|
+
background: var(--bg-primary);
|
|
32
|
+
color: var(--text-primary);
|
|
33
|
+
min-height: 100vh;
|
|
34
|
+
padding: 1.5rem;
|
|
35
|
+
line-height: 1.5;
|
|
36
|
+
}
|
|
37
|
+
.header {
|
|
38
|
+
margin-bottom: 1.5rem;
|
|
39
|
+
padding-bottom: 1rem;
|
|
40
|
+
border-bottom: 1px solid var(--border);
|
|
41
|
+
}
|
|
42
|
+
.header h1 {
|
|
43
|
+
font-size: 1.5rem;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
color: var(--text-primary);
|
|
46
|
+
}
|
|
47
|
+
.header p {
|
|
48
|
+
font-size: 0.875rem;
|
|
49
|
+
color: var(--text-secondary);
|
|
50
|
+
margin-top: 0.25rem;
|
|
51
|
+
}
|
|
52
|
+
.grid {
|
|
53
|
+
display: grid;
|
|
54
|
+
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
|
|
55
|
+
gap: 1rem;
|
|
56
|
+
margin-bottom: 1.5rem;
|
|
57
|
+
}
|
|
58
|
+
.card {
|
|
59
|
+
background: var(--bg-card);
|
|
60
|
+
border: 1px solid var(--border);
|
|
61
|
+
border-radius: 0.5rem;
|
|
62
|
+
padding: 1rem;
|
|
63
|
+
overflow: hidden;
|
|
64
|
+
}
|
|
65
|
+
.card-title {
|
|
66
|
+
font-size: 0.875rem;
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
color: var(--text-secondary);
|
|
69
|
+
text-transform: uppercase;
|
|
70
|
+
letter-spacing: 0.05em;
|
|
71
|
+
margin-bottom: 0.75rem;
|
|
72
|
+
}
|
|
73
|
+
.stat-row {
|
|
74
|
+
display: flex;
|
|
75
|
+
justify-content: space-between;
|
|
76
|
+
padding: 0.375rem 0;
|
|
77
|
+
font-size: 0.875rem;
|
|
78
|
+
}
|
|
79
|
+
.stat-label { color: var(--text-secondary); }
|
|
80
|
+
.stat-value { font-weight: 500; }
|
|
81
|
+
.status-up { color: var(--success); }
|
|
82
|
+
.status-down { color: var(--error); }
|
|
83
|
+
.table-wrap {
|
|
84
|
+
overflow-x: auto;
|
|
85
|
+
margin: 0 -1rem -1rem;
|
|
86
|
+
}
|
|
87
|
+
table {
|
|
88
|
+
width: 100%;
|
|
89
|
+
border-collapse: collapse;
|
|
90
|
+
font-size: 0.8125rem;
|
|
91
|
+
}
|
|
92
|
+
th, td {
|
|
93
|
+
padding: 0.5rem 1rem;
|
|
94
|
+
text-align: left;
|
|
95
|
+
border-top: 1px solid var(--border);
|
|
96
|
+
}
|
|
97
|
+
th {
|
|
98
|
+
color: var(--text-secondary);
|
|
99
|
+
font-weight: 500;
|
|
100
|
+
text-transform: uppercase;
|
|
101
|
+
letter-spacing: 0.05em;
|
|
102
|
+
}
|
|
103
|
+
.method {
|
|
104
|
+
display: inline-block;
|
|
105
|
+
padding: 0.125rem 0.375rem;
|
|
106
|
+
border-radius: 0.25rem;
|
|
107
|
+
font-size: 0.75rem;
|
|
108
|
+
font-weight: 600;
|
|
109
|
+
}
|
|
110
|
+
.method-GET { background: rgba(34, 197, 94, 0.2); color: var(--success); }
|
|
111
|
+
.method-POST { background: rgba(99, 102, 241, 0.2); color: var(--accent); }
|
|
112
|
+
.method-PUT { background: rgba(234, 179, 8, 0.2); color: var(--warning); }
|
|
113
|
+
.method-DELETE { background: rgba(239, 68, 68, 0.2); color: var(--error); }
|
|
114
|
+
.method-PATCH { background: rgba(168, 85, 247, 0.2); color: #a855f7; }
|
|
115
|
+
.refresh-badge {
|
|
116
|
+
display: inline-block;
|
|
117
|
+
font-size: 0.75rem;
|
|
118
|
+
color: var(--text-secondary);
|
|
119
|
+
margin-top: 1rem;
|
|
120
|
+
}
|
|
121
|
+
.loading { color: var(--text-secondary); font-style: italic; }
|
|
122
|
+
.error-msg { color: var(--error); font-size: 0.875rem; }
|
|
123
|
+
</style>
|
|
124
|
+
</head>
|
|
125
|
+
<body>
|
|
126
|
+
<div class="header">
|
|
127
|
+
<h1>Bun Server Dashboard</h1>
|
|
128
|
+
<p>Monitoring UI - Auto-refresh every 5 seconds</p>
|
|
129
|
+
</div>
|
|
130
|
+
<div class="grid">
|
|
131
|
+
<div class="card">
|
|
132
|
+
<div class="card-title">System Info</div>
|
|
133
|
+
<div id="system-info" class="loading">Loading...</div>
|
|
134
|
+
</div>
|
|
135
|
+
<div class="card">
|
|
136
|
+
<div class="card-title">Health Status</div>
|
|
137
|
+
<div id="health-info" class="loading">Loading...</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
<div class="card">
|
|
141
|
+
<div class="card-title">Registered Routes</div>
|
|
142
|
+
<div class="table-wrap">
|
|
143
|
+
<table>
|
|
144
|
+
<thead>
|
|
145
|
+
<tr>
|
|
146
|
+
<th>Method</th>
|
|
147
|
+
<th>Path</th>
|
|
148
|
+
<th>Controller</th>
|
|
149
|
+
<th>Method</th>
|
|
150
|
+
</tr>
|
|
151
|
+
</thead>
|
|
152
|
+
<tbody id="routes-body">
|
|
153
|
+
<tr><td colspan="4" class="loading">Loading...</td></tr>
|
|
154
|
+
</tbody>
|
|
155
|
+
</table>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="refresh-badge" id="last-update">Last update: -</div>
|
|
159
|
+
<script>
|
|
160
|
+
(function() {
|
|
161
|
+
var base = '${apiBase}';
|
|
162
|
+
function fetchJson(path) {
|
|
163
|
+
return fetch(base + path).then(function(r) {
|
|
164
|
+
if (!r.ok) throw new Error(r.status);
|
|
165
|
+
return r.json();
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function formatBytes(n) {
|
|
169
|
+
if (n < 1024) return n + ' B';
|
|
170
|
+
if (n < 1048576) return (n / 1024).toFixed(1) + ' KB';
|
|
171
|
+
return (n / 1048576).toFixed(2) + ' MB';
|
|
172
|
+
}
|
|
173
|
+
function renderSystem(data) {
|
|
174
|
+
var el = document.getElementById('system-info');
|
|
175
|
+
if (!data) { el.innerHTML = '<span class="error-msg">Failed to load</span>'; return; }
|
|
176
|
+
el.innerHTML = '<div class="stat-row"><span class="stat-label">Uptime</span><span class="stat-value">' + Math.floor(data.uptime || 0) + 's</span></div>' +
|
|
177
|
+
'<div class="stat-row"><span class="stat-label">RSS</span><span class="stat-value">' + formatBytes(data.memory?.rss || 0) + '</span></div>' +
|
|
178
|
+
'<div class="stat-row"><span class="stat-label">Heap Used</span><span class="stat-value">' + formatBytes(data.memory?.heapUsed || 0) + '</span></div>' +
|
|
179
|
+
'<div class="stat-row"><span class="stat-label">Heap Total</span><span class="stat-value">' + formatBytes(data.memory?.heapTotal || 0) + '</span></div>' +
|
|
180
|
+
'<div class="stat-row"><span class="stat-label">Platform</span><span class="stat-value">' + (data.platform || '-') + '</span></div>' +
|
|
181
|
+
'<div class="stat-row"><span class="stat-label">Bun</span><span class="stat-value">' + (data.bunVersion || '-') + '</span></div>';
|
|
182
|
+
}
|
|
183
|
+
function renderHealth(data) {
|
|
184
|
+
var el = document.getElementById('health-info');
|
|
185
|
+
if (!data) { el.innerHTML = '<span class="error-msg">Failed to load</span>'; return; }
|
|
186
|
+
var statusClass = data.status === 'up' ? 'status-up' : 'status-down';
|
|
187
|
+
el.innerHTML = '<div class="stat-row"><span class="stat-label">Status</span><span class="stat-value ' + statusClass + '">' + (data.status || 'unknown') + '</span></div>' +
|
|
188
|
+
'<div class="stat-row"><span class="stat-label">Timestamp</span><span class="stat-value">' + (data.timestamp ? new Date(data.timestamp).toLocaleTimeString() : '-') + '</span></div>';
|
|
189
|
+
}
|
|
190
|
+
function renderRoutes(data) {
|
|
191
|
+
var tbody = document.getElementById('routes-body');
|
|
192
|
+
if (!data || !Array.isArray(data)) {
|
|
193
|
+
tbody.innerHTML = '<tr><td colspan="4" class="error-msg">Failed to load</td></tr>';
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (data.length === 0) {
|
|
197
|
+
tbody.innerHTML = '<tr><td colspan="4" class="loading">No routes registered</td></tr>';
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
tbody.innerHTML = data.map(function(r) {
|
|
201
|
+
return '<tr><td><span class="method method-' + (r.method || 'GET') + '">' + (r.method || 'GET') + '</span></td>' +
|
|
202
|
+
'<td><code>' + (r.path || '') + '</code></td>' +
|
|
203
|
+
'<td>' + (r.controller || '-') + '</td>' +
|
|
204
|
+
'<td>' + (r.methodName || '-') + '</td></tr>';
|
|
205
|
+
}).join('');
|
|
206
|
+
}
|
|
207
|
+
function update() {
|
|
208
|
+
fetchJson('/api/system').then(renderSystem).catch(function() { renderSystem(null); });
|
|
209
|
+
fetchJson('/api/health').then(renderHealth).catch(function() { renderHealth(null); });
|
|
210
|
+
fetchJson('/api/routes').then(renderRoutes).catch(function() { renderRoutes(null); });
|
|
211
|
+
document.getElementById('last-update').textContent = 'Last update: ' + new Date().toLocaleTimeString();
|
|
212
|
+
}
|
|
213
|
+
update();
|
|
214
|
+
setInterval(update, 5000);
|
|
215
|
+
})();
|
|
216
|
+
</script>
|
|
217
|
+
</body>
|
|
218
|
+
</html>`;
|
|
219
|
+
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
InterceptorRegistry,
|
|
5
5
|
INTERCEPTOR_REGISTRY_TOKEN,
|
|
6
6
|
} from '../interceptor';
|
|
7
|
+
import { type AsyncModuleOptions, registerAsyncProviders } from '../di/async-module';
|
|
7
8
|
|
|
8
9
|
import { DatabaseExtension } from './database-extension';
|
|
9
10
|
import { DatabaseHealthIndicator } from './health-indicator';
|
|
@@ -107,6 +108,25 @@ export class DatabaseModule {
|
|
|
107
108
|
return DatabaseModule;
|
|
108
109
|
}
|
|
109
110
|
|
|
111
|
+
/**
|
|
112
|
+
* 异步创建数据库模块
|
|
113
|
+
* 允许通过工厂函数异步提供配置(如从 ConfigService 获取数据库连接信息)
|
|
114
|
+
* @param asyncOptions - 异步配置选项
|
|
115
|
+
*/
|
|
116
|
+
public static forRootAsync(
|
|
117
|
+
asyncOptions: AsyncModuleOptions<DatabaseModuleOptions>,
|
|
118
|
+
): typeof DatabaseModule {
|
|
119
|
+
const tokenMap = new Map<symbol, (config: DatabaseModuleOptions) => unknown>();
|
|
120
|
+
tokenMap.set(DATABASE_SERVICE_TOKEN, (config) => new DatabaseService(config));
|
|
121
|
+
tokenMap.set(DATABASE_OPTIONS_TOKEN, (config) => config);
|
|
122
|
+
|
|
123
|
+
return registerAsyncProviders(
|
|
124
|
+
DatabaseModule,
|
|
125
|
+
asyncOptions,
|
|
126
|
+
tokenMap,
|
|
127
|
+
) as typeof DatabaseModule;
|
|
128
|
+
}
|
|
129
|
+
|
|
110
130
|
/**
|
|
111
131
|
* 创建数据库健康检查指示器
|
|
112
132
|
* 用于在 HealthModule 中注册数据库健康检查
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Module, MODULE_METADATA_KEY } from '../di/module';
|
|
2
|
+
import type { ModuleProvider } from '../di/module';
|
|
3
|
+
import { RequestRecorder } from './recorder';
|
|
4
|
+
import { createDebugMiddleware } from './middleware';
|
|
5
|
+
import { createDebugUIMiddleware } from './debug-ui-middleware';
|
|
6
|
+
import {
|
|
7
|
+
DEBUG_OPTIONS_TOKEN,
|
|
8
|
+
DEBUG_RECORDER_TOKEN,
|
|
9
|
+
type DebugModuleOptions,
|
|
10
|
+
} from './types';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Debug 模块
|
|
14
|
+
* 开发模式下录制 HTTP 请求,支持查看和重放
|
|
15
|
+
*/
|
|
16
|
+
@Module({
|
|
17
|
+
providers: [],
|
|
18
|
+
extensions: [],
|
|
19
|
+
middlewares: [],
|
|
20
|
+
})
|
|
21
|
+
export class DebugModule {
|
|
22
|
+
/**
|
|
23
|
+
* 创建 Debug 模块
|
|
24
|
+
* @param options - 模块配置
|
|
25
|
+
*/
|
|
26
|
+
public static forRoot(options: DebugModuleOptions = {}): typeof DebugModule {
|
|
27
|
+
const enabled = options.enabled ?? true;
|
|
28
|
+
const maxRecords = options.maxRecords ?? 500;
|
|
29
|
+
const recordBody = options.recordBody ?? true;
|
|
30
|
+
const path = options.path ?? '/_debug';
|
|
31
|
+
|
|
32
|
+
const normalizedPath = path.endsWith('/') && path.length > 1
|
|
33
|
+
? path.slice(0, -1)
|
|
34
|
+
: path;
|
|
35
|
+
|
|
36
|
+
const recorder = new RequestRecorder(maxRecords);
|
|
37
|
+
|
|
38
|
+
const opts = {
|
|
39
|
+
enabled,
|
|
40
|
+
maxRecords,
|
|
41
|
+
recordBody,
|
|
42
|
+
path: normalizedPath,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const debugUIMiddleware = createDebugUIMiddleware(recorder, normalizedPath);
|
|
46
|
+
const recordingMiddleware = createDebugMiddleware(recorder, {
|
|
47
|
+
recordBody,
|
|
48
|
+
basePath: normalizedPath,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const providers: ModuleProvider[] = [
|
|
52
|
+
{ provide: DEBUG_OPTIONS_TOKEN, useValue: opts },
|
|
53
|
+
{ provide: DEBUG_RECORDER_TOKEN, useValue: recorder },
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
const middlewares = enabled
|
|
57
|
+
? [debugUIMiddleware, recordingMiddleware]
|
|
58
|
+
: [];
|
|
59
|
+
|
|
60
|
+
const existingMetadata = Reflect.getMetadata(MODULE_METADATA_KEY, DebugModule) ?? {};
|
|
61
|
+
const metadata = {
|
|
62
|
+
...existingMetadata,
|
|
63
|
+
providers: [...(existingMetadata.providers ?? []), ...providers],
|
|
64
|
+
middlewares: [...(existingMetadata.middlewares ?? []), ...middlewares],
|
|
65
|
+
};
|
|
66
|
+
Reflect.defineMetadata(MODULE_METADATA_KEY, metadata, DebugModule);
|
|
67
|
+
|
|
68
|
+
return DebugModule;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { Context } from '../core/context';
|
|
2
|
+
import type { Middleware, NextFunction } from '../middleware';
|
|
3
|
+
import type { RequestRecorder } from './recorder';
|
|
4
|
+
import { createDebugHTML } from './ui';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 创建 Debug UI 中间件
|
|
8
|
+
* 处理 Debug UI 页面和 API 端点
|
|
9
|
+
*/
|
|
10
|
+
export function createDebugUIMiddleware(
|
|
11
|
+
recorder: RequestRecorder,
|
|
12
|
+
basePath: string,
|
|
13
|
+
): Middleware {
|
|
14
|
+
const normalizedBase = basePath.endsWith('/') && basePath.length > 1
|
|
15
|
+
? basePath.slice(0, -1)
|
|
16
|
+
: basePath;
|
|
17
|
+
const apiPrefix = `${normalizedBase}/api`;
|
|
18
|
+
|
|
19
|
+
return async (context: Context, next: NextFunction): Promise<Response> => {
|
|
20
|
+
const path = context.path;
|
|
21
|
+
|
|
22
|
+
if (!path.startsWith(normalizedBase)) {
|
|
23
|
+
return await next();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const jsonResponse = (data: unknown, status = 200): Response => {
|
|
27
|
+
return new Response(JSON.stringify(data), {
|
|
28
|
+
status,
|
|
29
|
+
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
30
|
+
});
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
if (path === normalizedBase || path === `${normalizedBase}/`) {
|
|
34
|
+
const html = createDebugHTML(normalizedBase);
|
|
35
|
+
return new Response(html, {
|
|
36
|
+
headers: { 'Content-Type': 'text/html; charset=utf-8' },
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (path === `${apiPrefix}/records`) {
|
|
41
|
+
if (context.method === 'GET') {
|
|
42
|
+
const records = recorder.getAll();
|
|
43
|
+
return jsonResponse(records);
|
|
44
|
+
}
|
|
45
|
+
if (context.method === 'DELETE') {
|
|
46
|
+
recorder.clear();
|
|
47
|
+
return jsonResponse({ ok: true });
|
|
48
|
+
}
|
|
49
|
+
return jsonResponse({ error: 'Method not allowed' }, 405);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const recordsIdPrefix = `${apiPrefix}/records/`;
|
|
53
|
+
if (path.startsWith(recordsIdPrefix)) {
|
|
54
|
+
const id = path.slice(recordsIdPrefix.length).split('/')[0];
|
|
55
|
+
if (!id) {
|
|
56
|
+
return jsonResponse({ error: 'Invalid record id' }, 400);
|
|
57
|
+
}
|
|
58
|
+
const record = recorder.getById(id);
|
|
59
|
+
if (!record) {
|
|
60
|
+
return jsonResponse({ error: 'Not found' }, 404);
|
|
61
|
+
}
|
|
62
|
+
return jsonResponse(record);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const replayPrefix = `${apiPrefix}/replay/`;
|
|
66
|
+
if (path.startsWith(replayPrefix) && context.method === 'POST') {
|
|
67
|
+
const id = path.slice(replayPrefix.length).split('/')[0];
|
|
68
|
+
if (!id) {
|
|
69
|
+
return jsonResponse({ ok: false, error: 'Invalid record id' }, 400);
|
|
70
|
+
}
|
|
71
|
+
const record = recorder.getById(id);
|
|
72
|
+
if (!record) {
|
|
73
|
+
return jsonResponse({ ok: false, error: 'Record not found' }, 404);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const origin = new URL(context.request.url).origin;
|
|
78
|
+
const targetUrl = new URL(record.request.path, origin).href;
|
|
79
|
+
|
|
80
|
+
const headers = new Headers(record.request.headers);
|
|
81
|
+
let body: string | undefined;
|
|
82
|
+
if (record.request.body !== undefined && record.request.body !== null) {
|
|
83
|
+
body = JSON.stringify(record.request.body);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const replayedRequest = new Request(targetUrl, {
|
|
87
|
+
method: record.request.method,
|
|
88
|
+
headers,
|
|
89
|
+
body,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
const response = await fetch(replayedRequest);
|
|
93
|
+
const responseText = await response.text();
|
|
94
|
+
|
|
95
|
+
return jsonResponse({
|
|
96
|
+
ok: true,
|
|
97
|
+
status: response.status,
|
|
98
|
+
body: responseText,
|
|
99
|
+
});
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return jsonResponse({
|
|
102
|
+
ok: false,
|
|
103
|
+
error: (error as Error).message,
|
|
104
|
+
}, 500);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return await next();
|
|
109
|
+
};
|
|
110
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { DebugModule } from './debug-module';
|
|
2
|
+
export { RequestRecorder } from './recorder';
|
|
3
|
+
export { createDebugMiddleware } from './middleware';
|
|
4
|
+
export {
|
|
5
|
+
DEBUG_OPTIONS_TOKEN,
|
|
6
|
+
DEBUG_RECORDER_TOKEN,
|
|
7
|
+
type DebugModuleOptions,
|
|
8
|
+
type RequestRecord,
|
|
9
|
+
} from './types';
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Context } from '../core/context';
|
|
2
|
+
import type { Middleware, NextFunction } from '../middleware';
|
|
3
|
+
import type { RequestRecorder } from './recorder';
|
|
4
|
+
import type { RequestRecord } from './types';
|
|
5
|
+
import { RouteRegistry } from '../router/registry';
|
|
6
|
+
import type { HttpMethod } from '../router/types';
|
|
7
|
+
|
|
8
|
+
interface RouteHandlerInfo {
|
|
9
|
+
controller?: { name?: string };
|
|
10
|
+
method?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 将 Headers 转为 Record<string, string>
|
|
15
|
+
*/
|
|
16
|
+
function headersToRecord(headers: Headers): Record<string, string> {
|
|
17
|
+
const record: Record<string, string> = {};
|
|
18
|
+
headers.forEach((value, key) => {
|
|
19
|
+
record[key] = value;
|
|
20
|
+
});
|
|
21
|
+
return record;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 获取响应体大小
|
|
26
|
+
*/
|
|
27
|
+
async function getResponseBodySize(response: Response): Promise<number> {
|
|
28
|
+
const contentLength = response.headers.get('content-length');
|
|
29
|
+
if (contentLength) {
|
|
30
|
+
const n = parseInt(contentLength, 10);
|
|
31
|
+
if (!Number.isNaN(n)) {
|
|
32
|
+
return n;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
if (response.body) {
|
|
36
|
+
const clone = response.clone();
|
|
37
|
+
const buf = await clone.arrayBuffer();
|
|
38
|
+
return buf.byteLength;
|
|
39
|
+
}
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 创建调试录制中间件
|
|
45
|
+
* @param recorder - 请求录制器
|
|
46
|
+
* @param options - 配置选项
|
|
47
|
+
*/
|
|
48
|
+
export function createDebugMiddleware(
|
|
49
|
+
recorder: RequestRecorder,
|
|
50
|
+
options: {
|
|
51
|
+
recordBody?: boolean;
|
|
52
|
+
basePath?: string;
|
|
53
|
+
} = {},
|
|
54
|
+
): Middleware {
|
|
55
|
+
const recordBody = options.recordBody ?? true;
|
|
56
|
+
const basePath = options.basePath ?? '/_debug';
|
|
57
|
+
|
|
58
|
+
return async (context: Context, next: NextFunction): Promise<Response> => {
|
|
59
|
+
const startTime = Date.now();
|
|
60
|
+
|
|
61
|
+
const requestHeaders: Record<string, string> = {};
|
|
62
|
+
context.headers.forEach((value, key) => {
|
|
63
|
+
requestHeaders[key] = value;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
let requestBody: unknown = undefined;
|
|
67
|
+
if (recordBody && ['POST', 'PUT', 'PATCH'].includes(context.method)) {
|
|
68
|
+
try {
|
|
69
|
+
requestBody = await context.getBody();
|
|
70
|
+
} catch {
|
|
71
|
+
requestBody = undefined;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const response = await next();
|
|
76
|
+
const endTime = Date.now();
|
|
77
|
+
const totalMs = endTime - startTime;
|
|
78
|
+
|
|
79
|
+
if (context.path.startsWith(basePath)) {
|
|
80
|
+
return response;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const responseHeaders = headersToRecord(response.headers);
|
|
84
|
+
const bodySize = await getResponseBodySize(response);
|
|
85
|
+
|
|
86
|
+
const routeHandler = (context as { routeHandler?: RouteHandlerInfo }).routeHandler;
|
|
87
|
+
let matchedRoute: string | undefined;
|
|
88
|
+
try {
|
|
89
|
+
const registry = RouteRegistry.getInstance();
|
|
90
|
+
const router = registry.getRouter();
|
|
91
|
+
const route = router.findRoute(context.method as HttpMethod, context.path);
|
|
92
|
+
if (route) {
|
|
93
|
+
matchedRoute = route.path;
|
|
94
|
+
}
|
|
95
|
+
} catch {
|
|
96
|
+
matchedRoute = undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const record: Omit<RequestRecord, 'id'> = {
|
|
100
|
+
timestamp: startTime,
|
|
101
|
+
request: {
|
|
102
|
+
method: context.method,
|
|
103
|
+
path: context.path,
|
|
104
|
+
headers: requestHeaders,
|
|
105
|
+
body: requestBody,
|
|
106
|
+
},
|
|
107
|
+
response: {
|
|
108
|
+
status: response.status,
|
|
109
|
+
headers: responseHeaders,
|
|
110
|
+
bodySize,
|
|
111
|
+
},
|
|
112
|
+
timing: {
|
|
113
|
+
total: totalMs,
|
|
114
|
+
},
|
|
115
|
+
metadata: {
|
|
116
|
+
matchedRoute,
|
|
117
|
+
controller: routeHandler?.controller?.name,
|
|
118
|
+
methodName: routeHandler?.method,
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
recorder.record(record);
|
|
123
|
+
|
|
124
|
+
return response;
|
|
125
|
+
};
|
|
126
|
+
}
|