diff --git a/backend/src/chitai/services/dependencies.py b/backend/src/chitai/services/dependencies.py index 63e5b6f..ead76e3 100644 --- a/backend/src/chitai/services/dependencies.py +++ b/backend/src/chitai/services/dependencies.py @@ -127,6 +127,31 @@ def create_book_filter_dependencies( # Get base filters first filters = create_filter_dependencies(config, dep_defaults) + # OVERRIDE: id filter typed by the configured id type, not always `str` + # + # advanced_alchemy's `provide_id_filter` annotates `ids` as `list[str]` and + # ignores `config["id_filter"]` entirely, so `?ids=12` reaches the database as + # the string "12" and Postgres refuses `bigint = character varying`. Nothing + # called `?ids=` until the duplicates screen needed to fetch a handful of books + # by id, which is why it went unnoticed. + if id_type := config.get("id_filter"): + id_field = config.get("id_field", "id") + + def provide_typed_id_filter( + ids=Parameter(query="ids", default=None, required=False), + ) -> CollectionFilter: + return CollectionFilter(field_name=id_field, values=ids) + + # Attached as a type object rather than written as an annotation: this module + # has `from __future__ import annotations`, so a written one is stored as the + # string "Optional[list[id_type]]" and resolved against module globals, where + # a local named `id_type` does not exist. + provide_typed_id_filter.__annotations__["ids"] = Optional[list[id_type]] + + filters[dep_defaults.ID_FILTER_DEPENDENCY_KEY] = Provide( + provide_typed_id_filter, sync_to_thread=False + ) + # OVERRIDE: Custom search filter with trigram search if config.get("search"): search_fields = config.get("search") diff --git a/backend/tests/integration/test_book.py b/backend/tests/integration/test_book.py index 910bcdd..29ab956 100644 --- a/backend/tests/integration/test_book.py +++ b/backend/tests/integration/test_book.py @@ -209,6 +209,21 @@ async def test_get_book_file( assert downloaded_content == file_content +async def test_list_books_by_id(populated_authenticated_client: AsyncClient) -> None: + """ + `?ids=` has to reach the database as integers. + + advanced_alchemy's stock id filter annotates the parameter as `list[str]` whatever + the configured id type, so the ids arrived as strings and Postgres refused to + compare a bigint primary key against them. Nothing called it until a screen needed + to fetch a handful of books by id. + """ + response = await populated_authenticated_client.get("/books?ids=1&ids=2&pageSize=10") + + assert response.status_code == 200 + assert sorted(book["id"] for book in response.json()["items"]) == [1, 2] + + async def test_get_book_by_id(populated_authenticated_client: AsyncClient) -> None: """Test retrieving a specific book by ID."""