import { type EventEnvelope, assertValidEnvelope } from '@goodgo/contracts-events'; import { Injectable, Logger } from '@nestjs/common'; import type { Prisma } from '@prisma/client'; import { PrismaService } from '../prisma.service'; /** * Transactional outbox writer. Call inside the same Prisma transaction * as the domain change so the row commits atomically with the state * mutation it describes. The Outbox **never** publishes directly; the * relay (`OutboxRelay`) tails `event_outbox` and forwards to the EventBus. */ export interface OutboxAppendOptions { aggregateId?: string; } type EventOutboxDelegate = PrismaService['eventOutbox']; type PrismaTxLike = Pick | { eventOutbox: Pick }; @Injectable() export class OutboxService { private readonly logger = new Logger(OutboxService.name); constructor(private readonly prisma: PrismaService) {} async append( tx: PrismaTxLike | PrismaService, envelope: EventEnvelope, options: OutboxAppendOptions = {}, ): Promise { assertValidEnvelope(envelope); const client = ('eventOutbox' in tx ? tx.eventOutbox : tx) as EventOutboxDelegate; await client.create({ data: { eventId: envelope.eventId, eventType: envelope.eventType, aggregateId: options.aggregateId ?? null, envelope: envelope as unknown as Prisma.InputJsonValue, }, }); } async appendStandalone(envelope: EventEnvelope, options: OutboxAppendOptions = {}): Promise { await this.append(this.prisma, envelope, options); this.logger.warn( `appendStandalone used for ${envelope.eventType} eventId=${envelope.eventId} — prefer the transactional append()`, ); } }