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

248 lines
7.2 KiB
Markdown

# 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<UserIdMapping | null> {
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.