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
@@ -21,10 +21,32 @@ export const estimateVersionWorkflowProcedures = {
let estimate;
try {
estimate = await submitEstimateVersion(
ctx.db as unknown as Parameters<typeof submitEstimateVersion>[0],
input,
);
estimate = await ctx.db.$transaction(async (tx) => {
const submitted = await submitEstimateVersion(
tx as unknown as Parameters<typeof submitEstimateVersion>[0],
input,
);
await tx.auditLog.create({
data: {
entityType: "Estimate",
entityId: submitted.id,
action: "UPDATE",
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
changes: {
after: {
id: submitted.id,
status: submitted.status,
submittedVersionId: submitted.versions.find(
(version) => version.status === "SUBMITTED",
)?.id,
},
} as Prisma.InputJsonValue,
},
});
return submitted;
});
} catch (error) {
rethrowEstimateRouterError(error, [
{
@@ -41,24 +63,6 @@ export const estimateVersionWorkflowProcedures = {
]);
}
await ctx.db.auditLog.create({
data: {
entityType: "Estimate",
entityId: estimate.id,
action: "UPDATE",
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
changes: {
after: {
id: estimate.id,
status: estimate.status,
submittedVersionId: estimate.versions.find(
(version) => version.status === "SUBMITTED",
)?.id,
},
} as Prisma.InputJsonValue,
},
});
return estimate;
}),
@@ -69,10 +73,32 @@ export const estimateVersionWorkflowProcedures = {
let estimate;
try {
estimate = await approveEstimateVersion(
ctx.db as unknown as Parameters<typeof approveEstimateVersion>[0],
input,
);
estimate = await ctx.db.$transaction(async (tx) => {
const approved = await approveEstimateVersion(
tx as unknown as Parameters<typeof approveEstimateVersion>[0],
input,
);
await tx.auditLog.create({
data: {
entityType: "Estimate",
entityId: approved.id,
action: "UPDATE",
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
changes: {
after: {
id: approved.id,
status: approved.status,
approvedVersionId: approved.versions.find(
(version) => version.status === "APPROVED",
)?.id,
},
} as Prisma.InputJsonValue,
},
});
return approved;
});
} catch (error) {
rethrowEstimateRouterError(error, [
{
@@ -89,24 +115,6 @@ export const estimateVersionWorkflowProcedures = {
]);
}
await ctx.db.auditLog.create({
data: {
entityType: "Estimate",
entityId: estimate.id,
action: "UPDATE",
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
changes: {
after: {
id: estimate.id,
status: estimate.status,
approvedVersionId: estimate.versions.find(
(version) => version.status === "APPROVED",
)?.id,
},
} as Prisma.InputJsonValue,
},
});
return estimate;
}),
@@ -117,10 +125,33 @@ export const estimateVersionWorkflowProcedures = {
let estimate;
try {
estimate = await createEstimateRevision(
ctx.db as unknown as Parameters<typeof createEstimateRevision>[0],
input,
);
estimate = await ctx.db.$transaction(async (tx) => {
const revision = await createEstimateRevision(
tx as unknown as Parameters<typeof createEstimateRevision>[0],
input,
);
await tx.auditLog.create({
data: {
entityType: "Estimate",
entityId: revision.id,
action: "UPDATE",
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
changes: {
after: {
id: revision.id,
status: revision.status,
latestVersionNumber: revision.latestVersionNumber,
workingVersionId: revision.versions.find(
(version) => version.status === "WORKING",
)?.id,
},
} as Prisma.InputJsonValue,
},
});
return revision;
});
} catch (error) {
rethrowEstimateRouterError(error, [
{
@@ -138,25 +169,6 @@ export const estimateVersionWorkflowProcedures = {
]);
}
await ctx.db.auditLog.create({
data: {
entityType: "Estimate",
entityId: estimate.id,
action: "UPDATE",
...(ctx.dbUser?.id ? { userId: ctx.dbUser.id } : {}),
changes: {
after: {
id: estimate.id,
status: estimate.status,
latestVersionNumber: estimate.latestVersionNumber,
workingVersionId: estimate.versions.find(
(version) => version.status === "WORKING",
)?.id,
},
} as Prisma.InputJsonValue,
},
});
return estimate;
}),
};