/** * User Mapper * * Handles transformations between User domain entities and DTOs. * Validates data using Zod schemas from @archer/api-interface. * * Transformations: * - DTO → Domain: Uses User.fromPersistence() factory method * - Domain → DTO: Converts entity to API response format * - Request → Domain: Validates and creates domain entity from form/request data * * @layer Application */ import { IMapper } from './IMapper'; import { User, UserRole, UserStatus } from '../../domain/entities/User'; import { z } from 'zod'; /** * User DTO interface (localStorage format) * * Matches the structure stored in localStorage. * Dates are stored as ISO strings. */ interface UserDTO { id: string; email: string; name: string; companyId: string; role: UserRole; status: UserStatus; avatar?: string | null; createdAt: string; updatedAt: string; } /** * Create User Request Schema * * Validates user creation requests. * Based on @archer/api-interface but adapted for dashboard needs. */ const CreateUserRequestSchema = z.object({ email: z .string() .email('Invalid email format') .min(1, 'Email is required') .max(255, 'Email must be less than 255 characters'), name: z .string() .min(1, 'Name is required') .max(200, 'Name must be less than 200 characters'), companyId: z.string().uuid('Invalid company ID format'), role: z.nativeEnum(UserRole, { errorMap: () => ({ message: 'Invalid user role' }), }), status: z.nativeEnum(UserStatus, { errorMap: () => ({ message: 'Invalid user status' }), }).optional(), avatar: z.string().url('Invalid avatar URL').optional().nullable(), }); /** * Update User Request Schema * * Validates user update requests. * All fields are optional for partial updates. */ const UpdateUserRequestSchema = z.object({ email: z .string() .email('Invalid email format') .min(1, 'Email cannot be empty') .max(255, 'Email must be less than 255 characters') .optional(), name: z .string() .min(1, 'Name cannot be empty') .max(200, 'Name must be less than 200 characters') .optional(), role: z.nativeEnum(UserRole, { errorMap: () => ({ message: 'Invalid user role' }), }).optional(), status: z.nativeEnum(UserStatus, { errorMap: () => ({ message: 'Invalid user status' }), }).optional(), avatar: z.string().url('Invalid avatar URL').optional().nullable(), }); /** * UserMapper * * Implements IMapper interface for User domain entity. * Provides bidirectional transformations with validation. */ export class UserMapper implements IMapper { /** * Converts DTO to domain entity * * Transforms data from localStorage to User domain entity. * Uses User.fromPersistence() factory method. * * @param dto - User DTO from localStorage * @returns User domain entity * @throws Error if DTO is invalid or fails domain validation */ toDomain(dto: UserDTO): User { try { return User.fromPersistence({ id: dto.id, email: dto.email, name: dto.name, companyId: dto.companyId, role: dto.role, status: dto.status, avatar: dto.avatar, createdAt: dto.createdAt, updatedAt: dto.updatedAt, }); } catch (error) { throw new Error( `Failed to map DTO to User domain entity: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Converts domain entity to DTO * * Transforms User domain entity to localStorage format. * Uses entity's toPersistence() method. * * @param entity - User domain entity * @returns User DTO for localStorage */ toDTO(entity: User): UserDTO { return entity.toPersistence(); } /** * Converts create request to domain entity * * Validates and transforms user creation request to User domain entity. * Generates new UUID and timestamps. * * @param request - User creation request data * @returns User domain entity * @throws Error if request data is invalid */ fromCreateRequest(request: unknown): User { // Validate request schema const validated = CreateUserRequestSchema.parse(request); // Generate new ID const id = crypto.randomUUID(); // Create domain entity try { return User.create({ id, email: validated.email, name: validated.name, companyId: validated.companyId, role: validated.role, status: validated.status ?? UserStatus.PENDING, avatar: validated.avatar, }); } catch (error) { throw new Error( `Failed to create User from request: ${error instanceof Error ? error.message : String(error)}` ); } } /** * Converts update request to domain entity updates * * Validates and prepares update data for domain entity. * Returns validated partial update object. * * @param request - User update request data * @returns Validated update data * @throws Error if request data is invalid */ fromUpdateRequest(request: unknown): Partial<{ email: string; name: string; role: UserRole; status: UserStatus; avatar: string | null; }> { // Validate request schema const validated = UpdateUserRequestSchema.parse(request); // Return validated partial update return { ...(validated.email !== undefined && { email: validated.email }), ...(validated.name !== undefined && { name: validated.name }), ...(validated.role !== undefined && { role: validated.role }), ...(validated.status !== undefined && { status: validated.status }), ...(validated.avatar !== undefined && { avatar: validated.avatar }), }; } /** * Normalizes avatar URL * * Handles avatar URL normalization and validation. * Returns null for empty/invalid URLs. * * @param avatar - Avatar URL or null * @returns Normalized avatar URL or null */ static normalizeAvatar(avatar: string | null | undefined): string | null { if (!avatar || avatar.trim() === '') { return null; } // Validate URL format try { new URL(avatar); return avatar.trim(); } catch { return null; } } /** * Validates email format * * Checks if email has valid format. * * @param email - Email to validate * @returns true if email is valid */ static isValidEmail(email: string): boolean { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return emailRegex.test(email); } /** * Maps array of DTOs to domain entities * * Convenience method for batch transformations. * * @param dtos - Array of User DTOs * @returns Array of User domain entities */ static toDomainArray(dtos: UserDTO[]): User[] { const mapper = new UserMapper(); return dtos.map((dto) => mapper.toDomain(dto)); } /** * Maps array of domain entities to DTOs * * Convenience method for batch transformations. * * @param entities - Array of User domain entities * @returns Array of User DTOs */ static toDTOArray(entities: User[]): UserDTO[] { const mapper = new UserMapper(); return entities.map((entity) => mapper.toDTO(entity)); } }