import { Injectable, OnModuleDestroy } from '@nestjs/common'; import { PrismaPg } from '@prisma/adapter-pg'; import { PrismaClient } from '@prisma/client'; import { Pool } from 'pg'; import { requestContext } from '../common/request-context'; @Injectable() export class PrismaService extends PrismaClient implements OnModuleDestroy { private readonly databasePool: Pool; private readonly databasePoolMax: number; constructor() { const processRole = process.env.CMPP_PROCESS_ROLE?.trim() || 'api'; const workerRole = processRole === 'worker'; const outboxRole = processRole === 'outbox'; const callbackRole = processRole === 'callback'; const protocolLogRole = processRole === 'protocol-log-worker'; const databaseUrl = protocolLogRole ? process.env.API_PROTOCOL_LOG_DATABASE_URL || process.env.DATABASE_URL : outboxRole ? process.env.API_OUTBOX_DATABASE_URL || process.env.DATABASE_URL : callbackRole ? process.env.API_CALLBACK_DATABASE_URL || process.env.DATABASE_URL : workerRole ? process.env.API_WORKER_DATABASE_URL || process.env.DATABASE_URL : process.env.DATABASE_URL; const configuredPoolMax = Number(protocolLogRole ? process.env.API_PROTOCOL_LOG_DB_POOL_MAX ?? 4 : outboxRole ? process.env.API_OUTBOX_DB_POOL_MAX ?? 6 : callbackRole ? process.env.API_CALLBACK_DB_POOL_MAX ?? 16 : workerRole ? process.env.API_WORKER_DB_POOL_MAX ?? 8 : process.env.API_DB_POOL_MAX ?? 32); const poolMax = Number.isInteger(configuredPoolMax) && configuredPoolMax > 0 ? configuredPoolMax : protocolLogRole ? 4 : outboxRole ? 6 : callbackRole ? 16 : workerRole ? 8 : 32; const allowDevelopmentDefault = process.env.NODE_ENV === 'development' || process.env.NODE_ENV === 'test'; if (!databaseUrl && !allowDevelopmentDefault) { throw new Error(`DATABASE_URL is required for CMPP process role ${processRole}`); } const databasePool = new Pool({ connectionString: databaseUrl ?? 'postgresql://cmpp:cmpp_password@localhost:5432/cmpp_platform?schema=public', // API capacity must be reserved independently from the heavier Worker // transactions; explicit bounds also protect PostgreSQL max_connections. max: poolMax, }); super({ adapter: new PrismaPg(databasePool, { disposeExternalPool: true }) }); this.databasePool = databasePool; this.databasePoolMax = poolMax; const operationLog = this.operationLog; Object.defineProperty(this, 'operationLog', { value: new Proxy(operationLog, { get(target, property, receiver) { if (property === 'create') { return (args: { data: Record }) => { const ipAddress = requestContext.getStore()?.ipAddress; return (target.create as (input: unknown) => unknown)({ ...args, data: { ...args.data, ipAddress: typeof args.data.ipAddress === 'string' ? args.data.ipAddress : ipAddress }, }); }; } const value = Reflect.get(target, property, receiver); return typeof value === 'function' ? value.bind(target) : value; }, }), configurable: true, }); } getPoolState() { return { max: this.databasePoolMax, total: this.databasePool.totalCount, idle: this.databasePool.idleCount, waiting: this.databasePool.waitingCount, }; } async onModuleDestroy() { await this.$disconnect(); } }