Skip to content

Commit 46a1dde

Browse files
committed
Added RELAY_CONNECTION_MAX_LIMIT and RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST settings
Relay connections will be limited to 100 records by default.
1 parent 3803e9a commit 46a1dde

File tree

4 files changed

+161
-6
lines changed

4 files changed

+161
-6
lines changed

graphene_django/fields.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from graphene.relay import ConnectionField, PageInfo
77
from graphql_relay.connection.arrayconnection import connection_from_list_slice
88

9+
from .settings import graphene_settings
910
from .utils import DJANGO_FILTER_INSTALLED, maybe_queryset
1011

1112

@@ -30,6 +31,14 @@ class DjangoConnectionField(ConnectionField):
3031

3132
def __init__(self, *args, **kwargs):
3233
self.on = kwargs.pop('on', False)
34+
self.max_limit = kwargs.pop(
35+
'max_limit',
36+
graphene_settings.RELAY_CONNECTION_MAX_LIMIT
37+
)
38+
self.enforce_first_or_last = kwargs.pop(
39+
'enforce_first_or_last',
40+
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST
41+
)
3342
super(DjangoConnectionField, self).__init__(*args, **kwargs)
3443

3544
@property
@@ -51,7 +60,29 @@ def merge_querysets(cls, default_queryset, queryset):
5160
return default_queryset & queryset
5261

5362
@classmethod
54-
def connection_resolver(cls, resolver, connection, default_manager, root, args, context, info):
63+
def connection_resolver(cls, resolver, connection, default_manager, max_limit,
64+
enforce_first_or_last, root, args, context, info):
65+
first = args.get('first')
66+
last = args.get('last')
67+
68+
if enforce_first_or_last:
69+
assert first or last, (
70+
'You must provide a `first` or `last` value to properly paginate the `{}` connection.'
71+
).format(info.field_name)
72+
73+
if max_limit:
74+
if first:
75+
assert first <= max_limit, (
76+
'Requesting {} records on the `{}` connection exceeds the `first` limit of {} records.'
77+
).format(first, info.field_name, max_limit)
78+
args['first'] = min(first, max_limit)
79+
80+
if last:
81+
assert last <= max_limit, (
82+
'Requesting {} records on the `{}` connection exceeds the `last` limit of {} records.'
83+
).format(first, info.field_name, max_limit)
84+
args['last'] = min(last, max_limit)
85+
5586
iterable = resolver(root, args, context, info)
5687
if iterable is None:
5788
iterable = default_manager
@@ -78,7 +109,14 @@ def connection_resolver(cls, resolver, connection, default_manager, root, args,
78109
return connection
79110

80111
def get_resolver(self, parent_resolver):
81-
return partial(self.connection_resolver, parent_resolver, self.type, self.get_manager())
112+
return partial(
113+
self.connection_resolver,
114+
parent_resolver,
115+
self.type,
116+
self.get_manager(),
117+
self.max_limit,
118+
self.enforce_first_or_last
119+
)
82120

83121

84122
def get_connection_field(*args, **kwargs):

graphene_django/filter/fields.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,16 +67,35 @@ def merge_querysets(default_queryset, queryset):
6767
return queryset
6868

6969
@classmethod
70-
def connection_resolver(cls, resolver, connection, default_manager, filterset_class, filtering_args,
70+
def connection_resolver(cls, resolver, connection, default_manager, max_limit,
71+
enforce_first_or_last, filterset_class, filtering_args,
7172
root, args, context, info):
7273
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
7374
qs = filterset_class(
7475
data=filter_kwargs,
7576
queryset=default_manager.get_queryset()
7677
).qs
78+
7779
return super(DjangoFilterConnectionField, cls).connection_resolver(
78-
resolver, connection, qs, root, args, context, info)
80+
resolver,
81+
connection,
82+
qs,
83+
max_limit,
84+
enforce_first_or_last,
85+
root,
86+
args,
87+
context,
88+
info
89+
)
7990

8091
def get_resolver(self, parent_resolver):
81-
return partial(self.connection_resolver, parent_resolver, self.type, self.get_manager(),
82-
self.filterset_class, self.filtering_args)
92+
return partial(
93+
self.connection_resolver,
94+
parent_resolver,
95+
self.type,
96+
self.get_manager(),
97+
self.max_limit,
98+
self.enforce_first_or_last,
99+
self.filterset_class,
100+
self.filtering_args
101+
)

graphene_django/settings.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@
3030
'SCHEMA_OUTPUT': 'schema.json',
3131
'SCHEMA_INDENT': None,
3232
'MIDDLEWARE': (),
33+
# Set to True if the connection fields must have
34+
# either the first or last argument
35+
'RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST': False,
36+
# Max items returned in ConnectionFields / FilterConnectionFields
37+
'RELAY_CONNECTION_MAX_LIMIT': 100,
3338
}
3439

3540
if settings.DEBUG:

graphene_django/tests/test_query.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ..compat import MissingType, JSONField
1313
from ..fields import DjangoConnectionField
1414
from ..types import DjangoObjectType
15+
from ..settings import graphene_settings
1516
from .models import Article, Reporter
1617

1718
pytestmark = pytest.mark.django_db
@@ -452,3 +453,95 @@ class Query(graphene.ObjectType):
452453
result = schema.execute(query)
453454
assert not result.errors
454455
assert result.data == expected
456+
457+
458+
def test_should_enforce_first_or_last():
459+
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = True
460+
461+
class ReporterType(DjangoObjectType):
462+
463+
class Meta:
464+
model = Reporter
465+
interfaces = (Node, )
466+
467+
class Query(graphene.ObjectType):
468+
all_reporters = DjangoConnectionField(ReporterType)
469+
470+
r = Reporter.objects.create(
471+
first_name='John',
472+
last_name='Doe',
473+
email='johndoe@example.com',
474+
a_choice=1
475+
)
476+
477+
schema = graphene.Schema(query=Query)
478+
query = '''
479+
query NodeFilteringQuery {
480+
allReporters {
481+
edges {
482+
node {
483+
id
484+
}
485+
}
486+
}
487+
}
488+
'''
489+
490+
expected = {
491+
'allReporters': None
492+
}
493+
494+
result = schema.execute(query)
495+
assert len(result.errors) == 1
496+
assert str(result.errors[0]) == (
497+
'You must provide a `first` or `last` value to properly '
498+
'paginate the `allReporters` connection.'
499+
)
500+
assert result.data == expected
501+
502+
503+
def test_should_error_if_first_is_greater_than_max():
504+
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100
505+
506+
class ReporterType(DjangoObjectType):
507+
508+
class Meta:
509+
model = Reporter
510+
interfaces = (Node, )
511+
512+
class Query(graphene.ObjectType):
513+
all_reporters = DjangoConnectionField(ReporterType)
514+
515+
r = Reporter.objects.create(
516+
first_name='John',
517+
last_name='Doe',
518+
email='johndoe@example.com',
519+
a_choice=1
520+
)
521+
522+
schema = graphene.Schema(query=Query)
523+
query = '''
524+
query NodeFilteringQuery {
525+
allReporters(first: 101) {
526+
edges {
527+
node {
528+
id
529+
}
530+
}
531+
}
532+
}
533+
'''
534+
535+
expected = {
536+
'allReporters': None
537+
}
538+
539+
result = schema.execute(query)
540+
assert len(result.errors) == 1
541+
assert str(result.errors[0]) == (
542+
'Requesting 101 records on the `allReporters` connection '
543+
'exceeds the `first` limit of 100 records.'
544+
)
545+
assert result.data == expected
546+
547+
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False

0 commit comments

Comments
 (0)