/** * Info Center Mapper * * Handles bidirectional transformations for InfoCenter domain entities: * - DiscoveredPage: Web pages discovered during crawling * - PageAnalysis: Element analysis results for pages * * Mappers transform between: * - Domain entities (business logic layer) * - DTOs (persistence/API layer) * * @layer Application */ import { IMapper } from './IMapper'; import { DiscoveredPage, DiscoverySource, PageStatus, AnalysisStatus, } from '../../domain/entities/DiscoveredPage'; import { PageAnalysis, PageElement, SelectorStrategy, } from '../../domain/entities/PageAnalysis'; /** * DiscoveredPage DTO Type * * Persistence format for discovered pages (localStorage) */ export interface DiscoveredPageDTO { id: string; tenantId: string; url: string; normalizedUrl: string; title?: string; discoverySource: DiscoverySource; discoveredAt: string; depth: number; status: PageStatus; analysisStatus: AnalysisStatus; pageType?: string; semanticLabel?: string; parentPageId?: string; httpStatus?: number; lastAnalyzedAt?: string; elementCount?: number; addedToTraining?: boolean; } /** * PageAnalysis DTO Type * * Persistence format for page analyses (localStorage) */ export interface PageAnalysisDTO { id: string; tenantId: string; discoveredPageId?: string; url: string; version: number; status: AnalysisStatus; analyzedAt: string; title?: string; elements: PageElement[]; elementCount: number; analysisDurationMs?: number; } /** * DiscoveredPageMapper * * Handles DTO ↔ Domain transformations for DiscoveredPage entities. * Implements IMapper interface for consistency. */ export class DiscoveredPageMapper implements IMapper { /** * Converts DTO to DiscoveredPage domain entity * * @param dto - DiscoveredPage DTO from persistence * @returns DiscoveredPage domain entity */ toDomain(dto: DiscoveredPageDTO): DiscoveredPage { return DiscoveredPage.fromPersistence(dto); } /** * Converts DiscoveredPage domain entity to DTO * * @param entity - DiscoveredPage domain entity * @returns DiscoveredPage DTO for persistence */ toDTO(entity: DiscoveredPage): DiscoveredPageDTO { return entity.toPersistence(); } /** * Converts request data to DiscoveredPage domain entity * * Used for creating new discovered pages from form/API input. * * @param request - Page creation request data * @returns DiscoveredPage domain entity */ fromRequest(request: { id: string; tenantId: string; url: string; normalizedUrl: string; title?: string; discoverySource: DiscoverySource; depth: number; status?: PageStatus; pageType?: string; semanticLabel?: string; parentPageId?: string; }): DiscoveredPage { return DiscoveredPage.create(request); } /** * Converts array of DTOs to domain entities * * @param dtos - Array of DiscoveredPage DTOs * @returns Array of DiscoveredPage domain entities */ toDomainArray(dtos: DiscoveredPageDTO[]): DiscoveredPage[] { return dtos.map((dto) => this.toDomain(dto)); } /** * Converts array of domain entities to DTOs * * @param entities - Array of DiscoveredPage domain entities * @returns Array of DiscoveredPage DTOs */ toDTOArray(entities: DiscoveredPage[]): DiscoveredPageDTO[] { return entities.map((entity) => this.toDTO(entity)); } } /** * PageAnalysisMapper * * Handles DTO ↔ Domain transformations for PageAnalysis entities. * Manages complex nested element structures. */ export class PageAnalysisMapper implements IMapper { /** * Converts DTO to PageAnalysis domain entity * * @param dto - PageAnalysis DTO from persistence * @returns PageAnalysis domain entity */ toDomain(dto: PageAnalysisDTO): PageAnalysis { return PageAnalysis.fromPersistence(dto); } /** * Converts PageAnalysis domain entity to DTO * * @param entity - PageAnalysis domain entity * @returns PageAnalysis DTO for persistence */ toDTO(entity: PageAnalysis): PageAnalysisDTO { return entity.toPersistence(); } /** * Converts request data to PageAnalysis domain entity * * Used for creating new page analyses from form/API input. * * @param request - Analysis creation request data * @returns PageAnalysis domain entity */ fromRequest(request: { id: string; tenantId: string; discoveredPageId?: string; url: string; title?: string; elements: PageElement[]; analysisDurationMs?: number; }): PageAnalysis { return PageAnalysis.create(request); } /** * Converts array of DTOs to domain entities * * @param dtos - Array of PageAnalysis DTOs * @returns Array of PageAnalysis domain entities */ toDomainArray(dtos: PageAnalysisDTO[]): PageAnalysis[] { return dtos.map((dto) => this.toDomain(dto)); } /** * Converts array of domain entities to DTOs * * @param entities - Array of PageAnalysis domain entities * @returns Array of PageAnalysis DTOs */ toDTOArray(entities: PageAnalysis[]): PageAnalysisDTO[] { return entities.map((entity) => this.toDTO(entity)); } } /** * PageElementMapper * * Utility mapper for PageElement nested objects. * Handles selector strategies and position data. */ export class PageElementMapper { /** * Validates PageElement structure * * Ensures element has required fields and valid selector strategies. * * @param element - PageElement to validate * @throws Error if element is invalid */ static validate(element: PageElement): void { if (!element.id) { throw new Error('Page element must have an ID'); } if (!element.type) { throw new Error('Page element must have a type'); } if (!element.selectors || element.selectors.length === 0) { throw new Error('Page element must have at least one selector'); } if (!element.semanticDescription) { throw new Error('Page element must have a semantic description'); } // Validate selectors for (const selector of element.selectors) { this.validateSelector(selector); } } /** * Validates SelectorStrategy structure * * @param selector - SelectorStrategy to validate * @throws Error if selector is invalid */ static validateSelector(selector: SelectorStrategy): void { if (!selector.type) { throw new Error('Selector must have a type'); } if (!selector.value || selector.value.trim().length === 0) { throw new Error('Selector must have a value'); } if (typeof selector.score !== 'number' || selector.score < 0 || selector.score > 1) { throw new Error('Selector score must be a number between 0 and 1'); } } /** * Creates a deep copy of PageElement * * @param element - PageElement to copy * @returns Deep copy of PageElement */ static clone(element: PageElement): PageElement { return { id: element.id, type: element.type, selectors: element.selectors.map((s) => ({ ...s })), semanticDescription: element.semanticDescription, text: element.text, isVisible: element.isVisible, position: element.position ? { ...element.position } : undefined, }; } /** * Merges element updates into existing element * * @param element - Original PageElement * @param updates - Partial updates to apply * @returns Updated PageElement */ static merge(element: PageElement, updates: Partial): PageElement { return { ...element, ...updates, // Ensure selectors are properly merged (not shallow copied) selectors: updates.selectors ? updates.selectors.map((s) => ({ ...s })) : element.selectors, // Ensure position is properly merged position: updates.position !== undefined ? updates.position ? { ...updates.position } : undefined : element.position ? { ...element.position } : undefined, }; } } /** * Singleton mapper instances for reuse */ export const discoveredPageMapper = new DiscoveredPageMapper(); export const pageAnalysisMapper = new PageAnalysisMapper();