Fix race condition in collection and favorite rom membership updates

Replace full rom_ids list replacement with atomic POST/DELETE endpoints
that add or remove individual ROMs from a collection. This prevents
concurrent rapid clicks from overwriting each other (last-write-wins).

Also fix missing session.flush() in add_rom_user() and add collection
endpoint tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Spinnich
2026-04-14 15:08:53 -04:00
parent 7724aabb95
commit 2ecefa3d3f
9 changed files with 726 additions and 47 deletions

View File

@@ -1,6 +1,6 @@
import functools
from collections.abc import Sequence
from datetime import datetime
from datetime import datetime, timezone
from typing import Any
from sqlalchemy import delete, insert, literal, or_, select, update
@@ -146,6 +146,66 @@ class DBCollectionsHandler(DBBaseHandler):
return session.scalar(query.filter_by(id=id).limit(1))
@begin_session
@with_roms
def add_roms_to_collection(
self,
id: int,
rom_ids: list[int],
query: Query = None, # type: ignore
session: Session = None, # type: ignore
) -> Collection:
if rom_ids:
valid_rom_ids = set(
session.scalars(select(Rom.id).where(Rom.id.in_(rom_ids))).all()
)
existing_ids = set(
session.scalars(
select(CollectionRom.rom_id).where(
CollectionRom.collection_id == id
)
).all()
)
new_ids = valid_rom_ids - existing_ids
if new_ids:
session.execute(
insert(CollectionRom),
[{"collection_id": id, "rom_id": rom_id} for rom_id in new_ids],
)
session.execute(
update(Collection)
.where(Collection.id == id)
.values(updated_at=datetime.now(timezone.utc))
.execution_options(synchronize_session="evaluate")
)
return session.scalar(query.filter_by(id=id).limit(1))
@begin_session
@with_roms
def remove_roms_from_collection(
self,
id: int,
rom_ids: list[int],
query: Query = None, # type: ignore
session: Session = None, # type: ignore
) -> Collection:
if rom_ids:
session.execute(
delete(CollectionRom).where(
CollectionRom.collection_id == id,
CollectionRom.rom_id.in_(rom_ids),
)
)
session.execute(
update(Collection)
.where(Collection.id == id)
.values(updated_at=datetime.now(timezone.utc))
.execution_options(synchronize_session="evaluate")
)
return session.scalar(query.filter_by(id=id).limit(1))
@begin_session
def delete_collection(
self,

View File

@@ -1066,7 +1066,9 @@ class DBRomsHandler(DBBaseHandler):
user_id: int,
session: Session = None, # type: ignore
) -> RomUser:
return session.merge(RomUser(rom_id=rom_id, user_id=user_id))
rom_user = session.merge(RomUser(rom_id=rom_id, user_id=user_id))
session.flush()
return rom_user
@begin_session
def get_rom_user(