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
@@ -328,12 +328,13 @@ describe("resource router CRUD", () => {
describe("create", () => {
it("creates a resource and returns it", async () => {
const created = { ...sampleResource, id: "res_new", resourceRoles: [] };
const db = {
const db: Record<string, unknown> = {
resource: {
findFirst: vi.fn().mockResolvedValue(null),
create: vi.fn().mockResolvedValue(created),
},
auditLog: { create: vi.fn().mockResolvedValue({}) },
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
};
const caller = createManagerCaller(db);
@@ -399,13 +400,14 @@ describe("resource router CRUD", () => {
describe("update", () => {
it("updates resource fields", async () => {
const updated = { ...sampleResource, displayName: "Alice Updated" };
const db = {
const db: Record<string, unknown> = {
resource: {
findUnique: vi.fn().mockResolvedValue(sampleResource),
update: vi.fn().mockResolvedValue(updated),
},
resourceRole: { deleteMany: vi.fn().mockResolvedValue({ count: 0 }) },
auditLog: { create: vi.fn().mockResolvedValue({}) },
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
};
const caller = createManagerCaller(db);
@@ -443,11 +445,12 @@ describe("resource router CRUD", () => {
describe("deactivate", () => {
it("sets isActive to false", async () => {
const deactivated = { ...sampleResource, isActive: false };
const db = {
const db: Record<string, unknown> = {
resource: {
update: vi.fn().mockResolvedValue(deactivated),
},
auditLog: { create: vi.fn().mockResolvedValue({}) },
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
};
const caller = createManagerCaller(db);
@@ -469,12 +472,13 @@ describe("resource router CRUD", () => {
.fn()
.mockResolvedValueOnce({ ...sampleResource, id: "res_1", isActive: false })
.mockResolvedValueOnce({ ...sampleResource, id: "res_2", isActive: false });
const db = {
const auditCreate = vi.fn().mockResolvedValue({});
const db: Record<string, unknown> = {
resource: {
update,
},
$transaction: vi.fn(async (operations: Promise<unknown>[]) => Promise.all(operations)),
auditLog: { create: vi.fn().mockResolvedValue({}) },
auditLog: { create: auditCreate },
$transaction: vi.fn(async (fn: (tx: unknown) => unknown) => fn(db)),
};
const caller = createManagerCaller(db);
@@ -482,7 +486,7 @@ describe("resource router CRUD", () => {
expect(result).toEqual({ count: 2 });
expect(db.$transaction).toHaveBeenCalledTimes(1);
expect(db.auditLog.create).toHaveBeenCalledWith(
expect(auditCreate).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
entityType: "Resource",