Skip to content

Commit

Permalink
Merge pull request #78 from mts-ai/fix-joins-by-relationships
Browse files Browse the repository at this point in the history
Fix JOINS by rerlationships
  • Loading branch information
mahenzon authored Feb 16, 2024
2 parents 3e68db5 + f076c45 commit cfa5248
Show file tree
Hide file tree
Showing 6 changed files with 361 additions and 20 deletions.
49 changes: 32 additions & 17 deletions fastapi_jsonapi/data_layers/filtering/sqlalchemy.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
from fastapi_jsonapi.data_typing import TypeModel, TypeSchema
from fastapi_jsonapi.exceptions import InvalidFilters, InvalidType
from fastapi_jsonapi.exceptions.json_api import HTTPException
from fastapi_jsonapi.schema import get_model_field, get_relationships
from fastapi_jsonapi.schema import JSONAPISchemaIntrospectionError, get_model_field, get_relationships

log = logging.getLogger(__name__)

Expand All @@ -44,7 +44,7 @@ class RelationshipFilteringInfo(BaseModel):
target_schema: Type[TypeSchema]
model: Type[TypeModel]
aliased_model: AliasedClass
column: InstrumentedAttribute
join_column: InstrumentedAttribute

class Config:
arbitrary_types_allowed = True
Expand Down Expand Up @@ -288,7 +288,10 @@ def get_model_column(
schema: Type[TypeSchema],
field_name: str,
) -> InstrumentedAttribute:
model_field = get_model_field(schema, field_name)
try:
model_field = get_model_field(schema, field_name)
except JSONAPISchemaIntrospectionError as e:
raise InvalidFilters(str(e))

