diff --git a/datasette/stored_queries.py b/datasette/stored_queries.py index d7e1ec99..f5f977d9 100644 --- a/datasette/stored_queries.py +++ b/datasette/stored_queries.py @@ -83,7 +83,6 @@ def stored_query_page_to_dict(page: StoredQueryPage) -> dict[str, Any]: return { "queries": [stored_query_to_dict(query) for query in page.queries], "next": page.next, - "has_more": page.has_more, "limit": page.limit, } diff --git a/datasette/views/stored_queries.py b/datasette/views/stored_queries.py index bb23a6bd..3273d13c 100644 --- a/datasette/views/stored_queries.py +++ b/datasette/views/stored_queries.py @@ -120,9 +120,9 @@ class QueryListView(BaseView): if key != "_next" ] pairs.append(("_next", page.next)) - next_url = "{}?{}".format( - query_list_path, - urlencode(pairs), + next_url = self.ds.absolute_url( + request, + "{}?{}".format(request.path, urlencode(pairs)), ) current_filters = { @@ -208,7 +208,6 @@ class QueryListView(BaseView): "queries": page.queries, "next": page.next, "next_url": next_url, - "has_more": page.has_more, "limit": page.limit, "show_private_note": any(query.is_private for query in page.queries), "show_trusted_note": any(query.is_trusted for query in page.queries), diff --git a/existing-api.md b/existing-api.md index c0c8e442..55e9d6a2 100644 --- a/existing-api.md +++ b/existing-api.md @@ -1038,7 +1038,7 @@ databases (`database`/`database_color` are null, `show_database` true). 400 `"is_write must be 0 or 1"`), `source`, `owner_id`. - **Response** — 200: `{"ok": true, "database", "database_color", "queries": [...], "next", - "next_url", "has_more", "limit", "show_private_note", + "next_url", "limit", "show_private_note", "show_trusted_note", "query_list_path", "show_database", "facets": [{title, items: [{label, count, href, active}]}], "filters": {q, is_write, is_private, source, owner_id}}`. diff --git a/stable-api-recommendations.md b/stable-api-recommendations.md index ba837204..229dccd4 100644 --- a/stable-api-recommendations.md +++ b/stable-api-recommendations.md @@ -209,7 +209,13 @@ number. > values (previously silently clamped), and the `/-/allowed` and > `/-/rules` debug endpoints renamed `page`/`page_size` to > `_page`/`_size` with the same validation (400 instead of silent -> capping at 200). The `has_more`/`total` differences remain open. +> capping at 200). `has_more` has been **removed** from the query-list +> JSON — `next: null` is the single end-of-results signal everywhere, +> keeping default response keys minimal (`total` remains a debug-endpoint +> nicety). Fixing this also uncovered and fixed a bug where the query +> list's JSON `next_url` pointed at the HTML page (it dropped the `.json` +> extension) and was relative where the table `next_url` is absolute. +> §3 is now fully resolved. | Endpoint | Mechanism | Token | Extras | |---|---|---|---| diff --git a/tests/test_queries.py b/tests/test_queries.py index 699743c5..c25ec358 100644 --- a/tests/test_queries.py +++ b/tests/test_queries.py @@ -879,7 +879,7 @@ async def test_query_list_html_defaults_to_twenty_and_shows_pagination(): assert response.text.count('aria-label="Query pagination"') == 1 assert "Demo query 20" in response.text assert "Demo query 21" not in response.text - assert 'href="/data/-/queries?_next=' in response.text + assert 'href="http://localhost/data/-/queries?_next=' in response.text assert len(json_response.json()["queries"]) == 25 @@ -3827,3 +3827,27 @@ async def test_stored_query_json_uses_parameters_not_params(): query = [q for q in listing["queries"] if q["name"] == "with_params"][0] assert query["parameters"] == ["name", "age"] assert "params" not in query + + +@pytest.mark.asyncio +async def test_query_list_json_signals_pagination_via_next_only(): + ds = Datasette(memory=True) + ds.add_memory_database("query_list_next_only", name="data") + await ds.invoke_startup() + for i in range(3): + await ds.add_query( + "data", + name="q{}".format(i), + sql="select {}".format(i), + ) + first = (await ds.client.get("/data/-/queries.json?_size=2")).json() + assert "has_more" not in first + assert first["next"] is not None + assert first["next_url"] is not None + # The internal test client cannot follow absolute URLs + next_path = first["next_url"].replace("http://localhost", "") + assert next_path.startswith("/data/-/queries.json?") + last = (await ds.client.get(next_path)).json() + assert "has_more" not in last + assert last["next"] is None + assert last["next_url"] is None