@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 VytchesDDD
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/LLMGUIDE.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# @vytches/ddd-validation - LLM Guide
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
Specification pattern for domain validation with composable sync and async
|
|
6
|
+
predicates. Provides `BusinessRuleValidator` for fluent rule chains and the
|
|
7
|
+
`Specification` factory for inline lambda specs without class boilerplate.
|
|
8
|
+
|
|
9
|
+
## Quick Start
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
import { Specification, BusinessRuleValidator } from '@vytches/ddd-validation';
|
|
13
|
+
|
|
14
|
+
interface Order {
|
|
15
|
+
total: number;
|
|
16
|
+
status: 'pending' | 'paid' | 'cancelled';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Inline spec — the primary pattern, no class needed
|
|
20
|
+
const isPositive = Specification.create<Order>(o => o.total > 0);
|
|
21
|
+
const isPending = Specification.create<Order>(o => o.status === 'pending');
|
|
22
|
+
|
|
23
|
+
// Compose specs
|
|
24
|
+
const isPayable = isPositive.and(isPending);
|
|
25
|
+
|
|
26
|
+
// Validate
|
|
27
|
+
const order: Order = { total: 100, status: 'pending' };
|
|
28
|
+
const ok = isPayable.isSatisfiedBy(order); // true
|
|
29
|
+
|
|
30
|
+
// Fluent validator (returns Result<T, ValidationErrors>)
|
|
31
|
+
const validator = BusinessRuleValidator.create<Order>()
|
|
32
|
+
.addRule('total', o => o.total > 0, 'Total must be positive')
|
|
33
|
+
.addRule(
|
|
34
|
+
'status',
|
|
35
|
+
o => o.status !== 'cancelled',
|
|
36
|
+
'Cannot process cancelled order'
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const result = validator.validate(order);
|
|
40
|
+
if (result.isFailure) {
|
|
41
|
+
result.error.errors.forEach(e => console.error(e.property, e.message));
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Key API
|
|
46
|
+
|
|
47
|
+
| Export | Description |
|
|
48
|
+
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
49
|
+
| `Specification.create<T>(predicate)` | Create inline spec from lambda — primary pattern |
|
|
50
|
+
| `Specification.and<T>(...specs)` | Combine multiple specs with AND (static, variadic) |
|
|
51
|
+
| `Specification.or<T>(...specs)` | Combine multiple specs with OR (static, variadic) |
|
|
52
|
+
| `Specification.not<T>(spec)` | Negate a spec |
|
|
53
|
+
| `Specification.propertyEquals<T>(key, value)` | Property equality spec |
|
|
54
|
+
| `Specification.propertyIn<T>(key, values[])` | Property membership spec |
|
|
55
|
+
| `Specification.propertyBetween<T>(key, min, max)` | Numeric range spec |
|
|
56
|
+
| `Specification.alwaysTrue<T>()` | Unconditionally satisfied |
|
|
57
|
+
| `Specification.alwaysFalse<T>()` | Never satisfied |
|
|
58
|
+
| `CompositeSpecification<T>` | Base class for class-based specs; exposes `.and()`, `.or()`, `.not()` |
|
|
59
|
+
| `MemoizedSpecification<T>` | **Per-candidate caching** (VP-002) — wrap any spec; `WeakMap<T, boolean>` cache means repeated `isSatisfiedBy(sameCandidate)` runs the inner spec exactly once. `invalidate(c)` evicts manually. Use only for pure specs (no external state) |
|
|
60
|
+
| `AsyncCompositeSpecification<T>` | Async base class with optional `name`, `description`, and `explainFailureAsync` |
|
|
61
|
+
| `AsyncCompositeSpecification.create<T>(predicate, name?, desc?)` | Inline async spec |
|
|
62
|
+
| `BusinessRuleValidator<T>` | Fluent validator; returns `Result<T, ValidationErrors>` |
|
|
63
|
+
| `BusinessRuleValidator.fromSpecification<T>(spec, message)` | Validator from a single spec |
|
|
64
|
+
| `ValidationError` / `ValidationErrors` | Error types with `property`, `message`, `context` |
|
|
65
|
+
|
|
66
|
+
## Patterns
|
|
67
|
+
|
|
68
|
+
### Pattern 1: Inline specs (preferred)
|
|
69
|
+
|
|
70
|
+
Use `Specification.create` instead of classes for one-off or module-local specs.
|
|
71
|
+
This covers the vast majority of real-world cases.
|
|
72
|
+
|
|
73
|
+
```typescript
|
|
74
|
+
import { Specification } from '@vytches/ddd-validation';
|
|
75
|
+
|
|
76
|
+
interface Product {
|
|
77
|
+
price: number;
|
|
78
|
+
stock: number;
|
|
79
|
+
active: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const canPurchase = Specification.and(
|
|
83
|
+
Specification.create<Product>(p => p.active),
|
|
84
|
+
Specification.create<Product>(p => p.stock > 0),
|
|
85
|
+
Specification.create<Product>(p => p.price > 0)
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
canPurchase.isSatisfiedBy(product); // boolean
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Pattern 2: Class-based specs (reusable, named)
|
|
92
|
+
|
|
93
|
+
Use `CompositeSpecification` only when the spec is complex, reused across
|
|
94
|
+
multiple places, or needs an explicit name for error messages.
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
import { CompositeSpecification } from '@vytches/ddd-validation';
|
|
98
|
+
|
|
99
|
+
class MinimumOrderSpec extends CompositeSpecification<Order> {
|
|
100
|
+
constructor(private readonly minimum: number) {
|
|
101
|
+
super();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
isSatisfiedBy(order: Order): boolean {
|
|
105
|
+
return order.total >= this.minimum;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Composition still works on class instances
|
|
110
|
+
const policy = new MinimumOrderSpec(50).and(isPending);
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Pattern 3: Async specs for I/O-dependent rules
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { AsyncCompositeSpecification } from '@vytches/ddd-validation';
|
|
117
|
+
|
|
118
|
+
const isUniqueEmail = AsyncCompositeSpecification.create<User>(
|
|
119
|
+
async (user, ctx) => {
|
|
120
|
+
const exists = await (ctx?.db as Db).users.findOne({ email: user.email });
|
|
121
|
+
return exists === null;
|
|
122
|
+
},
|
|
123
|
+
'UniqueEmailSpec',
|
|
124
|
+
'Email must be unique in the system'
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
const combined = isUniqueEmail.and(anotherAsyncSpec);
|
|
128
|
+
const ok = await combined.isSatisfiedByAsync(user, { db });
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Anti-Patterns
|
|
132
|
+
|
|
133
|
+
**Creating a class for a one-off validation.** Use `Specification.create`
|
|
134
|
+
instead. Class-based specs make sense only when the spec is exported and reused
|
|
135
|
+
in several modules.
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
// Wrong: unnecessary class
|
|
139
|
+
class IsActiveSpec extends CompositeSpecification<User> {
|
|
140
|
+
isSatisfiedBy(u: User) {
|
|
141
|
+
return u.isActive;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Correct: inline
|
|
146
|
+
const isActive = Specification.create<User>(u => u.isActive);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
**Using `AsyncCompositeSpecification` when no I/O is involved.** Async execution
|
|
150
|
+
is slower and complicates composition. Prefer sync specs and convert to async
|
|
151
|
+
only at the point that actually needs awaiting.
|
|
152
|
+
|
|
153
|
+
**Ignoring `BusinessRuleValidator.validate()` return value.** The method returns
|
|
154
|
+
`Result<T, ValidationErrors>`, not a boolean. Always check `result.isFailure`
|
|
155
|
+
before proceeding.
|
|
156
|
+
|
|
157
|
+
**Calling `Specification.and()` or `Specification.or()` with zero arguments.**
|
|
158
|
+
Both return `AlwaysTrue` and `AlwaysFalse` respectively for the empty case —
|
|
159
|
+
which may be surprising.
|
|
160
|
+
|
|
161
|
+
**Using `when().otherwise()` without a preceding `when()` call.**
|
|
162
|
+
`BusinessRuleValidator.otherwise()` throws at runtime if `when()` was not called
|
|
163
|
+
immediately before it.
|
|
164
|
+
|
|
165
|
+
## Hidden Features
|
|
166
|
+
|
|
167
|
+
**`Specification.and<T>(...specs)` accepts variadic arguments.** Unlike the
|
|
168
|
+
instance `.and()` method (which takes one argument), the static
|
|
169
|
+
`Specification.and` accepts an arbitrary number of specs and chains them all.
|
|
170
|
+
|
|
171
|
+
**`AsyncCompositeSpecification` runs `.and()` children in parallel.**
|
|
172
|
+
`AndAsyncSpecification.isSatisfiedByAsync` uses `Promise.all`, so two async
|
|
173
|
+
specs are evaluated concurrently, not sequentially.
|
|
174
|
+
|
|
175
|
+
**`BusinessRuleValidator.when()` supports spec-based conditions via
|
|
176
|
+
`whenSatisfies`.** You can pass a `ISpecification<T>` directly instead of a
|
|
177
|
+
plain predicate function.
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const validator = BusinessRuleValidator.create<Order>().whenSatisfies(
|
|
181
|
+
Specification.create<Order>(o => o.type === 'international'),
|
|
182
|
+
v =>
|
|
183
|
+
v.addRule(
|
|
184
|
+
'country',
|
|
185
|
+
o => !!o.country,
|
|
186
|
+
'Country required for international orders'
|
|
187
|
+
)
|
|
188
|
+
);
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**`BusinessRuleValidator.addNested()` propagates dot-notation paths.** Nested
|
|
192
|
+
validators prefix error property paths automatically (e.g., `address.zip`).
|
|
193
|
+
|
|
194
|
+
## Package Dependencies
|
|
195
|
+
|
|
196
|
+
`@vytches/ddd-validation` depends on:
|
|
197
|
+
|
|
198
|
+
- `@vytches/ddd-contracts` — `ISpecification`, `IAsyncSpecification`,
|
|
199
|
+
`IValidator`, `IValidationRule`
|
|
200
|
+
- `@vytches/ddd-utils` — `Result<T, E>`
|
|
201
|
+
- `@vytches/ddd-logging` — structured logger
|
|
202
|
+
|
|
203
|
+
Packages that depend on `@vytches/ddd-validation`:
|
|
204
|
+
|
|
205
|
+
- `@vytches/ddd-policies` — `BusinessRuleValidatorAdapter`,
|
|
206
|
+
`BusinessRuleValidatorPolicy`
|
|
207
|
+
- `@vytches/ddd-enterprise` — re-exports everything
|