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:
@@ -100,28 +100,32 @@ export async function createEstimateRecord(
|
||||
await autoFillDemandLineRates(ctx.db, input.demandLines, input.projectId);
|
||||
const enrichedInput = { ...input, demandLines: enrichedLines };
|
||||
|
||||
const estimate = await createEstimate(
|
||||
ctx.db as unknown as Parameters<typeof createEstimate>[0],
|
||||
withComputedMetrics(enrichedInput, input.baseCurrency),
|
||||
);
|
||||
const estimate = await ctx.db.$transaction(async (tx) => {
|
||||
const created = await createEstimate(
|
||||
tx as unknown as Parameters<typeof createEstimate>[0],
|
||||
withComputedMetrics(enrichedInput, input.baseCurrency),
|
||||
);
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "CREATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: estimate.id,
|
||||
name: estimate.name,
|
||||
status: estimate.status,
|
||||
projectId: estimate.projectId,
|
||||
latestVersionNumber: estimate.latestVersionNumber,
|
||||
autoFilledRateCardLines: autoFilledIndices.length,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: created.id,
|
||||
action: "CREATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: created.id,
|
||||
name: created.name,
|
||||
status: created.status,
|
||||
projectId: created.projectId,
|
||||
latestVersionNumber: created.latestVersionNumber,
|
||||
autoFilledRateCardLines: autoFilledIndices.length,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return created;
|
||||
});
|
||||
|
||||
return estimate;
|
||||
@@ -133,10 +137,30 @@ export async function cloneEstimateRecord(
|
||||
) {
|
||||
let estimate;
|
||||
try {
|
||||
estimate = await cloneEstimate(
|
||||
ctx.db as unknown as Parameters<typeof cloneEstimate>[0],
|
||||
input,
|
||||
);
|
||||
estimate = await ctx.db.$transaction(async (tx) => {
|
||||
const cloned = await cloneEstimate(
|
||||
tx as unknown as Parameters<typeof cloneEstimate>[0],
|
||||
input,
|
||||
);
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: cloned.id,
|
||||
action: "CREATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: cloned.id,
|
||||
name: cloned.name,
|
||||
clonedFrom: input.sourceEstimateId,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return cloned;
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowEstimateRouterError(error, [
|
||||
{
|
||||
@@ -146,22 +170,6 @@ export async function cloneEstimateRecord(
|
||||
]);
|
||||
}
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "CREATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: estimate.id,
|
||||
name: estimate.name,
|
||||
clonedFrom: input.sourceEstimateId,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return estimate;
|
||||
}
|
||||
|
||||
@@ -194,10 +202,35 @@ export async function updateEstimateDraftRecord(
|
||||
|
||||
let estimate;
|
||||
try {
|
||||
estimate = await updateEstimateDraft(
|
||||
ctx.db as unknown as Parameters<typeof updateEstimateDraft>[0],
|
||||
withComputedMetrics(enrichedInput, input.baseCurrency ?? "EUR"),
|
||||
);
|
||||
estimate = await ctx.db.$transaction(async (tx) => {
|
||||
const updated = await updateEstimateDraft(
|
||||
tx as unknown as Parameters<typeof updateEstimateDraft>[0],
|
||||
withComputedMetrics(enrichedInput, input.baseCurrency ?? "EUR"),
|
||||
);
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: updated.id,
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: updated.id,
|
||||
name: updated.name,
|
||||
status: updated.status,
|
||||
latestVersionNumber: updated.latestVersionNumber,
|
||||
workingVersionId: updated.versions.find(
|
||||
(version) => version.status === "WORKING",
|
||||
)?.id,
|
||||
autoFilledRateCardLines: autoFilledIndices.length,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return updated;
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowEstimateRouterError(error, [
|
||||
{
|
||||
@@ -211,27 +244,6 @@ export async function updateEstimateDraftRecord(
|
||||
]);
|
||||
}
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: estimate.id,
|
||||
name: estimate.name,
|
||||
status: estimate.status,
|
||||
latestVersionNumber: estimate.latestVersionNumber,
|
||||
workingVersionId: estimate.versions.find(
|
||||
(version) => version.status === "WORKING",
|
||||
)?.id,
|
||||
autoFilledRateCardLines: autoFilledIndices.length,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return estimate;
|
||||
}
|
||||
|
||||
@@ -241,10 +253,35 @@ export async function createEstimateExportRecord(
|
||||
) {
|
||||
let estimate;
|
||||
try {
|
||||
estimate = await createEstimateExport(
|
||||
ctx.db as unknown as Parameters<typeof createEstimateExport>[0],
|
||||
input,
|
||||
);
|
||||
estimate = await ctx.db.$transaction(async (tx) => {
|
||||
const exported = await createEstimateExport(
|
||||
tx as unknown as Parameters<typeof createEstimateExport>[0],
|
||||
input,
|
||||
);
|
||||
|
||||
const exportedVersion = input.versionId
|
||||
? exported.versions.find((version) => version.id === input.versionId)
|
||||
: exported.versions[0];
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: exported.id,
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: exported.id,
|
||||
exportFormat: input.format,
|
||||
exportCount: exportedVersion?.exports.length ?? null,
|
||||
versionId: exportedVersion?.id ?? null,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return exported;
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowEstimateRouterError(error, [
|
||||
{
|
||||
@@ -258,27 +295,6 @@ export async function createEstimateExportRecord(
|
||||
]);
|
||||
}
|
||||
|
||||
const exportedVersion = input.versionId
|
||||
? estimate.versions.find((version) => version.id === input.versionId)
|
||||
: estimate.versions[0];
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: estimate.id,
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
id: estimate.id,
|
||||
exportFormat: input.format,
|
||||
exportCount: exportedVersion?.exports.length ?? null,
|
||||
versionId: exportedVersion?.id ?? null,
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return estimate;
|
||||
}
|
||||
|
||||
@@ -288,10 +304,36 @@ export async function createEstimatePlanningHandoffRecord(
|
||||
) {
|
||||
let result;
|
||||
try {
|
||||
result = await createEstimatePlanningHandoff(
|
||||
ctx.db as unknown as Parameters<typeof createEstimatePlanningHandoff>[0],
|
||||
input,
|
||||
);
|
||||
result = await ctx.db.$transaction(async (tx) => {
|
||||
const handoff = await createEstimatePlanningHandoff(
|
||||
tx as unknown as Parameters<typeof createEstimatePlanningHandoff>[0],
|
||||
input,
|
||||
);
|
||||
|
||||
await tx.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: handoff.estimateId,
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
planningHandoff: {
|
||||
versionId: handoff.estimateVersionId,
|
||||
versionNumber: handoff.estimateVersionNumber,
|
||||
projectId: handoff.projectId,
|
||||
createdCount: handoff.createdCount,
|
||||
assignedCount: handoff.assignedCount,
|
||||
placeholderCount: handoff.placeholderCount,
|
||||
fallbackPlaceholderCount: handoff.fallbackPlaceholderCount,
|
||||
},
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
return handoff;
|
||||
});
|
||||
} catch (error) {
|
||||
rethrowEstimateRouterError(error, [
|
||||
{
|
||||
@@ -319,28 +361,6 @@ export async function createEstimatePlanningHandoffRecord(
|
||||
]);
|
||||
}
|
||||
|
||||
await ctx.db.auditLog.create({
|
||||
data: {
|
||||
entityType: "Estimate",
|
||||
entityId: result.estimateId,
|
||||
action: "UPDATE",
|
||||
...withAuditUser(ctx.dbUser?.id),
|
||||
changes: {
|
||||
after: {
|
||||
planningHandoff: {
|
||||
versionId: result.estimateVersionId,
|
||||
versionNumber: result.estimateVersionNumber,
|
||||
projectId: result.projectId,
|
||||
createdCount: result.createdCount,
|
||||
assignedCount: result.assignedCount,
|
||||
placeholderCount: result.placeholderCount,
|
||||
fallbackPlaceholderCount: result.fallbackPlaceholderCount,
|
||||
},
|
||||
},
|
||||
} as Prisma.InputJsonValue,
|
||||
},
|
||||
});
|
||||
|
||||
for (const allocation of result.allocations) {
|
||||
emitAllocationCreated({
|
||||
id: allocation.id,
|
||||
|
||||
Reference in New Issue
Block a user