@dangao/bun-server 1.7.0 → 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 +196 -19
- package/dist/cache/cache-module.d.ts +18 -0
- package/dist/cache/cache-module.d.ts.map +1 -1
- package/dist/cache/index.d.ts +3 -1
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/interceptors.d.ts +41 -0
- package/dist/cache/interceptors.d.ts.map +1 -0
- package/dist/cache/service-proxy.d.ts +62 -0
- package/dist/cache/service-proxy.d.ts.map +1 -0
- package/dist/controller/controller.d.ts +8 -0
- package/dist/controller/controller.d.ts.map +1 -1
- package/dist/core/application.d.ts +5 -0
- package/dist/core/application.d.ts.map +1 -1
- package/dist/di/container.d.ts +18 -1
- package/dist/di/container.d.ts.map +1 -1
- package/dist/di/decorators.d.ts +37 -0
- package/dist/di/decorators.d.ts.map +1 -1
- package/dist/di/index.d.ts +2 -2
- 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/di/types.d.ts +22 -0
- package/dist/di/types.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 +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4641 -2840
- 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/symbol-interface-pattern.md +431 -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/symbol-interface-pattern.md +431 -0
- package/docs/zh/validation.md +407 -0
- package/package.json +1 -1
- package/src/cache/cache-module.ts +37 -0
- package/src/cache/index.ts +16 -1
- package/src/cache/interceptors.ts +295 -0
- package/src/cache/service-proxy.ts +219 -0
- package/src/controller/controller.ts +30 -6
- package/src/core/application.ts +25 -1
- package/src/di/container.ts +57 -7
- package/src/di/decorators.ts +46 -0
- package/src/di/index.ts +17 -2
- package/src/di/module-registry.ts +39 -0
- package/src/di/types.ts +29 -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 +140 -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/cache/cache-decorators.test.ts +284 -0
- package/tests/controller/path-combination.test.ts +353 -0
- 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,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
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { ValidationRuleDefinition } from './types';
|
|
2
|
+
|
|
3
|
+
export interface CustomValidatorOptions {
|
|
4
|
+
message?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 创建自定义验证器
|
|
9
|
+
*
|
|
10
|
+
* @param name - 验证器名称
|
|
11
|
+
* @param validate - 验证函数,接收值和可选参数,返回布尔值
|
|
12
|
+
* @param defaultMessage - 默认错误消息
|
|
13
|
+
* @returns 验证规则工厂函数
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* // 创建手机号验证器
|
|
17
|
+
* const IsPhoneNumber = createCustomValidator(
|
|
18
|
+
* 'isPhoneNumber',
|
|
19
|
+
* (value) => typeof value === 'string' && /^1[3-9]\d{9}$/.test(value),
|
|
20
|
+
* '必须是有效的手机号码'
|
|
21
|
+
* );
|
|
22
|
+
*
|
|
23
|
+
* // 使用
|
|
24
|
+
* class UserDto {
|
|
25
|
+
* @Validate(IsPhoneNumber())
|
|
26
|
+
* phone: string;
|
|
27
|
+
* }
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* // 创建带参数的验证器
|
|
31
|
+
* const IsDivisibleBy = createCustomValidator(
|
|
32
|
+
* 'isDivisibleBy',
|
|
33
|
+
* (value, divisor: number) => typeof value === 'number' && value % divisor === 0,
|
|
34
|
+
* (divisor: number) => `必须能被 ${divisor} 整除`
|
|
35
|
+
* );
|
|
36
|
+
*
|
|
37
|
+
* // 使用
|
|
38
|
+
* class NumberDto {
|
|
39
|
+
* @Validate(IsDivisibleBy(5))
|
|
40
|
+
* value: number;
|
|
41
|
+
* }
|
|
42
|
+
*/
|
|
43
|
+
export function createCustomValidator<TArgs extends unknown[] = []>(
|
|
44
|
+
name: string,
|
|
45
|
+
validate: (value: unknown, ...args: TArgs) => boolean,
|
|
46
|
+
defaultMessage: string | ((...args: TArgs) => string),
|
|
47
|
+
): (...args: TArgs) => (options?: CustomValidatorOptions) => ValidationRuleDefinition {
|
|
48
|
+
return (...args: TArgs) =>
|
|
49
|
+
(options: CustomValidatorOptions = {}): ValidationRuleDefinition => {
|
|
50
|
+
const message =
|
|
51
|
+
options.message ??
|
|
52
|
+
(typeof defaultMessage === 'function' ? defaultMessage(...args) : defaultMessage);
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
name,
|
|
56
|
+
message,
|
|
57
|
+
validate: (value: unknown) => validate(value, ...args),
|
|
58
|
+
};
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 创建简单自定义验证器(无参数)
|
|
64
|
+
*
|
|
65
|
+
* @param name - 验证器名称
|
|
66
|
+
* @param validate - 验证函数
|
|
67
|
+
* @param defaultMessage - 默认错误消息
|
|
68
|
+
* @returns 验证规则工厂函数
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* const IsPhoneNumber = createSimpleValidator(
|
|
72
|
+
* 'isPhoneNumber',
|
|
73
|
+
* (value) => typeof value === 'string' && /^1[3-9]\d{9}$/.test(value),
|
|
74
|
+
* '必须是有效的手机号码'
|
|
75
|
+
* );
|
|
76
|
+
*
|
|
77
|
+
* // 使用
|
|
78
|
+
* @Validate(IsPhoneNumber())
|
|
79
|
+
* phone: string;
|
|
80
|
+
*/
|
|
81
|
+
export function createSimpleValidator(
|
|
82
|
+
name: string,
|
|
83
|
+
validate: (value: unknown) => boolean,
|
|
84
|
+
defaultMessage: string,
|
|
85
|
+
): (options?: CustomValidatorOptions) => ValidationRuleDefinition {
|
|
86
|
+
return (options: CustomValidatorOptions = {}): ValidationRuleDefinition => ({
|
|
87
|
+
name,
|
|
88
|
+
message: options.message ?? defaultMessage,
|
|
89
|
+
validate,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 创建正则表达式验证器
|
|
95
|
+
*
|
|
96
|
+
* @param name - 验证器名称
|
|
97
|
+
* @param pattern - 正则表达式
|
|
98
|
+
* @param defaultMessage - 默认错误消息
|
|
99
|
+
* @returns 验证规则工厂函数
|
|
100
|
+
*
|
|
101
|
+
* @example
|
|
102
|
+
* const IsSlug = createRegexValidator(
|
|
103
|
+
* 'isSlug',
|
|
104
|
+
* /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
|
|
105
|
+
* '必须是有效的 slug 格式'
|
|
106
|
+
* );
|
|
107
|
+
*/
|
|
108
|
+
export function createRegexValidator(
|
|
109
|
+
name: string,
|
|
110
|
+
pattern: RegExp,
|
|
111
|
+
defaultMessage: string,
|
|
112
|
+
): (options?: CustomValidatorOptions) => ValidationRuleDefinition {
|
|
113
|
+
return createSimpleValidator(
|
|
114
|
+
name,
|
|
115
|
+
(value) => typeof value === 'string' && pattern.test(value),
|
|
116
|
+
defaultMessage,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ============= 内置扩展验证器 =============
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 验证中国大陆手机号
|
|
124
|
+
*/
|
|
125
|
+
export const IsPhoneNumber = createSimpleValidator(
|
|
126
|
+
'isPhoneNumber',
|
|
127
|
+
(value) => typeof value === 'string' && /^1[3-9]\d{9}$/.test(value),
|
|
128
|
+
'必须是有效的手机号码',
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 验证中国身份证号
|
|
133
|
+
*/
|
|
134
|
+
export const IsIdCard = createSimpleValidator(
|
|
135
|
+
'isIdCard',
|
|
136
|
+
(value) => {
|
|
137
|
+
if (typeof value !== 'string') return false;
|
|
138
|
+
// 15位身份证:6位地区码 + 6位出生日期(YYMMDD) + 3位顺序码
|
|
139
|
+
const pattern15 = /^[1-9]\d{5}\d{2}((0[1-9])|(1[0-2]))(([0-2]\d)|30|31)\d{3}$/;
|
|
140
|
+
// 18位身份证:6位地区码 + 8位出生日期(YYYYMMDD) + 3位顺序码 + 1位校验码
|
|
141
|
+
const pattern18 = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2]\d)|30|31)\d{3}[0-9Xx]$/;
|
|
142
|
+
return pattern15.test(value) || pattern18.test(value);
|
|
143
|
+
},
|
|
144
|
+
'必须是有效的身份证号码',
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 验证 IP 地址 (IPv4)
|
|
149
|
+
*/
|
|
150
|
+
export const IsIPv4 = createSimpleValidator(
|
|
151
|
+
'isIPv4',
|
|
152
|
+
(value) => {
|
|
153
|
+
if (typeof value !== 'string') return false;
|
|
154
|
+
const parts = value.split('.');
|
|
155
|
+
if (parts.length !== 4) return false;
|
|
156
|
+
return parts.every((part) => {
|
|
157
|
+
const num = parseInt(part, 10);
|
|
158
|
+
return !Number.isNaN(num) && num >= 0 && num <= 255 && String(num) === part;
|
|
159
|
+
});
|
|
160
|
+
},
|
|
161
|
+
'必须是有效的 IPv4 地址',
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* 验证端口号
|
|
166
|
+
*/
|
|
167
|
+
export const IsPort = createSimpleValidator(
|
|
168
|
+
'isPort',
|
|
169
|
+
(value) => {
|
|
170
|
+
if (typeof value === 'string') {
|
|
171
|
+
const num = parseInt(value, 10);
|
|
172
|
+
return !Number.isNaN(num) && num >= 0 && num <= 65535;
|
|
173
|
+
}
|
|
174
|
+
if (typeof value === 'number') {
|
|
175
|
+
return Number.isInteger(value) && value >= 0 && value <= 65535;
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
},
|
|
179
|
+
'必须是有效的端口号 (0-65535)',
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* 验证邮政编码(中国)
|
|
184
|
+
*/
|
|
185
|
+
export const IsPostalCode = createSimpleValidator(
|
|
186
|
+
'isPostalCode',
|
|
187
|
+
(value) => typeof value === 'string' && /^[1-9]\d{5}$/.test(value),
|
|
188
|
+
'必须是有效的邮政编码',
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* 验证信用卡号(Luhn 算法)
|
|
193
|
+
*/
|
|
194
|
+
export const IsCreditCard = createSimpleValidator(
|
|
195
|
+
'isCreditCard',
|
|
196
|
+
(value) => {
|
|
197
|
+
if (typeof value !== 'string') return false;
|
|
198
|
+
const sanitized = value.replace(/[\s-]/g, '');
|
|
199
|
+
if (!/^\d{13,19}$/.test(sanitized)) return false;
|
|
200
|
+
|
|
201
|
+
// Luhn 算法
|
|
202
|
+
let sum = 0;
|
|
203
|
+
let isEven = false;
|
|
204
|
+
for (let i = sanitized.length - 1; i >= 0; i--) {
|
|
205
|
+
let digit = parseInt(sanitized[i], 10);
|
|
206
|
+
if (isEven) {
|
|
207
|
+
digit *= 2;
|
|
208
|
+
if (digit > 9) digit -= 9;
|
|
209
|
+
}
|
|
210
|
+
sum += digit;
|
|
211
|
+
isEven = !isEven;
|
|
212
|
+
}
|
|
213
|
+
return sum % 10 === 0;
|
|
214
|
+
},
|
|
215
|
+
'必须是有效的信用卡号',
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 验证十六进制颜色值
|
|
220
|
+
*/
|
|
221
|
+
export const IsHexColor = createSimpleValidator(
|
|
222
|
+
'isHexColor',
|
|
223
|
+
(value) => typeof value === 'string' && /^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/.test(value),
|
|
224
|
+
'必须是有效的十六进制颜色值',
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* 验证 MAC 地址
|
|
229
|
+
*/
|
|
230
|
+
export const IsMacAddress = createSimpleValidator(
|
|
231
|
+
'isMacAddress',
|
|
232
|
+
(value) =>
|
|
233
|
+
typeof value === 'string' &&
|
|
234
|
+
/^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/.test(value),
|
|
235
|
+
'必须是有效的 MAC 地址',
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* 验证语义化版本号
|
|
240
|
+
*/
|
|
241
|
+
export const IsSemVer = createSimpleValidator(
|
|
242
|
+
'isSemVer',
|
|
243
|
+
(value) =>
|
|
244
|
+
typeof value === 'string' &&
|
|
245
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/.test(
|
|
246
|
+
value,
|
|
247
|
+
),
|
|
248
|
+
'必须是有效的语义化版本号',
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* 创建能被指定数字整除的验证器
|
|
253
|
+
*/
|
|
254
|
+
export const IsDivisibleBy = createCustomValidator(
|
|
255
|
+
'isDivisibleBy',
|
|
256
|
+
(value: unknown, divisor: number) =>
|
|
257
|
+
typeof value === 'number' && !Number.isNaN(value) && value % divisor === 0,
|
|
258
|
+
(divisor: number) => `必须能被 ${divisor} 整除`,
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 创建范围验证器
|
|
263
|
+
*/
|
|
264
|
+
export const IsBetween = createCustomValidator(
|
|
265
|
+
'isBetween',
|
|
266
|
+
(value: unknown, min: number, max: number) =>
|
|
267
|
+
typeof value === 'number' && value >= min && value <= max,
|
|
268
|
+
(min: number, max: number) => `必须在 ${min} 和 ${max} 之间`,
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* 验证是否包含指定子字符串
|
|
273
|
+
*/
|
|
274
|
+
export const Contains = createCustomValidator(
|
|
275
|
+
'contains',
|
|
276
|
+
(value: unknown, substring: string) =>
|
|
277
|
+
typeof value === 'string' && value.includes(substring),
|
|
278
|
+
(substring: string) => `必须包含 "${substring}"`,
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* 验证是否不包含指定子字符串
|
|
283
|
+
*/
|
|
284
|
+
export const NotContains = createCustomValidator(
|
|
285
|
+
'notContains',
|
|
286
|
+
(value: unknown, substring: string) =>
|
|
287
|
+
typeof value === 'string' && !value.includes(substring),
|
|
288
|
+
(substring: string) => `不能包含 "${substring}"`,
|
|
289
|
+
);
|
package/src/validation/errors.ts
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
export interface ValidationIssue {
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 参数索引(参数级别验证使用)
|
|
4
4
|
*/
|
|
5
|
-
index
|
|
5
|
+
index?: number;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 属性路径(类级别验证使用,支持嵌套如 'user.address.city')
|
|
9
|
+
*/
|
|
10
|
+
property?: string;
|
|
6
11
|
|
|
7
12
|
/**
|
|
8
13
|
* 失败的规则名称
|
|
@@ -13,6 +18,16 @@ export interface ValidationIssue {
|
|
|
13
18
|
* 错误信息
|
|
14
19
|
*/
|
|
15
20
|
message: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 验证失败的值
|
|
24
|
+
*/
|
|
25
|
+
value?: unknown;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 嵌套错误(用于嵌套对象验证)
|
|
29
|
+
*/
|
|
30
|
+
children?: ValidationIssue[];
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
export class ValidationError extends Error {
|
|
@@ -23,6 +38,39 @@ export class ValidationError extends Error {
|
|
|
23
38
|
this.name = 'ValidationError';
|
|
24
39
|
this.issues = issues;
|
|
25
40
|
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 获取所有错误的扁平化列表
|
|
44
|
+
*/
|
|
45
|
+
public getFlattened(): ValidationIssue[] {
|
|
46
|
+
const flatten = (issues: ValidationIssue[], prefix = ''): ValidationIssue[] => {
|
|
47
|
+
const result: ValidationIssue[] = [];
|
|
48
|
+
for (const issue of issues) {
|
|
49
|
+
const propertyPath = prefix ? `${prefix}.${issue.property ?? ''}` : (issue.property ?? '');
|
|
50
|
+
if (issue.children && issue.children.length > 0) {
|
|
51
|
+
result.push(...flatten(issue.children, propertyPath));
|
|
52
|
+
} else {
|
|
53
|
+
result.push({
|
|
54
|
+
...issue,
|
|
55
|
+
property: propertyPath || undefined,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
};
|
|
61
|
+
return flatten(this.issues);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 转换为简单的错误对象
|
|
66
|
+
*/
|
|
67
|
+
public toJSON(): Record<string, unknown> {
|
|
68
|
+
return {
|
|
69
|
+
name: this.name,
|
|
70
|
+
message: this.message,
|
|
71
|
+
issues: this.issues,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
26
74
|
}
|
|
27
75
|
|
|
28
76
|
|