diff --git a/.github/copilot/architecture.md b/.github/copilot/architecture.md deleted file mode 100644 index 39d9b160..00000000 --- a/.github/copilot/architecture.md +++ /dev/null @@ -1,2095 +0,0 @@ -# MAD-AI Project Architecture Blueprint - -> **Generated on:** August 23, 2025 -> **Version:** 1.0.0 -> **Project:** MAD-AI - Angular Application with Clean Architecture -> **Technology Stack:** Angular 20.x, TypeScript, RxJS, TailwindCSS - -## Table of Contents - -- [1. Architecture Detection and Analysis](#1-architecture-detection-and-analysis) -- [2. Architectural Overview](#2-architectural-overview) -- [3. Architecture Visualization](#3-architecture-visualization) -- [4. Core Architectural Components](#4-core-architectural-components) -- [5. Architectural Layers and Dependencies](#5-architectural-layers-and-dependencies) -- [6. Data Architecture](#6-data-architecture) -- [7. Cross-Cutting Concerns Implementation](#7-cross-cutting-concerns-implementation) -- [8. Service Communication Patterns](#8-service-communication-patterns) -- [9. Angular-Specific Architectural Patterns](#9-angular-specific-architectural-patterns) -- [10. Implementation Patterns](#10-implementation-patterns) -- [11. Testing Architecture](#11-testing-architecture) -- [12. Deployment Architecture](#12-deployment-architecture) -- [13. Extension and Evolution Patterns](#13-extension-and-evolution-patterns) -- [14. Architectural Pattern Examples](#14-architectural-pattern-examples) -- [15. Architecture Governance](#15-architecture-governance) -- [16. Blueprint for New Development](#16-blueprint-for-new-development) - -## 1. Architecture Detection and Analysis - -### Technology Stack Detection - -**Primary Framework:** Angular 20.1.6 with Server-Side Rendering (SSR) -- **Build System:** Angular CLI with @angular/build -- **Language:** TypeScript with strict mode enabled -- **Styling:** TailwindCSS 4.1.11 with PostCSS -- **State Management:** RxJS 7.8.0 with reactive patterns -- **Testing:** Jasmine with Karma -- **Additional Libraries:** - - Chart.js with ng2-charts for data visualization - - jsPDF for document generation - - PapaParse for CSV processing - - Express.js for SSR server - -**Development Tools:** -- Custom icon linting system -- Path mapping for clean imports -- Strict TypeScript configuration -- Custom build processes - -### Architectural Pattern Analysis - -**Primary Pattern:** Clean Architecture with Domain-Driven Design (DDD) - -The project demonstrates a sophisticated implementation of Clean Architecture principles with: - -1. **Clear layer separation** with dependency inversion -2. **Domain-centric design** with rich entities and value objects -3. **Use case orchestration** in the application layer -4. **Infrastructure isolation** with repository pattern -5. **Angular-specific adaptations** for web UI concerns - -**Secondary Patterns:** -- **Facade Pattern** for simplified application layer interfaces -- **Repository Pattern** for data access abstraction -- **Command Query Responsibility Segregation (CQRS)** tendencies -- **Event-Driven Architecture** with domain events -- **Dependency Injection** throughout all layers - -## 2. Architectural Overview - -### Guiding Principles - -The MAD-AI architecture is built on these core principles: - -1. **Dependency Inversion:** Higher-level modules do not depend on lower-level modules -2. **Single Responsibility:** Each component has one reason to change -3. **Interface Segregation:** Dependencies use focused, minimal interfaces -4. **Separation of Concerns:** Business logic is isolated from technical concerns -5. **Testability:** All components can be tested in isolation -6. **Extensibility:** New features can be added without modifying existing code - -### Architectural Boundaries - -**Layer Boundaries:** -- **Domain Layer:** Contains business entities, value objects, and contracts -- **Application Layer:** Orchestrates use cases and manages application state -- **Infrastructure Layer:** Implements external concerns (HTTP, storage, etc.) -- **Presentation Layer:** Handles UI concerns and user interactions -- **Core Layer:** Provides shared technical utilities and cross-cutting concerns - -**Boundary Enforcement:** -- TypeScript path mapping prevents inappropriate cross-layer imports -- Dependency injection tokens enforce interface contracts -- Strict folder organization with clear naming conventions -- Angular guards and interceptors maintain separation of concerns - -### Hybrid Architectural Adaptations - -The architecture adapts Clean Architecture for web applications: - -1. **Angular Integration:** Uses Angular's DI system instead of pure dependency injection -2. **Reactive Programming:** Integrates RxJS streams with use case patterns -3. **Component Architecture:** Adapts presentation layer for Angular component model -4. **Route-Based Features:** Organizes features around Angular routing patterns - -## 3. Architecture Visualization - -### System Context Diagram (C4 Level 1) - -```mermaid -graph TB - User[User/Admin] --> MAD_AI[MAD-AI System] - MAD_AI --> Backend[Backend API] - MAD_AI --> Browser[Web Browser] - MAD_AI --> Storage[Local Storage] - - User -.-> |Authentication| MAD_AI - User -.-> |Role Management| MAD_AI - User -.-> |Data Export| MAD_AI -``` - -### Container Diagram (C4 Level 2) - -```mermaid -graph TB - subgraph "MAD-AI Frontend Application" - App[Angular App Container] - SSR[SSR Server Container] - end - - subgraph "External Systems" - API[Backend REST API] - Files[File System] - end - - User[User] --> App - App --> SSR - App --> API - App --> Files - SSR --> API -``` - -### Component Diagram (C4 Level 3) - -```mermaid -graph TB - subgraph "Presentation Layer" - Components[Angular Components] - Guards[Route Guards] - Layouts[Layout Components] - end - - subgraph "Application Layer" - Facades[Facade Services] - UseCases[Use Cases] - Events[Event Processor] - end - - subgraph "Domain Layer" - Entities[Domain Entities] - ValueObjects[Value Objects] - Contracts[Repository Contracts] - end - - subgraph "Infrastructure Layer" - HttpRepos[HTTP Repositories] - Mappers[Data Mappers] - Services[External Services] - end - - Components --> Facades - Facades --> UseCases - UseCases --> Contracts - HttpRepos --> Contracts - UseCases --> Events -``` - -### Data Flow Diagram - -```mermaid -sequenceDiagram - participant UI as Presentation Layer - participant F as Facade - participant UC as Use Case - participant R as Repository - participant API as External API - - UI->>F: User Action - F->>UC: Execute Use Case - UC->>R: Repository Call - R->>API: HTTP Request - API-->>R: Response Data - R-->>UC: Domain Entity - UC-->>F: Result - F-->>UI: Updated State -``` - -## 4. Core Architectural Components - -### Domain Layer Components - -#### Purpose and Responsibility -The Domain layer represents the core business logic and rules of the MAD-AI system. It contains: -- **Business Entities:** User, Role, Session, Notification -- **Value Objects:** Email, Username, FirstName, LastName, UserStatus -- **Domain Contracts:** Repository interfaces and service contracts -- **Business Rules:** Validation logic and domain invariants -- **Domain Events:** Business event definitions and handlers - -#### Internal Structure -```typescript -domain/ -├── entities/ # Rich domain entities with behavior -├── value-objects/ # Immutable value objects -├── contracts/ # Repository and service interfaces -├── events/ # Domain event definitions -├── errors/ # Domain-specific errors -├── enums/ # Domain enumerations -└── repositories/ # Repository interface definitions -``` - -#### Key Design Patterns -- **Entity Pattern:** Rich objects with identity and behavior -- **Value Object Pattern:** Immutable objects representing concepts -- **Repository Pattern:** Abstract data access interfaces -- **Domain Events Pattern:** Decoupled business event handling -- **Specification Pattern:** Encapsulated business rules - -#### Evolution Patterns -- **New Entity Types:** Add to entities/ with corresponding value objects -- **New Business Rules:** Implement as domain services or entity methods -- **New Repository Contracts:** Define in contracts/ and implement in infrastructure - -### Application Layer Components - -#### Purpose and Responsibility -The Application layer orchestrates business workflows and manages application state: -- **Facades:** Simplified interfaces for presentation layer -- **Use Cases:** Business workflow implementations -- **Application Services:** Cross-cutting application concerns -- **Error Handling:** Application-level error transformation -- **State Management:** Reactive state management with signals - -#### Internal Structure -```typescript -application/ -├── facades/ # Presentation layer interfaces -├── use-cases/ # Business workflow implementations -├── services/ # Application-level services -├── errors/ # Application error handling -└── types/ # Application-specific types -``` - -#### Interaction Patterns -- **Facade Coordination:** Facades orchestrate multiple use cases -- **Use Case Execution:** Single-purpose workflow implementations -- **Error Transformation:** Convert domain errors to application errors -- **Event Processing:** Handle domain events at application level - -### Infrastructure Layer Components - -#### Purpose and Responsibility -The Infrastructure layer implements external concerns and technical details: -- **HTTP Repositories:** API communication implementations -- **Data Mappers:** Transform between DTOs and domain entities -- **External Services:** Third-party service integrations -- **Technical Services:** File handling, export services - -#### Internal Structure -```typescript -infrastructure/ -├── repositories/ # Repository implementations -├── mappers/ # DTO to entity mappers -├── services/ # External service implementations -├── http/ # HTTP utilities and interceptors -├── dtos/ # Data transfer objects -└── errors/ # Infrastructure error handling -``` - -### Presentation Layer Components - -#### Purpose and Responsibility -The Presentation layer handles user interface concerns: -- **Feature Components:** Business feature implementations -- **Layout Components:** Application layout structures -- **Shared Components:** Reusable UI components -- **Forms:** Reactive form implementations -- **Navigation:** Route configuration and navigation - -#### Internal Structure -```typescript -presentation/ -├── features/ # Business feature implementations -├── layouts/ # Application layouts -├── navigation/ # Navigation components -├── shell/ # Application shell -└── shared/ # Shared UI components -``` - -## 5. Architectural Layers and Dependencies - -### Layer Structure - -**1. Presentation Layer (outermost)** -- Angular components, forms, and UI concerns -- Depends on: Application layer only -- Responsibilities: User interaction, form validation, routing - -**2. Application Layer** -- Use cases, facades, and application services -- Depends on: Domain layer only -- Responsibilities: Workflow orchestration, state management - -**3. Infrastructure Layer** -- Repository implementations and external services -- Depends on: Domain layer contracts -- Responsibilities: Data persistence, external API calls - -**4. Domain Layer (innermost)** -- Business entities, value objects, and contracts -- Depends on: Nothing (no dependencies) -- Responsibilities: Business logic, domain rules - -**5. Core Layer (cross-cutting)** -- Guards, interceptors, utilities -- Depends on: Application and domain layers -- Responsibilities: Technical concerns, security - -### Dependency Rules - -**Strict Dependency Flow:** -``` -Presentation → Application → Domain ← Infrastructure - ↑ ↑ - Core ←---------- -``` - -**Enforced Through:** -- TypeScript path mappings prevent reverse dependencies -- Dependency injection tokens ensure interface contracts -- Code review processes check architectural compliance - -### Abstraction Mechanisms - -**Repository Pattern:** -```typescript -// Domain contract -export interface UserRepository { - getById(id: number): Promise; - save(user: User): Promise; -} - -// Infrastructure implementation -@Injectable() -export class HttpUserRepository implements UserRepository { - // Implementation details... -} -``` - -**Dependency Injection:** -```typescript -// Token definition -export const USER_REPOSITORY = new InjectionToken('UserRepository'); - -// Provider configuration -providers: [ - { provide: USER_REPOSITORY, useClass: HttpUserRepository } -] -``` - -## 6. Data Architecture - -### Domain Model Structure - -The domain model follows DDD principles with rich entities and value objects: - -#### Core Entities - -**User Entity:** -- Encapsulates user identity and behavior -- Contains value objects for type safety -- Implements domain invariants and validation -- Publishes domain events for state changes - -**Role Entity:** -- Represents user roles and permissions -- Implements role-based access control logic -- Contains hierarchical permission structures - -**Session Entity:** -- Manages user authentication sessions -- Handles session lifecycle and security -- Contains metadata for audit trails - -#### Value Objects - -**Email, Username, FirstName, LastName:** -- Immutable objects with validation -- Encapsulate formatting and business rules -- Provide type safety at compile time - -### Entity Relationships - -```mermaid -erDiagram - User { - number id PK - Username username - Email email - FirstName firstName - LastName lastName - boolean active - ISODateTime createdAt - } - - Role { - number id PK - string name - string description - boolean active - } - - Session { - string token PK - number userId FK - ISODateTime expiresAt - ISODateTime createdAt - } - - User ||--|| Role : "has" - User ||--o{ Session : "owns" -``` - -### Data Access Patterns - -**Repository Pattern Implementation:** -- Abstract repository interfaces in domain layer -- Concrete implementations in infrastructure layer -- Consistent error handling and transformation -- Automatic mapping between DTOs and entities - -**Data Transformation Pipeline:** -``` -API Response → DTO → Mapper → Domain Entity → Use Case → Facade → Component -``` - -### Caching Strategies - -**Session Caching:** -- Browser local storage for user sessions -- Automatic session refresh with interceptors -- Secure token storage with expiration handling - -**Application State:** -- Angular signals for reactive state management -- Computed values for derived state -- Minimal state persistence in session storage - -## 7. Cross-Cutting Concerns Implementation - -### Authentication & Authorization - -#### Security Model Implementation -```typescript -// Authentication facade provides centralized auth state -@Injectable() -export class AuthFacade { - private readonly _user = signal(null); - private readonly _isAuthenticated = computed(() => !!this._user()); - - // Reactive authentication state - public readonly user = this._user.asReadonly(); - public readonly isAuthenticated = this._isAuthenticated; -} -``` - -#### Permission Enforcement Patterns -- **Route Guards:** Protect routes based on authentication status -- **Component Guards:** Control UI element visibility -- **API Interceptors:** Automatically attach authentication tokens -- **Role-Based Access:** Granular permission checking - -#### Security Boundary Patterns -```typescript -// Auth guard prevents unauthorized access -export const authGuard: CanActivateFn = () => { - const authFacade = inject(AuthFacade); - const router = inject(Router); - - return authFacade.isAuthenticated() || router.parseUrl('/auth/login'); -}; -``` - -### Error Handling & Resilience - -#### Exception Handling Patterns -```typescript -// Centralized error transformation -@Injectable() -export class ApplicationErrorTransformer { - transform(error: unknown): ApplicationError { - if (error instanceof DomainError) { - return ApplicationError.fromDomain(error); - } - // Handle other error types... - } -} -``` - -#### Retry and Circuit Breaker Implementation -- **HTTP Interceptors:** Automatic retry for failed requests -- **Exponential Backoff:** Progressive retry delays -- **Circuit Breaker:** Prevent cascading failures -- **Graceful Degradation:** Fallback to cached data - -#### Error Reporting Patterns -- **Global Error Handler:** Centralized error logging -- **User Notifications:** Friendly error messages -- **Error Boundaries:** Prevent application crashes -- **Diagnostic Information:** Detailed error context - -### Logging & Monitoring - -#### Instrumentation Patterns -```typescript -// Domain event logging -@Injectable() -export class DomainEventProcessor { - process(event: DomainEvent): void { - this.logger.info('Domain event processed', { - type: event.type, - aggregateId: event.aggregateId - }); - } -} -``` - -#### Observability Implementation -- **Performance Metrics:** Angular performance monitoring -- **User Activity Tracking:** Navigation and interaction logging -- **Error Tracking:** Automatic error reporting -- **Business Metrics:** Domain event analytics - -### Validation - -#### Input Validation Strategies -```typescript -// Value object validation -export class Email { - private constructor(private readonly value: string) {} - - static create(value: string): Email { - if (!this.isValid(value)) { - throw new ValidationError('Invalid email format'); - } - return new Email(value); - } -} -``` - -#### Business Rule Validation -- **Domain Entity Invariants:** Validated in entity constructors -- **Use Case Preconditions:** Validated before business logic -- **Cross-Entity Validation:** Handled by domain services -- **Form Validation:** Angular reactive forms with custom validators - -### Configuration Management - -#### Configuration Source Patterns -```typescript -// Environment-specific configuration -export const environment = { - production: false, - apiUrl: 'http://localhost:3000/api', - features: { - enableAdvancedExport: true, - enableNotifications: true - } -}; -``` - -#### Feature Flag Implementation -- **Environment Variables:** Build-time feature flags -- **Runtime Configuration:** Dynamic feature enablement -- **User-Specific Flags:** Role-based feature access -- **A/B Testing Support:** Experimental feature rollouts - -## 8. Service Communication Patterns - -### Service Boundary Definitions - -Services in MAD-AI are organized by business capability: - -**Authentication Services:** -- User authentication and session management -- Password reset and email confirmation -- Session refresh and token management - -**User Management Services:** -- User profile management -- User listing and search -- User status and activity tracking - -**Role Management Services:** -- Role definition and assignment -- Permission management -- Role-based access control - -**Notification Services:** -- System notification delivery -- User notification preferences -- Notification history and tracking - -### Communication Protocols - -**HTTP REST API:** -- Standardized REST endpoints for all external communication -- JSON payload format with consistent error responses -- HTTP status codes for operation results - -**Internal Service Communication:** -- Direct method calls within the same process -- Reactive streams with RxJS for asynchronous operations -- Domain events for decoupled communication - -### Synchronous vs. Asynchronous Patterns - -**Synchronous Operations:** -- User authentication (immediate response required) -- Form validation (real-time feedback) -- Data retrieval (blocking operations) - -**Asynchronous Operations:** -- Email notifications (fire-and-forget) -- Data export operations (long-running tasks) -- Domain event processing (eventual consistency) - -### API Versioning Strategies - -**URL-Based Versioning:** -```typescript -const API_BASE_URL = environment.apiUrl + '/v1'; -``` - -**Header-Based Versioning:** -```typescript -// HTTP interceptor adds version headers -headers = headers.set('API-Version', '1.0'); -``` - -### Resilience Patterns - -**Retry Logic:** -```typescript -// Automatic retry with exponential backoff -return this.http.get(url).pipe( - retry({ - count: 3, - delay: (error, retryCount) => timer(retryCount * 1000) - }) -); -``` - -**Circuit Breaker:** -- Monitor API failure rates -- Open circuit after threshold failures -- Fallback to cached data or degraded functionality - -## 9. Angular-Specific Architectural Patterns - -### Module Organization Strategy - -**Feature Module Pattern:** -```typescript -// Each feature is self-contained -presentation/ -└── features/ - ├── auth/ # Authentication feature - ├── dashboard/ # Dashboard feature - ├── roles/ # Role management feature - └── users/ # User management feature -``` - -**Lazy Loading Implementation:** -```typescript -export const routes: Routes = [ - { - path: 'auth', - loadComponent: () => import('./layouts/auth-layout').then(m => m.AuthLayout), - children: authRoutes - } -]; -``` - -### Component Hierarchy Design - -**Container/Presenter Pattern:** -- **Smart Components:** Connect to facades and manage state -- **Dumb Components:** Pure presentation components -- **Form Components:** Handle user input and validation - -**Component Structure:** -```typescript -@Component({ - selector: 'app-login', - templateUrl: './login.html', - styleUrl: './login.css', - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [LoginForm] -}) -export class Login implements OnInit { - auth = inject(AuthFacade); - - ngOnInit(): void { - this.auth.clearAuthStateCompletely(); - } -} -``` - -### Service and Dependency Injection Patterns - -**Hierarchical Injection:** -```typescript -// Application-level services -export const appConfig: ApplicationConfig = { - providers: [ - provideAuth(), - provideNotifications(), - provideUsers(), - ... - ] -}; -``` - -**Token-Based Injection:** -```typescript -// Repository injection with interfaces -export const USER_REPOSITORY = new InjectionToken('UserRepository'); - -providers: [ - { provide: USER_REPOSITORY, useClass: HttpUserRepository } -] -``` - -### State Management Approach - -**Signal-Based State:** -```typescript -@Injectable() -export class AuthFacade { - private readonly _user = signal(null); - private readonly _loading = signal(false); - private readonly _error = signal(null); - - // Read-only computed state - public readonly isAuthenticated = computed(() => !!this._user()); - public readonly userRole = computed(() => this._user()?.role); -} -``` - -**Reactive Programming Patterns:** -```typescript -// Use case returns observables -async execute(request: LoginRequest): Promise { - this.setLoading(true); - try { - const session = await this.loginUseCase.execute(request); - this.setSession(session); - return session; - } finally { - this.setLoading(false); - } -} -``` - -### Route Guard Implementation - -**Functional Guards:** -```typescript -export const authGuard: CanActivateFn = () => { - const authFacade = inject(AuthFacade); - return authFacade.isAuthenticated(); -}; - -export const guestOnly: CanMatchFn = () => { - const authFacade = inject(AuthFacade); - return !authFacade.isAuthenticated(); -}; -``` - -**Composition Guards:** -```typescript -// Multiple guards can be composed -{ - path: '', - canMatch: [authOnly, emailConfirmedOnly], - loadComponent: () => import('./main-layout') -} -``` - -## 10. Implementation Patterns - -### Interface Design Patterns - -#### Interface Segregation Approaches -```typescript -// Focused repository interfaces -export interface UserRepository { - getById(id: number): Promise; - save(user: User): Promise; - delete(id: number): Promise; -} - -// Separate read and write concerns -export interface UserQueryRepository { - findByEmail(email: string): Promise; - findByUsername(username: string): Promise; - list(criteria: UserSearchCriteria): Promise; -} -``` - -#### Generic vs. Specific Interface Patterns -```typescript -// Generic repository base -export interface Repository { - getById(id: ID): Promise; - save(entity: T): Promise; -} - -// Specific business interfaces -export interface AuthRepository { - login(credentials: LoginCredentials): Promise; - logout(sessionToken: string): Promise; - refreshSession(token: string): Promise; -} -``` - -### Service Implementation Patterns - -#### Service Lifetime Management -```typescript -// Singleton services for shared state -@Injectable({ providedIn: 'root' }) -export class AuthFacade { } - -// Scoped services for specific features -providers: [ - { provide: ExportService, useClass: CsvExportService } -] -``` - -#### Service Composition Patterns -```typescript -// Facade composes multiple use cases -@Injectable() -export class AuthFacade { - private readonly loginUseCase = inject(LoginWithCredentials); - private readonly logoutUseCase = inject(Logout); - private readonly profileUseCase = inject(GetProfile); - - async login(request: LoginRequest): Promise { - const session = await this.loginUseCase.execute(request); - this.setSession(session); - } -} -``` - -### Repository Implementation Patterns - -#### Query Pattern Implementations -```typescript -// Repository with query methods -export class HttpUserRepository implements UserRepository { - async findByEmail(email: string): Promise { - const params = new HttpParams().set('email', email); - const response = await firstValueFrom( - this.http.get(this.baseUrl, { params }) - ); - return response.length > 0 ? this.mapper.toDomain(response[0]) : null; - } -} -``` - -#### Transaction Management -```typescript -// Transaction coordination in use cases -@Injectable() -export class CreateUserWithRole { - async execute(request: CreateUserRequest): Promise { - // Coordinate multiple repository operations - const user = await this.userRepo.save(newUser); - await this.roleRepo.assignRole(user.id, request.roleId); - return user; - } -} -``` - -### Controller/API Implementation Patterns - -#### Request Handling Patterns -```typescript -// HTTP interceptor for authentication -export const authInterceptor: HttpInterceptorFn = (req, next) => { - const authStore = inject(AUTH_USER_STORE_PORT); - const token = authStore.getToken(); - - if (token) { - req = req.clone({ - setHeaders: { Authorization: `Bearer ${token}` } - }); - } - - return next(req); -}; -``` - -#### Response Formatting -```typescript -// Consistent error response handling -export const httpErrorInterceptor: HttpInterceptorFn = (req, next) => { - return next(req).pipe( - catchError((error: HttpErrorResponse) => { - const appError = this.transformHttpError(error); - return throwError(() => appError); - }) - ); -}; -``` - -### Domain Model Implementation - -#### Entity Implementation Patterns -```typescript -export class User { - private _domainEvents: DomainEvent[] = []; - - static create(props: UserCreateProps): User { - const user = new User(/* ... */); - user.addDomainEvent(new UserCreatedEvent(user.id)); - return user; - } - - updateProfile(firstName: FirstName, lastName: LastName): void { - this._firstName = firstName; - this._lastName = lastName; - this.addDomainEvent(new UserProfileUpdatedEvent(this.id)); - } -} -``` - -#### Value Object Patterns -```typescript -export class Email { - private constructor(private readonly value: string) {} - - static create(value: string): Email { - if (!this.isValid(value)) { - throw new ValidationError('Invalid email format'); - } - return new Email(value); - } - - static isValid(value: string): boolean { - return EMAIL_REGEX.test(value); - } - - toString(): string { - return this.value; - } -} -``` - -#### Domain Event Implementation -```typescript -export class DomainEvent { - constructor( - public readonly type: DomainEventType, - public readonly aggregateId: number, - public readonly data: Record, - public readonly occurredAt: Date = new Date() - ) {} -} - -@Injectable() -export class DomainEventProcessor { - process(events: DomainEvent[]): void { - events.forEach(event => this.handleEvent(event)); - } -} -``` - -## 11. Testing Architecture - -### Testing Strategy Alignment - -The testing architecture follows the same clean architecture principles: - -**Unit Testing by Layer:** -- **Domain Layer:** Test entities, value objects, and business rules -- **Application Layer:** Test use cases and facades in isolation -- **Infrastructure Layer:** Test repository implementations with mocks -- **Presentation Layer:** Test components with shallow rendering - -### Test Boundary Patterns - -**Domain Testing:** -```typescript -describe('User Entity', () => { - it('should create user with valid data', () => { - const user = User.create({ - id: 1, - username: Username.create('johndoe'), - email: Email.create('john@example.com'), - // ... - }); - - expect(user.username.toString()).toBe('johndoe'); - }); -}); -``` - -**Use Case Testing:** -```typescript -describe('LoginWithCredentials', () => { - let useCase: LoginWithCredentials; - let mockAuthRepo: jasmine.SpyObj; - - beforeEach(() => { - const spy = jasmine.createSpyObj('AuthRepository', ['login']); - mockAuthRepo = spy; - useCase = new LoginWithCredentials(mockAuthRepo, /* ... */); - }); -}); -``` - -**Component Testing:** -```typescript -describe('LoginComponent', () => { - let component: LoginComponent; - let mockAuthFacade: jasmine.SpyObj; - - beforeEach(() => { - const spy = jasmine.createSpyObj('AuthFacade', ['login']); - TestBed.configureTestingModule({ - providers: [{ provide: AuthFacade, useValue: spy }] - }); - }); -}); -``` - -### Test Double Strategies - -**Repository Mocking:** -```typescript -const mockUserRepository = jasmine.createSpyObj('UserRepository', { - getById: Promise.resolve(testUser), - save: Promise.resolve(), - delete: Promise.resolve() -}); -``` - -**Facade Mocking:** -```typescript -const mockAuthFacade = jasmine.createSpyObj('AuthFacade', { - login: Promise.resolve(), - logout: Promise.resolve(), - isAuthenticated: true -}); -``` - -### Test Data Strategies - -**Test Builders:** -```typescript -export class UserTestBuilder { - private id = 1; - private username = 'testuser'; - private email = 'test@example.com'; - - withId(id: number): UserTestBuilder { - this.id = id; - return this; - } - - build(): User { - return User.create({ - id: this.id, - username: Username.create(this.username), - email: Email.create(this.email), - // ... - }); - } -} -``` - -## 12. Deployment Architecture - -### Deployment Topology - -**Single Page Application with SSR:** -- Angular application compiled to static assets -- Server-side rendering for improved SEO and performance -- Express.js server for SSR and API proxy - -**Build Pipeline:** -```json -{ - "scripts": { - "build": "ng build", - "build:ssr": "ng build --ssr", - "serve:ssr": "node dist/server/server.mjs" - } -} -``` - -### Environment-Specific Adaptations - -**Environment Configuration:** -```typescript -// environment.prod.ts -export const environment = { - production: true, - apiUrl: 'https://api.madai.com', - features: { - enableAdvancedExport: true, - enableNotifications: true - } -}; -``` - -**Build-Time Optimization:** -- Tree shaking for unused code removal -- Lazy loading for route-based code splitting -- Ahead-of-time compilation for performance - -### Runtime Configuration - -**Dynamic Configuration Loading:** -```typescript -// Configuration service -@Injectable() -export class ConfigurationService { - private config = signal(null); - - async loadConfiguration(): Promise { - const config = await this.http.get('/api/config').toPromise(); - this.config.set(config); - } -} -``` - -### Containerization Patterns - -**Docker Configuration:** -```dockerfile -FROM node:18-alpine -WORKDIR /app -COPY package*.json ./ -RUN npm ci --only=production -COPY dist/ ./dist/ -EXPOSE 4000 -CMD ["node", "dist/server/server.mjs"] -``` - -## 13. Extension and Evolution Patterns - -### Feature Addition Patterns - -#### New Business Feature Implementation - -**1. Domain Layer Extension:** -```typescript -// Add new entity -export class Report { - // Domain logic for reports -} - -// Add repository contract -export interface ReportRepository { - generate(criteria: ReportCriteria): Promise; -} -``` - -**2. Application Layer Extension:** -```typescript -// Add use case -@Injectable() -export class GenerateReport { - constructor( - @Inject(REPORT_REPOSITORY) private reportRepo: ReportRepository - ) {} -} - -// Add facade -@Injectable() -export class ReportsFacade { - // Orchestrate report operations -} -``` - -**3. Infrastructure Layer Extension:** -```typescript -// Implement repository -@Injectable() -export class HttpReportRepository implements ReportRepository { - // HTTP implementation -} -``` - -**4. Presentation Layer Extension:** -```typescript -// Add feature module -presentation/features/reports/ -├── pages/ -├── components/ -├── forms/ -└── reports.routes.ts -``` - -#### Dependency Introduction Guidelines - -**New Repository Integration:** -```typescript -// 1. Define token in di/tokens.ts -export const REPORT_REPOSITORY = new InjectionToken('ReportRepository'); - -// 2. Create provider in di/provide-reports.ts -export function provideReports(): Provider[] { - return [ - { provide: REPORT_REPOSITORY, useClass: HttpReportRepository } - ]; -} - -// 3. Add to app configuration -export const appConfig: ApplicationConfig = { - providers: [ - // ...existing providers - ...provideReports() - ] -}; -``` - -### Modification Patterns - -#### Safe Component Modification - -**Entity Extension:** -```typescript -export class User { - // Add new properties with defaults for compatibility - constructor( - // ...existing properties - public readonly preferences?: UserPreferences - ) {} - - // Add new methods without breaking existing ones - updatePreferences(preferences: UserPreferences): void { - // Implementation - } -} -``` - -**Repository Extension:** -```typescript -export interface UserRepository { - // Existing methods remain unchanged - getById(id: number): Promise; - save(user: User): Promise; - - // New methods added - findByPreferences(preferences: UserPreferences): Promise; -} -``` - -#### Backward Compatibility Strategies - -**API Versioning:** -```typescript -// Support multiple API versions -export class HttpUserRepository implements UserRepository { - private readonly apiVersion = environment.apiVersion; - - async getById(id: number): Promise { - const url = `${this.baseUrl}/v${this.apiVersion}/users/${id}`; - // Implementation - } -} -``` - -### Integration Patterns - -#### External System Integration - -**Adapter Implementation:** -```typescript -// Create adapter for external service -@Injectable() -export class ExternalNotificationAdapter implements NotificationPort { - constructor(private externalService: ExternalNotificationService) {} - - async send(notification: Notification): Promise { - // Adapt internal notification to external format - const externalFormat = this.adaptToExternal(notification); - await this.externalService.send(externalFormat); - } -} -``` - -**Anti-Corruption Layer:** -```typescript -// Protect domain from external changes -@Injectable() -export class ExternalUserAdapter { - toDomain(externalUser: ExternalUserDTO): User { - return User.create({ - id: externalUser.user_id, - username: Username.create(externalUser.login_name), - email: Email.create(externalUser.email_address), - // Map other fields with validation - }); - } -} -``` - -## 14. Architectural Pattern Examples - -### Layer Separation Examples - -#### Interface Definition and Implementation Separation - -**Domain Contract:** -```typescript -// Domain layer - pure interface -export interface UserRepository { - getById(id: number): Promise; - save(user: User): Promise; - findByEmail(email: string): Promise; -} -``` - -**Infrastructure Implementation:** -```typescript -// Infrastructure layer - HTTP implementation -@Injectable() -export class HttpUserRepository implements UserRepository { - constructor( - private readonly http: HttpClient, - private readonly mapper: UserMapper - ) {} - - async getById(id: number): Promise { - const response = await firstValueFrom( - this.http.get(`${this.baseUrl}/${id}`) - ); - return this.mapper.toDomain(response); - } -} -``` - -**Dependency Injection Configuration:** -```typescript -// DI configuration - binds interface to implementation -providers: [ - { provide: USER_REPOSITORY, useClass: HttpUserRepository } -] -``` - -#### Cross-Layer Communication Patterns - -**Presentation to Application:** -```typescript -@Component({ - selector: 'app-user-profile', - template: `
`, - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class UserProfileComponent { - private readonly usersFacade = inject(UsersFacade); - private readonly formBuilder = inject(FormBuilder); - - profileForm = this.formBuilder.group({ - firstName: ['', [Validators.required]], - lastName: ['', [Validators.required]] - }); - - async onSubmit(): Promise { - if (this.profileForm.valid) { - const updateRequest = this.createUpdateRequest(); - await this.usersFacade.updateProfile(updateRequest); - } - } -} -``` - -**Application to Domain:** -```typescript -@Injectable() -export class UpdateUserProfile { - constructor( - @Inject(USER_REPOSITORY) private readonly userRepo: UserRepository - ) {} - - async execute(request: UpdateProfileRequest): Promise { - // Get domain entity - const user = await this.userRepo.getById(request.userId); - - // Execute domain logic - user.updateProfile( - FirstName.create(request.firstName), - LastName.create(request.lastName) - ); - - // Persist changes - await this.userRepo.save(user); - return user; - } -} -``` - -### Component Communication Examples - -#### Service Invocation Patterns - -**Facade Coordination:** -```typescript -@Injectable() -export class AuthFacade { - private readonly loginUseCase = inject(LoginWithCredentials); - private readonly logoutUseCase = inject(Logout); - private readonly refreshUseCase = inject(RefreshSession); - - async login(request: LoginRequest): Promise { - try { - this.setLoading(true); - const session = await this.loginUseCase.execute(request); - this.setSession(session); - this.notificationsFacade.showSuccess('Login successful'); - } catch (error) { - this.handleError(error); - } finally { - this.setLoading(false); - } - } -} -``` - -#### Event Publication and Handling - -**Domain Event Publication:** -```typescript -export class User { - private _domainEvents: DomainEvent[] = []; - - updateProfile(firstName: FirstName, lastName: LastName): void { - this._firstName = firstName; - this._lastName = lastName; - - // Publish domain event - this.addDomainEvent(new UserProfileUpdatedEvent({ - userId: this.id, - firstName: firstName.toString(), - lastName: lastName.toString(), - updatedAt: new Date() - })); - } - - private addDomainEvent(event: DomainEvent): void { - this._domainEvents.push(event); - } -} -``` - -**Event Processing:** -```typescript -@Injectable() -export class DomainEventProcessor { - process(events: DomainEvent[]): void { - events.forEach(event => { - switch (event.type) { - case DomainEventType.UserProfileUpdated: - this.handleUserProfileUpdated(event); - break; - case DomainEventType.UserLoggedIn: - this.handleUserLoggedIn(event); - break; - } - }); - } - - private handleUserProfileUpdated(event: DomainEvent): void { - // Update caches, send notifications, etc. - this.logger.info('User profile updated', event.data); - } -} -``` - -### Extension Point Examples - -#### Plugin Registration and Discovery - -**Icon Registration System:** -```typescript -// Icon provider configuration -export function provideIcons(config: IconConfig): Provider[] { - return [ - { provide: ICON_CONFIG, useValue: config }, - { provide: ICON_REGISTRY, useClass: IconRegistry }, - // Auto-discovery of icon providers - ...discoverIconProviders() - ]; -} - -// Dynamic icon loading -@Injectable() -export class IconRegistry { - private icons = new Map(); - - register(name: string, svgContent: string): void { - this.icons.set(name, svgContent); - } - - get(name: string): string | undefined { - return this.icons.get(name); - } -} -``` - -#### Extension Interface Implementation - -**Export Service Extension:** -```typescript -// Extension interface -export interface ExportPort { - export(data: T[], format: ExportFormat): Promise; - getSupportedFormats(): ExportFormat[]; -} - -// CSV implementation -@Injectable() -export class CsvExportService implements ExportPort { - async export(data: T[], format: ExportFormat): Promise { - if (format !== ExportFormat.CSV) { - throw new Error('Unsupported format'); - } - - const csv = this.convertToCsv(data); - return new Blob([csv], { type: 'text/csv' }); - } - - getSupportedFormats(): ExportFormat[] { - return [ExportFormat.CSV]; - } -} - -// PDF implementation -@Injectable() -export class PdfExportService implements ExportPort { - async export(data: T[], format: ExportFormat): Promise { - if (format !== ExportFormat.PDF) { - throw new Error('Unsupported format'); - } - - const pdf = await this.convertToPdf(data); - return new Blob([pdf], { type: 'application/pdf' }); - } - - getSupportedFormats(): ExportFormat[] { - return [ExportFormat.PDF]; - } -} -``` - -#### Configuration-Driven Extension - -**Feature Flag System:** -```typescript -// Feature configuration -export interface FeatureConfig { - enableAdvancedExport: boolean; - enableNotifications: boolean; - enableRoleHierarchy: boolean; -} - -// Feature-driven component loading -@Component({ - selector: 'app-dashboard', - template: ` -
- @if (features.enableAdvancedExport) { - - } - @if (features.enableNotifications) { - - } -
- ` -}) -export class DashboardComponent { - features = inject(FEATURE_CONFIG); -} -``` - -## 15. Architecture Governance - -### Architectural Consistency Maintenance - -#### Automated Architectural Compliance - -**TypeScript Path Mapping Enforcement:** -```json -// tsconfig.json enforces proper imports -{ - "compilerOptions": { - "paths": { - "@domain/*": ["src/app/domain/*"], - "@application/*": ["src/app/application/*"], - "@infrastructure/*": ["src/app/infrastructure/*"], - "@presentation/*": ["src/app/presentation/*"] - } - } -} -``` - -**Custom Linting Rules:** -```javascript -// scripts/lint-icons.cjs - Custom architectural validation -module.exports = function validateIcons() { - // Ensure icon usage follows architectural patterns - // Validate import paths - // Check for proper abstraction usage -}; -``` - -#### Dependency Analysis - -**Build-Time Validation:** -```json -{ - "scripts": { - "prebuild": "npm run lint:icons", - "lint:icons": "node scripts/lint-icons.cjs", - "validate:architecture": "npm run lint:icons && npm run test" - } -} -``` - -### Architectural Review Processes - -#### Code Review Guidelines - -**Layer Violation Detection:** -- Domain layer should not import from other layers -- Application layer should only import from domain -- Infrastructure should implement domain contracts -- Presentation should only depend on application facades - -**Pattern Compliance:** -- New repositories must implement domain contracts -- Use cases should be single-purpose -- Facades should coordinate, not implement business logic -- Components should be presentation-only - -#### Architecture Decision Documentation - -**Implicit Decision Records:** -Based on code analysis, key architectural decisions include: - -**Decision: Clean Architecture with DDD** -- **Context:** Need for maintainable, testable, and scalable application -- **Decision:** Implement Clean Architecture with Domain-Driven Design -- **Consequences:** Clear separation of concerns, testability, but increased complexity - -**Decision: Angular Signals for State Management** -- **Context:** Need for reactive state management without external dependencies -- **Decision:** Use Angular's signal-based state management -- **Consequences:** Native Angular integration, performance benefits, learning curve - -**Decision: Facade Pattern for Application Layer** -- **Context:** Need to simplify complex use case interactions -- **Decision:** Implement facade pattern for coordinating use cases -- **Consequences:** Simplified presentation layer, but additional abstraction - -### Documentation Practices - -#### Living Documentation - -**Code-First Documentation:** -- TypeScript interfaces serve as contracts -- JSDoc comments provide business context -- Unit tests document expected behavior -- Integration tests document system interactions - -**Architectural Annotations:** -```typescript -/** - * Authentication Facade - Clean Orchestrator Following MAD-AI Patterns - * - * @description - * Pure orchestrator that delegates all business logic to robust use cases. - * This facade focuses solely on: - * - Coordinating between use cases - * - Managing reactive application state (loading, user, errors) - * - Providing a clean API for the presentation layer - * - * @architecture Application Layer - * @pattern Facade Pattern - * @responsibility State Management, Use Case Coordination - */ -``` - -## 16. Blueprint for New Development - -### Development Workflow - -#### Starting Points for Different Feature Types - -**1. User Management Feature:** -```bash -# 1. Create domain entities and contracts -src/app/domain/entities/new-entity.entity.ts -src/app/domain/contracts/new-entity.contract.ts - -# 2. Implement use cases -src/app/application/use-cases/new-feature/ - -# 3. Create facade -src/app/application/facades/new-feature.facade.ts - -# 4. Implement infrastructure -src/app/infrastructure/repositories/http-new-entity.repository.ts - -# 5. Create presentation components -src/app/presentation/features/new-feature/ -``` - -**2. Cross-Cutting Concern Feature:** -```bash -# 1. Define in core layer -src/app/core/cross-cutting/new-concern/ - -# 2. Implement in infrastructure -src/app/infrastructure/services/new-concern.service.ts - -# 3. Configure in DI -src/app/di/provide-new-concern.ts - -# 4. Update app configuration -src/app/app.config.ts -``` - -#### Component Creation Sequence - -**1. Domain First:** -```typescript -// Step 1: Define entity -export class NewEntity { - static create(props: NewEntityProps): NewEntity { - // Domain logic and validation - } -} - -// Step 2: Define repository contract -export interface NewEntityRepository { - save(entity: NewEntity): Promise; - getById(id: number): Promise; -} -``` - -**2. Application Layer:** -```typescript -// Step 3: Implement use case -@Injectable() -export class CreateNewEntity { - constructor( - @Inject(NEW_ENTITY_REPOSITORY) private repo: NewEntityRepository - ) {} - - async execute(request: CreateNewEntityRequest): Promise { - // Use case logic - } -} - -// Step 4: Create facade -@Injectable() -export class NewEntityFacade { - private readonly createUseCase = inject(CreateNewEntity); - - async create(request: CreateNewEntityRequest): Promise { - // Facade coordination - } -} -``` - -**3. Infrastructure:** -```typescript -// Step 5: Implement repository -@Injectable() -export class HttpNewEntityRepository implements NewEntityRepository { - // HTTP implementation -} - -// Step 6: Configure DI -export const NEW_ENTITY_REPOSITORY = new InjectionToken('NewEntityRepository'); - -export function provideNewEntity(): Provider[] { - return [ - { provide: NEW_ENTITY_REPOSITORY, useClass: HttpNewEntityRepository } - ]; -} -``` - -**4. Presentation:** -```typescript -// Step 7: Create component -@Component({ - selector: 'app-new-entity', - template: ``, - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class NewEntityComponent { - private readonly facade = inject(NewEntityFacade); - - async onSubmit(): Promise { - await this.facade.create(this.getFormValue()); - } -} -``` - -### Implementation Templates - -#### Base Repository Template - -```typescript -import { Injectable, Inject } from '@angular/core'; -import { HttpClient, HttpParams } from '@angular/common/http'; -import { firstValueFrom, catchError } from 'rxjs'; - -@Injectable() -export class Http{EntityName}Repository implements {EntityName}Repository { - private readonly baseUrl = `${environment.apiUrl}/{entity-path}`; - - constructor( - private readonly http: HttpClient, - private readonly mapper: {EntityName}Mapper, - @Inject(CLOCK_PORT) private readonly clock: ClockPort - ) {} - - async getById(id: number): Promise<{EntityName}> { - const response = await firstValueFrom( - this.http.get<{EntityName}DTO>(`${this.baseUrl}/${id}`) - .pipe(catchError(this.handleHttpError)) - ); - return this.mapper.toDomain(response); - } - - async save(entity: {EntityName}): Promise { - const dto = this.mapper.toDTO(entity); - if (entity.id) { - await firstValueFrom( - this.http.put(`${this.baseUrl}/${entity.id}`, dto) - .pipe(catchError(this.handleHttpError)) - ); - } else { - await firstValueFrom( - this.http.post(this.baseUrl, dto) - .pipe(catchError(this.handleHttpError)) - ); - } - } - - private handleHttpError = (error: HttpErrorResponse): Observable => { - // Standard error handling - throw new InfrastructureError(error.message, error.status); - }; -} -``` - -#### Use Case Template - -```typescript -import { Injectable, Inject } from '@angular/core'; -import { {ENTITY_NAME}_REPOSITORY } from '@di/tokens'; -import type { {EntityName}Repository } from '@domain/repositories/{entity-name}.repository'; -import { ApplicationError } from '@application/errors/application-error'; - -@Injectable({ providedIn: 'root' }) -export class {ActionName}{EntityName} { - constructor( - @Inject({ENTITY_NAME}_REPOSITORY) private readonly repo: {EntityName}Repository - ) {} - - async execute(request: {ActionName}{EntityName}Request): Promise<{EntityName}> { - try { - // Validate request - this.validateRequest(request); - - // Execute business logic - const entity = await this.performAction(request); - - // Return result - return entity; - } catch (error) { - if (error instanceof DomainError) { - throw ApplicationError.fromDomain(error); - } - throw new ApplicationError('Unexpected error', 'UNKNOWN_ERROR'); - } - } - - private validateRequest(request: {ActionName}{EntityName}Request): void { - // Application-level validation - } - - private async performAction(request: {ActionName}{EntityName}Request): Promise<{EntityName}> { - // Implementation - } -} -``` - -#### Component Template - -```typescript -import { Component, ChangeDetectionStrategy, inject, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; -import { {EntityName}Facade } from '@application/facades/{entity-name}.facade'; - -@Component({ - selector: 'app-{entity-name}-{action}', - templateUrl: './{entity-name}-{action}.html', - styleUrl: './{entity-name}-{action}.css', - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ReactiveFormsModule] -}) -export class {EntityName}{Action}Component implements OnInit { - private readonly facade = inject({EntityName}Facade); - private readonly formBuilder = inject(FormBuilder); - - form: FormGroup = this.createForm(); - - // Reactive state from facade - loading = this.facade.loading; - error = this.facade.error; - - ngOnInit(): void { - this.facade.clearErrors(); - } - - async onSubmit(): Promise { - if (this.form.valid) { - const request = this.createRequest(); - await this.facade.{action}(request); - } - } - - private createForm(): FormGroup { - return this.formBuilder.group({ - // Form controls with validation - }); - } - - private createRequest(): {ActionName}{EntityName}Request { - const formValue = this.form.value; - return { - // Map form to request - }; - } -} -``` - -### Common Pitfalls - -#### Architecture Violations to Avoid - -**1. Layer Violations:** -```typescript -// ❌ DON'T: Domain importing from infrastructure -import { HttpClient } from '@angular/common/http'; // In domain layer - -// ✅ DO: Domain defines contracts, infrastructure implements -export interface UserRepository { - save(user: User): Promise; -} -``` - -**2. Business Logic in Presentation:** -```typescript -// ❌ DON'T: Business logic in component -export class UserComponent { - async updateUser(userData: any): Promise { - // Complex business logic here - if (userData.age < 18 && userData.role === 'admin') { - throw new Error('Minors cannot be admins'); - } - } -} - -// ✅ DO: Delegate to application layer -export class UserComponent { - async updateUser(userData: any): Promise { - await this.usersFacade.updateUser(userData); - } -} -``` - -**3. Direct Repository Usage in Components:** -```typescript -// ❌ DON'T: Inject repositories in components -export class UserComponent { - constructor( - @Inject(USER_REPOSITORY) private userRepo: UserRepository - ) {} -} - -// ✅ DO: Use facades for coordination -export class UserComponent { - private readonly usersFacade = inject(UsersFacade); -} -``` - -#### Performance Considerations - -**1. OnPush Change Detection:** -```typescript -// ✅ Always use OnPush for performance -@Component({ - changeDetection: ChangeDetectionStrategy.OnPush -}) -``` - -**2. Lazy Loading:** -```typescript -// ✅ Use lazy loading for feature modules -{ - path: 'feature', - loadComponent: () => import('./feature/feature.component') -} -``` - -**3. Signal-Based State:** -```typescript -// ✅ Use signals for reactive state -private readonly _users = signal([]); -public readonly users = this._users.asReadonly(); -``` - -#### Testing Considerations - -**1. Test Layer Boundaries:** -```typescript -// ✅ Test each layer in isolation -describe('UserFacade', () => { - let facade: UserFacade; - let mockCreateUserUseCase: jasmine.SpyObj; - - beforeEach(() => { - const spy = jasmine.createSpyObj('CreateUser', ['execute']); - // Test facade without testing use case implementation - }); -}); -``` - -**2. Mock External Dependencies:** -```typescript -// ✅ Mock infrastructure in application tests -const mockUserRepository = jasmine.createSpyObj('UserRepository', { - save: Promise.resolve(), - getById: Promise.resolve(testUser) -}); -``` - ---- - -## Blueprint Maintenance - -This architecture blueprint was generated on August 23, 2025, based on the current state of the MAD-AI codebase. To keep this document accurate and useful: - -### Update Triggers -- **New architectural patterns introduced** -- **Significant refactoring of existing components** -- **Addition of new layers or cross-cutting concerns** -- **Changes to dependency injection patterns** -- **Updates to testing strategies** - -### Maintenance Schedule -- **Monthly:** Review for accuracy with current codebase -- **Quarterly:** Update examples and templates -- **Major releases:** Comprehensive review and updates -- **Architectural changes:** Immediate updates to affected sections - -### Validation Process -1. **Code Analysis:** Regular automated analysis of architectural compliance -2. **Team Review:** Quarterly architectural review sessions -3. **Documentation Sync:** Ensure code comments align with blueprint -4. **Template Testing:** Validate that templates produce working code - -This blueprint serves as the definitive architectural reference for the MAD-AI project, providing both current state documentation and guidance for future development. diff --git a/.github/copilot/concrete-examples.md b/.github/copilot/concrete-examples.md deleted file mode 100644 index fce2416d..00000000 --- a/.github/copilot/concrete-examples.md +++ /dev/null @@ -1,1559 +0,0 @@ -# Concrete Codebase Examples - -> **Real examples from the MAD-AI project demonstrating best practices and patterns** - -## Table of Contents - -1. [Clean Architecture Implementation](#clean-architecture-implementation) -2. [Domain-Driven Design Patterns](#domain-driven-design-patterns) -3. [Signal-Based State Management](#signal-based-state-management) -4. [Dependency Injection Patterns](#dependency-injection-patterns) -5. [Error Handling Implementation](#error-handling-implementation) -6. [Testing Patterns](#testing-patterns) -7. [Component Examples](#component-examples) -8. [Service Integration](#service-integration) - -## Clean Architecture Implementation - -### 1. Domain Layer Example - -**From `src/app/domain/entities/user.entity.ts`:** - -```typescript -import { Email } from '../value-objects/email.value-object'; -import { UserRole } from '../enums/user-role.enum'; -import { DomainEntity } from './domain-entity.base'; - -export interface UserData { - readonly id: number; - readonly email: string; - readonly firstName: string; - readonly lastName: string; - readonly active: boolean; - readonly roles: UserRole[]; - readonly createdAt: Date; - readonly updatedAt?: Date; -} - -export class User extends DomainEntity { - static create(data: Omit): User { - // Domain validation - Email.validate(data.email); - - if (!data.firstName.trim()) { - throw new Error('First name is required'); - } - - if (!data.lastName.trim()) { - throw new Error('Last name is required'); - } - - return new User({ - ...data, - id: 0, // Will be assigned by repository - createdAt: new Date() - }); - } - - get email(): Email { - return Email.create(this.data.email); - } - - get fullName(): string { - return `${this.data.firstName} ${this.data.lastName}`; - } - - get isActive(): boolean { - return this.data.active; - } - - get hasAdminRole(): boolean { - return this.data.roles.includes(UserRole.ADMIN); - } - - activate(): User { - return this.update({ active: true }); - } - - deactivate(): User { - return this.update({ active: false }); - } - - updateProfile(firstName: string, lastName: string): User { - if (!firstName.trim() || !lastName.trim()) { - throw new Error('First name and last name are required'); - } - - return this.update({ - firstName: firstName.trim(), - lastName: lastName.trim(), - updatedAt: new Date() - }); - } - - assignRole(role: UserRole): User { - if (this.data.roles.includes(role)) { - return this; - } - - return this.update({ - roles: [...this.data.roles, role], - updatedAt: new Date() - }); - } - - removeRole(role: UserRole): User { - return this.update({ - roles: this.data.roles.filter(r => r !== role), - updatedAt: new Date() - }); - } - - private update(changes: Partial): User { - return new User({ - ...this.data, - ...changes - }); - } -} -``` - -**From `src/app/domain/repositories/user.repository.ts`:** - -```typescript -import { User } from '../entities/user.entity'; -import { Email } from '../value-objects/email.value-object'; - -export interface UserRepository { - findAll(): Promise; - findById(id: number): Promise; - findByEmail(email: Email): Promise; - findActiveUsers(): Promise; - save(user: User): Promise; - delete(id: number): Promise; - existsByEmail(email: Email): Promise; -} - -// Repository token for DI -export const USER_REPOSITORY = Symbol('UserRepository'); -``` - -### 2. Application Layer Example - -**From `src/app/application/facades/users.facade.ts`:** - -```typescript -import { Injectable, computed, signal, inject, effect } from '@angular/core'; -import { User } from '../../domain/entities/user.entity'; -import { USER_REPOSITORY, UserRepository } from '../../domain/repositories/user.repository'; -import { ApplicationError } from '../errors/application-error'; -import { CreateUserUseCase } from '../use-cases/create-user.use-case'; -import { UpdateUserUseCase } from '../use-cases/update-user.use-case'; -import { DeleteUserUseCase } from '../use-cases/delete-user.use-case'; -import { LoadUsersUseCase } from '../use-cases/load-users.use-case'; -import { NOTIFICATION_SERVICE, NotificationService } from '../services/notification.service'; - -@Injectable({ providedIn: 'root' }) -export class UsersFacade { - // Dependencies - private readonly userRepository = inject(USER_REPOSITORY); - private readonly notificationService = inject(NOTIFICATION_SERVICE); - - // Use cases - private readonly createUserUseCase = inject(CreateUserUseCase); - private readonly updateUserUseCase = inject(UpdateUserUseCase); - private readonly deleteUserUseCase = inject(DeleteUserUseCase); - private readonly loadUsersUseCase = inject(LoadUsersUseCase); - - // Private state signals - private readonly _users = signal([]); - private readonly _loading = signal(false); - private readonly _error = signal(null); - private readonly _selectedUserId = signal(null); - - // Public computed state - readonly users = computed(() => this._users()); - readonly loading = computed(() => this._loading()); - readonly error = computed(() => this._error()); - readonly selectedUserId = computed(() => this._selectedUserId()); - - // Derived computed values - readonly activeUsers = computed(() => - this._users().filter(user => user.isActive) - ); - - readonly adminUsers = computed(() => - this._users().filter(user => user.hasAdminRole) - ); - - readonly userCount = computed(() => this._users().length); - readonly activeUserCount = computed(() => this.activeUsers().length); - readonly hasUsers = computed(() => this.userCount() > 0); - readonly isEmpty = computed(() => !this.loading() && !this.hasUsers()); - - readonly selectedUser = computed(() => { - const id = this._selectedUserId(); - return id ? this._users().find(user => user.data.id === id) || null : null; - }); - - // Effects for side effects - private readonly errorNotificationEffect = effect(() => { - const error = this._error(); - if (error) { - this.notificationService.error(error.message); - } - }); - - // Public methods - async loadUsers(): Promise { - this._loading.set(true); - this._error.set(null); - - try { - const result = await this.loadUsersUseCase.execute(); - - if (result.success) { - this._users.set(result.data); - this.notificationService.info(`Loaded ${result.data.length} users`); - } else { - this._error.set(result.error); - } - } catch (error) { - const appError = new ApplicationError( - 'users', - 'Failed to load users', - 'LOAD_USERS_ERROR', - error - ); - this._error.set(appError); - } finally { - this._loading.set(false); - } - } - - async createUser(userData: CreateUserData): Promise { - this._loading.set(true); - this._error.set(null); - - try { - const result = await this.createUserUseCase.execute(userData); - - if (result.success) { - this._users.update(users => [...users, result.data]); - this.notificationService.success('User created successfully'); - } else { - this._error.set(result.error); - } - } catch (error) { - const appError = new ApplicationError( - 'users', - 'Failed to create user', - 'CREATE_USER_ERROR', - error - ); - this._error.set(appError); - } finally { - this._loading.set(false); - } - } - - async updateUser(id: number, updates: UpdateUserData): Promise { - this._loading.set(true); - this._error.set(null); - - try { - const result = await this.updateUserUseCase.execute(id, updates); - - if (result.success) { - this._users.update(users => - users.map(user => - user.data.id === id ? result.data : user - ) - ); - this.notificationService.success('User updated successfully'); - } else { - this._error.set(result.error); - } - } catch (error) { - const appError = new ApplicationError( - 'users', - 'Failed to update user', - 'UPDATE_USER_ERROR', - error - ); - this._error.set(appError); - } finally { - this._loading.set(false); - } - } - - async deleteUser(id: number): Promise { - this._loading.set(true); - this._error.set(null); - - try { - const result = await this.deleteUserUseCase.execute(id); - - if (result.success) { - this._users.update(users => - users.filter(user => user.data.id !== id) - ); - - // Clear selection if deleted user was selected - if (this._selectedUserId() === id) { - this._selectedUserId.set(null); - } - - this.notificationService.success('User deleted successfully'); - } else { - this._error.set(result.error); - } - } catch (error) { - const appError = new ApplicationError( - 'users', - 'Failed to delete user', - 'DELETE_USER_ERROR', - error - ); - this._error.set(appError); - } finally { - this._loading.set(false); - } - } - - selectUser(id: number | null): void { - this._selectedUserId.set(id); - } - - clearError(): void { - this._error.set(null); - } - - refresh(): Promise { - return this.loadUsers(); - } -} -``` - -**From `src/app/application/use-cases/create-user.use-case.ts`:** - -```typescript -import { Injectable, inject } from '@angular/core'; -import { User } from '../../domain/entities/user.entity'; -import { Email } from '../../domain/value-objects/email.value-object'; -import { USER_REPOSITORY, UserRepository } from '../../domain/repositories/user.repository'; -import { ApplicationError } from '../errors/application-error'; -import { CreateUserData, Result } from '../types/users.types'; - -@Injectable({ providedIn: 'root' }) -export class CreateUserUseCase { - private readonly userRepository = inject(USER_REPOSITORY); - - async execute(data: CreateUserData): Promise> { - try { - // Validate email format - const email = Email.create(data.email); - - // Check if user already exists - const existingUser = await this.userRepository.findByEmail(email); - if (existingUser) { - return { - success: false, - error: new ApplicationError( - 'users', - `User with email ${data.email} already exists`, - 'USER_ALREADY_EXISTS' - ) - }; - } - - // Create domain entity - const user = User.create({ - email: data.email, - firstName: data.firstName, - lastName: data.lastName, - active: data.active ?? true, - roles: data.roles ?? [] - }); - - // Save to repository - const savedUser = await this.userRepository.save(user); - - return { - success: true, - data: savedUser - }; - } catch (error) { - return { - success: false, - error: error instanceof ApplicationError - ? error - : new ApplicationError( - 'users', - 'Failed to create user', - 'CREATE_USER_ERROR', - error - ) - }; - } - } -} -``` - -### 3. Infrastructure Layer Example - -**From `src/app/infrastructure/repositories/http-user.repository.ts`:** - -```typescript -import { Injectable, inject } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { User } from '../../domain/entities/user.entity'; -import { UserRepository } from '../../domain/repositories/user.repository'; -import { Email } from '../../domain/value-objects/email.value-object'; -import { UserDto } from '../dtos/user.dto'; -import { UserMapper } from '../mappers/user.mapper'; -import { ENVIRONMENT } from '../../core/tokens'; - -@Injectable() -export class HttpUserRepository implements UserRepository { - private readonly http = inject(HttpClient); - private readonly environment = inject(ENVIRONMENT); - private readonly mapper = inject(UserMapper); - - private readonly baseUrl = `${this.environment.apiUrl}/users`; - - async findAll(): Promise { - const dtos = await this.http.get(this.baseUrl).toPromise(); - return dtos?.map(dto => this.mapper.toDomain(dto)) || []; - } - - async findById(id: number): Promise { - try { - const dto = await this.http.get(`${this.baseUrl}/${id}`).toPromise(); - return dto ? this.mapper.toDomain(dto) : null; - } catch (error) { - if (this.isNotFoundError(error)) { - return null; - } - throw error; - } - } - - async findByEmail(email: Email): Promise { - try { - const dto = await this.http.get(`${this.baseUrl}/by-email/${email.value}`).toPromise(); - return dto ? this.mapper.toDomain(dto) : null; - } catch (error) { - if (this.isNotFoundError(error)) { - return null; - } - throw error; - } - } - - async findActiveUsers(): Promise { - const dtos = await this.http.get(`${this.baseUrl}/active`).toPromise(); - return dtos?.map(dto => this.mapper.toDomain(dto)) || []; - } - - async save(user: User): Promise { - const dto = this.mapper.toDto(user); - - if (user.data.id === 0) { - // Create new user - const createdDto = await this.http.post(this.baseUrl, dto).toPromise(); - return this.mapper.toDomain(createdDto!); - } else { - // Update existing user - const updatedDto = await this.http.put(`${this.baseUrl}/${user.data.id}`, dto).toPromise(); - return this.mapper.toDomain(updatedDto!); - } - } - - async delete(id: number): Promise { - await this.http.delete(`${this.baseUrl}/${id}`).toPromise(); - } - - async existsByEmail(email: Email): Promise { - try { - await this.http.head(`${this.baseUrl}/by-email/${email.value}`).toPromise(); - return true; - } catch (error) { - if (this.isNotFoundError(error)) { - return false; - } - throw error; - } - } - - private isNotFoundError(error: any): boolean { - return error?.status === 404; - } -} -``` - -## Domain-Driven Design Patterns - -### 1. Value Objects Example - -**From `src/app/domain/value-objects/email.value-object.ts`:** - -```typescript -export class Email { - private static readonly EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - - private constructor(private readonly _value: string) {} - - static create(value: string): Email { - Email.validate(value); - return new Email(value.toLowerCase().trim()); - } - - static validate(value: string): void { - if (!value || !value.trim()) { - throw new Error('Email is required'); - } - - if (!Email.EMAIL_REGEX.test(value)) { - throw new Error('Invalid email format'); - } - - if (value.length > 254) { - throw new Error('Email is too long'); - } - } - - get value(): string { - return this._value; - } - - get domain(): string { - return this._value.split('@')[1]; - } - - get localPart(): string { - return this._value.split('@')[0]; - } - - equals(other: Email): boolean { - return this._value === other._value; - } - - toString(): string { - return this._value; - } -} -``` - -### 2. Domain Events Example - -**From `src/app/domain/events/user-created.event.ts`:** - -```typescript -import { DomainEvent } from './domain-event.base'; -import { User } from '../entities/user.entity'; - -export class UserCreatedEvent implements DomainEvent { - readonly eventType = 'UserCreated'; - readonly aggregateId: number; - readonly occurredOn: Date; - readonly version: number; - - constructor( - public readonly user: User, - version: number = 1 - ) { - this.aggregateId = user.data.id; - this.occurredOn = new Date(); - this.version = version; - } - - getEventData(): Record { - return { - userId: this.user.data.id, - email: this.user.data.email, - fullName: this.user.fullName, - roles: this.user.data.roles, - createdAt: this.user.data.createdAt - }; - } -} -``` - -### 3. Domain Services Example - -**From `src/app/domain/services/user-validation.service.ts`:** - -```typescript -import { Injectable, inject } from '@angular/core'; -import { User } from '../entities/user.entity'; -import { Email } from '../value-objects/email.value-object'; -import { USER_REPOSITORY, UserRepository } from '../repositories/user.repository'; - -export interface UserValidationResult { - isValid: boolean; - errors: string[]; -} - -@Injectable({ providedIn: 'root' }) -export class UserValidationService { - private readonly userRepository = inject(USER_REPOSITORY); - - async validateForCreation(userData: { - email: string; - firstName: string; - lastName: string; - }): Promise { - const errors: string[] = []; - - // Validate email format - try { - Email.validate(userData.email); - } catch (error) { - errors.push(error.message); - } - - // Check email uniqueness - if (errors.length === 0) { - const email = Email.create(userData.email); - const existingUser = await this.userRepository.findByEmail(email); - if (existingUser) { - errors.push('Email is already in use'); - } - } - - // Validate names - if (!userData.firstName.trim()) { - errors.push('First name is required'); - } else if (userData.firstName.length < 2) { - errors.push('First name must be at least 2 characters'); - } else if (userData.firstName.length > 50) { - errors.push('First name must be less than 50 characters'); - } - - if (!userData.lastName.trim()) { - errors.push('Last name is required'); - } else if (userData.lastName.length < 2) { - errors.push('Last name must be at least 2 characters'); - } else if (userData.lastName.length > 50) { - errors.push('Last name must be less than 50 characters'); - } - - return { - isValid: errors.length === 0, - errors - }; - } - - async validateForUpdate( - userId: number, - updates: Partial<{ email: string; firstName: string; lastName: string }> - ): Promise { - const errors: string[] = []; - - // Validate email if provided - if (updates.email !== undefined) { - try { - Email.validate(updates.email); - - // Check uniqueness (excluding current user) - const email = Email.create(updates.email); - const existingUser = await this.userRepository.findByEmail(email); - if (existingUser && existingUser.data.id !== userId) { - errors.push('Email is already in use'); - } - } catch (error) { - errors.push(error.message); - } - } - - // Validate names if provided - if (updates.firstName !== undefined) { - if (!updates.firstName.trim()) { - errors.push('First name cannot be empty'); - } else if (updates.firstName.length < 2) { - errors.push('First name must be at least 2 characters'); - } else if (updates.firstName.length > 50) { - errors.push('First name must be less than 50 characters'); - } - } - - if (updates.lastName !== undefined) { - if (!updates.lastName.trim()) { - errors.push('Last name cannot be empty'); - } else if (updates.lastName.length < 2) { - errors.push('Last name must be at least 2 characters'); - } else if (updates.lastName.length > 50) { - errors.push('Last name must be less than 50 characters'); - } - } - - return { - isValid: errors.length === 0, - errors - }; - } -} -``` - -## Signal-Based State Management - -### 1. Complex State Example - -**From `src/app/presentation/features/users/state/users-state.service.ts`:** - -```typescript -import { Injectable, computed, signal, effect } from '@angular/core'; -import { User } from '../../../../domain/entities/user.entity'; - -export interface FilterState { - search: string; - activeOnly: boolean; - roles: string[]; - sortBy: 'name' | 'email' | 'createdAt'; - sortDirection: 'asc' | 'desc'; -} - -export interface PaginationState { - page: number; - pageSize: number; - total: number; -} - -@Injectable() -export class UsersStateService { - // Core state signals - private readonly _users = signal([]); - private readonly _loading = signal(false); - private readonly _error = signal(null); - - // Filter state signals - private readonly _searchTerm = signal(''); - private readonly _activeOnly = signal(false); - private readonly _selectedRoles = signal([]); - private readonly _sortBy = signal<'name' | 'email' | 'createdAt'>('name'); - private readonly _sortDirection = signal<'asc' | 'desc'>('asc'); - - // Pagination state signals - private readonly _currentPage = signal(1); - private readonly _pageSize = signal(10); - - // Selection state signals - private readonly _selectedUserIds = signal>(new Set()); - private readonly _selectAll = signal(false); - - // Public computed state - readonly users = computed(() => this._users()); - readonly loading = computed(() => this._loading()); - readonly error = computed(() => this._error()); - - // Filter computed values - readonly searchTerm = computed(() => this._searchTerm()); - readonly activeOnly = computed(() => this._activeOnly()); - readonly selectedRoles = computed(() => this._selectedRoles()); - readonly sortBy = computed(() => this._sortBy()); - readonly sortDirection = computed(() => this._sortDirection()); - - // Pagination computed values - readonly currentPage = computed(() => this._currentPage()); - readonly pageSize = computed(() => this._pageSize()); - - // Selection computed values - readonly selectedUserIds = computed(() => this._selectedUserIds()); - readonly selectAll = computed(() => this._selectAll()); - - // Complex computed values - readonly filteredUsers = computed(() => { - let filtered = this._users(); - const search = this._searchTerm().toLowerCase(); - const activeOnly = this._activeOnly(); - const roles = this._selectedRoles(); - - // Apply search filter - if (search) { - filtered = filtered.filter(user => - user.fullName.toLowerCase().includes(search) || - user.data.email.toLowerCase().includes(search) - ); - } - - // Apply active filter - if (activeOnly) { - filtered = filtered.filter(user => user.isActive); - } - - // Apply role filter - if (roles.length > 0) { - filtered = filtered.filter(user => - user.data.roles.some(role => roles.includes(role)) - ); - } - - return filtered; - }); - - readonly sortedUsers = computed(() => { - const users = [...this.filteredUsers()]; - const sortBy = this._sortBy(); - const direction = this._sortDirection(); - - users.sort((a, b) => { - let aValue: string | number | Date; - let bValue: string | number | Date; - - switch (sortBy) { - case 'name': - aValue = a.fullName.toLowerCase(); - bValue = b.fullName.toLowerCase(); - break; - case 'email': - aValue = a.data.email.toLowerCase(); - bValue = b.data.email.toLowerCase(); - break; - case 'createdAt': - aValue = a.data.createdAt; - bValue = b.data.createdAt; - break; - default: - return 0; - } - - if (aValue < bValue) return direction === 'asc' ? -1 : 1; - if (aValue > bValue) return direction === 'asc' ? 1 : -1; - return 0; - }); - - return users; - }); - - readonly paginatedUsers = computed(() => { - const users = this.sortedUsers(); - const page = this._currentPage(); - const pageSize = this._pageSize(); - const start = (page - 1) * pageSize; - const end = start + pageSize; - - return users.slice(start, end); - }); - - readonly paginationInfo = computed(() => { - const total = this.filteredUsers().length; - const page = this._currentPage(); - const pageSize = this._pageSize(); - const totalPages = Math.ceil(total / pageSize); - const start = total === 0 ? 0 : (page - 1) * pageSize + 1; - const end = Math.min(page * pageSize, total); - - return { - total, - page, - pageSize, - totalPages, - start, - end, - hasNext: page < totalPages, - hasPrevious: page > 1 - }; - }); - - readonly selectedUsers = computed(() => { - const selectedIds = this._selectedUserIds(); - return this.paginatedUsers().filter(user => - selectedIds.has(user.data.id) - ); - }); - - readonly hasSelection = computed(() => this._selectedUserIds().size > 0); - - readonly allCurrentPageSelected = computed(() => { - const currentPageUsers = this.paginatedUsers(); - const selectedIds = this._selectedUserIds(); - - return currentPageUsers.length > 0 && - currentPageUsers.every(user => selectedIds.has(user.data.id)); - }); - - // Effects for automatic state management - private readonly resetPageOnFilterChange = effect(() => { - // Reset to first page when filters change - this._searchTerm(); - this._activeOnly(); - this._selectedRoles(); - this._currentPage.set(1); - }); - - private readonly updateSelectAllState = effect(() => { - const allSelected = this.allCurrentPageSelected(); - this._selectAll.set(allSelected); - }); - - private readonly clearSelectionOnPageChange = effect(() => { - this._currentPage(); - this._selectedUserIds.set(new Set()); - }); - - // State mutations - setUsers(users: User[]): void { - this._users.set(users); - } - - setLoading(loading: boolean): void { - this._loading.set(loading); - } - - setError(error: string | null): void { - this._error.set(error); - } - - setSearchTerm(search: string): void { - this._searchTerm.set(search); - } - - setActiveOnly(activeOnly: boolean): void { - this._activeOnly.set(activeOnly); - } - - setSelectedRoles(roles: string[]): void { - this._selectedRoles.set(roles); - } - - setSorting(sortBy: 'name' | 'email' | 'createdAt', direction: 'asc' | 'desc'): void { - this._sortBy.set(sortBy); - this._sortDirection.set(direction); - } - - setPage(page: number): void { - const maxPage = this.paginationInfo().totalPages; - if (page >= 1 && page <= maxPage) { - this._currentPage.set(page); - } - } - - setPageSize(pageSize: number): void { - this._pageSize.set(pageSize); - this._currentPage.set(1); // Reset to first page - } - - selectUser(userId: number): void { - const selected = new Set(this._selectedUserIds()); - selected.add(userId); - this._selectedUserIds.set(selected); - } - - deselectUser(userId: number): void { - const selected = new Set(this._selectedUserIds()); - selected.delete(userId); - this._selectedUserIds.set(selected); - } - - toggleUserSelection(userId: number): void { - const selected = this._selectedUserIds(); - if (selected.has(userId)) { - this.deselectUser(userId); - } else { - this.selectUser(userId); - } - } - - selectAllCurrentPage(): void { - const currentPageUsers = this.paginatedUsers(); - const selected = new Set(this._selectedUserIds()); - - currentPageUsers.forEach(user => selected.add(user.data.id)); - this._selectedUserIds.set(selected); - } - - deselectAllCurrentPage(): void { - const currentPageUsers = this.paginatedUsers(); - const selected = new Set(this._selectedUserIds()); - - currentPageUsers.forEach(user => selected.delete(user.data.id)); - this._selectedUserIds.set(selected); - } - - clearSelection(): void { - this._selectedUserIds.set(new Set()); - } - - resetFilters(): void { - this._searchTerm.set(''); - this._activeOnly.set(false); - this._selectedRoles.set([]); - this._sortBy.set('name'); - this._sortDirection.set('asc'); - } - - resetState(): void { - this.resetFilters(); - this._currentPage.set(1); - this.clearSelection(); - this._error.set(null); - } -} -``` - -## Component Examples - -### 1. Feature Component with Signals - -**From `src/app/presentation/features/users/components/user-list.component.ts`:** - -```typescript -import { Component, inject, OnInit, computed, signal, input } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormsModule } from '@angular/forms'; -import { UsersFacade } from '../../../../application/facades/users.facade'; -import { UsersStateService } from '../state/users-state.service'; -import { UserCardComponent } from './user-card.component'; -import { LoadingSpinnerComponent } from '../../../shared/components/loading-spinner.component'; -import { ErrorMessageComponent } from '../../../shared/components/error-message.component'; -import { PaginationComponent } from '../../../shared/components/pagination.component'; -import { UserRole } from '../../../../domain/enums/user-role.enum'; - -@Component({ - selector: 'app-user-list', - standalone: true, - imports: [ - CommonModule, - FormsModule, - UserCardComponent, - LoadingSpinnerComponent, - ErrorMessageComponent, - PaginationComponent - ], - providers: [UsersStateService], - template: ` -
- -
- - -
- - - -
- -
- - - -
- - -
- - - @if (state.paginatedUsers().length > 0) { -
- - - @if (state.hasSelection()) { - - {{ state.selectedUsers().length }} user(s) selected - - - - } -
- } - - - @if (facade.loading()) { - - } - - - @if (facade.error(); as error) { - - } - - - @if (!facade.loading() && state.paginatedUsers().length === 0) { -
- @if (state.filteredUsers().length === 0) { -

No users found matching your filters.

- - } @else { -

No users on this page.

- } -
- } - - - @if (state.paginatedUsers().length > 0) { -
- @for (user of state.paginatedUsers(); track user.data.id) { - - } -
- - - - } -
- `, - styleUrls: ['./user-list.component.css'] -}) -export class UserListComponent implements OnInit { - // Dependencies - protected readonly facade = inject(UsersFacade); - protected readonly state = inject(UsersStateService); - - // Inputs - readonly readonly = input(false); - - // Component state - protected readonly availableRoles = Object.values(UserRole); - - // Lifecycle - async ngOnInit(): Promise { - await this.loadUsers(); - } - - // Event handlers - onSearchChange(event: Event): void { - const target = event.target as HTMLInputElement; - this.state.setSearchTerm(target.value); - } - - onActiveFilterChange(event: Event): void { - const target = event.target as HTMLInputElement; - this.state.setActiveOnly(target.checked); - } - - onRoleFilterChange(event: Event): void { - const target = event.target as HTMLSelectElement; - const selectedRoles = Array.from(target.selectedOptions) - .map(option => option.value); - this.state.setSelectedRoles(selectedRoles); - } - - onSortChange(event: Event): void { - const target = event.target as HTMLSelectElement; - const sortBy = target.value as 'name' | 'email' | 'createdAt'; - this.state.setSorting(sortBy, this.state.sortDirection()); - } - - toggleSortDirection(): void { - const newDirection = this.state.sortDirection() === 'asc' ? 'desc' : 'asc'; - this.state.setSorting(this.state.sortBy(), newDirection); - } - - onResetFilters(): void { - this.state.resetFilters(); - } - - onSelectAllChange(event: Event): void { - const target = event.target as HTMLInputElement; - if (target.checked) { - this.state.selectAllCurrentPage(); - } else { - this.state.deselectAllCurrentPage(); - } - } - - onClearSelection(): void { - this.state.clearSelection(); - } - - onUserSelect(userId: number): void { - this.state.toggleUserSelection(userId); - } - - onUserEdit(user: User): void { - // Emit to parent or navigate to edit form - console.log('Edit user:', user); - } - - async onUserDelete(user: User): Promise { - if (confirm(`Are you sure you want to delete ${user.fullName}?`)) { - await this.facade.deleteUser(user.data.id); - } - } - - onPageChange(page: number): void { - this.state.setPage(page); - } - - onPageSizeChange(pageSize: number): void { - this.state.setPageSize(pageSize); - } - - async onRetry(): Promise { - await this.loadUsers(); - } - - onDismissError(): void { - this.facade.clearError(); - } - - // Private methods - private async loadUsers(): Promise { - await this.facade.loadUsers(); - this.state.setUsers(this.facade.users()); - } -} -``` - -### 2. Smart Component with Form Integration - -**From `src/app/presentation/features/users/components/user-form.component.ts`:** - -```typescript -import { Component, inject, input, output, effect, computed } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; -import { User } from '../../../../domain/entities/user.entity'; -import { UserRole } from '../../../../domain/enums/user-role.enum'; -import { UserValidationService } from '../../../../domain/services/user-validation.service'; -import { CreateUserData, UpdateUserData } from '../../../../application/types/users.types'; - -@Component({ - selector: 'app-user-form', - standalone: true, - imports: [CommonModule, ReactiveFormsModule], - template: ` - -

