diff --git a/graphene_sqlalchemy/filters.py b/graphene_sqlalchemy/filters.py new file mode 100644 index 00000000..6bfe7c96 --- /dev/null +++ b/graphene_sqlalchemy/filters.py @@ -0,0 +1,2 @@ +class FloatFilter: + pass diff --git a/graphene_sqlalchemy/tests/models.py b/graphene_sqlalchemy/tests/models.py index 88e992b9..4cf2c3c8 100644 --- a/graphene_sqlalchemy/tests/models.py +++ b/graphene_sqlalchemy/tests/models.py @@ -6,7 +6,7 @@ func, select) from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.hybrid import hybrid_property -from sqlalchemy.orm import column_property, composite, mapper, relationship +from sqlalchemy.orm import backref, column_property, composite, mapper, relationship PetKind = Enum("cat", "dog", name="pet_kind") @@ -39,6 +39,7 @@ class Pet(Base): pet_kind = Column(PetKind, nullable=False) hair_kind = Column(Enum(HairKind, name="hair_kind"), nullable=False) reporter_id = Column(Integer(), ForeignKey("reporters.id")) + legs = Column(Integer(), default=4) class CompositeFullName(object): @@ -76,6 +77,27 @@ def hybrid_prop(self): composite_prop = composite(CompositeFullName, first_name, last_name, doc="Composite") +articles_tags_table = Table( + "articles_tags", + Base.metadata, + Column("article_id", ForeignKey("articles.id")), + Column("tag_id", ForeignKey("tags.id")), +) + + +class Image(Base): + __tablename__ = "images" + id = Column(Integer(), primary_key=True) + external_id = Column(Integer()) + description = Column(String(30)) + + +class Tag(Base): + __tablename__ = "tags" + id = Column(Integer(), primary_key=True) + name = Column(String(30)) + + class Article(Base): __tablename__ = "articles" id = Column(Integer(), primary_key=True) @@ -83,6 +105,13 @@ class Article(Base): pub_date = Column(Date()) reporter_id = Column(Integer(), ForeignKey("reporters.id")) + # one-to-one relationship with image + image_id = Column(Integer(), ForeignKey('images.id'), unique=True) + image = relationship("Image", backref=backref("articles", uselist=False)) + + # many-to-many relationship with tags + tags = relationship("Tag", secondary=articles_tags_table, backref="articles") + class ReflectedEditor(type): """Same as Editor, but using reflected table.""" diff --git a/graphene_sqlalchemy/tests/test_filters.py b/graphene_sqlalchemy/tests/test_filters.py new file mode 100644 index 00000000..f44b139c --- /dev/null +++ b/graphene_sqlalchemy/tests/test_filters.py @@ -0,0 +1,429 @@ +import graphene +import pytest + +from ..fields import SQLAlchemyConnectionField +from ..filters import FloatFilter +from ..types import SQLAlchemyObjectType +from .models import Article, Editor, HairKind, Image, Pet, Reporter, Tag +from .utils import to_std_dicts + + +def add_test_data(session): + reporter = Reporter( + first_name='John', last_name='Doe', favorite_pet_kind='cat') + session.add(reporter) + pet = Pet(name='Garfield', pet_kind='cat', hair_kind=HairKind.SHORT) + pet.reporters = reporter + session.add(pet) + pet = Pet(name='Snoopy', pet_kind='dog', hair_kind=HairKind.SHORT) + pet.reporters = reporter + session.add(pet) + reporter = Reporter( + first_name='John', last_name='Woe', favorite_pet_kind='cat') + session.add(reporter) + article = Article(headline='Hi!') + article.reporter = reporter + session.add(article) + reporter = Reporter( + first_name='Jane', last_name='Roe', favorite_pet_kind='dog') + session.add(reporter) + pet = Pet(name='Lassie', pet_kind='dog', hair_kind=HairKind.LONG) + pet.reporters.append(reporter) + session.add(pet) + editor = Editor(name="Jack") + session.add(editor) + session.commit() + + +def create_schema(session): + class ArticleType(SQLAlchemyObjectType): + class Meta: + model = Article + + class ImageType(SQLAlchemyObjectType): + class Meta: + model = Image + + class ReporterType(SQLAlchemyObjectType): + class Meta: + model = Reporter + + class Query(graphene.ObjectType): + article = graphene.Field(ArticleType) + articles = graphene.List(ArticleType) + # image = graphene.Field(ImageType) + # images = graphene.List(ImageType) + reporter = graphene.Field(ReporterType) + reporters = graphene.List(ReporterType) + + def resolve_article(self, _info): + return session.query(Article).first() + + def resolve_articles(self, _info): + return session.query(Article) + + def resolve_image(self, _info): + return session.query(Image).first() + + def resolve_images(self, _info): + return session.query(Image) + + def resolve_reporter(self, _info): + return session.query(Reporter).first() + + def resolve_reporters(self, _info): + return session.query(Reporter) + + return Query + + +# Test a simple example of filtering +@pytest.mark.xfail +def test_filter_simple(session): + add_test_data(session) + Query = create_schema(session) + + query = """ + query { + reporters(filters: {firstName: "John"}) { + firstName + } + } + """ + expected = { + "reporters": [{"firstName": "John"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# Test a custom filter type +@pytest.mark.xfail +def test_filter_custom_type(session): + add_test_data(session) + Query = create_schema(session) + + class MathFilter(FloatFilter): + def divisibleBy(dividend: float, divisor: float) -> float: + return dividend % divisor == 0. + + class ExtraQuery: + pets = SQLAlchemyConnectionField(Pet, filters=MathFilter()) + + class CustomQuery(Query, ExtraQuery): + pass + + query = """ + query { + pets (filters: { + legs: {divisibleBy: 2} + }) { + name + } + } + """ + expected = { + "pets": [{"name": "Garfield"}, {"name": "Lassie"}], + } + schema = graphene.Schema(query=CustomQuery) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + +# Test a 1:1 relationship +@pytest.mark.xfail +def test_filter_relationship_one_to_one(session): + article = Article(headline='Hi!') + image = Image(external_id=1, description="A beautiful image.") + article.image = image + session.add(article) + session.add(image) + session.commit() + + Query = create_schema(session) + + query = """ + query { + article (filters: { + image: {description: "A beautiful image."} + }) { + firstName + } + } + """ + expected = { + "article": [{"headline": "Hi!"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# Test a 1:n relationship +@pytest.mark.xfail +def test_filter_relationship_one_to_many(session): + add_test_data(session) + Query = create_schema(session) + + # test contains + query = """ + query { + reporter (filters: { + pets: { + contains: { + name: {in: ["Garfield", "Lassie"]} + } + } + }) { + lastName + } + } + """ + expected = { + "reporter": [{"lastName": "Doe"}, {"lastName": "Roe"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + # test containsAllOf + query = """ + query { + reporter (filters: { + pets: { + containsAllOf: [ + name: {eq: "Garfield"}, + name: {eq: "Snoopy"}, + ] + } + }) { + firstName + lastName + } + } + """ + expected = { + "reporter": [{"firstName": "John"}, {"lastName": "Doe"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + # test containsExactly + query = """ + query { + reporter (filters: { + pets: { + containsExactly: [ + name: {eq: "Garfield"} + ] + } + }) { + firstName + } + } + """ + expected = { + "reporter": [], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + +# Test a n:m relationship +@pytest.mark.xfail +def test_filter_relationship_many_to_many(session): + article1 = Article(headline='Article! Look!') + article2 = Article(headline='Woah! Another!') + tag1 = Tag(name="sensational") + tag2 = Tag(name="eye-grabbing") + article1.tags.append(tag1) + article2.tags.append([tag1, tag2]) + session.add(article1) + session.add(article2) + session.add(tag1) + session.add(tag2) + session.commit() + + Query = create_schema(session) + + # test contains + query = """ + query { + articles (filters: { + tags: { + contains: { + name: { in: ["sensational", "eye-grabbing"] } + } + } + }) { + headline + } + } + """ + expected = { + "articles": [ + {"headline": "Woah! Another!"}, + {"headline": "Article! Look!"}, + ], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + # test containsAllOf + query = """ + query { + articles (filters: { + tags: { + containsAllOf: [ + { tag: { name: { eq: "eye-grabbing" } } }, + { tag: { name: { eq: "sensational" } } }, + ] + } + }) { + headline + } + } + """ + expected = { + "articles": [{"headline": "Woah! Another!"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + # test containsExactly + query = """ + query { + articles (filters: { + containsExactly: [ + { tag: { name: { eq: "sensational" } } } + ] + }) { + headline + } + } + """ + expected = { + "articles": [{"headline": "Article! Look!"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# Test connecting filters with "and" +@pytest.mark.xfail +def test_filter_logic_and(session): + add_test_data(session) + + Query = create_schema(session) + + query = """ + query { + reporters (filters: { + and: [ + {firstName: "John"}, + {favoritePetKind: "cat"}, + ] + }) { + lastName + } + } + """ + expected = { + "reporters": [{"lastName": "Doe"}, {"lastName": "Woe"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# Test connecting filters with "or" +@pytest.mark.xfail +def test_filter_logic_or(session): + add_test_data(session) + Query = create_schema(session) + + query = """ + query { + reporters (filters: { + or: [ + {lastName: "Woe"}, + {favoritePetKind: "dog"}, + ] + }) { + firstName + lastName + } + } + """ + expected = { + "reporters": [ + {"firstName": "John", "lastName": "Woe"}, + {"firstName": "Jane", "lastName": "Roe"}, + ], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# Test connecting filters with "and" and "or" together +@pytest.mark.xfail +def test_filter_logic_and_or(session): + add_test_data(session) + Query = create_schema(session) + + query = """ + query { + reporters (filters: { + and: [ + {firstName: "John"}, + or : [ + {lastName: "Doe"}, + {favoritePetKind: "cat"}, + ] + ] + }) { + firstName + } + } + """ + expected = { + "reporters": [{"firstName": "John"}, {"firstName": "Jane"}], + } + schema = graphene.Schema(query=Query) + result = schema.execute(query) + assert not result.errors + result = to_std_dicts(result.data) + assert result == expected + + +# TODO hybrid property +@pytest.mark.xfail +def test_filter_hybrid_property(session): + raise NotImplementedError diff --git a/graphene_sqlalchemy/tests/test_sort_enums.py b/graphene_sqlalchemy/tests/test_sort_enums.py index 6291d4f8..64777dbc 100644 --- a/graphene_sqlalchemy/tests/test_sort_enums.py +++ b/graphene_sqlalchemy/tests/test_sort_enums.py @@ -40,6 +40,8 @@ class Meta: "HAIR_KIND_DESC", "REPORTER_ID_ASC", "REPORTER_ID_DESC", + "LEGS_ASC", + "LEGS_DESC", ] assert str(sort_enum.ID_ASC.value.value) == "pets.id ASC" assert str(sort_enum.ID_DESC.value.value) == "pets.id DESC" @@ -94,6 +96,8 @@ class Meta: "PET_KIND_DESC", "HAIR_KIND_ASC", "HAIR_KIND_DESC", + "LEGS_ASC", + "LEGS_DESC", ] @@ -134,6 +138,8 @@ class Meta: "HAIR_KIND_DESC", "REPORTER_ID_ASC", "REPORTER_ID_DESC", + "LEGS_ASC", + "LEGS_DESC", ] assert str(sort_enum.ID_ASC.value.value) == "pets.id ASC" assert str(sort_enum.ID_DESC.value.value) == "pets.id DESC" @@ -148,7 +154,7 @@ def test_sort_argument_with_excluded_fields_in_object_type(): class PetType(SQLAlchemyObjectType): class Meta: model = Pet - exclude_fields = ["hair_kind", "reporter_id"] + exclude_fields = ["hair_kind", "reporter_id", "legs"] sort_arg = PetType.sort_argument() sort_enum = sort_arg.type._of_type @@ -237,6 +243,8 @@ def get_symbol_name(column_name, sort_asc=True): "HairKindDown", "ReporterIdUp", "ReporterIdDown", + "LegsUp", + "LegsDown", ] assert sort_arg.default_value == ["IdUp"]