Newer
Older
client_dwatch / lib / logger / db-write.ts
/**
 * メイン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);
  }
}