Newer
Older
client_dwatch / lib / db / prisma.ts
/**
 * Prisma クライアントのシングルトン。
 *
 * ★このファイルを `lib/db/index.ts` の先頭で静的 import しないこと★
 *   一部のテスト(`lib/settings/__tests__/advice.test.ts` 等)が `@/lib/db` を import するため、
 *   静的 import にすると Prisma クライアント未生成の環境(`USE_FIXTURES=true` の
 *   通常の開発者を含む)で `npm test` が全滅する。`prismaSource` の各メソッドの中で
 *   `await import("./prisma")` して遅延読み込みすること(指示書M 段階1-4 / 3-2)。
 *
 * ★`globalThis` にキャッシュする理由★
 *   `next dev` は編集のたびにモジュールを作り直すため、素直に `new PrismaClient()` すると
 *   接続が増え続ける。後任が「普通に import すればいいのに」と直してしまわないよう、
 *   ここに理由を書き残す。
 */
import { databaseWriteCredentials } from "@/lib/config/env-check";
import type { PrismaClient } from "@/lib/generated/prisma/client";

declare global {
  var __dwatchPrisma: PrismaClient | undefined;
  /** 書き込み用アカウントの接続(`getWritePrisma()`。指示書AT §3-2) */
  var __dwatchPrismaWrite: PrismaClient | undefined;
}

/**
 * Prisma 7 は driver adapter が必須(`datasource.url` だけでは接続できない)。
 * MySQL/MariaDB 用は `@prisma/adapter-mariadb`(インストール済み。バージョンは
 * `@prisma/client` と揃える。指示書M 段階1 で判明した追加事項)。
 */
/**
 * アダプタ用の接続情報を組み立てる。
 *
 * ★パスワードに `@` が含まれるとき、`DATABASE_URL` の `%40` が dotenv 展開で壊れることがある★
 *   (`p%40ss` → `p` + 空の `%40ss`)。その場合は部品変数を優先する。
 * ★リモート DB は connectTimeout を延ばす★(既定 ~1s だと pool timeout になる)
 *
 * ★★ユーザー名に既定値を置かないこと★★ — 実在のDBユーザー名をコードに書くのは
 *   `CLAUDE.md` §9.3「認証情報を絶対にコミットしない」に反する。
 *   2026-09-10 のコミット `c832891` でテストから同じ値を除去した経緯がある。
 *   未設定なら黙って既定値で繋ぎにいかず、★何が足りないかを言って落とす★。
 */
function mariadbAdapterConfig(PrismaMariaDb: typeof import("@prisma/adapter-mariadb").PrismaMariaDb) {
  const host = process.env.DATABASE_HOST;
  const password = process.env.DATABASE_PASSWORD;
  if (host && password !== undefined) {
    const user = process.env.DATABASE_USER;
    const database = process.env.DATABASE_NAME;
    const missing = [
      user ? null : "DATABASE_USER",
      database ? null : "DATABASE_NAME",
    ].filter((name): name is string => name !== null);
    if (missing.length > 0) {
      throw new Error(
        `[db] DATABASE_HOST で接続する場合は ${missing.join(" と ")} も必要です。` +
          "環境変数(Docker の env_file・.env・.env.local など)を確認してください(実値はコードに書かないこと)。"
      );
    }
    return new PrismaMariaDb({
      host,
      port: Number(process.env.DATABASE_PORT || "3306"),
      user,
      password,
      database,
      connectTimeout: 30_000,
    });
  }
  const databaseUrl = process.env.DATABASE_URL;
  if (!databaseUrl) {
    throw new Error(
      "[db] DATABASE_URL(または DATABASE_HOST + DATABASE_PASSWORD)が設定されていません。環境変数(Docker の env_file・.env・.env.local など)を確認してください。"
    );
  }
  return new PrismaMariaDb(databaseUrl);
}

export async function getPrisma(): Promise<PrismaClient> {
  if (!globalThis.__dwatchPrisma) {
    const [{ PrismaClient: PrismaClientCtor }, { PrismaMariaDb }] = await Promise.all([
      import("@/lib/generated/prisma/client"),
      import("@prisma/adapter-mariadb"),
    ]);
    const adapter = mariadbAdapterConfig(PrismaMariaDb);
    globalThis.__dwatchPrisma = new PrismaClientCtor({ adapter });
  }
  return globalThis.__dwatchPrisma;
}

/**
 * 書き込み用の接続(指示書AT §3-2)。★メインDBへの書き込み(`lib/db/robot-error-write.ts`)だけが使う★。
 *
 * - `DATABASE_WRITE_USER` / `DATABASE_WRITE_PASSWORD` が★2つとも未設定★ → ★`getPrisma()` と同じクライアント★
 *   (参照用アカウントで書く。いまはこれで動く見込み)
 * - ★片方だけ★ → ★例外★(黙って参照用に倒さない)。メッセージに値を出さない
 * - ★2つとも設定★ → 別の接続を作る。★接続先(ホスト・ポート・DB名)は参照用と同じ DB★なので
 *   `DATABASE_HOST` / `DATABASE_PORT` / `DATABASE_NAME` を使う(★URL 形式は設けない★ —
 *   `%40` が壊れる問題(`mariadbAdapterConfig()` の注記)を持ち込まないため)
 *
 * ★`globalThis.__dwatchPrismaWrite` にキャッシュ★(`getPrisma()` と同じ理由)。
 * 判定の基準は `databaseWriteCredentials()`(`lib/config/env-check.ts`。`npm run check:env` と同じ1本)。
 */
export async function getWritePrisma(): Promise<PrismaClient> {
  const state = databaseWriteCredentials(process.env);
  if (state === "none") return getPrisma();
  if (state === "partial") {
    throw new Error(
      "[db] DATABASE_WRITE_USER と DATABASE_WRITE_PASSWORD は両方設定してください(片方だけでは書き込みません)。" +
        "値はこのエラーには出しません。"
    );
  }
  if (!globalThis.__dwatchPrismaWrite) {
    const host = process.env.DATABASE_HOST;
    const database = process.env.DATABASE_NAME;
    const missing = [
      host ? null : "DATABASE_HOST",
      database ? null : "DATABASE_NAME",
    ].filter((name): name is string => name !== null);
    if (missing.length > 0) {
      throw new Error(
        `[db] 書き込み用アカウント(DATABASE_WRITE_USER)を使う場合は ${missing.join(" と ")} も必要です。` +
          "環境変数(Docker の env_file・.env・.env.local など)を確認してください(実値はコードに書かないこと)。"
      );
    }
    const [{ PrismaClient: PrismaClientCtor }, { PrismaMariaDb }] = await Promise.all([
      import("@/lib/generated/prisma/client"),
      import("@prisma/adapter-mariadb"),
    ]);
    const adapter = new PrismaMariaDb({
      host,
      port: Number(process.env.DATABASE_PORT || "3306"),
      user: process.env.DATABASE_WRITE_USER,
      password: process.env.DATABASE_WRITE_PASSWORD,
      database,
      connectTimeout: 30_000,
    });
    globalThis.__dwatchPrismaWrite = new PrismaClientCtor({ adapter });
  }
  return globalThis.__dwatchPrismaWrite;
}