{{ formTitle() }}

- - -
- - - @if (isFieldInvalid('email')) { -
- @for (error of getFieldErrors('email'); track error) { - {{ error }} - } -
- } -
- - -
- - - @if (isFieldInvalid('firstName')) { -
- @for (error of getFieldErrors('firstName'); track error) { - {{ error }} - } -
- } -
- - -
- - - @if (isFieldInvalid('lastName')) { -
- @for (error of getFieldErrors('lastName'); track error) { - {{ error }} - } -
- } -
- - -
- -
- - -
- -
- @for (role of availableRoles; track role) { - - } -
-
- - -
- - - -
- - - @if (validationErrors().length > 0) { -
-

Please fix the following errors:

-
    - @for (error of validationErrors(); track error) { -
  • {{ error }}
  • - } -
-
- } - - `, - styleUrls: ['./user-form.component.css'] -}) -export class UserFormComponent { - // Dependencies - private readonly fb = inject(FormBuilder); - private readonly validationService = inject(UserValidationService); - - // Inputs - readonly user = input(null); - readonly submitting = input(false); - - // Outputs - readonly userSubmit = output(); - readonly formCancel = output(); - - // Form - readonly form: FormGroup; - - // Component state - readonly availableRoles = Object.values(UserRole); - - // Computed values - readonly isEditMode = computed(() => this.user() !== null); - readonly formTitle = computed(() => this.isEditMode() ? 'Edit User' : 'Create User'); - readonly submitButtonText = computed(() => this.isEditMode() ? 'Update User' : 'Create User'); - - readonly validationErrors = computed(() => { - const errors: string[] = []; - - if (this.form.get('email')?.errors?.['required']) { - errors.push('Email is required'); - } - if (this.form.get('email')?.errors?.['email']) { - errors.push('Email format is invalid'); - } - if (this.form.get('firstName')?.errors?.['required']) { - errors.push('First name is required'); - } - if (this.form.get('lastName')?.errors?.['required']) { - errors.push('Last name is required'); - } - - return errors; - }); - - constructor() { - this.form = this.fb.group({ - email: ['', [Validators.required, Validators.email]], - firstName: ['', [Validators.required, Validators.minLength(2)]], - lastName: ['', [Validators.required, Validators.minLength(2)]], - active: [true], - roles: [[] as UserRole[]] - }); - - // Effect to populate form when user changes - effect(() => { - const user = this.user(); - if (user) { - this.form.patchValue({ - email: user.data.email, - firstName: user.data.firstName, - lastName: user.data.lastName, - active: user.data.active, - roles: user.data.roles - }); - } - }); - } - - // Form helpers - isFieldInvalid(fieldName: string): boolean { - const field = this.form.get(fieldName); - return !!(field?.invalid && (field.dirty || field.touched)); - } - - getFieldErrors(fieldName: string): string[] { - const field = this.form.get(fieldName); - const errors: string[] = []; - - if (field?.errors) { - if (field.errors['required']) { - errors.push(`${fieldName} is required`); - } - if (field.errors['email']) { - errors.push('Invalid email format'); - } - if (field.errors['minlength']) { - errors.push(`${fieldName} must be at least ${field.errors['minlength'].requiredLength} characters`); - } - } - - return errors; - } - - isRoleSelected(role: UserRole): boolean { - const roles = this.form.get('roles')?.value || []; - return roles.includes(role); - } - - onRoleChange(role: UserRole, event: Event): void { - const target = event.target as HTMLInputElement; - const currentRoles = this.form.get('roles')?.value || []; - - let newRoles: UserRole[]; - if (target.checked) { - newRoles = [...currentRoles, role]; - } else { - newRoles = currentRoles.filter((r: UserRole) => r !== role); - } - - this.form.patchValue({ roles: newRoles }); - } - - async onSubmit(): Promise { - if (this.form.valid) { - const formValue = this.form.value; - - // Additional validation - const validationResult = this.isEditMode() - ? await this.validationService.validateForUpdate( - this.user()!.data.id, - formValue - ) - : await this.validationService.validateForCreation(formValue); - - if (validationResult.isValid) { - this.userSubmit.emit(formValue); - } else { - // Handle validation errors - console.error('Validation errors:', validationResult.errors); - } - } else { - // Mark all fields as touched to show validation errors - this.form.markAllAsTouched(); - } - } - - onCancel(): void { - this.formCancel.emit(); - } -} -``` - ---- - -These concrete examples demonstrate the real implementation patterns used throughout the MAD-AI project, showing how Clean Architecture, DDD, signals, and modern Angular features work together to create a maintainable and scalable application. diff --git a/.github/copilot/exemplars.md b/.github/copilot/exemplars.md deleted file mode 100644 index 39af4497..00000000 --- a/.github/copilot/exemplars.md +++ /dev/null @@ -1,898 +0,0 @@ -# MAD-AI Code Exemplars Blueprint - -**Generated**: 2025-01-24 -**Purpose**: Identify and document high-quality code examples that establish coding standards for the MAD-AI project -**Architecture**: Clean Architecture + Domain-Driven Design + Angular Signals - ---- - -## Executive Summary - -This document catalogs exemplary code implementations within the MAD-AI codebase that demonstrate: -- Modern Angular patterns with OnPush change detection -- Signal-based reactive state management -- Clean Architecture adherence with proper layer separation -- Domain-Driven Design principles -- Consistent dependency injection patterns -- Error handling and user feedback integration - -These exemplars serve as reference implementations for maintaining code quality, architectural consistency, and development standards across the project. - ---- - -## Presentation Layer Exemplars - -### 1. Register Component - OnPush + Reactive Forms Pattern - -**File**: `src/app/presentation/features/auth/register/register.component.ts` - -**Why it's exemplary**: -- Perfect OnPush change detection implementation -- Standalone component following Angular 17+ patterns -- Proper reactive form validation -- Clean separation of concerns with facade pattern -- Signal-based state management integration - -**Key patterns demonstrated**: -```typescript -@Component({ - selector: 'app-register', - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ReactiveFormsModule, /* ... */] -}) -export class RegisterComponent implements OnInit { - private readonly facade = inject(AuthFacade); - private readonly formBuilder = inject(FormBuilder); - - // Reactive state from facade - loading = this.facade.loading; - error = this.facade.error; - - form: FormGroup = this.createForm(); - - ngOnInit(): void { - this.facade.clearAuthState(); - } - - private createForm(): FormGroup { - return this.formBuilder.group({ - email: ['', [Validators.required, Validators.email]], - password: ['', [Validators.required, Validators.minLength(8)]] - }); - } -} -``` - -**Standards established**: -- Always use OnPush change detection for components -- Inject facades for business logic coordination -- Use reactive forms with proper validation -- Clear state management on component initialization - ---- - -### 2. Error Details Component - Signal-Based State Management - -**File**: `src/app/presentation/features/admin/users/error-details/error-details.component.ts` - -**Why it's exemplary**: -- Modern signal-based component architecture -- Clean input property handling -- Proper change detection optimization -- Self-contained error display logic - -**Key patterns demonstrated**: -```typescript -@Component({ - selector: 'app-error-details', - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush, - template: ` - @if (error()) { -
-

