@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.
@@ -0,0 +1,69 @@
1
+ import { ISpecification } from '@vytches/ddd-contracts';
2
+ import { BusinessRuleValidator } from './business-rules/business-rule-validator';
3
+ export interface RuleFunction<T> {
4
+ (validator: BusinessRuleValidator<T>): BusinessRuleValidator<T>;
5
+ }
6
+ export interface IRulesProvider {
7
+ readonly name: string;
8
+ }
9
+ export interface ICoreRules {
10
+ required: <T>(property: keyof T, message?: string) => RuleFunction<T>;
11
+ minLength: <T>(property: keyof T, length: number, message?: string) => RuleFunction<T>;
12
+ maxLength: <T>(property: keyof T, length: number, message?: string) => RuleFunction<T>;
13
+ /**
14
+ * Pattern-match a property against a RegExp.
15
+ *
16
+ * **Security warning (REL-007):** the `regex` argument is executed against
17
+ * input data. Do **NOT** construct the RegExp dynamically from
18
+ * user-controlled strings (e.g. `new RegExp(userInput)`) — this exposes
19
+ * your application to ReDoS (Regular Expression Denial of Service)
20
+ * attacks. Use only literal regex patterns in code review-able form, or
21
+ * pre-validate user input against a strict allow-list before constructing
22
+ * a RegExp.
23
+ *
24
+ * The library performs no internal validation of the regex; that is the
25
+ * consumer's responsibility.
26
+ */
27
+ pattern: <T>(property: keyof T, regex: RegExp, message?: string) => RuleFunction<T>;
28
+ range: <T>(property: keyof T, min: number, max: number, message?: string) => RuleFunction<T>;
29
+ email: <T>(property: keyof T, message?: string) => RuleFunction<T>;
30
+ satisfies: <T>(specification: ISpecification<T>, message: string) => RuleFunction<T>;
31
+ propertySatisfies: <T, P>(property: keyof T & string, specification: ISpecification<P>, message: string, getValue: (obj: T) => P) => RuleFunction<T>;
32
+ when: <T>(condition: (value: T) => boolean, thenRules: (validator: BusinessRuleValidator<T>) => void) => RuleFunction<T>;
33
+ whenSatisfies: <T>(specification: ISpecification<T>, thenRules: (validator: BusinessRuleValidator<T>) => void) => RuleFunction<T>;
34
+ otherwise: <T>(elseRules: (validator: BusinessRuleValidator<T>) => void) => RuleFunction<T>;
35
+ }
36
+ export declare class CoreRules implements ICoreRules, IRulesProvider {
37
+ readonly name = "core";
38
+ required: <T>(property: keyof T, message?: string) => (validator: BusinessRuleValidator<T>) => BusinessRuleValidator<T>;
39
+ minLength: <T>(property: keyof T, length: number, message?: string) => (validator: BusinessRuleValidator<T>) => BusinessRuleValidator<T>;
40
+ maxLength: <T>(property: keyof T, length: number, message?: string) => (validator: BusinessRuleValidator<T>) => BusinessRuleValidator<T>;
41
+ pattern: <T>(property: keyof T, regex: RegExp, message?: string) => (validator: BusinessRuleValidator<T>) => BusinessRuleValidator<T>;
42
+ range: <T>(property: keyof T, min: number, max: number, message?: string) => (validator: BusinessRuleValidator<T>) => BusinessRuleValidator<T>;
43
+ email: <T>(property: keyof T, message?: string) => (validator: BusinessRuleValidator<T>) => BusinessRuleValidator<T>;
44
+ satisfies: <T>(specification: ISpecification<T>, message: string) => (validator: BusinessRuleValidator<T>) => BusinessRuleValidator<T>;
45
+ propertySatisfies: <T, P>(property: keyof T & string, specification: ISpecification<P>, message: string, getValue: (obj: T) => P) => (validator: BusinessRuleValidator<T>) => BusinessRuleValidator<T>;
46
+ whenSatisfies: <T>(specification: ISpecification<T>, thenRules: (validator: BusinessRuleValidator<T>) => void) => (validator: BusinessRuleValidator<T>) => BusinessRuleValidator<T>;
47
+ otherwise: <T>(elseRules: (validator: BusinessRuleValidator<T>) => void) => (validator: BusinessRuleValidator<T>) => BusinessRuleValidator<T>;
48
+ when: <T>(condition: (value: T) => boolean, thenRules: (validator: BusinessRuleValidator<T>) => void) => (validator: BusinessRuleValidator<T>) => BusinessRuleValidator<T>;
49
+ }
50
+ export declare class RulesRegistry {
51
+ private static providers;
52
+ private static core;
53
+ /**
54
+ * Register a domain-specific rule provider
55
+ */
56
+ static register(provider: IRulesProvider): void;
57
+ /**
58
+ * Get a specific rule provider by name
59
+ */
60
+ static getProvider<T extends IRulesProvider>(name: string): T;
61
+ /**
62
+ * Get core rules
63
+ */
64
+ static get Rules(): ICoreRules;
65
+ /**
66
+ * Access domain-specific rules
67
+ */
68
+ static forDomain<T extends IRulesProvider>(domain: string): T;
69
+ }
@@ -0,0 +1,72 @@
1
+ import { IAsyncSpecification } from '../../../contracts/src/index.ts';
2
+ export declare abstract class AsyncCompositeSpecification<T> implements IAsyncSpecification<T> {
3
+ /**
4
+ * Check if the candidate satisfies this specification asynchronously
5
+ * @param candidate The entity to evaluate
6
+ * @param context Optional context for evaluation (e.g., user, tenant, environment)
7
+ */
8
+ abstract isSatisfiedByAsync(candidate: T, context?: Record<string, unknown>): Promise<boolean>;
9
+ /**
10
+ * Optional name for debugging/logging
11
+ */
12
+ readonly name?: string | undefined;
13
+ /**
14
+ * Optional description for debugging/logging
15
+ */
16
+ readonly description?: string | undefined;
17
+ /**
18
+ * Combine with another async specification using AND logic
19
+ */
20
+ and(other: IAsyncSpecification<T>): IAsyncSpecification<T>;
21
+ /**
22
+ * Combine with another async specification using OR logic
23
+ */
24
+ or(other: IAsyncSpecification<T>): IAsyncSpecification<T>;
25
+ /**
26
+ * Negate this async specification
27
+ */
28
+ not(): IAsyncSpecification<T>;
29
+ /**
30
+ * Create a specification from an async predicate function
31
+ */
32
+ static create<T>(predicate: (candidate: T, context?: Record<string, unknown>) => Promise<boolean>, name?: string, description?: string): IAsyncSpecification<T>;
33
+ /**
34
+ * Optional method to explain why specification failed
35
+ */
36
+ explainFailureAsync?(_candidate: T, _context?: Record<string, unknown>): Promise<string | null>;
37
+ }
38
+ /**
39
+ * @since 0.4.2
40
+ */
41
+ export declare class AndAsyncSpecification<T> extends AsyncCompositeSpecification<T> {
42
+ private readonly left;
43
+ private readonly right;
44
+ readonly name: string;
45
+ readonly description: string;
46
+ constructor(left: IAsyncSpecification<T>, right: IAsyncSpecification<T>);
47
+ isSatisfiedByAsync(candidate: T, context?: Record<string, unknown>): Promise<boolean>;
48
+ explainFailureAsync(candidate: T, context?: Record<string, unknown>): Promise<string | null>;
49
+ }
50
+ /**
51
+ * @since 0.4.2
52
+ */
53
+ export declare class OrAsyncSpecification<T> extends AsyncCompositeSpecification<T> {
54
+ private readonly left;
55
+ private readonly right;
56
+ readonly name: string;
57
+ readonly description: string;
58
+ constructor(left: IAsyncSpecification<T>, right: IAsyncSpecification<T>);
59
+ isSatisfiedByAsync(candidate: T, context?: Record<string, unknown>): Promise<boolean>;
60
+ explainFailureAsync(candidate: T, context?: Record<string, unknown>): Promise<string | null>;
61
+ }
62
+ /**
63
+ * @since 0.4.2
64
+ */
65
+ export declare class NotAsyncSpecification<T> extends AsyncCompositeSpecification<T> {
66
+ private readonly spec;
67
+ readonly name: string;
68
+ readonly description: string;
69
+ constructor(spec: IAsyncSpecification<T>);
70
+ isSatisfiedByAsync(candidate: T, context?: Record<string, unknown>): Promise<boolean>;
71
+ explainFailureAsync(candidate: T, context?: Record<string, unknown>): Promise<string | null>;
72
+ }
@@ -0,0 +1,25 @@
1
+ import { ISpecification } from '../../../contracts/src/index.ts';
2
+ export declare abstract class CompositeSpecification<T> implements ISpecification<T> {
3
+ abstract isSatisfiedBy(candidate: T): boolean;
4
+ and(other: ISpecification<T>): ISpecification<T>;
5
+ or(other: ISpecification<T>): ISpecification<T>;
6
+ not(): ISpecification<T>;
7
+ static create<T>(predicate: (candidate: T) => boolean): ISpecification<T>;
8
+ }
9
+ export declare class AndSpecification<T> extends CompositeSpecification<T> {
10
+ private readonly left;
11
+ private readonly right;
12
+ constructor(left: ISpecification<T>, right: ISpecification<T>);
13
+ isSatisfiedBy(candidate: T): boolean;
14
+ }
15
+ export declare class OrSpecification<T> extends CompositeSpecification<T> {
16
+ private readonly left;
17
+ private readonly right;
18
+ constructor(left: ISpecification<T>, right: ISpecification<T>);
19
+ isSatisfiedBy(candidate: T): boolean;
20
+ }
21
+ export declare class NotSpecification<T> extends CompositeSpecification<T> {
22
+ private readonly spec;
23
+ constructor(spec: ISpecification<T>);
24
+ isSatisfiedBy(candidate: T): boolean;
25
+ }
@@ -0,0 +1,5 @@
1
+ export * from './composite-specification';
2
+ export * from './async-composite-specification';
3
+ export * from './memoized-specification';
4
+ export * from './specification-operators';
5
+ export * from './specification-validator';
@@ -0,0 +1,84 @@
1
+ import { ISpecification } from '../../../contracts/src/index.ts';
2
+ import { CompositeSpecification } from './composite-specification';
3
+ /**
4
+ * Specification wrapper that memoizes `isSatisfiedBy` results per candidate.
5
+ *
6
+ * VP-002 (2026-05-09): cuts repeated evaluation cost when the same
7
+ * specification is checked multiple times against the same candidate
8
+ * during a single query — common in pipelines like "filter list by spec,
9
+ * then enrich with another spec, then sort by a third predicate that
10
+ * also calls the first spec." Without memoization, each `isSatisfiedBy`
11
+ * call walks the full predicate tree even when the answer is already
12
+ * known.
13
+ *
14
+ * **Memoization storage**: `WeakMap<object, boolean>` for object
15
+ * candidates — automatic GC when the candidate is unreachable. Primitive
16
+ * candidates (strings, numbers) bypass the cache entirely (WeakMap keys
17
+ * must be objects); for those the wrapper degrades to a pass-through.
18
+ *
19
+ * **When NOT to use this**: specs whose result depends on mutable
20
+ * external state (timestamps, counters, randomness, fetched data). The
21
+ * cache assumes "same candidate object → same answer." If your spec is
22
+ * pure (depends only on candidate fields), you're safe.
23
+ *
24
+ * **Lifecycle contract — NOT a singleton-safe Specification.** Per Evans
25
+ * (Blue Book, ch. 9), a canonical `Specification` is a *stateless predicate*:
26
+ * shareable, serializable, safe to cache as a singleton. `MemoizedSpecification`
27
+ * is intentionally stateful — its `WeakMap` cache makes results dependent
28
+ * on call history. Treat it as a *per-query* helper, not as something to
29
+ * stash in a module-level constant or DI singleton with a long lifetime.
30
+ * Construct fresh, use within a request, let it be collected.
31
+ *
32
+ * Composes correctly with `and`/`or`/`not` because it extends
33
+ * `CompositeSpecification` — but those operators wrap the *current*
34
+ * memoized result with new instances, so the composition itself isn't
35
+ * memoized. To memoize the whole tree, wrap the outer composite.
36
+ *
37
+ * @example Pure-function specification
38
+ * ```typescript
39
+ * import { MemoizedSpecification, CompositeSpecification } from '@vytches/ddd-validation';
40
+ *
41
+ * class HighValueOrder extends CompositeSpecification<Order> {
42
+ * isSatisfiedBy(o: Order) { return o.total > 1000; }
43
+ * }
44
+ *
45
+ * const spec = new MemoizedSpecification(new HighValueOrder());
46
+ * spec.isSatisfiedBy(order); // computes
47
+ * spec.isSatisfiedBy(order); // cached, no recomputation
48
+ * spec.isSatisfiedBy(otherOrder); // computes (different candidate)
49
+ *
50
+ * // After `order` is unreachable, its cache entry is GC'd automatically.
51
+ * ```
52
+ *
53
+ * @example Composing memoized + raw specs
54
+ * ```typescript
55
+ * const expensive = new MemoizedSpecification(new ComplexCustomerCheck());
56
+ * const cheap = new IsActiveOrder();
57
+ *
58
+ * // The composed spec re-runs `expensive` once per candidate (cache hit
59
+ * // on second call), and `cheap` directly each time.
60
+ * const both = expensive.and(cheap);
61
+ * ```
62
+ *
63
+ * @public
64
+ * @stable
65
+ * @since 0.25.0
66
+ */
67
+ export declare class MemoizedSpecification<T extends object> extends CompositeSpecification<T> {
68
+ private readonly inner;
69
+ private readonly cache;
70
+ constructor(inner: ISpecification<T>);
71
+ isSatisfiedBy(candidate: T): boolean;
72
+ /**
73
+ * Manually evict a candidate from the cache. Use when you know the
74
+ * candidate's relevant fields have changed and you want fresh evaluation
75
+ * without discarding cache entries for other candidates.
76
+ */
77
+ invalidate(candidate: T): void;
78
+ /**
79
+ * Forwarded to the inner spec, if it implements optional `explainFailure`.
80
+ * Skips the cache because failure explanations are typically only computed
81
+ * on-demand.
82
+ */
83
+ explainFailure(candidate: T): string | null;
84
+ }
@@ -0,0 +1,70 @@
1
+ import { ISpecification } from '../../../contracts/src/index.ts';
2
+ import { CompositeSpecification } from './composite-specification';
3
+ export declare class AlwaysTrueSpecification<T> extends CompositeSpecification<T> {
4
+ isSatisfiedBy(_candidate: T): boolean;
5
+ }
6
+ export declare class AlwaysFalseSpecification<T> extends CompositeSpecification<T> {
7
+ isSatisfiedBy(_candidate: T): boolean;
8
+ }
9
+ export declare class PredicateSpecification<T> extends CompositeSpecification<T> {
10
+ private readonly predicate;
11
+ constructor(predicate: (candidate: T) => boolean);
12
+ isSatisfiedBy(candidate: T): boolean;
13
+ }
14
+ export declare class PropertyEqualsSpecification<T> extends CompositeSpecification<T> {
15
+ private readonly propertyName;
16
+ private readonly expectedValue;
17
+ constructor(propertyName: keyof T, expectedValue: T[keyof T]);
18
+ isSatisfiedBy(candidate: T): boolean;
19
+ }
20
+ export declare class PropertyInSpecification<T> extends CompositeSpecification<T> {
21
+ private readonly propertyName;
22
+ private readonly possibleValues;
23
+ constructor(propertyName: keyof T, possibleValues: T[keyof T][]);
24
+ isSatisfiedBy(candidate: T): boolean;
25
+ }
26
+ export declare class PropertyBetweenSpecification<T> extends CompositeSpecification<T> {
27
+ private readonly propertyName;
28
+ private readonly min;
29
+ private readonly max;
30
+ constructor(propertyName: keyof T, min: number, max: number);
31
+ isSatisfiedBy(candidate: T): boolean;
32
+ }
33
+ export declare const Specification: {
34
+ /**
35
+ * Tworzy specyfikację zawsze prawdziwą
36
+ */
37
+ alwaysTrue<T>(): ISpecification<T>;
38
+ /**
39
+ * Tworzy specyfikację zawsze fałszywą
40
+ */
41
+ alwaysFalse<T>(): ISpecification<T>;
42
+ /**
43
+ * Tworzy specyfikację opartą o predykat
44
+ */
45
+ create<T>(predicate: (candidate: T) => boolean): ISpecification<T>;
46
+ /**
47
+ * Tworzy specyfikację sprawdzającą równość właściwości
48
+ */
49
+ propertyEquals<T>(propertyName: keyof T, expectedValue: T[keyof T]): ISpecification<T>;
50
+ /**
51
+ * Tworzy specyfikację sprawdzającą zawieranie się właściwości w zbiorze
52
+ */
53
+ propertyIn<T>(propertyName: keyof T, possibleValues: T[keyof T][]): ISpecification<T>;
54
+ /**
55
+ * Tworzy specyfikację sprawdzającą zakres wartości
56
+ */
57
+ propertyBetween<T>(propertyName: keyof T, min: number, max: number): ISpecification<T>;
58
+ /**
59
+ * Łączy specyfikacje operatorem AND
60
+ */
61
+ and<T>(...specifications: ISpecification<T>[]): ISpecification<T>;
62
+ /**
63
+ * Łączy specyfikacje operatorem OR
64
+ */
65
+ or<T>(...specifications: ISpecification<T>[]): ISpecification<T>;
66
+ /**
67
+ * Neguje specyfikację
68
+ */
69
+ not<T>(specification: ISpecification<T>): ISpecification<T>;
70
+ };
@@ -0,0 +1,26 @@
1
+ import { ISpecification, IValidator } from '../../../contracts/src/index.ts';
2
+ import { Result } from '../../../utils/src/index.ts';
3
+ import { ValidationErrors } from '../validation-error';
4
+ export declare class SpecificationValidator<T> implements IValidator<T> {
5
+ private validationRules;
6
+ /**
7
+ * Adds a validation rule based on a specification
8
+ */
9
+ addRule(specification: ISpecification<T>, message: string, property?: string, context?: Record<string, unknown>): SpecificationValidator<T>;
10
+ /**
11
+ * Adds a rule for a specific object property
12
+ */
13
+ addPropertyRule<P>(property: keyof T & string, specification: ISpecification<P>, message: string, getValue: (obj: T) => P, context?: Record<string, any>): SpecificationValidator<T>;
14
+ /**
15
+ * Performs validation based on all specifications
16
+ */
17
+ validate(value: T): Result<T, ValidationErrors>;
18
+ /**
19
+ * Creates a validator with a single rule
20
+ */
21
+ static fromSpecification<T>(specification: ISpecification<T>, message: string, property?: string, context?: Record<string, any>): SpecificationValidator<T>;
22
+ /**
23
+ * Creates an empty validator
24
+ */
25
+ static create<T>(): SpecificationValidator<T>;
26
+ }
@@ -0,0 +1,13 @@
1
+ import { IValidationError, IValidationErrors } from '@vytches/ddd-contracts';
2
+ export declare class ValidationError implements IValidationError {
3
+ readonly property: string;
4
+ readonly message: string;
5
+ readonly context?: Record<string, unknown> | undefined;
6
+ constructor(property: string, message: string, context?: Record<string, unknown> | undefined);
7
+ toString(): string;
8
+ }
9
+ export declare class ValidationErrors extends Error implements IValidationErrors {
10
+ readonly errors: IValidationError[];
11
+ constructor(errors: ValidationError[]);
12
+ get length(): number;
13
+ }
@@ -0,0 +1,51 @@
1
+ import { ISpecification, IValidationErrors, IValidator } from '@vytches/ddd-contracts';
2
+ import { Result } from '@vytches/ddd-utils';
3
+ import { BusinessRuleValidator } from './business-rules/business-rule-validator';
4
+ import { ValidationErrors } from './validation-error';
5
+ export declare const Validation: {
6
+ /**
7
+ * Tworzy nowy walidator reguł biznesowych
8
+ */
9
+ create<T>(): BusinessRuleValidator<T>;
10
+ /**
11
+ * Tworzy walidator oparty na specyfikacji
12
+ */
13
+ fromSpecification<T>(specification: ISpecification<T>, message: string, property?: string): IValidator<T>;
14
+ /**
15
+ * Tworzy walidator łączący wiele walidatorów
16
+ */
17
+ combine<T>(...validators: IValidator<T>[]): IValidator<T>;
18
+ /**
19
+ * Waliduje obiekt bezpośrednio za pomocą specyfikacji
20
+ */
21
+ validateWithSpecification<T>(value: T, specification: ISpecification<T>, message: string): Result<T, IValidationErrors>;
22
+ /**
23
+ * Waliduje za pomocą wielu specyfikacji z własnymi komunikatami błędów
24
+ */
25
+ validateWithRules<T>(value: T, rules: Array<{
26
+ specification: ISpecification<T>;
27
+ message: string;
28
+ property?: string;
29
+ }>): Result<T, ValidationErrors>;
30
+ /**
31
+ * Konwertuje specyfikację na walidator
32
+ */
33
+ specificationToValidator<T>(specification: ISpecification<T>, message: string, property?: string): IValidator<T>;
34
+ /**
35
+ * Konwertuje walidator na specyfikację
36
+ */
37
+ validatorToSpecification<T>(validator: IValidator<T>): ISpecification<T>;
38
+ /**
39
+ * Tworzy walidator dla głęboko zagnieżdżonej struktury obiektów
40
+ */
41
+ forNestedPath<T>(path: string[], validator: IValidator<unknown>): IValidator<T>;
42
+ /**
43
+ * Waliduje głęboko zagnieżdżoną właściwość
44
+ */
45
+ validatePath<T, P>(object: T, path: (string | number)[], valueValidator: IValidator<P>): Result<T, ValidationErrors>;
46
+ /**
47
+ * Używa zewnętrznego walidatora implementującego IValidator<T>
48
+ * Pozwala na integrację z bibliotekami takimi jak zod, class-validator, itp.
49
+ */
50
+ useExternal<T>(validator: IValidator<T>): IValidator<T>;
51
+ };
package/package.json ADDED
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "@vytches/ddd-validation",
3
+ "version": "0.26.0",
4
+ "description": "Business rules and specifications for domain validation",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "sideEffects": false,
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LLMGUIDE.md"
21
+ ],
22
+ "keywords": [
23
+ "ddd",
24
+ "domain-driven-design",
25
+ "typescript",
26
+ "validation"
27
+ ],
28
+ "author": "VytchesDDD",
29
+ "license": "MIT",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/vytches/ddd.git",
33
+ "directory": "packages/validation"
34
+ },
35
+ "devDependencies": {
36
+ "typescript": "^5.0.0",
37
+ "vite": "^7.3.2",
38
+ "vite-plugin-dts": "^4.0.0",
39
+ "vitest": "^2.0.0",
40
+ "@vytches/ddd-testing": "0.26.0"
41
+ },
42
+ "nx": {
43
+ "tags": [
44
+ "scope:validation",
45
+ "type:lib",
46
+ "layer:patterns"
47
+ ]
48
+ },
49
+ "publishConfig": {
50
+ "registry": "https://registry.npmjs.org",
51
+ "access": "public"
52
+ },
53
+ "dependencies": {
54
+ "@vytches/ddd-contracts": "0.26.0",
55
+ "@vytches/ddd-domain-primitives": "0.26.0",
56
+ "@vytches/ddd-logging": "0.26.0",
57
+ "@vytches/ddd-utils": "0.26.0"
58
+ },
59
+ "scripts": {
60
+ "build": "vite build",
61
+ "dev": "vite build --watch",
62
+ "clean": "rm -rf dist .tsbuildinfo",
63
+ "test": "vitest run",
64
+ "test:watch": "vitest",
65
+ "test:coverage": "vitest run --coverage",
66
+ "lint": "eslint src --ext .ts",
67
+ "type-check": "tsc --noEmit"
68
+ }
69
+ }