From d0608282569a69adc9be3938ad891cf6c24eaef8 Mon Sep 17 00:00:00 2001 From: greebo Date: Thu, 19 Mar 2026 19:29:00 +0300 Subject: [PATCH] feat(backend): prevent duplicate draft sector and group bindings reject duplicate sector and group bindings within draft mutations harden draft structure consistency at the backend layer prevent conflicting bindings before they reach publish flow --- backend/app/api/routes/editor.py | 14 +++++ backend/app/services/editor_validation.py | 72 +++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/backend/app/api/routes/editor.py b/backend/app/api/routes/editor.py index 5e09ada..6d8932c 100644 --- a/backend/app/api/routes/editor.py +++ b/backend/app/api/routes/editor.py @@ -49,6 +49,8 @@ from app.services.draft_guard import get_current_draft_context from app.services.editor_validation import ( validate_bulk_seat_patch_references, validate_bulk_seat_patch_uniqueness, + validate_create_group_uniqueness, + validate_create_sector_uniqueness, validate_group_patch_uniqueness, validate_sector_patch_uniqueness, validate_single_seat_patch_references, @@ -162,6 +164,12 @@ async def create_draft_sector( expected_scheme_version_id=expected_scheme_version_id, ) + await validate_create_sector_uniqueness( + scheme_version_id=version.scheme_version_id, + sector_id=payload.sector_id, + element_id=payload.element_id, + ) + row = await create_scheme_version_sector( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, @@ -205,6 +213,12 @@ async def create_draft_group( expected_scheme_version_id=expected_scheme_version_id, ) + await validate_create_group_uniqueness( + scheme_version_id=version.scheme_version_id, + group_id=payload.group_id, + element_id=payload.element_id, + ) + row = await create_scheme_version_group( scheme_id=scheme.scheme_id, scheme_version_id=version.scheme_version_id, diff --git a/backend/app/services/editor_validation.py b/backend/app/services/editor_validation.py index 0469853..eaccb4e 100644 --- a/backend/app/services/editor_validation.py +++ b/backend/app/services/editor_validation.py @@ -147,6 +147,78 @@ async def validate_group_patch_uniqueness( ) +async def validate_create_sector_uniqueness( + *, + scheme_version_id: str, + sector_id: str, + element_id: str | None, +) -> None: + rows = await list_scheme_version_sectors(scheme_version_id) + + for row in rows: + if row.sector_id == sector_id: + _raise_uniqueness_error( + f"Sector id already exists in current draft version: {sector_id}", + { + "code": "duplicate_sector_id", + "message": "Sector id already exists in current draft version", + "sector_id": sector_id, + "conflict_sector_record_id": row.sector_record_id, + }, + ) + + if element_id is None: + return + + for row in rows: + if row.element_id == element_id: + _raise_uniqueness_error( + f"Sector element binding already exists in current draft version: {element_id}", + { + "code": "duplicate_sector_element_id", + "message": "Sector element binding already exists in current draft version", + "element_id": element_id, + "conflict_sector_record_id": row.sector_record_id, + }, + ) + + +async def validate_create_group_uniqueness( + *, + scheme_version_id: str, + group_id: str, + element_id: str | None, +) -> None: + rows = await list_scheme_version_groups(scheme_version_id) + + for row in rows: + if row.group_id == group_id: + _raise_uniqueness_error( + f"Group id already exists in current draft version: {group_id}", + { + "code": "duplicate_group_id", + "message": "Group id already exists in current draft version", + "group_id": group_id, + "conflict_group_record_id": row.group_record_id, + }, + ) + + if element_id is None: + return + + for row in rows: + if row.element_id == element_id: + _raise_uniqueness_error( + f"Group element binding already exists in current draft version: {element_id}", + { + "code": "duplicate_group_element_id", + "message": "Group element binding already exists in current draft version", + "element_id": element_id, + "conflict_group_record_id": row.group_record_id, + }, + ) + + async def validate_single_seat_patch_references( *, scheme_version_id: str,