# BFF Database Mappers ## Overview Database mappers in the BFF layer are responsible for transforming **Prisma entities** (database schema) into **domain types** (business entities). These are **infrastructure concerns** and belong in the BFF layer, not the domain layer. ## Key Principle **Domain layer should NOT know about Prisma or any ORM**. Database schema is an implementation detail of the BFF layer. ``` ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ Prisma │ ────> │ DB Mapper │ ────> │ Domain │ │ (DB Type) │ │ (BFF) │ │ Type │ └─────────────┘ └─────────────┘ └─────────────┘ ``` ## Location All DB mappers are centralized in: ``` apps/bff/src/infra/mappers/ ``` ## Naming Convention All DB mapper functions follow this pattern: ```typescript mapPrisma{EntityName}ToDomain(entity: Prisma{Entity}): Domain{Type} ``` ### Examples: - `mapPrismaUserToDomain()` - User entity → AuthenticatedUser - `mapPrismaMappingToDomain()` - IdMapping entity → UserIdMapping - `mapPrismaOrderToDomain()` - Order entity → OrderDetails (if we had one) ## When to Create a DB Mapper Create a DB mapper when you need to: 1. ✅ Convert Prisma query results to domain types 2. ✅ Transform database timestamps to ISO strings 3. ✅ Handle nullable database fields → domain optional fields 4. ✅ Map database column names to domain property names (if different) Do NOT create a DB mapper when: 1. ❌ Transforming external API responses (use domain provider mappers) 2. ❌ Transforming between domain types (handle in domain layer) 3. ❌ Applying business logic (belongs in domain) ## Example: User Mapper **File**: `apps/bff/src/infra/mappers/user.mapper.ts` ```typescript import type { User as PrismaUser } from "@prisma/client"; import type { AuthenticatedUser } from "@customer-portal/domain/auth"; /** * Maps Prisma User entity to Domain AuthenticatedUser type * * NOTE: Profile fields must be fetched from WHMCS - this only maps auth state */ export function mapPrismaUserToDomain(user: PrismaUser): AuthenticatedUser { return { id: user.id, email: user.email, role: user.role, // Transform database booleans mfaEnabled: !!user.mfaSecret, emailVerified: user.emailVerified, // Transform Date objects to ISO strings lastLoginAt: user.lastLoginAt?.toISOString(), createdAt: user.createdAt.toISOString(), updatedAt: user.updatedAt.toISOString(), // Profile fields are null - must be fetched from WHMCS firstname: null, lastname: null, fullname: null, companyname: null, phonenumber: null, language: null, currencyCode: null, address: undefined, }; } ``` ## Example: ID Mapping Mapper **File**: `apps/bff/src/infra/mappers/mapping.mapper.ts` ```typescript import type { IdMapping as PrismaIdMapping } from "@prisma/client"; import type { UserIdMapping } from "@customer-portal/domain/mappings"; /** * Maps Prisma IdMapping entity to Domain UserIdMapping type */ export function mapPrismaMappingToDomain(mapping: PrismaIdMapping): UserIdMapping { return { id: mapping.userId, // Use userId as id since it's the primary key userId: mapping.userId, whmcsClientId: mapping.whmcsClientId, sfAccountId: mapping.sfAccountId, // Keep null as-is (don't convert to undefined) createdAt: mapping.createdAt, updatedAt: mapping.updatedAt, }; } ``` ## Usage Pattern ### In Services ```typescript import { mapPrismaMappingToDomain } from "@bff/infra/mappers"; @Injectable() export class MappingsService { async findByUserId(userId: string): Promise { const dbMapping = await this.prisma.idMapping.findUnique({ where: { userId }, }); if (!dbMapping) return null; // Use centralized DB mapper return mapPrismaMappingToDomain(dbMapping); } } ``` ### Batch Mapping ```typescript const dbMappings = await this.prisma.idMapping.findMany({ where: whereClause, }); // Map each entity const mappings = dbMappings.map(mapping => mapPrismaMappingToDomain(mapping)); ``` ## Contrast with Domain Provider Mappers ### DB Mappers (BFF Infrastructure) ```typescript // Location: apps/bff/src/infra/mappers/ // Purpose: Prisma → Domain mapPrismaUserToDomain(prismaUser: PrismaUser): AuthenticatedUser { // Transforms database entity to domain type } ``` ### Provider Mappers (Domain Layer) ```typescript // Location: packages/domain/{domain}/providers/{provider}/mapper.ts // Purpose: External API Response → Domain Providers.Salesforce.transformOrder( sfOrder: SalesforceOrderRecord ): OrderDetails { // Transforms external provider data to domain type } ``` ## Key Differences | Aspect | DB Mappers | Provider Mappers | | -------------------- | ----------------------------- | ------------------------------------- | | **Location** | `apps/bff/src/infra/mappers/` | `packages/domain/{domain}/providers/` | | **Input** | Prisma entity | External API response | | **Layer** | BFF Infrastructure | Domain | | **Purpose** | Hide ORM implementation | Transform business data | | **Import in Domain** | ❌ Never | ✅ Internal use only | | **Import in BFF** | ✅ Yes | ✅ Yes | ## Best Practices ### ✅ DO - Keep mappers simple and focused - Use TypeScript types from Prisma client - Handle null/undefined consistently - Transform Date objects to ISO strings - Document special cases in comments - Centralize all DB mappers in `/infra/mappers/` ### ❌ DON'T - Add business logic to mappers - Call external services from mappers - Import DB mappers in domain layer - Create one-off mappers scattered in services - Transform external API data here (use domain provider mappers) ## Testing DB mappers are pure functions and easy to test: ```typescript describe("mapPrismaUserToDomain", () => { it("should map Prisma user to domain user", () => { const prismaUser: PrismaUser = { id: "user-123", email: "test@example.com", role: "USER", mfaSecret: "secret", emailVerified: true, lastLoginAt: new Date("2025-01-01"), createdAt: new Date("2024-01-01"), updatedAt: new Date("2024-01-01"), // ... other fields }; const result = mapPrismaUserToDomain(prismaUser); expect(result.id).toBe("user-123"); expect(result.mfaEnabled).toBe(true); expect(result.lastLoginAt).toBe("2025-01-01T00:00:00.000Z"); }); }); ``` ## Summary DB mappers are: - ✅ **Infrastructure concerns** (BFF layer) - ✅ **Pure transformation functions** (Prisma → Domain) - ✅ **Centralized** in `/infra/mappers/` - ✅ **Consistently named** `mapPrismaXxxToDomain()` - ❌ **Never used in domain layer** - ❌ **Never contain business logic** This clear separation ensures the domain layer remains pure and independent of infrastructure choices.