@vytches/ddd-policies
v0.28.0
Published
Business policies and policy builder patterns
Maintainers
Readme
@vytches/ddd-policies
Enterprise-grade business policy validation with fluent API, temporal support, and sophisticated rule composition
Complete business policy framework with fluent builder API, temporal validation, policy behaviors, and comprehensive violation reporting. Designed for complex business rules with enterprise-grade context management.
📋 Table of Contents
- Installation
- Key Features
- Core Concepts
- Quick Start
- Policy Builder
- Temporal Policies
- Policy Groups
- Policy Behaviors
- Policy Registry
- Violation Handling
- Advanced Usage
- Integration Patterns
- Testing
- Best Practices
- Contributing
🚀 Installation
# npm
npm install @vytches/ddd-policies
# yarn
yarn add @vytches/ddd-policies
# pnpm
pnpm add @vytches/ddd-policiesPeer Dependencies
# Required for full functionality
npm install @vytches/ddd-domain-primitives @vytches/ddd-validation @vytches/ddd-utils✨ Key Features
Business Policy Framework
- Fluent API: Intuitive fluent builder for policy creation
- Temporal Support: Time-aware policy validation with business hours
- Policy Composition: Complex policy groups with AND/OR logic
- Violation Reporting: Rich violation details with severity levels
Advanced Features
- Policy Behaviors: MediatR-style behaviors for cross-cutting concerns
- Context Management: Enterprise-grade context propagation
- Specification Integration: Direct support for DDD specifications
- Event-Driven: Automatic policy evaluation events
Enterprise Quality
- Audit Trails: Complete audit logging for policy evaluations
- Multi-Tenancy: Tenant-aware policy resolution
- Performance: Optimized for high-throughput scenarios
- Testing Support: Comprehensive policy testing utilities
🎯 Core Concepts
Policy Pattern
Policies encapsulate business rules that can be evaluated against entities:
// Basic policy interface
interface IPolicy<T> {
check(request: PolicyRequest<T>): Promise<PolicyResult<T>>;
getId(): string;
getDomain(): string;
getName(): string;
}
// Policy request with context
interface PolicyRequest<T> {
entity: T;
context: PolicyContext;
metadata?: Record<string, any>;
}
// Policy result with violations
interface PolicyResult<T> {
isSuccess(): boolean;
isFailure(): boolean;
entity: T;
violations: PolicyViolation[];
metadata: Record<string, any>;
}Policy Context
Rich context for policy evaluation:
interface PolicyContext {
userId?: string;
sessionId?: string;
requestId?: string;
tenantId?: string;
timestamp: Date;
environment: string;
correlationId: string;
metadata: Record<string, any>;
}Policy Violation
Structured violation reporting:
interface PolicyViolation {
code: string;
message: string;
severity: 'ERROR' | 'WARNING' | 'INFO';
field?: string;
details?: Record<string, any>;
timestamp: Date;
}🚀 Quick Start
1. Basic Policy Creation
import { PolicyBuilder, PolicyContext } from '@vytches/ddd-policies';
import { AgeSpecification, EmailSpecification } from './specifications';
// Create a basic policy
const userPolicy = PolicyBuilder.create<User>()
.withId('user-validation')
.withDomain('authentication')
.withName('User Registration Policy')
.must(new AgeSpecification(18))
.withCode('AGE_TOO_LOW')
.withMessage('Must be at least 18 years old')
.withSeverity('ERROR')
.and()
.must(new EmailSpecification())
.withCode('INVALID_EMAIL')
.withMessage('Valid email required')
.withSeverity('ERROR')
.build();
// Execute policy
const user = new User('John Doe', '[email protected]', 25);
const context = PolicyContext.create()
.withUserId('user-123')
.withRequestId('req-456')
.build();
const result = await userPolicy.check({ entity: user, context });
if (result.isFailure()) {
console.log('Policy violations:', result.violations);
} else {
console.log('Policy passed successfully');
}2. Conditional Policies
import { PolicyBuilder } from '@vytches/ddd-policies';
// Policy with conditional logic
const orderPolicy = PolicyBuilder.create<Order>()
.withId('order-validation')
.withDomain('orders')
.withName('Order Processing Policy')
.must(new BasicOrderValidation())
.withCode('BASIC_VALIDATION_FAILED')
.when(order => order.amount > 10000)
.then()
.must(new ManagerApprovalSpec())
.withCode('APPROVAL_REQUIRED')
.withMessage('Manager approval required for large orders')
.withSeverity('ERROR')
.when(ctx => ctx.environment === 'production')
.then()
.must(new StrictSecurityPolicy())
.withCode('SECURITY_CHECK_FAILED')
.otherwise()
.should(new RelaxedValidation())
.withCode('RELAXED_VALIDATION')
.withSeverity('WARNING')
.build();
// Execute conditional policy
const order = new Order(customerId, items, 15000);
const result = await orderPolicy.check({ entity: order, context });3. Policy Groups
import { PolicyGroup } from '@vytches/ddd-policies';
// Create policy groups for complex logic
const excellentCreditGroup = PolicyGroup.create<LoanApplication>(
'excellent-credit'
).mustSatisfy(
app => app.creditScore >= 800,
'CREDIT_NOT_EXCELLENT',
'Excellent credit required'
);
const goodCreditWithCollateralGroup = PolicyGroup.create<LoanApplication>(
'good-credit-collateral'
)
.mustSatisfy(
app => app.creditScore >= 650,
'CREDIT_NOT_GOOD',
'Good credit required'
)
.and()
.mustSatisfy(
app => app.collateral >= 50000,
'INSUFFICIENT_COLLATERAL',
'Collateral required'
);
// Combine groups with OR logic
const loanPolicy = PolicyBuilder.create<LoanApplication>()
.withId('loan-approval')
.withDomain('lending')
.withName('Loan Approval Policy')
.shouldSatisfyAny(excellentCreditGroup, goodCreditWithCollateralGroup)
.build();
// Execute group policy
const loanApp = new LoanApplication(applicantId, requestedAmount, creditScore);
const result = await loanPolicy.check({ entity: loanApp, context });🔧 Policy Builder
Fluent API
// Complete fluent API example
const comprehensivePolicy = PolicyBuilder.create<User>()
.withId('comprehensive-user-policy')
.withDomain('user-management')
.withName('Comprehensive User Validation')
.withDescription(
'Complete user validation including age, email, and security checks'
)
.withVersion('1.2.0')
.withTags(['security', 'validation', 'user'])
// Basic validations
.must(new AgeSpecification(18))
.withCode('AGE_TOO_LOW')
.withMessage('User must be at least 18 years old')
.withSeverity('ERROR')
.withField('age')
.and()
.must(new EmailSpecification())
.withCode('INVALID_EMAIL')
.withMessage('Valid email address required')
.withSeverity('ERROR')
.withField('email')
// Conditional validations
.when(user => user.role === UserRole.ADMIN)
.then()
.must(new AdminSecuritySpecification())
.withCode('ADMIN_SECURITY_FAILED')
.withMessage('Admin users require additional security validation')
.withSeverity('ERROR')
// Warning-level validations
.should(new ProfileCompletnessSpecification())
.withCode('PROFILE_INCOMPLETE')
.withMessage('Consider completing your profile')
.withSeverity('WARNING')
// Events and audit
.withEvents({ enabled: true })
.withAuditTrail(true)
.build();Async Policies
// Async policy with external dependencies
const asyncPolicy = PolicyBuilder.create<User>()
.withId('async-user-validation')
.withDomain('user-management')
.mustAsync(new EmailUniquenessSpecification())
.withCode('EMAIL_NOT_UNIQUE')
.withMessage('Email address already exists')
.and()
.mustAsync(new ExternalBlacklistSpecification())
.withCode('USER_BLACKLISTED')
.withMessage('User is blacklisted')
.build();
// External specification example
class EmailUniquenessSpecification implements IAsyncSpecification<User> {
constructor(private readonly userRepository: IUserRepository) {}
async isSatisfiedByAsync(user: User): Promise<boolean> {
const existingUser = await this.userRepository.findByEmail(user.email);
return existingUser === null;
}
}Rule-Based Policies
// Policy with rule-based validation
const ruleBasedPolicy = PolicyBuilder.create<User>()
.withId('rule-based-user-policy')
.withDomain('user-management')
.mustSatisfyRules(rules =>
rules
.forProperty('name', Rules.required().minLength(2).maxLength(50))
.forProperty('email', Rules.required().email())
.forProperty('age', Rules.required().min(18).max(120))
.forProperty('phone', Rules.optional().phone())
)
.withCode('RULE_VALIDATION_FAILED')
.withMessage('User data validation failed')
.build();
// Execute rule-based policy
const user = new User('', 'invalid-email', 15); // Invalid data
const result = await ruleBasedPolicy.check({ entity: user, context });
// Result will contain specific field violations
result.violations.forEach(violation => {
console.log(`Field: ${violation.field}, Error: ${violation.message}`);
});⏰ Temporal Policies
Business Hours Support
import { PolicyTemporalBehavior } from '@vytches/ddd-policies';
// Create temporal policy with business hours
const temporalPolicy = PolicyTemporalBehavior.create(strictPolicy, {
businessHours: { start: '09:00', end: '17:00' },
workingDays: [1, 2, 3, 4, 5], // Monday to Friday
timezone: 'America/New_York',
});
// Alternative policies for different times
const businessHoursPolicy = PolicyTemporalBehaviorFactory.forBusinessHours(
strictPolicy,
relaxedPolicy,
{ start: '09:00', end: '17:00' }
);
// Execute temporal policy
const result = await temporalPolicy.check({ entity: order, context });Time-Based Conditions
// Policy with time-based conditions
const timeBasedPolicy = PolicyBuilder.create<Order>()
.withId('time-based-order-policy')
.withDomain('orders')
.when(ctx => {
const hour = ctx.timestamp.getHours();
return hour >= 9 && hour <= 17; // Business hours
})
.then()
.must(new StandardProcessingSpecification())
.withCode('STANDARD_PROCESSING')
.otherwise()
.must(new AfterHoursProcessingSpecification())
.withCode('AFTER_HOURS_PROCESSING')
.withMessage('After hours processing requires additional approval')
.build();
// Weekend vs weekday policies
const weekdayPolicy = PolicyBuilder.create<Transaction>()
.withId('weekday-transaction-policy')
.withDomain('transactions')
.when(ctx => {
const day = ctx.timestamp.getDay();
return day >= 1 && day <= 5; // Monday to Friday
})
.then()
.must(new StandardTransactionLimits())
.otherwise()
.must(new WeekendTransactionLimits())
.withCode('WEEKEND_LIMITS')
.withMessage('Weekend transactions have different limits')
.build();👥 Policy Groups
Complex Group Logic
import { PolicyGroup } from '@vytches/ddd-policies';
// Create complex group hierarchies
const creditCheckGroup = PolicyGroup.create<LoanApplication>('credit-check')
.mustSatisfy(
app => app.creditScore >= 650,
'CREDIT_SCORE_TOO_LOW',
'Minimum credit score of 650 required'
)
.and()
.mustSatisfy(
app => app.creditHistory.length >= 2,
'INSUFFICIENT_CREDIT_HISTORY',
'At least 2 years of credit history required'
);
const incomeVerificationGroup = PolicyGroup.create<LoanApplication>(
'income-verification'
)
.mustSatisfy(
app => app.annualIncome >= 50000,
'INCOME_TOO_LOW',
'Minimum annual income of $50,000 required'
)
.and()
.mustSatisfy(
app => app.employmentStatus === 'EMPLOYED',
'UNEMPLOYED',
'Applicant must be employed'
);
const collateralGroup = PolicyGroup.create<LoanApplication>(
'collateral'
).mustSatisfy(
app => app.collateralValue >= app.requestedAmount * 0.8,
'INSUFFICIENT_COLLATERAL',
'Collateral must be at least 80% of loan amount'
);
// Combine groups with complex logic
const loanApprovalPolicy = PolicyBuilder.create<LoanApplication>()
.withId('loan-approval-policy')
.withDomain('lending')
.withName('Loan Approval Policy')
// Must satisfy credit check AND income verification
.mustSatisfyAll(creditCheckGroup, incomeVerificationGroup)
// OR have sufficient collateral
.or()
.mustSatisfy(collateralGroup)
.build();Group Composition
// Compose groups for different scenarios
const individualApplicantGroups = PolicyGroup.create<LoanApplication>(
'individual-applicant'
).mustSatisfyAll(creditCheckGroup, incomeVerificationGroup);
const businessApplicantGroups = PolicyGroup.create<LoanApplication>(
'business-applicant'
)
.mustSatisfy(
app => app.businessRevenue >= 100000,
'BUSINESS_REVENUE_TOO_LOW',
'Business annual revenue must be at least $100,000'
)
.and()
.mustSatisfy(
app => app.businessAge >= 2,
'BUSINESS_TOO_NEW',
'Business must be operating for at least 2 years'
);
// Policy that handles different applicant types
const applicantTypePolicy = PolicyBuilder.create<LoanApplication>()
.withId('applicant-type-policy')
.withDomain('lending')
.when(app => app.applicantType === 'INDIVIDUAL')
.then()
.mustSatisfy(individualApplicantGroups)
.when(app => app.applicantType === 'BUSINESS')
.then()
.mustSatisfy(businessApplicantGroups)
.build();🔄 Policy Behaviors
Retry Behavior
import { PolicyRetryBehavior } from '@vytches/ddd-policies';
// Policy with retry logic for transient failures
const retryPolicy = PolicyRetryBehavior.create(basePolicy, {
maxAttempts: 3,
baseDelay: 1000,
backoff: 'exponential',
shouldRetry: violation => violation.code.includes('TIMEOUT'),
onRetry: (attempt, violation) => {
console.log(`Retry attempt ${attempt} for violation: ${violation.code}`);
},
});
// Factory methods for common scenarios
const transientFailurePolicy = PolicyRetryBehaviorFactory.forTransientFailures(
basePolicy,
3 // max attempts
);
const externalServicePolicy = PolicyRetryBehaviorFactory.forExternalServices(
basePolicy,
{ maxAttempts: 5, baseDelay: 2000, maxDelay: 60000 }
);
// Execute retry policy
const result = await retryPolicy.check({ entity: user, context });Caching Behavior
import { PolicyCachingBehavior } from '@vytches/ddd-policies';
// Policy with caching for performance
const cachedPolicy = PolicyCachingBehavior.create(basePolicy, {
ttl: 60000, // 1 minute
keyGenerator: request => `${request.entity.id}_${request.context.userId}`,
namespace: 'user-validation',
maxSize: 500,
});
// Simple TTL-based caching
const simpleCachedPolicy = PolicyCachingBehaviorFactory.withTTL(
basePolicy,
30000 // 30 seconds
);
// Execute cached policy
const result = await cachedPolicy.check({ entity: user, context });Temporal Behavior
import { PolicyTemporalBehavior } from '@vytches/ddd-policies';
// Time-aware policy behavior
const temporalPolicy = PolicyTemporalBehavior.create(basePolicy, {
businessHours: { start: '09:00', end: '17:00' },
workingDays: [1, 2, 3, 4, 5],
timezone: 'America/New_York',
duringBusinessHours: strictPolicy,
duringAfterHours: relaxedPolicy,
duringWeekends: weekendPolicy,
});
// Factory methods
const businessHoursPolicy = PolicyTemporalBehaviorFactory.forBusinessHours(
strictPolicy,
relaxedPolicy,
{ start: '09:00', end: '17:00' }
);
const workingDaysPolicy = PolicyTemporalBehaviorFactory.forWorkingDays(
businessPolicy,
weekendPolicy,
[1, 2, 3, 4, 5]
);Behavior Composition
// Chain multiple behaviors
const composedPolicy = PolicyCachingBehavior.create(
PolicyRetryBehavior.create(
PolicyTemporalBehavior.create(basePolicy, temporalConfig),
retryConfig
),
cacheConfig
);
// Execution order: Cache → Retry → Temporal → Base Policy
const result = await composedPolicy.check({ entity: user, context });📋 Policy Registry
Registration and Discovery
import { PolicyRegistry } from '@vytches/ddd-policies';
// Create policy registry
const registry = new PolicyRegistry();
// Register policies
registry.register({
id: 'user-validation',
domain: 'authentication',
name: 'User Validation Policy',
description: 'Validates user registration data',
version: '1.0.0',
policy: userPolicy,
tags: ['security', 'validation'],
metadata: {
author: 'Security Team',
lastUpdated: new Date(),
},
});
// Register with factory
registry.registerFactory({
id: 'dynamic-loan-policy',
domain: 'lending',
name: 'Dynamic Loan Policy',
factory: (context: PolicyContext) => {
// Create policy based on context
return context.environment === 'production'
? strictLoanPolicy
: relaxedLoanPolicy;
},
});Policy Resolution
// Resolve policies
const policy = registry.resolve<User>({
domain: 'authentication',
id: 'user-validation',
});
// Resolve with context
const contextualPolicy = registry.resolveWithContext<LoanApplication>(
{
domain: 'lending',
id: 'dynamic-loan-policy',
},
context
);
// Query policies
const authenticationPolicies = registry.findByDomain('authentication');
const securityPolicies = registry.findByTag('security');
const userPolicies = registry.findByPattern('user-*');Policy Metadata
// Get policy metadata
const metadata = registry.getMetadata('user-validation');
console.log(metadata.name, metadata.version, metadata.tags);
// List all policies
const allPolicies = registry.listPolicies();
allPolicies.forEach(policy => {
console.log(`${policy.domain}:${policy.id} - ${policy.name}`);
});
// Policy statistics
const stats = registry.getStatistics();
console.log(`Total policies: ${stats.totalPolicies}`);
console.log(`Domains: ${stats.domains.join(', ')}`);🚨 Violation Handling
Violation Structure
// Comprehensive violation information
interface PolicyViolation {
code: string;
message: string;
severity: 'ERROR' | 'WARNING' | 'INFO';
field?: string;
details?: Record<string, any>;
timestamp: Date;
context?: {
policyId: string;
domain: string;
ruleName?: string;
};
}Violation Processing
// Process violations by severity
const result = await policy.check({ entity: user, context });
if (result.isFailure()) {
const violations = result.violations;
// Separate by severity
const errors = violations.filter(v => v.severity === 'ERROR');
const warnings = violations.filter(v => v.severity === 'WARNING');
const info = violations.filter(v => v.severity === 'INFO');
// Handle errors
if (errors.length > 0) {
throw new PolicyViolationError('Policy validation failed', errors);
}
// Log warnings
warnings.forEach(warning => {
console.warn(`Warning: ${warning.message}`, warning.details);
});
// Process info
info.forEach(infoItem => {
console.info(`Info: ${infoItem.message}`, infoItem.details);
});
}Custom Violation Handlers
// Custom violation handler
class PolicyViolationHandler {
async handleViolations(violations: PolicyViolation[]): Promise<void> {
for (const violation of violations) {
switch (violation.severity) {
case 'ERROR':
await this.handleError(violation);
break;
case 'WARNING':
await this.handleWarning(violation);
break;
case 'INFO':
await this.handleInfo(violation);
break;
}
}
}
private async handleError(violation: PolicyViolation): Promise<void> {
// Log error
this.logger.error('Policy violation', violation);
// Send notification
await this.notificationService.sendAlert({
type: 'policy_violation',
severity: 'error',
message: violation.message,
details: violation.details,
});
// Update metrics
this.metrics.incrementCounter('policy_violations_error', {
code: violation.code,
domain: violation.context?.domain,
});
}
private async handleWarning(violation: PolicyViolation): Promise<void> {
// Log warning
this.logger.warn('Policy warning', violation);
// Update metrics
this.metrics.incrementCounter('policy_violations_warning', {
code: violation.code,
});
}
private async handleInfo(violation: PolicyViolation): Promise<void> {
// Log info
this.logger.info('Policy info', violation);
}
}🔧 Advanced Usage
Multi-Tenant Policies
// Tenant-aware policy
const tenantPolicy = PolicyBuilder.create<User>()
.withId('tenant-user-policy')
.withDomain('user-management')
.when(ctx => ctx.tenantId === 'enterprise-tenant')
.then()
.must(new EnterpriseUserSpecification())
.withCode('ENTERPRISE_USER_REQUIRED')
.when(ctx => ctx.tenantId === 'standard-tenant')
.then()
.must(new StandardUserSpecification())
.withCode('STANDARD_USER_REQUIRED')
.build();
// Tenant-specific registry
const tenantRegistry = new PolicyRegistry();
tenantRegistry.registerTenantPolicy('enterprise-tenant', enterprisePolicy);
tenantRegistry.registerTenantPolicy('standard-tenant', standardPolicy);
// Resolve tenant-specific policy
const tenantSpecificPolicy = tenantRegistry.resolveTenantPolicy<User>(
'enterprise-tenant',
'user-validation'
);Event-Driven Policies
// Policy with event emission
const eventDrivenPolicy = PolicyBuilder.create<User>()
.withId('event-driven-user-policy')
.withDomain('user-management')
.withEvents({ enabled: true })
.must(new UserSpecification())
.withCode('USER_VALIDATION_FAILED')
.build();
// Listen to policy events
policyEventBus.subscribe('POLICY_EVALUATED', (event: PolicyEvaluatedEvent) => {
console.log(`Policy ${event.policyId} evaluated`);
console.log(`Result: ${event.result.isSuccess() ? 'SUCCESS' : 'FAILURE'}`);
console.log(`Violations: ${event.result.violations.length}`);
});
policyEventBus.subscribe('POLICY_VIOLATION', (event: PolicyViolationEvent) => {
console.log(`Policy violation: ${event.violation.code}`);
console.log(`Severity: ${event.violation.severity}`);
});Policy Composition
// Compose policies from multiple sources
const composedPolicy = PolicyComposer.create<User>()
.withId('composed-user-policy')
.withDomain('user-management')
.addPolicy(basicUserPolicy)
.addPolicy(securityPolicy)
.addPolicy(compliancePolicy)
.withStrategy('ALL_MUST_PASS') // or 'ANY_MUST_PASS'
.build();
// Conditional composition
const conditionalComposition = PolicyComposer.create<Order>()
.withId('conditional-order-policy')
.withDomain('orders')
.when(order => order.amount > 10000)
.addPolicy(largeOrderPolicy)
.when(order => order.region === 'EU')
.addPolicy(gdprCompliancePolicy)
.build();🔗 Integration Patterns
CQRS Integration
// Command handler with policy validation
@CommandHandler(CreateUserCommand)
export class CreateUserCommandHandler {
constructor(
private readonly userRepository: IUserRepository,
private readonly policyRegistry: PolicyRegistry
) {}
async handle(
command: CreateUserCommand,
context: ExecutionContext
): Promise<void> {
// Resolve policy
const policy = this.policyRegistry.resolve<User>({
domain: 'user-management',
id: 'user-creation-policy',
});
// Create user entity
const user = User.create(command.name, command.email, command.age);
// Validate with policy
const policyContext = PolicyContext.create()
.withUserId(context.userId)
.withRequestId(context.requestId)
.withTenantId(context.tenantId)
.build();
const policyResult = await policy.check({
entity: user,
context: policyContext,
});
if (policyResult.isFailure()) {
throw new PolicyViolationError(
'User creation policy failed',
policyResult.violations
);
}
// Save user
await this.userRepository.save(user);
}
}Domain Service Integration
// Domain service with policy validation
class UserRegistrationService implements IDomainService {
readonly serviceName = 'UserRegistrationService';
constructor(
private readonly userRepository: IUserRepository,
private readonly policyRegistry: PolicyRegistry,
private readonly emailService: IEmailService
) {}
async registerUser(userData: UserRegistrationData): Promise<User> {
// Create user
const user = User.create(userData.name, userData.email, userData.age);
// Validate with policies
const policies = this.policyRegistry.findByDomain('user-management');
for (const policyInfo of policies) {
const policy = this.policyRegistry.resolve<User>(policyInfo);
const result = await policy.check({
entity: user,
context: userData.context,
});
if (result.isFailure()) {
throw new PolicyViolationError(
`Policy ${policyInfo.id} failed`,
result.violations
);
}
}
// Save user
await this.userRepository.save(user);
// Send welcome email
await this.emailService.sendWelcomeEmail(user.email);
return user;
}
}🧪 Testing
Policy Testing
import { describe, it, expect, beforeEach } from 'vitest';
import { PolicyBuilder, PolicyContext } from '@vytches/ddd-policies';
describe('UserValidationPolicy', () => {
let policy: IPolicy<User>;
let context: PolicyContext;
beforeEach(() => {
policy = PolicyBuilder.create<User>()
.withId('test-user-policy')
.withDomain('testing')
.must(new AgeSpecification(18))
.withCode('AGE_TOO_LOW')
.and()
.must(new EmailSpecification())
.withCode('INVALID_EMAIL')
.build();
context = PolicyContext.create()
.withUserId('test-user')
.withRequestId('test-request')
.build();
});
it('should pass with valid user', async () => {
// Arrange
const user = new User('John Doe', '[email protected]', 25);
// Act
const result = await policy.check({ entity: user, context });
// Assert
expect(result.isSuccess()).toBe(true);
expect(result.violations).toHaveLength(0);
});
it('should fail with underage user', async () => {
// Arrange
const user = new User('John Doe', '[email protected]', 17);
// Act
const result = await policy.check({ entity: user, context });
// Assert
expect(result.isFailure()).toBe(true);
expect(result.violations).toHaveLength(1);
expect(result.violations[0].code).toBe('AGE_TOO_LOW');
});
it('should fail with invalid email', async () => {
// Arrange
const user = new User('John Doe', 'invalid-email', 25);
// Act
const result = await policy.check({ entity: user, context });
// Assert
expect(result.isFailure()).toBe(true);
expect(result.violations).toHaveLength(1);
expect(result.violations[0].code).toBe('INVALID_EMAIL');
});
});Policy Behavior Testing
describe('PolicyRetryBehavior', () => {
let basePolicy: IPolicy<User>;
let retryPolicy: IPolicy<User>;
beforeEach(() => {
basePolicy = createMockPolicy();
retryPolicy = PolicyRetryBehavior.create(basePolicy, {
maxAttempts: 3,
baseDelay: 100,
shouldRetry: violation => violation.code === 'TRANSIENT_ERROR',
});
});
it('should retry on transient failures', async () => {
// Arrange
const user = new User('John Doe', '[email protected]', 25);
basePolicy.check
.mockResolvedValueOnce(
PolicyResult.failure([
new PolicyViolation('TRANSIENT_ERROR', 'Temporary failure', 'ERROR'),
])
)
.mockResolvedValueOnce(PolicyResult.success(user));
// Act
const result = await retryPolicy.check({ entity: user, context });
// Assert
expect(result.isSuccess()).toBe(true);
expect(basePolicy.check).toHaveBeenCalledTimes(2);
});
});Mock Utilities
// Mock policy for testing
function createMockPolicy<T>(): jest.Mocked<IPolicy<T>> {
return {
check: jest.fn(),
getId: jest.fn().mockReturnValue('mock-policy'),
getDomain: jest.fn().mockReturnValue('mock-domain'),
getName: jest.fn().mockReturnValue('Mock Policy'),
};
}
// Test builder for policies
class PolicyTestBuilder<T> {
private policy: IPolicy<T>;
static create<T>(): PolicyTestBuilder<T> {
return new PolicyTestBuilder<T>();
}
withPolicy(policy: IPolicy<T>): PolicyTestBuilder<T> {
this.policy = policy;
return this;
}
withSuccessResult(entity: T): PolicyTestBuilder<T> {
(this.policy.check as jest.Mock).mockResolvedValue(
PolicyResult.success(entity)
);
return this;
}
withFailureResult(violations: PolicyViolation[]): PolicyTestBuilder<T> {
(this.policy.check as jest.Mock).mockResolvedValue(
PolicyResult.failure(violations)
);
return this;
}
build(): jest.Mocked<IPolicy<T>> {
return this.policy as jest.Mocked<IPolicy<T>>;
}
}🏆 Best Practices
Policy Design
// ✅ Good: Specific, focused policies
const userAgePolicy = PolicyBuilder.create<User>()
.withId('user-age-policy')
.withDomain('user-management')
.must(new AgeSpecification(18))
.withCode('AGE_TOO_LOW')
.withMessage('User must be at least 18 years old')
.build();
// ❌ Bad: Overly complex, monolithic policy
const everythingPolicy = PolicyBuilder.create<User>()
.withId('everything-policy')
.withDomain('everything')
.must(new AgeSpecification(18))
.and()
.must(new EmailSpecification())
.and()
.must(new AddressSpecification())
.and()
.must(new PaymentSpecification())
// ... too many responsibilities
.build();Error Handling
// ✅ Good: Specific error handling
try {
const result = await policy.check({ entity: user, context });
if (result.isFailure()) {
const errorViolations = result.violations.filter(
v => v.severity === 'ERROR'
);
if (errorViolations.length > 0) {
throw new PolicyViolationError(
'Policy validation failed',
errorViolations
);
}
}
} catch (error) {
if (error instanceof PolicyViolationError) {
// Handle policy violations
this.handlePolicyViolations(error.violations);
} else {
// Handle other errors
this.handleGeneralError(error);
}
}
// ❌ Bad: Generic error handling
try {
await policy.check({ entity: user, context });
} catch (error) {
throw error; // No specific handling
}Context Usage
// ✅ Good: Rich context
const context = PolicyContext.create()
.withUserId(currentUser.id)
.withSessionId(session.id)
.withRequestId(request.id)
.withTenantId(tenant.id)
.withEnvironment(process.env.NODE_ENV)
.withMetadata({
userAgent: request.headers['user-agent'],
ipAddress: request.ip,
feature: 'user-registration',
})
.build();
// ❌ Bad: Minimal context
const context = PolicyContext.create().build();🤝 Contributing
We welcome contributions! Please see our Contributing Guide for details.
Development Setup
# Clone repository
git clone https://github.com/vytches/ddd.git
cd ddd
# Install dependencies
pnpm install
# Build package
pnpm build --filter=@vytches/ddd-policies
# Run tests
pnpm test --filter=@vytches/ddd-policies
# Run in development mode
pnpm dev --filter=@vytches/ddd-policies📄 License
This project is licensed under the MIT License - see the LICENSE file for details.
Part of the @vytches/ddd-core ecosystem
For more information, visit the main documentation.
