From cc16ffd8c9bf8ee27f25b162096b61af3a6e1802 Mon Sep 17 00:00:00 2001 From: German Bernadskiy Date: Mon, 8 Apr 2024 15:55:32 +1000 Subject: [PATCH 1/3] added test case --- tests/test_api/test_api_sqla_with_includes.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/test_api/test_api_sqla_with_includes.py b/tests/test_api/test_api_sqla_with_includes.py index 91fe7aa4..959b3fb4 100644 --- a/tests/test_api/test_api_sqla_with_includes.py +++ b/tests/test_api/test_api_sqla_with_includes.py @@ -1829,6 +1829,41 @@ async def test_update_to_many_relationships(self, async_session: AsyncSession, c ], } + async def test_remove_to_one_relationship_using_by_update(self, async_session: AsyncSession): + resource_type = "self_relationship" + app = build_app_custom( + model=SelfRelationship, + schema=SelfRelationshipSchema, + resource_type=resource_type, + ) + + parent_obj = SelfRelationship(name=fake.name()) + child_obj = SelfRelationship(name=fake.name(), self_relationship=parent_obj) + async_session.add_all([parent_obj, child_obj]) + await async_session.commit() + + assert child_obj.self_relationship_id == parent_obj.id + + async with AsyncClient(app=app, base_url="http://test") as client: + update_body = { + "data": { + "attributes": { + "name": fake.name(), + }, + "relationships": { + "self_relationship": { + "data": None, + }, + }, + }, + } + url = app.url_path_for(f"update_{resource_type}_detail", obj_id=child_obj.id) + res = await client.patch(url, json=update_body) + assert res.status_code == status.HTTP_200_OK, res.text + + await async_session.refresh(child_obj) + assert child_obj.self_relationship_id is None + class TestPatchObjectRelationshipsToOne: async def test_ok_when_foreign_key_of_related_object_is_nullable( From 193aabacb9a6d19eba1416fda14185e8f80ce553 Mon Sep 17 00:00:00 2001 From: German Bernadskiy Date: Mon, 8 Apr 2024 17:17:16 +1000 Subject: [PATCH 2/3] updated logic --- fastapi_jsonapi/data_layers/sqla_orm.py | 14 ++++++++----- tests/schemas.py | 8 ++++++- tests/test_api/test_api_sqla_with_includes.py | 21 +++++++++++++++++-- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/fastapi_jsonapi/data_layers/sqla_orm.py b/fastapi_jsonapi/data_layers/sqla_orm.py index 7ba2211c..939b3b6c 100644 --- a/fastapi_jsonapi/data_layers/sqla_orm.py +++ b/fastapi_jsonapi/data_layers/sqla_orm.py @@ -180,12 +180,16 @@ async def apply_relationships(self, obj: TypeModel, data_create: BaseJSONAPIItem ) else: assert isinstance(relationship_in, BaseJSONAPIRelationshipDataToOneSchema) - related_data = await self.get_related_object( - related_model=related_model, - related_id_field=relationship_info.id_field_name, - id_value=relationship_in.data.id, - ) + if relationship_in.data: + related_data = await self.get_related_object( + related_model=related_model, + related_id_field=relationship_info.id_field_name, + id_value=relationship_in.data.id, + ) + else: + setattr(obj, relation_name, None) + continue try: hasattr(obj, relation_name) except MissingGreenlet: diff --git a/tests/schemas.py b/tests/schemas.py index 11786e39..557f1a34 100644 --- a/tests/schemas.py +++ b/tests/schemas.py @@ -400,8 +400,14 @@ class CustomUUIDItemSchema(CustomUUIDItemAttributesSchema): id: UUID = Field(client_can_set_id=True) -class SelfRelationshipSchema(BaseModel): +class SelfRelationshipAttributesSchema(BaseModel): name: str + + class Config: + orm_mode = True + + +class SelfRelationshipSchema(SelfRelationshipAttributesSchema): self_relationship: Optional["SelfRelationshipSchema"] = Field( relationship=RelationshipInfo( resource_type="self_relationship", diff --git a/tests/test_api/test_api_sqla_with_includes.py b/tests/test_api/test_api_sqla_with_includes.py index 959b3fb4..3b24dcf5 100644 --- a/tests/test_api/test_api_sqla_with_includes.py +++ b/tests/test_api/test_api_sqla_with_includes.py @@ -52,6 +52,7 @@ CustomUUIDItemAttributesSchema, PostAttributesBaseSchema, PostCommentAttributesBaseSchema, + SelfRelationshipAttributesSchema, SelfRelationshipSchema, UserAttributesBaseSchema, UserBioAttributesBaseSchema, @@ -1845,10 +1846,12 @@ async def test_remove_to_one_relationship_using_by_update(self, async_session: A assert child_obj.self_relationship_id == parent_obj.id async with AsyncClient(app=app, base_url="http://test") as client: + expected_name = fake.name() update_body = { "data": { + "id": str(child_obj.id), "attributes": { - "name": fake.name(), + "name": expected_name, }, "relationships": { "self_relationship": { @@ -1857,9 +1860,23 @@ async def test_remove_to_one_relationship_using_by_update(self, async_session: A }, }, } + params = { + "include": "self_relationship", + } url = app.url_path_for(f"update_{resource_type}_detail", obj_id=child_obj.id) - res = await client.patch(url, json=update_body) + res = await client.patch(url, params=params, json=update_body) assert res.status_code == status.HTTP_200_OK, res.text + assert res.json() == { + "data": { + "attributes": SelfRelationshipAttributesSchema(name=expected_name).dict(), + "id": str(child_obj.id), + "relationships": {"self_relationship": {"data": None}}, + "type": "self_relationship", + }, + "included": [], + "jsonapi": {"version": "1.0"}, + "meta": None, + } await async_session.refresh(child_obj) assert child_obj.self_relationship_id is None From 651172e6629c5c922bafb8c5fd9ad14fcca57f49 Mon Sep 17 00:00:00 2001 From: German Bernadskiy Date: Mon, 8 Apr 2024 18:17:27 +1000 Subject: [PATCH 3/3] updated to many update logic --- fastapi_jsonapi/data_layers/sqla_orm.py | 13 +- tests/models.py | 9 +- tests/schemas.py | 8 +- tests/test_api/test_api_sqla_with_includes.py | 170 ++++++++++++------ 4 files changed, 138 insertions(+), 62 deletions(-) diff --git a/fastapi_jsonapi/data_layers/sqla_orm.py b/fastapi_jsonapi/data_layers/sqla_orm.py index 939b3b6c..d0f0ffca 100644 --- a/fastapi_jsonapi/data_layers/sqla_orm.py +++ b/fastapi_jsonapi/data_layers/sqla_orm.py @@ -173,11 +173,14 @@ async def apply_relationships(self, obj: TypeModel, data_create: BaseJSONAPIItem if relationship_info.many: assert isinstance(relationship_in, BaseJSONAPIRelationshipDataToManySchema) - related_data = await self.get_related_objects_list( - related_model=related_model, - related_id_field=relationship_info.id_field_name, - ids=[r.id for r in relationship_in.data], - ) + + related_data = [] + if relationship_in.data: + related_data = await self.get_related_objects_list( + related_model=related_model, + related_id_field=relationship_info.id_field_name, + ids=[r.id for r in relationship_in.data], + ) else: assert isinstance(relationship_in, BaseJSONAPIRelationshipDataToOneSchema) diff --git a/tests/models.py b/tests/models.py index e9ee6dc7..8039504b 100644 --- a/tests/models.py +++ b/tests/models.py @@ -305,8 +305,13 @@ class SelfRelationship(Base): ), nullable=True, ) - # parent = relationship("SelfRelationship", back_populates="s") - self_relationship = relationship("SelfRelationship", remote_side=[id]) + children_objects = relationship( + "SelfRelationship", + backref=backref("parent_object", remote_side=[id]), + ) + + if TYPE_CHECKING: + parent_object: Optional["SelfRelationship"] class ContainsTimestamp(Base): diff --git a/tests/schemas.py b/tests/schemas.py index 557f1a34..77e262f8 100644 --- a/tests/schemas.py +++ b/tests/schemas.py @@ -408,11 +408,17 @@ class Config: class SelfRelationshipSchema(SelfRelationshipAttributesSchema): - self_relationship: Optional["SelfRelationshipSchema"] = Field( + parent_object: Optional["SelfRelationshipSchema"] = Field( relationship=RelationshipInfo( resource_type="self_relationship", ), ) + children_objects: Optional[list["SelfRelationshipSchema"]] = Field( + relationship=RelationshipInfo( + resource_type="self_relatiosnhip", + many=True, + ), + ) class CascadeCaseSchema(BaseModel): diff --git a/tests/test_api/test_api_sqla_with_includes.py b/tests/test_api/test_api_sqla_with_includes.py index 3b24dcf5..82f3941d 100644 --- a/tests/test_api/test_api_sqla_with_includes.py +++ b/tests/test_api/test_api_sqla_with_includes.py @@ -1438,7 +1438,7 @@ async def test_create_with_relationship_to_the_same_table(self): "name": "child", }, "relationships": { - "self_relationship": { + "parent_object": { "data": { "type": resource_type, "id": parent_object_id, @@ -1447,7 +1447,7 @@ async def test_create_with_relationship_to_the_same_table(self): }, }, } - url = f"{url}?include=self_relationship" + url = f"{url}?include=parent_object" res = await client.post(url, json=create_with_relationship_body) assert res.status_code == status.HTTP_201_CREATED, res.text @@ -1459,7 +1459,7 @@ async def test_create_with_relationship_to_the_same_table(self): "attributes": {"name": "child"}, "id": child_object_id, "relationships": { - "self_relationship": { + "parent_object": { "data": { "id": parent_object_id, "type": "self_relationship", @@ -1830,57 +1830,6 @@ async def test_update_to_many_relationships(self, async_session: AsyncSession, c ], } - async def test_remove_to_one_relationship_using_by_update(self, async_session: AsyncSession): - resource_type = "self_relationship" - app = build_app_custom( - model=SelfRelationship, - schema=SelfRelationshipSchema, - resource_type=resource_type, - ) - - parent_obj = SelfRelationship(name=fake.name()) - child_obj = SelfRelationship(name=fake.name(), self_relationship=parent_obj) - async_session.add_all([parent_obj, child_obj]) - await async_session.commit() - - assert child_obj.self_relationship_id == parent_obj.id - - async with AsyncClient(app=app, base_url="http://test") as client: - expected_name = fake.name() - update_body = { - "data": { - "id": str(child_obj.id), - "attributes": { - "name": expected_name, - }, - "relationships": { - "self_relationship": { - "data": None, - }, - }, - }, - } - params = { - "include": "self_relationship", - } - url = app.url_path_for(f"update_{resource_type}_detail", obj_id=child_obj.id) - res = await client.patch(url, params=params, json=update_body) - assert res.status_code == status.HTTP_200_OK, res.text - assert res.json() == { - "data": { - "attributes": SelfRelationshipAttributesSchema(name=expected_name).dict(), - "id": str(child_obj.id), - "relationships": {"self_relationship": {"data": None}}, - "type": "self_relationship", - }, - "included": [], - "jsonapi": {"version": "1.0"}, - "meta": None, - } - - await async_session.refresh(child_obj) - assert child_obj.self_relationship_id is None - class TestPatchObjectRelationshipsToOne: async def test_ok_when_foreign_key_of_related_object_is_nullable( @@ -2094,6 +2043,60 @@ async def test_update_resource_error_same_id( ], } + async def test_remove_to_one_relationship_using_by_update(self, async_session: AsyncSession): + resource_type = "self_relationship" + with suppress(KeyError): + RoutersJSONAPI.all_jsonapi_routers.pop(resource_type) + + app = build_app_custom( + model=SelfRelationship, + schema=SelfRelationshipSchema, + resource_type=resource_type, + ) + + parent_obj = SelfRelationship(name=fake.name()) + child_obj = SelfRelationship(name=fake.name(), parent_object=parent_obj) + async_session.add_all([parent_obj, child_obj]) + await async_session.commit() + + assert child_obj.self_relationship_id == parent_obj.id + + async with AsyncClient(app=app, base_url="http://test") as client: + expected_name = fake.name() + update_body = { + "data": { + "id": str(child_obj.id), + "attributes": { + "name": expected_name, + }, + "relationships": { + "parent_object": { + "data": None, + }, + }, + }, + } + params = { + "include": "parent_object", + } + url = app.url_path_for(f"update_{resource_type}_detail", obj_id=child_obj.id) + res = await client.patch(url, params=params, json=update_body) + assert res.status_code == status.HTTP_200_OK, res.text + assert res.json() == { + "data": { + "attributes": SelfRelationshipAttributesSchema(name=expected_name).dict(), + "id": str(child_obj.id), + "relationships": {"parent_object": {"data": None}}, + "type": "self_relationship", + }, + "included": [], + "jsonapi": {"version": "1.0"}, + "meta": None, + } + + await async_session.refresh(child_obj) + assert child_obj.self_relationship_id is None + class TestPatchRelationshipsToMany: async def test_ok( @@ -2269,6 +2272,65 @@ async def test_relationship_not_found( ], } + async def test_remove_to_many_relationship_using_by_update(self, async_session: AsyncSession): + resource_type = "self_relationship" + with suppress(KeyError): + RoutersJSONAPI.all_jsonapi_routers.pop(resource_type) + + app = build_app_custom( + model=SelfRelationship, + schema=SelfRelationshipSchema, + resource_type=resource_type, + ) + + parent_obj = SelfRelationship(name=fake.name()) + child_obj_1 = SelfRelationship(name=fake.name(), parent_object=parent_obj) + child_obj_2 = SelfRelationship(name=fake.name(), parent_object=parent_obj) + async_session.add_all([parent_obj, child_obj_1, child_obj_2]) + await async_session.commit() + + assert child_obj_1.self_relationship_id == parent_obj.id + assert child_obj_2.self_relationship_id == parent_obj.id + assert len(parent_obj.children_objects) == 2 # noqa PLR2004 + + async with AsyncClient(app=app, base_url="http://test") as client: + expected_name = fake.name() + update_body = { + "data": { + "id": str(parent_obj.id), + "attributes": { + "name": expected_name, + }, + "relationships": { + "children_objects": { + "data": None, + }, + }, + }, + } + params = { + "include": "children_objects", + } + url = app.url_path_for(f"update_{resource_type}_detail", obj_id=parent_obj.id) + res = await client.patch(url, params=params, json=update_body) + assert res.status_code == status.HTTP_200_OK, res.text + assert res.json() == { + "data": { + "attributes": SelfRelationshipAttributesSchema(name=expected_name).dict(), + "id": str(parent_obj.id), + "relationships": {"children_objects": {"data": []}}, + "type": "self_relationship", + }, + "included": [], + "jsonapi": {"version": "1.0"}, + "meta": None, + } + + await async_session.refresh(child_obj_1) + await async_session.refresh(child_obj_2) + assert child_obj_1.self_relationship_id is None + assert child_obj_2.self_relationship_id is None + class TestDeleteObjects: async def test_delete_object_and_fetch_404(