@dangao/bun-server 1.7.1 → 1.8.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 +129 -21
- package/dist/di/decorators.d.ts +37 -0
- package/dist/di/decorators.d.ts.map +1 -1
- package/dist/di/index.d.ts +1 -1
- package/dist/di/index.d.ts.map +1 -1
- package/dist/di/module-registry.d.ts +17 -0
- package/dist/di/module-registry.d.ts.map +1 -1
- package/dist/events/decorators.d.ts +52 -0
- package/dist/events/decorators.d.ts.map +1 -0
- package/dist/events/event-module.d.ts +97 -0
- package/dist/events/event-module.d.ts.map +1 -0
- package/dist/events/index.d.ts +5 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/service.d.ts +76 -0
- package/dist/events/service.d.ts.map +1 -0
- package/dist/events/types.d.ts +184 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1511 -11
- package/dist/security/filter.d.ts +23 -0
- package/dist/security/filter.d.ts.map +1 -1
- package/dist/security/guards/builtin/auth-guard.d.ts +44 -0
- package/dist/security/guards/builtin/auth-guard.d.ts.map +1 -0
- package/dist/security/guards/builtin/index.d.ts +3 -0
- package/dist/security/guards/builtin/index.d.ts.map +1 -0
- package/dist/security/guards/builtin/roles-guard.d.ts +66 -0
- package/dist/security/guards/builtin/roles-guard.d.ts.map +1 -0
- package/dist/security/guards/decorators.d.ts +50 -0
- package/dist/security/guards/decorators.d.ts.map +1 -0
- package/dist/security/guards/execution-context.d.ts +56 -0
- package/dist/security/guards/execution-context.d.ts.map +1 -0
- package/dist/security/guards/guard-registry.d.ts +67 -0
- package/dist/security/guards/guard-registry.d.ts.map +1 -0
- package/dist/security/guards/index.d.ts +7 -0
- package/dist/security/guards/index.d.ts.map +1 -0
- package/dist/security/guards/reflector.d.ts +57 -0
- package/dist/security/guards/reflector.d.ts.map +1 -0
- package/dist/security/guards/types.d.ts +126 -0
- package/dist/security/guards/types.d.ts.map +1 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/security-module.d.ts +20 -0
- package/dist/security/security-module.d.ts.map +1 -1
- package/dist/validation/class-validator.d.ts +108 -0
- package/dist/validation/class-validator.d.ts.map +1 -0
- package/dist/validation/custom-validator.d.ts +130 -0
- package/dist/validation/custom-validator.d.ts.map +1 -0
- package/dist/validation/errors.d.ts +22 -2
- package/dist/validation/errors.d.ts.map +1 -1
- package/dist/validation/index.d.ts +7 -1
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/rules/array.d.ts +33 -0
- package/dist/validation/rules/array.d.ts.map +1 -0
- package/dist/validation/rules/common.d.ts +90 -0
- package/dist/validation/rules/common.d.ts.map +1 -0
- package/dist/validation/rules/conditional.d.ts +30 -0
- package/dist/validation/rules/conditional.d.ts.map +1 -0
- package/dist/validation/rules/index.d.ts +5 -0
- package/dist/validation/rules/index.d.ts.map +1 -0
- package/dist/validation/rules/object.d.ts +30 -0
- package/dist/validation/rules/object.d.ts.map +1 -0
- package/dist/validation/types.d.ts +52 -1
- package/dist/validation/types.d.ts.map +1 -1
- package/docs/events.md +494 -0
- package/docs/guards.md +376 -0
- package/docs/guide.md +309 -1
- package/docs/request-lifecycle.md +444 -0
- package/docs/validation.md +407 -0
- package/docs/zh/events.md +494 -0
- package/docs/zh/guards.md +376 -0
- package/docs/zh/guide.md +309 -1
- package/docs/zh/request-lifecycle.md +444 -0
- package/docs/zh/validation.md +407 -0
- package/package.json +1 -1
- package/src/di/decorators.ts +46 -0
- package/src/di/index.ts +10 -1
- package/src/di/module-registry.ts +39 -0
- package/src/events/decorators.ts +103 -0
- package/src/events/event-module.ts +272 -0
- package/src/events/index.ts +32 -0
- package/src/events/service.ts +352 -0
- package/src/events/types.ts +223 -0
- package/src/index.ts +133 -1
- package/src/security/filter.ts +88 -8
- package/src/security/guards/builtin/auth-guard.ts +68 -0
- package/src/security/guards/builtin/index.ts +3 -0
- package/src/security/guards/builtin/roles-guard.ts +165 -0
- package/src/security/guards/decorators.ts +124 -0
- package/src/security/guards/execution-context.ts +152 -0
- package/src/security/guards/guard-registry.ts +164 -0
- package/src/security/guards/index.ts +7 -0
- package/src/security/guards/reflector.ts +99 -0
- package/src/security/guards/types.ts +144 -0
- package/src/security/index.ts +1 -0
- package/src/security/security-module.ts +72 -2
- package/src/validation/class-validator.ts +322 -0
- package/src/validation/custom-validator.ts +289 -0
- package/src/validation/errors.ts +50 -2
- package/src/validation/index.ts +103 -1
- package/src/validation/rules/array.ts +118 -0
- package/src/validation/rules/common.ts +286 -0
- package/src/validation/rules/conditional.ts +52 -0
- package/src/validation/rules/index.ts +51 -0
- package/src/validation/rules/object.ts +86 -0
- package/src/validation/types.ts +61 -1
- package/tests/di/global-module.test.ts +487 -0
- package/tests/events/event-decorators.test.ts +173 -0
- package/tests/events/event-emitter.test.ts +373 -0
- package/tests/events/event-module.test.ts +373 -0
- package/tests/security/guards/guards-integration.test.ts +371 -0
- package/tests/security/guards/guards.test.ts +775 -0
- package/tests/security/security-module.test.ts +2 -2
- package/tests/validation/class-validator.test.ts +349 -0
- package/tests/validation/custom-validator.test.ts +335 -0
- package/tests/validation/rules.test.ts +543 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { Context } from '../../core/context';
|
|
2
|
+
import type { ResponseBuilder } from '../../request/response';
|
|
3
|
+
import type { Constructor } from '../../core/types';
|
|
4
|
+
import type { ServerWebSocket } from 'bun';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 守卫接口
|
|
8
|
+
* 守卫用于决定请求是否可以继续执行
|
|
9
|
+
*/
|
|
10
|
+
export interface CanActivate {
|
|
11
|
+
/**
|
|
12
|
+
* 判断是否允许访问
|
|
13
|
+
* @param context - 执行上下文
|
|
14
|
+
* @returns 是否允许访问,可以是同步或异步的布尔值
|
|
15
|
+
*/
|
|
16
|
+
canActivate(context: ExecutionContext): boolean | Promise<boolean>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* HTTP 参数主机接口
|
|
21
|
+
* 提供 HTTP 请求相关的上下文信息
|
|
22
|
+
*/
|
|
23
|
+
export interface HttpArgumentsHost {
|
|
24
|
+
/**
|
|
25
|
+
* 获取请求上下文
|
|
26
|
+
* @returns Context 对象
|
|
27
|
+
*/
|
|
28
|
+
getRequest(): Context;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 获取响应构建器
|
|
32
|
+
* @returns ResponseBuilder 对象(可能为 undefined)
|
|
33
|
+
*/
|
|
34
|
+
getResponse(): ResponseBuilder | undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* WebSocket 参数主机接口
|
|
39
|
+
* 提供 WebSocket 连接相关的上下文信息
|
|
40
|
+
*/
|
|
41
|
+
export interface WsArgumentsHost {
|
|
42
|
+
/**
|
|
43
|
+
* 获取 WebSocket 客户端
|
|
44
|
+
* @returns WebSocket 连接对象
|
|
45
|
+
*/
|
|
46
|
+
getClient(): ServerWebSocket<unknown>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 获取消息数据
|
|
50
|
+
* @returns 消息数据
|
|
51
|
+
*/
|
|
52
|
+
getData(): unknown;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 执行上下文接口
|
|
57
|
+
* 提供请求处理过程中的上下文信息
|
|
58
|
+
*/
|
|
59
|
+
export interface ExecutionContext {
|
|
60
|
+
/**
|
|
61
|
+
* 获取 HTTP 上下文
|
|
62
|
+
* @returns HTTP 参数主机
|
|
63
|
+
*/
|
|
64
|
+
switchToHttp(): HttpArgumentsHost;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 获取 WebSocket 上下文
|
|
68
|
+
* @returns WebSocket 参数主机
|
|
69
|
+
*/
|
|
70
|
+
switchToWs(): WsArgumentsHost;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 获取当前处理的控制器类
|
|
74
|
+
* @returns 控制器类构造函数
|
|
75
|
+
*/
|
|
76
|
+
getClass(): Constructor<unknown>;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 获取当前处理的方法
|
|
80
|
+
* @returns 方法函数
|
|
81
|
+
*/
|
|
82
|
+
getHandler(): Function;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 获取方法名
|
|
86
|
+
* @returns 方法名字符串
|
|
87
|
+
*/
|
|
88
|
+
getMethodName(): string;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* 获取方法或类的元数据
|
|
92
|
+
* @param key - 元数据键
|
|
93
|
+
* @returns 元数据值,如果不存在则返回 undefined
|
|
94
|
+
*/
|
|
95
|
+
getMetadata<T>(key: string | symbol): T | undefined;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 获取请求参数
|
|
99
|
+
* @returns 请求参数数组
|
|
100
|
+
*/
|
|
101
|
+
getArgs(): unknown[];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 守卫类型:可以是守卫类构造函数或守卫实例
|
|
106
|
+
*/
|
|
107
|
+
export type GuardType = Constructor<CanActivate> | CanActivate;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 守卫元数据
|
|
111
|
+
*/
|
|
112
|
+
export interface GuardMetadata {
|
|
113
|
+
/**
|
|
114
|
+
* 守卫列表
|
|
115
|
+
*/
|
|
116
|
+
guards: GuardType[];
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 守卫配置选项
|
|
121
|
+
*/
|
|
122
|
+
export interface GuardOptions {
|
|
123
|
+
/**
|
|
124
|
+
* 是否跳过全局守卫
|
|
125
|
+
* @default false
|
|
126
|
+
*/
|
|
127
|
+
skipGlobalGuards?: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 守卫元数据键
|
|
132
|
+
*/
|
|
133
|
+
export const GUARDS_METADATA_KEY = Symbol('@dangao/bun-server:guards');
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 守卫注册表 Token
|
|
137
|
+
*/
|
|
138
|
+
export const GUARD_REGISTRY_TOKEN = Symbol('@dangao/bun-server:guard-registry');
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Roles 元数据键
|
|
142
|
+
*/
|
|
143
|
+
export const ROLES_METADATA_KEY = Symbol('@dangao/bun-server:roles');
|
|
144
|
+
|
package/src/security/index.ts
CHANGED
|
@@ -2,11 +2,15 @@ import { Module, MODULE_METADATA_KEY } from '../di/module';
|
|
|
2
2
|
import { AuthenticationManager } from './authentication-manager';
|
|
3
3
|
import { JwtAuthenticationProvider } from './providers/jwt-provider';
|
|
4
4
|
import { OAuth2AuthenticationProvider } from './providers/oauth2-provider';
|
|
5
|
-
import { createSecurityFilter } from './filter';
|
|
5
|
+
import { createSecurityFilter, getGuardRegistry, registerReflector } from './filter';
|
|
6
6
|
import { JWTUtil } from '../auth/jwt';
|
|
7
7
|
import { OAuth2Service } from '../auth/oauth2';
|
|
8
8
|
import { OAuth2Controller, OAUTH2_SERVICE_TOKEN, JWT_UTIL_TOKEN } from '../auth/controller';
|
|
9
9
|
import type { JWTConfig, OAuth2Client, UserInfo } from '../auth/types';
|
|
10
|
+
import type { GuardType } from './guards/types';
|
|
11
|
+
import { GUARD_REGISTRY_TOKEN } from './guards/types';
|
|
12
|
+
import { GuardRegistry } from './guards/guard-registry';
|
|
13
|
+
import { Reflector, REFLECTOR_TOKEN } from './guards/reflector';
|
|
10
14
|
|
|
11
15
|
/**
|
|
12
16
|
* 安全模块配置
|
|
@@ -45,8 +49,17 @@ export interface SecurityModuleConfig {
|
|
|
45
49
|
* @default false
|
|
46
50
|
*/
|
|
47
51
|
defaultAuthRequired?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* 全局守卫列表
|
|
54
|
+
*/
|
|
55
|
+
globalGuards?: GuardType[];
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
/**
|
|
59
|
+
* 内部存储:容器引用和守卫注册表
|
|
60
|
+
*/
|
|
61
|
+
let _guardRegistry: GuardRegistry | null = null;
|
|
62
|
+
|
|
50
63
|
/**
|
|
51
64
|
* 安全模块
|
|
52
65
|
*/
|
|
@@ -82,7 +95,23 @@ export class SecurityModule {
|
|
|
82
95
|
new OAuth2AuthenticationProvider(oauth2Service, jwtUtil),
|
|
83
96
|
);
|
|
84
97
|
|
|
85
|
-
//
|
|
98
|
+
// 创建守卫注册表(每次 forRoot 都创建新实例,避免测试间污染)
|
|
99
|
+
const guardRegistry = new GuardRegistry();
|
|
100
|
+
// 清理旧的 registry(如果存在)
|
|
101
|
+
if (_guardRegistry) {
|
|
102
|
+
_guardRegistry.clearGlobalGuards();
|
|
103
|
+
}
|
|
104
|
+
_guardRegistry = guardRegistry;
|
|
105
|
+
|
|
106
|
+
// 添加全局守卫
|
|
107
|
+
if (config.globalGuards && config.globalGuards.length > 0) {
|
|
108
|
+
guardRegistry.addGlobalGuards(...config.globalGuards);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 创建 Reflector
|
|
112
|
+
const reflector = new Reflector();
|
|
113
|
+
|
|
114
|
+
// 创建安全过滤器(暂时不传 container,将在模块注册时更新)
|
|
86
115
|
const securityFilter = createSecurityFilter({
|
|
87
116
|
authenticationManager,
|
|
88
117
|
excludePaths: [
|
|
@@ -92,6 +121,7 @@ export class SecurityModule {
|
|
|
92
121
|
: []),
|
|
93
122
|
],
|
|
94
123
|
defaultAuthRequired: config.defaultAuthRequired ?? false,
|
|
124
|
+
guardRegistry,
|
|
95
125
|
});
|
|
96
126
|
|
|
97
127
|
const controllers: any[] = [];
|
|
@@ -117,6 +147,14 @@ export class SecurityModule {
|
|
|
117
147
|
provide: AuthenticationManager,
|
|
118
148
|
useValue: authenticationManager,
|
|
119
149
|
},
|
|
150
|
+
{
|
|
151
|
+
provide: GUARD_REGISTRY_TOKEN,
|
|
152
|
+
useValue: guardRegistry,
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
provide: REFLECTOR_TOKEN,
|
|
156
|
+
useValue: reflector,
|
|
157
|
+
},
|
|
120
158
|
);
|
|
121
159
|
|
|
122
160
|
// 添加安全过滤器中间件
|
|
@@ -135,11 +173,43 @@ export class SecurityModule {
|
|
|
135
173
|
JWT_UTIL_TOKEN,
|
|
136
174
|
OAUTH2_SERVICE_TOKEN,
|
|
137
175
|
AuthenticationManager,
|
|
176
|
+
GUARD_REGISTRY_TOKEN,
|
|
177
|
+
REFLECTOR_TOKEN,
|
|
138
178
|
],
|
|
139
179
|
};
|
|
140
180
|
Reflect.defineMetadata(MODULE_METADATA_KEY, metadata, SecurityModule);
|
|
141
181
|
|
|
142
182
|
return SecurityModule;
|
|
143
183
|
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 获取守卫注册表
|
|
187
|
+
* @returns 守卫注册表实例
|
|
188
|
+
*/
|
|
189
|
+
public static getGuardRegistry(): GuardRegistry | null {
|
|
190
|
+
return _guardRegistry;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 添加全局守卫
|
|
195
|
+
* @param guards - 守卫类或实例
|
|
196
|
+
*/
|
|
197
|
+
public static addGlobalGuards(...guards: GuardType[]): void {
|
|
198
|
+
if (_guardRegistry) {
|
|
199
|
+
_guardRegistry.addGlobalGuards(...guards);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* 重置模块状态(主要用于测试)
|
|
205
|
+
*/
|
|
206
|
+
public static reset(): void {
|
|
207
|
+
if (_guardRegistry) {
|
|
208
|
+
_guardRegistry.clearGlobalGuards();
|
|
209
|
+
}
|
|
210
|
+
_guardRegistry = null;
|
|
211
|
+
// 清除模块元数据
|
|
212
|
+
Reflect.deleteMetadata(MODULE_METADATA_KEY, SecurityModule);
|
|
213
|
+
}
|
|
144
214
|
}
|
|
145
215
|
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import 'reflect-metadata';
|
|
2
|
+
import type { ValidationRuleDefinition, ClassValidationMetadata, ValidationOptions } from './types';
|
|
3
|
+
import { ValidationError, type ValidationIssue } from './errors';
|
|
4
|
+
|
|
5
|
+
const CLASS_VALIDATION_METADATA_KEY = Symbol('validation:class');
|
|
6
|
+
const PROPERTY_VALIDATION_METADATA_KEY = Symbol('validation:property');
|
|
7
|
+
const VALIDATE_CLASS_METADATA_KEY = Symbol('validation:validateClass');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 标记类需要验证
|
|
11
|
+
* 应用此装饰器后,类的实例可以使用 validateObject 函数进行验证
|
|
12
|
+
*/
|
|
13
|
+
export function ValidateClass(): ClassDecorator {
|
|
14
|
+
return (target) => {
|
|
15
|
+
Reflect.defineMetadata(VALIDATE_CLASS_METADATA_KEY, true, target);
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 属性验证装饰器
|
|
21
|
+
* 用于 DTO 类的属性级别验证
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* @ValidateClass()
|
|
25
|
+
* class CreateUserDto {
|
|
26
|
+
* @Property(IsString(), MinLength(2))
|
|
27
|
+
* name: string;
|
|
28
|
+
*
|
|
29
|
+
* @Property(IsEmail())
|
|
30
|
+
* email: string;
|
|
31
|
+
*
|
|
32
|
+
* @Property(IsOptional(), IsNumber())
|
|
33
|
+
* age?: number;
|
|
34
|
+
* }
|
|
35
|
+
*/
|
|
36
|
+
export function Property(...rules: ValidationRuleDefinition[]): PropertyDecorator {
|
|
37
|
+
return (target, propertyKey) => {
|
|
38
|
+
if (typeof propertyKey === 'symbol') {
|
|
39
|
+
throw new Error('@Property decorator does not support symbol property keys');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 获取或创建类级别的元数据数组
|
|
43
|
+
const existingMetadata: ClassValidationMetadata[] =
|
|
44
|
+
Reflect.getMetadata(CLASS_VALIDATION_METADATA_KEY, target.constructor) ?? [];
|
|
45
|
+
|
|
46
|
+
// 查找现有的属性元数据
|
|
47
|
+
let propertyMetadata = existingMetadata.find((m) => m.property === propertyKey);
|
|
48
|
+
if (!propertyMetadata) {
|
|
49
|
+
propertyMetadata = { property: propertyKey, rules: [] };
|
|
50
|
+
existingMetadata.push(propertyMetadata);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 添加规则
|
|
54
|
+
propertyMetadata.rules.push(...rules);
|
|
55
|
+
|
|
56
|
+
// 保存元数据
|
|
57
|
+
Reflect.defineMetadata(CLASS_VALIDATION_METADATA_KEY, existingMetadata, target.constructor);
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 获取类的验证元数据
|
|
63
|
+
*/
|
|
64
|
+
export function getClassValidationMetadata(target: new () => unknown): ClassValidationMetadata[] {
|
|
65
|
+
return Reflect.getMetadata(CLASS_VALIDATION_METADATA_KEY, target) ?? [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 检查类是否标记为需要验证
|
|
70
|
+
*/
|
|
71
|
+
export function isValidateClass(target: new () => unknown): boolean {
|
|
72
|
+
return Reflect.getMetadata(VALIDATE_CLASS_METADATA_KEY, target) === true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* 验证单个值
|
|
77
|
+
*/
|
|
78
|
+
function validateValue(
|
|
79
|
+
value: unknown,
|
|
80
|
+
rules: ValidationRuleDefinition[],
|
|
81
|
+
property: string,
|
|
82
|
+
fullObject: unknown,
|
|
83
|
+
): ValidationIssue[] {
|
|
84
|
+
const issues: ValidationIssue[] = [];
|
|
85
|
+
let currentValue = value;
|
|
86
|
+
let shouldSkip = false;
|
|
87
|
+
|
|
88
|
+
for (const rule of rules) {
|
|
89
|
+
// 处理条件验证
|
|
90
|
+
if (rule.condition && !rule.condition(currentValue, fullObject)) {
|
|
91
|
+
shouldSkip = true;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// 处理可选字段
|
|
96
|
+
if (rule.optional && (currentValue === undefined || currentValue === null)) {
|
|
97
|
+
shouldSkip = true;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 处理转换
|
|
102
|
+
if (rule.transform) {
|
|
103
|
+
currentValue = rule.transform(currentValue);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 处理嵌套验证
|
|
107
|
+
if (rule.nested) {
|
|
108
|
+
if (rule.each && Array.isArray(currentValue)) {
|
|
109
|
+
// 数组嵌套验证
|
|
110
|
+
for (let i = 0; i < currentValue.length; i++) {
|
|
111
|
+
const item = currentValue[i];
|
|
112
|
+
if (typeof item === 'object' && item !== null && rule.nestedType) {
|
|
113
|
+
const nestedIssues = validateObjectInternal(item, rule.nestedType, `${property}[${i}]`);
|
|
114
|
+
issues.push(...nestedIssues);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else if (typeof currentValue === 'object' && currentValue !== null && rule.nestedType) {
|
|
118
|
+
// 单个对象嵌套验证
|
|
119
|
+
const nestedIssues = validateObjectInternal(currentValue, rule.nestedType, property);
|
|
120
|
+
issues.push(...nestedIssues);
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 执行验证
|
|
126
|
+
const passed = rule.validate(currentValue, fullObject);
|
|
127
|
+
if (!passed) {
|
|
128
|
+
issues.push({
|
|
129
|
+
property,
|
|
130
|
+
rule: rule.name,
|
|
131
|
+
message: rule.message,
|
|
132
|
+
value: currentValue,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return shouldSkip ? [] : issues;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 内部验证函数
|
|
142
|
+
*/
|
|
143
|
+
function validateObjectInternal(
|
|
144
|
+
obj: unknown,
|
|
145
|
+
targetClass: new () => unknown,
|
|
146
|
+
prefix = '',
|
|
147
|
+
): ValidationIssue[] {
|
|
148
|
+
const metadata = getClassValidationMetadata(targetClass);
|
|
149
|
+
const issues: ValidationIssue[] = [];
|
|
150
|
+
|
|
151
|
+
if (typeof obj !== 'object' || obj === null) {
|
|
152
|
+
issues.push({
|
|
153
|
+
property: prefix || 'root',
|
|
154
|
+
rule: 'isObject',
|
|
155
|
+
message: '必须是对象',
|
|
156
|
+
value: obj,
|
|
157
|
+
});
|
|
158
|
+
return issues;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const objRecord = obj as Record<string, unknown>;
|
|
162
|
+
|
|
163
|
+
for (const meta of metadata) {
|
|
164
|
+
const propertyPath = prefix ? `${prefix}.${meta.property}` : meta.property;
|
|
165
|
+
const value = objRecord[meta.property];
|
|
166
|
+
const propertyIssues = validateValue(value, meta.rules, propertyPath, obj);
|
|
167
|
+
issues.push(...propertyIssues);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return issues;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* 验证对象
|
|
175
|
+
*
|
|
176
|
+
* @param obj - 要验证的对象
|
|
177
|
+
* @param targetClass - DTO 类
|
|
178
|
+
* @param options - 验证选项
|
|
179
|
+
* @throws {ValidationError} 验证失败时抛出
|
|
180
|
+
*
|
|
181
|
+
* @example
|
|
182
|
+
* @ValidateClass()
|
|
183
|
+
* class CreateUserDto {
|
|
184
|
+
* @Property(IsString(), MinLength(2))
|
|
185
|
+
* name: string;
|
|
186
|
+
*
|
|
187
|
+
* @Property(IsEmail())
|
|
188
|
+
* email: string;
|
|
189
|
+
* }
|
|
190
|
+
*
|
|
191
|
+
* const dto = { name: 'A', email: 'invalid' };
|
|
192
|
+
* validateObject(dto, CreateUserDto); // throws ValidationError
|
|
193
|
+
*/
|
|
194
|
+
export function validateObject<T>(
|
|
195
|
+
obj: unknown,
|
|
196
|
+
targetClass: new () => T,
|
|
197
|
+
options: ValidationOptions = {},
|
|
198
|
+
): void {
|
|
199
|
+
const issues = validateObjectInternal(obj, targetClass);
|
|
200
|
+
|
|
201
|
+
if (options.stopAtFirstError && issues.length > 0) {
|
|
202
|
+
throw new ValidationError('Validation failed', [issues[0]]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (issues.length > 0) {
|
|
206
|
+
throw new ValidationError('Validation failed', issues);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* 验证对象并返回验证结果(不抛出异常)
|
|
212
|
+
*
|
|
213
|
+
* @param obj - 要验证的对象
|
|
214
|
+
* @param targetClass - DTO 类
|
|
215
|
+
* @param options - 验证选项
|
|
216
|
+
* @returns 验证结果
|
|
217
|
+
*/
|
|
218
|
+
export function validateObjectSync<T>(
|
|
219
|
+
obj: unknown,
|
|
220
|
+
targetClass: new () => T,
|
|
221
|
+
options: ValidationOptions = {},
|
|
222
|
+
): { valid: boolean; issues: ValidationIssue[] } {
|
|
223
|
+
const issues = validateObjectInternal(obj, targetClass);
|
|
224
|
+
|
|
225
|
+
if (options.stopAtFirstError && issues.length > 0) {
|
|
226
|
+
return { valid: false, issues: [issues[0]] };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return { valid: issues.length === 0, issues };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 带嵌套类型的属性验证装饰器
|
|
234
|
+
* 用于嵌套对象验证
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* @ValidateClass()
|
|
238
|
+
* class AddressDto {
|
|
239
|
+
* @Property(IsString())
|
|
240
|
+
* city: string;
|
|
241
|
+
* }
|
|
242
|
+
*
|
|
243
|
+
* @ValidateClass()
|
|
244
|
+
* class CreateUserDto {
|
|
245
|
+
* @Property(IsString())
|
|
246
|
+
* name: string;
|
|
247
|
+
*
|
|
248
|
+
* @NestedProperty(AddressDto)
|
|
249
|
+
* address: AddressDto;
|
|
250
|
+
* }
|
|
251
|
+
*/
|
|
252
|
+
export function NestedProperty<T>(nestedClass: new () => T): PropertyDecorator {
|
|
253
|
+
return (target, propertyKey) => {
|
|
254
|
+
if (typeof propertyKey === 'symbol') {
|
|
255
|
+
throw new Error('@NestedProperty decorator does not support symbol property keys');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const existingMetadata: ClassValidationMetadata[] =
|
|
259
|
+
Reflect.getMetadata(CLASS_VALIDATION_METADATA_KEY, target.constructor) ?? [];
|
|
260
|
+
|
|
261
|
+
let propertyMetadata = existingMetadata.find((m) => m.property === propertyKey);
|
|
262
|
+
if (!propertyMetadata) {
|
|
263
|
+
propertyMetadata = { property: propertyKey, rules: [] };
|
|
264
|
+
existingMetadata.push(propertyMetadata);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
propertyMetadata.rules.push({
|
|
268
|
+
name: 'validateNested',
|
|
269
|
+
message: '嵌套对象验证失败',
|
|
270
|
+
nested: true,
|
|
271
|
+
nestedType: nestedClass,
|
|
272
|
+
validate: (value) => typeof value === 'object' && value !== null && !Array.isArray(value),
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
Reflect.defineMetadata(CLASS_VALIDATION_METADATA_KEY, existingMetadata, target.constructor);
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 数组嵌套属性验证装饰器
|
|
281
|
+
* 用于数组中每个元素的验证
|
|
282
|
+
*
|
|
283
|
+
* @example
|
|
284
|
+
* @ValidateClass()
|
|
285
|
+
* class ItemDto {
|
|
286
|
+
* @Property(IsString())
|
|
287
|
+
* name: string;
|
|
288
|
+
* }
|
|
289
|
+
*
|
|
290
|
+
* @ValidateClass()
|
|
291
|
+
* class OrderDto {
|
|
292
|
+
* @ArrayNestedProperty(ItemDto)
|
|
293
|
+
* items: ItemDto[];
|
|
294
|
+
* }
|
|
295
|
+
*/
|
|
296
|
+
export function ArrayNestedProperty<T>(nestedClass: new () => T): PropertyDecorator {
|
|
297
|
+
return (target, propertyKey) => {
|
|
298
|
+
if (typeof propertyKey === 'symbol') {
|
|
299
|
+
throw new Error('@ArrayNestedProperty decorator does not support symbol property keys');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const existingMetadata: ClassValidationMetadata[] =
|
|
303
|
+
Reflect.getMetadata(CLASS_VALIDATION_METADATA_KEY, target.constructor) ?? [];
|
|
304
|
+
|
|
305
|
+
let propertyMetadata = existingMetadata.find((m) => m.property === propertyKey);
|
|
306
|
+
if (!propertyMetadata) {
|
|
307
|
+
propertyMetadata = { property: propertyKey, rules: [] };
|
|
308
|
+
existingMetadata.push(propertyMetadata);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
propertyMetadata.rules.push({
|
|
312
|
+
name: 'validateNestedArray',
|
|
313
|
+
message: '数组元素验证失败',
|
|
314
|
+
nested: true,
|
|
315
|
+
nestedType: nestedClass,
|
|
316
|
+
each: true,
|
|
317
|
+
validate: (value) => Array.isArray(value),
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
Reflect.defineMetadata(CLASS_VALIDATION_METADATA_KEY, existingMetadata, target.constructor);
|
|
321
|
+
};
|
|
322
|
+
}
|