Repository: stac-utils/stac-fastapi (language: python)

Issue:
Queryables link relation and endpoint issues
- [Item Search Filter Ext] / : 'http://www.opengis.net/def/rel/ogc/1.0/queryables' (Queryables) link relation missing from both root and each collection
- [Item Search - Filter Ext] Queryables endpoints return Content-Type header 'application/json', must return 'application/schema+json'
- [Item Search - Filter Ext] Queryables endpoint json schmea'$id' value invalid, must be same as queryables url (instead it's the example value given in the Filter Extension)


Discussion / hints:

Fix patch:
diff --git a/CHANGES.md b/CHANGES.md
index e21ed5df8..413b255c1 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -4,6 +4,9 @@
 
 ### Added
 
+* Add support in pgstac backend for /queryables and /collections/{collection_id}/queryables endpoints with functions exposed in pgstac 0.6.8
+* Update pgstac requirement to 0.6.8
+
 ### Changed
 
 ### Removed
diff --git a/docker-compose.yml b/docker-compose.yml
index 6c79e5eca..8b262cc9e 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -63,7 +63,7 @@ services:
 
   database:
     container_name: stac-db
-    image: ghcr.io/stac-utils/pgstac:v0.6.6
+    image: ghcr.io/stac-utils/pgstac:v0.6.8
     environment:
       - POSTGRES_USER=username
       - POSTGRES_PASSWORD=password
diff --git a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py
index 46c4568b3..33065fcd6 100644
--- a/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py
+++ b/stac_fastapi/extensions/stac_fastapi/extensions/core/transaction.py
@@ -18,14 +18,14 @@
 class PostItem(CollectionUri):
     """Create Item."""
 
-    item: stac_types.Item = attr.ib(default=Body())
+    item: stac_types.Item = attr.ib(default=Body(None))
 
 
 @attr.s
 class PutItem(ItemUri):
     """Update Item."""
 
-    item: stac_types.Item = attr.ib(default=Body())
+    item: stac_types.Item = attr.ib(default=Body(None))
 
 
 @attr.s
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/app.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/app.py
index a715433cc..250623c0c 100644
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/app.py
+++ b/stac_fastapi/pgstac/stac_fastapi/pgstac/app.py
@@ -6,6 +6,7 @@
 from stac_fastapi.extensions.core import (
     ContextExtension,
     FieldsExtension,
+    FilterExtension,
     SortExtension,
     TokenPaginationExtension,
     TransactionExtension,
@@ -15,6 +16,7 @@
 from stac_fastapi.pgstac.core import CoreCrudClient
 from stac_fastapi.pgstac.db import close_db_connection, connect_to_db
 from stac_fastapi.pgstac.extensions import QueryExtension
+from stac_fastapi.pgstac.extensions.filter import FiltersClient
 from stac_fastapi.pgstac.transactions import BulkTransactionsClient, TransactionsClient
 from stac_fastapi.pgstac.types.search import PgstacSearch
 
@@ -30,6 +32,7 @@
     FieldsExtension(),
     TokenPaginationExtension(),
     ContextExtension(),
+    FilterExtension(client=FiltersClient()),
     BulkTransactionExtension(client=BulkTransactionsClient()),
 ]
 
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py
index 410bc63f1..005441794 100644
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py
+++ b/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/__init__.py
@@ -1,5 +1,6 @@
 """pgstac extension customisations."""
 
+from .filter import FiltersClient
 from .query import QueryExtension
 
-__all__ = ["QueryExtension"]
+__all__ = ["QueryExtension", "FiltersClient"]
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/filter.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/filter.py
new file mode 100644
index 000000000..80c448c49
--- /dev/null
+++ b/stac_fastapi/pgstac/stac_fastapi/pgstac/extensions/filter.py
@@ -0,0 +1,37 @@
+"""Get Queryables."""
+from typing import Any, Optional
+
+from buildpg import render
+from fastapi import Request
+from fastapi.responses import JSONResponse
+
+from stac_fastapi.types.core import AsyncBaseFiltersClient
+
+
+class FiltersClient(AsyncBaseFiltersClient):
+    """Defines a pattern for implementing the STAC filter extension."""
+
+    async def get_queryables(
+        self, request: Request, collection_id: Optional[str] = None, **kwargs: Any
+    ) -> JSONResponse:
+        """Get the queryables available for the given collection_id.
+
+        If collection_id is None, returns the intersection of all
+        queryables over all collections.
+        This base implementation returns a blank queryable schema. This is not allowed
+        under OGC CQL but it is allowed by the STAC API Filter Extension
+        https://github.com/radiantearth/stac-api-spec/tree/master/fragments/filter#queryables
+        """
+        pool = request.app.state.readpool
+
+        async with pool.acquire() as conn:
+            q, p = render(
+                """
+                    SELECT * FROM get_queryables(:collection::text);
+                """,
+                collection=collection_id,
+            )
+            queryables = await conn.fetchval(q, *p)
+            queryables["$id"] = str(request.url)
+            headers = {"Content-Type": "application/schema+json"}
+            return JSONResponse(queryables, headers=headers)


Test patch:
diff --git a/stac_fastapi/pgstac/tests/api/test_api.py b/stac_fastapi/pgstac/tests/api/test_api.py
index 4ace9496d..115cb5c63 100644
--- a/stac_fastapi/pgstac/tests/api/test_api.py
+++ b/stac_fastapi/pgstac/tests/api/test_api.py
@@ -391,3 +391,27 @@ async def test_search_duplicate_forward_headers(
     for feature in features:
         for link in feature["links"]:
             assert link["href"].startswith("https://test:1234/")
+
+
+@pytest.mark.asyncio
+async def test_base_queryables(load_test_data, app_client, load_test_collection):
+    resp = await app_client.get("/queryables")
+    assert resp.headers["Content-Type"] == "application/schema+json"
+    q = resp.json()
+    assert q["$id"].endswith("/queryables")
+    assert q["type"] == "object"
+    assert "properties" in q
+    assert "id" in q["properties"]
+    assert "eo:cloud_cover" in q["properties"]
+
+
+@pytest.mark.asyncio
+async def test_collection_queryables(load_test_data, app_client, load_test_collection):
+    resp = await app_client.get("/collections/test-collection/queryables")
+    assert resp.headers["Content-Type"] == "application/schema+json"
+    q = resp.json()
+    assert q["$id"].endswith("/collections/test-collection/queryables")
+    assert q["type"] == "object"
+    assert "properties" in q
+    assert "id" in q["properties"]
+    assert "eo:cloud_cover" in q["properties"]
diff --git a/stac_fastapi/pgstac/tests/conftest.py b/stac_fastapi/pgstac/tests/conftest.py
index ed3f970eb..75601daf4 100644
--- a/stac_fastapi/pgstac/tests/conftest.py
+++ b/stac_fastapi/pgstac/tests/conftest.py
@@ -17,6 +17,7 @@
 from stac_fastapi.api.app import StacApi
 from stac_fastapi.api.models import create_get_request_model, create_post_request_model
 from stac_fastapi.extensions.core import (
+    ContextExtension,
     FieldsExtension,
     FilterExtension,
     SortExtension,
@@ -28,6 +29,7 @@
 from stac_fastapi.pgstac.core import CoreCrudClient
 from stac_fastapi.pgstac.db import close_db_connection, connect_to_db
 from stac_fastapi.pgstac.extensions import QueryExtension
+from stac_fastapi.pgstac.extensions.filter import FiltersClient
 from stac_fastapi.pgstac.transactions import BulkTransactionsClient, TransactionsClient
 from stac_fastapi.pgstac.types.search import PgstacSearch
 
@@ -133,12 +135,14 @@ def api_client(request, pg):
     extensions = [
         TransactionExtension(client=TransactionsClient(), settings=settings),
         QueryExtension(),
-        FilterExtension(),
         SortExtension(),
         FieldsExtension(),
         TokenPaginationExtension(),
+        ContextExtension(),
+        FilterExtension(client=FiltersClient()),
         BulkTransactionExtension(client=BulkTransactionsClient()),
     ]
+
     post_request_model = create_post_request_model(extensions, base_model=PgstacSearch)
     api = StacApi(
         settings=api_settings,