try:
return getattr(model, model_field)
Expand Down Expand Up @@ -327,8 +330,9 @@ def gather_relationships_info(
model: Type[TypeModel],
schema: Type[TypeSchema],
relationship_path: List[str],
collected_info: dict,
collected_info: dict[RelationshipPath, RelationshipFilteringInfo],
target_relationship_idx: int = 0,
prev_aliased_model: Optional[Any] = None,
) -> dict[RelationshipPath, RelationshipFilteringInfo]:
is_last_relationship = target_relationship_idx == len(relationship_path) - 1
target_relationship_path = RELATIONSHIP_SPLITTER.join(
Expand All @@ -342,25 +346,36 @@ def gather_relationships_info(

target_schema = schema.__fields__[target_relationship_name].type_
target_model = getattr(model, target_relationship_name).property.mapper.class_
target_column = get_model_column(
model,
schema,
target_relationship_name,
)

if prev_aliased_model:
join_column = get_model_column(
model=prev_aliased_model,
schema=schema,
field_name=target_relationship_name,
)
else:
join_column = get_model_column(
model,
schema,
target_relationship_name,
)

aliased_model = aliased(target_model)
collected_info[target_relationship_path] = RelationshipFilteringInfo(
target_schema=target_schema,
model=target_model,
aliased_model=aliased(target_model),
column=target_column,
aliased_model=aliased_model,
join_column=join_column,
)

if not is_last_relationship:
return gather_relationships_info(
target_model,
target_schema,
relationship_path,
collected_info,
target_relationship_idx + 1,
model=target_model,
schema=target_schema,
relationship_path=relationship_path,
collected_info=collected_info,
target_relationship_idx=target_relationship_idx + 1,
prev_aliased_model=aliased_model,
)

return collected_info
Expand Down Expand Up @@ -553,5 +568,5 @@ def create_filters_and_joins(
target_schema=schema,
relationships_info=relationships_info,
)
joins = [(info.aliased_model, info.column) for info in relationships_info.values()]
joins = [(info.aliased_model, info.join_column) for info in relationships_info.values()]
return expressions, joins
6 changes: 5 additions & 1 deletion fastapi_jsonapi/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ class JSONAPIResultDetailSchema(BaseJSONAPIResultSchema):
]


class JSONAPISchemaIntrospectionError(Exception):
pass


def get_model_field(schema: Type["TypeSchema"], field: str) -> str:
"""
Get the model field of a schema field.
Expand All @@ -145,7 +149,7 @@ class ComputerSchema(pydantic_base):
schema=schema.__name__,
field=field,
)
raise Exception(msg)
raise JSONAPISchemaIntrospectionError(msg)
return field


Expand Down
83 changes: 82 additions & 1 deletion tests/fixtures/app.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
from pathlib import Path
from typing import Type
from typing import Optional, Type

import pytest
from fastapi import APIRouter, FastAPI
from pydantic import BaseModel

from fastapi_jsonapi import RoutersJSONAPI, init
from fastapi_jsonapi.atomic import AtomicOperations
from fastapi_jsonapi.data_typing import TypeModel
from fastapi_jsonapi.views.detail_view import DetailViewBase
from fastapi_jsonapi.views.list_view import ListViewBase
from tests.fixtures.views import (
DetailViewBaseGeneric,
ListViewBaseGeneric,
)
from tests.models import (
Alpha,
Beta,
Child,
Computer,
CustomUUIDItem,
Delta,
Gamma,
Parent,
ParentToChildAssociation,
Post,
Expand All @@ -25,13 +31,17 @@
UserBio,
)
from tests.schemas import (
AlphaSchema,
BetaSchema,
ChildInSchema,
ChildPatchSchema,
ChildSchema,
ComputerInSchema,
ComputerPatchSchema,
ComputerSchema,
CustomUUIDItemSchema,
DeltaSchema,
GammaSchema,
ParentPatchSchema,
ParentSchema,
ParentToChildAssociationSchema,
Expand Down Expand Up @@ -245,3 +255,74 @@ def build_app_custom(
app.include_router(atomic.router, prefix="")
init(app)
return app


def build_alphabet_app() -> FastAPI:
return build_custom_app_by_schemas(
[
ResourceInfoDTO(
path="/alpha",
resource_type="alpha",
model=Alpha,
schema_=AlphaSchema,
),
ResourceInfoDTO(
path="/beta",
resource_type="beta",
model=Beta,
schema_=BetaSchema,
),
ResourceInfoDTO(
path="/gamma",
resource_type="gamma",
model=Gamma,
schema_=GammaSchema,
),
ResourceInfoDTO(
path="/delta",
resource_type="delta",
model=Delta,
schema_=DeltaSchema,
),
],
)


class ResourceInfoDTO(BaseModel):
path: str
resource_type: str
model: Type[TypeModel]
schema_: Type[BaseModel]
schema_in_patch: Optional[BaseModel] = None
schema_in_post: Optional[BaseModel] = None
class_list: Type[ListViewBase] = ListViewBaseGeneric
class_detail: Type[DetailViewBase] = DetailViewBaseGeneric

class Config:
arbitrary_types_allowed = True


def build_custom_app_by_schemas(resources_info: list[ResourceInfoDTO]):
router: APIRouter = APIRouter()

for info in resources_info:
RoutersJSONAPI(
router=router,
path=info.path,
tags=["Misc"],
class_list=info.class_list,
class_detail=info.class_detail,
schema=info.schema_,
resource_type=info.resource_type,
schema_in_patch=info.schema_in_patch,
schema_in_post=info.schema_in_post,
model=info.model,
)

app = build_app_plain()
app.include_router(router, prefix="")

atomic = AtomicOperations()
app.include_router(atomic.router, prefix="")
init(app)
return app
78 changes: 78 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,3 +312,81 @@ class SelfRelationship(Base):
class ContainsTimestamp(Base):
id = Column(Integer, primary_key=True)
timestamp = Column(DateTime(True), nullable=False)


class Alpha(Base):
__tablename__ = "alpha"

id = Column(Integer, primary_key=True, autoincrement=True)
beta_id = Column(
Integer,
ForeignKey("beta.id"),
nullable=False,
index=True,
)
beta = relationship("Beta", back_populates="alphas")
gamma_id = Column(Integer, ForeignKey("gamma.id"), nullable=False)
gamma: "Gamma" = relationship("Gamma")


class BetaGammaBinding(Base):
__tablename__ = "beta_gamma_binding"

id: int = Column(Integer, primary_key=True)
beta_id: int = Column(ForeignKey("beta.id", ondelete="CASCADE"), nullable=False)
gamma_id: int = Column(ForeignKey("gamma.id", ondelete="CASCADE"), nullable=False)


class Beta(Base):
__tablename__ = "beta"

id = Column(Integer, primary_key=True, autoincrement=True)
gammas: List["Gamma"] = relationship(
"Gamma",
secondary="beta_gamma_binding",
back_populates="betas",
lazy="noload",
)
alphas = relationship("Alpha")
deltas: List["Delta"] = relationship(
"Delta",
secondary="beta_delta_binding",
lazy="noload",
)


class Gamma(Base):
__tablename__ = "gamma"

id = Column(Integer, primary_key=True, autoincrement=True)
betas: List["Beta"] = relationship(
"Beta",
secondary="beta_gamma_binding",
back_populates="gammas",
lazy="raise",
)
delta_id: int = Column(
Integer,
ForeignKey("delta.id", ondelete="CASCADE"),
nullable=False,
index=True,
)
alpha = relationship("Alpha")
delta: "Delta" = relationship("Delta")


class BetaDeltaBinding(Base):
__tablename__ = "beta_delta_binding"

id: int = Column(Integer, primary_key=True)
beta_id: int = Column(ForeignKey("beta.id", ondelete="CASCADE"), nullable=False)
delta_id: int = Column(ForeignKey("delta.id", ondelete="CASCADE"), nullable=False)


class Delta(Base):
__tablename__ = "delta"

id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String)
gammas: List["Gamma"] = relationship("Gamma", back_populates="delta", lazy="noload")
betas: List["Beta"] = relationship("Beta", secondary="beta_delta_binding", back_populates="deltas", lazy="noload")
69 changes: 69 additions & 0 deletions tests/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -412,3 +412,72 @@ class SelfRelationshipSchema(BaseModel):
class CustomUserAttributesSchema(UserBaseSchema):
spam: str
eggs: str


class AlphaSchema(BaseModel):
beta: Optional["BetaSchema"] = Field(
relationship=RelationshipInfo(
resource_type="beta",
),
)
gamma: Optional["GammaSchema"] = Field(
relationship=RelationshipInfo(
resource_type="gamma",
),
)


class BetaSchema(BaseModel):
alphas: Optional["AlphaSchema"] = Field(
relationship=RelationshipInfo(
resource_type="alpha",
),
)
gammas: Optional["GammaSchema"] = Field(
None,
relationship=RelationshipInfo(
resource_type="gamma",
many=True,
),
)
deltas: Optional["DeltaSchema"] = Field(
None,
relationship=RelationshipInfo(
resource_type="delta",
many=True,
),
)


class GammaSchema(BaseModel):
betas: Optional["BetaSchema"] = Field(
None,
relationship=RelationshipInfo(
resource_type="beta",
many=True,
),
)
delta: Optional["DeltaSchema"] = Field(
None,
relationship=RelationshipInfo(
resource_type="Delta",
),
)


class DeltaSchema(BaseModel):
name: str
gammas: Optional["GammaSchema"] = Field(
None,
relationship=RelationshipInfo(
resource_type="gamma",
many=True,
),
)
betas: Optional["BetaSchema"] = Field(
None,
relationship=RelationshipInfo(
resource_type="beta",
many=True,
),
)
Loading

0 comments on commit cfa5248

Please sign in to comment.