From d6e04137c1cb9d2e0161b50ef1f839b97d613d96 Mon Sep 17 00:00:00 2001 From: Manuel Holtgrewe Date: Thu, 4 Jan 2024 10:46:46 +0100 Subject: [PATCH] feat: improved validation for writing bookmarks etc. (#365) (#367) --- backend/app/schemas/__init__.py | 1 + backend/app/schemas/acmgseqvar.py | 6 +- backend/app/schemas/bookmark.py | 22 +- backend/app/schemas/clinvarsub.py | 5 +- backend/app/schemas/common.py | 37 +++ backend/tests/api/api_v1/test_acmgseqvar.py | 13 +- backend/tests/api/api_v1/test_bookmarks.py | 140 +++++----- backend/tests/app/test_schemas.py | 258 ++++++++++++++++++ backend/tests/conftest.py | 34 +++ backend/tests/crud/test_acmgseqvar.py | 5 +- backend/tests/crud/test_bookmark.py | 5 +- frontend/src/api/__tests__/acmgseqvar.spec.ts | 19 +- .../src/lib/__tests__/genomicVars.spec.ts | 46 ++++ frontend/src/lib/genomicVars.ts | 100 +++++++ frontend/src/stores/seqVarAcmgRating.ts | 16 +- 15 files changed, 593 insertions(+), 114 deletions(-) create mode 100644 backend/app/schemas/common.py create mode 100644 backend/tests/app/test_schemas.py diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py index 12dd5c8b..9451349f 100644 --- a/backend/app/schemas/__init__.py +++ b/backend/app/schemas/__init__.py @@ -20,5 +20,6 @@ SubmittingOrgUpdate, VariantPresence, ) +from app.schemas.common import RE_HGNCID, RE_SEQVAR, RE_STRUCVAR # noqa from app.schemas.msg import Msg # noqa from app.schemas.user import UserCreate, UserRead, UserUpdate # noqa diff --git a/backend/app/schemas/acmgseqvar.py b/backend/app/schemas/acmgseqvar.py index e0d084e8..d18fcff8 100644 --- a/backend/app/schemas/acmgseqvar.py +++ b/backend/app/schemas/acmgseqvar.py @@ -3,6 +3,8 @@ from pydantic import BaseModel, ConfigDict +from app.schemas.common import SeqvarName + class Presence(str, Enum): Present = "Present" @@ -63,8 +65,8 @@ class AcmgRank(BaseModel): class AcmgSeqVar(BaseModel): user: UUID | None = None - seqvar_name: str | None = None - acmg_rank: AcmgRank | None = None + seqvar_name: SeqvarName + acmg_rank: AcmgRank class AcmgSeqVarCreate(AcmgSeqVar): diff --git a/backend/app/schemas/bookmark.py b/backend/app/schemas/bookmark.py index 9f33ff5b..84ce1cd7 100644 --- a/backend/app/schemas/bookmark.py +++ b/backend/app/schemas/bookmark.py @@ -1,7 +1,11 @@ +import re from enum import Enum from uuid import UUID -from pydantic import BaseModel, ConfigDict +from pydantic import BaseModel, ConfigDict, model_validator + +from app.schemas import common +from app.schemas.common import BookmarkableId class BookmarkTypes(Enum): @@ -12,8 +16,20 @@ class BookmarkTypes(Enum): class BookmarkBase(BaseModel): user: UUID | None = None - obj_type: BookmarkTypes | None = None - obj_id: str | None = None + obj_type: BookmarkTypes + obj_id: BookmarkableId + + @model_validator(mode="after") + def check_obj_type_id(self): + if self.obj_type == BookmarkTypes.seqvar: + assert re.match(common.RE_SEQVAR, self.obj_id), "obj_id is not a valid seqvar" + elif self.obj_type == BookmarkTypes.strucvar: + assert re.match(common.RE_STRUCVAR, self.obj_id), "obj_id is not a valid strucvar" + elif self.obj_type == BookmarkTypes.gene: + assert re.match(common.RE_HGNCID, self.obj_id), "obj_id is not a valid HGNC ID" + else: + assert False, "unknown obj_type" + return self class BookmarkCreate(BookmarkBase): diff --git a/backend/app/schemas/clinvarsub.py b/backend/app/schemas/clinvarsub.py index d4d22e04..bc6536e1 100644 --- a/backend/app/schemas/clinvarsub.py +++ b/backend/app/schemas/clinvarsub.py @@ -13,6 +13,7 @@ SubmissionThreadStatus, VariantPresence, ) +from app.schemas.common import VarName class SubmittingOrgBase(BaseModel): @@ -59,7 +60,7 @@ class SubmissionThreadCreate(SubmissionThreadBase): model_config = ConfigDict(from_attributes=True) submittingorg_id: UUID - primary_variant_desc: str + primary_variant_desc: VarName class SubmissionThreadUpdate(SubmissionThreadBase): @@ -73,7 +74,7 @@ class SubmissionThreadInDbBase(SubmissionThreadBase): created: datetime.datetime updated: datetime.datetime submittingorg_id: UUID - primary_variant_desc: str + primary_variant_desc: VarName class SubmissionThreadRead(SubmissionThreadInDbBase): diff --git a/backend/app/schemas/common.py b/backend/app/schemas/common.py new file mode 100644 index 00000000..c9453853 --- /dev/null +++ b/backend/app/schemas/common.py @@ -0,0 +1,37 @@ +"""Common schema-related code""" + +from typing import TypeAlias + +from pydantic import constr + +#: Regular expression for sequence variants. +RE_SEQVAR = r"^(grch37|grch38)-([1-9]|1[0-9]|2[12]|X|Y|MT)-(\d+)-([CGAT]+)-([CGAT]+)$" + +#: Regular expression for structurals variants. +RE_STRUCVAR = r"^(DEL|DUP)-(grch37|grch38)-([1-9]|1[0-9]|2[12]|X|Y|MT)-(\d+)-(\d+)$" + +#: Regular expression for HGNC IDs. +RE_HGNCID = r"^HGNC:(\d+)$" + +#: Type for a sequence variant name. +SeqvarName: TypeAlias = constr( # type: ignore + min_length=1, strip_whitespace=True, pattern=RE_SEQVAR +) + +#: Type for a structural variant name. +StrucvarName: TypeAlias = constr( # type: ignore + min_length=1, strip_whitespace=True, pattern=RE_STRUCVAR +) + +#: Type for either a sequence or a structural variant name. +VarName: TypeAlias = constr( # type: ignore + min_length=1, strip_whitespace=True, pattern=f"{RE_SEQVAR}|{RE_STRUCVAR}" +) + +#: Type for a HGNC ID. +HgncId: TypeAlias = constr(min_length=1, strip_whitespace=True, pattern=RE_HGNCID) # type: ignore + +#: Type for a bookmarkable object. +BookmarkableId: TypeAlias = constr( # type: ignore + min_length=1, strip_whitespace=True, pattern=f"{RE_SEQVAR}|{RE_STRUCVAR}|{RE_HGNCID}" +) diff --git a/backend/tests/api/api_v1/test_acmgseqvar.py b/backend/tests/api/api_v1/test_acmgseqvar.py index db307c4f..f6861c5b 100644 --- a/backend/tests/api/api_v1/test_acmgseqvar.py +++ b/backend/tests/api/api_v1/test_acmgseqvar.py @@ -7,7 +7,7 @@ from app.core.config import settings from app.models.user import User -from tests.conftest import UserChoice +from tests.conftest import ObjNames, UserChoice #: Shortcut for regular user. REGUL = UserChoice.REGULAR @@ -20,9 +20,9 @@ @pytest.fixture -def acmgseqvar_post_data() -> dict[str, Any]: +def acmgseqvar_post_data(obj_names: ObjNames) -> dict[str, Any]: return { - "seqvar_name": "chr0:123:A:C", + "seqvar_name": obj_names.seqvar[0], "acmg_rank": { "comment": "No comment", "criterias": [ @@ -104,6 +104,7 @@ async def test_create_acmgseqvar_invalid_data( db_session: AsyncSession, client_user: TestClient, test_user: User, + obj_names: ObjNames, ): """Test creating a acmgseqvar with invalid data.""" _ = db_session @@ -111,7 +112,7 @@ async def test_create_acmgseqvar_invalid_data( # act: response = client_user.post( f"{settings.API_V1_STR}/acmgseqvar/create", - json={"seqvar_name": "chr0:123:A:C", "acmg_rank": {"comment": "No comment"}}, + json={"seqvar_name": obj_names.seqvar[0], "acmg_rank": {"comment": "No comment"}}, ) # assert: assert response.status_code == 422 @@ -526,9 +527,9 @@ async def test_get_no_acmgseqvar( @pytest.fixture -def acmgseqvar_update_data() -> dict[str, Any]: +def acmgseqvar_update_data(obj_names: ObjNames) -> dict[str, Any]: return { - "seqvar_name": "chr0:123:A:C", + "seqvar_name": obj_names.seqvar[0], "acmg_rank": { "comment": "Update", "criterias": [ diff --git a/backend/tests/api/api_v1/test_bookmarks.py b/backend/tests/api/api_v1/test_bookmarks.py index e6a986e8..36a182a8 100644 --- a/backend/tests/api/api_v1/test_bookmarks.py +++ b/backend/tests/api/api_v1/test_bookmarks.py @@ -6,7 +6,7 @@ from app.core.config import settings from app.models.user import User -from tests.conftest import UserChoice +from tests.conftest import ObjNames, UserChoice #: Shortcut for regular user. REGUL = UserChoice.REGULAR @@ -21,53 +21,51 @@ @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(REGUL, REGUL)], indirect=True) async def test_create_bookmark( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test creating a bookmark as regular user.""" _ = db_session # act: response = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) # assert: assert response.status_code == 200 assert response.json()["obj_type"] == "gene" - assert response.json()["obj_id"] == "exampleGene" + assert response.json()["obj_id"] == obj_names.gene[0] assert response.json()["user"] == str(test_user.id) @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(SUPER, SUPER)], indirect=True) async def test_create_bookmark_superuser( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test creating a bookmark as superuser.""" _ = db_session # act: response = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) # assert: assert response.status_code == 200 assert response.json()["obj_type"] == "gene" - assert response.json()["obj_id"] == "exampleGene" + assert response.json()["obj_id"] == obj_names.gene[0] assert response.json()["user"] == str(test_user.id) @pytest.mark.anyio -async def test_create_bookmark_anon(db_session: AsyncSession, client: TestClient): +async def test_create_bookmark_anon( + db_session: AsyncSession, client: TestClient, obj_names: ObjNames +): """Test creating a bookmark as anonymous user.""" _ = db_session # act: response = client.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) # assert: assert response.status_code == 401 @@ -77,9 +75,7 @@ async def test_create_bookmark_anon(db_session: AsyncSession, client: TestClient @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(SUPER, SUPER)], indirect=True) async def test_create_bookmark_invalid_data( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test creating a bookmark with invalid data.""" _ = db_session @@ -101,9 +97,7 @@ async def test_create_bookmark_invalid_data( @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(REGUL, REGUL)], indirect=True) async def test_list_all_bookmarks( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test listing all bookmarks as regular user.""" _ = db_session @@ -112,7 +106,7 @@ async def test_list_all_bookmarks( # Create a bookmark response_create = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) response_list_all = client_user.get(f"{settings.API_V1_STR}/bookmarks/list-all/") # assert:s @@ -124,9 +118,7 @@ async def test_list_all_bookmarks( @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(SUPER, SUPER)], indirect=True) async def test_list_all_bookmarks_superuser( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test listing all bookmarks as superuser.""" _ = db_session @@ -134,14 +126,14 @@ async def test_list_all_bookmarks_superuser( # Create a bookmark response_create = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) response_list_all = client_user.get(f"{settings.API_V1_STR}/bookmarks/list-all/") # assert: assert response_create.status_code == 200 assert response_list_all.status_code == 200 assert response_list_all.json()[0]["obj_type"] == "gene" - assert response_list_all.json()[0]["obj_id"] == "exampleGene" + assert response_list_all.json()[0]["obj_id"] == obj_names.gene[0] assert response_list_all.json()[0]["user"] == str(test_user.id) @@ -181,9 +173,7 @@ async def test_list_all_no_bookmarks( @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(REGUL, REGUL)], indirect=True) async def test_get_bookmark_by_id( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test getting a bookmark by id as regular user.""" _ = db_session @@ -199,9 +189,7 @@ async def test_get_bookmark_by_id( @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(SUPER, SUPER)], indirect=True) async def test_get_bookmark_by_id_superuser( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test getting a bookmark by id as superuser.""" _ = db_session @@ -210,7 +198,7 @@ async def test_get_bookmark_by_id_superuser( # Create a bookmark response_create = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) # Get the bookmark id response_list = client_user.get(f"{settings.API_V1_STR}/bookmarks/list/") @@ -264,9 +252,7 @@ async def test_get_bookmark_by_invalid_id( @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(REGUL, REGUL)], indirect=True) async def test_delete_bookmark_by_id( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test deleting a bookmark by id as regular user.""" _ = db_session @@ -275,7 +261,7 @@ async def test_delete_bookmark_by_id( # Create a bookmark response_create = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) # Get the bookmark id response_list = client_user.get(f"{settings.API_V1_STR}/bookmarks/list/") @@ -296,9 +282,7 @@ async def test_delete_bookmark_by_id( @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(SUPER, SUPER)], indirect=True) async def test_delete_bookmark_by_id_superuser( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test deleting a bookmark by id as superuser.""" _ = db_session @@ -307,7 +291,7 @@ async def test_delete_bookmark_by_id_superuser( # Create a bookmark response_creat = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) # Get the bookmark id response_list = client_user.get(f"{settings.API_V1_STR}/bookmarks/list/") @@ -366,9 +350,7 @@ async def test_delete_bookmark_by_invalid_id( @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(REGUL, REGUL)], indirect=True) async def test_list_bookmarks( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test listing bookmarks as regular user.""" _ = db_session @@ -376,23 +358,21 @@ async def test_list_bookmarks( # Create a bookmark response_create = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) response_list = client_user.get(f"{settings.API_V1_STR}/bookmarks/list/") # assert: assert response_create.status_code == 200 assert response_list.status_code == 200 assert response_list.json()[0]["obj_type"] == "gene" - assert response_list.json()[0]["obj_id"] == "exampleGene" + assert response_list.json()[0]["obj_id"] == obj_names.gene[0] assert response_list.json()[0]["user"] == str(test_user.id) @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(SUPER, SUPER)], indirect=True) async def test_list_bookmarks_superuser( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test listing bookmarks as superuser.""" _ = db_session @@ -400,14 +380,14 @@ async def test_list_bookmarks_superuser( # Create a bookmark response_create = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) response_list = client_user.get(f"{settings.API_V1_STR}/bookmarks/list/") # assert: assert response_create.status_code == 200 assert response_list.status_code == 200 assert response_list.json()[0]["obj_type"] == "gene" - assert response_list.json()[0]["obj_id"] == "exampleGene" + assert response_list.json()[0]["obj_id"] == obj_names.gene[0] assert response_list.json()[0]["user"] == str(test_user.id) @@ -448,6 +428,7 @@ async def test_get_bookmark( db_session: AsyncSession, client_user: TestClient, test_user: User, + obj_names: ObjNames, ): """Test getting a bookmark as regular user.""" _ = db_session @@ -455,25 +436,23 @@ async def test_get_bookmark( # Create a bookmark response_create = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) response_get = client_user.get( - f"{settings.API_V1_STR}/bookmarks/get?obj_type=gene&obj_id=exampleGene" + f"{settings.API_V1_STR}/bookmarks/get?obj_type=gene&obj_id={obj_names.gene[0]}" ) # assert: assert response_create.status_code == 200 assert response_get.status_code == 200 assert response_get.json()["obj_type"] == "gene" - assert response_get.json()["obj_id"] == "exampleGene" + assert response_get.json()["obj_id"] == obj_names.gene[0] assert response_get.json()["user"] == str(test_user.id) @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(SUPER, SUPER)], indirect=True) async def test_get_bookmark_superuser( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test getting a bookmark as superuser.""" _ = db_session @@ -481,25 +460,27 @@ async def test_get_bookmark_superuser( # Create a bookmark response_create = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) response_get = client_user.get( - f"{settings.API_V1_STR}/bookmarks/get?obj_type=gene&obj_id=exampleGene" + f"{settings.API_V1_STR}/bookmarks/get?obj_type=gene&obj_id={obj_names.gene[0]}" ) # assert: assert response_create.status_code == 200 assert response_get.status_code == 200 assert response_get.json()["obj_type"] == "gene" - assert response_get.json()["obj_id"] == "exampleGene" + assert response_get.json()["obj_id"] == obj_names.gene[0] assert response_get.json()["user"] == str(test_user.id) @pytest.mark.anyio -async def test_get_bookmark_anon(db_session: AsyncSession, client: TestClient): +async def test_get_bookmark_anon(db_session: AsyncSession, client: TestClient, obj_names: ObjNames): """Test getting a bookmark as anonymous user.""" _ = db_session # act: - response = client.get(f"{settings.API_V1_STR}/bookmarks/get?obj_type=gene&obj_id=exampleGene") + response = client.get( + f"{settings.API_V1_STR}/bookmarks/get?obj_type=gene&obj_id={obj_names.gene[0]}" + ) # assert: assert response.status_code == 401 assert response.json() == {"detail": "Unauthorized"} @@ -508,14 +489,13 @@ async def test_get_bookmark_anon(db_session: AsyncSession, client: TestClient): @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(SUPER, SUPER)], indirect=True) async def test_get_no_bookmarks( - db_session: AsyncSession, - client_user: TestClient, + db_session: AsyncSession, client_user: TestClient, obj_names: ObjNames ): """Test getting a bookmark as superuser when there are no bookmarks.""" _ = db_session # act: response = client_user.get( - f"{settings.API_V1_STR}/bookmarks/get?obj_type=gene&obj_id=exampleGene" + f"{settings.API_V1_STR}/bookmarks/get?obj_type=gene&obj_id={obj_names.gene[0]}" ) # assert: assert response.status_code == 404 @@ -533,21 +513,23 @@ async def test_delete_bookmark( db_session: AsyncSession, client_user: TestClient, test_user: User, + obj_names: ObjNames, ): """Test deleting a bookmark as regular user.""" _ = db_session + _ = test_user # act: # Create a bookmark - response_create = client_user.post( + _response_create = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) response_delete = client_user.delete( - f"{settings.API_V1_STR}/bookmarks/delete?obj_type=gene&obj_id=exampleGene" + f"{settings.API_V1_STR}/bookmarks/delete?obj_type=gene&obj_id={obj_names.gene[0]}" ) # Verify that the bookmark is indeed deleted response_get = client_user.get( - f"{settings.API_V1_STR}/bookmarks/get?obj_type=gene&obj_id=exampleGene" + f"{settings.API_V1_STR}/bookmarks/get?obj_type=gene&obj_id={obj_names.gene[0]}" ) # assert: assert response_delete.status_code == 200 @@ -558,24 +540,23 @@ async def test_delete_bookmark( @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(SUPER, SUPER)], indirect=True) async def test_delete_bookmark_superuser( - db_session: AsyncSession, - client_user: TestClient, - test_user: User, + db_session: AsyncSession, client_user: TestClient, test_user: User, obj_names: ObjNames ): """Test deleting a bookmark as superuser.""" _ = db_session + _ = test_user # act: # Create a bookmark response_create = client_user.post( f"{settings.API_V1_STR}/bookmarks/create/", - json={"obj_type": "gene", "obj_id": "exampleGene"}, + json={"obj_type": "gene", "obj_id": obj_names.gene[0]}, ) response_delete = client_user.delete( - f"{settings.API_V1_STR}/bookmarks/delete?obj_type=gene&obj_id=exampleGene" + f"{settings.API_V1_STR}/bookmarks/delete?obj_type=gene&obj_id={obj_names.gene[0]}" ) # Verify that the bookmark is indeed deleted response_get = client_user.get( - f"{settings.API_V1_STR}/bookmarks/get?obj_type=gene&obj_id=exampleGene" + f"{settings.API_V1_STR}/bookmarks/get?obj_type=gene&obj_id={obj_names.gene[0]}" ) # assert: assert response_delete.status_code == 200 @@ -584,12 +565,14 @@ async def test_delete_bookmark_superuser( @pytest.mark.anyio -async def test_delete_bookmark_anon(db_session: AsyncSession, client: TestClient): +async def test_delete_bookmark_anon( + db_session: AsyncSession, client: TestClient, obj_names: ObjNames +): """Test deleting a bookmark as anonymous user.""" _ = db_session # act: response = client.delete( - f"{settings.API_V1_STR}/bookmarks/delete?obj_type=gene&obj_id=exampleGene" + f"{settings.API_V1_STR}/bookmarks/delete?obj_type=gene&obj_id={obj_names.gene[0]}" ) # assert: assert response.status_code == 401 @@ -599,14 +582,13 @@ async def test_delete_bookmark_anon(db_session: AsyncSession, client: TestClient @pytest.mark.anyio @pytest.mark.parametrize("test_user, client_user", [(SUPER, SUPER)], indirect=True) async def test_delete_no_bookmarks( - db_session: AsyncSession, - client_user: TestClient, + db_session: AsyncSession, client_user: TestClient, obj_names: ObjNames ): """Test deleting a bookmark as superuser when there are no bookmarks.""" _ = db_session # act: response = client_user.delete( - f"{settings.API_V1_STR}/bookmarks/delete?obj_type=gene&obj_id=exampleGene" + f"{settings.API_V1_STR}/bookmarks/delete?obj_type=gene&obj_id={obj_names.gene[0]}" ) # assert: assert response.status_code == 404 diff --git a/backend/tests/app/test_schemas.py b/backend/tests/app/test_schemas.py new file mode 100644 index 00000000..f8d60b75 --- /dev/null +++ b/backend/tests/app/test_schemas.py @@ -0,0 +1,258 @@ +import re + +import pydantic +import pytest + +from app.schemas import common + +GOOD_SEQVARS = [ + ("grch37", "1", 123, "A", "T"), + ("grch38", "1", 123, "A", "T"), + ("grch37", "22", 123, "A", "T"), + ("grch37", "X", 123, "A", "T"), + ("grch37", "Y", 123, "A", "T"), + ("grch37", "MT", 123, "A", "T"), + ("grch37", "1", 123, "TA", "T"), + ("grch37", "1", 123, "A", "AT"), +] + + +@pytest.mark.parametrize( + "release,chrom,pos,ref,alt", + GOOD_SEQVARS, +) +def test_re_seqvar_good( + release: str, + chrom: str, + pos: int, + ref: str, + alt: str, +): + assert re.match(common.RE_SEQVAR, f"{release}-{chrom}-{pos}-{ref}-{alt}") is not None + + +BAD_SEQVARS = [ + ("T2T", "1", 123, "A", "T"), + ("GRCh37", "1", 123, "A", "T"), + ("grch37", "23", 123, "A", "T"), + ("grch37", "0", 123, "A", "T"), + ("grch37", "1", 123, "", "T"), + ("grch37", "1", 123, "A", ""), + ("grch37", "1", 123, "A", "Tx"), +] + + +@pytest.mark.parametrize( + "release,chrom,pos,ref,alt", + GOOD_SEQVARS, +) +def test_seqvarname_good( + release: str, + chrom: str, + pos: int, + ref: str, + alt: str, +): + class Model(pydantic.BaseModel): + name: common.SeqvarName + + assert ( + Model(name=f"{release}-{chrom}-{pos}-{ref}-{alt}").name + == f"{release}-{chrom}-{pos}-{ref}-{alt}" + ) + + class Model2(pydantic.BaseModel): + name: common.VarName + + assert ( + Model2(name=f"{release}-{chrom}-{pos}-{ref}-{alt}").name + == f"{release}-{chrom}-{pos}-{ref}-{alt}" + ) + + +@pytest.mark.parametrize( + "release,chrom,pos,ref,alt", + BAD_SEQVARS, +) +def test_re_seqvar_bad( + release: str, + chrom: str, + pos: int, + ref: str, + alt: str, +): + assert re.match(common.RE_SEQVAR, f"{release}-{chrom}-{pos}-{ref}-{alt}") is None + + +@pytest.mark.parametrize( + "release,chrom,pos,ref,alt", + BAD_SEQVARS, +) +def test_seqvarname_bad( + release: str, + chrom: str, + pos: int, + ref: str, + alt: str, +): + class Model(pydantic.BaseModel): + name: common.SeqvarName + + with pytest.raises(pydantic.ValidationError): + Model(name=f"{release}-{chrom}-{pos}-{ref}-{alt}") + + class Model2(pydantic.BaseModel): + name: common.VarName + + with pytest.raises(pydantic.ValidationError): + Model2(name=f"{release}-{chrom}-{pos}-{ref}-{alt}") + + +GOOD_STRUCVARS = [ + ("DEL", "grch37", "1", 123, 456), + ("DUP", "grch37", "1", 123, 456), + ("DEL", "grch38", "1", 123, 456), + ("DEL", "grch37", "2", 123, 456), + ("DEL", "grch37", "22", 123, 456), + ("DEL", "grch37", "X", 123, 456), + ("DEL", "grch37", "Y", 123, 456), + ("DEL", "grch37", "MT", 123, 456), +] + + +@pytest.mark.parametrize( + "typ,release,chrom,start,stop", + GOOD_STRUCVARS, +) +def test_re_strucvar_good( + typ: str, + release: str, + chrom: str, + start: int, + stop: str, +): + assert re.match(common.RE_STRUCVAR, f"{typ}-{release}-{chrom}-{start}-{stop}") is not None + + +@pytest.mark.parametrize( + "typ,release,chrom,start,stop", + GOOD_STRUCVARS, +) +def test_strucvarname_good( + typ: str, + release: str, + chrom: str, + start: int, + stop: str, +): + class Model(pydantic.BaseModel): + name: common.StrucvarName + + assert ( + Model(name=f"{typ}-{release}-{chrom}-{start}-{stop}").name + == f"{typ}-{release}-{chrom}-{start}-{stop}" + ) + + class Model2(pydantic.BaseModel): + name: common.VarName + + assert ( + Model2(name=f"{typ}-{release}-{chrom}-{start}-{stop}").name + == f"{typ}-{release}-{chrom}-{start}-{stop}" + ) + + +BAD_STRUCVARS = [ + ("INV", "grch37", "1", 123, 456), + ("DEL", "GRCh37", "1", 123, 456), + ("DEL", "grch37", "23", 123, 456), + ("DEL", "grch37", "0", 123, 456), +] + + +@pytest.mark.parametrize( + "typ,release,chrom,start,stop", + BAD_STRUCVARS, +) +def test_re_strucvar_bad( + typ: str, + release: str, + chrom: str, + start: int, + stop: str, +): + assert re.match(common.RE_STRUCVAR, f"{typ}-{release}-{chrom}-{start}-{stop}") is None + + +@pytest.mark.parametrize( + "typ,release,chrom,start,stop", + BAD_STRUCVARS, +) +def test_strucvarname_bad( + typ: str, + release: str, + chrom: str, + start: int, + stop: str, +): + class Model(pydantic.BaseModel): + name: common.StrucvarName + + with pytest.raises(pydantic.ValidationError): + Model(name=f"{typ}-{release}-{chrom}-{start}-{stop}") + + class Model2(pydantic.BaseModel): + name: common.VarName + + with pytest.raises(pydantic.ValidationError): + Model2(name=f"{typ}-{release}-{chrom}-{start}-{stop}") + + +GOOD_HGNCIDS = [ + "HGNC:123", + "HGNC:345", +] + + +@pytest.mark.parametrize( + "hgnc_id", + GOOD_HGNCIDS, +) +def test_re_hgncid_good( + hgnc_id: str, +): + assert re.match(common.RE_HGNCID, hgnc_id) is not None + + +@pytest.mark.parametrize( + "hgnc_id", + GOOD_HGNCIDS, +) +def test_hgncid_good(hgnc_id: str): + class Model(pydantic.BaseModel): + name: common.HgncId + + assert Model(name=hgnc_id).name == hgnc_id + + +BAD_HGNCIDS = ["HGNC:", "123", "HGNC:123x"] + + +@pytest.mark.parametrize( + "hgnc_id", + BAD_HGNCIDS, +) +def test_re_hgncid_bad(hgnc_id: str): + assert re.match(common.RE_HGNCID, hgnc_id) is None + + +@pytest.mark.parametrize( + "hgnc_id", + BAD_HGNCIDS, +) +def test_hgncid_bad(hgnc_id: str): + class Model(pydantic.BaseModel): + name: common.HgncId + + with pytest.raises(pydantic.ValidationError): + Model(name=hgnc_id) diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index cac999c0..779b71fd 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -4,6 +4,7 @@ import os from typing import AsyncGenerator, Iterator +import pydantic import pytest from _pytest.monkeypatch import MonkeyPatch from fastapi.testclient import TestClient @@ -43,6 +44,39 @@ logging.getLogger("sqlalchemy.engine").setLevel(logging.INFO) +class ObjNames(pydantic.BaseModel): + """Namespace of valid object identifiers.""" + + #: Valid seqvar identifiers. + seqvar: list[str] + #: Valid strucvar identifiers. + strucvar: list[str] + #: Valid gene identifiers. + gene: list[str] + + +@pytest.fixture +def obj_names() -> ObjNames: + """Fixture with a namespace of valid object identifiers.""" + return ObjNames( + seqvar=[ + "grch37-1-123-A-C", + "grch37-1-123-AT-A", + "grch37-1-123-A-AT", + ], + strucvar=[ + "DEL-grch37-1-123-456", + "DUP-grch37-1-123-456", + "DEL-grch38-1-123-456", + ], + gene=[ + "HGNC:1100", + "HGNC:123", + "HGNC:456", + ], + ) + + @pytest.fixture def anyio_backend(): return "asyncio" diff --git a/backend/tests/crud/test_acmgseqvar.py b/backend/tests/crud/test_acmgseqvar.py index 4e3d5f43..dcaf2713 100644 --- a/backend/tests/crud/test_acmgseqvar.py +++ b/backend/tests/crud/test_acmgseqvar.py @@ -12,10 +12,11 @@ Presence, SeqVarCriteria, ) +from tests.conftest import ObjNames @pytest.fixture -def acmgseqvar_create() -> AcmgSeqVarCreate: +def acmgseqvar_create(obj_names: ObjNames) -> AcmgSeqVarCreate: """Create a AcmgSeqVarCreate object.""" pm4 = SeqVarCriteria( criteria=Criteria.PM4, @@ -28,7 +29,7 @@ def acmgseqvar_create() -> AcmgSeqVarCreate: ) return AcmgSeqVarCreate( user=uuid.uuid4(), - seqvar_name="chr0:123:A:C", + seqvar_name=obj_names.seqvar[0], acmg_rank=rank, ) diff --git a/backend/tests/crud/test_bookmark.py b/backend/tests/crud/test_bookmark.py index 48466581..3aa5dc32 100644 --- a/backend/tests/crud/test_bookmark.py +++ b/backend/tests/crud/test_bookmark.py @@ -5,14 +5,15 @@ from app import crud from app.schemas.bookmark import BookmarkCreate, BookmarkTypes +from tests.conftest import ObjNames @pytest.fixture -def bookmark_create() -> BookmarkCreate: +def bookmark_create(obj_names: ObjNames) -> BookmarkCreate: """Fixture for creating a bookmark.""" return BookmarkCreate( obj_type=BookmarkTypes.gene, - obj_id=str(uuid.uuid4()), + obj_id=obj_names.gene[0], user=uuid.uuid4(), ) diff --git a/frontend/src/api/__tests__/acmgseqvar.spec.ts b/frontend/src/api/__tests__/acmgseqvar.spec.ts index 355f406c..4528c487 100644 --- a/frontend/src/api/__tests__/acmgseqvar.spec.ts +++ b/frontend/src/api/__tests__/acmgseqvar.spec.ts @@ -2,11 +2,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import createFetchMock from 'vitest-fetch-mock' import { AcmgSeqVarClient } from '@/api/acmgseqvar' +import { SeqvarImpl } from '@/lib/genomicVars' import { type AcmgRatingBackend } from '@/stores/seqVarAcmgRating' const fetchMocker = createFetchMock(vi) -const mockVariantName = 'chr0:1234:A:C' +const seqVar = new SeqvarImpl('grch37', '1', 123, 'A', 'G') const mockAcmgRating: AcmgRatingBackend = { comment: 'exampleComment', criterias: [ @@ -51,7 +52,7 @@ describe.concurrent('AcmgSeqVar Client', () => { fetchMocker.mockResponse(JSON.stringify(mockAcmgRating)) const client = new AcmgSeqVarClient() - const result = await client.fetchAcmgRating(mockVariantName) + const result = await client.fetchAcmgRating(seqVar.toName()) expect(result).toEqual(mockAcmgRating) }) @@ -65,7 +66,7 @@ describe.concurrent('AcmgSeqVar Client', () => { }) const client = new AcmgSeqVarClient() - const result = await client.fetchAcmgRating(mockVariantName) + const result = await client.fetchAcmgRating(seqVar.toName()) expect(result).toEqual({ status: 500 }) }) @@ -74,7 +75,7 @@ describe.concurrent('AcmgSeqVar Client', () => { fetchMocker.mockResponse(JSON.stringify(mockAcmgRating)) const client = new AcmgSeqVarClient() - const result = await client.saveAcmgRating(mockVariantName, mockAcmgRating) + const result = await client.saveAcmgRating(seqVar.toName(), mockAcmgRating) expect(result).toEqual(mockAcmgRating) }) @@ -88,7 +89,7 @@ describe.concurrent('AcmgSeqVar Client', () => { }) const client = new AcmgSeqVarClient() - const result = await client.saveAcmgRating(mockVariantName, mockAcmgRating) + const result = await client.saveAcmgRating(seqVar.toName(), mockAcmgRating) expect(result).toEqual({ status: 500 }) }) @@ -97,7 +98,7 @@ describe.concurrent('AcmgSeqVar Client', () => { fetchMocker.mockResponse(JSON.stringify(mockAcmgRating)) const client = new AcmgSeqVarClient() - const result = await client.updateAcmgRating(mockVariantName, mockAcmgRating) + const result = await client.updateAcmgRating(seqVar.toName(), mockAcmgRating) expect(result).toEqual(mockAcmgRating) }) @@ -111,7 +112,7 @@ describe.concurrent('AcmgSeqVar Client', () => { }) const client = new AcmgSeqVarClient() - const result = await client.updateAcmgRating(mockVariantName, mockAcmgRating) + const result = await client.updateAcmgRating(seqVar.toName(), mockAcmgRating) expect(result).toEqual({ status: 500 }) }) @@ -120,7 +121,7 @@ describe.concurrent('AcmgSeqVar Client', () => { fetchMocker.mockResponse(JSON.stringify({})) const client = new AcmgSeqVarClient() - const result = await client.deleteAcmgRating(mockVariantName) + const result = await client.deleteAcmgRating(seqVar.toName()) expect(result).toEqual({}) }) @@ -134,7 +135,7 @@ describe.concurrent('AcmgSeqVar Client', () => { }) const client = new AcmgSeqVarClient() - const result = await client.deleteAcmgRating(mockVariantName) + const result = await client.deleteAcmgRating(seqVar.toName()) expect(result).toEqual({ status: 500 }) }) diff --git a/frontend/src/lib/__tests__/genomicVars.spec.ts b/frontend/src/lib/__tests__/genomicVars.spec.ts index 96058e09..7891ea46 100644 --- a/frontend/src/lib/__tests__/genomicVars.spec.ts +++ b/frontend/src/lib/__tests__/genomicVars.spec.ts @@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest' import { GENOME_BUILD_LABELS, type GenomeBuild } from '@/lib/genomeBuilds' import { + type LinearStrucvar, + LinearStrucvarImpl, REGEX_CANONICAL_SPDI, REGEX_CLINVAR_ID, REGEX_CNV_COLON, @@ -10,10 +12,14 @@ import { REGEX_DBSNP_ID, REGEX_GNOMAD_VARIANT, REGEX_RELAXED_SPDI, + type Seqvar, + SeqvarImpl, + linearStrucvarImplFromLinearStrucvar, parseCanonicalSpdiSeqvar, parseIscnCnv, parseSeparatedSeqvar, parseSeparatedStrucvar, + seqvarImplFromSeqvar, validateSeqvar } from '@/lib/genomicVars' @@ -577,3 +583,43 @@ describe.concurrent('parseIscnCnv', () => { expect(() => parseSeparatedStrucvar('arr[GRCh37] 2q12.3q13 (100_200)x1')).toThrow() }) }) + +describe.concurrent('SeqvarImpl', () => { + it('should work properly with toName()', () => { + const variant = new SeqvarImpl('grch37', '1', 100, 'AT', 'TG') + expect(variant.toName()).toEqual('grch37-1-100-AT-TG') + }) + + it('should be constructable with seqvarImplFromSeqvar()', () => { + const seqvar: Seqvar = { + genomeBuild: 'grch37', + chrom: '1', + pos: 100, + del: 'AT', + ins: 'TG', + userRepr: 'TEST' + } + const variant = seqvarImplFromSeqvar(seqvar) + expect(variant).toEqual(seqvar) + }) +}) + +describe.concurrent('LinearStrucvarImpl', () => { + it('should work properly with toName()', () => { + const variant = new LinearStrucvarImpl('DEL', 'grch37', '1', 100, 200, undefined, undefined) + expect(variant.toName()).toEqual('DEL-grch37-1-100-200') + }) + + it('should be constructable with seqvarImplFromSeqvar()', () => { + const strucvar: LinearStrucvar = { + svType: 'DEL', + genomeBuild: 'grch37', + chrom: '1', + start: 100, + stop: 200, + userRepr: 'DEL-grch37-1-100-200' + } + const variant = linearStrucvarImplFromLinearStrucvar(strucvar) + expect(variant).toEqual(strucvar) + }) +}) diff --git a/frontend/src/lib/genomicVars.ts b/frontend/src/lib/genomicVars.ts index b27ba642..17a2aa17 100644 --- a/frontend/src/lib/genomicVars.ts +++ b/frontend/src/lib/genomicVars.ts @@ -117,6 +117,54 @@ export interface Seqvar { userRepr: string } +/** + * Implementation of the `Seqvar` interface. + */ +export class SeqvarImpl implements Seqvar { + genomeBuild: GenomeBuild + chrom: string + pos: number + del: string + ins: string + userRepr: string + + constructor( + genomeBuild: GenomeBuild, + chrom: string, + pos: number, + del: string, + ins: string, + userRepr?: string + ) { + this.genomeBuild = genomeBuild + this.chrom = chrom + this.pos = pos + this.del = del + this.ins = ins + this.userRepr = + userRepr ?? `${this.genomeBuild}-${this.chrom}-${this.pos}-${this.del}-${this.ins}` + } + + /** Return the "object name" to be used in the API to the backend etc. */ + toName(): string { + return `${this.genomeBuild}-${this.chrom}-${this.pos}-${this.del}-${this.ins}` + } +} + +/** + * Construct a `SeqvarImpl` from a `Seqvar`. + */ +export function seqvarImplFromSeqvar(variant: Seqvar): SeqvarImpl { + return new SeqvarImpl( + variant.genomeBuild, + variant.chrom, + variant.pos, + variant.del, + variant.ins, + variant.userRepr + ) +} + /** Base class for exceptions when parsing variants. */ export class InvalidVariant extends Error { constructor(message: string) { @@ -295,6 +343,58 @@ export interface LinearStrucvar { userRepr: string } +/** + * Implementation of the `LinearStrucvar` interface. + */ +export class LinearStrucvarImpl implements LinearStrucvar { + svType: 'DEL' | 'DUP' + genomeBuild: GenomeBuild + chrom: string + start: number + stop: number + copyNumber?: number + userRepr: string + + constructor( + svType: 'DEL' | 'DUP', + genomeBuild: GenomeBuild, + chrom: string, + start: number, + stop: number, + copyNumber?: number, + userRepr?: string + ) { + this.svType = svType + this.genomeBuild = genomeBuild + this.chrom = chrom + this.start = start + this.stop = stop + this.copyNumber = copyNumber + this.userRepr = + userRepr ?? `${this.svType}-${this.genomeBuild}-${this.chrom}-${this.start}-${this.stop}` + } + + /** Return the "object name" to be used in the API to the backend etc. */ + toName(): string { + return `${this.svType}-${this.genomeBuild}-${this.chrom}-${this.start}-${this.stop}` + } +} + +/** + * Construct a `LinearStrucvarImpl` from a `LinearStrucvar`. + */ +export function linearStrucvarImplFromLinearStrucvar(variant: LinearStrucvar): LinearStrucvarImpl { + return new LinearStrucvarImpl( + variant.svType, + variant.genomeBuild, + variant.chrom, + variant.start, + variant.stop, + variant.copyNumber, + variant.userRepr + ) +} + /** All supported structural variant types. */ export type Strucvar = LinearStrucvar // | ... diff --git a/frontend/src/stores/seqVarAcmgRating.ts b/frontend/src/stores/seqVarAcmgRating.ts index 4d4f53b9..e0e30811 100644 --- a/frontend/src/stores/seqVarAcmgRating.ts +++ b/frontend/src/stores/seqVarAcmgRating.ts @@ -14,7 +14,7 @@ import { Presence, StateSource } from '@/lib/acmgSeqVar' -import { type Seqvar } from '@/lib/genomicVars' +import { type Seqvar, seqvarImplFromSeqvar } from '@/lib/genomicVars' import { StoreState } from '@/stores/misc' export interface AcmgRatingBackendCriteria { @@ -194,17 +194,16 @@ export const useSeqVarAcmgRatingStore = defineStore('seqVarAcmgRating', () => { if (!seqvar.value) { throw new Error('Cannot save ACMG rating without a variant.') } - const { chrom, pos, del, ins } = seqvar.value - const variantName = `${chrom}:${pos}:${del}:${ins}` + const seqvarImpl = seqvarImplFromSeqvar(seqvar.value) const acmgRating = transformAcmgRating() try { const acmgSeqVarClient = new AcmgSeqVarClient() - const acmgSeqVar = await acmgSeqVarClient.fetchAcmgRating(variantName) + const acmgSeqVar = await acmgSeqVarClient.fetchAcmgRating(seqvarImpl.toName()) if (acmgSeqVar && acmgSeqVar.detail !== 'ACMG Sequence Variant not found') { - await acmgSeqVarClient.updateAcmgRating(variantName, acmgRating) + await acmgSeqVarClient.updateAcmgRating(seqvarImpl.toName(), acmgRating) } else { - await acmgSeqVarClient.saveAcmgRating(variantName, acmgRating) + await acmgSeqVarClient.saveAcmgRating(seqvarImpl.toName(), acmgRating) } acmgRatingStatus.value = true } catch (e) { @@ -222,11 +221,10 @@ export const useSeqVarAcmgRatingStore = defineStore('seqVarAcmgRating', () => { if (!seqvar.value) { throw new Error('Cannot delete ACMG rating without a variant.') } - const { chrom, pos, del, ins } = seqvar.value - const variantName = `${chrom}:${pos}:${del}:${ins}` + const seqvarImpl = seqvarImplFromSeqvar(seqvar.value) try { const acmgSeqVarClient = new AcmgSeqVarClient() - await acmgSeqVarClient.deleteAcmgRating(variantName) + await acmgSeqVarClient.deleteAcmgRating(seqvarImpl.toName()) acmgRatingStatus.value = false } catch (e) { throw new Error(`There was an error deleting the ACMG data: ${e}`)