Skip to content

Commit 5346496

Browse files
authored
Merge pull request #101 from CaselIT/sortable_field
Added sort support
2 parents 8cb52a1 + fb6913b commit 5346496

File tree

10 files changed

+466
-32
lines changed

10 files changed

+466
-32
lines changed

docs/examples.rst

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,32 @@ Schema Examples
33

44

55
Search all Models with Union
6-
-----------------
6+
----------------------------
77

88
.. code:: python
99
1010
class Book(SQLAlchemyObjectType):
1111
class Meta:
1212
model = BookModel
1313
interfaces = (relay.Node,)
14-
15-
14+
15+
16+
class BookConnection(relay.Connection):
17+
class Meta:
18+
node = Book
19+
20+
1621
class Author(SQLAlchemyObjectType):
1722
class Meta:
1823
model = AuthorModel
1924
interfaces = (relay.Node,)
2025
2126
27+
class AuthorConnection(relay.Connection):
28+
class Meta:
29+
node = Author
30+
31+
2232
class SearchResult(graphene.Union):
2333
class Meta:
2434
types = (Book, Author)
@@ -29,8 +39,8 @@ Search all Models with Union
2939
search = graphene.List(SearchResult, q=graphene.String()) # List field for search results
3040
3141
# Normal Fields
32-
all_books = SQLAlchemyConnectionField(Book)
33-
all_authors = SQLAlchemyConnectionField(Author)
42+
all_books = SQLAlchemyConnectionField(BookConnection)
43+
all_authors = SQLAlchemyConnectionField(AuthorConnection)
3444
3545
def resolve_search(self, info, **args):
3646
q = args.get("q") # Search query
@@ -47,13 +57,13 @@ Search all Models with Union
4757
# Query Authors
4858
authors = author_query.filter(AuthorModel.name.contains(q)).all()
4959
50-
return authors + books # Combine lists
60+
return authors + books # Combine lists
5161
5262
schema = graphene.Schema(query=Query, types=[Book, Author, SearchResult])
5363
5464
Example GraphQL query
5565