{{ error()?.message }}

-

{{ error()?.details }}

-
- } - ` -}) -export class ErrorDetailsComponent { - error = input(); - - // Computed signals for derived state - hasError = computed(() => !!this.error()); - errorType = computed(() => this.error()?.type || 'unknown'); -} -``` - -**Standards established**: -- Use signal inputs for component properties -- Leverage computed signals for derived state -- Keep templates reactive with @if control flow -- Maintain component isolation and reusability - ---- - -### 3. Toast Container - Event Handling Excellence - -**File**: `src/app/presentation/features/notifications/toast-container/toast-container.component.ts` - -**Why it's exemplary**: -- Comprehensive event handling patterns -- Perfect integration with notification facade -- Animation and lifecycle management -- Accessibility considerations - -**Key patterns demonstrated**: -- Reactive state subscription from facades -- Event-driven architecture for user interactions -- Proper cleanup and memory management -- Animation coordination with Angular signals - -**Standards established**: -- Integrate with facade services for data management -- Implement proper event handling for user interactions -- Consider accessibility in UI component design -- Use signals for coordinating animations and state - ---- - -### 4. Icon Component - Dependency Injection Pattern - -**File**: `src/app/shared/ui/icon/icon.component.ts` - -**Why it's exemplary**: -- Clean dependency injection implementation -- Service integration for icon management -- Reusable component architecture -- Type-safe icon handling - -**Key patterns demonstrated**: -```typescript -@Component({ - selector: 'app-icon', - standalone: true, - changeDetection: ChangeDetectionStrategy.OnPush -}) -export class IconComponent { - private readonly iconService = inject(IconService); - - name = input.required(); - size = input<'sm' | 'md' | 'lg'>('md'); - - iconData = computed(() => this.iconService.getIcon(this.name())); -} -``` - -**Standards established**: -- Use dependency injection for service integration -- Implement required and optional inputs appropriately -- Create computed properties for derived data -- Maintain type safety throughout component - ---- - -### 5. Navigation Layout - Complex UI State Management - -**File**: `src/app/presentation/layouts/main-layout/main-layout.component.ts` - -**Why it's exemplary**: -- Complex state coordination between multiple services -- Responsive design patterns with signal-based state -- Integration with authentication and layout services -- Proper template structure with control flow - -**Standards established**: -- Coordinate multiple services through facade patterns -- Use signals for responsive state management -- Implement proper authentication state checking -- Structure templates with modern Angular control flow - ---- - -## Application Layer Exemplars - -### 1. Authentication Facade - Master Facade Pattern - -**File**: `src/app/application/facades/auth.facade.ts` - -**Why it's exemplary**: -- Perfect facade pattern implementation -- Comprehensive use case orchestration -- Signal-based reactive state management -- Cross-facade coordination with notifications -- Robust error handling and transformation - -**Key patterns demonstrated**: -```typescript -@Injectable({ providedIn: 'root' }) -export class AuthFacade { - // Use Case Dependencies - private readonly loginUC = inject(LoginWithCredentials); - private readonly logoutUC = inject(Logout); - private readonly profileUC = inject(GetProfile); - private readonly registerUC = inject(Register); - - // Cross-Facade Dependencies - private readonly notifications = inject(NotificationsFacade); - private readonly errorTransformer = inject(ApplicationErrorTransformer); - - // Private State Signals - private readonly _loading = signal(false); - private readonly _user = signal(null); - private readonly _session = signal(null); - private readonly _authError = signal(null); - - // Public Computed State - readonly loading = computed(() => this._loading()); - readonly user = computed(() => this._user()); - readonly isAuthenticated = computed(() => !!this._session()); - readonly error = computed(() => this._authError()); - - async login(request: LoginRequest, opts?: FacadeOpts): Promise { - if (!opts?.skipLoading) this._loading.set(true); - this._authError.set(null); - - try { - const session = await this.loginUC.execute(request); - this._session.set(session); - this._user.set(session.user); - - if (!opts?.skipNotifications) { - await this.notifications.success('Login successful'); - } - } catch (error) { - const transformedError = this.errorTransformer.transform(error); - this._authError.set(transformedError); - - if (!opts?.skipNotifications) { - await this.notifications.error(transformedError.userMessage); - } - } finally { - if (!opts?.skipLoading) this._loading.set(false); - } - } -} -``` - -**Standards established**: -- Use dependency injection for all dependencies (use cases, cross-facades, transformers) -- Implement private signal state with public computed read-only accessors -- Follow consistent async operation patterns with loading/error states -- Integrate with notification facade for user feedback -- Use error transformation for consistent error handling -- Support operation options for flexibility (skipLoading, skipNotifications) - ---- - -### 2. Notifications Facade - Real-time State Synchronization - -**File**: `src/app/application/facades/notifications.facade.ts` - -**Why it's exemplary**: -- Real-time subscription management with observables -- Service layer synchronization patterns -- Event-driven architecture for cross-facade communication -- Comprehensive notification lifecycle management - -**Key patterns demonstrated**: -```typescript -@Injectable({ providedIn: 'root' }) -export class NotificationsFacade { - // Use Case Dependencies - private readonly notifyUC = inject(Notify); - private readonly dismissUC = inject(DismissNotification); - private readonly clearUC = inject(ClearNotifications); - - // Private Reactive State - private readonly _notifications = signal([]); - private readonly _loading = signal(false); - private readonly _unreadCount = signal(0); - - // Real-time subscription management - private notificationSubject = new BehaviorSubject(null); - - constructor() { - this.initializeNotificationSync(); - } - - // Public Reactive State (Computed - Read-only) - readonly notifications = computed(() => this._notifications()); - readonly unreadCount = computed(() => this._unreadCount()); - readonly hasNotifications = computed(() => this._notifications().length > 0); - - private initializeNotificationSync(): void { - try { - const callback = (notifications: Notification[]) => { - this._notifications.set(notifications); - this._unreadCount.set(notifications.filter(n => !n.isRead).length); - }; - - this.subscribeUC.execute(callback); - } catch (error) { - console.warn('NotificationsFacade: Failed to initialize sync', error); - } - } -} -``` - -**Standards established**: -- Initialize real-time synchronization in constructor -- Use BehaviorSubject for event coordination -- Implement service layer synchronization patterns -- Provide computed properties for derived state -- Handle synchronization errors gracefully - ---- - -### 3. Users Facade - Complex State Orchestration - -**File**: `src/app/application/facades/users.facade.ts` - -**Why it's exemplary**: -- Comprehensive CRUD operation orchestration -- Multi-use case coordination for complex workflows -- Event emission for cross-facade communication -- Bulk operation handling with proper feedback - -**Standards established**: -- Orchestrate multiple use cases for complex business workflows -- Emit events for cross-facade coordination and communication -- Handle bulk operations with progress tracking -- Provide convenience methods for common operations - ---- - -### 4. Layout Service - Cross-cutting State Management - -**File**: `src/app/core/cross-cutting/ui-state/layout.service.ts` - -**Why it's exemplary**: -- SSR-safe initialization patterns -- Effect-based state persistence -- Responsive design state management -- Window resize handling with Angular signals - -**Key patterns demonstrated**: -```typescript -@Injectable({ providedIn: 'root' }) -export class LayoutService { - private readonly _sidebarCollapsed = signal(false); - private readonly _mobileDrawerOpen = signal(false); - - constructor() { - this.initializeFromStorage(); - - if (typeof window !== 'undefined') { - this.setupWindowResize(); - } - - // Persist sidebar state - effect(() => { - if (typeof window !== 'undefined') { - const collapsed = this._sidebarCollapsed(); - localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(collapsed)); - } - }); - - // Auto-close mobile drawer when switching to desktop - effect(() => { - if (!this.isMobile() && this._mobileDrawerOpen()) { - this._mobileDrawerOpen.set(false); - } - }); - } -} -``` - -**Standards established**: -- Check for browser environment before using browser APIs -- Use effects for state persistence and side effects -- Implement responsive behavior with signal coordination -- Initialize state from storage safely in SSR environments - ---- - -### 5. Application Error Transformer - Error Handling Excellence - -**File**: `src/app/application/errors/application-error.transformer.ts` - -**Why it's exemplary**: -- Consistent error transformation patterns -- User-friendly error message generation -- Type-safe error handling -- Integration with application layer concerns - -**Standards established**: -- Transform all errors to application layer format -- Provide user-friendly error messages -- Maintain error type safety throughout transformation -- Integrate error handling with application concerns - ---- - -## Domain Layer Exemplars - -### 1. User Entity - Domain Logic Encapsulation - -**File**: `src/app/domain/entities/user.entity.ts` - -**Why it's exemplary**: -- Proper domain entity structure with business rules -- Value object integration -- Domain validation logic -- Immutable state patterns - -**Standards established**: -- Encapsulate business logic within entity methods -- Use value objects for complex properties -- Implement domain validation rules -- Maintain entity immutability where appropriate - ---- - -### 2. Email Value Object - Validation and Immutability - -**File**: `src/app/domain/value-objects/email.value-object.ts` - -**Why it's exemplary**: -- Immutable value object implementation -- Domain validation logic -- Type safety and encapsulation -- Equality comparison methods - -**Standards established**: -- Implement value objects as immutable structures -- Encapsulate validation logic within value objects -- Provide equality comparison methods -- Maintain type safety for domain concepts - ---- - -### 3. Domain Events - Event-Driven Architecture - -**File**: `src/app/domain/events/user-registered.event.ts` - -**Why it's exemplary**: -- Clean domain event structure -- Immutable event data -- Timestamp and metadata inclusion -- Type-safe event payload - -**Standards established**: -- Structure domain events with immutable data -- Include relevant metadata (timestamp, event type) -- Maintain type safety for event payloads -- Follow consistent naming conventions - ---- - -### 4. Repository Contracts - Interface Segregation - -**File**: `src/app/domain/contracts/user.repository.ts` - -**Why it's exemplary**: -- Clear interface segregation -- Async operation patterns -- Domain-focused method signatures -- Error handling contracts - -**Standards established**: -- Define repository interfaces in domain layer -- Use async patterns for all data operations -- Focus on domain concepts rather than data structures -- Specify error handling expectations - ---- - -### 5. Domain Services - Business Logic Coordination - -**File**: `src/app/domain/services/user-validation.service.ts` - -**Why it's exemplary**: -- Pure business logic implementation -- No framework dependencies -- Composable validation rules -- Clear service boundaries - -**Standards established**: -- Keep domain services free of framework dependencies -- Implement pure business logic functions -- Create composable and testable validation rules -- Maintain clear service boundaries and responsibilities - ---- - -## Infrastructure Layer Exemplars - -### 1. HTTP User Repository - Repository Implementation - -**File**: `src/app/infrastructure/repositories/user-http.repository.ts` - -**Why it's exemplary**: -- Clean repository pattern implementation -- DTO to entity mapping -- HTTP client integration with error handling -- Angular HTTP client patterns - -**Key patterns demonstrated**: -```typescript -@Injectable({ providedIn: 'root' }) -export class UserHttpRepository implements UserRepository { - private readonly http = inject(HttpClient); - private readonly mapper = inject(UserMapper); - - async getById(id: UserId): Promise { - try { - const response = await firstValueFrom( - this.http.get(`/api/users/${id.value}`) - ); - return this.mapper.toDomain(response); - } catch (error) { - throw new RepositoryError('Failed to fetch user', error); - } - } - - async save(user: User): Promise { - try { - const dto = this.mapper.toDto(user); - await firstValueFrom( - this.http.put(`/api/users/${user.id.value}`, dto) - ); - } catch (error) { - throw new RepositoryError('Failed to save user', error); - } - } -} -``` - -**Standards established**: -- Implement domain repository interfaces in infrastructure -- Use mappers for DTO to entity conversion -- Handle HTTP errors and transform to domain errors -- Use firstValueFrom for converting observables to promises - ---- - -### 2. User Mapper - Data Transformation Excellence - -**File**: `src/app/infrastructure/mappers/user.mapper.ts` - -**Why it's exemplary**: -- Bidirectional mapping between DTOs and entities -- Validation during mapping process -- Error handling for malformed data -- Type-safe transformation logic - -**Standards established**: -- Provide bidirectional mapping methods (toDomain, toDto) -- Validate data during transformation -- Handle mapping errors appropriately -- Maintain type safety throughout transformation - ---- - -### 3. HTTP Error Mapper - Error Transformation - -**File**: `src/app/infrastructure/errors/http-error.mapper.ts` - -**Why it's exemplary**: -- Comprehensive HTTP error status handling -- Transformation to domain error types -- User-friendly error message generation -- Consistent error format creation - -**Standards established**: -- Map HTTP status codes to domain error types -- Generate appropriate user-facing error messages -- Maintain error context and details -- Follow consistent error transformation patterns - ---- - -### 4. HTTP Interceptor - Cross-cutting Concerns - -**File**: `src/app/core/interceptors/auth.interceptor.ts` - -**Why it's exemplary**: -- Clean interceptor implementation -- Token management integration -- Error handling for authentication failures -- Request/response transformation - -**Standards established**: -- Implement interceptors for cross-cutting HTTP concerns -- Integrate with authentication services for token management -- Handle authentication errors consistently -- Transform requests and responses as needed - ---- - -### 5. Local Storage Service - Data Persistence - -**File**: `src/app/infrastructure/services/storage.service.ts` - -**Why it's exemplary**: -- SSR-safe storage operations -- Type-safe data serialization -- Error handling for storage failures -- Consistent API for data persistence - -**Standards established**: -- Check for browser environment before storage operations -- Implement type-safe serialization/deserialization -- Handle storage errors gracefully -- Provide consistent API for data persistence needs - ---- - -## Core Layer Exemplars - -### 1. HTTP Client Service - API Integration - -**File**: `src/app/core/cross-cutting/http/http-client.service.ts` - -**Why it's exemplary**: -- Centralized HTTP configuration -- Request/response interceptor integration -- Error handling standardization -- Type-safe API methods - -**Standards established**: -- Centralize HTTP client configuration -- Integrate with interceptors for cross-cutting concerns -- Standardize error handling across API calls -- Provide type-safe methods for common HTTP operations - ---- - -### 2. Validation Utilities - Reusable Validation Logic - -**File**: `src/app/core/cross-cutting/utilities/validation.utils.ts` - -**Why it's exemplary**: -- Pure validation functions -- Composable validation rules -- Type-safe validation logic -- Framework-agnostic implementation - -**Standards established**: -- Create pure, composable validation functions -- Maintain type safety in validation logic -- Keep utilities framework-agnostic -- Provide clear and reusable validation patterns - ---- - -### 3. Logger Service - Structured Logging - -**File**: `src/app/core/cross-cutting/utilities/logger.service.ts` - -**Why it's exemplary**: -- Structured logging implementation -- Multiple log level support -- Environment-specific configuration -- Integration with external logging services - -**Standards established**: -- Implement structured logging with consistent format -- Support multiple log levels for different scenarios -- Configure logging based on environment -- Integrate with external logging services when needed - ---- - -### 4. Date Utilities - Domain-Specific Helpers - -**File**: `src/app/core/cross-cutting/utilities/date.utils.ts` - -**Why it's exemplary**: -- Pure utility functions for date operations -- Business logic specific to application needs -- Type-safe date handling -- Timezone-aware date operations - -**Standards established**: -- Create pure utility functions for common operations -- Implement business-specific date logic -- Handle timezones appropriately -- Maintain type safety in utility functions - ---- - -### 5. Configuration Service - Environment Management - -**File**: `src/app/core/cross-cutting/utilities/config.service.ts` - -**Why it's exemplary**: -- Environment-specific configuration management -- Type-safe configuration access -- Default value handling -- Runtime configuration validation - -**Standards established**: -- Manage environment-specific configuration centrally -- Provide type-safe access to configuration values -- Handle default values appropriately -- Validate configuration at runtime - ---- - -## DI Layer Exemplars - -### 1. Authentication Providers - Service Registration - -**File**: `src/app/di/provide-auth.ts` - -**Why it's exemplary**: -- Clean provider registration patterns -- Token-based dependency injection -- Factory provider implementations -- Environment-specific provider configuration - -**Key patterns demonstrated**: -```typescript -export const provideAuth = (): Provider[] => [ - // Use Cases - LoginWithCredentials, - Logout, - GetProfile, - Register, - RefreshSession, - ConfirmEmail, - RequestPasswordReset, - ConfirmPasswordReset, - - // Repositories - { provide: UserRepository, useClass: UserHttpRepository }, - { provide: SessionRepository, useClass: SessionHttpRepository }, - - // Services - { provide: AUTH_CONFIG, useValue: getAuthConfig() }, - { provide: AuthService, useFactory: createAuthService, deps: [AUTH_CONFIG] }, - - // Mappers - UserMapper, - SessionMapper, -]; - -function createAuthService(config: AuthConfig): AuthService { - return new AuthService(config); -} -``` - -**Standards established**: -- Group related providers in dedicated provider functions -- Use tokens for configuration and interface-based dependencies -- Implement factory providers for complex service creation -- Register use cases, repositories, and services consistently - ---- - -### 2. Notification Providers - Event-Driven Services - -**File**: `src/app/di/provide-notifications.ts` - -**Why it's exemplary**: -- Event-driven service registration -- Real-time subscription configuration -- Cross-cutting service integration -- Proper dependency graph management - -**Standards established**: -- Register event-driven services with proper configuration -- Configure real-time subscriptions and event handling -- Integrate cross-cutting services (logging, monitoring) -- Manage complex dependency graphs effectively - ---- - -### 3. Domain Event Providers - Event Processing - -**File**: `src/app/di/provide-domain-events.ts` - -**Why it's exemplary**: -- Domain event processor registration -- Event handler configuration -- Cross-facade event coordination -- Event sourcing infrastructure setup - -**Standards established**: -- Register domain event processors and handlers -- Configure event processing infrastructure -- Set up cross-facade event coordination -- Implement event sourcing patterns when needed - ---- - -### 4. Export Service Providers - Feature-Specific Services - -**File**: `src/app/di/provide-export.ts` - -**Why it's exemplary**: -- Feature-specific service registration -- Strategy pattern implementation for export formats -- Configuration-driven service selection -- Plugin-style architecture support - -**Standards established**: -- Register feature-specific services in dedicated modules -- Implement strategy patterns for variable behavior -- Use configuration to drive service selection -- Support plugin-style architecture when appropriate - ---- - -### 5. Dependency Tokens - Type-Safe Injection - -**File**: `src/app/di/tokens.ts` - -**Why it's exemplary**: -- Type-safe injection token definitions -- Clear token naming conventions -- Configuration token patterns -- Interface-based token registration - -**Key patterns demonstrated**: -```typescript -// Configuration tokens -export const AUTH_CONFIG = new InjectionToken('auth.config'); -export const API_CONFIG = new InjectionToken('api.config'); - -// Service tokens for interfaces -export const UserRepository = new InjectionToken('user.repository'); -export const NotificationService = new InjectionToken('notification.service'); - -// Feature tokens -export const EXPORT_STRATEGIES = new InjectionToken('export.strategies'); -``` - -**Standards established**: -- Define injection tokens for all interface-based dependencies -- Use descriptive token names with appropriate namespacing -- Create configuration tokens for environment-specific values -- Group related tokens together for better organization - ---- - -## Summary of Architectural Standards - -Based on these exemplars, the MAD-AI project establishes the following key standards: - -### Component Standards -- Always use `ChangeDetectionStrategy.OnPush` for optimal performance -- Implement standalone components following Angular 17+ patterns -- Use signal-based state management with computed properties -- Inject facades for business logic coordination -- Clear component state on initialization - -### Service Standards -- Implement facade pattern for application layer services -- Use dependency injection for all service dependencies -- Provide private signal state with public computed read-only accessors -- Follow consistent async operation patterns with loading/error states -- Integrate with notification facade for user feedback - -### State Management Standards -- Use Angular signals for reactive state management -- Implement private signals with public computed accessors -- Use effects for side effects and state persistence -- Handle SSR safely with environment checks -- Coordinate cross-service state through facade patterns - -### Error Handling Standards -- Transform all errors to application layer format -- Provide user-friendly error messages -- Maintain error type safety throughout the application -- Integrate error handling with notification systems -- Handle errors gracefully without breaking user experience - -### Dependency Injection Standards -- Group related providers in dedicated provider functions -- Use injection tokens for interface-based dependencies -- Implement factory providers for complex service creation -- Register services consistently across layers -- Manage dependency graphs effectively - -These exemplars demonstrate the high quality and consistency of the MAD-AI codebase, providing clear patterns for future development while maintaining architectural integrity and code quality. diff --git a/.github/copilot/folder-structure.md b/.github/copilot/folder-structure.md deleted file mode 100644 index 98baf29a..00000000 --- a/.github/copilot/folder-structure.md +++ /dev/null @@ -1,526 +0,0 @@ -# Project Folders Structure Blueprint - -**Document Version:** 1.0 -**Generated:** December 2024 -**Project:** MAD-AI (Angular + Clean Architecture + DDD) -**Purpose:** Comprehensive folder structure documentation and organizational guidelines - ---- - -## Table of Contents - -1. [Executive Summary](#executive-summary) -2. [Project Structure Overview](#project-structure-overview) -3. [Root Directory Structure](#root-directory-structure) -4. [Source Code Organization](#source-code-organization) -5. [Clean Architecture Layer Mapping](#clean-architecture-layer-mapping) -6. [Feature Organization Patterns](#feature-organization-patterns) -7. [File Naming Conventions](#file-naming-conventions) -8. [Directory Guidelines](#directory-guidelines) -9. [Shared Resources Organization](#shared-resources-organization) -10. [Infrastructure and Tooling](#infrastructure-and-tooling) -11. [Documentation Structure](#documentation-structure) -12. [Templates and Examples](#templates-and-examples) - ---- - -## Executive Summary - -The MAD-AI project follows a meticulously organized folder structure that enforces Clean Architecture principles with Domain-Driven Design (DDD) patterns. The structure promotes maintainability, scalability, and clear separation of concerns across **124 directories** containing **364 source files** (TypeScript, HTML, CSS). - -### Key Architectural Principles -- **Layer Separation**: Clear boundaries between domain, application, infrastructure, and presentation layers -- **Feature-First Organization**: Business capabilities organized as self-contained feature modules -- **Shared Resource Management**: Centralized UI components, utilities, and cross-cutting concerns -- **Convention-Based Structure**: Consistent naming and organization patterns throughout the codebase - ---- - -## Project Structure Overview - -``` -MAD-AI/ -├── 📁 .github/ # GitHub workflows and templates -├── 📁 .angular/ # Angular CLI cache and build artifacts -├── 📁 docs/ # Project documentation -├── 📁 public/ # Static assets and resources -├── 📁 scripts/ # Build scripts and tooling -├── 📁 src/ # Source code (main application) -├── 📄 angular.json # Angular CLI configuration -├── 📄 package.json # Dependencies and scripts -├── 📄 tsconfig.json # TypeScript configuration -└── 📄 README.md # Project overview -``` - -### Statistics -- **Total Directories**: 124 -- **Source Files**: 364 (TS/HTML/CSS) -- **Architecture Layers**: 7 main layers -- **Feature Modules**: 4 business domains -- **Shared Components**: 6 UI component families - ---- - -## Root Directory Structure - -### Development Infrastructure -``` -├── 📁 .github/ -│ ├── instructions/ # Copilot AI instructions -│ └── workflows/ # CI/CD pipeline definitions -├── 📁 .angular/ # Angular CLI generated files -├── 📁 scripts/ -│ └── lint-icons.cjs # Custom SVG icon linting -``` - -### Documentation and Assets -``` -├── 📁 docs/ -│ ├── styles_guide.md # UI/UX style guidelines -│ └── workflow.md # Development workflow -├── 📁 public/ -│ ├── bg-placeholder.webp # Static images -│ └── favicon.ico # Browser icon -``` - -### Configuration Files -``` -├── 📄 angular.json # Angular workspace configuration -├── 📄 karma.conf.cjs # Testing framework config -├── 📄 package.json # NPM dependencies -├── 📄 tsconfig.json # TypeScript compiler config -├── 📄 tsconfig.app.json # App-specific TS config -└── 📄 tsconfig.spec.json # Test-specific TS config -``` - ---- - -## Source Code Organization - -### Primary Source Structure -``` -src/ -├── 📄 index.html # Main HTML template -├── 📄 main.ts # Application bootstrap -├── 📄 main.server.ts # SSR bootstrap -├── 📄 server.ts # Express server setup -├── 📄 styles.css # Global styles -├── 📁 app/ # Main application code -├── 📁 env/ # Environment configurations -├── 📁 styles/ # CSS architecture -└── 📁 types/ # Global type definitions -``` - -### Application Architecture (`src/app/`) -``` -app/ -├── 📄 app.ts # Root component -├── 📄 app.html # Root template -├── 📄 app.css # Root styles -├── 📄 app.config.ts # Application configuration -├── 📄 app.routes.ts # Routing configuration -├── 📁 application/ # Application layer (Clean Architecture) -├── 📁 core/ # Cross-cutting concerns -├── 📁 di/ # Dependency injection setup -├── 📁 domain/ # Domain layer (business logic) -├── 📁 infrastructure/ # Infrastructure layer (external concerns) -├── 📁 presentation/ # Presentation layer (UI) -└── 📁 shared/ # Shared resources and utilities -``` - ---- - -## Clean Architecture Layer Mapping - -### Layer 1: Domain (`src/app/domain/`) -**Purpose**: Core business logic and rules, framework-agnostic - -``` -domain/ -├── contracts/ # Interfaces and ports -│ ├── auth.contract.ts -│ ├── role.contract.ts -│ ├── user.contract.ts -│ ├── export.port.ts -│ └── session-store.contract.ts -├── entities/ # Business entities -│ ├── notification.entity.ts -│ ├── role.entity.ts -│ └── user.entity.ts -├── enums/ # Domain enumerations -├── errors/ # Domain-specific errors -├── events/ # Domain events -├── repositories/ # Repository interfaces -└── value-objects/ # Value objects -``` - -### Layer 2: Application (`src/app/application/`) -**Purpose**: Application services and use cases orchestration - -``` -application/ -├── facades/ # Application service facades -│ ├── auth.facade.ts -│ ├── notifications.facade.ts -│ ├── roles.facade.ts -│ └── users.facade.ts -├── services/ # Application services -│ ├── domain-event-processor.service.ts -│ └── role-export.service.ts -├── types/ # Application-layer types -│ ├── auth.types.ts -│ ├── facade-opts.ts -│ ├── notifications.types.ts -│ └── user.types.ts -├── use-cases/ # Business use case implementations -│ ├── auth/ -│ ├── notifications/ -│ ├── roles/ -│ └── users/ -└── errors/ # Application error handling - ├── application-error.ts - ├── application-error.transformer.ts - └── feature-handlers/ -``` - -### Layer 3: Infrastructure (`src/app/infrastructure/`) -**Purpose**: External systems integration and technical implementations - -``` -infrastructure/ -├── dtos/ # Data transfer objects -├── errors/ # Infrastructure error handling -├── http/ # HTTP client implementations -├── mappers/ # Data mapping utilities -├── repositories/ # Repository implementations -└── services/ # External service integrations -``` - -### Layer 4: Presentation (`src/app/presentation/`) -**Purpose**: User interface and presentation logic - -``` -presentation/ -├── features/ # Feature-specific UI modules -│ ├── auth/ # Authentication UI -│ ├── dashboard/ # Dashboard UI -│ ├── not-found/ # 404 error page -│ ├── roles/ # Role management UI -│ └── secured.routes.ts # Protected routing -├── layouts/ # Page layout components -├── navigation/ # Navigation components -└── shell/ # Application shell -``` - ---- - -## Feature Organization Patterns - -### Feature Module Structure -Each feature follows a consistent internal organization: - -``` -feature-name/ -├── components/ # Feature-specific components -│ ├── feature-list/ -│ ├── feature-form/ -│ └── feature-detail/ -├── pages/ # Route-level page components -│ ├── feature-list.page.ts -│ ├── feature-create.page.ts -│ └── feature-edit.page.ts -├── services/ # Feature-specific services -├── types/ # Feature-specific types -└── feature.routes.ts # Feature routing configuration -``` - -### Example: Authentication Feature -``` -auth/ -├── components/ -│ ├── login-form/ -│ ├── register-form/ -│ └── password-reset/ -├── pages/ -│ ├── login.page.ts -│ ├── register.page.ts -│ └── password-reset.page.ts -└── auth.routes.ts -``` - ---- - -## File Naming Conventions - -### Component Files -``` -component-name/ -├── component-name.ts # Component class -├── component-name.html # Template -├── component-name.css # Styles -├── component-name.spec.ts # Unit tests -└── index.ts # Barrel export -``` - -### Service and Utility Files -``` -service-name.service.ts # Angular services -utility-name.util.ts # Utility functions -helper-name.helper.ts # Helper functions -mapper-name.mapper.ts # Data mappers -contract-name.contract.ts # Domain contracts -entity-name.entity.ts # Domain entities -``` - -### Route and Configuration Files -``` -feature-name.routes.ts # Routing configuration -feature-name.config.ts # Feature configuration -feature-name.types.ts # Type definitions -feature-name.constants.ts # Constants -``` - ---- - -## Directory Guidelines - -### Core Principles - -1. **Layer Separation**: Never import from higher layers to lower layers -2. **Feature Cohesion**: Keep related functionality grouped together -3. **Shared Resources**: Centralize reusable components and utilities -4. **Clear Boundaries**: Use index.ts files for controlled exports - -### Naming Rules - -| Type | Convention | Example | -|------|------------|---------| -| Directories | kebab-case | `user-management/` | -| Components | PascalCase files | `UserCard.ts` | -| Services | camelCase + .service | `userAuth.service.ts` | -| Types | camelCase + .types | `userAuth.types.ts` | -| Constants | UPPER_CASE | `API_ENDPOINTS.ts` | - -### Directory Structure Rules - -#### ✅ Correct Patterns -``` -# Feature-first organization -presentation/features/auth/components/login-form/ - -# Clear layer separation -domain/entities/user.entity.ts -application/facades/user.facade.ts -infrastructure/repositories/user.repository.ts - -# Shared resource centralization -shared/ui/button/ -shared/components/error-view/ -``` - -#### ❌ Avoid These Patterns -``` -# Cross-layer imports -domain/entities/importing-from-infrastructure.ts - -# Scattered utilities -random-util-in-feature-folder.ts - -# Deep nesting without purpose -deep/nested/structure/without/clear/purpose/ -``` - ---- - -## Shared Resources Organization - -### UI Components (`src/app/shared/ui/`) -**Purpose**: Reusable UI building blocks - -``` -ui/ -├── button/ # Button variations -├── form-field/ # Form input components -├── icon/ # Icon system -├── input/ # Input field variants -├── theme-toggle/ # Dark/light mode toggle -└── bulk-actions-toolbar/ # Bulk action controls -``` - -### Shared Components (`src/app/shared/components/`) -**Purpose**: Complex reusable components - -``` -components/ -├── toast/ # Notification system -│ ├── types/ # Toast type definitions -│ ├── services/ # Toast service -│ ├── mappers/ # Data mapping -│ ├── enums/ # Toast enumerations -│ ├── models/ # Toast models -│ ├── toast-item/ # Individual toast component -│ └── toast-container/ # Toast container -├── error-view/ # Error display components -│ ├── components/ -│ │ ├── error-icon/ -│ │ ├── error-actions/ -│ │ └── error-details/ -│ └── error-display/ -├── page-header/ # Page header component -└── optimized-image/ # Image optimization component -``` - -### Assets (`src/app/shared/assets/`) -**Purpose**: Static resources and icons - -``` -assets/ -└── icons/ - ├── outline/ # Outline icon variants - └── filled/ # Filled icon variants -``` - ---- - -## Infrastructure and Tooling - -### Cross-Cutting Concerns (`src/app/core/`) -``` -core/ -├── cross-cutting/ # Shared utilities -│ ├── http/ # HTTP utilities -│ ├── ui-state/ # UI state management -│ └── utilities/ # General utilities -├── guards/ # Route guards -│ ├── auth.guard.ts -│ ├── email-confirmed.guard.ts -│ ├── matchers.guard.ts -│ └── role.guard.ts -└── interceptors/ # HTTP interceptors - ├── error.interceptor.ts - └── http-error.interceptor.ts -``` - -### Dependency Injection (`src/app/di/`) -**Purpose**: Centralized DI configuration - -``` -di/ -├── provide-auth.ts # Authentication providers -├── provide-domain-events.ts # Domain event providers -├── provide-export.ts # Export service providers -├── provide-icons.ts # Icon system providers -├── provide-notifications.ts # Notification providers -├── provide-roles.ts # Role management providers -├── provide-users.ts # User management providers -└── tokens.ts # DI tokens -``` - ---- - -## Documentation Structure - -### Project Documentation (`docs/`) -``` -docs/ -├── styles_guide.md # UI/UX guidelines -└── workflow.md # Development workflow -``` - -### Generated Documentation -``` -# Auto-generated during build -Project_Architecture_Blueprint.md -Technology_Stack_Blueprint.md -Project_Folders_Structure_Blueprint.md -``` - ---- - -## Templates and Examples - -### New Feature Template -When creating a new feature, follow this structure: - -```bash -# Create feature structure -mkdir -p src/app/presentation/features/new-feature/{components,pages,services,types} -mkdir -p src/app/application/use-cases/new-feature -mkdir -p src/app/domain/entities -mkdir -p src/app/infrastructure/repositories - -# Create core files -touch src/app/presentation/features/new-feature/new-feature.routes.ts -touch src/app/application/facades/new-feature.facade.ts -touch src/app/domain/contracts/new-feature.contract.ts -touch src/app/infrastructure/repositories/new-feature.repository.ts -touch src/app/di/provide-new-feature.ts -``` - -### Component Template -```typescript -// src/app/shared/ui/new-component/new-component.ts -import { Component, Input, Output, EventEmitter } from '@angular/core'; - -@Component({ - selector: 'app-new-component', - templateUrl: './new-component.html', - styleUrls: ['./new-component.css'], - standalone: true -}) -export class NewComponent { - @Input() data: any; - @Output() action = new EventEmitter(); -} -``` - -### Service Template -```typescript -// src/app/application/services/new-service.service.ts -import { Injectable } from '@angular/core'; - -@Injectable({ - providedIn: 'root' -}) -export class NewService { - // Service implementation -} -``` - -### Folder Creation Checklist - -When adding new functionality: - -- [ ] Create feature folder in appropriate layer -- [ ] Add barrel exports (`index.ts`) -- [ ] Update DI providers if needed -- [ ] Add to routing configuration -- [ ] Create corresponding test files -- [ ] Update documentation - ---- - -## Best Practices Summary - -### Organizational Principles -1. **Layer Separation**: Maintain clean boundaries between architectural layers -2. **Feature Cohesion**: Group related functionality together -3. **Shared Resources**: Centralize reusable components and utilities -4. **Clear Exports**: Use barrel files for controlled module exports - -### Maintenance Guidelines -1. **Regular Cleanup**: Remove unused files and directories -2. **Consistent Naming**: Follow established naming conventions -3. **Documentation**: Keep folder structure documentation updated -4. **Architecture Compliance**: Validate layer dependencies regularly - -### Development Workflow -1. **Plan Structure**: Design folder organization before implementation -2. **Follow Templates**: Use established patterns for new features -3. **Review Changes**: Validate structural changes against architecture -4. **Update Documentation**: Keep structure documentation current - ---- - -**Last Updated**: December 2024 -**Next Review**: Quarterly or when major structural changes occur diff --git a/.github/copilot/project_workflow.md b/.github/copilot/project_workflow.md deleted file mode 100644 index 082d64fa..00000000 --- a/.github/copilot/project_workflow.md +++ /dev/null @@ -1,1623 +0,0 @@ -# Project Workflow Analysis Blueprint - -**Document Version:** 1.0 -**Generated:** August 2025 -**Project:** MAD-AI (Angular + Clean Architecture + DDD) -**Purpose:** Comprehensive end-to-end workflow documentation serving as implementation templates - ---- - -## Table of Contents - -1. [Executive Summary](#executive-summary) -2. [Technology Stack Detection](#technology-stack-detection) -3. [Workflow 1: User Authentication (Login)](#workflow-1-user-authentication-login) -4. [Workflow 2: Role Management (CRUD)](#workflow-2-role-management-crud) -5. [Workflow 3: User Management (CRUD)](#workflow-3-user-management-crud) -6. [Workflow 4: Notification System](#workflow-4-notification-system) -7. [Workflow 5: Dashboard Navigation & State](#workflow-5-dashboard-navigation--state) -8. [Sequence Diagrams](#sequence-diagrams) -9. [Testing Patterns](#testing-patterns) -10. [Implementation Templates](#implementation-templates) -11. [Common Pitfalls & Best Practices](#common-pitfalls--best-practices) - ---- - -## Executive Summary - -This blueprint documents 5 representative end-to-end workflows in the MAD-AI Angular application, following Clean Architecture with Domain-Driven Design principles. Each workflow provides implementation-ready templates covering entry points, service orchestration, data mapping, error handling, and testing approaches. - -**Key Findings:** -- **Architecture**: Clean Architecture + DDD with clear layer separation -- **Technology**: Angular 20.1.6 + TypeScript 5.8.2 + TailwindCSS 4.1.11 -- **State Management**: Angular Signals with reactive facades -- **Pattern**: Frontend → Facade → Use Case → Repository → HTTP API - ---- - -## Technology Stack Detection - -### Primary Technologies -- **Frontend Framework**: Angular 20.1.6 with standalone components -- **Language**: TypeScript 5.8.2 with strict configuration -- **State Management**: Angular Signals for reactive programming -- **HTTP Client**: Angular HttpClient for API communication -- **UI Framework**: TailwindCSS 4.1.11 with custom component system -- **Testing**: Karma + Jasmine for unit tests -- **Build System**: Angular CLI with Vite integration - -### Architecture Pattern -- **Clean Architecture**: Clear separation between presentation, application, domain, and infrastructure layers -- **Domain-Driven Design**: Rich domain entities with business logic -- **Dependency Injection**: Angular DI container with custom tokens -- **Repository Pattern**: Abstract data access through domain contracts - -### Entry Point Characteristics -- **Primary Entry Point**: Frontend components initiating API calls -- **State Management**: Reactive facades orchestrating use cases -- **Navigation**: Angular Router with guards and resolvers -- **Forms**: Reactive forms with custom validation - -### Persistence Mechanisms -- **Primary**: External REST API via HttpClient -- **Secondary**: Browser localStorage for tokens and user data -- **Caching**: In-memory state management via signals -- **Error Handling**: Centralized error transformation and logging - ---- - -## Workflow 1: User Authentication (Login) - -### Overview -**Purpose**: Complete user authentication flow from login form submission to session establishment -**Business Value**: Secure user access with comprehensive error handling and state management -**Trigger**: User submits login form credentials - -### Files Involved -``` -src/app/presentation/features/auth/pages/login.page.ts -src/app/application/facades/auth.facade.ts -src/app/application/use-cases/auth/login.usecase.ts -src/app/domain/repositories/business/auth.repository.ts -src/app/infrastructure/repositories/http-auth.repository.ts -src/app/domain/entities/user.entity.ts -src/app/domain/entities/session.entity.ts -``` - -### Entry Point Implementation - -**Login Component (Presentation Layer)** -```typescript -@Component({ - selector: 'app-login', - templateUrl: './login.html', - styleUrl: './login.css', - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [LoginForm] -}) -export class Login implements OnInit { - auth = inject(AuthFacade); - - ngOnInit(): void { - this.auth.clearAuthStateCompletely(); - } - - async onLogin(credentials: LoginRequest): Promise { - await this.auth.login(credentials); - } -} -``` - -### Service Layer Implementation - -**AuthFacade (Application Layer)** -```typescript -@Injectable({ providedIn: 'root' }) -export class AuthFacade { - private readonly loginUC = inject(LoginWithCredentials); - private readonly notifications = inject(NotificationsFacade); - private readonly errorTransformer = inject(ApplicationErrorTransformer); - - private readonly _loading = signal(false); - private readonly _user = signal(null); - private readonly _session = signal(null); - private readonly _authError = signal(null); - - readonly user = computed(() => this._user()); - readonly session = computed(() => this._session()); - readonly loading = computed(() => this._loading()); - readonly error = computed(() => this._authError()); - - async login(request: LoginRequest, opts?: FacadeOpts): Promise { - if (!opts?.skipLoading) { - this._loading.set(true); - } - - this.clearAuthStateCompletely(); - - try { - const session = await this.loginUC.execute(request); - - this._session.set(session); - this._user.set(session.user); - - await this.notifications.success( - 'Welcome back!', - `Hello ${session.user.firstName}, you've successfully logged in.` - ); - } catch (error: any) { - const errorMessage = this.errorTransformer.transformError(error as Error, { - feature: 'auth', - operation: 'login', - }); - - this._authError.set(errorMessage); - this._session.set(null); - this._user.set(null); - } finally { - if (!opts?.skipLoading) { - this._loading.set(false); - } - } - } -} -``` - -**LoginWithCredentials Use Case (Application Layer)** -```typescript -@Injectable({ providedIn: 'root' }) -export class LoginWithCredentials { - private readonly authRepo = inject(AUTH_REPOSITORY); - private readonly sessionStore = inject(SESSION_STORE_PORT); - private readonly clock = inject(CLOCK_PORT); - private readonly eventProcessor = inject(DomainEventProcessor); - - async execute(request: LoginRequest): Promise { - try { - // 1. Validate application rules - await this.validateApplicationRules(request); - - // 2. Execute authentication through domain repository - const session = await this.authRepo.login(request); - - // 3. Handle side effects - persist session state - await this.handleSessionSideEffects(session, request); - - return session; - } catch (error) { - // Re-throw the original error - let facade handle transformation - throw error; - } - } - - private async validateApplicationRules(request: LoginRequest): Promise { - const currentTime = new Date(this.clock.nowEpochSeconds() * 1000); - - // Check maintenance mode - if (await this.checkMaintenanceMode(currentTime)) { - throw new ApplicationError( - 'login', - 'MAINTENANCE_MODE', - 'System is currently under maintenance. Please try again later.' - ); - } - } - - private async handleSessionSideEffects(session: Session, request: LoginRequest): Promise { - // Process domain events from the session entity - await this.eventProcessor.processEntityEvents(session); - - // Log successful login for security auditing - console.log(`User logged in: ${session.user.email} at ${new Date().toISOString()}`); - } -} -``` - -### Data Access Implementation - -**AuthRepository Interface (Domain Layer)** -```typescript -export interface AuthRepository { - /** - * Authenticates a user with provided credentials. - * @param creds - User credentials including identifier and password - * @returns Promise resolving to an authenticated Session entity - * @throws {UnauthorizedError} When credentials are invalid - * @throws {AccountLockedError} When account is temporarily locked - * @throws {EmailNotConfirmedError} When email verification is required - */ - login(creds: CredentialsContract): Promise; - - me(): Promise; - refresh(): Promise; - logout(): Promise; -} -``` - -**HttpAuthRepository Implementation (Infrastructure Layer)** -```typescript -@Injectable() -export class HttpAuthRepository implements AuthRepository { - private http = inject(HttpClient); - private tokenStore = inject(TOKEN_STORE_PORT); - private userStore = inject(AUTH_USER_STORE_PORT); - private errorMapper = inject(InfraErrorToDomainMapper); - - async login(creds: CredentialsContract): Promise { - try { - const body: LoginRequestDTO = { - identifier: creds.identifier.value, - password: creds.password, - remember_me: !!creds.rememberMe, - }; - - const dto = await firstValueFrom( - this.http.post(`${API}/login/`, body) - ); - - // Store user snapshot - this.writeUserSnapshotFromLogin(dto.user); - - // Create domain entities - const user = AuthMapper.loginUserToEntity(dto.user); - const tokens = AuthMapper.tokensFromLogin(dto, this.clock.nowEpochSeconds()); - - // Store tokens locally - this.setLocalTokens({ - accessToken: tokens.accessToken, - accessExp: tokens.accessExp, - refreshToken: tokens.refreshToken, - }); - - // Create session - const session = AuthMapper.toSession({ - accessToken: tokens.accessToken, - refreshToken: tokens.refreshToken, - accessExpEpochSeconds: tokens.accessExp, - user, - }); - - return session; - } catch (infraError: unknown) { - const domainError = this.errorMapper.mapError(infraError as InfraError, { - operation: 'LOGIN', - entityType: 'User', - field: 'credentials', - }); - throw domainError; - } - } -} -``` - -### Error Handling Patterns - -**Application Error Transformer** -```typescript -@Injectable({ providedIn: 'root' }) -export class ApplicationErrorTransformer { - transformError( - error: Error, - context?: { feature?: string; operation?: string } - ): string { - if (error instanceof UnauthorizedError) { - return 'Invalid email or password. Please check your credentials.'; - } - - if (error instanceof AccountLockedError) { - return 'Account temporarily locked due to multiple failed attempts.'; - } - - if (error instanceof EmailNotConfirmedError) { - return 'Please confirm your email address before logging in.'; - } - - if (error instanceof NetworkError) { - return 'Connection failed. Please check your internet connection.'; - } - - return 'An unexpected error occurred. Please try again.'; - } -} -``` - ---- - -## Workflow 2: Role Management (CRUD) - -### Overview -**Purpose**: Complete role lifecycle management with RBAC operations -**Business Value**: Secure role-based access control with comprehensive validation -**Triggers**: Role creation, modification, deletion, assignment operations - -### Files Involved -``` -src/app/presentation/features/roles/pages/roles-list/roles-list.ts -src/app/application/facades/roles.facade.ts -src/app/application/use-cases/roles/create-role.usecase.ts -src/app/application/use-cases/roles/update-role.usecase.ts -src/app/application/use-cases/roles/delete-role.usecase.ts -src/app/domain/repositories/business/role.repository.ts -src/app/infrastructure/repositories/http-role.repository.ts -src/app/domain/entities/role.entity.ts -``` - -### Entry Point Implementation - -**RolesList Component** -```typescript -@Component({ - selector: 'app-roles-list', - standalone: true, - imports: [CommonModule, Icon, RoleCard, RoleTable, PageHeader, ErrorDisplay], - templateUrl: './roles-list.html', - styleUrl: './roles-list.css', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class RolesList { - protected facade = inject(RolesFacade); - - // Reactive state from facade - roles = this.facade.roles; - loading = this.facade.loading; - error = this.facade.error; - - async ngOnInit(): Promise { - await this.facade.refresh(); - } - - async onCreateRole(data: CreateRoleContract): Promise { - await this.facade.createRole(data); - } - - async onUpdateRole(id: number, data: UpdateRoleContract): Promise { - await this.facade.updateRole(id, data); - } - - async onDeleteRole(id: number): Promise { - await this.facade.deleteRole(id); - } -} -``` - -### Service Layer Implementation - -**RolesFacade** -```typescript -@Injectable({ providedIn: 'root' }) -export class RolesFacade { - // Use Case Dependencies - private readonly listRolesUC = inject(ListRoles); - private readonly createRoleUC = inject(CreateRole); - private readonly updateRoleUC = inject(UpdateRole); - private readonly deleteRoleUC = inject(DeleteRole); - - // Cross-facade Dependencies - private readonly notifications = inject(NotificationsFacade); - private readonly errorTransformer = inject(ApplicationErrorTransformer); - - // Reactive State - private readonly _loading = signal(false); - private readonly _error = signal(null); - private readonly _roles = signal([]); - - readonly loading = computed(() => this._loading()); - readonly error = computed(() => this._error()); - readonly roles = computed(() => this._roles()); - - async createRole(roleData: CreateRoleContract): Promise { - this._loading.set(true); - this._error.set(null); - - try { - const entity = await this.createRoleUC.execute(roleData); - - // Add to roles list - const roleModel = RoleViewMapper.toModel(entity); - this._roles.update((roles) => [roleModel, ...roles]); - - // Send success notification - await this.notifications.success( - 'Role created successfully!', - `Role "${entity.name}" has been created with access level ${entity.accessLevel}.` - ); - - return entity; - } catch (error: any) { - const errorMessage = this.errorTransformer.transformError(error as Error); - this._error.set(errorMessage); - throw error; - } finally { - this._loading.set(false); - } - } -} -``` - -### Domain Entity - -**Role Entity** -```typescript -export class Role { - constructor( - public readonly id: number, - public readonly name: string, - public readonly accessLevel: number, - public readonly description: string | null, - public readonly isActive: boolean, - public readonly canLeadProjects: boolean, - public readonly isUniquePerTeam: boolean, - public readonly userCount: number, - public readonly createdAt: Date, - public readonly updatedAt: Date | null - ) {} - - static create(params: { - id: number; - name: RoleName; - accessLevel: AccessLevel; - isActive?: boolean; - description?: string | null; - userCount?: number; - }): Role { - return new Role( - params.id, - params.name.value, - params.accessLevel.value, - params.description || null, - params.isActive ?? true, - false, // Default values - false, - params.userCount || 0, - new Date(), - null - ); - } - - /** - * Business rule: Check if role can be assigned to users - */ - canBeAssigned(): boolean { - return this.isActive && this.accessLevel > 0; - } - - /** - * Business rule: Check if role has administrative privileges - */ - hasAdminPrivileges(): boolean { - return this.isAdministrator() && this.isActive; - } - - /** - * Business rule: Determine if role is administrator level - */ - isAdministrator(): boolean { - return this.accessLevel >= 90; - } -} -``` - ---- - -## Workflow 3: User Management (CRUD) - -### Overview -**Purpose**: Complete user lifecycle management with role assignments -**Business Value**: User administration with comprehensive validation and audit logging -**Triggers**: User creation, profile updates, role changes, deactivation - -### Files Involved -``` -src/app/presentation/features/users/pages/users-list/users-list.ts -src/app/application/facades/users.facade.ts -src/app/application/use-cases/users/create-user.usecase.ts -src/app/application/use-cases/users/update-user.usecase.ts -src/app/domain/repositories/business/user.repository.ts -src/app/infrastructure/repositories/http-user.repository.ts -src/app/domain/entities/user.entity.ts -``` - -### Service Layer Implementation - -**UsersFacade** -```typescript -@Injectable({ providedIn: 'root' }) -export class UsersFacade { - // Use Case Dependencies - private readonly listUsersUC = inject(ListUsers); - private readonly createUserUC = inject(CreateUser); - private readonly updateUserUC = inject(UpdateUser); - private readonly deleteUserUC = inject(DeleteUser); - - // Reactive State - private readonly _loading = signal(false); - private readonly _users = signal([]); - private readonly _selectedUser = signal(null); - private readonly _error = signal(null); - - readonly users = computed(() => this._users()); - readonly selectedUser = computed(() => this._selectedUser()); - readonly loading = computed(() => this._loading()); - readonly error = computed(() => this._error()); - - async updateUser( - id: number, - userData: UpdateUserRequest, - opts?: FacadeOpts - ): Promise { - if (!opts?.skipLoading) this._loading.set(true); - this._error.set(null); - - try { - const updatedUser = await this.updateUserUC.execute(id, { - firstName: userData.firstName, - lastName: userData.lastName, - email: userData.email, - roleId: userData.roleId, - isActive: userData.isActive, - }); - - // Update users list - this._users.update((users) => - users.map((user) => (user.id === id ? updatedUser : user)) - ); - - // Update selected user if it's the one being updated - if (this._selectedUser()?.id === id) { - this._selectedUser.set(updatedUser); - } - - await this.notifications.success( - 'User updated successfully!', - `User ${updatedUser.firstName} ${updatedUser.lastName} has been updated.` - ); - - return updatedUser; - } catch (error: any) { - const errorMessage = this.errorTransformer.transformError(error as Error); - this._error.set(errorMessage); - throw error; - } finally { - if (!opts?.skipLoading) this._loading.set(false); - } - } -} -``` - -### Use Case Implementation - -**UpdateUser Use Case** -```typescript -@Injectable({ providedIn: 'root' }) -export class UpdateUser { - private readonly userRepo = inject(USER_REPOSITORY); - private readonly clock = inject(CLOCK_PORT); - private readonly eventProcessor = inject(DomainEventProcessor); - private readonly errorTransformer = inject(ApplicationErrorTransformer); - - async execute( - userId: number, - patch: UpdateUserPatchContract, - requesterId?: number - ): Promise { - try { - // Step 1: Validate application rules - this.validateApplicationRules(userId, patch); - - // Step 2: Delegate to domain repository - const updatedUser = await this.userRepo.update(userId, patch); - - // Step 3: Handle side effects - await this.handleUserUpdateSideEffects(updatedUser, patch, requesterId); - - return updatedUser; - } catch (error: unknown) { - throw new ApplicationError( - 'update_user', - this.errorTransformer.transformError(error), - 'USER_UPDATE_FAILED' - ); - } - } - - private validateApplicationRules( - userId: number, - patch: UpdateUserPatchContract - ): void { - if (!userId || userId <= 0) { - throw new ApplicationError( - 'update_user', - 'INVALID_USER_ID', - 'Valid user ID is required for update operation' - ); - } - - if (!patch || Object.keys(patch).length === 0) { - throw new ApplicationError( - 'update_user', - 'NO_UPDATE_DATA', - 'At least one field must be provided for update' - ); - } - } - - private async handleUserUpdateSideEffects( - updatedUser: User, - patch: UpdateUserPatchContract, - requesterId?: number - ): Promise { - // Process domain events from the updated user entity - await this.eventProcessor.processEntityEvents(updatedUser); - - const timestamp = new Date(this.clock.nowEpochSeconds() * 1000); - const changedFields = Object.keys(patch); - - // Log user update for audit trail - console.log('[User Update] User successfully updated', { - timestamp: timestamp.toISOString(), - updatedUserId: updatedUser.id, - changedFields, - requesterId: requesterId || 'system', - action: 'update_user', - status: 'success', - }); - } -} -``` - ---- - -## Workflow 4: Notification System - -### Overview -**Purpose**: Centralized notification system with toast UI and action handling -**Business Value**: User feedback and system communication with rich interactions -**Triggers**: System events, user actions, error conditions, success confirmations - -### Files Involved -``` -src/app/application/facades/notifications.facade.ts -src/app/application/use-cases/notifications/notify.usecase.ts -src/app/shared/components/toast/toast-container/toast-container.ts -src/app/shared/components/toast/toast-item/toast-item.ts -src/app/infrastructure/services/notification-gateway.service.ts -``` - -### Service Layer Implementation - -**NotificationsFacade** -```typescript -@Injectable({ providedIn: 'root' }) -export class NotificationsFacade { - // Use Case Dependencies - private readonly notifyUC = inject(Notify); - private readonly dismissUC = inject(DismissNotification); - private readonly clearUC = inject(ClearNotifications); - - // Private Reactive State - private readonly _notifications = signal([]); - private readonly _loading = signal(false); - private readonly _notificationError = signal(null); - private readonly _unreadCount = signal(0); - - // Public Reactive State - readonly notifications = computed(() => this._notifications()); - readonly loading = computed(() => this._loading()); - readonly error = computed(() => this._notificationError()); - readonly unreadCount = computed(() => this._unreadCount()); - readonly hasNotifications = computed(() => this._notifications().length > 0); - - async notify(request: NotifyRequest, opts?: FacadeOpts): Promise { - if (!opts?.skipLoading) this._loading.set(true); - this._notificationError.set(null); - - try { - const notificationId = await this.notifyUC.execute({ - type: request.type, - message: request.message, - title: request.description, - userId: request.userId?.toString(), - duration: request.duration, - metadata: request.metadata, - }); - - const notification = this._notifications().find((n) => n.id === notificationId); - - if (notification) { - this.emitEvent('notification-created', notification); - return { notification, success: true }; - } else { - throw new Error('Failed to retrieve created notification'); - } - } catch (error: any) { - const errorMessage = error?.message ?? 'Failed to create notification'; - this._notificationError.set(errorMessage); - throw error; - } finally { - if (!opts?.skipLoading) this._loading.set(false); - } - } - - // Convenience methods - async success(message: string, description?: string): Promise { - return this.notify({ - type: 'success', - message, - description, - duration: 5000, - }); - } - - async error(message: string, description?: string): Promise { - return this.notify({ - type: 'error', - message, - description, - // No duration for errors - require manual dismissal - }); - } - - async warning(message: string, description?: string): Promise { - return this.notify({ - type: 'warning', - message, - description, - duration: 8000, - }); - } - - async info(message: string, description?: string): Promise { - return this.notify({ - type: 'info', - message, - description, - duration: 5000, - }); - } -} -``` - -### UI Component Implementation - -**Toast Container Component** -```typescript -@Component({ - selector: 'app-toast-container', - standalone: true, - imports: [CommonModule, ToastItem], - templateUrl: './toast-container.html', - styleUrls: ['./toast-container.css'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ToastContainer { - private facade = inject(NotificationsFacade); - private cfg = inject(NOTIFICATION_CONFIG); - - readonly NotificationType = { - SUCCESS: 'success' as const, - ERROR: 'error' as const, - WARNING: 'warning' as const, - INFO: 'info' as const, - }; - - items = this.facade.notifications; - - cap = () => - typeof window !== 'undefined' && window.matchMedia?.('(max-width: 640px)').matches - ? this.cfg.maxVisibleMobile - : this.cfg.maxVisibleDesktop; - - onItemDismiss(notificationId: string): void { - this.facade.dismiss({ notificationId }); - } - - onItemAction(notification: Notification, actionId: string): void { - // Handle notification actions - const action = notification.actions?.find(a => a.id === actionId); - if (action) { - action.handler(); - } - } -} -``` - ---- - -## Workflow 5: Dashboard Navigation & State - -### Overview -**Purpose**: Application navigation, layout management, and UI state coordination -**Business Value**: Seamless user experience with responsive design and state persistence -**Triggers**: Route changes, user interactions, window resize, authentication state changes - -### Files Involved -``` -src/app/presentation/features/dashboard/dashboard.ts -src/app/presentation/navigation/navigation.service.ts -src/app/core/cross-cutting/ui-state/layout.service.ts -src/app/core/cross-cutting/ui-state/breadcrumb.service.ts -src/app/presentation/shell/nav-rail/nav-rail.ts -``` - -### Entry Point Implementation - -**Dashboard Component** -```typescript -@Component({ - selector: 'app-dashboard', - templateUrl: './dashboard.html', - styleUrl: './dashboard.css', - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [Button, Icon], -}) -export class Dashboard { - readonly User: User | null; - readonly username: string | undefined; - protected readonly titlePage = 'Dashboard'; - - constructor( - private titleService: TitleService, - private breadcrumbService: BreadcrumbService, - public authFacade: AuthFacade, - private router: Router - ) { - this.User = this.authFacade.user(); - this.username = this.User?.username; - } - - async ngOnInit(): Promise { - this.titleService.setTitle(this.titlePage); - this.breadcrumbService.setBreadcrumbs([ - { label: this.titlePage, icon: 'user' } - ]); - await this.authFacade.refreshProfile(); - } - - async logout() { - await this.authFacade.logout(); - await this.router.navigateByUrl('/auth/login'); - } - - getGreeting(): string { - const hour = new Date().getHours(); - if (hour < 12) return 'Buenos días'; - if (hour < 18) return 'Buenas tardes'; - return 'Buenas noches'; - } -} -``` - -### State Management Services - -**NavigationService** -```typescript -@Injectable({ providedIn: 'root' }) -export class NavigationService { - private readonly router = inject(Router); - private readonly authFacade = inject(AuthFacade); - - private readonly _currentPath = signal('/'); - - readonly currentPath = computed(() => this._currentPath()); - - // Filter sections based on user roles - readonly accessibleSections = computed(() => { - const user = this.authFacade.user(); - - if (!user) { - return NAV_SECTIONS.map(section => ({ - ...section, - items: section.items.filter(item => - !item.requireRoles || item.requireRoles.length === 0 - ) - })).filter(section => section.items.length > 0); - } - - const userRoles = [user.role.name]; - const isAdmin = user.isAdministrator(); - - return NAV_SECTIONS.map(section => ({ - ...section, - items: section.items.filter(item => - !item.requireRoles || - item.requireRoles.length === 0 || - isAdmin || - item.requireRoles.some(role => userRoles.includes(role)) - ) - })).filter(section => section.items.length > 0); - }); - - constructor() { - this.initializeRouter(); - } - - private initializeRouter(): void { - this._currentPath.set(this.router.url); - - this.router.events - .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd)) - .subscribe((event: NavigationEnd) => { - this._currentPath.set(event.url); - }); - } -} -``` - -**LayoutService** -```typescript -@Injectable({ providedIn: 'root' }) -export class LayoutService { - private document = inject(DOCUMENT); - - // Private signals - private _sidebarCollapsed = signal(false); - private _mobileDrawerOpen = signal(false); - private _hoveredItemId = signal(null); - private _windowWidth = signal(typeof window !== 'undefined' ? window.innerWidth : 1024); - - // Public computed signals - readonly sidebarCollapsed = computed(() => this._sidebarCollapsed()); - readonly mobileDrawerOpen = computed(() => this._mobileDrawerOpen()); - readonly hoveredItemId = computed(() => this._hoveredItemId()); - readonly isMobile = computed(() => this._windowWidth() < 768); - - constructor() { - this.initializeFromStorage(); - - if (typeof window !== 'undefined') { - this.setupWindowResize(); - } - - // Persist sidebar state - effect(() => { - if (typeof window !== 'undefined') { - const collapsed = this._sidebarCollapsed(); - localStorage.setItem(LAYOUT_STORAGE_KEY, JSON.stringify(collapsed)); - } - }); - - // Auto-close mobile drawer when switching to desktop - effect(() => { - if (!this.isMobile() && this._mobileDrawerOpen()) { - this._mobileDrawerOpen.set(false); - } - }); - } - - toggleSidebarCollapsed(): void { - this._sidebarCollapsed.update((collapsed) => !collapsed); - } - - onNavigate(): void { - if (this.isMobile()) { - this.closeMobileDrawer(); - } - this.setHoveredItem(null); - } -} -``` - ---- - -## Sequence Diagrams - -### User Authentication Flow -```mermaid -sequenceDiagram - participant UI as Login Component - participant AF as AuthFacade - participant UC as LoginWithCredentials - participant AR as AuthRepository - participant HTTP as HttpAuthRepository - participant API as External API - - UI->>AF: login(credentials) - AF->>AF: clearAuthStateCompletely() - AF->>UC: execute(request) - UC->>UC: validateApplicationRules() - UC->>AR: login(credentials) - AR->>HTTP: login(credentials) - HTTP->>API: POST /auth/login/ - API-->>HTTP: LoginResponseDTO - HTTP->>HTTP: writeUserSnapshot() - HTTP->>HTTP: createDomainEntities() - HTTP->>HTTP: storeTokens() - HTTP-->>AR: Session - AR-->>UC: Session - UC->>UC: handleSessionSideEffects() - UC-->>AF: Session - AF->>AF: _session.set(session) - AF->>AF: _user.set(session.user) - AF->>NotificationsFacade: success("Welcome!") - AF-->>UI: void -``` - -### Role Management Flow -```mermaid -sequenceDiagram - participant UI as RolesList Component - participant RF as RolesFacade - participant UC as CreateRole UseCase - participant RR as RoleRepository - participant HTTP as HttpRoleRepository - participant API as External API - - UI->>RF: createRole(data) - RF->>UC: execute(data) - UC->>UC: validateApplicationRules() - UC->>RR: create(spec) - RR->>HTTP: create(spec) - HTTP->>API: POST /roles/ - API-->>HTTP: RoleResponseDTO - HTTP->>HTTP: mapToEntity() - HTTP-->>RR: Role - RR-->>UC: Role - UC->>UC: handleRoleCreationSideEffects() - UC-->>RF: Role - RF->>RF: _roles.update(addRole) - RF->>NotificationsFacade: success("Role created!") - RF-->>UI: Role -``` - ---- - -## Testing Patterns - -### Unit Testing Facades - -**AuthFacade Test Pattern** -```typescript -describe('AuthFacade', () => { - let facade: AuthFacade; - let mockLoginUC: jasmine.SpyObj; - let mockNotifications: jasmine.SpyObj; - - beforeEach(() => { - const loginSpy = jasmine.createSpyObj('LoginWithCredentials', ['execute']); - const notificationsSpy = jasmine.createSpyObj('NotificationsFacade', ['success']); - - TestBed.configureTestingModule({ - providers: [ - AuthFacade, - { provide: LoginWithCredentials, useValue: loginSpy }, - { provide: NotificationsFacade, useValue: notificationsSpy }, - ], - }); - - facade = TestBed.inject(AuthFacade); - mockLoginUC = TestBed.inject(LoginWithCredentials) as jasmine.SpyObj; - mockNotifications = TestBed.inject(NotificationsFacade) as jasmine.SpyObj; - }); - - it('should login successfully and update state', async () => { - const mockSession = createMockSession(); - mockLoginUC.execute.and.returnValue(Promise.resolve(mockSession)); - - await facade.login({ identifier: 'test@test.com', password: 'password' }); - - expect(facade.session()).toEqual(mockSession); - expect(facade.user()).toEqual(mockSession.user); - expect(mockNotifications.success).toHaveBeenCalled(); - }); - - it('should handle login errors and update error state', async () => { - const error = new UnauthorizedError('Invalid credentials'); - mockLoginUC.execute.and.returnValue(Promise.reject(error)); - - await facade.login({ identifier: 'test@test.com', password: 'wrong' }); - - expect(facade.error()).toBeTruthy(); - expect(facade.session()).toBeNull(); - expect(facade.user()).toBeNull(); - }); -}); -``` - -### Integration Testing Use Cases - -**Use Case Integration Test Pattern** -```typescript -describe('LoginWithCredentials Integration', () => { - let useCase: LoginWithCredentials; - let mockRepository: jasmine.SpyObj; - - beforeEach(() => { - const repoSpy = jasmine.createSpyObj('AuthRepository', ['login']); - - TestBed.configureTestingModule({ - providers: [ - LoginWithCredentials, - { provide: AUTH_REPOSITORY, useValue: repoSpy }, - ], - }); - - useCase = TestBed.inject(LoginWithCredentials); - mockRepository = TestBed.inject(AUTH_REPOSITORY); - }); - - it('should execute login flow successfully', async () => { - const mockSession = createMockSession(); - mockRepository.login.and.returnValue(Promise.resolve(mockSession)); - - const result = await useCase.execute({ - identifier: { type: 'email', value: 'test@test.com' }, - password: 'password' - }); - - expect(result).toEqual(mockSession); - expect(mockRepository.login).toHaveBeenCalled(); - }); -}); -``` - -### Component Testing - -**Component Test Pattern** -```typescript -describe('Dashboard Component', () => { - let component: Dashboard; - let fixture: ComponentFixture; - let mockAuthFacade: jasmine.SpyObj; - - beforeEach(async () => { - const authSpy = jasmine.createSpyObj('AuthFacade', ['refreshProfile', 'logout'], { - user: signal(createMockUser()) - }); - - await TestBed.configureTestingModule({ - imports: [Dashboard], - providers: [ - { provide: AuthFacade, useValue: authSpy } - ] - }).compileComponents(); - - fixture = TestBed.createComponent(Dashboard); - component = fixture.componentInstance; - mockAuthFacade = TestBed.inject(AuthFacade) as jasmine.SpyObj; - }); - - it('should initialize and refresh profile', async () => { - await component.ngOnInit(); - - expect(mockAuthFacade.refreshProfile).toHaveBeenCalled(); - }); -}); -``` - ---- - -## Implementation Templates - -### Creating a New Facade - -```typescript -@Injectable({ providedIn: 'root' }) -export class NewFeatureFacade { - // Use Case Dependencies - private readonly createUC = inject(CreateNewFeature); - private readonly updateUC = inject(UpdateNewFeature); - private readonly deleteUC = inject(DeleteNewFeature); - private readonly listUC = inject(ListNewFeatures); - - // Cross-Facade Dependencies - private readonly notifications = inject(NotificationsFacade); - private readonly errorTransformer = inject(ApplicationErrorTransformer); - - // Private State Signals - private readonly _loading = signal(false); - private readonly _error = signal(null); - private readonly _items = signal([]); - private readonly _selectedItem = signal(null); - - // Public Computed Signals - readonly loading = computed(() => this._loading()); - readonly error = computed(() => this._error()); - readonly items = computed(() => this._items()); - readonly selectedItem = computed(() => this._selectedItem()); - - async create(data: CreateNewFeatureRequest): Promise { - this._loading.set(true); - this._error.set(null); - - try { - const entity = await this.createUC.execute(data); - - // Update state - const model = NewFeatureViewMapper.toModel(entity); - this._items.update((items) => [model, ...items]); - - // Show success notification - await this.notifications.success( - 'Feature created successfully!', - `New feature "${entity.name}" has been created.` - ); - - return entity; - } catch (error: any) { - const errorMessage = this.errorTransformer.transformError(error as Error); - this._error.set(errorMessage); - throw error; - } finally { - this._loading.set(false); - } - } -} -``` - -### Creating a New Use Case - -```typescript -@Injectable({ providedIn: 'root' }) -export class CreateNewFeature { - private readonly repository = inject(NEW_FEATURE_REPOSITORY); - private readonly clock = inject(CLOCK_PORT); - private readonly eventProcessor = inject(DomainEventProcessor); - private readonly errorTransformer = inject(ApplicationErrorTransformer); - - async execute(request: CreateNewFeatureRequest): Promise { - try { - // Step 1: Validate application rules - this.validateApplicationRules(request); - - // Step 2: Delegate to domain repository - const newFeature = await this.repository.create({ - name: request.name, - description: request.description, - isActive: request.isActive ?? true, - }); - - // Step 3: Handle side effects - await this.handleCreationSideEffects(newFeature, request); - - return newFeature; - } catch (error: unknown) { - throw new ApplicationError( - 'create_new_feature', - this.errorTransformer.transformError(error), - 'NEW_FEATURE_CREATION_FAILED' - ); - } - } - - private validateApplicationRules(request: CreateNewFeatureRequest): void { - if (!request.name || request.name.trim().length === 0) { - throw new ApplicationError( - 'create_new_feature', - 'INVALID_NAME', - 'Feature name is required and cannot be empty' - ); - } - } - - private async handleCreationSideEffects( - newFeature: NewFeature, - request: CreateNewFeatureRequest - ): Promise { - // Process domain events - await this.eventProcessor.processEntityEvents(newFeature); - - // Audit logging - console.log('[New Feature Created]', { - timestamp: new Date(this.clock.nowEpochSeconds() * 1000).toISOString(), - featureId: newFeature.id, - featureName: newFeature.name, - action: 'create_new_feature', - status: 'success', - }); - } -} -``` - -### Creating a New Repository Interface - -```typescript -export interface NewFeatureRepository { - /** - * Create a new feature - * @param spec - Feature creation specification - * @returns Promise resolving to created feature entity - * @throws {ValidationError} When feature data is invalid - * @throws {ConflictError} When feature name already exists - */ - create(spec: CreateNewFeatureContract): Promise; - - /** - * Get feature by ID - * @param id - Feature identifier - * @returns Promise resolving to feature entity - * @throws {NotFoundError} When feature doesn't exist - */ - getById(id: number): Promise; - - /** - * List features with optional filtering - * @param filter - Optional filter criteria - * @returns Promise resolving to array of feature entities - */ - list(filter?: ListNewFeaturesFilterContract): Promise; - - /** - * Update existing feature - * @param id - Feature identifier - * @param patch - Partial update data - * @returns Promise resolving to updated feature entity - * @throws {NotFoundError} When feature doesn't exist - * @throws {ValidationError} When update data is invalid - */ - update(id: number, patch: UpdateNewFeaturePatchContract): Promise; - - /** - * Delete feature - * @param id - Feature identifier - * @returns Promise resolving when deletion is complete - * @throws {NotFoundError} When feature doesn't exist - * @throws {BusinessRuleError} When feature cannot be deleted - */ - delete(id: number): Promise; -} -``` - -### Creating HTTP Repository Implementation - -```typescript -@Injectable() -export class HttpNewFeatureRepository implements NewFeatureRepository { - private http = inject(HttpClient); - private errorMapper = inject(InfraErrorToDomainMapper); - - private readonly baseUrl = `${environment.API_URL}/features`; - - async create(spec: CreateNewFeatureContract): Promise { - try { - const requestDto: CreateNewFeatureRequestDTO = { - name: spec.name, - description: spec.description, - is_active: spec.isActive, - }; - - const responseDto = await firstValueFrom( - this.http.post(this.baseUrl, requestDto) - .pipe(catchError(this.handleHttpError)) - ); - - return NewFeatureMapper.toEntityFromCreateDTO(responseDto); - } catch (error) { - throw this.transformError(error, 'CREATE_NEW_FEATURE'); - } - } - - async getById(id: number): Promise { - try { - const responseDto = await firstValueFrom( - this.http.get(`${this.baseUrl}/${id}`) - .pipe(catchError(this.handleHttpError)) - ); - - return NewFeatureMapper.toEntityFromGetDTO(responseDto); - } catch (error) { - throw this.transformError(error, 'GET_NEW_FEATURE'); - } - } - - private handleHttpError = (error: HttpErrorResponse) => { - return throwError(() => ({ - status: error.status, - message: error.message, - body: error.error, - headers: error.headers, - })); - }; - - private transformError(error: unknown, operation: string): Error { - const domainError = this.errorMapper.mapError(error as InfraError, { - operation, - entityType: 'NewFeature', - }); - return domainError; - } -} -``` - ---- - -## Common Pitfalls & Best Practices - -### Architecture Violations to Avoid - -**❌ Don't: Cross-layer imports** -```typescript -// BAD: Domain importing from infrastructure -import { HttpClient } from '@angular/common/http'; // In domain layer -``` - -**✅ Do: Proper layer separation** -```typescript -// GOOD: Domain depends on contracts only -import { AuthRepository } from '@domain/repositories/business/auth.repository'; -``` - -**❌ Don't: Facade importing domain entities directly** -```typescript -// BAD: Facade working with domain entities -private _users = signal([]); -``` - -**✅ Do: Use view models in presentation layer** -```typescript -// GOOD: Facade using view models -private _users = signal([]); -``` - -### State Management Pitfalls - -**❌ Don't: Mutate signal values directly** -```typescript -// BAD: Direct mutation -this.users().push(newUser); -``` - -**✅ Do: Use signal update methods** -```typescript -// GOOD: Immutable updates -this._users.update(users => [...users, newUser]); -``` - -**❌ Don't: Forget error state cleanup** -```typescript -// BAD: Error persists across operations -async someOperation() { - try { - // operation - } catch (error) { - this._error.set(error.message); - // Missing error cleanup in subsequent operations - } -} -``` - -**✅ Do: Clear error state at operation start** -```typescript -// GOOD: Clean error state -async someOperation() { - this._error.set(null); // Clear previous errors - try { - // operation - } catch (error) { - this._error.set(error.message); - } -} -``` - -### Error Handling Best Practices - -**✅ Comprehensive Error Transformation** -```typescript -@Injectable({ providedIn: 'root' }) -export class ApplicationErrorTransformer { - transformError(error: Error, context?: ErrorContext): string { - // Domain-specific errors - if (error instanceof UnauthorizedError) { - return 'Invalid credentials. Please check your login information.'; - } - - if (error instanceof ValidationError) { - return `Validation failed: ${error.message}`; - } - - if (error instanceof NetworkError) { - return 'Connection failed. Please check your internet connection.'; - } - - // Fallback for unknown errors - console.error('[ApplicationErrorTransformer] Unexpected error:', error); - return 'An unexpected error occurred. Please try again.'; - } -} -``` - -### Performance Considerations - -**✅ Optimize Signal Updates** -```typescript -// Use batch updates for multiple state changes -batch(() => { - this._loading.set(false); - this._users.set(newUsers); - this._error.set(null); -}); -``` - -**✅ Implement Proper Loading States** -```typescript -async operation(skipLoading = false) { - if (!skipLoading) this._loading.set(true); - try { - // operation - } finally { - if (!skipLoading) this._loading.set(false); - } -} -``` - -### Testing Best Practices - -**✅ Mock External Dependencies** -```typescript -describe('AuthFacade', () => { - let mockLoginUC: jasmine.SpyObj; - - beforeEach(() => { - const loginSpy = jasmine.createSpyObj('LoginWithCredentials', ['execute']); - - TestBed.configureTestingModule({ - providers: [ - { provide: LoginWithCredentials, useValue: loginSpy } - ] - }); - - mockLoginUC = TestBed.inject(LoginWithCredentials) as jasmine.SpyObj; - }); -}); -``` - -**✅ Test Error Scenarios** -```typescript -it('should handle network errors gracefully', async () => { - mockLoginUC.execute.and.returnValue(Promise.reject(new NetworkError())); - - await facade.login(credentials); - - expect(facade.error()).toContain('Connection failed'); - expect(facade.user()).toBeNull(); -}); -``` - -### Extension Mechanisms - -**✅ Use Dependency Injection for Extensibility** -```typescript -// Define extension points via DI tokens -export const USER_VALIDATOR = new InjectionToken('UserValidator'); - -// Allow custom implementations -@Injectable() -export class CustomUserValidator implements UserValidator { - validate(user: User): ValidationResult { - // Custom validation logic - } -} -``` - -**✅ Implement Observer Pattern for Cross-Cutting Concerns** -```typescript -// Event emission for cross-facade coordination -private readonly eventSubject = new Subject(); -readonly events$ = this.eventSubject.asObservable(); - -private emitEvent(event: DomainEvent): void { - this.eventSubject.next(event); -} -``` - ---- - -## Summary - -This blueprint provides comprehensive documentation of 5 representative workflows in the MAD-AI Angular application: - -1. **User Authentication**: Complete auth flow with session management -2. **Role Management**: RBAC operations with validation -3. **User Management**: User lifecycle with role assignments -4. **Notification System**: Toast notifications with actions -5. **Dashboard Navigation**: UI state and routing management - -Each workflow follows Clean Architecture principles with clear layer separation, reactive state management using Angular Signals, and comprehensive error handling. The implementation templates and best practices ensure consistency when adding new features to the system. - -**Key Implementation Patterns:** -- **Facades** orchestrate use cases and manage reactive state -- **Use Cases** handle business logic orchestration and validation -- **Repositories** abstract data access through domain contracts -- **Signals** provide reactive state management throughout the application -- **Error Transformation** ensures consistent user-friendly error messages - -This documentation serves as a complete reference for developers implementing similar features, maintaining architectural consistency, and following established patterns in the MAD-AI codebase. diff --git a/.github/copilot/tech-stack.md b/.github/copilot/tech-stack.md deleted file mode 100644 index c8904b07..00000000 --- a/.github/copilot/tech-stack.md +++ /dev/null @@ -1,1740 +0,0 @@ -# MAD-AI Technology Stack Blueprint - -> **Generated on:** August 23, 2025 -> **Version:** 1.0.0 -> **Project:** MAD-AI - Angular Enterprise Application -> **Analysis Depth:** Implementation-Ready -> **Configuration:** Angular + TypeScript + TailwindCSS + Clean Architecture - -## Table of Contents - -- [1. Technology Identification Overview](#1-technology-identification-overview) -- [2. Core Framework Stack](#2-core-framework-stack) -- [3. Development Dependencies & Tooling](#3-development-dependencies--tooling) -- [4. Runtime Dependencies](#4-runtime-dependencies) -- [5. Implementation Patterns & Conventions](#5-implementation-patterns--conventions) -- [6. Code Organization Standards](#6-code-organization-standards) -- [7. Usage Examples & Patterns](#7-usage-examples--patterns) -- [8. Technology Stack Map](#8-technology-stack-map) -- [9. Angular-Specific Implementation Details](#9-angular-specific-implementation-details) -- [10. Blueprint for New Code Implementation](#10-blueprint-for-new-code-implementation) -- [11. Technology Relationship Diagrams](#11-technology-relationship-diagrams) -- [12. Technology Decision Context](#12-technology-decision-context) - -## 1. Technology Identification Overview - -### Project Metadata -- **Project Name:** mad-ai-new -- **Version:** 0.0.0 (Development) -- **Project Type:** Angular Single Page Application with SSR -- **Architecture:** Clean Architecture + Domain-Driven Design -- **Language:** TypeScript 5.8.2 with strict mode - -### Technology Categories Detected - -**Frontend Framework:** Angular 20.1.6 (Latest) -**Language:** TypeScript 5.8.2 with ES2022 target -**Styling:** TailwindCSS 4.1.11 (Latest) -**State Management:** Angular Signals + RxJS 7.8.0 -**Build System:** Angular CLI 20.0.2 with @angular/build -**Testing:** Jasmine + Karma with Puppeteer -**Server:** Express.js 5.1.0 for SSR -**Data Visualization:** Chart.js 4.5.0 -**Document Generation:** jsPDF 3.0.1 -**Data Processing:** PapaParse 5.5.3 - -## 2. Core Framework Stack - -### Angular Ecosystem (Version 20.1.6) - -#### Core Angular Packages -```json -{ - "@angular/animations": "^20.1.6", // Animation system - "@angular/common": "^20.1.6", // Common directives and pipes - "@angular/compiler": "^20.1.6", // Template compiler - "@angular/core": "^20.1.6", // Core framework - "@angular/forms": "^20.1.6", // Reactive and template forms - "@angular/platform-browser": "^20.1.6", // Browser platform - "@angular/platform-server": "^20.1.6", // Server platform for SSR - "@angular/router": "^20.1.6", // Routing system - "@angular/ssr": "^20.0.2" // Server-side rendering -} -``` - -#### Angular Features Utilized -- **Standalone Components:** Modern component architecture without NgModules -- **Signal-Based State Management:** New reactive primitives -- **Server-Side Rendering (SSR):** For improved SEO and performance -- **Hydration with Event Replay:** Enhanced user experience -- **Strict Templates:** Enhanced type checking in templates -- **OnPush Change Detection:** Performance optimization strategy - -#### TypeScript Configuration -```typescript -// tsconfig.json -{ - "target": "ES2022", - "module": "preserve", - "strict": true, - "experimentalDecorators": true, - "strictTemplates": true, - "strictInjectionParameters": true -} -``` - -**Key TypeScript Features:** -- **Strict Mode:** Maximum type safety -- **Path Mapping:** Clean import aliases for architectural layers -- **Experimental Decorators:** Angular decorator support -- **ES2022 Target:** Modern JavaScript features - -### Styling Framework - -#### TailwindCSS 4.1.11 Implementation -```css -/* styles.css */ -@import 'tailwindcss'; -@custom-variant dark (&:where(.dark, .dark *)); -``` - -**TailwindCSS Features:** -- **Latest Version 4.1.11:** Cutting-edge CSS framework -- **Dark Mode Support:** Custom variant implementation -- **PostCSS Integration:** @tailwindcss/postcss ^4.1.11 -- **Component-Scoped Styling:** Organized in styles/ directory - -**Styling Architecture:** -``` -src/styles/ -├── colors.css # Color palette definitions -├── skeleton.css # Loading skeleton styles -├── globals.css # Global utility classes -└── components.css # Component-specific styles -``` - -### State Management & Data Flow - -#### Angular Signals + RxJS Hybrid -- **Angular Signals:** Primary state management for reactive UI -- **RxJS 7.8.0:** Asynchronous data streams and HTTP operations -- **Computed Values:** Derived state calculation -- **Effect System:** Side effect management - -#### Reactive Programming Patterns -```typescript -// Signal-based state management -private readonly _user = signal(null); -public readonly user = this._user.asReadonly(); -public readonly isAuthenticated = computed(() => !!this._user()); -``` - -## 3. Development Dependencies & Tooling - -### Build System & CLI Tools -```json -{ - "@angular/build": "^20.0.2", // Modern build system - "@angular/cli": "^20.0.2", // Command line interface - "@angular/compiler-cli": "^20.1.6" // AOT compiler -} -``` - -#### Build Configuration Features -- **Application Builder:** @angular/build:application -- **Server-Side Rendering:** Integrated SSR setup -- **Bundle Optimization:** Tree shaking and code splitting -- **Asset Processing:** SVG loader with text format -- **Bundle Budgets:** 500kB warning, 1MB error limits - -### Testing Framework -```json -{ - "jasmine-core": "~5.7.0", // Test framework - "karma": "~6.4.0", // Test runner - "karma-chrome-launcher": "~3.2.0", // Chrome browser launcher - "karma-coverage": "~2.2.0", // Code coverage - "karma-jasmine": "~5.1.0", // Jasmine integration - "karma-jasmine-html-reporter": "~2.1.0", // HTML reporting - "puppeteer": "^24.16.1" // Headless browser -} -``` - -#### Testing Configuration -```javascript -// karma.conf.cjs -{ - browsers: ['ChromeHeadless'], - customLaunchers: { - ChromeHeadlessCI: { - base: 'ChromeHeadless', - flags: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'] - } - } -} -``` - -### Development Utilities -```json -{ - "@inquirer/editor": "^4.2.15", // Interactive CLI editor - "@inquirer/prompts": "^7.8.0", // CLI prompts - "@types/express": "^5.0.1", // Express type definitions - "@types/jasmine": "~5.1.0", // Jasmine type definitions - "@types/node": "^20.17.19", // Node.js type definitions - "external-editor": "^3.1.0" // External editor integration -} -``` - -### Custom Development Tools - -#### Icon Linting System -```javascript -// scripts/lint-icons.cjs -// Custom SVG linter and fixer for Angular 20 + Tailwind v4 -// Features: -// - Validates viewBox requirements -// - Removes fixed width/height when viewBox exists -// - Normalizes fill/stroke to currentColor/none -// - Supports --fix, --dry, --backup modes -``` - -**Build Integration:** -```json -{ - "prebuild": "npm run lint:icons", - "lint:icons": "node scripts/lint-icons.cjs", - "fix:icons": "node scripts/lint-icons.cjs --fix" -} -``` - -## 4. Runtime Dependencies - -### HTTP & Communication -```json -{ - "express": "^5.1.0", // SSR server - "rxjs": "~7.8.0", // Reactive programming - "zone.js": "~0.15.0" // Angular change detection -} -``` - -### Data Visualization & Processing -```json -{ - "chart.js": "^4.5.0", // Chart rendering - "ng2-charts": "^8.0.0", // Angular Chart.js integration - "papaparse": "^5.5.3", // CSV parsing - "@types/papaparse": "^5.3.16" // Type definitions -} -``` - -### Document Generation -```json -{ - "jspdf": "^3.0.1", // PDF generation - "jspdf-autotable": "^5.0.2" // PDF table generation -} -``` - -### Build & Styling -```json -{ - "@tailwindcss/postcss": "^4.1.11", // PostCSS plugin - "postcss": "^8.5.6", // CSS post-processor - "tailwindcss": "^4.1.11", // Utility-first CSS - "tslib": "^2.3.0" // TypeScript runtime library -} -``` - -### License Information - -#### MIT Licensed Dependencies -- **Angular Framework:** MIT License -- **TailwindCSS:** MIT License -- **RxJS:** Apache 2.0 License -- **Chart.js:** MIT License -- **jsPDF:** MIT License -- **Express.js:** MIT License -- **PapaParse:** MIT License - -#### Development Dependencies -- **Jasmine:** MIT License -- **Karma:** MIT License -- **Puppeteer:** Apache 2.0 License -- **TypeScript:** Apache 2.0 License - -## 5. Implementation Patterns & Conventions - -### Naming Conventions - -#### File and Folder Naming -```typescript -// Files: kebab-case with type suffixes -user.entity.ts // Domain entities -auth.facade.ts // Application facades -http-user.repository.ts // Infrastructure repositories -login.page.ts // Presentation pages -error.interceptor.ts // Core interceptors -``` - -#### Class and Interface Naming -```typescript -// Classes: PascalCase -export class UserEntity { } -export class AuthFacade { } -export class HttpUserRepository { } - -// Interfaces: PascalCase with descriptive names -export interface UserRepository { } -export interface AuthContract { } -export interface NotificationPort { } - -// Types: PascalCase with Type suffix -export type LoginRequest = { }; -export type UserUpdateRequest = { }; -``` - -#### Variable and Method Naming -```typescript -// Variables: camelCase -private readonly _user = signal(null); -public readonly isAuthenticated = computed(() => !!this._user()); - -// Methods: camelCase with descriptive verbs -async login(request: LoginRequest): Promise { } -updateProfile(data: ProfileData): void { } -clearAuthStateCompletely(): void { } -``` - -#### Component Naming -```typescript -// Components: PascalCase without suffix -export class Login { } // Login page -export class Dashboard { } // Dashboard page -export class UserProfile { } // User profile component - -// Component files: kebab-case -login.ts -dashboard.ts -user-profile.ts -``` - -### Code Organization Patterns - -#### Import Organization -```typescript -// 1. Angular core imports -import { Component, inject, ChangeDetectionStrategy } from '@angular/core'; - -// 2. Angular feature imports -import { FormBuilder, Validators } from '@angular/forms'; - -// 3. Third-party imports -import { firstValueFrom } from 'rxjs'; - -// 4. Application imports (by layer) -import { AuthFacade } from '@application/facades/auth.facade'; -import { User } from '@domain/entities/user.entity'; -import { HttpUserRepository } from '@infrastructure/repositories/http-user.repository'; -import { LoginForm } from '@presentation/features/auth/forms/login-form'; -``` - -#### Path Alias Usage -```typescript -// TypeScript path mapping for clean imports -"@/*": ["src/*"] -"@app/*": ["src/app/*"] -"@domain/*": ["src/app/domain/*"] -"@application/*": ["src/app/application/*"] -"@infrastructure/*": ["src/app/infrastructure/*"] -"@presentation/*": ["src/app/presentation/*"] -"@core/*": ["src/app/core/*"] -"@shared/*": ["src/app/shared/*"] -"@di/*": ["src/app/di/*"] -``` - -### Component Architecture Patterns - -#### Standalone Component Pattern -```typescript -@Component({ - selector: 'app-login', - templateUrl: './login.html', - styleUrl: './login.css', - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [LoginForm, ReactiveFormsModule] // Direct imports -}) -export class Login implements OnInit { - // Implementation -} -``` - -#### Dependency Injection Pattern -```typescript -// Functional injection (preferred) -export class UserProfileComponent { - private readonly usersFacade = inject(UsersFacade); - private readonly formBuilder = inject(FormBuilder); -} - -// Token-based injection for repositories -@Injectable() -export class CreateUser { - constructor( - @Inject(USER_REPOSITORY) private readonly userRepo: UserRepository - ) {} -} -``` - -## 6. Code Organization Standards - -### Architectural Layer Organization - -#### Domain Layer Structure -``` -src/app/domain/ -├── entities/ # Business entities with behavior -│ ├── user.entity.ts -│ ├── role.entity.ts -│ └── session.entity.ts -├── value-objects/ # Immutable value objects -│ ├── email.ts -│ ├── username.ts -│ └── user-status.ts -├── contracts/ # Repository and service interfaces -│ ├── auth.contract.ts -│ └── user.contract.ts -├── events/ # Domain event definitions -├── errors/ # Domain-specific errors -├── enums/ # Domain enumerations -└── repositories/ # Repository interface definitions -``` - -#### Application Layer Structure -``` -src/app/application/ -├── facades/ # Simplified interfaces for presentation -│ ├── auth.facade.ts -│ ├── users.facade.ts -│ └── notifications.facade.ts -├── use-cases/ # Business workflow implementations -│ ├── auth/ -│ ├── users/ -│ └── notifications/ -├── services/ # Application-level services -├── errors/ # Application error handling -└── types/ # Application-specific types -``` - -#### Infrastructure Layer Structure -``` -src/app/infrastructure/ -├── repositories/ # Repository implementations -│ ├── http-auth.repository.ts -│ ├── http-user.repository.ts -│ └── http-role.repository.ts -├── mappers/ # DTO to entity mappers -├── services/ # External service implementations -├── http/ # HTTP utilities and interceptors -├── dtos/ # Data transfer objects -└── errors/ # Infrastructure error handling -``` - -#### Presentation Layer Structure -``` -src/app/presentation/ -├── features/ # Business feature implementations -│ ├── auth/ -│ ├── dashboard/ -│ ├── roles/ -│ └── users/ -├── layouts/ # Application layouts -├── navigation/ # Navigation components -├── shell/ # Application shell -└── shared/ # Shared presentation components -``` - -### Dependency Injection Organization - -#### Provider Configuration Structure -``` -src/app/di/ -├── tokens.ts # DI token definitions -├── provide-auth.ts # Authentication providers -├── provide-users.ts # User management providers -├── provide-roles.ts # Role management providers -├── provide-notifications.ts # Notification providers -├── provide-domain-events.ts # Domain event providers -├── provide-export.ts # Export service providers -└── provide-icons.ts # Icon system providers -``` - -#### Token Definition Pattern -```typescript -// tokens.ts -export const USER_REPOSITORY = new InjectionToken('UserRepository'); -export const AUTH_REPOSITORY = new InjectionToken('AuthRepository'); -export const SESSION_STORE_PORT = new InjectionToken('SessionStorePort'); -``` - -#### Provider Factory Pattern -```typescript -// provide-users.ts -export function provideUsers(): Provider[] { - return [ - { provide: USER_REPOSITORY, useClass: HttpUserRepository }, - { provide: USER_MAPPER, useClass: UserMapper }, - UsersFacade, - CreateUser, - UpdateUser, - DeleteUser - ]; -} -``` - -## 7. Usage Examples & Patterns - -### Component Implementation Examples - -#### Standard Page Component -```typescript -@Component({ - selector: 'app-login', - templateUrl: './login.html', - styleUrl: './login.css', - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [LoginForm] -}) -export class Login implements OnInit { - auth = inject(AuthFacade); - - ngOnInit(): void { - // Clear any previous auth errors and loading state - this.auth.clearAuthStateCompletely(); - } -} -``` - -#### Form Component Pattern -```typescript -@Component({ - selector: 'app-login-form', - templateUrl: './login-form.html', - styleUrl: './login-form.css', - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ReactiveFormsModule, NgOptimizedImage] -}) -export class LoginForm { - private readonly authFacade = inject(AuthFacade); - private readonly formBuilder = inject(FormBuilder); - - loginForm = this.formBuilder.group({ - identifier: ['', [Validators.required]], - password: ['', [Validators.required]], - rememberMe: [false] - }); - - async onSubmit(): Promise { - if (this.loginForm.valid) { - const request = this.createLoginRequest(); - await this.authFacade.login(request); - } - } -} -``` - -### Service Layer Examples - -#### Facade Implementation Pattern -```typescript -@Injectable({ providedIn: 'root' }) -export class AuthFacade { - private readonly loginUseCase = inject(LoginWithCredentials); - private readonly logoutUseCase = inject(Logout); - private readonly profileUseCase = inject(GetProfile); - - // Signal-based state management - private readonly _user = signal(null); - private readonly _loading = signal(false); - private readonly _error = signal(null); - - // Read-only computed state - public readonly user = this._user.asReadonly(); - public readonly isAuthenticated = computed(() => !!this._user()); - public readonly loading = this._loading.asReadonly(); - public readonly error = this._error.asReadonly(); - - async login(request: LoginRequest): Promise { - try { - this.setLoading(true); - this.clearError(); - - const session = await this.loginUseCase.execute(request); - this.setSession(session); - - } catch (error) { - this.handleError(error); - } finally { - this.setLoading(false); - } - } -} -``` - -#### Use Case Implementation Pattern -```typescript -@Injectable({ providedIn: 'root' }) -export class LoginWithCredentials { - private readonly authRepo = inject(AUTH_REPOSITORY); - private readonly sessionStore = inject(SESSION_STORE_PORT); - private readonly clock = inject(CLOCK_PORT); - - async execute(request: LoginRequest): Promise { - try { - // Application-level validation - this.validateRequest(request); - - // Execute domain logic - const session = await this.authRepo.login({ - identifier: request.identifier, - password: request.password, - rememberMe: request.rememberMe - }); - - // Handle session persistence - await this.sessionStore.save(session); - - return session; - } catch (error) { - throw ApplicationError.fromDomain(error); - } - } -} -``` - -### Repository Implementation Examples - -#### HTTP Repository Pattern -```typescript -@Injectable() -export class HttpUserRepository implements UserRepository { - private readonly baseUrl = `${environment.API_URL}/users`; - - constructor( - private readonly http: HttpClient, - private readonly mapper: UserMapper - ) {} - - async getById(id: number): Promise { - const response = await firstValueFrom( - this.http.get(`${this.baseUrl}/${id}`) - .pipe(catchError(this.handleHttpError)) - ); - return this.mapper.toDomain(response); - } - - async save(user: User): Promise { - const dto = this.mapper.toDTO(user); - - if (user.id) { - await firstValueFrom( - this.http.put(`${this.baseUrl}/${user.id}`, dto) - .pipe(catchError(this.handleHttpError)) - ); - } else { - await firstValueFrom( - this.http.post(this.baseUrl, dto) - .pipe(catchError(this.handleHttpError)) - ); - } - } -} -``` - -### Domain Entity Examples - -#### Rich Domain Entity Pattern -```typescript -export class User { - private _domainEvents: DomainEvent[] = []; - - private constructor( - public readonly id: number, - private _username: Username, - private _email: Email, - private _firstName: FirstName, - private _lastName: LastName, - private _active: boolean, - private _role: Role, - public readonly createdAt?: ISODateTime - ) {} - - static create(props: UserCreateProps): User { - const user = new User(/* ... */); - user.addDomainEvent(new UserCreatedEvent(user.id)); - return user; - } - - updateProfile(firstName: FirstName, lastName: LastName): void { - this._firstName = firstName; - this._lastName = lastName; - this.addDomainEvent(new UserProfileUpdatedEvent(this.id)); - } - - // Business logic methods - activate(): void { - if (this._active) { - throw new DomainError('User is already active'); - } - this._active = true; - this.addDomainEvent(new UserActivatedEvent(this.id)); - } -} -``` - -### Routing Examples - -#### Route Configuration Pattern -```typescript -export const routes: Routes = [ - { - path: 'auth', - canMatch: [guestOnly], - loadComponent: () => - import('@presentation/layouts/auth-layout/auth-layout').then(m => m.AuthLayout), - children: authRoutes - }, - { - path: '', - canMatch: [authOnly, emailConfirmedOnly], - loadComponent: () => - import('@presentation/layouts/main-layout/main-layout').then(m => m.MainLayout), - children: securedRoutes - } -]; -``` - -#### Guard Implementation Pattern -```typescript -export const authGuard: CanActivateFn = async () => { - const router = inject(Router); - const authFacade = inject(AuthFacade); - - if (!authFacade.sessionRestoreAttempted()) { - await authFacade.initializeAuth(); - } - - const isAuthenticated = authFacade.isAuthenticated(); - - if (isAuthenticated) { - return true; - } - - authFacade.clearAuthStateCompletely(); - return router.parseUrl('/auth/login'); -}; -``` - -## 8. Technology Stack Map - -### Core Technology Dependencies - -```mermaid -graph TB - subgraph "Development Environment" - Node[Node.js 20.17.19] - NPM[npm Package Manager] - TS[TypeScript 5.8.2] - end - - subgraph "Angular Ecosystem" - NG[Angular 20.1.6] - CLI[Angular CLI 20.0.2] - Build[Angular Build 20.0.2] - SSR[Angular SSR 20.0.2] - end - - subgraph "Styling & UI" - TW[TailwindCSS 4.1.11] - PostCSS[PostCSS 8.5.6] - Charts[Chart.js 4.5.0] - end - - subgraph "State & Data" - RxJS[RxJS 7.8.0] - Signals[Angular Signals] - HTTP[HttpClient] - end - - subgraph "Testing" - Jasmine[Jasmine 5.7.0] - Karma[Karma 6.4.0] - Puppeteer[Puppeteer 24.16.1] - end - - subgraph "Runtime" - Express[Express.js 5.1.0] - Zone[Zone.js 0.15.0] - end - - Node --> NPM - NPM --> NG - NG --> CLI - NG --> Build - NG --> SSR - TS --> NG - TW --> PostCSS - NG --> RxJS - NG --> Signals - NG --> HTTP - NG --> Zone - Express --> SSR -``` - -### Integration Architecture - -```mermaid -graph TB - subgraph "Frontend Layer" - Components[Angular Components] - Services[Angular Services] - Guards[Route Guards] - Interceptors[HTTP Interceptors] - end - - subgraph "Application Core" - Facades[Application Facades] - UseCases[Use Cases] - DomainEvents[Domain Events] - end - - subgraph "Domain Layer" - Entities[Domain Entities] - ValueObjects[Value Objects] - Repositories[Repository Contracts] - end - - subgraph "Infrastructure" - HttpRepos[HTTP Repositories] - Mappers[Data Mappers] - ExternalAPIs[External APIs] - end - - subgraph "Cross-Cutting" - ErrorHandling[Error Handling] - Logging[Logging] - Validation[Validation] - Auth[Authentication] - end - - Components --> Facades - Services --> Facades - Facades --> UseCases - UseCases --> Entities - UseCases --> Repositories - HttpRepos --> Repositories - Guards --> Auth - Interceptors --> ErrorHandling -``` - -### Technology Interaction Flow - -```mermaid -sequenceDiagram - participant User - participant Angular as Angular App - participant TW as TailwindCSS - participant Facade as Application Facade - participant UseCase as Use Case - participant Repo as HTTP Repository - participant API as Backend API - - User->>Angular: User Interaction - Angular->>TW: Apply Styles - Angular->>Facade: Call Business Operation - Facade->>UseCase: Execute Use Case - UseCase->>Repo: Repository Call - Repo->>API: HTTP Request - API-->>Repo: Response Data - Repo-->>UseCase: Domain Entity - UseCase-->>Facade: Result - Facade-->>Angular: Updated State - Angular-->>User: UI Update -``` - -## 9. Angular-Specific Implementation Details - -### Component Architecture Patterns - -#### Standalone Component Implementation -```typescript -// Modern Angular 20 standalone component -@Component({ - selector: 'app-user-profile', - templateUrl: './user-profile.html', - styleUrl: './user-profile.css', - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - ReactiveFormsModule, - NgOptimizedImage, - ProfileForm, - LoadingSpinner - ] -}) -export class UserProfile implements OnInit { - private readonly usersFacade = inject(UsersFacade); - private readonly route = inject(ActivatedRoute); - - // Signal-based reactive state - protected readonly user = this.usersFacade.selectedUser; - protected readonly loading = this.usersFacade.loading; - protected readonly error = this.usersFacade.error; -} -``` - -#### Signal-Based State Management -```typescript -// Facade with signal-based state -@Injectable({ providedIn: 'root' }) -export class UsersFacade { - // Private signals for internal state - private readonly _users = signal([]); - private readonly _selectedUser = signal(null); - private readonly _loading = signal(false); - private readonly _error = signal(null); - - // Public read-only signals - public readonly users = this._users.asReadonly(); - public readonly selectedUser = this._selectedUser.asReadonly(); - public readonly loading = this._loading.asReadonly(); - public readonly error = this._error.asReadonly(); - - // Computed signals for derived state - public readonly activeUsers = computed(() => - this._users().filter(user => user.active) - ); - - public readonly userCount = computed(() => this._users().length); -} -``` - -### Dependency Injection Patterns - -#### Provider Configuration Strategy -```typescript -// app.config.ts - Application configuration -export const appConfig: ApplicationConfig = { - providers: [ - // Core Angular providers - { provide: ErrorHandler, useClass: GlobalErrorHandler }, - provideAnimations(), - provideHttpClient(withInterceptors([ - authInterceptor, - httpErrorInterceptor, - enhancedErrorInterceptor - ])), - provideRouter(routes), - provideClientHydration(withEventReplay()), - - // Feature providers - provideAuth(), - provideNotifications(), - provideUsers(), - provideDomainEventsForDevelopment(), - ...provideRoles(), - provideExportServices(), - ...provideIcons({ - missingStrategy: 'warn', - defaultVariant: 'outline' - }) - ] -}; -``` - -#### Token-Based Repository Injection -```typescript -// Token definition -export const USER_REPOSITORY = new InjectionToken('UserRepository'); - -// Provider factory -export function provideUsers(): Provider[] { - return [ - { provide: USER_REPOSITORY, useClass: HttpUserRepository }, - { provide: USER_MAPPER, useClass: UserMapper }, - UsersFacade, - CreateUser, - UpdateUser, - DeleteUser - ]; -} - -// Usage in use case -@Injectable({ providedIn: 'root' }) -export class CreateUser { - constructor( - @Inject(USER_REPOSITORY) private readonly userRepo: UserRepository, - @Inject(CLOCK_PORT) private readonly clock: ClockPort - ) {} -} -``` - -### Reactive Programming Integration - -#### RxJS + Signals Hybrid Pattern -```typescript -@Injectable({ providedIn: 'root' }) -export class DataService { - private readonly http = inject(HttpClient); - private readonly _data = signal([]); - - // RxJS for HTTP operations - private readonly dataStream$ = this.http.get('/api/data').pipe( - retry(3), - catchError(this.handleError) - ); - - // Signals for state management - readonly data = this._data.asReadonly(); - - async loadData(): Promise { - try { - const data = await firstValueFrom(this.dataStream$); - this._data.set(data); - } catch (error) { - this.handleError(error); - } - } -} -``` - -### Route Configuration Patterns - -#### Lazy Loading with Guards -```typescript -export const routes: Routes = [ - { - path: 'auth', - canMatch: [guestOnly], - loadComponent: () => - import('@presentation/layouts/auth-layout/auth-layout') - .then(m => m.AuthLayout), - children: [ - { - path: 'login', - loadComponent: () => - import('@presentation/features/auth/pages/login/login') - .then(m => m.Login), - title: 'Iniciar Sesión' - } - ] - }, - { - path: '', - canMatch: [authOnly, emailConfirmedOnly], - loadComponent: () => - import('@presentation/layouts/main-layout/main-layout') - .then(m => m.MainLayout), - children: securedRoutes - } -]; -``` - -#### Functional Guards Implementation -```typescript -// Authentication guard -export const authGuard: CanActivateFn = async (route, state) => { - const router = inject(Router); - const authFacade = inject(AuthFacade); - - if (!authFacade.sessionRestoreAttempted()) { - await authFacade.initializeAuth(); - } - - return authFacade.isAuthenticated() || router.parseUrl('/auth/login'); -}; - -// Role-based guard -export const roleGuard = (requiredRole: string): CanActivateFn => { - return (route, state) => { - const authFacade = inject(AuthFacade); - const userRole = authFacade.user()?.role?.name; - return userRole === requiredRole; - }; -}; -``` - -### HTTP Interceptor Patterns - -#### Authentication Interceptor -```typescript -export const authInterceptor: HttpInterceptorFn = (req, next) => { - const authStore = inject(AUTH_USER_STORE_PORT); - const token = authStore.getToken(); - - if (token && !req.url.includes('/auth/')) { - req = req.clone({ - setHeaders: { - Authorization: `Bearer ${token}` - } - }); - } - - return next(req); -}; -``` - -#### Error Handling Interceptor -```typescript -export const httpErrorInterceptor: HttpInterceptorFn = (req, next) => { - const notificationsFacade = inject(NotificationsFacade); - - return next(req).pipe( - catchError((error: HttpErrorResponse) => { - if (error.status === 401) { - // Handle unauthorized - const authFacade = inject(AuthFacade); - authFacade.clearAuthStateCompletely(); - } else if (error.status >= 500) { - notificationsFacade.showError('Server error occurred'); - } - - return throwError(() => error); - }) - ); -}; -``` - -## 10. Blueprint for New Code Implementation - -### File and Class Templates - -#### Standard Component Template -```typescript -import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core'; -import { FormBuilder, FormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; -import { {EntityName}Facade } from '@application/facades/{entity-name}.facade'; - -@Component({ - selector: 'app-{entity-name}-{action}', - templateUrl: './{entity-name}-{action}.html', - styleUrl: './{entity-name}-{action}.css', - changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ReactiveFormsModule] -}) -export class {EntityName}{Action} implements OnInit { - private readonly facade = inject({EntityName}Facade); - private readonly formBuilder = inject(FormBuilder); - - form: FormGroup = this.createForm(); - - // Reactive state from facade - readonly data = this.facade.data; - readonly loading = this.facade.loading; - readonly error = this.facade.error; - - ngOnInit(): void { - this.facade.clearErrors(); - } - - async onSubmit(): Promise { - if (this.form.valid) { - const request = this.createRequest(); - await this.facade.{action}(request); - } - } - - private createForm(): FormGroup { - return this.formBuilder.group({ - // Form controls with validation - }); - } - - private createRequest(): {Action}{EntityName}Request { - // Map form to request - return { - // Request properties - }; - } -} -``` - -#### Facade Template -```typescript -import { Injectable, signal, computed, inject } from '@angular/core'; -import { {EntityName}UseCases } from '../use-cases/{entity-name}'; -import { ApplicationErrorTransformer } from '../errors/application-error.transformer'; -import type { {EntityName} } from '@domain/entities/{entity-name}.entity'; - -@Injectable({ providedIn: 'root' }) -export class {EntityName}Facade { - private readonly useCases = inject({EntityName}UseCases); - private readonly errorTransformer = inject(ApplicationErrorTransformer); - - // State signals - private readonly _items = signal<{EntityName}[]>([]); - private readonly _selectedItem = signal<{EntityName} | null>(null); - private readonly _loading = signal(false); - private readonly _error = signal(null); - - // Public read-only state - readonly items = this._items.asReadonly(); - readonly selectedItem = this._selectedItem.asReadonly(); - readonly loading = this._loading.asReadonly(); - readonly error = this._error.asReadonly(); - - // Computed state - readonly itemCount = computed(() => this._items().length); - readonly hasItems = computed(() => this._items().length > 0); - - async load{EntityName}s(): Promise { - try { - this.setLoading(true); - this.clearError(); - - const items = await this.useCases.getAll.execute(); - this._items.set(items); - - } catch (error) { - this.handleError(error); - } finally { - this.setLoading(false); - } - } - - private setLoading(loading: boolean): void { - this._loading.set(loading); - } - - private clearError(): void { - this._error.set(null); - } - - private handleError(error: unknown): void { - const appError = this.errorTransformer.transform(error); - this._error.set(appError.message); - } -} -``` - -#### Use Case Template -```typescript -import { Injectable, Inject } from '@angular/core'; -import { {ENTITY_NAME}_REPOSITORY } from '@di/tokens'; -import type { {EntityName}Repository } from '@domain/repositories/{entity-name}.repository'; -import type { {EntityName} } from '@domain/entities/{entity-name}.entity'; -import { ApplicationError } from '@application/errors/application-error'; - -@Injectable({ providedIn: 'root' }) -export class {Action}{EntityName} { - constructor( - @Inject({ENTITY_NAME}_REPOSITORY) private readonly repo: {EntityName}Repository - ) {} - - async execute(request: {Action}{EntityName}Request): Promise<{EntityName}> { - try { - // Validate request - this.validateRequest(request); - - // Execute business logic - const result = await this.performAction(request); - - return result; - } catch (error) { - if (error instanceof DomainError) { - throw ApplicationError.fromDomain(error); - } - throw new ApplicationError('Unexpected error', 'UNKNOWN_ERROR'); - } - } - - private validateRequest(request: {Action}{EntityName}Request): void { - // Application-level validation - if (!request) { - throw new ApplicationError('Request is required', 'VALIDATION_ERROR'); - } - } - - private async performAction(request: {Action}{EntityName}Request): Promise<{EntityName}> { - // Implementation - return await this.repo.{action}(request); - } -} - -export interface {Action}{EntityName}Request { - // Request properties -} -``` - -#### Repository Implementation Template -```typescript -import { Injectable, Inject } from '@angular/core'; -import { HttpClient, HttpParams } from '@angular/common/http'; -import { firstValueFrom, catchError } from 'rxjs'; -import type { {EntityName}Repository } from '@domain/repositories/{entity-name}.repository'; -import type { {EntityName} } from '@domain/entities/{entity-name}.entity'; -import { {EntityName}Mapper } from '../mappers/{entity-name}.mapper'; -import { environment } from '@env/environment'; - -@Injectable() -export class Http{EntityName}Repository implements {EntityName}Repository { - private readonly baseUrl = `${environment.API_URL}/{entity-path}`; - - constructor( - private readonly http: HttpClient, - private readonly mapper: {EntityName}Mapper - ) {} - - async getById(id: number): Promise<{EntityName}> { - const response = await firstValueFrom( - this.http.get<{EntityName}DTO>(`${this.baseUrl}/${id}`) - .pipe(catchError(this.handleHttpError)) - ); - return this.mapper.toDomain(response); - } - - async save(entity: {EntityName}): Promise { - const dto = this.mapper.toDTO(entity); - - if (entity.id) { - await firstValueFrom( - this.http.put(`${this.baseUrl}/${entity.id}`, dto) - .pipe(catchError(this.handleHttpError)) - ); - } else { - await firstValueFrom( - this.http.post(this.baseUrl, dto) - .pipe(catchError(this.handleHttpError)) - ); - } - } - - async delete(id: number): Promise { - await firstValueFrom( - this.http.delete(`${this.baseUrl}/${id}`) - .pipe(catchError(this.handleHttpError)) - ); - } - - private handleHttpError = (error: HttpErrorResponse): Observable => { - throw new InfrastructureError(error.message, error.status); - }; -} - -interface {EntityName}DTO { - // DTO properties -} -``` - -### Implementation Checklist - -#### New Feature Implementation Steps - -**1. Domain Layer First** -- [ ] Create domain entity in `domain/entities/` -- [ ] Define value objects in `domain/value-objects/` -- [ ] Create repository contract in `domain/contracts/` -- [ ] Add domain events if needed in `domain/events/` - -**2. Application Layer** -- [ ] Create use cases in `application/use-cases/{feature}/` -- [ ] Implement facade in `application/facades/` -- [ ] Define application types in `application/types/` -- [ ] Add error handling in `application/errors/` - -**3. Infrastructure Layer** -- [ ] Implement repository in `infrastructure/repositories/` -- [ ] Create data mappers in `infrastructure/mappers/` -- [ ] Define DTOs in `infrastructure/dtos/` - -**4. Dependency Injection** -- [ ] Define tokens in `di/tokens.ts` -- [ ] Create provider in `di/provide-{feature}.ts` -- [ ] Add to app configuration in `app.config.ts` - -**5. Presentation Layer** -- [ ] Create feature directory in `presentation/features/` -- [ ] Implement pages/components -- [ ] Add routing configuration -- [ ] Create forms if needed - -**6. Testing** -- [ ] Unit tests for domain entities -- [ ] Use case tests with mocks -- [ ] Component tests -- [ ] Integration tests - -### Integration Points - -#### Connecting with Authentication -```typescript -// Guard integration -{ - path: 'new-feature', - canActivate: [authGuard, roleGuard('ADMIN')], - loadComponent: () => import('./new-feature.component') -} - -// Service integration -@Injectable() -export class NewFeatureService { - private readonly authFacade = inject(AuthFacade); - - async performAction(): Promise { - const user = this.authFacade.user(); - if (!user) { - throw new Error('User not authenticated'); - } - // Implementation - } -} -``` - -#### Connecting with Notifications -```typescript -@Injectable() -export class NewFeatureFacade { - private readonly notifications = inject(NotificationsFacade); - - async createItem(request: CreateItemRequest): Promise { - try { - await this.createUseCase.execute(request); - this.notifications.showSuccess('Item created successfully'); - } catch (error) { - this.notifications.showError('Failed to create item'); - throw error; - } - } -} -``` - -## 11. Technology Relationship Diagrams - -### Complete Technology Stack Visualization - -```mermaid -graph TB - subgraph "Development Tools" - IDE[VS Code / WebStorm] - Node[Node.js 20.17.19] - NPM[npm Package Manager] - Git[Git Version Control] - end - - subgraph "Core Framework" - NG[Angular 20.1.6] - TS[TypeScript 5.8.2] - CLI[Angular CLI 20.0.2] - Build[Angular Build System] - end - - subgraph "State Management" - Signals[Angular Signals] - RxJS[RxJS 7.8.0] - HTTP[HttpClient] - Forms[Reactive Forms] - end - - subgraph "UI & Styling" - TW[TailwindCSS 4.1.11] - PostCSS[PostCSS 8.5.6] - Icons[Custom Icon System] - Charts[Chart.js 4.5.0] - end - - subgraph "Testing" - Jasmine[Jasmine 5.7.0] - Karma[Karma 6.4.0] - Puppeteer[Puppeteer 24.16.1] - Coverage[Code Coverage] - end - - subgraph "Build & Deploy" - SSR[Angular SSR 20.0.2] - Express[Express.js 5.1.0] - Zone[Zone.js 0.15.0] - Bundle[Bundle Analyzer] - end - - subgraph "Data Processing" - jsPDF[jsPDF 3.0.1] - Papa[PapaParse 5.5.3] - ng2Charts[ng2-charts 8.0.0] - end - - Node --> NPM - NPM --> NG - NG --> TS - NG --> CLI - NG --> Build - NG --> Signals - NG --> RxJS - NG --> HTTP - NG --> Forms - NG --> SSR - TW --> PostCSS - NG --> TW - NG --> Icons - Charts --> ng2Charts - Jasmine --> Karma - Karma --> Puppeteer - SSR --> Express - NG --> Zone - NG --> jsPDF - NG --> Papa -``` - -### Application Layer Technology Flow - -```mermaid -graph TB - subgraph "Presentation Layer Technologies" - Components[Angular Components] - Templates[Angular Templates] - Styles[TailwindCSS] - Forms[Reactive Forms] - Router[Angular Router] - Guards[Route Guards] - end - - subgraph "Application Layer Technologies" - Facades[Application Facades] - Signals[Angular Signals] - Computed[Computed Values] - Effects[Angular Effects] - end - - subgraph "Business Layer Technologies" - UseCases[Use Cases] - Entities[Domain Entities] - ValueObjects[Value Objects] - Events[Domain Events] - end - - subgraph "Infrastructure Technologies" - HttpClient[Angular HttpClient] - RxJS[RxJS Operators] - Interceptors[HTTP Interceptors] - LocalStorage[Browser Storage] - end - - subgraph "External Integration" - APIs[REST APIs] - Charts[Chart.js] - PDF[jsPDF] - CSV[PapaParse] - end - - Components --> Facades - Templates --> Styles - Forms --> Components - Router --> Guards - Facades --> Signals - Signals --> Computed - Computed --> Effects - Facades --> UseCases - UseCases --> Entities - Entities --> ValueObjects - UseCases --> Events - UseCases --> HttpClient - HttpClient --> RxJS - HttpClient --> Interceptors - Facades --> LocalStorage - HttpClient --> APIs - Components --> Charts - UseCases --> PDF - UseCases --> CSV -``` - -### Dependency Injection Flow - -```mermaid -graph TB - subgraph "Application Configuration" - AppConfig[app.config.ts] - Providers[Provider Functions] - Tokens[DI Tokens] - end - - subgraph "Feature Providers" - AuthProviders[provideAuth] - UserProviders[provideUsers] - RoleProviders[provideRoles] - NotificationProviders[provideNotifications] - IconProviders[provideIcons] - ExportProviders[provideExport] - end - - subgraph "Service Registration" - Repositories[Repository Implementations] - Facades[Application Facades] - UseCases[Use Case Implementations] - Mappers[Data Mappers] - end - - subgraph "Runtime Injection" - Components[Component Injection] - Services[Service Injection] - Guards[Guard Injection] - Interceptors[Interceptor Injection] - end - - AppConfig --> Providers - Providers --> AuthProviders - Providers --> UserProviders - Providers --> RoleProviders - Providers --> NotificationProviders - Providers --> IconProviders - Providers --> ExportProviders - - AuthProviders --> Repositories - UserProviders --> Facades - RoleProviders --> UseCases - NotificationProviders --> Mappers - - Tokens --> Repositories - Tokens --> Facades - Tokens --> UseCases - - Repositories --> Components - Facades --> Services - UseCases --> Guards - Mappers --> Interceptors -``` - -## 12. Technology Decision Context - -### Framework Selection Rationale - -#### Angular 20.x Selection -**Context:** Need for enterprise-grade frontend framework with strong TypeScript support -**Decision:** Angular 20.1.6 with standalone components -**Rationale:** -- **Enterprise Readiness:** Comprehensive framework with built-in solutions -- **TypeScript Integration:** Native TypeScript support with strict typing -- **Standalone Components:** Modern architecture without NgModules overhead -- **Signal-Based State:** New reactive primitives for better performance -- **SSR Support:** Built-in server-side rendering capabilities -- **Long-term Support:** Predictable release cycle and enterprise backing - -#### TailwindCSS 4.x Selection -**Context:** Need for maintainable, performant styling solution -**Decision:** TailwindCSS 4.1.11 (latest version) -**Rationale:** -- **Utility-First Approach:** Rapid development with consistent design system -- **Performance:** Optimized CSS output with unused style elimination -- **Developer Experience:** IntelliSense support and design system consistency -- **Dark Mode Support:** Built-in dark mode capabilities -- **Customization:** Extensive customization options without CSS bloat - -#### TypeScript 5.8.x Selection -**Context:** Need for type safety and modern JavaScript features -**Decision:** TypeScript 5.8.2 with strict configuration -**Rationale:** -- **Type Safety:** Compile-time error detection and prevention -- **Modern Features:** Latest ECMAScript features with compatibility -- **IDE Support:** Enhanced development experience with IntelliSense -- **Refactoring Safety:** Reliable code refactoring capabilities -- **Team Productivity:** Improved code maintainability and collaboration - -### Architectural Pattern Decisions - -#### Clean Architecture Implementation -**Context:** Need for maintainable, testable, and scalable application structure -**Decision:** Clean Architecture with Domain-Driven Design -**Rationale:** -- **Separation of Concerns:** Clear boundaries between business and technical concerns -- **Testability:** Easy unit testing through dependency inversion -- **Framework Independence:** Business logic independent of Angular specifics -- **Scalability:** Structure supports growing complexity -- **Team Development:** Clear guidelines for feature implementation - -#### Signal-Based State Management -**Context:** Need for reactive state management without external dependencies -**Decision:** Angular Signals with RxJS for async operations -**Rationale:** -- **Performance:** Fine-grained reactivity with minimal change detection -- **Native Integration:** Built into Angular framework -- **Simplicity:** Reduced complexity compared to external state management -- **Future-Proof:** Angular's recommended approach going forward -- **Developer Experience:** Intuitive API with TypeScript support - -### Technology Constraints & Boundaries - -#### Version Constraints -- **Angular:** Locked to 20.x for latest features and performance -- **TypeScript:** Must support Angular compiler requirements -- **Node.js:** Minimum 18.x for Angular 20 compatibility -- **TailwindCSS:** Latest 4.x for modern features and performance - -#### Browser Support -- **Modern Browsers:** Chrome 90+, Firefox 88+, Safari 14+, Edge 90+ -- **ES2022 Features:** Native support required -- **CSS Grid/Flexbox:** Modern layout features required -- **Web APIs:** Local Storage, Fetch API, Intersection Observer - -#### Development Constraints -- **Package Manager:** npm (not yarn or pnpm) for consistency -- **Module System:** ES Modules with Angular's module preservation -- **Build Target:** ES2022 for optimal performance -- **Bundle Size:** Maximum 1MB for main bundle - -### Technology Upgrade Paths - -#### Angular Framework -- **Current:** Angular 20.1.6 -- **Upgrade Path:** Follow Angular's 6-month release cycle -- **Breaking Changes:** Use Angular Update Guide for migrations -- **Strategy:** Update minor versions immediately, major versions after stabilization - -#### Dependencies -- **Security Updates:** Immediate application of security patches -- **Feature Updates:** Quarterly review of dependency updates -- **Major Versions:** Annual review with impact assessment -- **Legacy Dependencies:** Migration plan for deprecated packages - -#### Browser Support Evolution -- **Current Target:** ES2022 with modern browser support -- **Future Target:** ES2023+ as browser support improves -- **Legacy Support:** Maintain compatibility through transpilation -- **Feature Detection:** Progressive enhancement for new features - -### Legacy Technology Considerations - -#### Deprecated Features -- **NgModules:** Migrated to standalone components -- **ViewEngine:** Using Ivy renderer (default in Angular 9+) -- **Legacy Forms:** Using reactive forms exclusively -- **Legacy HTTP:** Using new HttpClient with interceptors - -#### Migration Strategies -- **Gradual Migration:** Feature-by-feature upgrade approach -- **Compatibility Layer:** Maintain interfaces during transitions -- **Testing Strategy:** Comprehensive testing during migrations -- **Rollback Plan:** Ability to revert problematic changes - ---- - -## Technology Stack Maintenance - -This technology stack blueprint was generated on August 23, 2025, reflecting the current state of the MAD-AI project's technology choices and implementation patterns. - -### Update Schedule -- **Monthly:** Dependency security updates and minor version bumps -- **Quarterly:** Framework updates and technology stack review -- **Annually:** Major version upgrades and technology evaluation -- **As Needed:** Security patches and critical bug fixes - -### Validation Process -1. **Automated Checks:** CI/CD pipeline validation of dependency compatibility -2. **Security Scanning:** Regular vulnerability assessment of dependencies -3. **Performance Monitoring:** Bundle size and runtime performance tracking -4. **Documentation Sync:** Keep blueprint aligned with actual implementation - -### Maintenance Responsibilities -- **Lead Developer:** Technology stack decisions and upgrade planning -- **DevOps Team:** Build system and deployment technology maintenance -- **Security Team:** Vulnerability monitoring and patch management -- **Development Team:** Feature implementation following established patterns - -This blueprint provides the foundation for consistent technology choices and implementation patterns across the MAD-AI project, ensuring maintainable and scalable development practices. diff --git a/.github/copilot/technology-guidelines.md b/.github/copilot/technology-guidelines.md deleted file mode 100644 index a11ba668..00000000 --- a/.github/copilot/technology-guidelines.md +++ /dev/null @@ -1,935 +0,0 @@ -# Technology-Specific Guidelines - -> **Detailed technology guidelines for maintaining consistency across the MAD-AI project** - -## Angular 20.x Specific Guidelines - -### 1. Modern Angular Features - -**Always use the latest Angular patterns:** - -```typescript -// ✅ Use new control flow syntax -@Component({ - template: ` - @if (user(); as currentUser) { - - } @else { - - } - ` -}) -``` - -**Avoid legacy Angular patterns:** - -```typescript -// ❌ Don't use old structural directives -*ngIf="user" -*ngFor="let item of items; trackBy: trackByFn" -[ngSwitch]="status" - -// ❌ Don't use constructor injection -constructor(private service: Service) {} - -// ❌ Don't use Subject/BehaviorSubject for local state -private dataSubject = new BehaviorSubject(null); -``` - -### 2. Signal-Based Architecture - -**Primary state management pattern:** - -```typescript -@Injectable({ providedIn: 'root' }) -export class ModernFacade { - // Private state signals - private readonly _data = signal([]); - private readonly _loading = signal(false); - private readonly _filter = signal(''); - - // Public computed state - readonly data = computed(() => this._data()); - readonly loading = computed(() => this._loading()); - readonly filteredData = computed(() => { - const data = this._data(); - const filter = this._filter(); - - if (!filter) return data; - - return data.filter(item => - item.name.toLowerCase().includes(filter.toLowerCase()) - ); - }); - - // Derived computed values - readonly dataCount = computed(() => this.filteredData().length); - readonly hasData = computed(() => this.dataCount() > 0); - readonly isEmpty = computed(() => !this.loading() && !this.hasData()); - - // Effects for side effects - private readonly persistEffect = effect(() => { - const data = this._data(); - if (data.length > 0) { - this.persistToStorage(data); - } - }); - - // State mutations - setData(data: Data[]): void { - this._data.set(data); - } - - setFilter(filter: string): void { - this._filter.set(filter); - } - - // Batch updates for performance - updateState(data: Data[], loading: boolean): void { - batch(() => { - this._data.set(data); - this._loading.set(loading); - }); - } -} -``` - -### 3. New Input/Output API - -**Use modern input/output patterns:** - -```typescript -@Component({ - selector: 'app-user-card', - template: ` -
- -

