refactor: add repository + unit of work layer for database access
Add BaseRepository generic class with typed CRUD operations. Create UnitOfWork service wrapping TransactionService for atomic multi-entity operations. Add concrete repositories for SimVoiceOptions, IdMapping, and AuditLog. Migrate VoiceOptionsService, MappingsService, and AuditLogService from direct PrismaService usage to repositories.
This commit is contained in:
parent
2d076cf6d4
commit
e5fe68b25e
84
apps/bff/src/infra/database/base.repository.ts
Normal file
84
apps/bff/src/infra/database/base.repository.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import type { PrismaService } from "./prisma.service.js";
|
||||
|
||||
/**
|
||||
* Typed facade for Prisma model delegates.
|
||||
*
|
||||
* Uses `object` for findMany/count args to avoid `exactOptionalPropertyTypes`
|
||||
* incompatibility with Prisma's complex generic parameter types.
|
||||
*/
|
||||
interface PrismaDelegate<TEntity, TCreateInput, TUpdateInput, TWhereUnique, TWhere> {
|
||||
findUnique(args: { where: TWhereUnique }): Promise<TEntity | null>;
|
||||
findFirst(args: { where: TWhere }): Promise<TEntity | null>;
|
||||
findMany(args: object): Promise<TEntity[]>;
|
||||
create(args: { data: TCreateInput }): Promise<TEntity>;
|
||||
update(args: { where: TWhereUnique; data: TUpdateInput }): Promise<TEntity>;
|
||||
delete(args: { where: TWhereUnique }): Promise<TEntity>;
|
||||
count(args?: object): Promise<number>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic base repository for Prisma entities.
|
||||
*
|
||||
* Concrete repositories override `delegate` to return their Prisma model delegate.
|
||||
* The delegate is typed as `unknown` to avoid type incompatibilities with Prisma's
|
||||
* generated delegate types under `exactOptionalPropertyTypes`, then narrowed via
|
||||
* a typed facade in the private `d` getter.
|
||||
*
|
||||
* @typeParam TEntity - The entity type returned by queries
|
||||
* @typeParam TCreateInput - The input type for create operations
|
||||
* @typeParam TUpdateInput - The input type for update operations
|
||||
* @typeParam TWhereUnique - The unique identifier filter type
|
||||
* @typeParam TWhere - The general where filter type
|
||||
*/
|
||||
export abstract class BaseRepository<TEntity, TCreateInput, TUpdateInput, TWhereUnique, TWhere> {
|
||||
constructor(protected readonly prisma: PrismaService) {}
|
||||
|
||||
protected abstract get delegate(): unknown;
|
||||
|
||||
private get d(): PrismaDelegate<TEntity, TCreateInput, TUpdateInput, TWhereUnique, TWhere> {
|
||||
return this.delegate as PrismaDelegate<
|
||||
TEntity,
|
||||
TCreateInput,
|
||||
TUpdateInput,
|
||||
TWhereUnique,
|
||||
TWhere
|
||||
>;
|
||||
}
|
||||
|
||||
async findById(where: TWhereUnique): Promise<TEntity | null> {
|
||||
return this.d.findUnique({ where });
|
||||
}
|
||||
|
||||
async findOne(where: TWhere): Promise<TEntity | null> {
|
||||
return this.d.findFirst({ where });
|
||||
}
|
||||
|
||||
async findMany(
|
||||
where?: TWhere,
|
||||
options?: { skip?: number; take?: number; orderBy?: unknown }
|
||||
): Promise<TEntity[]> {
|
||||
return this.d.findMany({
|
||||
...(where !== undefined && { where }),
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
async create(data: TCreateInput): Promise<TEntity> {
|
||||
return this.d.create({ data });
|
||||
}
|
||||
|
||||
async update(where: TWhereUnique, data: TUpdateInput): Promise<TEntity> {
|
||||
return this.d.update({ where, data });
|
||||
}
|
||||
|
||||
async delete(where: TWhereUnique): Promise<TEntity> {
|
||||
return this.d.delete({ where });
|
||||
}
|
||||
|
||||
async count(where?: TWhere): Promise<number> {
|
||||
if (where !== undefined) {
|
||||
return this.d.count({ where });
|
||||
}
|
||||
return this.d.count();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { AuditLog, Prisma } from "@prisma/client";
|
||||
import { BaseRepository } from "../base.repository.js";
|
||||
import { PrismaService } from "../prisma.service.js";
|
||||
|
||||
@Injectable()
|
||||
export class AuditLogRepository extends BaseRepository<
|
||||
AuditLog,
|
||||
Prisma.AuditLogUncheckedCreateInput,
|
||||
Prisma.AuditLogUncheckedUpdateInput,
|
||||
Prisma.AuditLogWhereUniqueInput,
|
||||
Prisma.AuditLogWhereInput
|
||||
> {
|
||||
constructor(prisma: PrismaService) {
|
||||
super(prisma);
|
||||
}
|
||||
|
||||
protected get delegate() {
|
||||
return this.prisma.auditLog;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { IdMapping, Prisma } from "@prisma/client";
|
||||
import { BaseRepository } from "../base.repository.js";
|
||||
import { PrismaService } from "../prisma.service.js";
|
||||
|
||||
@Injectable()
|
||||
export class IdMappingRepository extends BaseRepository<
|
||||
IdMapping,
|
||||
Prisma.IdMappingUncheckedCreateInput,
|
||||
Prisma.IdMappingUncheckedUpdateInput,
|
||||
Prisma.IdMappingWhereUniqueInput,
|
||||
Prisma.IdMappingWhereInput
|
||||
> {
|
||||
constructor(prisma: PrismaService) {
|
||||
super(prisma);
|
||||
}
|
||||
|
||||
protected get delegate() {
|
||||
return this.prisma.idMapping;
|
||||
}
|
||||
|
||||
async exists(where: Prisma.IdMappingWhereUniqueInput): Promise<boolean> {
|
||||
const result = await this.delegate.findUnique({
|
||||
where,
|
||||
select: { userId: true },
|
||||
});
|
||||
return result !== null;
|
||||
}
|
||||
}
|
||||
3
apps/bff/src/infra/database/repositories/index.ts
Normal file
3
apps/bff/src/infra/database/repositories/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { IdMappingRepository } from "./id-mapping.repository.js";
|
||||
export { AuditLogRepository } from "./audit-log.repository.js";
|
||||
export { SimVoiceOptionsRepository } from "./sim-voice-options.repository.js";
|
||||
@ -0,0 +1,25 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { SimVoiceOptions, Prisma } from "@prisma/client";
|
||||
import { BaseRepository } from "../base.repository.js";
|
||||
import { PrismaService } from "../prisma.service.js";
|
||||
|
||||
@Injectable()
|
||||
export class SimVoiceOptionsRepository extends BaseRepository<
|
||||
SimVoiceOptions,
|
||||
Prisma.SimVoiceOptionsCreateInput,
|
||||
Prisma.SimVoiceOptionsUpdateInput,
|
||||
Prisma.SimVoiceOptionsWhereUniqueInput,
|
||||
Prisma.SimVoiceOptionsWhereInput
|
||||
> {
|
||||
constructor(prisma: PrismaService) {
|
||||
super(prisma);
|
||||
}
|
||||
|
||||
protected get delegate() {
|
||||
return this.prisma.simVoiceOptions;
|
||||
}
|
||||
|
||||
async upsert(args: Prisma.SimVoiceOptionsUpsertArgs): Promise<SimVoiceOptions> {
|
||||
return this.delegate.upsert(args);
|
||||
}
|
||||
}
|
||||
41
apps/bff/src/infra/database/unit-of-work.service.ts
Normal file
41
apps/bff/src/infra/database/unit-of-work.service.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import {
|
||||
TransactionService,
|
||||
type TransactionOptions,
|
||||
type TransactionResult,
|
||||
} from "./services/transaction.service.js";
|
||||
|
||||
/**
|
||||
* Unit of Work — coordinates multi-entity operations within a single transaction.
|
||||
*
|
||||
* Wraps the existing TransactionService to provide a cleaner API for orchestrating
|
||||
* multiple repository operations atomically.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = await this.unitOfWork.execute(async (tx) => {
|
||||
* const user = await tx.user.create({ data: userData });
|
||||
* await tx.idMapping.create({ data: { userId: user.id, ... } });
|
||||
* return user;
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
@Injectable()
|
||||
export class UnitOfWork {
|
||||
constructor(private readonly transactionService: TransactionService) {}
|
||||
|
||||
/**
|
||||
* Execute a function within a database transaction.
|
||||
* All Prisma operations on the `tx` client are atomic.
|
||||
*/
|
||||
async execute<T>(
|
||||
fn: (tx: Prisma.TransactionClient) => Promise<T>,
|
||||
options?: TransactionOptions
|
||||
): Promise<TransactionResult<T>> {
|
||||
return this.transactionService.executeTransaction(async (tx, context) => {
|
||||
context.addOperation("UnitOfWork.execute");
|
||||
return fn(tx);
|
||||
}, options);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user