@isa/common/decorators
A comprehensive collection of TypeScript decorators for enhancing method behavior in Angular applications. This library provides decorators for validation, caching, debouncing, rate limiting, and more.
Installation
This library is already configured in the project's tsconfig.base.json. Import decorators using:
import {
// Validation decorators
ValidateParams, ValidateParam, ZodValidationError,
// Caching decorators
Cache, CacheTimeToLive,
// Rate limiting decorators
Debounce, InFlight, InFlightWithKey, InFlightWithCache
} from '@isa/common/decorators';
Available Decorators
🛡️ Validation Decorators
ValidateParams
Method decorator that validates method parameters using Zod schemas at runtime.
Features:
- Runtime type validation with Zod
- Custom parameter names for better error messages
- Schema transformations and default values
- Async method support
- Selective parameter validation
import { ValidateParams, ZodValidationError } from '@isa/common/decorators';
import { z } from 'zod';
@Injectable()
class UserService {
@ValidateParams([
z.string().email(),
z.object({
name: z.string().min(1),
age: z.number().int().min(0).max(120)
})
], ['email', 'userData'])
async createUser(email: string, userData: UserData): Promise<User> {
// Method implementation - parameters are validated before execution
return await this.apiClient.createUser(email, userData);
}
}
// Usage with error handling
try {
await userService.createUser('invalid-email', userData);
} catch (error) {
if (error instanceof ZodValidationError) {
console.log(`Validation failed: ${error.message}`);
console.log('Parameter:', error.parameterName);
console.log('Details:', error.zodError.issues);
}
}
ValidateParam
Method decorator for validating a single parameter at a specific index.
@Injectable()
class EmailService {
@ValidateParam(0, z.string().email(), 'emailAddress')
async sendEmail(email: string, subject: string): Promise<void> {
// Only the first parameter (email) is validated
return await this.emailClient.send(email, subject);
}
}
Advanced Validation Examples:
class AdvancedValidationService {
// Complex object validation with transformations
@ValidateParams([
z.string().transform(s => s.trim().toLowerCase()),
z.array(z.string().uuid()).min(1),
z.object({
priority: z.enum(['low', 'medium', 'high']).default('medium'),
retries: z.number().int().min(0).max(5).default(3)
}).optional()
])
processRequest(name: string, ids: string[], options?: RequestOptions): void {
// name is trimmed and lowercase, options have defaults applied
}
// Skip validation for specific parameters
@ValidateParams([
z.string().email(), // Validate first parameter
undefined, // Skip second parameter
z.number().positive() // Validate third parameter
])
mixedValidation(email: string, metadata: any, count: number): void {
// email and count validated, metadata can be anything
}
}
📦 Caching Decorators
Cache
Method decorator that caches the results of both synchronous and asynchronous method calls.
Features:
- Automatic caching based on method arguments
- TTL (Time To Live) support with predefined constants
- Custom cache key generation
- Separate cache per instance
- Error handling (errors are not cached)
import { Cache, CacheTimeToLive } from '@isa/common/decorators';
@Injectable()
class DataService {
@Cache({
ttl: CacheTimeToLive.fiveMinutes,
keyGenerator: (query: string) => `search-${query}`
})
async searchData(query: string): Promise<SearchResult[]> {
// Expensive API call - results cached for 5 minutes
return await this.apiClient.search(query);
}
@Cache({ ttl: CacheTimeToLive.oneHour })
calculateComplexResult(input: number): number {
// Expensive calculation - cached for 1 hour
return this.performHeavyCalculation(input);
}
}
Cache TTL Constants:
CacheTimeToLive.oneMinute(60,000ms)CacheTimeToLive.fiveMinutes(300,000ms)CacheTimeToLive.tenMinutes(600,000ms)CacheTimeToLive.thirtyMinutes(1,800,000ms)CacheTimeToLive.oneHour(3,600,000ms)
⏱️ Rate Limiting Decorators
Debounce
Method decorator that debounces method calls using lodash's debounce function.
Features:
- Configurable wait time
- Leading and trailing edge execution
- Maximum wait time limits
- Separate debounced functions per instance
import { Debounce } from '@isa/common/decorators';
@Injectable()
class SearchService {
@Debounce({ wait: 300 })
performSearch(query: string): void {
// Called only after 300ms of no additional calls
console.log('Searching for:', query);
}
@Debounce({
wait: 500,
leading: true,
trailing: false
})
saveData(data: any): void {
// Executes immediately on first call, then debounces
console.log('Saving:', data);
}
@Debounce({
wait: 1000,
maxWait: 5000
})
autoSave(): void {
// Forces execution after 5 seconds even with continuous calls
console.log('Auto-saving...');
}
}
🚀 InFlight Decorators
Prevent multiple simultaneous calls to the same async method. All concurrent calls receive the same Promise result.
Basic Usage
import { InFlight } from '@isa/common/decorators';
@Injectable()
class DataService {
@InFlight()
async fetchData(): Promise<Data> {
// Even if called multiple times simultaneously,
// only one API call will be made
return await this.http.get<Data>('/api/data').toPromise();
}
}
Benefits:
- Prevents duplicate API calls
- Reduces server load
- Improves application performance
- All callers receive the same result
🔑 InFlightWithKey
Prevents duplicate calls while considering method arguments. Each unique set of arguments gets its own in-flight tracking.
import { InFlightWithKey } from '@isa/common/decorators';
@Injectable()
class UserService {
@InFlightWithKey({
keyGenerator: (userId: string) => userId
})
async fetchUser(userId: string): Promise<User> {
// Multiple calls with same userId share the same request
// Different userIds can execute simultaneously
return await this.http.get<User>(`/api/users/${userId}`).toPromise();
}
@InFlightWithKey() // Uses JSON.stringify by default
async searchUsers(query: string, page: number): Promise<User[]> {
return await this.http.get<User[]>(`/api/users/search`, {
params: { query, page: page.toString() }
}).toPromise();
}
}
Configuration Options:
keyGenerator?: (...args) => string- Custom key generation function- If not provided, uses
JSON.stringify(args)as the key
🗄️ InFlightWithCache
Combines in-flight request deduplication with result caching.
import { InFlightWithCache } from '@isa/common/decorators';
@Injectable()
class ProductService {
@InFlightWithCache({
cacheTime: 5 * 60 * 1000, // Cache for 5 minutes
keyGenerator: (productId: string) => productId
})
async getProduct(productId: string): Promise<Product> {
// Results are cached for 5 minutes
// Multiple calls within cache time return cached result
return await this.http.get<Product>(`/api/products/${productId}`).toPromise();
}
}
Configuration Options:
cacheTime?: number- Cache duration in millisecondskeyGenerator?: (...args) => string- Custom key generation function
How It Works
Memory Management
All decorators use WeakMap for memory efficiency:
- Automatic garbage collection when instances are destroyed
- No memory leaks
- Per-instance state isolation
Error Handling
- Failed requests are not cached
- In-flight tracking is cleaned up on both success and error
- All concurrent callers receive the same error
Thread Safety
- Decorators are instance-aware
- Each service instance has its own in-flight tracking
- No shared state between instances
Real-World Examples
Solving Your Original Problem
// Before: Multiple simultaneous calls
@Injectable({ providedIn: 'root' })
export class RemissionProductGroupService {
async fetchProductGroups(): Promise<KeyValueStringAndString[]> {
// Multiple calls = multiple API requests
return await this.apiCall();
}
}
// After: Using InFlight decorator
@Injectable({ providedIn: 'root' })
export class RemissionProductGroupService {
@InFlight()
async fetchProductGroups(): Promise<KeyValueStringAndString[]> {
// Multiple simultaneous calls = single API request
return await this.apiCall();
}
}
Advanced Scenarios
@Injectable()
class OrderService {
// Different cache times for different data types
@InFlightWithCache({ cacheTime: 30 * 1000 }) // 30 seconds
async getOrderStatus(orderId: string): Promise<OrderStatus> {
return await this.http.get<OrderStatus>(`/api/orders/${orderId}/status`).toPromise();
}
@InFlightWithCache({ cacheTime: 10 * 60 * 1000 }) // 10 minutes
async getOrderHistory(customerId: string): Promise<Order[]> {
return await this.http.get<Order[]>(`/api/customers/${customerId}/orders`).toPromise();
}
// Custom key generation for complex parameters
@InFlightWithKey({
keyGenerator: (filter: OrderFilter) =>
`${filter.status}-${filter.dateFrom}-${filter.dateTo}`
})
async searchOrders(filter: OrderFilter): Promise<Order[]> {
return await this.http.post<Order[]>('/api/orders/search', filter).toPromise();
}
}
Advanced Usage Examples
Combining Decorators
Decorators can be combined for powerful behavior:
@Injectable()
class AdvancedService {
@Cache({ ttl: CacheTimeToLive.tenMinutes })
@Debounce({ wait: 500 })
@ValidateParams([z.string().min(1)], ['query'])
async searchWithCacheAndDebounce(query: string): Promise<SearchResult[]> {
// 1. Parameter is validated
// 2. Call is debounced by 500ms
// 3. Result is cached for 10 minutes
return await this.performSearch(query);
}
@InFlight()
@ValidateParams([
z.string().uuid(),
z.object({
retries: z.number().int().min(0).max(5).default(3),
timeout: z.number().positive().default(5000)
}).optional()
], ['operationId', 'options'])
async executeOperation(
operationId: string,
options?: { retries?: number; timeout?: number }
): Promise<void> {
// 1. Parameters are validated with defaults applied
// 2. Only one execution per instance allowed
await this.performOperation(operationId, options);
}
}
Complex Real-World Examples
@Injectable()
class ProductService {
// Comprehensive product search with validation, debouncing, and caching
@Cache({ ttl: CacheTimeToLive.fiveMinutes })
@Debounce({ wait: 300 })
@ValidateParams([
z.object({
query: z.string().min(1).transform(s => s.trim()),
category: z.string().optional(),
priceRange: z.object({
min: z.number().min(0),
max: z.number().positive()
}).optional(),
sortBy: z.enum(['price', 'name', 'rating']).default('name'),
page: z.number().int().min(1).default(1)
})
], ['searchParams'])
async searchProducts(params: ProductSearchParams): Promise<ProductResults> {
return await this.apiClient.searchProducts(params);
}
// Bulk operations with validation and in-flight protection
@InFlightWithKey({
keyGenerator: (operation: string) => `bulk-${operation}`
})
@ValidateParams([
z.enum(['update', 'delete', 'activate', 'deactivate']),
z.array(z.string().uuid()).min(1).max(100),
z.object({
batchSize: z.number().int().positive().max(50).default(10),
delayMs: z.number().min(0).default(100)
}).optional()
], ['operation', 'productIds', 'options'])
async bulkOperation(
operation: BulkOperationType,
productIds: string[],
options?: BulkOptions
): Promise<BulkOperationResult> {
return await this.performBulkOperation(operation, productIds, options);
}
}
@Injectable()
class UserManagementService {
// User creation with comprehensive validation
@ValidateParams([
z.object({
email: z.string().email().transform(e => e.toLowerCase()),
name: z.string().min(1).max(100).transform(n => n.trim()),
role: z.enum(['admin', 'user', 'manager']).default('user'),
permissions: z.array(z.string()).default([]),
metadata: z.record(z.string()).optional()
}),
z.object({
sendWelcomeEmail: z.boolean().default(true),
skipValidation: z.boolean().default(false),
auditLog: z.boolean().default(true)
}).optional()
], ['userData', 'options'])
async createUser(
userData: CreateUserData,
options?: CreateUserOptions
): Promise<User> {
// All parameters are validated and transformed
return await this.userRepository.create(userData, options);
}
// Password reset with rate limiting and caching
@Cache({ ttl: CacheTimeToLive.oneMinute }) // Prevent spam
@Debounce({ wait: 2000, leading: true, trailing: false })
@ValidateParams([z.string().email()], ['email'])
async initiatePasswordReset(email: string): Promise<void> {
await this.authService.sendPasswordResetEmail(email);
}
}
Error Handling Patterns
@Injectable()
class RobustService {
@Cache({ ttl: CacheTimeToLive.fiveMinutes })
@InFlight()
@ValidateParams([z.string().url()], ['endpoint'])
async fetchWithRetry(endpoint: string): Promise<ApiResponse> {
try {
const response = await this.httpClient.get(endpoint);
return response.data;
} catch (error) {
// Errors are not cached, in-flight tracking is cleaned up
console.error('API call failed:', error);
throw new Error(`Failed to fetch data from ${endpoint}: ${error.message}`);
}
}
// Graceful error handling in validation
async processUserInput(input: unknown): Promise<ProcessResult> {
try {
// Manual validation for dynamic scenarios
const validatedInput = z.object({
action: z.enum(['create', 'update', 'delete']),
data: z.record(z.any())
}).parse(input);
return await this.processValidatedInput(validatedInput);
} catch (error) {
if (error instanceof ZodValidationError) {
return {
success: false,
error: 'Invalid input format',
details: error.zodError.issues
};
}
throw error;
}
}
@ValidateParams([
z.object({
action: z.enum(['create', 'update', 'delete']),
data: z.record(z.any())
})
])
async processValidatedInput(input: ProcessInput): Promise<ProcessResult> {
// Input is guaranteed to be valid
return { success: true, result: await this.execute(input) };
}
}
Best Practices
✅ Validation Best Practices
- Use descriptive parameter names for better error messages
- Define schemas separately for complex validations and reuse them
- Use transformations to normalize input data (trim, lowercase, etc.)
- Provide defaults for optional parameters using Zod's
.default() - Validate at service boundaries (API endpoints, user input handlers)
// ✅ Good: Reusable schema with descriptive names
const UserSchema = z.object({
email: z.string().email().transform(e => e.toLowerCase()),
name: z.string().min(1).transform(n => n.trim())
});
@ValidateParams([UserSchema], ['userData'])
createUser(userData: UserData): Promise<User> { }
// ❌ Avoid: Inline complex schemas without parameter names
@ValidateParams([z.object({ /* complex schema */ })])
createUser(userData: any): Promise<User> { }
✅ Caching Best Practices
- Use appropriate TTL based on data volatility
- Custom key generators for complex parameters
- Cache expensive operations only
- Don't cache operations with side effects
// ✅ Good: Appropriate TTL and custom key generation
@Cache({
ttl: CacheTimeToLive.fiveMinutes,
keyGenerator: (filter: SearchFilter) =>
`search-${filter.category}-${filter.sortBy}-${filter.page}`
})
searchProducts(filter: SearchFilter): Promise<Product[]> { }
// ❌ Avoid: Caching operations with side effects
@Cache()
async sendEmail(to: string): Promise<void> { } // Don't cache this!
✅ Debouncing Best Practices
- Debounce user input handlers (search, form validation)
- Use leading edge for immediate feedback actions
- Set maxWait for critical operations that must eventually execute
- Consider user experience - don't make interactions feel sluggish
// ✅ Good: User input debouncing
@Debounce({ wait: 300 })
onSearchInputChange(query: string): void { }
// ✅ Good: Button click with immediate feedback
@Debounce({ wait: 1000, leading: true, trailing: false })
onSaveButtonClick(): void { }
✅ InFlight Best Practices
- Use for expensive async operations to prevent duplicates
- Use InFlightWithKey when parameters affect the result
- Use InFlightWithCache for data that doesn't change frequently
- Don't use on methods with side effects unless intentional
✅ Do
- Use
@InFlight()for simple methods without parameters - Use
@InFlightWithKey()for methods with parameters - Use
@InFlightWithCache()for expensive operations with stable results - Provide custom
keyGeneratorfor complex parameter objects - Set appropriate cache times based on data volatility
❌ Don't
- Use on methods that return different results for the same input
- Use excessively long cache times for dynamic data
- Use on methods that have side effects (POST, PUT, DELETE)
- Rely on argument order for default key generation
Performance Considerations
Memory Usage
InFlight: Minimal memory overhead (one Promise per instance)InFlightWithKey: Memory usage scales with unique parameter combinationsInFlightWithCache: Additional memory for cached results
Cleanup
- In-flight requests are automatically cleaned up on completion
- Cache entries are cleaned up on expiry
- WeakMap ensures instances can be garbage collected
Testing
The decorators are fully tested with comprehensive unit tests. Key test scenarios include:
- Multiple simultaneous calls deduplication
- Error handling and cleanup
- Cache expiration
- Instance isolation
- Key generation
Run tests with:
npx nx test common-decorators
Migration Guide
From Manual Implementation
// Before: Manual in-flight tracking
class MyService {
private inFlight: Promise<Data> | null = null;
async fetchData(): Promise<Data> {
if (this.inFlight) {
return this.inFlight;
}
this.inFlight = this.doFetch();
try {
return await this.inFlight;
} finally {
this.inFlight = null;
}
}
}
// After: Using decorator
class MyService {
@InFlight()
async fetchData(): Promise<Data> {
return await this.doFetch();
}
}
From RxJS shareReplay
// Before: RxJS approach
class MyService {
private data$ = this.http.get<Data>('/api/data').pipe(
shareReplay({ bufferSize: 1, refCount: true })
);
getData(): Observable<Data> {
return this.data$;
}
}
// After: Promise-based with decorator
class MyService {
@InFlightWithCache({ cacheTime: 5 * 60 * 1000 })
async getData(): Promise<Data> {
return await this.http.get<Data>('/api/data').toPromise();
}
}
Testing Decorators
When testing methods with decorators, consider the decorator behavior:
import { ZodValidationError } from '@isa/common/decorators';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
jest.clearAllMocks();
});
describe('validation', () => {
it('should validate email parameter', () => {
// Test valid input
expect(() => service.createUser('valid@email.com', userData))
.not.toThrow();
// Test invalid input
expect(() => service.createUser('invalid-email', userData))
.toThrow(ZodValidationError);
});
it('should provide detailed error information', () => {
try {
service.createUser('invalid-email', userData);
} catch (error) {
expect(error).toBeInstanceOf(ZodValidationError);
expect(error.parameterName).toBe('email');
expect(error.parameterIndex).toBe(0);
}
});
});
describe('caching', () => {
it('should cache expensive operations', async () => {
const expensiveOperationSpy = jest.spyOn(service, 'expensiveOperation');
// First call
await service.cachedMethod('param');
expect(expensiveOperationSpy).toHaveBeenCalledTimes(1);
// Second call with same parameters (should use cache)
await service.cachedMethod('param');
expect(expensiveOperationSpy).toHaveBeenCalledTimes(1);
// Third call with different parameters (should not use cache)
await service.cachedMethod('different');
expect(expensiveOperationSpy).toHaveBeenCalledTimes(2);
});
});
describe('debouncing', () => {
it('should debounce method calls', (done) => {
const debouncedSpy = jest.spyOn(service, 'actualMethod');
// Call multiple times rapidly
service.debouncedMethod('param1');
service.debouncedMethod('param2');
service.debouncedMethod('param3');
// Should not have been called yet
expect(debouncedSpy).not.toHaveBeenCalled();
// Wait for debounce period
setTimeout(() => {
expect(debouncedSpy).toHaveBeenCalledTimes(1);
expect(debouncedSpy).toHaveBeenCalledWith('param3');
done();
}, 350); // Assuming 300ms debounce
});
});
describe('in-flight protection', () => {
it('should prevent duplicate async calls', async () => {
const apiCallSpy = jest.spyOn(service, 'apiCall');
// Start multiple calls simultaneously
const promises = [
service.inFlightMethod(),
service.inFlightMethod(),
service.inFlightMethod()
];
await Promise.all(promises);
// Should only have made one actual API call
expect(apiCallSpy).toHaveBeenCalledTimes(1);
});
});
});
Testing with Vitest
For projects using Vitest (new standard):
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { ZodValidationError } from '@isa/common/decorators';
describe('UserService', () => {
let service: UserService;
beforeEach(() => {
service = new UserService();
vi.clearAllMocks();
});
it('should validate parameters', () => {
expect(() => service.validateMethod('valid-input')).not.toThrow();
expect(() => service.validateMethod('')).toThrow(ZodValidationError);
});
});
TypeScript Configuration
Ensure your tsconfig.json has experimental decorators enabled:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"lib": ["ES2022", "DOM"]
}
}
Dependencies
This library depends on:
- Zod (v3.24.2) - Schema validation library for the validation decorators
- Lodash - Specifically the
debouncefunction for the Debounce decorator - TypeScript - Experimental decorators support required
All dependencies are already configured in the project's package.json.
Performance Considerations
Memory Usage
- Validation: Adds runtime overhead for parameter validation
- Cache: Memory usage scales with cached results - set appropriate TTLs
- Debounce: Minimal memory overhead, one timer per method instance
- InFlight: Memory usage scales with concurrent operations
Execution Overhead
- Validation: Schema validation adds ~0.1-1ms per call depending on complexity
- Cache: Cache lookups are generally < 0.1ms
- Debounce: Timer management adds minimal overhead
- InFlight: Promise management adds ~0.1ms per call
Optimization Tips
- Use specific Zod schemas - avoid overly complex validations in hot paths
- Set reasonable cache TTLs - balance memory usage vs. performance gains
- Profile critical paths - measure the impact of decorators in performance-critical code
- Consider decorator order - validation → debouncing → caching → in-flight typically works best
Quick Reference
Decorator Comparison Table
| Decorator | Purpose | Use Case | Key Features | Example |
|---|---|---|---|---|
ValidateParams |
Parameter validation | API inputs, user data | Zod schemas, transformations, custom errors | @ValidateParams([z.string().email()]) |
ValidateParam |
Single parameter validation | Simple validation | Index-based, custom names | @ValidateParam(0, z.string()) |
Cache |
Result caching | Expensive operations | TTL, custom keys, per-instance | @Cache({ ttl: CacheTimeToLive.fiveMinutes }) |
Debounce |
Rate limiting | User input, auto-save | Wait time, leading/trailing edge | @Debounce({ wait: 300 }) |
InFlight |
Duplicate prevention | API calls | Promise sharing, per-instance | @InFlight() |
InFlightWithKey |
Keyed duplicate prevention | Parameterized API calls | Argument-based keys | @InFlightWithKey() |
InFlightWithCache |
Cache + duplicate prevention | Expensive + stable data | Combined caching and deduplication | @InFlightWithCache({ cacheTime: 60000 }) |
Common Patterns
// Form validation with debouncing
@Debounce({ wait: 300 })
@ValidateParams([z.string().min(1)], ['query'])
onSearchInput(query: string) { }
// API call with caching and duplicate prevention
@Cache({ ttl: CacheTimeToLive.fiveMinutes })
@InFlight()
@ValidateParams([z.string().uuid()], ['id'])
async fetchUser(id: string): Promise<User> { }
// Complex validation with transformation
@ValidateParams([
z.object({
email: z.string().email().transform(e => e.toLowerCase()),
data: z.record(z.any()).transform(d => sanitizeData(d))
})
])
processUserData(input: UserInput) { }
Migration Guide
Migrating from Manual Validation
// Before: Manual validation
class OldService {
async createUser(email: string, userData: any): Promise<User> {
if (!email || !isValidEmail(email)) {
throw new Error('Invalid email');
}
if (!userData || !userData.name) {
throw new Error('Name is required');
}
// ... rest of method
}
}
// After: Decorator validation
class NewService {
@ValidateParams([
z.string().email(),
z.object({ name: z.string().min(1) })
], ['email', 'userData'])
async createUser(email: string, userData: UserData): Promise<User> {
// Parameters are guaranteed to be valid
// ... rest of method
}
}
Migrating from RxJS Patterns
// Before: RxJS shareReplay pattern
class OldService {
private userCache$ = new BehaviorSubject<User[]>([]);
getUsers(): Observable<User[]> {
return this.http.get<User[]>('/users').pipe(
tap(users => this.userCache$.next(users)),
shareReplay(1)
);
}
}
// After: Decorator pattern
class NewService {
@Cache({ ttl: CacheTimeToLive.fiveMinutes })
@InFlight()
async getUsers(): Promise<User[]> {
const response = await this.http.get<User[]>('/users').toPromise();
return response;
}
}
Troubleshooting
Common Issues
Decorators not working:
- Ensure
experimentalDecorators: truein tsconfig.json - Check decorator import statements
- Verify TypeScript version supports decorators
Validation errors:
- Check Zod schema definitions
- Verify parameter names match expectations
- Use
ZodValidationErrorfor proper error handling
Caching not working:
- Verify TTL settings are appropriate
- Check if custom key generators are working correctly
- Ensure methods are not throwing errors (errors are not cached)
Performance issues:
- Profile decorator overhead in critical paths
- Consider simpler validation schemas for hot code
- Review cache TTL settings for memory usage
Debug Mode
Enable debug logging for decorators:
// In development environment
if (environment.development) {
// Enable Zod error details
z.setErrorMap((issue, ctx) => ({
message: `${ctx.defaultError} (Path: ${issue.path.join('.')})`
}));
}
License
This library is part of the ISA Frontend project and follows the project's licensing terms.
Contributing
When adding new decorators:
- Implementation: Add in
src/lib/[decorator-name].decorator.ts - Tests: Include comprehensive unit tests with edge cases
- Documentation: Update this README with examples and usage
- Exports: Export from
src/index.ts - Examples: Add real-world usage examples
- Performance: Consider memory and execution overhead
For more detailed examples and usage patterns, see USAGE.md.
This comprehensive decorator library enhances Angular applications with powerful cross-cutting concerns while maintaining clean, readable code. Each decorator is designed to be composable, performant, and easy to test.