diff --git a/graphene_sqlalchemy/converter.py b/graphene_sqlalchemy/converter.py index f4b805e2..3ac2da46 100644 --- a/graphene_sqlalchemy/converter.py +++ b/graphene_sqlalchemy/converter.py @@ -1,9 +1,14 @@ from enum import EnumMeta from singledispatch import singledispatch -from sqlalchemy import types +from sqlalchemy import inspect, types from sqlalchemy.dialects import postgresql -from sqlalchemy.orm import interfaces, strategies +from sqlalchemy.ext.associationproxy import AssociationProxy +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.orm import (ColumnProperty, CompositeProperty, + interfaces, RelationshipProperty, + strategies) +from sqlalchemy.orm.attributes import InstrumentedAttribute from graphene import (ID, Boolean, Dynamic, Enum, Field, Float, Int, List, String) @@ -33,6 +38,59 @@ def is_column_nullable(column): return bool(getattr(column, "nullable", True)) +def convert_sqlalchemy_association_proxy(association_prop, registry, connection_field_factory, batching, resolver, **field_kwargs): + model = association_prop.target_class + + attr = getattr(model, association_prop.value_attr) + if isinstance(attr, InstrumentedAttribute): + attr = inspect(attr).property + + def dynamic_type(): + if isinstance(attr, AssociationProxy): + return convert_sqlalchemy_association_proxy( + attr, + registry, + connection_field_factory, + batching, + resolver, + **field_kwargs + ) + elif isinstance(attr, ColumnProperty): + return convert_sqlalchemy_column( + attr, + registry, + resolver, + **field_kwargs + ) + elif isinstance(attr, CompositeProperty): + return convert_sqlalchemy_composite( + attr, + registry, + resolver + ) + elif isinstance(attr, RelationshipProperty): + batching_ = field_kwargs.pop('batching', batching) + return convert_sqlalchemy_relationship( + attr, + registry, + connection_field_factory, + batching_, + association_prop.value_attr, + **field_kwargs + # resolve Dynamic type + ).get_type() + elif isinstance(attr, hybrid_property): + return convert_sqlalchemy_hybrid_method( + attr, + resolver, + **field_kwargs + ) + + raise NotImplementedError(attr) + + return Dynamic(dynamic_type) + + def convert_sqlalchemy_relationship(relationship_prop, obj_type, connection_field_factory, batching, orm_field_name, **field_kwargs): """ diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index 88e992b9..d7b089ce 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -4,6 +4,7 @@ from sqlalchemy import (Column, Date, Enum, ForeignKey, Integer, String, Table, func, select) +from sqlalchemy.ext.associationproxy import association_proxy from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import column_property, composite, mapper, relationship @@ -65,6 +66,8 @@ class Reporter(Base): articles = relationship("Article", backref="reporter") favorite_article = relationship("Article", uselist=False) + pet_names = association_proxy('pets', 'name') + @hybrid_property def hybrid_prop(self): return self.first_name @@ -82,6 +85,7 @@ class Article(Base): headline = Column(String(100)) pub_date = Column(Date()) reporter_id = Column(Integer(), ForeignKey("reporters.id")) + reporter_pets = association_proxy('reporter', 'pets') class ReflectedEditor(type): diff --git a/graphene_sqlalchemy/tests/test_converter.py b/graphene_sqlalchemy/tests/test_converter.py index e9ee2379..6953d4e8 100644 --- a/graphene_sqlalchemy/tests/test_converter.py +++ b/graphene_sqlalchemy/tests/test_converter.py @@ -13,7 +13,8 @@ from graphene.types.datetime import DateTime from graphene.types.json import JSONString -from ..converter import (convert_sqlalchemy_column, +from ..converter import (convert_sqlalchemy_association_proxy, + convert_sqlalchemy_column, convert_sqlalchemy_composite, convert_sqlalchemy_relationship) from ..fields import (UnsortedSQLAlchemyConnectionField, @@ -285,6 +286,34 @@ class Meta: assert graphene_type.type == A +def test_should_convert_association_proxy(): + class P(SQLAlchemyObjectType): + class Meta: + model = Pet + + dynamic_field = convert_sqlalchemy_association_proxy( + Reporter.pet_names, + P, + default_connection_field_factory, + True, + mock_resolver, + ) + assert isinstance(dynamic_field, graphene.Dynamic) + graphene_type = dynamic_field.get_type() + assert isinstance(graphene_type, graphene.Field) + assert graphene_type.type == graphene.String + + dynamic_field = convert_sqlalchemy_association_proxy( + Article.reporter_pets, + P, + default_connection_field_factory, + True, + mock_resolver, + ) + assert isinstance(dynamic_field.get_type().type, graphene.List) + assert dynamic_field.get_type().type.of_type == P + + def test_should_postgresql_uuid_convert(): assert get_field(postgresql.UUID()).type == graphene.String diff --git a/graphene_sqlalchemy/types.py b/graphene_sqlalchemy/types.py index ff22cded..365aeaa5 100644 --- a/graphene_sqlalchemy/types.py +++ b/graphene_sqlalchemy/types.py @@ -1,6 +1,7 @@ from collections import OrderedDict import sqlalchemy +from sqlalchemy.ext.associationproxy import AssociationProxy from sqlalchemy.ext.hybrid import hybrid_property from sqlalchemy.orm import (ColumnProperty, CompositeProperty, RelationshipProperty) @@ -12,7 +13,9 @@ from graphene.types.utils import yank_fields_from_attrs from graphene.utils.orderedtype import OrderedType -from .converter import (convert_sqlalchemy_column, +from .batching import get_batch_resolver +from .converter import (convert_sqlalchemy_association_proxy, + convert_sqlalchemy_column, convert_sqlalchemy_composite, convert_sqlalchemy_hybrid_method, convert_sqlalchemy_relationship) @@ -113,7 +116,7 @@ def construct_fields( inspected_model.column_attrs.items() + inspected_model.composites.items() + [(name, item) for name, item in inspected_model.all_orm_descriptors.items() - if isinstance(item, hybrid_property)] + + if isinstance(item, (AssociationProxy, hybrid_property))] + inspected_model.relationships.items() ) @@ -172,6 +175,15 @@ def construct_fields( field = convert_sqlalchemy_composite(attr, registry, resolver) elif isinstance(attr, hybrid_property): field = convert_sqlalchemy_hybrid_method(attr, resolver, **orm_field.kwargs) + elif isinstance(attr, AssociationProxy): + field = convert_sqlalchemy_association_proxy( + attr.for_class(model), + registry, + connection_field_factory, + batching, + resolver, + **orm_field.kwargs + ) else: raise Exception('Property type is not supported') # Should never happen