Wrap song updates and deletes in database transactions for consistency

This commit is contained in:
Hördle Bot
2025-12-03 18:36:32 +01:00
parent 71abb7c322
commit 8ecf430bf5

View File

@@ -469,51 +469,55 @@ export async function PUT(request: Request) {
}; };
} }
// Handle SpecialSong relations separately // Execute all database write operations in a transaction to ensure consistency
if (effectiveSpecialIds !== undefined) { const updatedSong = await prisma.$transaction(async (tx) => {
// First, get current special assignments // Handle SpecialSong relations separately
const currentSpecials = await prisma.specialSong.findMany({ if (effectiveSpecialIds !== undefined) {
where: { songId: Number(id) } // First, get current special assignments (within transaction)
}); const currentSpecials = await tx.specialSong.findMany({
where: { songId: Number(id) }
const currentSpecialIds = currentSpecials.map(ss => ss.specialId);
const newSpecialIds = effectiveSpecialIds as number[];
// Delete removed specials
const toDelete = currentSpecialIds.filter(sid => !newSpecialIds.includes(sid));
if (toDelete.length > 0) {
await prisma.specialSong.deleteMany({
where: {
songId: Number(id),
specialId: { in: toDelete }
}
}); });
}
// Add new specials const currentSpecialIds = currentSpecials.map(ss => ss.specialId);
const toAdd = newSpecialIds.filter(sid => !currentSpecialIds.includes(sid)); const newSpecialIds = effectiveSpecialIds as number[];
if (toAdd.length > 0) {
await prisma.specialSong.createMany({
data: toAdd.map(specialId => ({
songId: Number(id),
specialId,
startTime: 0
}))
});
}
}
const updatedSong = await prisma.song.update({ // Delete removed specials
where: { id: Number(id) }, const toDelete = currentSpecialIds.filter(sid => !newSpecialIds.includes(sid));
data, if (toDelete.length > 0) {
include: { await tx.specialSong.deleteMany({
genres: true, where: {
specials: { songId: Number(id),
include: { specialId: { in: toDelete }
special: true }
} });
}
// Add new specials
const toAdd = newSpecialIds.filter(sid => !currentSpecialIds.includes(sid));
if (toAdd.length > 0) {
await tx.specialSong.createMany({
data: toAdd.map(specialId => ({
songId: Number(id),
specialId,
startTime: 0
}))
});
} }
} }
// Update song (this also handles genre relations via Prisma's set operation)
return await tx.song.update({
where: { id: Number(id) },
data,
include: {
genres: true,
specials: {
include: {
special: true
}
}
}
});
}); });
return NextResponse.json(updatedSong); return NextResponse.json(updatedSong);
@@ -559,7 +563,7 @@ export async function DELETE(request: Request) {
} }
} }
// Delete file // Delete files first (outside transaction, as file system operations can't be rolled back)
const filePath = path.join(process.cwd(), 'public/uploads', song.filename); const filePath = path.join(process.cwd(), 'public/uploads', song.filename);
try { try {
await unlink(filePath); await unlink(filePath);
@@ -578,9 +582,11 @@ export async function DELETE(request: Request) {
} }
} }
// Delete from database (will cascade delete related puzzles) // Delete from database in transaction (will cascade delete related puzzles, SpecialSong, etc.)
await prisma.song.delete({ await prisma.$transaction(async (tx) => {
where: { id: Number(id) }, await tx.song.delete({
where: { id: Number(id) },
});
}); });
return NextResponse.json({ success: true }); return NextResponse.json({ success: true });