barsa 7c929eb4dc Update Customer Portal Documentation and Remove Deprecated Files
- Streamlined the README.md for clarity and conciseness.
- Deleted outdated documentation files related to Freebit SIM management, SIM management API data flow, and various architectural guides to reduce clutter and improve maintainability.
- Updated the last modified date in the README to reflect the latest changes.
2025-12-23 15:43:36 +09:00

7.2 KiB

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:

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

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

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

import { mapPrismaMappingToDomain } from "@bff/infra/mappers";

@Injectable()
export class MappingsService {
  async findByUserId(userId: string): Promise<UserIdMapping | null> {
    const dbMapping = await this.prisma.idMapping.findUnique({
      where: { userId },
    });

    if (!dbMapping) return null;

    // Use centralized DB mapper
    return mapPrismaMappingToDomain(dbMapping);
  }
}

Batch Mapping

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)

// Location: apps/bff/src/infra/mappers/
// Purpose: Prisma → Domain

mapPrismaUserToDomain(prismaUser: PrismaUser): AuthenticatedUser {
  // Transforms database entity to domain type
}

Provider Mappers (Domain Layer)

// 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:

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.