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

Issue:
Transactions endpoint responses include incorrect links
When using `POST /collections` to add a new Collection, the `self` and `root` links in the response are the original links from the submitted Collection instead of the links for the newly created Collection in the API.

**Observed Response**
As an example, add the following self link to the example `joplin` Collection:

```json
{
    "rel": "self",
    "href": "/some/path/to/collection.json"
}
```

The `POST /collections` response includes that same `self` link unchanged:

```console
$ curl -X POST -H "Content-Type: application/json" -d "@tests/data/transactions_api/collection.json" -s http://localhost:8080/collections | jq ".links[] | select(.rel == \"self\")"
{
  "rel": "self",
  "href": "/some/path/to/collection.json"
}
```

The same issue exists with `PUT /collections`, `POST /collections/{collection_id}/items`, and `PUT /collections/{collection_id}/items`. 

**Expected Response**
I expected this to instead return the new `self` link:

```console
$  curl -X POST -H "Content-Type: application/json" -d "@tests/data/transactions_api/collection.json" -s http://localhost:8080/collections | jq ".links[] | select(.rel == \"self\")"
{
  "rel": "self",
  "type": "application/json",
  "href": "http://localhost:8080/collections/joplin"
}
```

**Possible Solution**

This can be easily fixed by adding the `CollectionLinks` logic to the `AsyncBaseTransactionsClient.create_collection` handler (and the equivalent handlers for the other endpoints):

```python
async def create_collection(
    self, collection: stac_types.Collection, **kwargs
) -> Optional[Union[stac_types.Collection, Response]]:
    """Create collection."""
    request = kwargs["request"]
    pool = request.app.state.writepool
    await dbfunc(pool, "create_collection", collection)
    collection["links"] = await CollectionLinks(
        collection_id=collection["id"], request=request
    ).get_links(extra_links=collection.get("links"))

    return stac_types.Collection(**collection)
```

Just a note that this would be really helpful in implementing a solution for stac-utils/pystac-client#241 because we could create a `CollectionClient` directly from the `POST` or `PUT` response rather than having to fetch the Collection again in order to get the correct links.

I can work on a PR for this in the `pgstac` backend.


Discussion / hints:

Fix patch:
diff --git a/CHANGES.md b/CHANGES.md
index 9e51040f1..6bffd8bc8 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -42,6 +42,7 @@
 * Transactions Extension update Item endpoint validates that the `{collection_id}` path parameter matches the Item `"collection"` property 
   from the request body, if present, and falls back to using the path parameter if no `"collection"` property is found in the body
   ([#425](https://github.com/stac-utils/stac-fastapi/pull/425))
+* PGStac Backend Transactions endpoints return added Item/Collection instead of Item/Collection from request ([#424](https://github.com/stac-utils/stac-fastapi/pull/424))
 
 ## [2.3.0]
 
diff --git a/stac_fastapi/pgstac/stac_fastapi/pgstac/transactions.py b/stac_fastapi/pgstac/stac_fastapi/pgstac/transactions.py
index d2b2e9810..9013a6319 100644
--- a/stac_fastapi/pgstac/stac_fastapi/pgstac/transactions.py
+++ b/stac_fastapi/pgstac/stac_fastapi/pgstac/transactions.py
@@ -12,6 +12,7 @@
     Items,
 )
 from stac_fastapi.pgstac.db import dbfunc
+from stac_fastapi.pgstac.models.links import CollectionLinks, ItemLinks
 from stac_fastapi.types import stac as stac_types
 from stac_fastapi.types.core import AsyncBaseTransactionsClient
 
@@ -37,7 +38,12 @@ async def create_item(
         request = kwargs["request"]
         pool = request.app.state.writepool
         await dbfunc(pool, "create_item", item)
-        return item
+        item["links"] = await ItemLinks(
+            collection_id=collection_id,
+            item_id=item["id"],
+            request=request,
+        ).get_links(extra_links=item.get("links"))
+        return stac_types.Item(**item)
 
     async def update_item(
         self, collection_id: str, item_id: str, item: stac_types.Item, **kwargs
@@ -59,7 +65,12 @@ async def update_item(
         request = kwargs["request"]
         pool = request.app.state.writepool
         await dbfunc(pool, "update_item", item)
-        return item
+        item["links"] = await ItemLinks(
+            collection_id=collection_id,
+            item_id=item["id"],
+            request=request,
+        ).get_links(extra_links=item.get("links"))
+        return stac_types.Item(**item)
 
     async def create_collection(
         self, collection: stac_types.Collection, **kwargs
@@ -68,7 +79,11 @@ async def create_collection(
         request = kwargs["request"]
         pool = request.app.state.writepool
         await dbfunc(pool, "create_collection", collection)
-        return collection
+        collection["links"] = await CollectionLinks(
+            collection_id=collection["id"], request=request
+        ).get_links(extra_links=collection.get("links"))
+
+        return stac_types.Collection(**collection)
 
     async def update_collection(
         self, collection: stac_types.Collection, **kwargs
@@ -77,7 +92,10 @@ async def update_collection(
         request = kwargs["request"]
         pool = request.app.state.writepool
         await dbfunc(pool, "update_collection", collection)
-        return collection
+        collection["links"] = await CollectionLinks(
+            collection_id=collection["id"], request=request
+        ).get_links(extra_links=collection.get("links"))
+        return stac_types.Collection(**collection)
 
     async def delete_item(
         self, item_id: str, **kwargs


Test patch:
diff --git a/stac_fastapi/pgstac/tests/resources/test_collection.py b/stac_fastapi/pgstac/tests/resources/test_collection.py
index bbb8c124a..6910f0fe7 100644
--- a/stac_fastapi/pgstac/tests/resources/test_collection.py
+++ b/stac_fastapi/pgstac/tests/resources/test_collection.py
@@ -20,6 +20,13 @@ async def test_create_collection(app_client, load_test_data: Callable):
     get_coll = Collection.parse_obj(resp.json())
     assert post_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"})
 
+    post_self_link = next(
+        (link for link in post_coll.links if link.rel == "self"), None
+    )
+    get_self_link = next((link for link in get_coll.links if link.rel == "self"), None)
+    assert post_self_link is not None and get_self_link is not None
+    assert post_self_link.href == get_self_link.href
+
 
 async def test_update_collection(app_client, load_test_data, load_test_collection):
     in_coll = load_test_collection
@@ -27,6 +34,7 @@ async def test_update_collection(app_client, load_test_data, load_test_collectio
 
     resp = await app_client.put("/collections", json=in_coll.dict())
     assert resp.status_code == 200
+    put_coll = Collection.parse_obj(resp.json())
 
     resp = await app_client.get(f"/collections/{in_coll.id}")
     assert resp.status_code == 200
@@ -35,6 +43,11 @@ async def test_update_collection(app_client, load_test_data, load_test_collectio
     assert in_coll.dict(exclude={"links"}) == get_coll.dict(exclude={"links"})
     assert "newkeyword" in get_coll.keywords
 
+    put_self_link = next((link for link in put_coll.links if link.rel == "self"), None)
+    get_self_link = next((link for link in get_coll.links if link.rel == "self"), None)
+    assert put_self_link is not None and get_self_link is not None
+    assert put_self_link.href == get_self_link.href
+
 
 async def test_delete_collection(
     app_client, load_test_data: Callable, load_test_collection
diff --git a/stac_fastapi/pgstac/tests/resources/test_item.py b/stac_fastapi/pgstac/tests/resources/test_item.py
index 5ce39a73c..f15f475a1 100644
--- a/stac_fastapi/pgstac/tests/resources/test_item.py
+++ b/stac_fastapi/pgstac/tests/resources/test_item.py
@@ -82,6 +82,13 @@ async def test_create_item(app_client, load_test_data: Callable, load_test_colle
     get_item = Item.parse_obj(resp.json())
     assert in_item.dict(exclude={"links"}) == get_item.dict(exclude={"links"})
 
+    post_self_link = next(
+        (link for link in post_item.links if link.rel == "self"), None
+    )
+    get_self_link = next((link for link in get_item.links if link.rel == "self"), None)
+    assert post_self_link is not None and get_self_link is not None
+    assert post_self_link.href == get_self_link.href
+
 
 async def test_create_item_mismatched_collection_id(
     app_client, load_test_data: Callable, load_test_collection
@@ -141,6 +148,7 @@ async def test_update_item(
         f"/collections/{coll.id}/items/{item.id}", content=item.json()
     )
     assert resp.status_code == 200
+    put_item = Item.parse_obj(resp.json())
 
     resp = await app_client.get(f"/collections/{coll.id}/items/{item.id}")
     assert resp.status_code == 200
@@ -149,6 +157,11 @@ async def test_update_item(
     assert item.dict(exclude={"links"}) == get_item.dict(exclude={"links"})
     assert get_item.properties.description == "Update Test"
 
+    post_self_link = next((link for link in put_item.links if link.rel == "self"), None)
+    get_self_link = next((link for link in get_item.links if link.rel == "self"), None)
+    assert post_self_link is not None and get_self_link is not None
+    assert post_self_link.href == get_self_link.href
+
 
 async def test_update_item_mismatched_collection_id(
     app_client, load_test_data: Callable, load_test_collection, load_test_item