Skip to content

Commit 1132d9d

Browse files
Pagination Optimised for faster execution
Pagination support added for custom connection resolvers; eg: def resolve_author_query(self, info, **kwargs): return info.context.queryset.filter(is_verified=True) Pagination Test Passed
1 parent 2c91b7f commit 1132d9d

File tree

3 files changed

+153
-32
lines changed

3 files changed

+153
-32
lines changed

graphene_mongo/fields.py

Lines changed: 87 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
from graphene.types.argument import to_arguments
1616
from graphene.types.dynamic import Dynamic
1717
from graphene.types.structures import Structure
18-
from graphql_relay.connection.arrayconnection import connection_from_list_slice
18+
from graphql_relay.connection.arrayconnection import cursor_to_offset
19+
from mongoengine import QuerySet
1920

2021
from .advanced_types import (
2122
FileFieldType,
@@ -25,7 +26,8 @@
2526
)
2627
from .converter import convert_mongoengine_field, MongoEngineConversionError
2728
from .registry import get_global_registry
28-
from .utils import get_model_reference_fields, get_node_from_global_id, get_query_fields
29+
from .utils import get_model_reference_fields, get_node_from_global_id, get_query_fields, find_skip_and_limit, \
30+
connection_from_iterables
2931

3032

3133
class MongoengineConnectionField(ConnectionField):
@@ -188,7 +190,7 @@ def get_reference_field(r, kv):
188190
def fields(self):
189191
return self._type._meta.fields
190192

191-
def get_queryset(self, model, info, required_fields=list(), **args):
193+
def get_queryset(self, model, info, required_fields=list(), skip=None, limit=None, reversed=False, **args):
192194
if args:
193195
reference_fields = get_model_reference_fields(self.model)
194196
hydrated_references = {}
@@ -206,7 +208,30 @@ def get_queryset(self, model, info, required_fields=list(), **args):
206208
return queryset_or_filters
207209
else:
208210
args.update(queryset_or_filters)
209-
211+
if limit is not None:
212+
if reversed:
213+
order_by = ""
214+
if self.order_by:
215+
order_by = self.order_by + ",-pk"
216+
else:
217+
order_by = "-pk"
218+
return model.objects(**args).no_dereference().only(*required_fields).order_by(order_by).skip(
219+
skip if skip else 0).limit(limit)
220+
else:
221+
return model.objects(**args).no_dereference().only(*required_fields).order_by(self.order_by).skip(
222+
skip if skip else 0).limit(limit)
223+
elif skip is not None:
224+
if reversed:
225+
order_by = ""
226+
if self.order_by:
227+
order_by = self.order_by + ",-pk"
228+
else:
229+
order_by = "-pk"
230+
return model.objects(**args).no_dereference().only(*required_fields).order_by(order_by).skip(
231+
skip)
232+
else:
233+
return model.objects(**args).no_dereference().only(*required_fields).order_by(self.order_by).skip(
234+
skip)
210235
return model.objects(**args).no_dereference().only(*required_fields).order_by(self.order_by)
211236

212237
def default_resolver(self, _root, info, required_fields=list(), **args):
@@ -217,38 +242,66 @@ def default_resolver(self, _root, info, required_fields=list(), **args):
217242
if getattr(_root, field_name, []) is not None:
218243
args["pk__in"] = [r.id for r in getattr(_root, field_name, [])]
219244

220-
connection_args = {
221-
"first": args.pop("first", None),
222-
"last": args.pop("last", None),
223-
"before": args.pop("before", None),
224-
"after": args.pop("after", None),
225-
}
226-
227245
_id = args.pop('id', None)
228246

