/**
* 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;
}