@dangao/bun-server 1.0.0 → 1.0.3
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/package.json +4 -2
- package/readme.md +163 -2
- package/src/auth/controller.ts +148 -0
- package/src/auth/decorators.ts +81 -0
- package/src/auth/index.ts +12 -0
- package/src/auth/jwt.ts +169 -0
- package/src/auth/oauth2.ts +244 -0
- package/src/auth/types.ts +248 -0
- package/src/cache/cache-module.ts +67 -0
- package/src/cache/decorators.ts +202 -0
- package/src/cache/index.ts +27 -0
- package/src/cache/service.ts +151 -0
- package/src/cache/types.ts +420 -0
- package/src/config/config-module.ts +76 -0
- package/src/config/index.ts +8 -0
- package/src/config/service.ts +93 -0
- package/src/config/types.ts +27 -0
- package/src/controller/controller.ts +251 -0
- package/src/controller/decorators.ts +84 -0
- package/src/controller/index.ts +7 -0
- package/src/controller/metadata.ts +27 -0
- package/src/controller/param-binder.ts +157 -0
- package/src/core/application.ts +233 -0
- package/src/core/context.ts +228 -0
- package/src/core/index.ts +4 -0
- package/src/core/server.ts +128 -0
- package/src/core/types.ts +2 -0
- package/src/database/connection-manager.ts +239 -0
- package/src/database/connection-pool.ts +322 -0
- package/src/database/database-extension.ts +62 -0
- package/src/database/database-module.ts +115 -0
- package/src/database/health-indicator.ts +51 -0
- package/src/database/index.ts +47 -0
- package/src/database/orm/decorators.ts +155 -0
- package/src/database/orm/drizzle-repository.ts +39 -0
- package/src/database/orm/index.ts +23 -0
- package/src/database/orm/repository-decorator.ts +39 -0
- package/src/database/orm/repository.ts +103 -0
- package/src/database/orm/service.ts +49 -0
- package/src/database/orm/transaction-decorator.ts +45 -0
- package/src/database/orm/transaction-interceptor.ts +243 -0
- package/src/database/orm/transaction-manager.ts +276 -0
- package/src/database/orm/transaction-types.ts +140 -0
- package/src/database/orm/types.ts +99 -0
- package/src/database/service.ts +221 -0
- package/src/database/types.ts +171 -0
- package/src/di/container.ts +398 -0
- package/src/di/decorators.ts +228 -0
- package/src/di/index.ts +4 -0
- package/src/di/module-registry.ts +188 -0
- package/src/di/module.ts +65 -0
- package/src/di/types.ts +67 -0
- package/src/error/error-codes.ts +222 -0
- package/src/error/filter.ts +43 -0
- package/src/error/handler.ts +66 -0
- package/src/error/http-exception.ts +115 -0
- package/src/error/i18n.ts +217 -0
- package/src/error/index.ts +16 -0
- package/src/extensions/index.ts +5 -0
- package/src/extensions/logger-extension.ts +31 -0
- package/src/extensions/logger-module.ts +69 -0
- package/src/extensions/types.ts +14 -0
- package/src/files/index.ts +5 -0
- package/src/files/static-middleware.ts +53 -0
- package/src/files/storage.ts +67 -0
- package/src/files/types.ts +33 -0
- package/src/files/upload-middleware.ts +45 -0
- package/src/health/controller.ts +76 -0
- package/src/health/health-module.ts +51 -0
- package/src/health/index.ts +12 -0
- package/src/health/types.ts +28 -0
- package/src/index.ts +270 -0
- package/src/metrics/collector.ts +209 -0
- package/src/metrics/controller.ts +40 -0
- package/src/metrics/index.ts +15 -0
- package/src/metrics/metrics-module.ts +58 -0
- package/src/metrics/middleware.ts +46 -0
- package/src/metrics/prometheus.ts +79 -0
- package/src/metrics/types.ts +103 -0
- package/src/middleware/builtin/cors.ts +60 -0
- package/src/middleware/builtin/error-handler.ts +90 -0
- package/src/middleware/builtin/file-upload.ts +42 -0
- package/src/middleware/builtin/index.ts +14 -0
- package/src/middleware/builtin/logger.ts +91 -0
- package/src/middleware/builtin/rate-limit.ts +252 -0
- package/src/middleware/builtin/static-file.ts +88 -0
- package/src/middleware/decorators.ts +91 -0
- package/src/middleware/index.ts +11 -0
- package/src/middleware/middleware.ts +13 -0
- package/src/middleware/pipeline.ts +93 -0
- package/src/queue/decorators.ts +110 -0
- package/src/queue/index.ts +26 -0
- package/src/queue/queue-module.ts +64 -0
- package/src/queue/service.ts +302 -0
- package/src/queue/types.ts +341 -0
- package/src/request/body-parser.ts +133 -0
- package/src/request/file-handler.ts +46 -0
- package/src/request/index.ts +5 -0
- package/src/request/request.ts +107 -0
- package/src/request/response.ts +150 -0
- package/src/router/decorators.ts +122 -0
- package/src/router/index.ts +6 -0
- package/src/router/registry.ts +98 -0
- package/src/router/route.ts +140 -0
- package/src/router/router.ts +241 -0
- package/src/router/types.ts +27 -0
- package/src/security/access-decision-manager.ts +34 -0
- package/src/security/authentication-manager.ts +47 -0
- package/src/security/context.ts +92 -0
- package/src/security/filter.ts +162 -0
- package/src/security/index.ts +8 -0
- package/src/security/providers/index.ts +3 -0
- package/src/security/providers/jwt-provider.ts +60 -0
- package/src/security/providers/oauth2-provider.ts +70 -0
- package/src/security/security-module.ts +145 -0
- package/src/security/types.ts +165 -0
- package/src/session/decorators.ts +45 -0
- package/src/session/index.ts +19 -0
- package/src/session/middleware.ts +143 -0
- package/src/session/service.ts +218 -0
- package/src/session/session-module.ts +69 -0
- package/src/session/types.ts +373 -0
- package/src/swagger/decorators.ts +133 -0
- package/src/swagger/generator.ts +234 -0
- package/src/swagger/index.ts +7 -0
- package/src/swagger/swagger-extension.ts +41 -0
- package/src/swagger/swagger-module.ts +83 -0
- package/src/swagger/types.ts +188 -0
- package/src/swagger/ui.ts +98 -0
- package/src/testing/harness.ts +96 -0
- package/src/validation/decorators.ts +95 -0
- package/src/validation/errors.ts +28 -0
- package/src/validation/index.ts +14 -0
- package/src/validation/types.ts +35 -0
- package/src/validation/validator.ts +63 -0
- package/src/websocket/decorators.ts +51 -0
- package/src/websocket/index.ts +12 -0
- package/src/websocket/registry.ts +133 -0
- package/tests/cache/cache-module.test.ts +212 -0
- package/tests/config/config-module.test.ts +151 -0
- package/tests/controller/controller.test.ts +189 -0
- package/tests/core/application.test.ts +57 -0
- package/tests/core/context-body.test.ts +44 -0
- package/tests/core/context.test.ts +86 -0
- package/tests/core/edge-cases.test.ts +432 -0
- package/tests/database/database-module.test.ts +385 -0
- package/tests/database/orm.test.ts +164 -0
- package/tests/database/postgres-mysql-integration.test.ts +395 -0
- package/tests/database/transaction.test.ts +238 -0
- package/tests/di/container.test.ts +264 -0
- package/tests/di/module.test.ts +128 -0
- package/tests/error/error-codes.test.ts +121 -0
- package/tests/error/error-handler.test.ts +68 -0
- package/tests/error/error-handling.test.ts +254 -0
- package/tests/error/http-exception.test.ts +37 -0
- package/tests/error/i18n-integration.test.ts +175 -0
- package/tests/extensions/logger-extension.test.ts +40 -0
- package/tests/files/static-middleware.test.ts +67 -0
- package/tests/files/upload-middleware.test.ts +43 -0
- package/tests/health/health-module.test.ts +116 -0
- package/tests/integration/application-router.test.ts +85 -0
- package/tests/integration/body-parsing.test.ts +88 -0
- package/tests/integration/cache-e2e.test.ts +114 -0
- package/tests/integration/oauth2-e2e.test.ts +615 -0
- package/tests/integration/session-e2e.test.ts +207 -0
- package/tests/metrics/metrics-module.test.ts +178 -0
- package/tests/middleware/builtin.test.ts +206 -0
- package/tests/middleware/file-upload.test.ts +41 -0
- package/tests/middleware/middleware.test.ts +120 -0
- package/tests/middleware/pipeline.test.ts +72 -0
- package/tests/middleware/rate-limit.test.ts +314 -0
- package/tests/middleware/static-file.test.ts +62 -0
- package/tests/perf/harness.test.ts +48 -0
- package/tests/perf/optimization.test.ts +183 -0
- package/tests/perf/regression.test.ts +120 -0
- package/tests/queue/queue-module.test.ts +217 -0
- package/tests/request/body-parser.test.ts +96 -0
- package/tests/request/response.test.ts +99 -0
- package/tests/router/decorators.test.ts +48 -0
- package/tests/router/registry.test.ts +51 -0
- package/tests/router/route.test.ts +71 -0
- package/tests/router/router-normalization.test.ts +106 -0
- package/tests/router/router.test.ts +133 -0
- package/tests/security/access-decision-manager.test.ts +84 -0
- package/tests/security/authentication-manager.test.ts +81 -0
- package/tests/security/context.test.ts +302 -0
- package/tests/security/filter.test.ts +225 -0
- package/tests/security/jwt-provider.test.ts +106 -0
- package/tests/security/oauth2-provider.test.ts +269 -0
- package/tests/security/security-module.test.ts +143 -0
- package/tests/session/session-module.test.ts +307 -0
- package/tests/stress/di-stress.test.ts +30 -0
- package/tests/swagger/decorators.test.ts +153 -0
- package/tests/swagger/generator.test.ts +202 -0
- package/tests/swagger/swagger-extension.test.ts +72 -0
- package/tests/swagger/swagger-module.test.ts +79 -0
- package/tests/utils/test-port.ts +10 -0
- package/tests/validation/controller-validation.test.ts +64 -0
- package/tests/validation/validation.test.ts +42 -0
- package/tests/websocket/gateway.test.ts +68 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Module, MODULE_METADATA_KEY, type ModuleProvider } from '../di/module';
|
|
2
|
+
|
|
3
|
+
import { MetricsController } from './controller';
|
|
4
|
+
import { MetricsCollector } from './collector';
|
|
5
|
+
import type { MetricsModuleOptions } from './types';
|
|
6
|
+
import { METRICS_SERVICE_TOKEN, METRICS_OPTIONS_TOKEN } from './types';
|
|
7
|
+
|
|
8
|
+
@Module({
|
|
9
|
+
controllers: [MetricsController],
|
|
10
|
+
providers: [],
|
|
11
|
+
})
|
|
12
|
+
export class MetricsModule {
|
|
13
|
+
/**
|
|
14
|
+
* 创建指标监控模块
|
|
15
|
+
* @param options - 模块配置
|
|
16
|
+
*/
|
|
17
|
+
public static forRoot(options: MetricsModuleOptions = {}): typeof MetricsModule {
|
|
18
|
+
const providers: ModuleProvider[] = [];
|
|
19
|
+
|
|
20
|
+
const collector = new MetricsCollector();
|
|
21
|
+
|
|
22
|
+
// 注册自定义指标
|
|
23
|
+
if (options.customMetrics) {
|
|
24
|
+
for (const metric of options.customMetrics) {
|
|
25
|
+
collector.registerCustomMetric(metric);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
providers.push(
|
|
30
|
+
{
|
|
31
|
+
provide: METRICS_SERVICE_TOKEN,
|
|
32
|
+
useValue: collector,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
provide: METRICS_OPTIONS_TOKEN,
|
|
36
|
+
useValue: options,
|
|
37
|
+
},
|
|
38
|
+
MetricsCollector,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// 动态更新模块元数据
|
|
42
|
+
const existingMetadata =
|
|
43
|
+
Reflect.getMetadata(MODULE_METADATA_KEY, MetricsModule) || {};
|
|
44
|
+
const metadata = {
|
|
45
|
+
...existingMetadata,
|
|
46
|
+
controllers: [...(existingMetadata.controllers || []), MetricsController],
|
|
47
|
+
providers: [...(existingMetadata.providers || []), ...providers],
|
|
48
|
+
exports: [
|
|
49
|
+
...(existingMetadata.exports || []),
|
|
50
|
+
METRICS_SERVICE_TOKEN,
|
|
51
|
+
MetricsCollector,
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
Reflect.defineMetadata(MODULE_METADATA_KEY, metadata, MetricsModule);
|
|
55
|
+
|
|
56
|
+
return MetricsModule;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Context } from '../core/context';
|
|
2
|
+
import type { Middleware } from '../middleware';
|
|
3
|
+
import { MetricsCollector } from './collector';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 创建 HTTP 请求指标收集中间件
|
|
7
|
+
*/
|
|
8
|
+
export function createHttpMetricsMiddleware(collector: MetricsCollector): Middleware {
|
|
9
|
+
return async (context: Context, next) => {
|
|
10
|
+
const startTime = Date.now();
|
|
11
|
+
|
|
12
|
+
// 执行请求
|
|
13
|
+
const response = await next();
|
|
14
|
+
|
|
15
|
+
// 计算延迟
|
|
16
|
+
const duration = Date.now() - startTime;
|
|
17
|
+
const durationSeconds = duration / 1000;
|
|
18
|
+
|
|
19
|
+
// 收集请求指标
|
|
20
|
+
const method = context.method;
|
|
21
|
+
const path = context.path;
|
|
22
|
+
const statusCode = response.status;
|
|
23
|
+
|
|
24
|
+
// HTTP 请求总数(计数器)
|
|
25
|
+
collector.incrementCounter('http_requests_total', {
|
|
26
|
+
method,
|
|
27
|
+
path,
|
|
28
|
+
status: statusCode.toString(),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// HTTP 请求延迟(直方图)
|
|
32
|
+
collector.observeHistogram('http_request_duration_seconds', {
|
|
33
|
+
method,
|
|
34
|
+
path,
|
|
35
|
+
status: statusCode.toString(),
|
|
36
|
+
}, durationSeconds);
|
|
37
|
+
|
|
38
|
+
// HTTP 请求延迟(摘要,用于 p50, p95, p99)
|
|
39
|
+
collector.observeHistogram('http_request_duration_seconds_summary', {
|
|
40
|
+
method,
|
|
41
|
+
path,
|
|
42
|
+
}, durationSeconds);
|
|
43
|
+
|
|
44
|
+
return response;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { MetricDataPoint } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 将指标数据点转换为 Prometheus 格式
|
|
5
|
+
*/
|
|
6
|
+
export class PrometheusFormatter {
|
|
7
|
+
/**
|
|
8
|
+
* 格式化指标为 Prometheus 文本格式
|
|
9
|
+
*/
|
|
10
|
+
public format(dataPoints: MetricDataPoint[]): string {
|
|
11
|
+
const lines: string[] = [];
|
|
12
|
+
const metricGroups = this.groupByMetricName(dataPoints);
|
|
13
|
+
|
|
14
|
+
for (const [metricName, points] of metricGroups.entries()) {
|
|
15
|
+
// 添加帮助文本(如果有)
|
|
16
|
+
const help = points[0]?.help;
|
|
17
|
+
if (help) {
|
|
18
|
+
lines.push(`# HELP ${metricName} ${help}`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 添加类型(如果有)
|
|
22
|
+
const type = points[0]?.type;
|
|
23
|
+
if (type) {
|
|
24
|
+
lines.push(`# TYPE ${metricName} ${type}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// 添加数据点
|
|
28
|
+
for (const point of points) {
|
|
29
|
+
const labelString = this.formatLabels(point.labels);
|
|
30
|
+
const line = labelString
|
|
31
|
+
? `${point.name}${labelString} ${point.value}`
|
|
32
|
+
: `${point.name} ${point.value}`;
|
|
33
|
+
lines.push(line);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
lines.push(''); // 空行分隔
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return lines.join('\n');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 按指标名称分组
|
|
44
|
+
*/
|
|
45
|
+
private groupByMetricName(dataPoints: MetricDataPoint[]): Map<string, MetricDataPoint[]> {
|
|
46
|
+
const groups = new Map<string, MetricDataPoint[]>();
|
|
47
|
+
|
|
48
|
+
for (const point of dataPoints) {
|
|
49
|
+
const name = point.name;
|
|
50
|
+
const existing = groups.get(name) || [];
|
|
51
|
+
existing.push(point);
|
|
52
|
+
groups.set(name, existing);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return groups;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 格式化标签
|
|
60
|
+
*/
|
|
61
|
+
private formatLabels(labels?: Record<string, string>): string {
|
|
62
|
+
if (!labels || Object.keys(labels).length === 0) {
|
|
63
|
+
return '';
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const labelPairs = Object.keys(labels)
|
|
67
|
+
.sort()
|
|
68
|
+
.map((key) => `${key}="${this.escapeLabelValue(labels[key])}"`)
|
|
69
|
+
.join(',');
|
|
70
|
+
return `{${labelPairs}}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 转义标签值
|
|
75
|
+
*/
|
|
76
|
+
private escapeLabelValue(value: string): string {
|
|
77
|
+
return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 指标类型
|
|
3
|
+
*/
|
|
4
|
+
export type MetricType = 'counter' | 'gauge' | 'histogram' | 'summary';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 指标标签
|
|
8
|
+
*/
|
|
9
|
+
export type MetricLabels = Record<string, string>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 指标值
|
|
13
|
+
*/
|
|
14
|
+
export type MetricValue = number;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 指标数据点
|
|
18
|
+
*/
|
|
19
|
+
export interface MetricDataPoint {
|
|
20
|
+
/**
|
|
21
|
+
* 指标名称
|
|
22
|
+
*/
|
|
23
|
+
name: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 指标类型
|
|
27
|
+
*/
|
|
28
|
+
type: MetricType;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 指标值
|
|
32
|
+
*/
|
|
33
|
+
value: MetricValue;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 标签
|
|
37
|
+
*/
|
|
38
|
+
labels?: MetricLabels;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 帮助文本
|
|
42
|
+
*/
|
|
43
|
+
help?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 自定义指标
|
|
48
|
+
*/
|
|
49
|
+
export interface CustomMetric {
|
|
50
|
+
/**
|
|
51
|
+
* 指标名称
|
|
52
|
+
*/
|
|
53
|
+
name: string;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 指标类型
|
|
57
|
+
*/
|
|
58
|
+
type: MetricType;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 帮助文本
|
|
62
|
+
*/
|
|
63
|
+
help?: string;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 获取当前值
|
|
67
|
+
* @param labels - 标签
|
|
68
|
+
* @returns 指标值
|
|
69
|
+
*/
|
|
70
|
+
getValue(labels?: MetricLabels): MetricValue | Promise<MetricValue>;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* MetricsModule 配置选项
|
|
75
|
+
*/
|
|
76
|
+
export interface MetricsModuleOptions {
|
|
77
|
+
/**
|
|
78
|
+
* 是否启用 HTTP 请求指标收集
|
|
79
|
+
* @default true
|
|
80
|
+
*/
|
|
81
|
+
enableHttpMetrics?: boolean;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 自定义指标列表
|
|
85
|
+
*/
|
|
86
|
+
customMetrics?: CustomMetric[];
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Metrics 端点路径
|
|
90
|
+
* @default '/metrics'
|
|
91
|
+
*/
|
|
92
|
+
path?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Metrics 服务 Token
|
|
97
|
+
*/
|
|
98
|
+
export const METRICS_SERVICE_TOKEN = Symbol('@dangao/bun-server:metrics:service');
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Metrics 选项 Token
|
|
102
|
+
*/
|
|
103
|
+
export const METRICS_OPTIONS_TOKEN = Symbol('@dangao/bun-server:metrics:options');
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { Middleware } from '../middleware';
|
|
2
|
+
|
|
3
|
+
export interface CorsOptions {
|
|
4
|
+
origin?: string | string[] | '*';
|
|
5
|
+
methods?: string[];
|
|
6
|
+
allowedHeaders?: string[];
|
|
7
|
+
exposedHeaders?: string[];
|
|
8
|
+
credentials?: boolean;
|
|
9
|
+
maxAge?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getOriginHeader(option: CorsOptions['origin'], requestOrigin: string | null): string {
|
|
13
|
+
if (!option || option === '*') {
|
|
14
|
+
return '*';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (Array.isArray(option)) {
|
|
18
|
+
if (requestOrigin && option.includes(requestOrigin)) {
|
|
19
|
+
return requestOrigin;
|
|
20
|
+
}
|
|
21
|
+
return option[0];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return option;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* CORS 中间件
|
|
29
|
+
*/
|
|
30
|
+
export function createCorsMiddleware(options: CorsOptions = {}): Middleware {
|
|
31
|
+
const methods = options.methods ?? ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'];
|
|
32
|
+
const allowedHeaders = options.allowedHeaders ?? ['Content-Type', 'Authorization'];
|
|
33
|
+
const exposedHeaders = options.exposedHeaders ?? [];
|
|
34
|
+
const credentials = options.credentials ?? true;
|
|
35
|
+
const maxAge = options.maxAge ?? 600;
|
|
36
|
+
|
|
37
|
+
return async (context, next) => {
|
|
38
|
+
const requestOrigin = context.getHeader('Origin');
|
|
39
|
+
const originHeader = getOriginHeader(options.origin ?? '*', requestOrigin);
|
|
40
|
+
|
|
41
|
+
context.setHeader('Access-Control-Allow-Origin', originHeader);
|
|
42
|
+
if (credentials) {
|
|
43
|
+
context.setHeader('Access-Control-Allow-Credentials', 'true');
|
|
44
|
+
}
|
|
45
|
+
context.setHeader('Access-Control-Allow-Methods', methods.join(','));
|
|
46
|
+
context.setHeader('Access-Control-Allow-Headers', allowedHeaders.join(','));
|
|
47
|
+
if (exposedHeaders.length > 0) {
|
|
48
|
+
context.setHeader('Access-Control-Expose-Headers', exposedHeaders.join(','));
|
|
49
|
+
}
|
|
50
|
+
context.setHeader('Access-Control-Max-Age', maxAge.toString());
|
|
51
|
+
|
|
52
|
+
if (context.method === 'OPTIONS') {
|
|
53
|
+
return new Response(null, { status: 204, headers: context.responseHeaders });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return await next();
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { Middleware } from "../middleware";
|
|
2
|
+
import { ValidationError } from "../../validation";
|
|
3
|
+
import { LoggerManager } from "@dangao/logsmith";
|
|
4
|
+
import { HttpException } from "../../error";
|
|
5
|
+
import { handleError } from "../../error/handler";
|
|
6
|
+
import { ErrorMessageI18n } from "../../error/i18n";
|
|
7
|
+
|
|
8
|
+
export interface ErrorHandlerOptions {
|
|
9
|
+
/**
|
|
10
|
+
* 自定义错误日志函数
|
|
11
|
+
*/
|
|
12
|
+
logger?: (error: unknown, context: { method: string; path: string }) => void;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 是否返回详细错误信息(默认 false,用于生产环境隐藏细节)
|
|
16
|
+
*/
|
|
17
|
+
exposeError?: boolean;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 默认状态码
|
|
21
|
+
*/
|
|
22
|
+
statusCode?: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 错误处理中间件:捕获下游异常并统一生成响应
|
|
27
|
+
*/
|
|
28
|
+
export function createErrorHandlingMiddleware(
|
|
29
|
+
options: ErrorHandlerOptions = {},
|
|
30
|
+
): Middleware {
|
|
31
|
+
const log = options.logger ??
|
|
32
|
+
((error: unknown, context: { method: string; path: string }) => {
|
|
33
|
+
LoggerManager.getLogger().error("[Error]", { ...context, error });
|
|
34
|
+
});
|
|
35
|
+
const expose = options.exposeError ?? false;
|
|
36
|
+
const defaultStatus = options.statusCode ?? 500;
|
|
37
|
+
|
|
38
|
+
return async (context, next) => {
|
|
39
|
+
const logger = LoggerManager.getLogger();
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
return await next();
|
|
43
|
+
} catch (error) {
|
|
44
|
+
log(error, { method: context.method, path: context.path });
|
|
45
|
+
logger.error("Unhandled error", {
|
|
46
|
+
method: context.method,
|
|
47
|
+
path: context.path,
|
|
48
|
+
error,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (error instanceof Response) {
|
|
52
|
+
return error as Response;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (error instanceof ValidationError) {
|
|
56
|
+
return context.createResponse(
|
|
57
|
+
{
|
|
58
|
+
error: error.message,
|
|
59
|
+
issues: error.issues,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
status: 400,
|
|
63
|
+
},
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (error instanceof HttpException) {
|
|
68
|
+
// 统一使用 handleError 处理,它已经包含了错误码和国际化逻辑
|
|
69
|
+
return await handleError(error, context);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (error instanceof Error && !expose) {
|
|
73
|
+
return await handleError(error, context);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (error instanceof Error) {
|
|
77
|
+
return context.createResponse(
|
|
78
|
+
{
|
|
79
|
+
error: error.message,
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
status: defaultStatus,
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return await handleError(error, context);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Middleware } from '../middleware';
|
|
2
|
+
import { FileHandler } from '../../request/file-handler';
|
|
3
|
+
|
|
4
|
+
export interface FileUploadOptions {
|
|
5
|
+
maxSize?: number;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 简单的文件上传中间件:解析 multipart/form-data 并将文件附加到 context.body
|
|
10
|
+
*/
|
|
11
|
+
export function createFileUploadMiddleware(options: FileUploadOptions = {}): Middleware {
|
|
12
|
+
const maxSize = options.maxSize ?? 10 * 1024 * 1024;
|
|
13
|
+
|
|
14
|
+
return async (context, next) => {
|
|
15
|
+
const contentType = context.getHeader('Content-Type');
|
|
16
|
+
if (!contentType || !contentType.includes('multipart/form-data')) {
|
|
17
|
+
return await next();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const formData = await FileHandler.parseFormData(context);
|
|
21
|
+
const files = await FileHandler.getFiles(formData);
|
|
22
|
+
|
|
23
|
+
// 限制大小
|
|
24
|
+
for (const fileList of Object.values(files)) {
|
|
25
|
+
for (const file of fileList) {
|
|
26
|
+
if (file.size > maxSize) {
|
|
27
|
+
context.setStatus(413);
|
|
28
|
+
return context.createResponse({ error: `File ${file.name} exceeds max size` });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
context.body = {
|
|
34
|
+
fields: Object.fromEntries(formData.entries()),
|
|
35
|
+
files,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return await next();
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export { createLoggerMiddleware, createRequestLoggingMiddleware } from './logger';
|
|
2
|
+
export { createErrorHandlingMiddleware } from './error-handler';
|
|
3
|
+
export { createCorsMiddleware } from './cors';
|
|
4
|
+
export { createFileUploadMiddleware } from './file-upload';
|
|
5
|
+
export { createStaticFileMiddleware } from './static-file';
|
|
6
|
+
export {
|
|
7
|
+
createRateLimitMiddleware,
|
|
8
|
+
createTokenKeyGenerator,
|
|
9
|
+
createUserKeyGenerator,
|
|
10
|
+
type RateLimitOptions,
|
|
11
|
+
type RateLimitStore,
|
|
12
|
+
} from './rate-limit';
|
|
13
|
+
|
|
14
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import type { Middleware } from "../middleware";
|
|
2
|
+
import { LoggerManager } from "@dangao/logsmith";
|
|
3
|
+
|
|
4
|
+
export interface LoggerMiddlewareOptions {
|
|
5
|
+
/**
|
|
6
|
+
* 自定义日志函数
|
|
7
|
+
*/
|
|
8
|
+
logger?: (message: string, details?: Record<string, unknown>) => void;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 日志前缀
|
|
12
|
+
*/
|
|
13
|
+
prefix?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* 简单日志中间件:记录请求方法与路径
|
|
18
|
+
*/
|
|
19
|
+
export function createLoggerMiddleware(
|
|
20
|
+
options: LoggerMiddlewareOptions = {},
|
|
21
|
+
): Middleware {
|
|
22
|
+
const log = options.logger ??
|
|
23
|
+
((message: string, details?: Record<string, unknown>) => {
|
|
24
|
+
const logger = LoggerManager.getLogger();
|
|
25
|
+
if (details) {
|
|
26
|
+
logger.info(message, details);
|
|
27
|
+
} else {
|
|
28
|
+
logger.info(message);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
const prefix = options.prefix ?? "[Logger]";
|
|
32
|
+
|
|
33
|
+
return async (context, next) => {
|
|
34
|
+
let response: Response | undefined;
|
|
35
|
+
try {
|
|
36
|
+
response = await next();
|
|
37
|
+
return response;
|
|
38
|
+
} finally {
|
|
39
|
+
const status = response?.status ?? context.statusCode ?? 200;
|
|
40
|
+
log(`${prefix} ${context.method} ${context.path} ${status}`);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface RequestLoggingOptions extends LoggerMiddlewareOptions {
|
|
46
|
+
/**
|
|
47
|
+
* 是否在响应头中附加请求耗时
|
|
48
|
+
*/
|
|
49
|
+
setHeader?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* 请求日志中间件:记录耗时与状态码
|
|
54
|
+
*/
|
|
55
|
+
export function createRequestLoggingMiddleware(
|
|
56
|
+
options: RequestLoggingOptions = {},
|
|
57
|
+
): Middleware {
|
|
58
|
+
const log = options.logger ??
|
|
59
|
+
((message: string, details?: Record<string, unknown>) => {
|
|
60
|
+
const logger = LoggerManager.getLogger();
|
|
61
|
+
logger.info(message, details);
|
|
62
|
+
});
|
|
63
|
+
const prefix = options.prefix ?? "[Request]";
|
|
64
|
+
const setHeader = options.setHeader ?? true;
|
|
65
|
+
|
|
66
|
+
return async (context, next) => {
|
|
67
|
+
const start = performance.now();
|
|
68
|
+
try {
|
|
69
|
+
const response = await next();
|
|
70
|
+
const duration = performance.now() - start;
|
|
71
|
+
log(
|
|
72
|
+
`${prefix} ${context.method} ${context.path} ${response.status} ${
|
|
73
|
+
duration.toFixed(2)
|
|
74
|
+
}ms`,
|
|
75
|
+
);
|
|
76
|
+
if (setHeader) {
|
|
77
|
+
context.setHeader("x-request-duration", duration.toFixed(2));
|
|
78
|
+
}
|
|
79
|
+
return response;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
const duration = performance.now() - start;
|
|
82
|
+
log(
|
|
83
|
+
`${prefix} ${context.method} ${context.path} error ${
|
|
84
|
+
duration.toFixed(2)
|
|
85
|
+
}ms`,
|
|
86
|
+
error instanceof Error ? { error: error.message } : undefined,
|
|
87
|
+
);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|