Skip to content

Commit

Permalink
Merge pull request #101 from CaselIT/sortable_field
Browse files Browse the repository at this point in the history
Added sort support
  • Loading branch information
syrusakbary authored Jun 22, 2018
2 parents 8cb52a1 + fb6913b commit 5346496
Show file tree
Hide file tree
Showing 10 changed files with 466 additions and 32 deletions.
24 changes: 17 additions & 7 deletions docs/examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,32 @@ Schema Examples


Search all Models with Union
-----------------
----------------------------

.. code:: python
class Book(SQLAlchemyObjectType):
class Meta:
model = BookModel
interfaces = (relay.Node,)
class BookConnection(relay.Connection):
class Meta:
node = Book
class Author(SQLAlchemyObjectType):
class Meta:
model = AuthorModel
interfaces = (relay.Node,)
class AuthorConnection(relay.Connection):
class Meta:
node = Author
class SearchResult(graphene.Union):
class Meta:
types = (Book, Author)
Expand All @@ -29,8 +39,8 @@ Search all Models with Union
search = graphene.List(SearchResult, q=graphene.String()) # List field for search results
# Normal Fields
all_books = SQLAlchemyConnectionField(Book)
all_authors = SQLAlchemyConnectionField(Author)
all_books = SQLAlchemyConnectionField(BookConnection)
all_authors = SQLAlchemyConnectionField(AuthorConnection)
def resolve_search(self, info, **args):
q = args.get("q") # Search query
Expand All @@ -47,13 +57,13 @@ Search all Models with Union
# Query Authors
authors = author_query.filter(AuthorModel.name.contains(q)).all()
return authors + books # Combine lists
return authors + books # Combine lists
schema = graphene.Schema(query=Query, types=[Book, Author, SearchResult])
Example GraphQL query

