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 entitytoDto(): 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
}