From 35fc170cefc9327ef71fff53091cc8bccf1846be Mon Sep 17 00:00:00 2001 From: greebo Date: Thu, 19 Mar 2026 19:42:03 +0300 Subject: [PATCH] feat(backend): add single-record draft read endpoints add backend read endpoints for single draft records support direct fetch of individual draft entities improve draft inspection and targeted editor workflows --- backend/app/api/routes/editor.py | 82 ++++++++++++++++++++++ backend/app/repositories/scheme_groups.py | 23 ++++++ backend/app/repositories/scheme_seats.py | 23 ++++++ backend/app/repositories/scheme_sectors.py | 23 ++++++ backend/docs/api-map.md | 4 ++ backend/docs/smoke-regression.md | 14 ++++ 6 files changed, 169 insertions(+) diff --git a/backend/app/api/routes/editor.py b/backend/app/api/routes/editor.py index d8c13e7..fb45bbb 100644 --- a/backend/app/api/routes/editor.py +++ b/backend/app/api/routes/editor.py @@ -5,6 +5,7 @@ from app.repositories.audit import create_audit_event from app.repositories.scheme_groups import ( create_scheme_version_group, delete_scheme_version_group_by_record_id, + get_scheme_version_group_by_record_id, list_scheme_version_groups, update_scheme_version_group_by_record_id, ) @@ -12,12 +13,14 @@ from app.repositories.scheme_seats import ( bulk_update_scheme_version_seats_by_record_id, cascade_update_seat_group_reference, cascade_update_seat_sector_reference, + get_scheme_version_seat_by_record_id, list_scheme_version_seats, update_scheme_version_seat_by_record_id, ) from app.repositories.scheme_sectors import ( create_scheme_version_sector, delete_scheme_version_sector_by_record_id, + get_scheme_version_sector_by_record_id, list_scheme_version_sectors, update_scheme_version_sector_by_record_id, ) @@ -154,6 +157,85 @@ async def get_draft_validation( } +@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/seats/records/{{seat_record_id}}", response_model=DraftSeatItem) +async def get_draft_seat_by_record_id( + scheme_id: str, + seat_record_id: str, + role: str = Depends(require_api_key), +): + _scheme, version = await get_current_draft_context(scheme_id) + row = await get_scheme_version_seat_by_record_id( + scheme_version_id=version.scheme_version_id, + seat_record_id=seat_record_id, + ) + return DraftSeatItem( + seat_record_id=row.seat_record_id, + scheme_id=row.scheme_id, + scheme_version_id=row.scheme_version_id, + element_id=row.element_id, + seat_id=row.seat_id, + sector_id=row.sector_id, + group_id=row.group_id, + row_label=row.row_label, + seat_number=row.seat_number, + tag=row.tag, + classes_raw=row.classes_raw, + x=row.x, + y=row.y, + cx=row.cx, + cy=row.cy, + width=row.width, + height=row.height, + created_at=row.created_at.isoformat(), + ) + + +@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/sectors/records/{{sector_record_id}}", response_model=DraftSectorItem) +async def get_draft_sector_by_record_id( + scheme_id: str, + sector_record_id: str, + role: str = Depends(require_api_key), +): + _scheme, version = await get_current_draft_context(scheme_id) + row = await get_scheme_version_sector_by_record_id( + scheme_version_id=version.scheme_version_id, + sector_record_id=sector_record_id, + ) + return DraftSectorItem( + sector_record_id=row.sector_record_id, + scheme_id=row.scheme_id, + scheme_version_id=row.scheme_version_id, + element_id=row.element_id, + sector_id=row.sector_id, + name=row.name, + classes_raw=row.classes_raw, + created_at=row.created_at.isoformat(), + ) + + +@router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/groups/records/{{group_record_id}}", response_model=DraftGroupItem) +async def get_draft_group_by_record_id( + scheme_id: str, + group_record_id: str, + role: str = Depends(require_api_key), +): + _scheme, version = await get_current_draft_context(scheme_id) + row = await get_scheme_version_group_by_record_id( + scheme_version_id=version.scheme_version_id, + group_record_id=group_record_id, + ) + return DraftGroupItem( + group_record_id=row.group_record_id, + scheme_id=row.scheme_id, + scheme_version_id=row.scheme_version_id, + element_id=row.element_id, + group_id=row.group_id, + name=row.name, + classes_raw=row.classes_raw, + created_at=row.created_at.isoformat(), + ) + + @router.get(f"{settings.api_v1_prefix}/schemes/{{scheme_id}}/draft/compare-preview", response_model=StructureDiffResponse) async def get_draft_compare_preview( scheme_id: str, diff --git a/backend/app/repositories/scheme_groups.py b/backend/app/repositories/scheme_groups.py index 4ad3893..854dc85 100644 --- a/backend/app/repositories/scheme_groups.py +++ b/backend/app/repositories/scheme_groups.py @@ -166,3 +166,26 @@ async def delete_scheme_version_group_by_record_id( await session.delete(group) await session.commit() + + +async def get_scheme_version_group_by_record_id( + *, + scheme_version_id: str, + group_record_id: str, +) -> SchemeGroupRecord: + async with AsyncSessionLocal() as session: + result = await session.execute( + select(SchemeGroupRecord).where( + SchemeGroupRecord.scheme_version_id == scheme_version_id, + SchemeGroupRecord.group_record_id == group_record_id, + ) + ) + row = result.scalar_one_or_none() + + if row is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Group record not found in current draft version", + ) + + return row diff --git a/backend/app/repositories/scheme_seats.py b/backend/app/repositories/scheme_seats.py index 15acd8a..a8ab9c8 100644 --- a/backend/app/repositories/scheme_seats.py +++ b/backend/app/repositories/scheme_seats.py @@ -114,6 +114,29 @@ async def get_scheme_version_seat_by_seat_id( return row +async def get_scheme_version_seat_by_record_id( + *, + scheme_version_id: str, + seat_record_id: str, +) -> SchemeSeatRecord: + async with AsyncSessionLocal() as session: + result = await session.execute( + select(SchemeSeatRecord).where( + SchemeSeatRecord.scheme_version_id == scheme_version_id, + SchemeSeatRecord.seat_record_id == seat_record_id, + ) + ) + row = result.scalar_one_or_none() + + if row is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Seat record not found in current draft version", + ) + + return row + + async def update_scheme_version_seat_by_record_id( *, scheme_version_id: str, diff --git a/backend/app/repositories/scheme_sectors.py b/backend/app/repositories/scheme_sectors.py index fd06685..cff84db 100644 --- a/backend/app/repositories/scheme_sectors.py +++ b/backend/app/repositories/scheme_sectors.py @@ -166,3 +166,26 @@ async def delete_scheme_version_sector_by_record_id( await session.delete(sector) await session.commit() + + +async def get_scheme_version_sector_by_record_id( + *, + scheme_version_id: str, + sector_record_id: str, +) -> SchemeSectorRecord: + async with AsyncSessionLocal() as session: + result = await session.execute( + select(SchemeSectorRecord).where( + SchemeSectorRecord.scheme_version_id == scheme_version_id, + SchemeSectorRecord.sector_record_id == sector_record_id, + ) + ) + row = result.scalar_one_or_none() + + if row is None: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Sector record not found in current draft version", + ) + + return row diff --git a/backend/docs/api-map.md b/backend/docs/api-map.md index d8b424f..d4401aa 100644 --- a/backend/docs/api-map.md +++ b/backend/docs/api-map.md @@ -57,6 +57,10 @@ ## app/api/routes/editor.py - GET /api/v1/schemes/{scheme_id}/draft/structure +- GET /api/v1/schemes/{scheme_id}/draft/validation +- GET /api/v1/schemes/{scheme_id}/draft/seats/records/{seat_record_id} +- GET /api/v1/schemes/{scheme_id}/draft/sectors/records/{sector_record_id} +- GET /api/v1/schemes/{scheme_id}/draft/groups/records/{group_record_id} - GET /api/v1/schemes/{scheme_id}/draft/compare-preview - POST /api/v1/schemes/{scheme_id}/draft/sectors - POST /api/v1/schemes/{scheme_id}/draft/groups diff --git a/backend/docs/smoke-regression.md b/backend/docs/smoke-regression.md index c7d1e0d..ce7c97f 100644 --- a/backend/docs/smoke-regression.md +++ b/backend/docs/smoke-regression.md @@ -79,6 +79,7 @@ Validate: ## 6. Draft publish preview +- GET /api/v1/schemes/{scheme_id}/draft/validation -> 200 - GET /api/v1/schemes/{scheme_id}/publish/validation -> 200 - POST /api/v1/schemes/{scheme_id}/draft/pricing/snapshot -> 200 when scheme is in draft - GET /api/v1/schemes/{scheme_id}/draft/publish-preview?refresh=true -> 200 @@ -140,3 +141,16 @@ Run this checklist after: - display pipeline changes - route reorganization - startup/import/config changes + + +## 3.1. Draft editor read model + +- GET /api/v1/schemes/{scheme_id}/draft/structure -> 200 when current version is draft +- GET /api/v1/schemes/{scheme_id}/draft/seats/records/{seat_record_id} -> 200 for known seat record +- GET /api/v1/schemes/{scheme_id}/draft/sectors/records/{sector_record_id} -> 200 for known sector record +- GET /api/v1/schemes/{scheme_id}/draft/groups/records/{group_record_id} -> 200 for known group record + +Validate: +- returned record belongs to current draft scheme_version_id +- single-entity endpoints match items visible in draft structure +- missing draft record returns 404, not 500