229247
if _id is not None:
230248
args['pk'] = from_global_id(_id)[-1]
231-
249+
iterables = []
250+
list_length = 0
251+
skip = 0
252+
count = 0
253+
limit = None
254+
reverse = False
232255
if callable(getattr(self.model, "objects", None)):
233-
iterables = self.get_queryset(self.model, info, required_fields, **args)
234-
if isinstance(info, ResolveInfo):
235-
if not info.context:
236-
info.context = Context()
237-
info.context.queryset = iterables
238-
list_length = iterables.count()
239-
else:
240-
iterables = []
241-
list_length = 0
242-
243-
connection = connection_from_list_slice(
244-
list_slice=iterables,
245-
args=connection_args,
246-
list_length=list_length,
247-
list_slice_length=list_length,
248-
connection_type=self.type,
249-
edge_type=self.type.Edge,
250-
pageinfo_type=graphene.PageInfo,
251-
)
256+
first = args.pop("first", None)
257+
after = cursor_to_offset(args.pop("after", None))
258+
last = args.pop("last", None)
259+
before = cursor_to_offset(args.pop("before", None))
260+
if after is not None:
261+
has_previous_page = after > 0
262+
elif (before is not None and last is not None):
263+
has_previous_page = before - last <= 0
264+
if "pk__in" in args and args["pk__in"]:
265+
count = len(args["pk__in"])
266+
skip, limit, reverse = find_skip_and_limit(first=first, last=last, after=after, before=before,
267+
count=count)
268+
if limit:
269+
if reverse:
270+
args["pk__in"] = args["pk__in"][::-1][skip:skip + limit]
271+
else:
272+
args["pk__in"] = args["pk__in"][skip:skip + limit]
273+
elif skip:
274+
args["pk__in"] = args["pk__in"][skip:]
275+
iterables = self.get_queryset(self.model, info, required_fields, **args)
276+
list_length = len(iterables)
277+
if isinstance(info, ResolveInfo):
278+
if not info.context:
279+
info.context = Context()
280+
info.context.queryset = self.get_queryset(self.model, info, required_fields, **args)
281+
elif _root is None:
282+
count = self.get_queryset(self.model, info, required_fields, **args).count()
283+
if count != 0:
284+
skip, limit, reverse = find_skip_and_limit(first=first, after=after, last=last, before=before,
285+
count=count)
286+
iterables = self.get_queryset(self.model, info, required_fields, skip, limit, reverse, **args)
287+
list_length = len(iterables)
288+
if isinstance(info, ResolveInfo):
289+
if not info.context:
290+
info.context = Context()
291+
info.context.queryset = self.get_queryset(self.model, info, required_fields, **args)
292+
has_next_page = True if (0 if limit is None else limit) + (0 if skip is None else skip) < count else False
293+
has_previous_page = True if skip else False
294+
if reverse:
295+
iterables = list(iterables)
296+
iterables.reverse()
297+
skip = limit
298+
connection = connection_from_iterables(edges=iterables, start_offset=skip,
299+
has_previous_page=has_previous_page,
300+
has_next_page=has_next_page,
301+
connection_type=self.type,
302+
edge_type=self.type.Edge,
303+
pageinfo_type=graphene.PageInfo)
304+
252305
connection.iterable = iterables
253306
connection.list_length = list_length
254307
return connection
@@ -280,6 +333,9 @@ def chained_resolver(self, resolver, is_partial, root, info, **args):
280333
return resolved
281334
elif not isinstance(resolved[0], DBRef):
282335
return resolved
336+
elif isinstance(resolved, QuerySet):
337+
args.update(resolved._query)
338+
return self.default_resolver(root, info, required_fields, **args)
283339
else:
284340
return resolved
285341
return self.default_resolver(root, info, required_fields, **args)

graphene_mongo/tests/test_fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,4 +56,4 @@ def test_default_resolver_connection_list_length(fixtures):
5656

5757
connection = field.default_resolver(None, {}, **{"first": 1})
5858
assert hasattr(connection, "list_length")
59-
assert connection.list_length == 3
59+
assert connection.list_length == 1

graphene_mongo/utils.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from graphene import Node
88
from graphene.utils.trim_docstring import trim_docstring
99
from graphql.utils.ast_to_dict import ast_to_dict
10+
from graphql_relay.connection.arrayconnection import offset_to_cursor
1011

1112

1213
def get_model_fields(model, excluding=None):
@@ -164,3 +165,67 @@ def get_query_fields(info):
164165
if "edges" in query:
165166
return query["edges"]["node"].keys()
166167
return query
168+
169+
170+
def find_skip_and_limit(first, last, after, before, count):
171+
reverse = False
172+
skip = 0
173+
limit = None
174+
if first is not None and after is not None:
175+
skip = after + 1
176+
limit = first
177+
elif first is not None and before is not None:
178+
if first >= before:
179+
limit = before - 1
180+
else:
181+
limit = first
182+
elif first is not None:
183+
skip = 0
184+
limit = first
185+
elif last is not None and before is not None:
186+
reverse = False
187+
if last >= before:
188+
limit = before
189+
else:
190+
limit = last
191+
skip = before - last
192+
elif last is not None and after is not None:
193+
reverse = True
194+
if last + after < count:
195+
limit = last
196+
else:
197+
limit = count - after - 1
198+
elif last is not None:
199+
skip = 0
200+
limit = last
201+
reverse = True
202+
elif after is not None:
203+
skip = after + 1
204+
elif before is not None:
205+
limit = before
206+
return skip, limit, reverse
207+
208+
209+
def connection_from_iterables(edges, start_offset, has_previous_page, has_next_page, connection_type,
210+
edge_type,
211+
pageinfo_type):
212+
edges_items = [
213+
edge_type(
214+
node=node,
215+
cursor=offset_to_cursor((0 if start_offset is None else start_offset) + i)
216+
)
217+
for i, node in enumerate(edges)
218+
]
219+
220+
first_edge_cursor = edges_items[0].cursor if edges_items else None
221+
last_edge_cursor = edges_items[-1].cursor if edges_items else None
222+
223+
return connection_type(
224+
edges=edges_items,
225+
page_info=pageinfo_type(
226+
start_cursor=first_edge_cursor,
227+
end_cursor=last_edge_cursor,
228+
has_previous_page=has_previous_page,
229+
has_next_page=has_next_page
230+
)
231+
)

0 commit comments

Comments
 (0)