fix(decisions): wrap mutations in db.transaction() for audit atomicity

This commit is contained in:
admin-valentin 2026-07-31 10:48:37 +00:00
parent 880c2a7018
commit 3b1b14e380

View file

@ -26,49 +26,53 @@ export class DecisionsService {
} }
async create(session: AuthenticatedSession, dto: CreateDecisionDto) { async create(session: AuthenticatedSession, dto: CreateDecisionDto) {
const [row] = await db return db.transaction(async (tx) => {
.insert(decisions) const [row] = await tx
.values({ .insert(decisions)
.values({
tenantId: session.tenantId,
ownerUserId: session.userId,
context: dto.context,
options: (dto.options ?? []) as unknown[],
assumptions: (dto.assumptions ?? []) as unknown[],
evidence: (dto.evidence ?? []) as unknown[],
reviewDueAt: dto.reviewDueAt ? new Date(dto.reviewDueAt) : undefined,
})
.returning();
await this.audit.record(tx, {
tenantId: session.tenantId, tenantId: session.tenantId,
ownerUserId: session.userId, actorId: session.userId,
context: dto.context, action: 'decision.created',
options: (dto.options ?? []) as unknown[], resource: `decision:${row.id}`,
assumptions: (dto.assumptions ?? []) as unknown[], });
evidence: (dto.evidence ?? []) as unknown[], return row;
reviewDueAt: dto.reviewDueAt ? new Date(dto.reviewDueAt) : undefined,
})
.returning();
await this.audit.record(null, {
tenantId: session.tenantId,
actorId: session.userId,
action: 'decision.created',
resource: `decision:${row.id}`,
}); });
return row;
} }
async update(session: AuthenticatedSession, id: string, dto: UpdateDecisionDto) { async update(session: AuthenticatedSession, id: string, dto: UpdateDecisionDto) {
await this.getById(session, id); await this.getById(session, id);
const [row] = await db return db.transaction(async (tx) => {
.update(decisions) const [row] = await tx
.set({ .update(decisions)
...(dto.context !== undefined ? { context: dto.context } : {}), .set({
...(dto.options !== undefined ? { options: dto.options as unknown[] } : {}), ...(dto.context !== undefined ? { context: dto.context } : {}),
...(dto.assumptions !== undefined ? { assumptions: dto.assumptions as unknown[] } : {}), ...(dto.options !== undefined ? { options: dto.options as unknown[] } : {}),
...(dto.evidence !== undefined ? { evidence: dto.evidence as unknown[] } : {}), ...(dto.assumptions !== undefined ? { assumptions: dto.assumptions as unknown[] } : {}),
...(dto.selectedOption !== undefined ? { selectedOption: dto.selectedOption } : {}), ...(dto.evidence !== undefined ? { evidence: dto.evidence as unknown[] } : {}),
...(dto.outcomeReview !== undefined ? { outcomeReview: dto.outcomeReview } : {}), ...(dto.selectedOption !== undefined ? { selectedOption: dto.selectedOption } : {}),
...(dto.reviewDueAt !== undefined ? { reviewDueAt: new Date(dto.reviewDueAt) } : {}), ...(dto.outcomeReview !== undefined ? { outcomeReview: dto.outcomeReview } : {}),
updatedAt: new Date(), ...(dto.reviewDueAt !== undefined ? { reviewDueAt: new Date(dto.reviewDueAt) } : {}),
}) updatedAt: new Date(),
.where(and(eq(decisions.id, id), eq(decisions.tenantId, session.tenantId))) })
.returning(); .where(and(eq(decisions.id, id), eq(decisions.tenantId, session.tenantId)))
await this.audit.record(null, { .returning();
tenantId: session.tenantId, await this.audit.record(tx, {
actorId: session.userId, tenantId: session.tenantId,
action: dto.selectedOption !== undefined ? 'decision.option_selected' : 'decision.updated', actorId: session.userId,
resource: `decision:${id}`, action: dto.selectedOption !== undefined ? 'decision.option_selected' : 'decision.updated',
resource: `decision:${id}`,
});
return row;
}); });
return row;
} }
} }