56-
.. code:: GraphQL
66+
.. code::
5767
5868
book(id: "Qm9vazow") {
5969
id

docs/tips.rst

Lines changed: 58 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,6 @@
22
Tips
33
====
44

5-
Tips
6-
====
7-
85
Querying
96
--------
107

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

3229
``A query in the model Base or a session in the schema is required for querying.``
30+
31+
Sorting
32+
-------
33+
34+
By default the SQLAlchemyConnectionField sorts the result elements over the primary key(s).
35+
The query has a `sort` argument which allows to sort over a different column(s)
36+
37+
Given the model
38+
39+
.. code:: python
40+
41+
class Pet(Base):
42+
__tablename__ = 'pets'
43+
id = Column(Integer(), primary_key=True)
44+
name = Column(String(30))
45+
pet_kind = Column(Enum('cat', 'dog', name='pet_kind'), nullable=False)
46+
47+
48+
class PetNode(SQLAlchemyObjectType):
49+
class Meta:
50+
model = Pet
51+
52+
53+
class PetConnection(Connection):
54+
class Meta:
55+
node = PetNone
56+
57+
58+
class Query(ObjectType):
59+
allPets = SQLAlchemyConnectionField(PetConnection)
60+
61+
some of the allowed queries are
62+
63+
- Sort in ascending order over the `name` column
64+
65+
.. code::
66+
67+
allPets(sort: name_asc){
68+
edges {
69+
node {
70+
name
71+
}
72+
}
73+
}
74+
75+
- Sort in descending order over the `per_kind` column and in ascending order over the `name` column
76+
77+
.. code::
78+
79+
allPets(sort: [pet_kind_desc, name_asc]) {
80+
edges {
81+
node {
82+
name
83+
petKind
84+
}
85+
}
86+
}
87+

docs/tutorial.rst

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,15 +102,28 @@ Create ``flask_sqlalchemy/schema.py`` and type the following:
102102
interfaces = (relay.Node, )
103103
104104
105+
class DepartmentConnection(relay.Connection):
106+
class Meta:
107+
node = Department
108+
109+
105110
class Employee(SQLAlchemyObjectType):
106111
class Meta:
107112
model = EmployeeModel
108113
interfaces = (relay.Node, )
109114
110115
116+
class EmployeeConnection(relay.Connection):
117+
class Meta:
118+
node = Employee
119+
120+
111121
class Query(graphene.ObjectType):
112122
node = relay.Node.Field()
113-
all_employees = SQLAlchemyConnectionField(Employee)
123+
# Allows sorting over multiple columns, by default over the primary key
124+
all_employees = SQLAlchemyConnectionField(EmployeeConnection)
125+
# Disable sorting over this field
126+
all_departments = SQLAlchemyConnectionField(DepartmentConnection, sort=None)
114127
115128
schema = graphene.Schema(query=Query)
116129

examples/flask_sqlalchemy/schema.py

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,60 @@
11
import graphene
22
from graphene import relay
3-
from graphene_sqlalchemy import SQLAlchemyConnectionField, SQLAlchemyObjectType
3+
from graphene_sqlalchemy import SQLAlchemyConnectionField, SQLAlchemyObjectType, utils
44
from models import Department as DepartmentModel
55
from models import Employee as EmployeeModel
66
from models import Role as RoleModel
77

88

99
class Department(SQLAlchemyObjectType):
10-
1110
class Meta:
1211
model = DepartmentModel
1312
interfaces = (relay.Node, )
1413

1514

16-
class Employee(SQLAlchemyObjectType):
15+
class DepartmentConnection(relay.Connection):
16+
class Meta:
17+
node = Department
18+
1719

20+
class Employee(SQLAlchemyObjectType):
1821
class Meta:
1922
model = EmployeeModel
2023
interfaces = (relay.Node, )
2124

2225

23-
class Role(SQLAlchemyObjectType):
26+
class EmployeeConnection(relay.Connection):
27+
class Meta:
28+
node = Employee
2429

30+
31+
class Role(SQLAlchemyObjectType):
2532
class Meta:
2633
model = RoleModel
2734
interfaces = (relay.Node, )
2835

2936

37+
class RoleConnection(relay.Connection):
38+
class Meta:
39+
node = Role
40+
41+
42+
SortEnumEmployee = utils.sort_enum_for_model(EmployeeModel, 'SortEnumEmployee',
43+
lambda c, d: c.upper() + ('_ASC' if d else '_DESC'))
44+
45+
3046
class Query(graphene.ObjectType):
3147
node = relay.Node.Field()
32-
all_employees = SQLAlchemyConnectionField(Employee)
33-
all_roles = SQLAlchemyConnectionField(Role)
34-
role = graphene.Field(Role)
48+
# Allow only single column sorting
49+
all_employees = SQLAlchemyConnectionField(
50+
EmployeeConnection,
51+
sort=graphene.Argument(
52+
SortEnumEmployee,
53+
default_value=utils.EnumValue('id_asc', EmployeeModel.id.asc())))
54+
# Allows sorting over multiple columns, by default over the primary key
55+
all_roles = SQLAlchemyConnectionField(RoleConnection)
56+
# Disable sorting over this field
57+
all_departments = SQLAlchemyConnectionField(DepartmentConnection, sort=None)
3558

3659

3760
schema = graphene.Schema(query=Query, types=[Department, Employee, Role])

graphene_sqlalchemy/fields.py

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,28 @@
22
from promise import is_thenable, Promise
33
from sqlalchemy.orm.query import Query
44

5-
from graphene.relay import ConnectionField
5+
from graphene.relay import Connection, ConnectionField
66
from graphene.relay.connection import PageInfo
77
from graphql_relay.connection.arrayconnection import connection_from_list_slice
88

9-
from .utils import get_query
9+
from .utils import get_query, sort_argument_for_model
1010

1111

12-
class SQLAlchemyConnectionField(ConnectionField):
12+
class UnsortedSQLAlchemyConnectionField(ConnectionField):
1313

1414
@property
1515
def model(self):
1616
return self.type._meta.node._meta.model
1717

1818
@classmethod
19-
def get_query(cls, model, info, **args):
20-
return get_query(model, info.context)
19+
def get_query(cls, model, info, sort=None, **args):
20+
query = get_query(model, info.context)
21+
if sort is not None:
22+
if isinstance(sort, str):
23+
query = query.order_by(sort.value)
24+
else:
25+
query = query.order_by(*(col.value for col in sort))
26+
return query
2127

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

5763

58-
__connectionFactory = SQLAlchemyConnectionField
64+
class SQLAlchemyConnectionField(UnsortedSQLAlchemyConnectionField):
65+
66+
def __init__(self, type, *args, **kwargs):
67+
if 'sort' not in kwargs and issubclass(type, Connection):
68+
# Let super class raise if type is not a Connection
69+
try:
70+
model = type.Edge.node._type._meta.model
71+
kwargs.setdefault('sort', sort_argument_for_model(model))
72+
except Exception:
73+
raise Exception(
74+
'Cannot create sort argument for {}. A model is required. Set the "sort" argument'
75+
' to None to disabling the creation of the sort query argument'.format(type.__name__)
76+
)
77+
elif 'sort' in kwargs and kwargs['sort'] is None:
78+
del kwargs['sort']
79+
super(SQLAlchemyConnectionField, self).__init__(type, *args, **kwargs)
80+
81+
82+
__connectionFactory = UnsortedSQLAlchemyConnectionField
5983

6084

6185
def createConnectionField(_type):
@@ -69,4 +93,4 @@ def registerConnectionFieldFactory(factoryMethod):
6993

7094
def unregisterConnectionFieldFactory():
7195
global __connectionFactory
72-
__connectionFactory = SQLAlchemyConnectionField
96+
__connectionFactory = UnsortedSQLAlchemyConnectionField

graphene_sqlalchemy/tests/test_converter.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from ..converter import (convert_sqlalchemy_column,
1717
convert_sqlalchemy_composite,
1818
convert_sqlalchemy_relationship)
19-
from ..fields import SQLAlchemyConnectionField
19+
from ..fields import UnsortedSQLAlchemyConnectionField
2020
from ..registry import Registry
2121
from ..types import SQLAlchemyObjectType
2222
from .models import Article, Pet, Reporter
@@ -205,7 +205,7 @@ class Meta:
205205

206206
dynamic_field = convert_sqlalchemy_relationship(Reporter.pets.property, A._meta.registry)
207207
assert isinstance(dynamic_field, graphene.Dynamic)
208-
assert isinstance(dynamic_field.get_type(), SQLAlchemyConnectionField)
208+
assert isinstance(dynamic_field.get_type(), UnsortedSQLAlchemyConnectionField)
209209

210210

211211
def test_should_manytoone_convert_connectionorlist():
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from graphene.relay import Connection
2+
import pytest
3+
4+
from ..fields import SQLAlchemyConnectionField
5+
from ..types import SQLAlchemyObjectType
6+
from ..utils import sort_argument_for_model
7+
from .models import Pet as PetModel, Editor
8+
9+
10+
class Pet(SQLAlchemyObjectType):
11+
class Meta:
12+
model = PetModel
13+
14+
15+
class PetConn(Connection):
16+
class Meta:
17+
node = Pet
18+
19+
20+
def test_sort_added_by_default():
21+
arg = SQLAlchemyConnectionField(PetConn)
22+
assert 'sort' in arg.args
23+
assert arg.args['sort'] == sort_argument_for_model(PetModel)
24+
25+
26+
def test_sort_can_be_removed():
27+
arg = SQLAlchemyConnectionField(PetConn, sort=None)
28+
assert 'sort' not in arg.args
29+
30+
31+
def test_custom_sort():
32+
arg = SQLAlchemyConnectionField(PetConn, sort=sort_argument_for_model(Editor))
33+
assert arg.args['sort'] == sort_argument_for_model(Editor)
34+
35+
36+
def test_init_raises():
37+
with pytest.raises(Exception, match='Cannot create sort'):
38+
SQLAlchemyConnectionField(Connection)

0 commit comments

Comments
 (0)