Added RELAY_CONNECTION_MAX_LIMIT and RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST settings

Relay connections will be limited to 100 records by default.
This commit is contained in:
Syrus Akbary 2017-04-15 02:09:05 -07:00
parent 3803e9a762
commit 46a1ddedd8
4 changed files with 161 additions and 6 deletions

View File

@ -6,6 +6,7 @@ from graphene.types import Field, List
from graphene.relay import ConnectionField, PageInfo from graphene.relay import ConnectionField, PageInfo
from graphql_relay.connection.arrayconnection import connection_from_list_slice from graphql_relay.connection.arrayconnection import connection_from_list_slice
from .settings import graphene_settings
from .utils import DJANGO_FILTER_INSTALLED, maybe_queryset from .utils import DJANGO_FILTER_INSTALLED, maybe_queryset
@ -30,6 +31,14 @@ class DjangoConnectionField(ConnectionField):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.on = kwargs.pop('on', False) self.on = kwargs.pop('on', False)
self.max_limit = kwargs.pop(
'max_limit',
graphene_settings.RELAY_CONNECTION_MAX_LIMIT
)
self.enforce_first_or_last = kwargs.pop(
'enforce_first_or_last',
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST
)
super(DjangoConnectionField, self).__init__(*args, **kwargs) super(DjangoConnectionField, self).__init__(*args, **kwargs)
@property @property
@ -51,7 +60,29 @@ class DjangoConnectionField(ConnectionField):
return default_queryset & queryset return default_queryset & queryset
@classmethod @classmethod
def connection_resolver(cls, resolver, connection, default_manager, root, args, context, info): def connection_resolver(cls, resolver, connection, default_manager, max_limit,
enforce_first_or_last, root, args, context, info):
first = args.get('first')
last = args.get('last')
if enforce_first_or_last:
assert first or last, (
'You must provide a `first` or `last` value to properly paginate the `{}` connection.'
).format(info.field_name)
if max_limit:
if first:
assert first <= max_limit, (
'Requesting {} records on the `{}` connection exceeds the `first` limit of {} records.'
).format(first, info.field_name, max_limit)
args['first'] = min(first, max_limit)
if last:
assert last <= max_limit, (
'Requesting {} records on the `{}` connection exceeds the `last` limit of {} records.'
).format(first, info.field_name, max_limit)
args['last'] = min(last, max_limit)
iterable = resolver(root, args, context, info) iterable = resolver(root, args, context, info)
if iterable is None: if iterable is None:
iterable = default_manager iterable = default_manager
@ -78,7 +109,14 @@ class DjangoConnectionField(ConnectionField):
return connection return connection
def get_resolver(self, parent_resolver): def get_resolver(self, parent_resolver):
return partial(self.connection_resolver, parent_resolver, self.type, self.get_manager()) return partial(
self.connection_resolver,
parent_resolver,
self.type,
self.get_manager(),
self.max_limit,
self.enforce_first_or_last
)
def get_connection_field(*args, **kwargs): def get_connection_field(*args, **kwargs):

View File

@ -67,16 +67,35 @@ class DjangoFilterConnectionField(DjangoConnectionField):
return queryset return queryset
@classmethod @classmethod
def connection_resolver(cls, resolver, connection, default_manager, filterset_class, filtering_args, def connection_resolver(cls, resolver, connection, default_manager, max_limit,
enforce_first_or_last, filterset_class, filtering_args,
root, args, context, info): root, args, context, info):
filter_kwargs = {k: v for k, v in args.items() if k in filtering_args} filter_kwargs = {k: v for k, v in args.items() if k in filtering_args}
qs = filterset_class( qs = filterset_class(
data=filter_kwargs, data=filter_kwargs,
queryset=default_manager.get_queryset() queryset=default_manager.get_queryset()
).qs ).qs
return super(DjangoFilterConnectionField, cls).connection_resolver( return super(DjangoFilterConnectionField, cls).connection_resolver(
resolver, connection, qs, root, args, context, info) resolver,
connection,
qs,
max_limit,
enforce_first_or_last,
root,
args,
context,
info
)
def get_resolver(self, parent_resolver): def get_resolver(self, parent_resolver):
return partial(self.connection_resolver, parent_resolver, self.type, self.get_manager(), return partial(
self.filterset_class, self.filtering_args) self.connection_resolver,
parent_resolver,
self.type,
self.get_manager(),
self.max_limit,
self.enforce_first_or_last,
self.filterset_class,
self.filtering_args
)

View File

@ -30,6 +30,11 @@ DEFAULTS = {
'SCHEMA_OUTPUT': 'schema.json', 'SCHEMA_OUTPUT': 'schema.json',
'SCHEMA_INDENT': None, 'SCHEMA_INDENT': None,
'MIDDLEWARE': (), 'MIDDLEWARE': (),
# Set to True if the connection fields must have
# either the first or last argument
'RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST': False,
# Max items returned in ConnectionFields / FilterConnectionFields
'RELAY_CONNECTION_MAX_LIMIT': 100,
} }
if settings.DEBUG: if settings.DEBUG:

View File

@ -12,6 +12,7 @@ from ..utils import DJANGO_FILTER_INSTALLED
from ..compat import MissingType, JSONField from ..compat import MissingType, JSONField
from ..fields import DjangoConnectionField from ..fields import DjangoConnectionField
from ..types import DjangoObjectType from ..types import DjangoObjectType
from ..settings import graphene_settings
from .models import Article, Reporter from .models import Article, Reporter
pytestmark = pytest.mark.django_db pytestmark = pytest.mark.django_db
@ -452,3 +453,95 @@ def test_should_query_node_multiple_filtering():
result = schema.execute(query) result = schema.execute(query)
assert not result.errors assert not result.errors
assert result.data == expected assert result.data == expected
def test_should_enforce_first_or_last():
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = True
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
r = Reporter.objects.create(
first_name='John',
last_name='Doe',
email='johndoe@example.com',
a_choice=1
)
schema = graphene.Schema(query=Query)
query = '''
query NodeFilteringQuery {
allReporters {
edges {
node {
id
}
}
}
}
'''
expected = {
'allReporters': None
}
result = schema.execute(query)
assert len(result.errors) == 1
assert str(result.errors[0]) == (
'You must provide a `first` or `last` value to properly '
'paginate the `allReporters` connection.'
)
assert result.data == expected
def test_should_error_if_first_is_greater_than_max():
graphene_settings.RELAY_CONNECTION_MAX_LIMIT = 100
class ReporterType(DjangoObjectType):
class Meta:
model = Reporter
interfaces = (Node, )
class Query(graphene.ObjectType):
all_reporters = DjangoConnectionField(ReporterType)
r = Reporter.objects.create(
first_name='John',
last_name='Doe',
email='johndoe@example.com',
a_choice=1
)
schema = graphene.Schema(query=Query)
query = '''
query NodeFilteringQuery {
allReporters(first: 101) {
edges {
node {
id
}
}
}
}
'''
expected = {
'allReporters': None
}
result = schema.execute(query)
assert len(result.errors) == 1
assert str(result.errors[0]) == (
'Requesting 101 records on the `allReporters` connection '
'exceeds the `first` limit of 100 records.'
)
assert result.data == expected
graphene_settings.RELAY_CONNECTION_ENFORCE_FIRST_OR_LAST = False