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
@@ -135,18 +135,22 @@ export async function createRole(
requirePermission(ctx, PermissionKey.MANAGE_ROLES);
await assertRoleNameAvailable(ctx.db, input.name);
const role = await ctx.db.role.create({
data: buildRoleCreateData(input),
include: { _count: { select: { resourceRoles: true } } },
});
const role = await ctx.db.$transaction(async (tx) => {
const created = await tx.role.create({
data: buildRoleCreateData(input),
include: { _count: { select: { resourceRoles: true } } },
});
await ctx.db.auditLog.create({
data: {
entityType: "Role",
entityId: role.id,
action: "CREATE",
changes: { after: role },
},
await tx.auditLog.create({
data: {
entityType: "Role",
entityId: created.id,
action: "CREATE",
changes: { after: created },
},
});
return created;
});
emitRoleCreated({ id: role.id, name: role.name });
@@ -168,19 +172,23 @@ export async function updateRole(
await assertRoleNameAvailable(ctx.db, input.data.name, input.id);
}
const updated = await ctx.db.role.update({
where: { id: input.id },
data: buildRoleUpdateData(input.data),
include: { _count: { select: { resourceRoles: true } } },
});
const updated = await ctx.db.$transaction(async (tx) => {
const result = await tx.role.update({
where: { id: input.id },
data: buildRoleUpdateData(input.data),
include: { _count: { select: { resourceRoles: true } } },
});
await ctx.db.auditLog.create({
data: {
entityType: "Role",
entityId: input.id,
action: "UPDATE",
changes: { before: existing, after: updated },
},
await tx.auditLog.create({
data: {
entityType: "Role",
entityId: input.id,
action: "UPDATE",
changes: { before: existing, after: result },
},
});
return result;
});
emitRoleUpdated({ id: updated.id, name: updated.name });
@@ -213,15 +221,17 @@ export async function deleteRole(
});
}
await ctx.db.role.delete({ where: { id: input.id } });
await ctx.db.$transaction(async (tx) => {
await tx.role.delete({ where: { id: input.id } });
await ctx.db.auditLog.create({
data: {
entityType: "Role",
entityId: input.id,
action: "DELETE",
changes: { before: role },
},
await tx.auditLog.create({
data: {
entityType: "Role",
entityId: input.id,
action: "DELETE",
changes: { before: role },
},
});
});
emitRoleDeleted(input.id);
@@ -234,19 +244,23 @@ export async function deactivateRole(
input: z.infer<typeof RoleIdInputSchema>,
) {
requirePermission(ctx, PermissionKey.MANAGE_ROLES);
const role = await ctx.db.role.update({
where: { id: input.id },
data: { isActive: false },
include: { _count: { select: { resourceRoles: true } } },
});
const role = await ctx.db.$transaction(async (tx) => {
const result = await tx.role.update({
where: { id: input.id },
data: { isActive: false },
include: { _count: { select: { resourceRoles: true } } },
});
await ctx.db.auditLog.create({
data: {
entityType: "Role",
entityId: input.id,
action: "UPDATE",
changes: { after: { isActive: false } },
},
await tx.auditLog.create({
data: {
entityType: "Role",
entityId: input.id,
action: "UPDATE",
changes: { after: { isActive: false } },
},
});
return result;
});
emitRoleUpdated({ id: role.id, isActive: false });