@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 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