Skip to content

Commit 13af972

Browse files
committed
local ids, coverage
1 parent 74e1d83 commit 13af972

File tree

8 files changed

+961
-8
lines changed

8 files changed

+961
-8
lines changed

fastapi_jsonapi/atomic/atomic_handler.py

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
from collections import defaultdict
34
from typing import (
45
TYPE_CHECKING,
56
Any,
@@ -13,7 +14,7 @@
1314
from starlette.requests import Request
1415

1516
from fastapi_jsonapi import RoutersJSONAPI
16-
from fastapi_jsonapi.atomic.prepared_atomic_operation import OperationBase
17+
from fastapi_jsonapi.atomic.prepared_atomic_operation import LocalIdsType, OperationBase
1718
from fastapi_jsonapi.atomic.schemas import AtomicOperationRequest, AtomicResultResponse
1819
from fastapi_jsonapi.utils.dependency_helper import DependencyHelper
1920
from fastapi_jsonapi.views.utils import HTTPMethodConfig
@@ -79,21 +80,32 @@ async def handle(self) -> Union[AtomicResponseDict, AtomicResultResponse, None]:
7980

8081
# TODO: try/except, catch schema ValidationError
8182

83+
only_empty_responses = True
84+
local_ids_cache: LocalIdsType = defaultdict(dict)
8285
success = True
8386
previous_dl: Optional[BaseDataLayer] = None
8487
for operation in prepared_operations:
8588
dl: BaseDataLayer = await operation.get_data_layer()
8689
await dl.atomic_start(previous_dl=previous_dl)
8790
previous_dl = dl
91+
operation.update_relationships_with_lid(local_ids=local_ids_cache)
8892
response = await operation.handle(dl=dl)
8993
# response.data.id
90-
if response:
91-
results.append({"data": response.data})
94+
if not response:
95+
# https://jsonapi.org/ext/atomic/#result-objects
96+
# An empty result object ({}) is acceptable
97+
# for operations that are not required to return data.
98+
results.append({})
99+
continue
100+
only_empty_responses = False
101+
results.append({"data": response.data})
102+
if operation.data.lid and response.data:
103+
local_ids_cache[operation.data.type][operation.data.lid] = response.data.id
92104

93105
if previous_dl:
94106
await previous_dl.atomic_end(success=success)
95107

96-
if results:
108+
if not only_empty_responses:
97109
return {"atomic:results": results}
98110

99111
"""

fastapi_jsonapi/atomic/prepared_atomic_operation.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@
1414
from fastapi_jsonapi.views.list_view import ListViewBase
1515
from fastapi_jsonapi.views.view_base import ViewBase
1616

17+
LocalIdsType = Dict[str, Dict[str, str]]
18+
1719

1820
@dataclass
1921
class OperationBase:
@@ -62,6 +64,55 @@ async def get_data_layer(self) -> BaseDataLayer:
6264
async def handle(self, dl: BaseDataLayer):
6365
raise NotImplementedError
6466

67+
@classmethod
68+
def upd_one_relationship_with_local_id(cls, relationship_info: dict, local_ids: LocalIdsType):
69+
"""
70+
TODO: refactor
71+
72+
:param relationship_info:
73+
:param local_ids:
74+
:return:
75+
"""
76+
missing = object()
77+
lid = relationship_info.get("lid", missing)
78+
if lid is missing:
79+
return
80+
81+
resource_type = relationship_info["type"]
82+
if resource_type not in local_ids:
83+
msg = (
84+
f"Resource {resource_type} not found in previous operations,"
85+
f" no lid {lid} defined yet, cannot create {relationship_info}"
86+
)
87+
raise ValueError(
88+
msg,
89+
)
90+
91+
lids_for_resource = local_ids[resource_type]
92+
if lid not in lids_for_resource:
93+
msg = (
94+
f"lid {lid} for {resource_type} not found in previous operations,"
95+
f" cannot process {relationship_info}"
96+
)
97+
raise ValueError(msg)
98+
99+
relationship_info.pop("lid")
100+
relationship_info["id"] = lids_for_resource[lid]
101+
102+
def update_relationships_with_lid(self, local_ids: LocalIdsType):
103+
if not (self.data and self.data.relationships):
104+
return
105+
for relationship_name, relationship_value in self.data.relationships.items():
106+
relationship_data = relationship_value["data"]
107+
if isinstance(relationship_data, list):
108+
for data in relationship_data:
109+
self.upd_one_relationship_with_local_id(data, local_ids=local_ids)
110+
elif isinstance(relationship_data, dict):
111+
self.upd_one_relationship_with_local_id(relationship_data, local_ids=local_ids)
112+
else:
113+
msg = "unexpected relationship data"
114+
raise ValueError(msg)
115+
65116

66117
class ListOperationBase(OperationBase):
67118
view: ListViewBase
@@ -83,6 +134,10 @@ async def handle(self, dl: BaseDataLayer):
83134

84135
class OperationUpdate(DetailOperationBase):
85136
async def handle(self, dl: BaseDataLayer):
137+
if self.data is None:
138+
# TODO: clear to-one relationships
139+
pass
140+
# TODO: handle relationship update requests (relationship resources)
86141
data_in = self.jsonapi.schema_in_patch(data=self.data)
87142
obj_id = self.ref and self.ref.id or self.data and self.data.id
88143
response = await self.view.process_update_object(

fastapi_jsonapi/atomic/schemas.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class OperationItemInSchema(BaseModel):
1919

2020
type: str = Field(default=..., description="Resource type")
2121
id: Optional[str] = Field(default=None, description="Resource object ID")
22+
lid: Optional[str] = Field(default=None, description="Resource object local ID")
2223
attributes: Optional[dict] = Field(None, description="Resource object attributes")
2324
relationships: Optional[dict] = Field(None, description="Resource object relationships")
2425

tests/models.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Dict, List, Optional
1+
from typing import TYPE_CHECKING, Dict, List, Optional
22
from uuid import UUID
33

44
from sqlalchemy import JSON, Column, ForeignKey, Index, Integer, String, Text
@@ -53,6 +53,8 @@ class User(AutoIdMixin, Base):
5353
)
5454
computers = relationship(
5555
"Computer",
56+
# TODO: rename
57+
# back_populates="owner",
5658
back_populates="user",
5759
uselist=True,
5860
)
@@ -61,6 +63,8 @@ class User(AutoIdMixin, Base):
6163
back_populates="user",
6264
uselist=False,
6365
)
66+
if TYPE_CHECKING:
67+
computers: list["Computer"]
6468

6569
def __repr__(self):
6670
return f"{self.__class__.__name__}(id={self.id}, name={self.name!r})"
@@ -194,6 +198,8 @@ class Computer(AutoIdMixin, Base):
194198
id = Column(Integer, primary_key=True, autoincrement=True)
195199
name = Column(String, nullable=False)
196200
user_id = Column(Integer, ForeignKey("users.id"), nullable=True)
201+
# TODO: rename
202+
# owner = relationship("User", back_populates="computers")
197203
user = relationship("User", back_populates="computers")
198204

199205
def __repr__(self):

tests/schemas.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,14 @@ class Config:
299299

300300
id: int
301301

302+
# TODO: rename
303+
# owner: Optional["UserSchema"] = Field(
304+
user: Optional["UserSchema"] = Field(
305+
relationship=RelationshipInfo(
306+
resource_type="user",
307+
),
308+
)
309+
302310

303311
class WorkplaceBaseSchema(BaseModel):
304312
"""Workplace base schema."""

0 commit comments

Comments
 (0)