.. code:: GraphQL
.. code::
book(id: "Qm9vazow") {
id
Expand Down
61 changes: 58 additions & 3 deletions docs/tips.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@
Tips
====

Tips
====

Querying
--------

Expand All @@ -30,3 +27,61 @@ For make querying to the database work, there are two alternatives:
If you don't specify any, the following error will be displayed:

``A query in the model Base or a session in the schema is required for querying.``

Sorting
-------

By default the SQLAlchemyConnectionField sorts the result elements over the primary key(s).
The query has a `sort` argument which allows to sort over a different column(s)

Given the model

.. code:: python
class Pet(Base):
__tablename__ = 'pets'
id = Column(Integer(), primary_key=True)
name = Column(String(30))
pet_kind = Column(Enum('cat', 'dog', name='pet_kind'), nullable=False)
class PetNode(SQLAlchemyObjectType):
class Meta:
model = Pet
class PetConnection(Connection):
class Meta:
node = PetNone
class Query(ObjectType):
allPets = SQLAlchemyConnectionField(PetConnection)
some of the allowed queries are

- Sort in ascending order over the `name` column

.. code::
allPets(sort: name_asc){
edges {
node {
name
}
}
}
- Sort in descending order over the `per_kind` column and in ascending order over the `name` column

.. code::
allPets(sort: [pet_kind_desc, name_asc]) {
edges {
node {
name
petKind
}
}
}
15 changes: 14 additions & 1 deletion docs/tutorial.rst
Original file line number Diff line number Diff line change
Expand Up @@ -102,15 +102,28 @@ Create ``flask_sqlalchemy/schema.py`` and type the following:
interfaces = (relay.Node, )
class DepartmentConnection(relay.Connection):
class Meta:
node = Department
class Employee(SQLAlchemyObjectType):
class Meta:
model = EmployeeModel
interfaces = (relay.Node, )
class EmployeeConnection(relay.Connection):
class Meta:
node = Employee
class Query(graphene.ObjectType):
node = relay.Node.Field()
all_employees = SQLAlchemyConnectionField(Employee)
# Allows sorting over multiple columns, by default over the primary key
all_employees = SQLAlchemyConnectionField(EmployeeConnection)
# Disable sorting over this field
all_departments = SQLAlchemyConnectionField(DepartmentConnection, sort=None)
schema = graphene.Schema(query=Query)
Expand Down
37 changes: 30 additions & 7 deletions examples/flask_sqlalchemy/schema.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,60 @@
import graphene
from graphene import relay
from graphene_sqlalchemy import SQLAlchemyConnectionField, SQLAlchemyObjectType
from graphene_sqlalchemy import SQLAlchemyConnectionField, SQLAlchemyObjectType, utils
from models import Department as DepartmentModel
from models import Employee as EmployeeModel
from models import Role as RoleModel


class Department(SQLAlchemyObjectType):

class Meta:
model = DepartmentModel
interfaces = (relay.Node, )


class Employee(SQLAlchemyObjectType):
class DepartmentConnection(relay.Connection):
class Meta:
node = Department


class Employee(SQLAlchemyObjectType):
class Meta:
model = EmployeeModel
interfaces = (relay.Node, )


class Role(SQLAlchemyObjectType):
class EmployeeConnection(relay.Connection):
class Meta:
node = Employee


class Role(SQLAlchemyObjectType):
class Meta:
model = RoleModel
interfaces = (relay.Node, )


class RoleConnection(relay.Connection):
class Meta:
node = Role


SortEnumEmployee = utils.sort_enum_for_model(EmployeeModel, 'SortEnumEmployee',
lambda c, d: c.upper() + ('_ASC' if d else '_DESC'))


class Query(graphene.ObjectType):
node = relay.Node.Field()
all_employees = SQLAlchemyConnectionField(Employee)
all_roles = SQLAlchemyConnectionField(Role)
role = graphene.Field(Role)
# Allow only single column sorting
all_employees = SQLAlchemyConnectionField(
EmployeeConnection,
sort=graphene.Argument(
SortEnumEmployee,
default_value=utils.EnumValue('id_asc', EmployeeModel.id.asc())))
# Allows sorting over multiple columns, by default over the primary key
all_roles = SQLAlchemyConnectionField(RoleConnection)
# Disable sorting over this field
all_departments = SQLAlchemyConnectionField(DepartmentConnection, sort=None)


schema = graphene.Schema(query=Query, types=[Department, Employee, Role])
38 changes: 31 additions & 7 deletions graphene_sqlalchemy/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,28 @@
from promise import is_thenable, Promise
from sqlalchemy.orm.query import Query

from graphene.relay import ConnectionField
from graphene.relay import Connection, ConnectionField
from graphene.relay.connection import PageInfo
from graphql_relay.connection.arrayconnection import connection_from_list_slice

from .utils import get_query
from .utils import get_query, sort_argument_for_model


class SQLAlchemyConnectionField(ConnectionField):
class UnsortedSQLAlchemyConnectionField(ConnectionField):

@property
def model(self):
return self.type._meta.node._meta.model

@classmethod
def get_query(cls, model, info, **args):
return get_query(model, info.context)
def get_query(cls, model, info, sort=None, **args):
query = get_query(model, info.context)
if sort is not None:
if isinstance(sort, str):
query = query.order_by(sort.value)
else:
query = query.order_by(*(col.value for col in sort))
return query

@classmethod
def resolve_connection(cls, connection_type, model, info, args, resolved):
Expand Down Expand Up @@ -55,7 +61,25 @@ def get_resolver(self, parent_resolver):
return partial(self.connection_resolver, parent_resolver, self.type, self.model)


__connectionFactory = SQLAlchemyConnectionField
class SQLAlchemyConnectionField(UnsortedSQLAlchemyConnectionField):

def __init__(self, type, *args, **kwargs):
if 'sort' not in kwargs and issubclass(type, Connection):
# Let super class raise if type is not a Connection
try:
model = type.Edge.node._type._meta.model
kwargs.setdefault('sort', sort_argument_for_model(model))
except Exception:
raise Exception(
'Cannot create sort argument for {}. A model is required. Set the "sort" argument'
' to None to disabling the creation of the sort query argument'.format(type.__name__)
)
elif 'sort' in kwargs and kwargs['sort'] is None:
del kwargs['sort']
super(SQLAlchemyConnectionField, self).__init__(type, *args, **kwargs)


__connectionFactory = UnsortedSQLAlchemyConnectionField


def createConnectionField(_type):
Expand All @@ -69,4 +93,4 @@ def registerConnectionFieldFactory(factoryMethod):

def unregisterConnectionFieldFactory():
global __connectionFactory
__connectionFactory = SQLAlchemyConnectionField
__connectionFactory = UnsortedSQLAlchemyConnectionField
4 changes: 2 additions & 2 deletions graphene_sqlalchemy/tests/test_converter.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from ..converter import (convert_sqlalchemy_column,
convert_sqlalchemy_composite,
convert_sqlalchemy_relationship)
from ..fields import SQLAlchemyConnectionField
from ..fields import UnsortedSQLAlchemyConnectionField
from ..registry import Registry
from ..types import SQLAlchemyObjectType
from .models import Article, Pet, Reporter
Expand Down Expand Up @@ -205,7 +205,7 @@ class Meta:

dynamic_field = convert_sqlalchemy_relationship(Reporter.pets.property, A._meta.registry)
assert isinstance(dynamic_field, graphene.Dynamic)
assert isinstance(dynamic_field.get_type(), SQLAlchemyConnectionField)
assert isinstance(dynamic_field.get_type(), UnsortedSQLAlchemyConnectionField)


def test_should_manytoone_convert_connectionorlist():
Expand Down
38 changes: 38 additions & 0 deletions graphene_sqlalchemy/tests/test_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from graphene.relay import Connection
import pytest

from ..fields import SQLAlchemyConnectionField
from ..types import SQLAlchemyObjectType
from ..utils import sort_argument_for_model
from .models import Pet as PetModel, Editor


class Pet(SQLAlchemyObjectType):
class Meta:
model = PetModel


class PetConn(Connection):
class Meta:
node = Pet


def test_sort_added_by_default():
arg = SQLAlchemyConnectionField(PetConn)
assert 'sort' in arg.args
assert arg.args['sort'] == sort_argument_for_model(PetModel)


def test_sort_can_be_removed():
arg = SQLAlchemyConnectionField(PetConn, sort=None)
assert 'sort' not in arg.args


def test_custom_sort():
arg = SQLAlchemyConnectionField(PetConn, sort=sort_argument_for_model(Editor))
assert arg.args['sort'] == sort_argument_for_model(Editor)


def test_init_raises():
with pytest.raises(Exception, match='Cannot create sort'):
SQLAlchemyConnectionField(Connection)
Loading

0 comments on commit 5346496

Please sign in to comment.