import { Injectable, OnModuleDestroy, OnModuleInit } from "@nestjs/common"; import { PrismaClient } from "@prisma/client"; import { loadEnv } from "../config/env.js"; import { logger } from "./logger.js"; /** * Prisma client as a Nest provider. The schema lives at the repo root * (../../prisma/schema.prisma); set PRISMA_SCHEMA for unusual checkouts. * * TASK-0135 (pooling + replica): this service is the PRIMARY client — all * writes, transactions, permission decisions, and the `If-Match` version CAS * go through it. It also owns the READER client (`this.reader`): the * read-replica when `DATABASE_REPLICA_URL` is set, otherwise the primary. * The hot-path read sites (block tree load, record listing) call * `this.read()` — the spec's `db.read()`/`db.write()` contract from * `@factory/database-client` (the spec's `prisma/clients.ts`; a package * because the apps compile with `rootDir: "src"`). The replica LAGS — it is * NEVER used for authorization or optimistic-concurrency version checks. */ @Injectable() export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy { /** The read-replica client (or the primary when no replica is configured). */ readonly reader: PrismaClient; /** True when a distinct replica is in use. */ readonly hasReplica: boolean; constructor() { const env = loadEnv(); super({ datasources: { db: { url: env.DATABASE_URL } }, log: process.env.NODE_ENV === "production" ? ["warn", "error"] : ["warn", "error"] }); const replicaUrl = env.DATABASE_REPLICA_URL?.trim(); if (replicaUrl) { this.reader = new PrismaClient({ datasources: { db: { url: replicaUrl } }, log: ["warn", "error"] }); this.hasReplica = true; } else { this.reader = this; this.hasReplica = false; } } /** `db.read()` — replica-routed for tree/listing/search reads ONLY. */ read(): PrismaClient { return this.reader; } /** `db.write()` — the primary (this service). Writes + version CAS. */ write(): PrismaClient { return this; } async onModuleInit(): Promise { try { await this.$connect(); if (this.hasReplica) { // Surface a broken replica URL at boot (a bad replica must not fail // lazily on the first tree load). The primary already connected. await this.reader.$connect(); logger.info("prisma connected (primary + read-replica)"); } else { logger.info("prisma connected"); } } catch (err) { // Boot without a live DB only in non-prod; surface the failure in prod. logger.warn(`prisma connect failed: ${(err as Error).message}`); if (process.env.NODE_ENV === "production") throw err; } } async onModuleDestroy(): Promise { await this.$disconnect(); if (this.hasReplica) await this.reader.$disconnect(); } }