fix(api): wrap audit log writes inside their parent transactions

Prevents mutations from committing without an audit trail if the
auditLog.create call fails after the main write already succeeded.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 16:40:10 +02:00
parent a01f99561d
commit 3c0179fcec
25 changed files with 758 additions and 656 deletions
@@ -221,13 +221,14 @@ describe("project router", () => {
describe("create", () => {
it("creates a project and returns its id", async () => {
const created = { ...sampleProject, id: "project_new" };
const db = {
const db: Record<string, unknown> = {
project: {
findUnique: vi.fn().mockResolvedValue(null), // no shortCode conflict
create: vi.fn().mockResolvedValue(created),
},
auditLog: { create: vi.fn().mockResolvedValue({}) },
webhook: { findMany: vi.fn().mockResolvedValue([]) },
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
};
const caller = createManagerCaller(db);
@@ -253,13 +254,14 @@ describe("project router", () => {
vi.mocked(dispatchWebhooks).mockRejectedValueOnce(new Error("webhook unavailable"));
const created = { ...sampleProject, id: "project_safe_create" };
const db = {
const db: Record<string, unknown> = {
project: {
findUnique: vi.fn().mockResolvedValue(null),
create: vi.fn().mockResolvedValue(created),
},
auditLog: { create: vi.fn().mockResolvedValue({}) },
webhook: { findMany: vi.fn().mockResolvedValue([]) },
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
};
const caller = createManagerCaller(db);
@@ -451,12 +453,13 @@ describe("project router", () => {
describe("update", () => {
it("updates project fields", async () => {
const updated = { ...sampleProject, name: "Updated Name" };
const db = {
const db: Record<string, unknown> = {
project: {
findUnique: vi.fn().mockResolvedValue(sampleProject),
update: vi.fn().mockResolvedValue(updated),
},
auditLog: { create: vi.fn().mockResolvedValue({}) },
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
};
const caller = createManagerCaller(db);
@@ -558,14 +561,12 @@ describe("project router", () => {
describe("batchUpdateStatus", () => {
it("updates multiple projects and returns count", async () => {
const db = {
const db: Record<string, unknown> = {
project: {
update: vi.fn().mockResolvedValue(sampleProject),
},
auditLog: { create: vi.fn().mockResolvedValue({}) },
$transaction: vi.fn((calls: unknown[]) =>
Promise.all((calls as Promise<unknown>[]).map(() => sampleProject)),
),
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
};
const caller = createManagerCaller(db);
@@ -575,7 +576,7 @@ describe("project router", () => {
});
expect(result.count).toBe(3);
expect(db.auditLog.create).toHaveBeenCalled();
expect((db.auditLog as Record<string, ReturnType<typeof vi.fn>>).create).toHaveBeenCalled();
});
});