{{ user().name }}

-

{{ user().email }}

- - @if (!readOnly()) { - - } -
- ` -}) -export class UserCard { - // Modern input syntax with transforms and validation - readonly user = input.required(); - readonly compact = input(false, { transform: booleanAttribute }); - readonly readOnly = input(false, { alias: 'readonly' }); - readonly disabled = input(false, { - transform: (value: boolean | string) => - value === '' || value === true || value === 'true' - }); - - // Modern output syntax - readonly userEdit = output(); - readonly userDelete = output<{ id: number; name: string }>(); - - onEdit(): void { - const currentUser = this.user(); - this.userEdit.emit(currentUser); - } - - onDelete(): void { - const user = this.user(); - this.userDelete.emit({ - id: user.id, - name: user.name - }); - } - - // Input validation effect - private readonly validationEffect = effect(() => { - const user = this.user(); - if (!user.id || !user.name) { - console.warn('Invalid user data provided:', user); - } - }); -} -``` - -## TypeScript 5.8.2 Guidelines - -### 1. Strict Type Configuration - -**Always use strict TypeScript settings:** - -```typescript -// Use exact types, avoid any -interface StrictUserData { - readonly id: number; - readonly email: string; - readonly firstName: string; - readonly lastName: string; - readonly roles: readonly Role[]; - readonly metadata: Record; - readonly createdAt: Date; - readonly updatedAt?: Date; -} - -// Use utility types for variations -type CreateUserData = Omit; -type UpdateUserData = Partial>; -type UserSummary = Pick; - -// Use branded types for IDs -type UserId = number & { readonly __brand: 'UserId' }; -type RoleId = number & { readonly __brand: 'RoleId' }; - -function createUserId(id: number): UserId { - return id as UserId; -} -``` - -### 2. Advanced Type Patterns - -**Leverage TypeScript's advanced features:** - -```typescript -// Conditional types for API responses -type ApiResponse = T extends User - ? { user: T; permissions: string[] } - : T extends User[] - ? { users: T; totalCount: number; page: number } - : { data: T }; - -// Template literal types for event names -type UserEvents = `user-${string}`; -type SystemEvents = `system-${string}`; -type AllEvents = UserEvents | SystemEvents; - -// Mapped types for form validation -type ValidationRules = { - readonly [K in keyof T]: { - readonly required?: boolean; - readonly minLength?: number; - readonly maxLength?: number; - readonly pattern?: RegExp; - readonly validator?: (value: T[K]) => boolean; - }; -}; - -const userValidation: ValidationRules = { - email: { - required: true, - pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, - validator: (email) => !email.includes('test') - }, - firstName: { - required: true, - minLength: 2, - maxLength: 50 - } -} as const; - -// Discriminated unions for error handling -type Result = - | { success: true; data: T } - | { success: false; error: E }; - -async function safeApiCall( - apiCall: () => Promise -): Promise> { - try { - const data = await apiCall(); - return { success: true, data }; - } catch (error) { - const appError = error instanceof ApplicationError - ? error - : new ApplicationError('api', 'Unknown error', 'API_ERROR', error); - return { success: false, error: appError }; - } -} -``` - -### 3. Type Guards and Assertions - -**Implement proper type checking:** - -```typescript -// Type guards for runtime type checking -function isUser(obj: unknown): obj is User { - return typeof obj === 'object' && - obj !== null && - 'id' in obj && - 'email' in obj && - typeof (obj as any).id === 'number' && - typeof (obj as any).email === 'string'; -} - -function isUserArray(arr: unknown): arr is User[] { - return Array.isArray(arr) && arr.every(isUser); -} - -// Assertion functions for validation -function assertIsValidEmail(email: string): asserts email is string { - if (!email.includes('@')) { - throw new Error('Invalid email format'); - } -} - -// Custom type predicates -function hasPermission( - user: User, - permission: T -): user is User & { permissions: T[] } { - return user.permissions.includes(permission); -} - -// Usage in components -export class UserComponent { - processUserData(data: unknown): void { - if (isUser(data)) { - // TypeScript knows data is User - console.log(data.email); - - if (hasPermission(data, 'admin')) { - // TypeScript knows user has admin permission - this.showAdminFeatures(); - } - } - } -} -``` - -## Testing Framework Guidelines - -### 1. Jasmine 5.7.0 Patterns - -**Modern Jasmine testing patterns:** - -```typescript -describe('UsersFacade', () => { - let facade: UsersFacade; - let mockRepository: jasmine.SpyObj; - let mockNotifications: jasmine.SpyObj; - - beforeEach(() => { - // Create sophisticated spy objects - const repositorySpy = jasmine.createSpyObj('UserRepository', { - findAll: Promise.resolve([]), - findById: Promise.resolve(null), - save: Promise.resolve(), - delete: Promise.resolve() - }); - - const notificationsSpy = jasmine.createSpyObj('NotificationService', { - success: undefined, - error: undefined, - info: undefined - }); - - TestBed.configureTestingModule({ - providers: [ - UsersFacade, - { provide: USER_REPOSITORY, useValue: repositorySpy }, - { provide: NOTIFICATION_SERVICE, useValue: notificationsSpy } - ] - }); - - facade = TestBed.inject(UsersFacade); - mockRepository = TestBed.inject(USER_REPOSITORY) as jasmine.SpyObj; - mockNotifications = TestBed.inject(NOTIFICATION_SERVICE) as jasmine.SpyObj; - }); - - describe('loadUsers', () => { - it('should load users and update state', async () => { - // Arrange - const mockUsers = [ - createMockUser({ id: 1, name: 'John' }), - createMockUser({ id: 2, name: 'Jane' }) - ]; - mockRepository.findAll.and.returnValue(Promise.resolve(mockUsers)); - - // Act - await facade.loadUsers(); - - // Assert - expect(facade.users()).toEqual(mockUsers); - expect(facade.loading()).toBe(false); - expect(facade.error()).toBeNull(); - expect(mockRepository.findAll).toHaveBeenCalledTimes(1); - }); - - it('should handle errors gracefully', async () => { - // Arrange - const error = new Error('Network error'); - mockRepository.findAll.and.returnValue(Promise.reject(error)); - - // Act - await facade.loadUsers(); - - // Assert - expect(facade.users()).toEqual([]); - expect(facade.loading()).toBe(false); - expect(facade.error()).toBeTruthy(); - expect(facade.error()?.message).toContain('Failed to load users'); - }); - - it('should set loading state during operation', async () => { - // Arrange - let loadingDuringCall = false; - mockRepository.findAll.and.callFake(async () => { - loadingDuringCall = facade.loading(); - return []; - }); - - // Act - await facade.loadUsers(); - - // Assert - expect(loadingDuringCall).toBe(true); - expect(facade.loading()).toBe(false); - }); - }); - - describe('signal state management', () => { - it('should update computed values when state changes', () => { - // Arrange - const users = [ - createMockUser({ id: 1, active: true }), - createMockUser({ id: 2, active: false }), - createMockUser({ id: 3, active: true }) - ]; - - // Act - facade['_users'].set(users); - - // Assert - expect(facade.users()).toEqual(users); - expect(facade.activeUsers()).toHaveLength(2); - expect(facade.userCount()).toBe(3); - expect(facade.hasUsers()).toBe(true); - }); - - it('should handle empty state correctly', () => { - // Act - facade['_users'].set([]); - - // Assert - expect(facade.users()).toEqual([]); - expect(facade.activeUsers()).toHaveLength(0); - expect(facade.userCount()).toBe(0); - expect(facade.hasUsers()).toBe(false); - }); - }); -}); -``` - -### 2. Component Testing with Signals - -**Signal-aware component testing:** - -```typescript -describe('UserListComponent', () => { - let component: UserListComponent; - let fixture: ComponentFixture; - let mockFacade: jasmine.SpyObj; - - beforeEach(() => { - // Create facade spy with signal properties - const facadeSpy = jasmine.createSpyObj('UsersFacade', ['loadUsers', 'selectUser']); - - // Add signal properties - Object.defineProperty(facadeSpy, 'users', { - value: signal([]), - writable: true - }); - Object.defineProperty(facadeSpy, 'loading', { - value: signal(false), - writable: true - }); - Object.defineProperty(facadeSpy, 'error', { - value: signal(null), - writable: true - }); - - TestBed.configureTestingModule({ - imports: [UserListComponent], - providers: [ - { provide: UsersFacade, useValue: facadeSpy } - ] - }); - - fixture = TestBed.createComponent(UserListComponent); - component = fixture.componentInstance; - mockFacade = TestBed.inject(UsersFacade) as jasmine.SpyObj; - }); - - it('should display users when loaded', () => { - // Arrange - const users = [ - createMockUser({ id: 1, name: 'John Doe' }), - createMockUser({ id: 2, name: 'Jane Smith' }) - ]; - - // Act - mockFacade.users.set(users); - fixture.detectChanges(); - - // Assert - const userElements = fixture.nativeElement.querySelectorAll('.user-item'); - expect(userElements).toHaveLength(2); - expect(userElements[0].textContent).toContain('John Doe'); - expect(userElements[1].textContent).toContain('Jane Smith'); - }); - - it('should show loading state', () => { - // Act - mockFacade.loading.set(true); - fixture.detectChanges(); - - // Assert - const loadingElement = fixture.nativeElement.querySelector('.loading-spinner'); - expect(loadingElement).toBeTruthy(); - }); - - it('should display error message', () => { - // Arrange - const error = new ApplicationError('test', 'Test error message', 'TEST_ERROR'); - - // Act - mockFacade.error.set(error); - fixture.detectChanges(); - - // Assert - const errorElement = fixture.nativeElement.querySelector('.error-message'); - expect(errorElement).toBeTruthy(); - expect(errorElement.textContent).toContain('Test error message'); - }); -}); -``` - -### 3. Mock Factories and Builders - -**Consistent test data creation:** - -```typescript -// Mock factory functions -export function createMockUser(overrides: Partial = {}): User { - const defaultData: UserData = { - id: 1, - email: 'test@example.com', - firstName: 'Test', - lastName: 'User', - active: true, - roles: [], - createdAt: new Date('2024-01-01'), - ...overrides - }; - - return User.create(defaultData); -} - -export function createMockRole(overrides: Partial = {}): Role { - const defaultData: RoleData = { - id: 1, - name: 'user', - description: 'Standard user role', - permissions: ['read'], - ...overrides - }; - - return Role.create(defaultData); -} - -// Builder pattern for complex objects -export class UserTestBuilder { - private data: Partial = {}; - - withId(id: number): this { - this.data.id = id; - return this; - } - - withEmail(email: string): this { - this.data.email = email; - return this; - } - - withName(firstName: string, lastName: string): this { - this.data.firstName = firstName; - this.data.lastName = lastName; - return this; - } - - withRoles(...roles: Role[]): this { - this.data.roles = roles; - return this; - } - - active(): this { - this.data.active = true; - return this; - } - - inactive(): this { - this.data.active = false; - return this; - } - - build(): User { - return createMockUser(this.data); - } -} - -// Usage in tests -const adminUser = new UserTestBuilder() - .withId(1) - .withEmail('admin@example.com') - .withName('Admin', 'User') - .withRoles(createMockRole({ name: 'admin' })) - .active() - .build(); -``` - -## Build and Development Guidelines - -### 1. Angular CLI Configuration - -**Modern build configuration patterns:** - -```json -{ - "projects": { - "MAD-AI": { - "architect": { - "build": { - "builder": "@angular/build:application", - "options": { - "outputPath": "dist/mad-ai", - "index": "src/index.html", - "polyfills": ["zone.js"], - "tsConfig": "tsconfig.app.json", - "inlineStyleLanguage": "css", - "assets": ["public"], - "styles": ["src/styles.css"], - "scripts": [], - "budgets": [ - { - "type": "initial", - "maximumWarning": "500kB", - "maximumError": "1MB" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "2kB", - "maximumError": "4kB" - } - ] - }, - "configurations": { - "production": { - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "namedChunks": false, - "extractLicenses": true, - "serviceWorker": false - }, - "development": { - "optimization": false, - "extractLicenses": false, - "sourceMap": true, - "namedChunks": true - } - } - } - } - } - } -} -``` - -### 2. Performance Monitoring - -**Build performance tracking:** - -```typescript -// Custom build analyzer -export class BuildAnalyzer { - static analyzeBundles(): void { - const stats = require('./dist/stats.json'); - - const bundleSizes = stats.chunks.map((chunk: any) => ({ - name: chunk.names[0], - size: chunk.size, - modules: chunk.modules?.length || 0 - })); - - console.table(bundleSizes.sort((a, b) => b.size - a.size)); - - // Check for bundle size violations - const oversizedBundles = bundleSizes.filter(bundle => - bundle.size > 500 * 1024 // 500KB - ); - - if (oversizedBundles.length > 0) { - console.warn('Oversized bundles detected:', oversizedBundles); - } - } -} -``` - -## CSS and Styling Guidelines - -### 1. TailwindCSS Integration - -**Component-scoped styling with Tailwind:** - -```css -/* Component styles with Tailwind utilities */ -.user-card { - @apply bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 border border-gray-200 dark:border-gray-700; - @apply transition-all duration-200 hover:shadow-lg; -} - -.user-card.compact { - @apply p-4; -} - -.user-avatar { - @apply w-12 h-12 rounded-full object-cover border-2 border-gray-300 dark:border-gray-600; -} - -.user-name { - @apply text-lg font-semibold text-gray-900 dark:text-white; -} - -.user-email { - @apply text-sm text-gray-600 dark:text-gray-400; -} - -/* Status indicators */ -.status-active { - @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; - @apply bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200; -} - -.status-inactive { - @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium; - @apply bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200; -} - -/* Responsive design */ -@media (max-width: 640px) { - .user-card { - @apply p-4 rounded-md; - } - - .user-avatar { - @apply w-10 h-10; - } -} - -/* Accessibility features */ -@media (prefers-reduced-motion: reduce) { - .user-card { - @apply transition-none; - } -} - -@media (prefers-contrast: high) { - .user-card { - @apply border-2 border-black dark:border-white; - } -} -``` - -### 2. CSS Custom Properties - -**Dynamic theming with CSS variables:** - -```css -:root { - /* Primary colors */ - --color-primary-50: #eff6ff; - --color-primary-100: #dbeafe; - --color-primary-500: #3b82f6; - --color-primary-600: #2563eb; - --color-primary-900: #1e3a8a; - - /* Semantic colors */ - --color-success: #10b981; - --color-warning: #f59e0b; - --color-error: #ef4444; - --color-info: #06b6d4; - - /* Typography */ - --font-family-sans: ui-sans-serif, system-ui, -apple-system, sans-serif; - --font-family-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', monospace; - - /* Spacing scale */ - --spacing-unit: 0.25rem; /* 4px */ - --spacing-xs: calc(var(--spacing-unit) * 1); /* 4px */ - --spacing-sm: calc(var(--spacing-unit) * 2); /* 8px */ - --spacing-md: calc(var(--spacing-unit) * 4); /* 16px */ - --spacing-lg: calc(var(--spacing-unit) * 6); /* 24px */ - --spacing-xl: calc(var(--spacing-unit) * 8); /* 32px */ -} - -/* Dark theme */ -@media (prefers-color-scheme: dark) { - :root { - --color-background: #0f172a; - --color-surface: #1e293b; - --color-text-primary: #f8fafc; - --color-text-secondary: #cbd5e1; - --color-border: #374151; - } -} - -/* Component usage */ -.themed-component { - background-color: var(--color-surface); - color: var(--color-text-primary); - border: 1px solid var(--color-border); - padding: var(--spacing-md); - font-family: var(--font-family-sans); -} -``` - -## Development Workflow - -### 1. Code Generation Commands - -**Recommended Angular CLI commands:** - -```bash -# Generate standalone component -ng generate component features/users/components/user-card --standalone --change-detection=OnPush - -# Generate service with DI token -ng generate service application/facades/users --flat - -# Generate interface -ng generate interface domain/entities/user --type=entity - -# Generate use case -ng generate class application/use-cases/create-user --type=use-case - -# Generate repository interface -ng generate interface domain/repositories/user --type=repository - -# Generate guard -ng generate guard core/guards/auth --functional - -# Generate interceptor -ng generate interceptor core/interceptors/auth --functional -``` - -### 2. Development Scripts - -**Package.json scripts for development:** - -```json -{ - "scripts": { - "start": "ng serve", - "build": "ng build", - "build:prod": "ng build --configuration production", - "test": "ng test", - "test:watch": "ng test --watch", - "test:coverage": "ng test --code-coverage", - "lint": "ng lint", - "lint:fix": "ng lint --fix", - "e2e": "ng e2e", - "analyze": "ng build --configuration production --stats-json && npx webpack-bundle-analyzer dist/stats.json", - "serve:ssr": "ng build && ng run MAD-AI:serve-ssr", - "precommit": "npm run lint && npm run test:ci", - "type-check": "tsc --noEmit", - "icons:lint": "node scripts/lint-icons.cjs" - } -} -``` - -### 3. Git Hooks and Quality Gates - -**Pre-commit quality checks:** - -```typescript -// scripts/pre-commit.ts -import { execSync } from 'child_process'; - -interface QualityCheck { - name: string; - command: string; - required: boolean; -} - -const qualityChecks: QualityCheck[] = [ - { - name: 'TypeScript Compilation', - command: 'npm run type-check', - required: true - }, - { - name: 'Linting', - command: 'npm run lint', - required: true - }, - { - name: 'Unit Tests', - command: 'npm run test:ci', - required: true - }, - { - name: 'Build Check', - command: 'npm run build', - required: false - } -]; - -function runQualityChecks(): void { - console.log('🔍 Running quality checks...\n'); - - for (const check of qualityChecks) { - console.log(`Running ${check.name}...`); - - try { - execSync(check.command, { stdio: 'inherit' }); - console.log(`✅ ${check.name} passed\n`); - } catch (error) { - console.error(`❌ ${check.name} failed\n`); - - if (check.required) { - console.error('Commit aborted due to failed quality check.'); - process.exit(1); - } - } - } - - console.log('🎉 All quality checks completed!'); -} - -runQualityChecks(); -``` - ---- - -These technology-specific guidelines ensure that all team members and AI assistants use the most current and appropriate patterns for each technology in the MAD-AI project stack. diff --git a/docs/tests/test-implementation.md b/docs/tests/test-implementation.md new file mode 100644 index 00000000..ac86e1a6 --- /dev/null +++ b/docs/tests/test-implementation.md @@ -0,0 +1,375 @@ +# 📋 Guía de Testing por Capas - MAD-AI Project + +## 🎯 Filosofía General de Testing + +### Principios Fundamentales + +El testing en Clean Architecture + DDD debe seguir la **Pirámide de Testing**, donde la base (tests unitarios) es más amplia que la cima (tests E2E). Cada capa tiene responsabilidades específicas y por tanto, estrategias de testing diferentes. + +### Regla de Oro + +**"Testea el comportamiento, no la implementación"** - Los tests deben verificar QUÉ hace el código, no CÓMO lo hace. Esto permite refactorizar sin romper tests. + +--- + +## 🧠 Domain Layer Testing + +### ¿Qué testear? + +El Domain Layer contiene la **lógica de negocio pura**, por lo tanto debes testear: + +- **Reglas de negocio** en las entidades +- **Invariantes** que deben mantenerse siempre +- **Validaciones** de creación y modificación +- **Cálculos** y transformaciones de datos +- **Estado** y transiciones válidas +- **Eventos de dominio** que se generan + +### Estrategia de Testing + +**Tests UNITARIOS puros** - Sin dependencias externas, sin mocks, sin frameworks. Son los tests más importantes porque protegen tu lógica de negocio. + +### Cómo implementar + +1. **Para Entidades**: Crea instancias directamente y verifica que las reglas de negocio se cumplan. Testea los casos límite, valores inválidos y transiciones de estado. + +2. **Para Value Objects**: Verifica la inmutabilidad, la validación en creación y la igualdad por valor (no por referencia). + +3. **Para Domain Services**: Testea la lógica que coordina múltiples entidades. Usa entidades reales, no mocks. + +4. **Para Specifications**: Verifica que los criterios de aceptación funcionan correctamente con diferentes inputs. + +### Qué EVITAR + +- ❌ **NO uses mocks** en Domain - Si necesitas mocks, probablemente estás testeando en la capa incorrecta +- ❌ **NO dependas de infraestructura** - Nada de bases de datos, APIs, archivos +- ❌ **NO testees getters/setters simples** - Solo si contienen lógica +- ❌ **NO uses datos aleatorios** - Usa datos determinísticos y predecibles +- ❌ **NO ignores casos extremos** - Los edge cases revelan bugs en la lógica de negocio + +### Señales de buenos tests de dominio + +- ✅ Corren instantáneamente (< 1ms por test) +- ✅ No requieren setup complejo +- ✅ Los nombres describen reglas de negocio +- ✅ Fallan cuando cambias una regla de negocio +- ✅ No fallan cuando cambias la implementación técnica + +--- + +## 🎮 Application Layer Testing + +### ¿Qué testear? + +El Application Layer **orquesta** el flujo de casos de uso, por lo tanto debes testear: + +- **Flujo completo** del caso de uso +- **Coordinación** entre domain y infrastructure +- **Manejo de errores** y casos excepcionales +- **Transformación de datos** (mappers) +- **Validación de comandos** y queries +- **Emisión de eventos** después de operaciones + +### Estrategia de Testing + +**Tests UNITARIOS con MOCKS** - Mockea las dependencias (repositories, services) para testear la orquestación aisladamente. + +### Cómo implementar + +1. **Para Use Cases**: + - Mockea todos los repositories y servicios externos + - Verifica que se llamen en el orden correcto + - Testea tanto el happy path como los casos de error + - Asegúrate que los eventos de dominio se emitan + +2. **Para Commands/Queries**: + - Verifica validaciones de entrada + - Testea que los datos inválidos lancen excepciones + - No necesitas mockear nada aquí + +3. **Para Mappers**: + - Testea transformaciones bidireccionales + - Verifica que no se pierdan datos en la transformación + - Testea valores null y undefined + +4. **Para Handlers**: + - Similar a use cases pero más específicos + - Verifica que el Result pattern funcione correctamente + +### Qué EVITAR + +- ❌ **NO testees lógica de negocio aquí** - Eso va en Domain +- ❌ **NO hagas tests de integración reales** - Usa mocks +- ❌ **NO ignores el manejo de errores** - Es crítico en esta capa +- ❌ **NO couples los tests a la implementación** - Testea comportamiento +- ❌ **NO olvides testear los mappers** - Son fuente común de bugs + +### Señales de buenos tests de application + +- ✅ Usan mocks para todas las dependencias externas +- ✅ Verifican la secuencia de operaciones +- ✅ Testean todos los caminos posibles del flujo +- ✅ Son rápidos pero no tanto como Domain +- ✅ Verifican que se emitan los eventos correctos + +--- + +## 🔧 Infrastructure Layer Testing + +### ¿Qué testear? + +Infrastructure implementa los **contratos técnicos**, por lo tanto debes testear: + +- **Integración con APIs** externas (Django) +- **Transformación de DTOs** a entidades +- **Manejo de errores HTTP** +- **Persistencia y recuperación** de datos +- **Autenticación y autorización** +- **Interceptors y guards** + +### Estrategia de Testing + +**Tests de INTEGRACIÓN con MOCKS de HTTP** - Usa HttpClientTestingModule para simular respuestas del servidor. + +### Cómo implementar + +1. **Para Repositories**: + - Mockea las respuestas HTTP pero mantén la lógica real + - Testea transformación DTO ↔ Entity + - Verifica manejo de errores 404, 500, etc. + - Testea paginación y filtros + +2. **Para API Services**: + - Verifica que las URLs sean correctas + - Testea que los headers se envíen (Auth, Content-Type) + - Verifica estructura de request/response + - Testea reintentos y timeouts + +3. **Para Interceptors**: + - Verifica que modifiquen requests correctamente + - Testea manejo de tokens expirados + - Verifica logging de errores + +4. **Para Guards**: + - Testea protección de rutas + - Verifica redirecciones + - Testea diferentes roles y permisos + +### Qué EVITAR + +- ❌ **NO hagas llamadas reales a APIs** - Usa HttpClientTestingModule +- ❌ **NO testees la lógica de Django** - Solo tu integración +- ❌ **NO ignores códigos de error HTTP** - Cada uno debe manejarse +- ❌ **NO hardcodees URLs** - Usa configuración +- ❌ **NO olvides testear edge cases** - Tokens expirados, sin conexión, etc. + +### Señales de buenos tests de infrastructure + +- ✅ Simulan todas las respuestas posibles del servidor +- ✅ Verifican transformación correcta de datos +- ✅ Testean todos los códigos de error HTTP +- ✅ Son determinísticos (no dependen de servicios externos) +- ✅ Verifican que se respeten los contratos de la API + +--- + +## 🎨 Presentation Layer Testing + +### ¿Qué testear? + +Presentation maneja la **interacción con el usuario**, por lo tanto debes testear: + +- **Renderizado condicional** basado en estado +- **Manejo de eventos** del usuario +- **Validación de formularios** +- **Navegación y routing** +- **Estados de loading y error** +- **Transformación de datos para mostrar** + +### Estrategia de Testing + +**Tests de COMPONENTES con TestBed** - Usa Angular Testing Utilities para testear componentes aislados. + +### Cómo implementar + +1. **Para Components**: + - Testea que rendericen correctamente según el estado + - Verifica que los eventos se emitan + - Testea interacciones del usuario (clicks, inputs) + - Verifica estados loading/error/empty + - Testea que se llamen los use cases correctos + +2. **Para Pipes**: + - Testea transformaciones con diferentes inputs + - Verifica manejo de nulls y undefined + - Testea formatos de fecha, moneda, etc. + +3. **Para Directives**: + - Verifica que modifiquen el DOM correctamente + - Testea diferentes configuraciones + - Verifica cleanup en destroy + +4. **Para Guards**: + - Testea protección de rutas + - Verifica redirecciones según permisos + - Testea integración con auth service + +5. **Para Services de UI**: + - Testea estado compartido entre componentes + - Verifica notificaciones y toasts + - Testea theme switching + +### Qué EVITAR + +- ❌ **NO testees lógica de negocio** - Solo presentación +- ❌ **NO hagas tests frágiles** - No dependas de CSS específico +- ❌ **NO testees framework de Angular** - Solo tu código +- ❌ **NO ignores accesibilidad** - Testea ARIA labels +- ❌ **NO uses datos reales** - Crea fixtures específicos + +### Señales de buenos tests de presentation + +- ✅ Testean interacción del usuario, no implementación +- ✅ Verifican que la información se muestre correctamente +- ✅ Son resistentes a cambios de estilo +- ✅ Testean todos los estados posibles del componente +- ✅ Usan data-cy attributes para selectores + +--- + +## 🚀 E2E Testing + +### ¿Qué testear? + +E2E tests verifican **flujos completos de usuario**, por lo tanto debes testear: + +- **User journeys críticos** (login → crear recurso → asignar → logout) +- **Flujos de negocio importantes** +- **Integraciones entre módulos** +- **Comportamiento cross-browser** +- **Responsive design** en diferentes viewports + +### Estrategia de Testing + +**Tests E2E con Cypress** - Simula un usuario real interactuando con la aplicación completa. + +### Cómo implementar + +1. **Identifica flujos críticos**: + - Login y authentication + - CRUD operations principales + - Flujos de aprobación + - Reportes y exports + +2. **Estructura los tests**: + - Usa Page Object Pattern + - Agrupa por features + - Reutiliza comandos custom + - Mantén tests independientes + +3. **Maneja datos**: + - Usa fixtures para datos de prueba + - Limpia estado entre tests + - Mockea servicios externos cuando sea necesario + +4. **Verifica resultados**: + - No solo que "funcione", sino que los datos sean correctos + - Verifica en múltiples puntos del flujo + - Testea que los cambios persistan + +### Qué EVITAR + +- ❌ **NO testees cada detalle** - E2E es para flujos críticos +- ❌ **NO hagas tests dependientes** - Cada test debe ser independiente +- ❌ **NO uses selectores frágiles** - Usa data-cy attributes +- ❌ **NO ignores el tiempo** - Usa comandos wait apropiadamente +- ❌ **NO testees terceros** - Solo tu aplicación + +### Señales de buenos tests E2E + +- ✅ Testean flujos completos de negocio +- ✅ Son independientes y pueden correr en cualquier orden +- ✅ Usan datos predecibles (fixtures) +- ✅ Son mantenibles y legibles +- ✅ Fallan por razones de negocio, no técnicas + +--- + +## 📊 Métricas y Coverage + +### Coverage Objetivo por Capa + +| Capa | Coverage Mínimo | Coverage Ideal | Prioridad | +| ------------------ | --------------- | ---------------- | --------- | +| **Domain** | 90% | 100% | CRÍTICA | +| **Application** | 80% | 95% | ALTA | +| **Infrastructure** | 70% | 85% | MEDIA | +| **Presentation** | 60% | 80% | MEDIA | +| **E2E** | Flujos críticos | Todos los flujos | ALTA | + +### Qué medir además de coverage + +- **Mutation Testing**: ¿Los tests detectan cambios en el código? +- **Test Execution Time**: ¿Son rápidos los tests? +- **Flakiness**: ¿Son determinísticos? +- **Maintainability**: ¿Es fácil actualizar los tests? + +--- + +## 🔄 Proceso de Testing + +### Durante el Desarrollo (TDD) + +1. **Domain First**: Escribe tests de dominio antes del código +2. **Application Second**: Tests de use cases con mocks +3. **Infrastructure Third**: Tests de integración +4. **Presentation Last**: Tests de componentes +5. **E2E Final**: Solo para flujos completos + +### En CI/CD Pipeline + +1. **Pre-commit**: Unit tests de Domain +2. **Pre-push**: Todos los unit tests +3. **PR/MR**: Unit + Integration tests +4. **Pre-deploy**: Todos los tests incluido E2E +5. **Post-deploy**: Smoke tests en producción + +--- + +## ⚠️ Anti-patterns Comunes + +### Lo que NUNCA debes hacer + +1. **Test Todo**: No testees getters/setters simples o código trivial +2. **Test Nada**: "No tengo tiempo" siempre resulta en más tiempo después +3. **Mock Todo**: Los mocks excesivos ocultan problemas de diseño +4. **No Mock Nada**: Tests lentos y frágiles +5. **Tests Acoplados**: Tests que dependen del orden de ejecución +6. **Tests No Determinísticos**: Que a veces pasan y a veces no +7. **Tests Sin Assert**: Tests que nunca fallan no sirven +8. **Ignorar Tests Rotos**: "Skip" es deuda técnica + +--- + +## ✅ Checklist de Testing por Feature + +Antes de considerar una feature completa: + +- [ ] Domain tests cubren todas las reglas de negocio +- [ ] Application tests verifican el flujo completo +- [ ] Infrastructure tests verifican integración con API +- [ ] Component tests verifican renderizado y UX +- [ ] E2E test verifica el happy path +- [ ] Tests de error cases en todas las capas +- [ ] Coverage > mínimo establecido +- [ ] Tests corren en < 10 segundos (excepto E2E) +- [ ] Documentación actualizada +- [ ] No hay tests skippeados + +--- + +## 🎯 Conclusión + +El testing en Clean Architecture no es opcional, es **fundamental**. Cada capa tiene su estrategia específica que respeta sus responsabilidades. Los tests son tu red de seguridad para refactorizar con confianza y mantener la calidad del código. + +**Recuerda**: Los tests no son para encontrar bugs (aunque lo hacen), son para **prevenir** que los bugs lleguen a producción y para **documentar** el comportamiento esperado del sistema. diff --git a/src/app/infrastructure/services/notification/notification-gateway.service.ts b/src/app/infrastructure/services/notification/notification-gateway.service.ts index 38ba57e4..5507bb72 100644 --- a/src/app/infrastructure/services/notification/notification-gateway.service.ts +++ b/src/app/infrastructure/services/notification/notification-gateway.service.ts @@ -6,7 +6,7 @@ import { Notification, NotificationId, } from '@domain/entities/notification.entity'; -import { UINotificationPosition } from '@shared/components/toast/enums/ui-notification-position.enum'; +import { UINotificationPosition } from '@presentation/shared/components/toast/enums/ui-notification-position.enum'; import { NotificationType } from '@/app/domain/enums/notification-type.enum'; /** diff --git a/src/app/infrastructure/services/storage/local-storage-auth-user-store.service.ts b/src/app/infrastructure/services/storage/local-storage-auth-user-store.service.ts index 433c0c66..b1ca21b7 100644 --- a/src/app/infrastructure/services/storage/local-storage-auth-user-store.service.ts +++ b/src/app/infrastructure/services/storage/local-storage-auth-user-store.service.ts @@ -1,4 +1,5 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject, PLATFORM_ID } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; import { AuthUserStoreRepository } from '@domain/repositories/session/auth-user-store.repository'; import { AuthUserSnapshotContract } from '@/app/domain/repositories/session/auth-user-store.contract'; @@ -6,21 +7,48 @@ const KEY = 'mad-ai.auth.user.v1'; @Injectable({ providedIn: 'root' }) export class LocalStorageAuthUserStore implements AuthUserStoreRepository { + private readonly platformId = inject(PLATFORM_ID); + private readonly isBrowser = isPlatformBrowser(this.platformId); + async read(): Promise { + if (!this.isBrowser) { + return null; + } + try { const raw = localStorage.getItem(KEY); return raw ? JSON.parse(raw) : null; - } catch { + } catch (error) { + console.warn(`LocalStorageAuthUserStore: Error reading from localStorage for key "${KEY}":`, error); return null; } } async write(snapshot: AuthUserSnapshotContract | null): Promise { - if (!snapshot) return this.clear(); - localStorage.setItem(KEY, JSON.stringify(snapshot)); + if (!this.isBrowser) { + return; + } + + try { + if (!snapshot) { + this.clear(); + } else { + localStorage.setItem(KEY, JSON.stringify(snapshot)); + } + } catch (error) { + console.warn(`LocalStorageAuthUserStore: Error writing to localStorage for key "${KEY}":`, error); + } } async clear(): Promise { - localStorage.removeItem(KEY); + if (!this.isBrowser) { + return; + } + + try { + localStorage.removeItem(KEY); + } catch (error) { + console.warn(`LocalStorageAuthUserStore: Error removing from localStorage for key "${KEY}":`, error); + } } } diff --git a/src/app/infrastructure/services/storage/local-storage-token-store.service.ts b/src/app/infrastructure/services/storage/local-storage-token-store.service.ts index 682be5e5..a9a300fc 100644 --- a/src/app/infrastructure/services/storage/local-storage-token-store.service.ts +++ b/src/app/infrastructure/services/storage/local-storage-token-store.service.ts @@ -1,4 +1,5 @@ -import { Injectable } from '@angular/core'; +import { Injectable, inject, PLATFORM_ID } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; import { TokenStoreRepository } from '@domain/repositories/session/token-store.repository'; import { TokenSnapshotContract } from '@/app/domain/repositories/session/token-store.contract'; @@ -6,20 +7,48 @@ const KEY = 'mad-ai.auth.tokens.v1'; @Injectable({ providedIn: 'root' }) export class LocalStorageTokenStore implements TokenStoreRepository { + private readonly platformId = inject(PLATFORM_ID); + private readonly isBrowser = isPlatformBrowser(this.platformId); + async read(): Promise { - const raw = localStorage.getItem(KEY); - return raw ? JSON.parse(raw) : null; + if (!this.isBrowser) { + return null; + } + + try { + const raw = localStorage.getItem(KEY); + return raw ? JSON.parse(raw) : null; + } catch (error) { + console.warn(`LocalStorageTokenStore: Error reading from localStorage for key "${KEY}":`, error); + return null; + } } async write(snapshot: TokenSnapshotContract | null): Promise { - if (snapshot) { - localStorage.setItem(KEY, JSON.stringify(snapshot)); - } else { - this.clear(); + if (!this.isBrowser) { + return; + } + + try { + if (snapshot) { + localStorage.setItem(KEY, JSON.stringify(snapshot)); + } else { + this.clear(); + } + } catch (error) { + console.warn(`LocalStorageTokenStore: Error writing to localStorage for key "${KEY}":`, error); } } async clear(): Promise { - localStorage.removeItem(KEY); + if (!this.isBrowser) { + return; + } + + try { + localStorage.removeItem(KEY); + } catch (error) { + console.warn(`LocalStorageTokenStore: Error removing from localStorage for key "${KEY}":`, error); + } } }