@vytches/ddd-validation 0.26.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/LICENSE +21 -0
- package/LLMGUIDE.md +207 -0
- package/README.md +1562 -0
- package/dist/adapters/base-adapter.d.ts +54 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/business-rules/business-rule-validator-extension.d.ts +29 -0
- package/dist/business-rules/business-rule-validator.d.ts +51 -0
- package/dist/business-rules/index.d.ts +2 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +803 -0
- package/dist/rules-registry.d.ts +69 -0
- package/dist/specifications/async-composite-specification.d.ts +72 -0
- package/dist/specifications/composite-specification.d.ts +25 -0
- package/dist/specifications/index.d.ts +5 -0
- package/dist/specifications/memoized-specification.d.ts +84 -0
- package/dist/specifications/specification-operators.d.ts +70 -0
- package/dist/specifications/specification-validator.d.ts +26 -0
- package/dist/validation-error.d.ts +13 -0
- package/dist/validation-facade.d.ts +51 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,1562 @@
|
|
|
1
|
+
# @vytches/ddd-validation
|
|
2
|
+
|
|
3
|
+
<!-- LLM-METADATA
|
|
4
|
+
Package: @vytches/ddd-validation
|
|
5
|
+
Category: Patterns
|
|
6
|
+
Purpose: Comprehensive validation framework with specifications, rules, and domain-specific validators
|
|
7
|
+
Dependencies: @vytches/ddd-domain-primitives, @vytches/ddd-utils
|
|
8
|
+
Complexity: Medium
|
|
9
|
+
DDD Patterns: Specification Pattern, Validation Rules, Domain Validators
|
|
10
|
+
Integration Points: @vytches/ddd-policies, @vytches/ddd-cqrs, @vytches/ddd-value-objects
|
|
11
|
+
-->
|
|
12
|
+
|
|
13
|
+
[](https://badge.fury.io/js/%40vytches%2Fddd-validation)
|
|
14
|
+
[](https://www.typescriptlang.org/)
|
|
15
|
+
[](https://opensource.org/licenses/MIT)
|
|
16
|
+
|
|
17
|
+
> **Comprehensive validation framework with specifications, fluent rules, and
|
|
18
|
+
> domain-specific validators**
|
|
19
|
+
|
|
20
|
+
Enterprise-grade validation system with specification pattern integration,
|
|
21
|
+
fluent rule builder, async validation support, and comprehensive error
|
|
22
|
+
reporting. Designed for complex domain validation scenarios.
|
|
23
|
+
|
|
24
|
+
## 📋 Table of Contents
|
|
25
|
+
|
|
26
|
+
- [Installation](#installation)
|
|
27
|
+
- [Key Features](#key-features)
|
|
28
|
+
- [Core Concepts](#core-concepts)
|
|
29
|
+
- [Quick Start](#quick-start)
|
|
30
|
+
- [Specification Pattern](#specification-pattern)
|
|
31
|
+
- [Fluent Rules](#fluent-rules)
|
|
32
|
+
- [Domain Validators](#domain-validators)
|
|
33
|
+
- [Async Validation](#async-validation)
|
|
34
|
+
- [Validation Context](#validation-context)
|
|
35
|
+
- [Error Handling](#error-handling)
|
|
36
|
+
- [Custom Validators](#custom-validators)
|
|
37
|
+
- [Integration Patterns](#integration-patterns)
|
|
38
|
+
- [Testing](#testing)
|
|
39
|
+
- [Best Practices](#best-practices)
|
|
40
|
+
- [Contributing](#contributing)
|
|
41
|
+
|
|
42
|
+
## 🚀 Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# npm
|
|
46
|
+
npm install @vytches/ddd-validation
|
|
47
|
+
|
|
48
|
+
# yarn
|
|
49
|
+
yarn add @vytches/ddd-validation
|
|
50
|
+
|
|
51
|
+
# pnpm
|
|
52
|
+
pnpm add @vytches/ddd-validation
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Peer Dependencies
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
# Required for full functionality
|
|
59
|
+
npm install @vytches/ddd-domain-primitives @vytches/ddd-utils
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## ✨ Key Features
|
|
63
|
+
|
|
64
|
+
### Validation Framework
|
|
65
|
+
|
|
66
|
+
- **Specification Pattern**: Composable business rules with logical operations
|
|
67
|
+
- **Fluent Rules**: Intuitive fluent API for building validation rules
|
|
68
|
+
- **Domain Validators**: Built-in validators for common domain objects
|
|
69
|
+
- **Async Validation**: Support for asynchronous validation operations
|
|
70
|
+
|
|
71
|
+
### Enterprise Features
|
|
72
|
+
|
|
73
|
+
- **Validation Context**: Rich context propagation for complex scenarios
|
|
74
|
+
- **Error Reporting**: Comprehensive error details with field-level information
|
|
75
|
+
- **Conditional Validation**: Dynamic validation based on conditions
|
|
76
|
+
- **Rule Composition**: Combine multiple validation rules with logical operators
|
|
77
|
+
|
|
78
|
+
### Developer Experience
|
|
79
|
+
|
|
80
|
+
- **Type Safety**: Full TypeScript support with strict typing
|
|
81
|
+
- **Extensible**: Easy to create custom validators and specifications
|
|
82
|
+
- **Testing Support**: Comprehensive testing utilities and mocks
|
|
83
|
+
- **Integration**: Seamless integration with other VytchesDDD packages
|
|
84
|
+
|
|
85
|
+
## 🎯 Core Concepts
|
|
86
|
+
|
|
87
|
+
### Specification Pattern
|
|
88
|
+
|
|
89
|
+
Specifications encapsulate validation rules that can be composed:
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// Base specification interface
|
|
93
|
+
interface ISpecification<T> {
|
|
94
|
+
isSatisfiedBy(entity: T): boolean;
|
|
95
|
+
and(other: ISpecification<T>): ISpecification<T>;
|
|
96
|
+
or(other: ISpecification<T>): ISpecification<T>;
|
|
97
|
+
not(): ISpecification<T>;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Async specification interface
|
|
101
|
+
interface IAsyncSpecification<T> {
|
|
102
|
+
isSatisfiedByAsync(entity: T): Promise<boolean>;
|
|
103
|
+
andAsync(other: IAsyncSpecification<T>): IAsyncSpecification<T>;
|
|
104
|
+
orAsync(other: IAsyncSpecification<T>): IAsyncSpecification<T>;
|
|
105
|
+
notAsync(): IAsyncSpecification<T>;
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Validation Rules
|
|
110
|
+
|
|
111
|
+
Fluent API for building validation rules:
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
// Rule builder interface
|
|
115
|
+
interface IRuleBuilder<T> {
|
|
116
|
+
required(): IRuleBuilder<T>;
|
|
117
|
+
optional(): IRuleBuilder<T>;
|
|
118
|
+
minLength(length: number): IRuleBuilder<T>;
|
|
119
|
+
maxLength(length: number): IRuleBuilder<T>;
|
|
120
|
+
pattern(regex: RegExp): IRuleBuilder<T>;
|
|
121
|
+
custom(validator: (value: T) => boolean): IRuleBuilder<T>;
|
|
122
|
+
build(): IValidationRule<T>;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Validation Result
|
|
127
|
+
|
|
128
|
+
Comprehensive validation result with error details:
|
|
129
|
+
|
|
130
|
+
```typescript
|
|
131
|
+
interface ValidationResult {
|
|
132
|
+
isValid: boolean;
|
|
133
|
+
errors: ValidationError[];
|
|
134
|
+
warnings: ValidationWarning[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface ValidationError {
|
|
138
|
+
field: string;
|
|
139
|
+
code: string;
|
|
140
|
+
message: string;
|
|
141
|
+
details?: Record<string, any>;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## 🚀 Quick Start
|
|
146
|
+
|
|
147
|
+
### 1. Basic Specification Usage
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
import { Specification } from '@vytches/ddd-validation';
|
|
151
|
+
|
|
152
|
+
// Create a simple specification
|
|
153
|
+
class AgeSpecification extends Specification<User> {
|
|
154
|
+
constructor(private readonly minAge: number) {
|
|
155
|
+
super();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
isSatisfiedBy(user: User): boolean {
|
|
159
|
+
return user.age >= this.minAge;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
class EmailSpecification extends Specification<User> {
|
|
164
|
+
isSatisfiedBy(user: User): boolean {
|
|
165
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
166
|
+
return emailRegex.test(user.email);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Compose specifications
|
|
171
|
+
const userValidation = new AgeSpecification(18).and(new EmailSpecification());
|
|
172
|
+
|
|
173
|
+
// Validate user
|
|
174
|
+
const user = new User('John Doe', 'john@example.com', 25);
|
|
175
|
+
const isValid = userValidation.isSatisfiedBy(user);
|
|
176
|
+
console.log('User is valid:', isValid);
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### 2. Fluent Rules
|
|
180
|
+
|
|
181
|
+
```typescript
|
|
182
|
+
import { Rules } from '@vytches/ddd-validation';
|
|
183
|
+
|
|
184
|
+
// Create fluent rules
|
|
185
|
+
const nameRule = Rules.forString()
|
|
186
|
+
.required()
|
|
187
|
+
.minLength(2)
|
|
188
|
+
.maxLength(50)
|
|
189
|
+
.pattern(/^[A-Za-z\s]+$/)
|
|
190
|
+
.build();
|
|
191
|
+
|
|
192
|
+
const emailRule = Rules.forString().required().email().build();
|
|
193
|
+
|
|
194
|
+
const ageRule = Rules.forNumber().required().min(18).max(120).build();
|
|
195
|
+
|
|
196
|
+
// Validate individual fields
|
|
197
|
+
const nameValidation = nameRule.validate('John Doe');
|
|
198
|
+
const emailValidation = emailRule.validate('john@example.com');
|
|
199
|
+
const ageValidation = ageRule.validate(25);
|
|
200
|
+
|
|
201
|
+
console.log('Name valid:', nameValidation.isValid);
|
|
202
|
+
console.log('Email valid:', emailValidation.isValid);
|
|
203
|
+
console.log('Age valid:', ageValidation.isValid);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### 3. Domain Validators
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { DomainValidator } from '@vytches/ddd-validation';
|
|
210
|
+
|
|
211
|
+
// Create domain validator
|
|
212
|
+
const userValidator = DomainValidator.create<User>()
|
|
213
|
+
.forProperty('name', Rules.forString().required().minLength(2))
|
|
214
|
+
.forProperty('email', Rules.forString().required().email())
|
|
215
|
+
.forProperty('age', Rules.forNumber().required().min(18))
|
|
216
|
+
.build();
|
|
217
|
+
|
|
218
|
+
// Validate entire object
|
|
219
|
+
const user = new User('John Doe', 'john@example.com', 25);
|
|
220
|
+
const validation = await userValidator.validate(user);
|
|
221
|
+
|
|
222
|
+
if (!validation.isValid) {
|
|
223
|
+
console.log('Validation errors:');
|
|
224
|
+
validation.errors.forEach(error => {
|
|
225
|
+
console.log(`${error.field}: ${error.message}`);
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## 🔍 Specification Pattern
|
|
231
|
+
|
|
232
|
+
### Basic Specifications
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
import { Specification } from '@vytches/ddd-validation';
|
|
236
|
+
|
|
237
|
+
// User age specification
|
|
238
|
+
class UserAgeSpecification extends Specification<User> {
|
|
239
|
+
constructor(private readonly minAge: number) {
|
|
240
|
+
super();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
isSatisfiedBy(user: User): boolean {
|
|
244
|
+
return user.age >= this.minAge;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// User email specification
|
|
249
|
+
class UserEmailSpecification extends Specification<User> {
|
|
250
|
+
isSatisfiedBy(user: User): boolean {
|
|
251
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
252
|
+
return emailRegex.test(user.email);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// User role specification
|
|
257
|
+
class UserRoleSpecification extends Specification<User> {
|
|
258
|
+
constructor(private readonly allowedRoles: UserRole[]) {
|
|
259
|
+
super();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
isSatisfiedBy(user: User): boolean {
|
|
263
|
+
return this.allowedRoles.includes(user.role);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
### Composite Specifications
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
// Combine specifications with logical operators
|
|
272
|
+
const adultUserSpec = new UserAgeSpecification(18);
|
|
273
|
+
const validEmailSpec = new UserEmailSpecification();
|
|
274
|
+
const adminOrManagerSpec = new UserRoleSpecification([
|
|
275
|
+
UserRole.ADMIN,
|
|
276
|
+
UserRole.MANAGER,
|
|
277
|
+
]);
|
|
278
|
+
|
|
279
|
+
// AND operation
|
|
280
|
+
const validAdultUser = adultUserSpec.and(validEmailSpec);
|
|
281
|
+
|
|
282
|
+
// OR operation
|
|
283
|
+
const privilegedUser = adminOrManagerSpec.or(
|
|
284
|
+
new UserRoleSpecification([UserRole.SUPERVISOR])
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// NOT operation
|
|
288
|
+
const nonAdminUser = adminOrManagerSpec.not();
|
|
289
|
+
|
|
290
|
+
// Complex composition
|
|
291
|
+
const validPrivilegedAdult = adultUserSpec
|
|
292
|
+
.and(validEmailSpec)
|
|
293
|
+
.and(privilegedUser);
|
|
294
|
+
|
|
295
|
+
// Usage
|
|
296
|
+
const user = new User('John Doe', 'john@example.com', 25, UserRole.ADMIN);
|
|
297
|
+
const isValid = validPrivilegedAdult.isSatisfiedBy(user);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
### Parametrized Specifications
|
|
301
|
+
|
|
302
|
+
```typescript
|
|
303
|
+
// Specifications with parameters
|
|
304
|
+
class UserRegistrationDateSpecification extends Specification<User> {
|
|
305
|
+
constructor(
|
|
306
|
+
private readonly startDate: Date,
|
|
307
|
+
private readonly endDate: Date
|
|
308
|
+
) {
|
|
309
|
+
super();
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
isSatisfiedBy(user: User): boolean {
|
|
313
|
+
return (
|
|
314
|
+
user.registeredAt >= this.startDate && user.registeredAt <= this.endDate
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
class UserLocationSpecification extends Specification<User> {
|
|
320
|
+
constructor(private readonly allowedCountries: string[]) {
|
|
321
|
+
super();
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
isSatisfiedBy(user: User): boolean {
|
|
325
|
+
return this.allowedCountries.includes(user.country);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Usage with parameters
|
|
330
|
+
const recentUsers = new UserRegistrationDateSpecification(
|
|
331
|
+
new Date('2023-01-01'),
|
|
332
|
+
new Date('2023-12-31')
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
const euUsers = new UserLocationSpecification(['DE', 'FR', 'ES', 'IT']);
|
|
336
|
+
|
|
337
|
+
const recentEuUsers = recentUsers.and(euUsers);
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Async Specifications
|
|
341
|
+
|
|
342
|
+
For specifications that require asynchronous operations like database queries or
|
|
343
|
+
API calls:
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
import { AsyncCompositeSpecification } from '@vytches/ddd-validation';
|
|
347
|
+
|
|
348
|
+
// Database-based async specification
|
|
349
|
+
class UserExistsSpecification extends AsyncCompositeSpecification<{
|
|
350
|
+
email: string;
|
|
351
|
+
repository: IUserRepository;
|
|
352
|
+
}> {
|
|
353
|
+
async isSatisfiedByAsync(candidate: {
|
|
354
|
+
email: string;
|
|
355
|
+
repository: IUserRepository;
|
|
356
|
+
}): Promise<boolean> {
|
|
357
|
+
const existingUser = await candidate.repository.findByEmail(
|
|
358
|
+
candidate.email
|
|
359
|
+
);
|
|
360
|
+
return existingUser !== null;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// External API validation
|
|
365
|
+
class EmailBlacklistSpecification extends AsyncCompositeSpecification<string> {
|
|
366
|
+
async isSatisfiedByAsync(email: string): Promise<boolean> {
|
|
367
|
+
const response = await fetch(
|
|
368
|
+
`https://api.blacklist.com/check?email=${email}`
|
|
369
|
+
);
|
|
370
|
+
const data = await response.json();
|
|
371
|
+
return !data.isBlacklisted;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Permission check specification
|
|
376
|
+
class HasPermissionSpecification extends AsyncCompositeSpecification<{
|
|
377
|
+
user: User;
|
|
378
|
+
permission: string;
|
|
379
|
+
service: IPermissionService;
|
|
380
|
+
}> {
|
|
381
|
+
async isSatisfiedByAsync(candidate: {
|
|
382
|
+
user: User;
|
|
383
|
+
permission: string;
|
|
384
|
+
service: IPermissionService;
|
|
385
|
+
}): Promise<boolean> {
|
|
386
|
+
const permissions = await candidate.service.getUserPermissions(
|
|
387
|
+
candidate.user.id
|
|
388
|
+
);
|
|
389
|
+
return permissions.includes(candidate.permission);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Combine async specifications
|
|
394
|
+
const userExists = new UserExistsSpecification();
|
|
395
|
+
const notBlacklisted = new EmailBlacklistSpecification();
|
|
396
|
+
const hasAdminPermission = new HasPermissionSpecification();
|
|
397
|
+
|
|
398
|
+
// Logical operations work the same way
|
|
399
|
+
const isValidNewUser = userExists.not().and(notBlacklisted);
|
|
400
|
+
|
|
401
|
+
// Execute async specification
|
|
402
|
+
const isValid = await isValidNewUser.isSatisfiedByAsync({
|
|
403
|
+
email: 'user@example.com',
|
|
404
|
+
repository: userRepository,
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// Create from async predicate
|
|
408
|
+
const customCheck = AsyncCompositeSpecification.create<User>(
|
|
409
|
+
async (user, context) => {
|
|
410
|
+
// Async operation
|
|
411
|
+
const result = await someAsyncOperation(user, context);
|
|
412
|
+
return result.isValid;
|
|
413
|
+
},
|
|
414
|
+
'CustomAsyncCheck',
|
|
415
|
+
'Custom async validation check'
|
|
416
|
+
);
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### Parallel Execution
|
|
420
|
+
|
|
421
|
+
Async specifications execute combined operations in parallel for better
|
|
422
|
+
performance:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
// Both database and API calls run simultaneously
|
|
426
|
+
const complexValidation = new AndAsyncSpecification(
|
|
427
|
+
new DatabaseCheckSpecification(), // Runs in parallel
|
|
428
|
+
new ApiValidationSpecification() // Runs in parallel
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
// Result is ready when both complete
|
|
432
|
+
const isValid = await complexValidation.isSatisfiedByAsync(candidate);
|
|
433
|
+
|
|
434
|
+
// OR operations also run in parallel
|
|
435
|
+
const alternativeCheck = new OrAsyncSpecification(
|
|
436
|
+
new PrimaryValidationSpec(), // Runs in parallel
|
|
437
|
+
new FallbackValidationSpec() // Runs in parallel
|
|
438
|
+
);
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Error Explanation
|
|
442
|
+
|
|
443
|
+
Async specifications can provide detailed failure explanations:
|
|
444
|
+
|
|
445
|
+
```typescript
|
|
446
|
+
class DetailedAsyncSpec extends AsyncCompositeSpecification<User> {
|
|
447
|
+
async isSatisfiedByAsync(user: User): Promise<boolean> {
|
|
448
|
+
const result = await this.validateUser(user);
|
|
449
|
+
return result.isValid;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async explainFailureAsync(user: User): Promise<string | null> {
|
|
453
|
+
const result = await this.validateUser(user);
|
|
454
|
+
if (!result.isValid) {
|
|
455
|
+
return `User validation failed: ${result.reason}`;
|
|
456
|
+
}
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
private async validateUser(user: User) {
|
|
461
|
+
// Complex async validation logic
|
|
462
|
+
return { isValid: false, reason: 'Missing required permissions' };
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Get detailed error explanation
|
|
467
|
+
const spec = new DetailedAsyncSpec();
|
|
468
|
+
const isValid = await spec.isSatisfiedByAsync(user);
|
|
469
|
+
if (!isValid) {
|
|
470
|
+
const explanation = await spec.explainFailureAsync(user);
|
|
471
|
+
console.log(explanation); // "User validation failed: Missing required permissions"
|
|
472
|
+
}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## 📏 Fluent Rules
|
|
476
|
+
|
|
477
|
+
### String Rules
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
import { Rules } from '@vytches/ddd-validation';
|
|
481
|
+
|
|
482
|
+
// String validation rules
|
|
483
|
+
const nameRule = Rules.forString()
|
|
484
|
+
.required()
|
|
485
|
+
.minLength(2)
|
|
486
|
+
.maxLength(50)
|
|
487
|
+
.pattern(/^[A-Za-z\s]+$/)
|
|
488
|
+
.withMessage('Name must contain only letters and spaces')
|
|
489
|
+
.build();
|
|
490
|
+
|
|
491
|
+
const emailRule = Rules.forString()
|
|
492
|
+
.required()
|
|
493
|
+
.email()
|
|
494
|
+
.withMessage('Please provide a valid email address')
|
|
495
|
+
.build();
|
|
496
|
+
|
|
497
|
+
const phoneRule = Rules.forString()
|
|
498
|
+
.optional()
|
|
499
|
+
.phone()
|
|
500
|
+
.withMessage('Please provide a valid phone number')
|
|
501
|
+
.build();
|
|
502
|
+
|
|
503
|
+
const passwordRule = Rules.forString()
|
|
504
|
+
.required()
|
|
505
|
+
.minLength(8)
|
|
506
|
+
.pattern(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/)
|
|
507
|
+
.withMessage(
|
|
508
|
+
'Password must contain uppercase, lowercase, number and special character'
|
|
509
|
+
)
|
|
510
|
+
.build();
|
|
511
|
+
|
|
512
|
+
// Validate strings
|
|
513
|
+
const nameValidation = nameRule.validate('John Doe');
|
|
514
|
+
const emailValidation = emailRule.validate('john@example.com');
|
|
515
|
+
const phoneValidation = phoneRule.validate('+1-555-123-4567');
|
|
516
|
+
const passwordValidation = passwordRule.validate('SecurePass123!');
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### Number Rules
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
// Number validation rules
|
|
523
|
+
const ageRule = Rules.forNumber()
|
|
524
|
+
.required()
|
|
525
|
+
.min(18)
|
|
526
|
+
.max(120)
|
|
527
|
+
.withMessage('Age must be between 18 and 120')
|
|
528
|
+
.build();
|
|
529
|
+
|
|
530
|
+
const priceRule = Rules.forNumber()
|
|
531
|
+
.required()
|
|
532
|
+
.min(0)
|
|
533
|
+
.precision(2)
|
|
534
|
+
.withMessage('Price must be a positive number with up to 2 decimal places')
|
|
535
|
+
.build();
|
|
536
|
+
|
|
537
|
+
const scoreRule = Rules.forNumber()
|
|
538
|
+
.required()
|
|
539
|
+
.min(0)
|
|
540
|
+
.max(100)
|
|
541
|
+
.integer()
|
|
542
|
+
.withMessage('Score must be an integer between 0 and 100')
|
|
543
|
+
.build();
|
|
544
|
+
|
|
545
|
+
// Validate numbers
|
|
546
|
+
const ageValidation = ageRule.validate(25);
|
|
547
|
+
const priceValidation = priceRule.validate(99.99);
|
|
548
|
+
const scoreValidation = scoreRule.validate(85);
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
### Date Rules
|
|
552
|
+
|
|
553
|
+
```typescript
|
|
554
|
+
// Date validation rules
|
|
555
|
+
const birthDateRule = Rules.forDate()
|
|
556
|
+
.required()
|
|
557
|
+
.before(new Date()) // Must be in the past
|
|
558
|
+
.after(new Date('1900-01-01')) // Not too far in the past
|
|
559
|
+
.withMessage('Birth date must be between 1900 and today')
|
|
560
|
+
.build();
|
|
561
|
+
|
|
562
|
+
const appointmentRule = Rules.forDate()
|
|
563
|
+
.required()
|
|
564
|
+
.after(new Date()) // Must be in the future
|
|
565
|
+
.within(30, 'days') // Within 30 days
|
|
566
|
+
.withMessage('Appointment must be scheduled within the next 30 days')
|
|
567
|
+
.build();
|
|
568
|
+
|
|
569
|
+
// Validate dates
|
|
570
|
+
const birthDateValidation = birthDateRule.validate(new Date('1990-05-15'));
|
|
571
|
+
const appointmentValidation = appointmentRule.validate(new Date('2024-02-15'));
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
### Array Rules
|
|
575
|
+
|
|
576
|
+
```typescript
|
|
577
|
+
// Array validation rules
|
|
578
|
+
const tagsRule = Rules.forArray<string>()
|
|
579
|
+
.required()
|
|
580
|
+
.minLength(1)
|
|
581
|
+
.maxLength(10)
|
|
582
|
+
.eachItem(Rules.forString().required().minLength(2))
|
|
583
|
+
.withMessage('Tags must contain 1-10 items, each at least 2 characters')
|
|
584
|
+
.build();
|
|
585
|
+
|
|
586
|
+
const scoresRule = Rules.forArray<number>()
|
|
587
|
+
.optional()
|
|
588
|
+
.minLength(0)
|
|
589
|
+
.maxLength(100)
|
|
590
|
+
.eachItem(Rules.forNumber().min(0).max(100))
|
|
591
|
+
.withMessage('Scores must be between 0 and 100')
|
|
592
|
+
.build();
|
|
593
|
+
|
|
594
|
+
// Validate arrays
|
|
595
|
+
const tagsValidation = tagsRule.validate([
|
|
596
|
+
'technology',
|
|
597
|
+
'programming',
|
|
598
|
+
'typescript',
|
|
599
|
+
]);
|
|
600
|
+
const scoresValidation = scoresRule.validate([85, 92, 78, 90]);
|
|
601
|
+
```
|
|
602
|
+
|
|
603
|
+
### Object Rules
|
|
604
|
+
|
|
605
|
+
```typescript
|
|
606
|
+
// Object validation rules
|
|
607
|
+
const addressRule = Rules.forObject<Address>()
|
|
608
|
+
.required()
|
|
609
|
+
.property('street', Rules.forString().required().minLength(5))
|
|
610
|
+
.property('city', Rules.forString().required().minLength(2))
|
|
611
|
+
.property(
|
|
612
|
+
'zipCode',
|
|
613
|
+
Rules.forString()
|
|
614
|
+
.required()
|
|
615
|
+
.pattern(/^\d{5}(-\d{4})?$/)
|
|
616
|
+
)
|
|
617
|
+
.property('country', Rules.forString().required().minLength(2))
|
|
618
|
+
.build();
|
|
619
|
+
|
|
620
|
+
const userRule = Rules.forObject<User>()
|
|
621
|
+
.required()
|
|
622
|
+
.property('name', Rules.forString().required().minLength(2))
|
|
623
|
+
.property('email', Rules.forString().required().email())
|
|
624
|
+
.property('age', Rules.forNumber().required().min(18))
|
|
625
|
+
.property('address', addressRule)
|
|
626
|
+
.build();
|
|
627
|
+
|
|
628
|
+
// Validate objects
|
|
629
|
+
const address = new Address('123 Main St', 'New York', '10001', 'USA');
|
|
630
|
+
const user = new User('John Doe', 'john@example.com', 25, address);
|
|
631
|
+
const userValidation = userRule.validate(user);
|
|
632
|
+
```
|
|
633
|
+
|
|
634
|
+
## 🏗️ Domain Validators
|
|
635
|
+
|
|
636
|
+
### Entity Validators
|
|
637
|
+
|
|
638
|
+
```typescript
|
|
639
|
+
import { DomainValidator } from '@vytches/ddd-validation';
|
|
640
|
+
|
|
641
|
+
// User entity validator
|
|
642
|
+
const userValidator = DomainValidator.create<User>()
|
|
643
|
+
.forProperty('id', Rules.forString().required().uuid())
|
|
644
|
+
.forProperty('name', Rules.forString().required().minLength(2).maxLength(50))
|
|
645
|
+
.forProperty('email', Rules.forString().required().email())
|
|
646
|
+
.forProperty('age', Rules.forNumber().required().min(18).max(120))
|
|
647
|
+
.forProperty('role', Rules.forEnum(UserRole).required())
|
|
648
|
+
.forProperty('createdAt', Rules.forDate().required().before(new Date()))
|
|
649
|
+
.forProperty(
|
|
650
|
+
'updatedAt',
|
|
651
|
+
Rules.forDate()
|
|
652
|
+
.required()
|
|
653
|
+
.after(entity => entity.createdAt)
|
|
654
|
+
)
|
|
655
|
+
.build();
|
|
656
|
+
|
|
657
|
+
// Order entity validator
|
|
658
|
+
const orderValidator = DomainValidator.create<Order>()
|
|
659
|
+
.forProperty('id', Rules.forString().required().uuid())
|
|
660
|
+
.forProperty('customerId', Rules.forString().required().uuid())
|
|
661
|
+
.forProperty('items', Rules.forArray().required().minLength(1))
|
|
662
|
+
.forProperty('status', Rules.forEnum(OrderStatus).required())
|
|
663
|
+
.forProperty('totalAmount', Rules.forNumber().required().min(0))
|
|
664
|
+
.forProperty('currency', Rules.forString().required().length(3))
|
|
665
|
+
.build();
|
|
666
|
+
|
|
667
|
+
// Validate entities
|
|
668
|
+
const userValidation = await userValidator.validate(user);
|
|
669
|
+
const orderValidation = await orderValidator.validate(order);
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
### Value Object Validators
|
|
673
|
+
|
|
674
|
+
```typescript
|
|
675
|
+
// Email value object validator
|
|
676
|
+
const emailValidator = DomainValidator.create<Email>()
|
|
677
|
+
.forProperty('value', Rules.forString().required().email())
|
|
678
|
+
.forProperty('domain', Rules.forString().required())
|
|
679
|
+
.forProperty('localPart', Rules.forString().required())
|
|
680
|
+
.build();
|
|
681
|
+
|
|
682
|
+
// Money value object validator
|
|
683
|
+
const moneyValidator = DomainValidator.create<Money>()
|
|
684
|
+
.forProperty('amount', Rules.forNumber().required().min(0))
|
|
685
|
+
.forProperty('currency', Rules.forString().required().length(3))
|
|
686
|
+
.build();
|
|
687
|
+
|
|
688
|
+
// Address value object validator
|
|
689
|
+
const addressValidator = DomainValidator.create<Address>()
|
|
690
|
+
.forProperty('street', Rules.forString().required().minLength(5))
|
|
691
|
+
.forProperty('city', Rules.forString().required().minLength(2))
|
|
692
|
+
.forProperty('state', Rules.forString().required().length(2))
|
|
693
|
+
.forProperty(
|
|
694
|
+
'zipCode',
|
|
695
|
+
Rules.forString()
|
|
696
|
+
.required()
|
|
697
|
+
.pattern(/^\d{5}(-\d{4})?$/)
|
|
698
|
+
)
|
|
699
|
+
.forProperty('country', Rules.forString().required().length(2))
|
|
700
|
+
.build();
|
|
701
|
+
|
|
702
|
+
// Validate value objects
|
|
703
|
+
const emailValidation = await emailValidator.validate(email);
|
|
704
|
+
const moneyValidation = await moneyValidator.validate(money);
|
|
705
|
+
const addressValidation = await addressValidator.validate(address);
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
### Aggregate Validators
|
|
709
|
+
|
|
710
|
+
```typescript
|
|
711
|
+
// User aggregate validator
|
|
712
|
+
const userAggregateValidator = DomainValidator.create<UserAggregate>()
|
|
713
|
+
.forProperty('id', Rules.forString().required().uuid())
|
|
714
|
+
.forProperty('version', Rules.forNumber().required().min(0))
|
|
715
|
+
.forProperty('profile', userValidator)
|
|
716
|
+
.forProperty('preferences', Rules.forObject().optional())
|
|
717
|
+
.forProperty('subscription', Rules.forObject().optional())
|
|
718
|
+
.build();
|
|
719
|
+
|
|
720
|
+
// Order aggregate validator
|
|
721
|
+
const orderAggregateValidator = DomainValidator.create<OrderAggregate>()
|
|
722
|
+
.forProperty('id', Rules.forString().required().uuid())
|
|
723
|
+
.forProperty('version', Rules.forNumber().required().min(0))
|
|
724
|
+
.forProperty('customerId', Rules.forString().required().uuid())
|
|
725
|
+
.forProperty('items', Rules.forArray().required().minLength(1))
|
|
726
|
+
.forProperty('status', Rules.forEnum(OrderStatus).required())
|
|
727
|
+
.forProperty('paymentInfo', Rules.forObject().optional())
|
|
728
|
+
.forProperty('shippingAddress', addressValidator)
|
|
729
|
+
.forProperty('billingAddress', addressValidator)
|
|
730
|
+
.build();
|
|
731
|
+
|
|
732
|
+
// Validate aggregates
|
|
733
|
+
const userAggregateValidation =
|
|
734
|
+
await userAggregateValidator.validate(userAggregate);
|
|
735
|
+
const orderAggregateValidation =
|
|
736
|
+
await orderAggregateValidator.validate(orderAggregate);
|
|
737
|
+
```
|
|
738
|
+
|
|
739
|
+
## ⚡ Async Validation
|
|
740
|
+
|
|
741
|
+
### Async Specifications
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
import { AsyncSpecification } from '@vytches/ddd-validation';
|
|
745
|
+
|
|
746
|
+
// Email uniqueness specification
|
|
747
|
+
class EmailUniquenessSpecification extends AsyncSpecification<User> {
|
|
748
|
+
constructor(private readonly userRepository: IUserRepository) {
|
|
749
|
+
super();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
async isSatisfiedByAsync(user: User): Promise<boolean> {
|
|
753
|
+
const existingUser = await this.userRepository.findByEmail(user.email);
|
|
754
|
+
return existingUser === null || existingUser.id === user.id;
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// External validation specification
|
|
759
|
+
class ExternalValidationSpecification extends AsyncSpecification<User> {
|
|
760
|
+
constructor(private readonly externalService: IExternalValidationService) {
|
|
761
|
+
super();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
async isSatisfiedByAsync(user: User): Promise<boolean> {
|
|
765
|
+
const result = await this.externalService.validateUser(user);
|
|
766
|
+
return result.isValid;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Compose async specifications
|
|
771
|
+
const asyncUserValidation = new EmailUniquenessSpecification(
|
|
772
|
+
userRepository
|
|
773
|
+
).andAsync(new ExternalValidationSpecification(externalService));
|
|
774
|
+
|
|
775
|
+
// Validate asynchronously
|
|
776
|
+
const user = new User('John Doe', 'john@example.com', 25);
|
|
777
|
+
const isValid = await asyncUserValidation.isSatisfiedByAsync(user);
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
### Async Rules
|
|
781
|
+
|
|
782
|
+
```typescript
|
|
783
|
+
// Async validation rules
|
|
784
|
+
const uniqueEmailRule = Rules.forString()
|
|
785
|
+
.required()
|
|
786
|
+
.email()
|
|
787
|
+
.asyncCustom(async (email: string) => {
|
|
788
|
+
const existingUser = await userRepository.findByEmail(email);
|
|
789
|
+
return existingUser === null;
|
|
790
|
+
})
|
|
791
|
+
.withMessage('Email address already exists')
|
|
792
|
+
.build();
|
|
793
|
+
|
|
794
|
+
const validDomainRule = Rules.forString()
|
|
795
|
+
.required()
|
|
796
|
+
.asyncCustom(async (domain: string) => {
|
|
797
|
+
const isValid = await domainValidationService.validateDomain(domain);
|
|
798
|
+
return isValid;
|
|
799
|
+
})
|
|
800
|
+
.withMessage('Domain is not valid')
|
|
801
|
+
.build();
|
|
802
|
+
|
|
803
|
+
// Async validation
|
|
804
|
+
const emailValidation = await uniqueEmailRule.validateAsync('john@example.com');
|
|
805
|
+
const domainValidation = await validDomainRule.validateAsync('example.com');
|
|
806
|
+
```
|
|
807
|
+
|
|
808
|
+
### Async Domain Validators
|
|
809
|
+
|
|
810
|
+
```typescript
|
|
811
|
+
// Async domain validator
|
|
812
|
+
const asyncUserValidator = DomainValidator.create<User>()
|
|
813
|
+
.forProperty('name', Rules.forString().required().minLength(2))
|
|
814
|
+
.forProperty('email', uniqueEmailRule)
|
|
815
|
+
.forProperty(
|
|
816
|
+
'username',
|
|
817
|
+
Rules.forString()
|
|
818
|
+
.required()
|
|
819
|
+
.asyncCustom(async (username: string) => {
|
|
820
|
+
const exists = await userRepository.existsByUsername(username);
|
|
821
|
+
return !exists;
|
|
822
|
+
})
|
|
823
|
+
)
|
|
824
|
+
.build();
|
|
825
|
+
|
|
826
|
+
// Async validation
|
|
827
|
+
const user = new User('John Doe', 'john@example.com', 25);
|
|
828
|
+
const validation = await asyncUserValidator.validateAsync(user);
|
|
829
|
+
|
|
830
|
+
if (!validation.isValid) {
|
|
831
|
+
console.log('Async validation errors:');
|
|
832
|
+
validation.errors.forEach(error => {
|
|
833
|
+
console.log(`${error.field}: ${error.message}`);
|
|
834
|
+
});
|
|
835
|
+
}
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
## 🔧 Validation Context
|
|
839
|
+
|
|
840
|
+
### Context Creation
|
|
841
|
+
|
|
842
|
+
```typescript
|
|
843
|
+
import { ValidationContext } from '@vytches/ddd-validation';
|
|
844
|
+
|
|
845
|
+
// Create validation context
|
|
846
|
+
const context = ValidationContext.create()
|
|
847
|
+
.withUserId('user-123')
|
|
848
|
+
.withRequestId('req-456')
|
|
849
|
+
.withTenantId('tenant-789')
|
|
850
|
+
.withEnvironment('production')
|
|
851
|
+
.withMetadata({
|
|
852
|
+
feature: 'user-registration',
|
|
853
|
+
version: '1.2.0',
|
|
854
|
+
})
|
|
855
|
+
.build();
|
|
856
|
+
|
|
857
|
+
// Context-aware validation
|
|
858
|
+
const contextualValidator = DomainValidator.create<User>()
|
|
859
|
+
.forProperty('name', Rules.forString().required().minLength(2))
|
|
860
|
+
.forProperty('email', Rules.forString().required().email())
|
|
861
|
+
.forProperty('role', Rules.forEnum(UserRole).required())
|
|
862
|
+
.withContext(context)
|
|
863
|
+
.build();
|
|
864
|
+
|
|
865
|
+
// Validate with context
|
|
866
|
+
const validation = await contextualValidator.validate(user);
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
### Context-Dependent Rules
|
|
870
|
+
|
|
871
|
+
```typescript
|
|
872
|
+
// Rules that depend on context
|
|
873
|
+
const contextualAgeRule = Rules.forNumber()
|
|
874
|
+
.required()
|
|
875
|
+
.min(context => (context.metadata.feature === 'admin-registration' ? 21 : 18))
|
|
876
|
+
.withMessage('Age requirement depends on registration type')
|
|
877
|
+
.build();
|
|
878
|
+
|
|
879
|
+
const environmentalRule = Rules.forString()
|
|
880
|
+
.required()
|
|
881
|
+
.when(context => context.environment === 'production')
|
|
882
|
+
.then(Rules.forString().pattern(/^[A-Z0-9]+$/))
|
|
883
|
+
.otherwise(Rules.forString().minLength(1))
|
|
884
|
+
.build();
|
|
885
|
+
|
|
886
|
+
// Tenant-specific rules
|
|
887
|
+
const tenantRule = Rules.forString()
|
|
888
|
+
.required()
|
|
889
|
+
.asyncCustom(async (value: string, context: ValidationContext) => {
|
|
890
|
+
const tenantConfig = await configService.getTenantConfig(context.tenantId);
|
|
891
|
+
return tenantConfig.allowedValues.includes(value);
|
|
892
|
+
})
|
|
893
|
+
.build();
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
## 🚨 Error Handling
|
|
897
|
+
|
|
898
|
+
### Validation Error Types
|
|
899
|
+
|
|
900
|
+
```typescript
|
|
901
|
+
// Validation error hierarchy
|
|
902
|
+
interface ValidationError {
|
|
903
|
+
field: string;
|
|
904
|
+
code: string;
|
|
905
|
+
message: string;
|
|
906
|
+
severity: 'ERROR' | 'WARNING' | 'INFO';
|
|
907
|
+
details?: Record<string, any>;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
interface ValidationResult {
|
|
911
|
+
isValid: boolean;
|
|
912
|
+
errors: ValidationError[];
|
|
913
|
+
warnings: ValidationError[];
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
// Specific error types
|
|
917
|
+
class RequiredFieldError extends ValidationError {
|
|
918
|
+
constructor(field: string) {
|
|
919
|
+
super({
|
|
920
|
+
field,
|
|
921
|
+
code: 'REQUIRED_FIELD',
|
|
922
|
+
message: `${field} is required`,
|
|
923
|
+
severity: 'ERROR',
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
class InvalidFormatError extends ValidationError {
|
|
929
|
+
constructor(field: string, expectedFormat: string) {
|
|
930
|
+
super({
|
|
931
|
+
field,
|
|
932
|
+
code: 'INVALID_FORMAT',
|
|
933
|
+
message: `${field} must be in ${expectedFormat} format`,
|
|
934
|
+
severity: 'ERROR',
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
```
|
|
939
|
+
|
|
940
|
+
### Error Handling Patterns
|
|
941
|
+
|
|
942
|
+
```typescript
|
|
943
|
+
// Comprehensive error handling
|
|
944
|
+
const userValidator = DomainValidator.create<User>()
|
|
945
|
+
.forProperty('name', Rules.forString().required().minLength(2))
|
|
946
|
+
.forProperty('email', Rules.forString().required().email())
|
|
947
|
+
.forProperty('age', Rules.forNumber().required().min(18))
|
|
948
|
+
.build();
|
|
949
|
+
|
|
950
|
+
// Validate and handle errors
|
|
951
|
+
const validation = await userValidator.validate(user);
|
|
952
|
+
|
|
953
|
+
if (!validation.isValid) {
|
|
954
|
+
// Group errors by field
|
|
955
|
+
const errorsByField = validation.errors.reduce(
|
|
956
|
+
(acc, error) => {
|
|
957
|
+
if (!acc[error.field]) {
|
|
958
|
+
acc[error.field] = [];
|
|
959
|
+
}
|
|
960
|
+
acc[error.field].push(error);
|
|
961
|
+
return acc;
|
|
962
|
+
},
|
|
963
|
+
{} as Record<string, ValidationError[]>
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
// Handle each field's errors
|
|
967
|
+
Object.entries(errorsByField).forEach(([field, errors]) => {
|
|
968
|
+
console.log(`Errors for ${field}:`);
|
|
969
|
+
errors.forEach(error => {
|
|
970
|
+
console.log(` - ${error.message} (${error.code})`);
|
|
971
|
+
});
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
// Handle by severity
|
|
975
|
+
const criticalErrors = validation.errors.filter(e => e.severity === 'ERROR');
|
|
976
|
+
const warnings = validation.errors.filter(e => e.severity === 'WARNING');
|
|
977
|
+
|
|
978
|
+
if (criticalErrors.length > 0) {
|
|
979
|
+
throw new ValidationException('Critical validation errors', criticalErrors);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (warnings.length > 0) {
|
|
983
|
+
console.warn('Validation warnings:', warnings);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
### Custom Error Messages
|
|
989
|
+
|
|
990
|
+
```typescript
|
|
991
|
+
// Custom error messages
|
|
992
|
+
const customMessageValidator = DomainValidator.create<User>()
|
|
993
|
+
.forProperty(
|
|
994
|
+
'name',
|
|
995
|
+
Rules.forString()
|
|
996
|
+
.required()
|
|
997
|
+
.withMessage('Full name is required')
|
|
998
|
+
.minLength(2)
|
|
999
|
+
.withMessage('Name must be at least 2 characters')
|
|
1000
|
+
)
|
|
1001
|
+
.forProperty(
|
|
1002
|
+
'email',
|
|
1003
|
+
Rules.forString()
|
|
1004
|
+
.required()
|
|
1005
|
+
.withMessage('Email address is required')
|
|
1006
|
+
.email()
|
|
1007
|
+
.withMessage('Please provide a valid email address')
|
|
1008
|
+
)
|
|
1009
|
+
.forProperty(
|
|
1010
|
+
'age',
|
|
1011
|
+
Rules.forNumber()
|
|
1012
|
+
.required()
|
|
1013
|
+
.withMessage('Age is required')
|
|
1014
|
+
.min(18)
|
|
1015
|
+
.withMessage('You must be at least 18 years old')
|
|
1016
|
+
)
|
|
1017
|
+
.build();
|
|
1018
|
+
|
|
1019
|
+
// Localized error messages
|
|
1020
|
+
const localizedValidator = DomainValidator.create<User>()
|
|
1021
|
+
.forProperty(
|
|
1022
|
+
'name',
|
|
1023
|
+
Rules.forString()
|
|
1024
|
+
.required()
|
|
1025
|
+
.withMessage('validation.name.required')
|
|
1026
|
+
.minLength(2)
|
|
1027
|
+
.withMessage('validation.name.min_length')
|
|
1028
|
+
)
|
|
1029
|
+
.withLocalizationService(localizationService)
|
|
1030
|
+
.build();
|
|
1031
|
+
```
|
|
1032
|
+
|
|
1033
|
+
## 🛠️ Custom Validators
|
|
1034
|
+
|
|
1035
|
+
### Custom Specification
|
|
1036
|
+
|
|
1037
|
+
```typescript
|
|
1038
|
+
// Custom domain-specific specification
|
|
1039
|
+
class ValidBusinessEmailSpecification extends Specification<User> {
|
|
1040
|
+
constructor(private readonly businessDomains: string[]) {
|
|
1041
|
+
super();
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
isSatisfiedBy(user: User): boolean {
|
|
1045
|
+
const domain = user.email.split('@')[1];
|
|
1046
|
+
return this.businessDomains.includes(domain);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Custom credit score specification
|
|
1051
|
+
class CreditScoreSpecification extends Specification<LoanApplication> {
|
|
1052
|
+
constructor(private readonly minimumScore: number) {
|
|
1053
|
+
super();
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
isSatisfiedBy(application: LoanApplication): boolean {
|
|
1057
|
+
return application.creditScore >= this.minimumScore;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// Usage
|
|
1062
|
+
const businessDomains = ['company.com', 'enterprise.org', 'business.net'];
|
|
1063
|
+
const businessEmailSpec = new ValidBusinessEmailSpecification(businessDomains);
|
|
1064
|
+
const creditSpec = new CreditScoreSpecification(650);
|
|
1065
|
+
|
|
1066
|
+
const businessUserValidation = businessEmailSpec.and(
|
|
1067
|
+
new UserAgeSpecification(21)
|
|
1068
|
+
);
|
|
1069
|
+
|
|
1070
|
+
const loanValidation = creditSpec.and(
|
|
1071
|
+
new IncomeVerificationSpecification(50000)
|
|
1072
|
+
);
|
|
1073
|
+
```
|
|
1074
|
+
|
|
1075
|
+
### Custom Rules
|
|
1076
|
+
|
|
1077
|
+
```typescript
|
|
1078
|
+
// Custom validation rule
|
|
1079
|
+
class CustomRule<T> implements IValidationRule<T> {
|
|
1080
|
+
constructor(
|
|
1081
|
+
private readonly validator: (value: T) => boolean,
|
|
1082
|
+
private readonly errorMessage: string,
|
|
1083
|
+
private readonly errorCode: string
|
|
1084
|
+
) {}
|
|
1085
|
+
|
|
1086
|
+
validate(value: T): ValidationResult {
|
|
1087
|
+
const isValid = this.validator(value);
|
|
1088
|
+
|
|
1089
|
+
if (!isValid) {
|
|
1090
|
+
return {
|
|
1091
|
+
isValid: false,
|
|
1092
|
+
errors: [
|
|
1093
|
+
{
|
|
1094
|
+
field: '',
|
|
1095
|
+
code: this.errorCode,
|
|
1096
|
+
message: this.errorMessage,
|
|
1097
|
+
severity: 'ERROR',
|
|
1098
|
+
},
|
|
1099
|
+
],
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
return { isValid: true, errors: [] };
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
// Custom rule factory
|
|
1108
|
+
const customRuleFactory = {
|
|
1109
|
+
palindrome: () =>
|
|
1110
|
+
new CustomRule(
|
|
1111
|
+
(value: string) => value === value.split('').reverse().join(''),
|
|
1112
|
+
'Value must be a palindrome',
|
|
1113
|
+
'NOT_PALINDROME'
|
|
1114
|
+
),
|
|
1115
|
+
|
|
1116
|
+
divisibleBy: (divisor: number) =>
|
|
1117
|
+
new CustomRule(
|
|
1118
|
+
(value: number) => value % divisor === 0,
|
|
1119
|
+
`Value must be divisible by ${divisor}`,
|
|
1120
|
+
'NOT_DIVISIBLE'
|
|
1121
|
+
),
|
|
1122
|
+
|
|
1123
|
+
uniqueArray: () =>
|
|
1124
|
+
new CustomRule(
|
|
1125
|
+
(value: any[]) => value.length === new Set(value).size,
|
|
1126
|
+
'Array must contain unique values',
|
|
1127
|
+
'NOT_UNIQUE'
|
|
1128
|
+
),
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
// Usage
|
|
1132
|
+
const palindromeRule = customRuleFactory.palindrome();
|
|
1133
|
+
const divisibleByFiveRule = customRuleFactory.divisibleBy(5);
|
|
1134
|
+
const uniqueArrayRule = customRuleFactory.uniqueArray();
|
|
1135
|
+
```
|
|
1136
|
+
|
|
1137
|
+
### Custom Async Validators
|
|
1138
|
+
|
|
1139
|
+
```typescript
|
|
1140
|
+
// Custom async validator
|
|
1141
|
+
class AsyncCustomValidator<T> implements IAsyncValidationRule<T> {
|
|
1142
|
+
constructor(
|
|
1143
|
+
private readonly asyncValidator: (value: T) => Promise<boolean>,
|
|
1144
|
+
private readonly errorMessage: string,
|
|
1145
|
+
private readonly errorCode: string
|
|
1146
|
+
) {}
|
|
1147
|
+
|
|
1148
|
+
async validateAsync(value: T): Promise<ValidationResult> {
|
|
1149
|
+
const isValid = await this.asyncValidator(value);
|
|
1150
|
+
|
|
1151
|
+
if (!isValid) {
|
|
1152
|
+
return {
|
|
1153
|
+
isValid: false,
|
|
1154
|
+
errors: [
|
|
1155
|
+
{
|
|
1156
|
+
field: '',
|
|
1157
|
+
code: this.errorCode,
|
|
1158
|
+
message: this.errorMessage,
|
|
1159
|
+
severity: 'ERROR',
|
|
1160
|
+
},
|
|
1161
|
+
],
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
return { isValid: true, errors: [] };
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
// Custom async rule factory
|
|
1170
|
+
const asyncRuleFactory = {
|
|
1171
|
+
uniqueInDatabase: (repository: IRepository, field: string) =>
|
|
1172
|
+
new AsyncCustomValidator(
|
|
1173
|
+
async (value: any) => {
|
|
1174
|
+
const existing = await repository.findByField(field, value);
|
|
1175
|
+
return existing === null;
|
|
1176
|
+
},
|
|
1177
|
+
`${field} already exists`,
|
|
1178
|
+
'NOT_UNIQUE_IN_DATABASE'
|
|
1179
|
+
),
|
|
1180
|
+
|
|
1181
|
+
validWithExternalService: (service: IExternalValidationService) =>
|
|
1182
|
+
new AsyncCustomValidator(
|
|
1183
|
+
async (value: any) => {
|
|
1184
|
+
const result = await service.validate(value);
|
|
1185
|
+
return result.isValid;
|
|
1186
|
+
},
|
|
1187
|
+
'External validation failed',
|
|
1188
|
+
'EXTERNAL_VALIDATION_FAILED'
|
|
1189
|
+
),
|
|
1190
|
+
};
|
|
1191
|
+
```
|
|
1192
|
+
|
|
1193
|
+
## 🔗 Integration Patterns
|
|
1194
|
+
|
|
1195
|
+
### CQRS Integration
|
|
1196
|
+
|
|
1197
|
+
```typescript
|
|
1198
|
+
// Command validation
|
|
1199
|
+
@CommandHandler(CreateUserCommand)
|
|
1200
|
+
export class CreateUserCommandHandler {
|
|
1201
|
+
constructor(
|
|
1202
|
+
private readonly userRepository: IUserRepository,
|
|
1203
|
+
private readonly userValidator: DomainValidator<User>
|
|
1204
|
+
) {}
|
|
1205
|
+
|
|
1206
|
+
async handle(
|
|
1207
|
+
command: CreateUserCommand,
|
|
1208
|
+
context: ExecutionContext
|
|
1209
|
+
): Promise<void> {
|
|
1210
|
+
// Create user from command
|
|
1211
|
+
const user = User.create(command.name, command.email, command.age);
|
|
1212
|
+
|
|
1213
|
+
// Validate user
|
|
1214
|
+
const validation = await this.userValidator.validate(user);
|
|
1215
|
+
|
|
1216
|
+
if (!validation.isValid) {
|
|
1217
|
+
throw new ValidationException(
|
|
1218
|
+
'User validation failed',
|
|
1219
|
+
validation.errors
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Save valid user
|
|
1224
|
+
await this.userRepository.save(user);
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// Query validation
|
|
1229
|
+
@QueryHandler(GetUsersByAgeRangeQuery)
|
|
1230
|
+
export class GetUsersByAgeRangeQueryHandler {
|
|
1231
|
+
constructor(
|
|
1232
|
+
private readonly userRepository: IUserRepository,
|
|
1233
|
+
private readonly queryValidator: DomainValidator<GetUsersByAgeRangeQuery>
|
|
1234
|
+
) {}
|
|
1235
|
+
|
|
1236
|
+
async handle(query: GetUsersByAgeRangeQuery): Promise<User[]> {
|
|
1237
|
+
// Validate query
|
|
1238
|
+
const validation = await this.queryValidator.validate(query);
|
|
1239
|
+
|
|
1240
|
+
if (!validation.isValid) {
|
|
1241
|
+
throw new ValidationException(
|
|
1242
|
+
'Query validation failed',
|
|
1243
|
+
validation.errors
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
// Execute valid query
|
|
1248
|
+
return await this.userRepository.findByAgeRange(query.minAge, query.maxAge);
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
```
|
|
1252
|
+
|
|
1253
|
+
### Policy Integration
|
|
1254
|
+
|
|
1255
|
+
```typescript
|
|
1256
|
+
// Validation with policies
|
|
1257
|
+
import { PolicyBuilder } from '@vytches/ddd-policies';
|
|
1258
|
+
|
|
1259
|
+
// Create policy with validation
|
|
1260
|
+
const userCreationPolicy = PolicyBuilder.create<User>()
|
|
1261
|
+
.withId('user-creation-policy')
|
|
1262
|
+
.withDomain('user-management')
|
|
1263
|
+
.must(new UserAgeSpecification(18))
|
|
1264
|
+
.and()
|
|
1265
|
+
.must(new UserEmailSpecification())
|
|
1266
|
+
.and()
|
|
1267
|
+
.must(
|
|
1268
|
+
new AsyncSpecification(async (user: User) => {
|
|
1269
|
+
const validation = await userValidator.validate(user);
|
|
1270
|
+
return validation.isValid;
|
|
1271
|
+
})
|
|
1272
|
+
)
|
|
1273
|
+
.build();
|
|
1274
|
+
|
|
1275
|
+
// Use in service
|
|
1276
|
+
class UserService {
|
|
1277
|
+
async createUser(userData: CreateUserData): Promise<User> {
|
|
1278
|
+
const user = User.create(userData.name, userData.email, userData.age);
|
|
1279
|
+
|
|
1280
|
+
// Validate with policy
|
|
1281
|
+
const policyResult = await userCreationPolicy.check({
|
|
1282
|
+
entity: user,
|
|
1283
|
+
context,
|
|
1284
|
+
});
|
|
1285
|
+
|
|
1286
|
+
if (policyResult.isFailure()) {
|
|
1287
|
+
throw new PolicyViolationError(
|
|
1288
|
+
'User creation policy failed',
|
|
1289
|
+
policyResult.violations
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
return await this.userRepository.save(user);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
```
|
|
1297
|
+
|
|
1298
|
+
### Repository Integration
|
|
1299
|
+
|
|
1300
|
+
```typescript
|
|
1301
|
+
// Repository with validation
|
|
1302
|
+
class ValidatingUserRepository extends BaseRepository<User> {
|
|
1303
|
+
constructor(
|
|
1304
|
+
eventBus: IEventBus,
|
|
1305
|
+
storageAdapter: IStorageAdapter<User>,
|
|
1306
|
+
private readonly userValidator: DomainValidator<User>
|
|
1307
|
+
) {
|
|
1308
|
+
super(eventBus, storageAdapter);
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
async save(user: User): Promise<void> {
|
|
1312
|
+
// Validate before saving
|
|
1313
|
+
const validation = await this.userValidator.validate(user);
|
|
1314
|
+
|
|
1315
|
+
if (!validation.isValid) {
|
|
1316
|
+
throw new ValidationException(
|
|
1317
|
+
'Cannot save invalid user',
|
|
1318
|
+
validation.errors
|
|
1319
|
+
);
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// Save valid user
|
|
1323
|
+
await super.save(user);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
```
|
|
1327
|
+
|
|
1328
|
+
## 🧪 Testing
|
|
1329
|
+
|
|
1330
|
+
### Specification Testing
|
|
1331
|
+
|
|
1332
|
+
```typescript
|
|
1333
|
+
import { describe, it, expect } from 'vitest';
|
|
1334
|
+
|
|
1335
|
+
describe('UserAgeSpecification', () => {
|
|
1336
|
+
it('should pass for users of legal age', () => {
|
|
1337
|
+
// Arrange
|
|
1338
|
+
const spec = new UserAgeSpecification(18);
|
|
1339
|
+
const user = new User('John Doe', 'john@example.com', 25);
|
|
1340
|
+
|
|
1341
|
+
// Act
|
|
1342
|
+
const result = spec.isSatisfiedBy(user);
|
|
1343
|
+
|
|
1344
|
+
// Assert
|
|
1345
|
+
expect(result).toBe(true);
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
it('should fail for underage users', () => {
|
|
1349
|
+
// Arrange
|
|
1350
|
+
const spec = new UserAgeSpecification(18);
|
|
1351
|
+
const user = new User('Jane Doe', 'jane@example.com', 17);
|
|
1352
|
+
|
|
1353
|
+
// Act
|
|
1354
|
+
const result = spec.isSatisfiedBy(user);
|
|
1355
|
+
|
|
1356
|
+
// Assert
|
|
1357
|
+
expect(result).toBe(false);
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
|
|
1361
|
+
describe('Composite Specifications', () => {
|
|
1362
|
+
it('should combine specifications with AND', () => {
|
|
1363
|
+
// Arrange
|
|
1364
|
+
const ageSpec = new UserAgeSpecification(18);
|
|
1365
|
+
const emailSpec = new UserEmailSpecification();
|
|
1366
|
+
const combined = ageSpec.and(emailSpec);
|
|
1367
|
+
|
|
1368
|
+
const validUser = new User('John Doe', 'john@example.com', 25);
|
|
1369
|
+
const invalidUser = new User('Jane Doe', 'invalid-email', 25);
|
|
1370
|
+
|
|
1371
|
+
// Act & Assert
|
|
1372
|
+
expect(combined.isSatisfiedBy(validUser)).toBe(true);
|
|
1373
|
+
expect(combined.isSatisfiedBy(invalidUser)).toBe(false);
|
|
1374
|
+
});
|
|
1375
|
+
});
|
|
1376
|
+
```
|
|
1377
|
+
|
|
1378
|
+
### Rule Testing
|
|
1379
|
+
|
|
1380
|
+
```typescript
|
|
1381
|
+
describe('Fluent Rules', () => {
|
|
1382
|
+
it('should validate string rules', () => {
|
|
1383
|
+
// Arrange
|
|
1384
|
+
const rule = Rules.forString()
|
|
1385
|
+
.required()
|
|
1386
|
+
.minLength(2)
|
|
1387
|
+
.maxLength(50)
|
|
1388
|
+
.pattern(/^[A-Za-z\s]+$/)
|
|
1389
|
+
.build();
|
|
1390
|
+
|
|
1391
|
+
// Act & Assert
|
|
1392
|
+
expect(rule.validate('John Doe').isValid).toBe(true);
|
|
1393
|
+
expect(rule.validate('J').isValid).toBe(false); // Too short
|
|
1394
|
+
expect(rule.validate('John123').isValid).toBe(false); // Invalid pattern
|
|
1395
|
+
expect(rule.validate('').isValid).toBe(false); // Required
|
|
1396
|
+
});
|
|
1397
|
+
|
|
1398
|
+
it('should validate number rules', () => {
|
|
1399
|
+
// Arrange
|
|
1400
|
+
const rule = Rules.forNumber().required().min(18).max(120).build();
|
|
1401
|
+
|
|
1402
|
+
// Act & Assert
|
|
1403
|
+
expect(rule.validate(25).isValid).toBe(true);
|
|
1404
|
+
expect(rule.validate(17).isValid).toBe(false); // Too low
|
|
1405
|
+
expect(rule.validate(121).isValid).toBe(false); // Too high
|
|
1406
|
+
});
|
|
1407
|
+
});
|
|
1408
|
+
```
|
|
1409
|
+
|
|
1410
|
+
### Async Validation Testing
|
|
1411
|
+
|
|
1412
|
+
```typescript
|
|
1413
|
+
describe('Async Validation', () => {
|
|
1414
|
+
it('should validate async specifications', async () => {
|
|
1415
|
+
// Arrange
|
|
1416
|
+
const mockRepository = {
|
|
1417
|
+
findByEmail: jest.fn().mockResolvedValue(null),
|
|
1418
|
+
};
|
|
1419
|
+
|
|
1420
|
+
const spec = new EmailUniquenessSpecification(mockRepository);
|
|
1421
|
+
const user = new User('John Doe', 'john@example.com', 25);
|
|
1422
|
+
|
|
1423
|
+
// Act
|
|
1424
|
+
const result = await spec.isSatisfiedByAsync(user);
|
|
1425
|
+
|
|
1426
|
+
// Assert
|
|
1427
|
+
expect(result).toBe(true);
|
|
1428
|
+
expect(mockRepository.findByEmail).toHaveBeenCalledWith('john@example.com');
|
|
1429
|
+
});
|
|
1430
|
+
|
|
1431
|
+
it('should handle async validation errors', async () => {
|
|
1432
|
+
// Arrange
|
|
1433
|
+
const mockRepository = {
|
|
1434
|
+
findByEmail: jest.fn().mockRejectedValue(new Error('Database error')),
|
|
1435
|
+
};
|
|
1436
|
+
|
|
1437
|
+
const spec = new EmailUniquenessSpecification(mockRepository);
|
|
1438
|
+
const user = new User('John Doe', 'john@example.com', 25);
|
|
1439
|
+
|
|
1440
|
+
// Act & Assert
|
|
1441
|
+
await expect(spec.isSatisfiedByAsync(user)).rejects.toThrow(
|
|
1442
|
+
'Database error'
|
|
1443
|
+
);
|
|
1444
|
+
});
|
|
1445
|
+
});
|
|
1446
|
+
```
|
|
1447
|
+
|
|
1448
|
+
## 🏆 Best Practices
|
|
1449
|
+
|
|
1450
|
+
### Specification Design
|
|
1451
|
+
|
|
1452
|
+
```typescript
|
|
1453
|
+
// ✅ Good: Single responsibility
|
|
1454
|
+
class UserAgeSpecification extends Specification<User> {
|
|
1455
|
+
constructor(private readonly minAge: number) {
|
|
1456
|
+
super();
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
isSatisfiedBy(user: User): boolean {
|
|
1460
|
+
return user.age >= this.minAge;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// ❌ Bad: Multiple responsibilities
|
|
1465
|
+
class UserValidationSpecification extends Specification<User> {
|
|
1466
|
+
isSatisfiedBy(user: User): boolean {
|
|
1467
|
+
return user.age >= 18 && user.email.includes('@') && user.name.length > 0;
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
```
|
|
1471
|
+
|
|
1472
|
+
### Rule Composition
|
|
1473
|
+
|
|
1474
|
+
```typescript
|
|
1475
|
+
// ✅ Good: Composable rules
|
|
1476
|
+
const nameRule = Rules.forString().required().minLength(2);
|
|
1477
|
+
const emailRule = Rules.forString().required().email();
|
|
1478
|
+
const ageRule = Rules.forNumber().required().min(18);
|
|
1479
|
+
|
|
1480
|
+
const userValidator = DomainValidator.create<User>()
|
|
1481
|
+
.forProperty('name', nameRule)
|
|
1482
|
+
.forProperty('email', emailRule)
|
|
1483
|
+
.forProperty('age', ageRule)
|
|
1484
|
+
.build();
|
|
1485
|
+
|
|
1486
|
+
// ❌ Bad: Monolithic validation
|
|
1487
|
+
const massiveValidator = DomainValidator.create<User>()
|
|
1488
|
+
.forProperty(
|
|
1489
|
+
'everything',
|
|
1490
|
+
Rules.forObject().custom(user => {
|
|
1491
|
+
// Complex validation logic mixed together
|
|
1492
|
+
return user.name && user.email && user.age >= 18;
|
|
1493
|
+
})
|
|
1494
|
+
)
|
|
1495
|
+
.build();
|
|
1496
|
+
```
|
|
1497
|
+
|
|
1498
|
+
### Error Handling
|
|
1499
|
+
|
|
1500
|
+
```typescript
|
|
1501
|
+
// ✅ Good: Specific error handling
|
|
1502
|
+
const validation = await userValidator.validate(user);
|
|
1503
|
+
|
|
1504
|
+
if (!validation.isValid) {
|
|
1505
|
+
const requiredErrors = validation.errors.filter(
|
|
1506
|
+
e => e.code === 'REQUIRED_FIELD'
|
|
1507
|
+
);
|
|
1508
|
+
const formatErrors = validation.errors.filter(
|
|
1509
|
+
e => e.code === 'INVALID_FORMAT'
|
|
1510
|
+
);
|
|
1511
|
+
|
|
1512
|
+
if (requiredErrors.length > 0) {
|
|
1513
|
+
throw new RequiredFieldsError(requiredErrors);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
if (formatErrors.length > 0) {
|
|
1517
|
+
throw new InvalidFormatError(formatErrors);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// ❌ Bad: Generic error handling
|
|
1522
|
+
const validation = await userValidator.validate(user);
|
|
1523
|
+
if (!validation.isValid) {
|
|
1524
|
+
throw new Error('Validation failed');
|
|
1525
|
+
}
|
|
1526
|
+
```
|
|
1527
|
+
|
|
1528
|
+
## 🤝 Contributing
|
|
1529
|
+
|
|
1530
|
+
We welcome contributions! Please see our
|
|
1531
|
+
[Contributing Guide](../../CONTRIBUTING.md) for details.
|
|
1532
|
+
|
|
1533
|
+
### Development Setup
|
|
1534
|
+
|
|
1535
|
+
```bash
|
|
1536
|
+
# Clone repository
|
|
1537
|
+
git clone https://github.com/vytches/ddd.git
|
|
1538
|
+
cd ddd
|
|
1539
|
+
|
|
1540
|
+
# Install dependencies
|
|
1541
|
+
pnpm install
|
|
1542
|
+
|
|
1543
|
+
# Build package
|
|
1544
|
+
pnpm build --filter=@vytches/ddd-validation
|
|
1545
|
+
|
|
1546
|
+
# Run tests
|
|
1547
|
+
pnpm test --filter=@vytches/ddd-validation
|
|
1548
|
+
|
|
1549
|
+
# Run in development mode
|
|
1550
|
+
pnpm dev --filter=@vytches/ddd-validation
|
|
1551
|
+
```
|
|
1552
|
+
|
|
1553
|
+
## 📄 License
|
|
1554
|
+
|
|
1555
|
+
This project is licensed under the MIT License - see the
|
|
1556
|
+
[LICENSE](../../LICENSE) file for details.
|
|
1557
|
+
|
|
1558
|
+
---
|
|
1559
|
+
|
|
1560
|
+
**Part of the [@vytches/ddd-core](https://github.com/vytches/ddd) ecosystem**
|
|
1561
|
+
|
|
1562
|
+
For more information, visit the [main documentation](../../README.md).
|