Skip to content

Repository Patterns

Overview

Repositories follow the Port/Adapter pattern:

  • Port (Interface): Defined in the domain layer — describes what operations are needed
  • Adapter (Implementation): Defined in the infrastructure layer — implements with Prisma, MongoDB, or InMemory

Domain Interface (Port)

typescript
// packages/cex-wallets/domain/repositories/CexAccountRepository.ts
import type { CexAccount } from '../entities/CexAccount'

export interface CexAccountRepository {
  findById(id: string): Promise<CexAccount | null>
  findAll(): Promise<CexAccount[]>
  findAllActive(): Promise<CexAccount[]>
  save(account: CexAccount): Promise<void>
  delete(id: string): Promise<void>
}

Prisma Implementation

typescript
// packages/cex-wallets/infrastructure/repositories/CexAccountPrismaRepository.ts
import type { PrismaClient } from '@sws/database'
import type { CexAccountRepository } from '../../domain/repositories/CexAccountRepository'
import { CexAccount } from '../../domain/entities/CexAccount'

export class CexAccountPrismaRepository implements CexAccountRepository {
  constructor(private readonly prisma: PrismaClient) {}

  async findById(id: string): Promise<CexAccount | null> {
    const data = await this.prisma.cexAccount.findUnique({ where: { id } })
    return data ? CexAccount.fromDto(data) : null
  }

  async findAll(): Promise<CexAccount[]> {
    const data = await this.prisma.cexAccount.findMany()
    return data.map(CexAccount.fromDto)
  }

  async findAllActive(): Promise<CexAccount[]> {
    const data = await this.prisma.cexAccount.findMany({ where: { active: true } })
    return data.map(CexAccount.fromDto)
  }

  async save(account: CexAccount): Promise<void> {
    const data = account.toDto()
    await this.prisma.cexAccount.upsert({
      where: { id: data.id },
      create: data,
      update: data
    })
  }

  async delete(id: string): Promise<void> {
    await this.prisma.cexAccount.delete({ where: { id } })
  }
}

InMemory Implementation (for tests)

typescript
// packages/rates/infrastructure/repositories/inMemory/PairInMemoryRepository.ts
import type { PairRepository } from '../../../domain/repositories/PairRepository'
import { Pair } from '../../../domain/entities/Pair'

export class PairInMemoryRepository implements PairRepository {
  private pairs: Map<string, Pair> = new Map()

  async findById(id: string): Promise<Pair | null> {
    return this.pairs.get(id) ?? null
  }

  async findAll(): Promise<Pair[]> {
    return Array.from(this.pairs.values())
  }

  async save(pair: Pair): Promise<void> {
    this.pairs.set(pair.id.toString(), pair)
  }

  async delete(id: string): Promise<void> {
    this.pairs.delete(id)
  }
}

Entity Transformation

Entities implement:

  • static fromDto(data: DatabaseModel): Entity — transforms database model to domain entity
  • toDto(): DatabaseModel — transforms domain entity to database model
typescript
export class CexAccount {
  private constructor(
    public readonly id: CexAccountId,
    public readonly cex: Cex,
    public readonly active: boolean
  ) {}

  static fromDto(dto: { id: string; cex: string; active: boolean }): CexAccount {
    return new CexAccount(
      CexAccountId.fromString(dto.id),
      Cex.fromString(dto.cex),
      dto.active
    )
  }

  toDto(): { id: string; cex: string; active: boolean } {
    return {
      id: this.id.toString(),
      cex: this.cex.toString(),
      active: this.active
    }
  }
}

Table Config Pattern

Some repositories use a separate TableConfig file with Prisma select/include configs:

typescript
// packages/cex-wallets/infrastructure/repositories/CexAccountTableConfig.ts
export const CexAccountTableConfig = {
  include: {
    cexAccountCoinWallets: true
  }
}

Factory Registration

Always register repositories in the factory with TestDependencyOverrides support:

typescript
static cexAccountRepository(overrides?: TestDependencyOverrides): CexAccountRepository {
  if (overrides?.cexAccountRepository) return overrides.cexAccountRepository
  if (!CexAccountFactory.repositoryInstance) {
    CexAccountFactory.repositoryInstance = new CexAccountPrismaRepository(prisma)
  }
  return CexAccountFactory.repositoryInstance
}