New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add filters #164

Open
wants to merge 1 commit into
base: master
from

Conversation

Projects
None yet
3 participants
@comtihon
Copy link

comtihon commented Sep 7, 2018

Add filters support for connections.

    class Query(graphene.ObjectType):
        pets = FilterableConnectionField(PetConnection)

FilterableConnectionField will create InputFilter object with filters for every Connection's Node's property. The date type will be the same as in Node object. Currenly enums and comlex objects (not scalars) are not supported.

I've added this filter operations: eq, ne, like, lt, gt based on sqlalchemy.sql.operators.

Query:

query {
        pets(filter: {name: {eq: "Lassie"}}) {
            edges {
                node {
                    name
                }
            }
        }
    }

Multiple filters will always work like AND.
This wil generate select * from pets where name == "Lassie"

@coveralls

This comment has been minimized.

Copy link

coveralls commented Sep 7, 2018

Coverage Status

Coverage decreased (-1.5%) to 90.376% when pulling 487697f on comtihon:master into 33d5b74 on graphql-python:master.

1 similar comment
@coveralls

This comment has been minimized.

Copy link

coveralls commented Sep 7, 2018

Coverage Status

Coverage decreased (-1.5%) to 90.376% when pulling 487697f on comtihon:master into 33d5b74 on graphql-python:master.

@comtihon comtihon referenced this pull request Sep 8, 2018

Open

How to filter and limit? #27

@ahokinson

This comment has been minimized.

Copy link

ahokinson commented Nov 1, 2018

This should really be merged. @syrusakbary, is there a reason why it hasn't been yet?

Edit: I can actually see a couple of problems with this commit. I will attempt to provide some suggestions with the new Github features.

@comtihon

This comment has been minimized.

Copy link

comtihon commented Dec 9, 2018

@ahokinson @syrusakbary
Hi,
It there any problem with this review (besides coverage report)?
Thanks.

@ahokinson

This comment has been minimized.

Copy link

ahokinson commented Dec 11, 2018

I think my criticisms are stylistic and personal preference. There's some typos and I personally wouldn't leave TODO comments in a contribution.

I also think that some of your function names as well the use of @staticmethod are confusing.

However, I don't dispute that this works. I borrowed and adapted it for my own needs and it helped me to understand how arguments are constructed.

Honestly, I'm really not sure what the stance graphql-python has on contributions. If you look across their projects, there are so many ignored questions and contributions in addition to tutorials and examples that don't even work correctly. Either way, I support adding filtering to this similar to how it is done in the graphene-django implementation.

Here is the relevant part of my implementation for comparison:

from graphene_sqlalchemy import SQLAlchemyConnectionField
from sqlalchemy import inspect
from sqlalchemy.orm.attributes import InstrumentedAttribute

from core.aggregations import create_group_by_argument, create_order_by_argument
from core.exceptions import ArgumentCreationException, RequiredFilterMissingException, RequiredFilterTypeException
from core.filters import create_filter_argument, filter_query
from utilities.types.string import camel_to_snake


class ConnectionField(SQLAlchemyConnectionField):
    def __init__(self, type_, *args, **kwargs):
        try:
            model = type_.Edge.node._type._meta.model
            kwargs.setdefault("filter", create_filter_argument(model))
        except Exception:
            raise ArgumentCreationException(type_.__name__)

        self.required = kwargs.pop("required") if "required" in kwargs else None
        for required in self.required:
            if not issubclass(type(required), InstrumentedAttribute):
                raise RequiredFilterTypeException(type(required))

        super(ConnectionField, self).__init__(type_, *args, **kwargs)

    @classmethod
    def get_query(cls, model, info, filter=None, group_by=None, order_by=None, **kwargs):
        query = super(ConnectionField, cls).get_query(model, info, **kwargs)
        columns = inspect(model).columns.values()

        if filter:
            for k, v in filter.items():
                query = filter_query(query, model, k, v)

        return query

    @classmethod
    def resolve_connection(cls, connection_type, model, info, args, resolved):
        filters = args.get("filter", {})
        required_filters = [rf.key for rf in getattr(info.schema._query, camel_to_snake(info.field_name)).required]

        missing_filters = set(required_filters) - set(filters.keys())
        if missing_filters:
            raise RequiredFilterMissingException(missing_filters)

        return super(ConnectionField, cls).resolve_connection(
            connection_type, model, info, args, resolved)
from collections import OrderedDict

from graphene import Argument, Field, InputObjectType
from graphene_sqlalchemy.converter import convert_sqlalchemy_type
from sqlalchemy import inspect

argument_cache = {}
field_cache = {}


class FilterArgument:
    pass


class FilterField:
    pass


def filter_query(query, model, field, value):
    [(operator, value)] = value.items()
    if operator is "equal":
        query = query.filter(getattr(model, field) == value)
    elif operator is "notEqual":
        query = query.filter(getattr(model, field) != value)
    elif operator is "lessThan":
        query = query.filter(getattr(model, field) < value)
    elif operator is "greaterThan":
        query = query.filter(getattr(model, field) > value)
    elif operator is "like":
        query = query.filter(getattr(model, field).like(value))
    return query


def create_filter_argument(cls):
    name = "{}Filter".format(cls.__name__)
    if name in argument_cache:
        return Argument(argument_cache[name])

    fields = OrderedDict((column.name, field)
                         for column, field in [(column, create_filter_field(column))
                                               for column in inspect(cls).columns.values()] if field)

    argument_class = type(name, (FilterArgument, InputObjectType), {})
    argument_class._meta.fields.update(fields)

    argument_cache[name] = argument_class

    return Argument(argument_class)


def create_filter_field(column):
    graphene_type = convert_sqlalchemy_type(column.type, column)
    if graphene_type.__class__ == Field:
        return None

    name = "{}Filter".format(str(graphene_type.__class__))
    if name in field_cache:
        return Field(field_cache[name])

    fields = OrderedDict((key, Field(graphene_type.__class__))
                         for key in ["equal", "notEqual", "lessThan", "greaterThan", "like"])

    field_class = type(name, (FilterField, InputObjectType), {})
    field_class._meta.fields.update(fields)

    field_cache[name] = field_class

    return Field(field_class)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment