/**
* メインDB書き込みの監査ログ(`db.write`。`docs/logging-design.md` §6 / 指示書AT §3-7)。
*
* ★成功・失敗の両方で1件ずつ出す★。★空パッチ(何も書かない要求)では出さない★(呼び出し側が呼ばない)。
* ★備考の中身は絶対に載せない★(`comment_changed: boolean` だけ)。
* ★エラーメッセージは載せない★ — DB のユーザー名が入る(例: 列の権限エラー 1143 の文言)。
* 載せるのは `dbErrorCode()` が返すコードだけ。
*/
import { logEvent } from "@/lib/logger";
import type { DbWriteLogEvent } from "@/lib/logger/events";
/**
* 例外から「Prisma のエラーコード」と「ドライバのエラー番号」だけを取り出す(純粋関数)。
*
* Prisma 7 + `@prisma/adapter-mariadb` では、DB のエラーは
* `PrismaClientKnownRequestError`(`code: "P2039"` 等)に包まれ、ドライバの番号は
* `meta.driverAdapterError.cause.originalCode`(例: `"1143"`)に入る。
* ★形式に合う値(`P` + 4桁 / 数字だけ)以外は拾わない★(メッセージが紛れ込まないように)。
*
* @returns 例: `"P2039:1143"` / `"P2039"` / `"1143"`。何も取れなければ `"unknown"`
*/
export function dbErrorCode(err: unknown): string {
const parts: string[] = [];
if (err !== null && typeof err === "object") {
const record = err as Record<string, unknown>;
if (typeof record.code === "string" && /^P\d{4}$/.test(record.code)) {
parts.push(record.code);
}
const driverCode = driverErrorNumber(record);
if (driverCode !== null) parts.push(driverCode);
}
return parts.length > 0 ? parts.join(":") : "unknown";
}
function driverErrorNumber(record: Record<string, unknown>): string | null {
// Prisma に包まれた形: meta.driverAdapterError.cause.originalCode
const meta = asRecord(record.meta);
const adapterError = asRecord(meta?.driverAdapterError);
const cause = asRecord(adapterError?.cause) ?? asRecord(record.cause);
const candidates: unknown[] = [cause?.originalCode, cause?.code, record.errno];
for (const candidate of candidates) {
if (typeof candidate === "number" && Number.isInteger(candidate)) return String(candidate);
if (typeof candidate === "string" && /^\d+$/.test(candidate)) return candidate;
}
return null;
}
function asRecord(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : null;
}
/** `db.write` を1件出す。成功は info、失敗は error(★`err` は渡さない★。メッセージにユーザー名が入るため) */
export function logDbWrite(payload: Omit<DbWriteLogEvent, "event">): void {
const event: DbWriteLogEvent = { event: "db.write", ...payload };
if (payload.success) {
logEvent("info", event);
} else {
logEvent("error", event);
}
}