Skip to content

Commit 8a8c6aa

Browse files
icanhasjonasPascalSenn
andauthoredJul 16, 2021
added ability to opt out from forward/backwards cursor pagination (#2528)
Co-authored-by: Pascal Senn <senn.pasc@gmail.com>
1 parent ff687a5 commit 8a8c6aa

File tree

7 files changed

+261
-21
lines changed

7 files changed

+261
-21
lines changed
 

‎src/HotChocolate/Core/src/Types.CursorPagination/Extensions/PagingObjectFieldDescriptorExtensions.cs

+59-13
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using HotChocolate.Types.Descriptors;
88
using HotChocolate.Types.Pagination;
99
using static HotChocolate.Utilities.ThrowHelper;
10+
using static HotChocolate.Types.Pagination.PagingDefaults;
1011

1112
namespace HotChocolate.Types
1213
{
@@ -41,15 +42,29 @@ public static IObjectFieldDescriptor UsePaging(
4142

4243
resolvePagingProvider ??= ResolvePagingProvider;
4344

44-
descriptor.AddPagingArguments();
45-
4645
PagingHelper.UsePaging(
4746
descriptor,
4847
type,
4948
entityType,
5049
(services, source) => resolvePagingProvider(services, source),
5150
options);
5251

52+
descriptor
53+
.Extend()
54+
.OnBeforeCreate((c, d) =>
55+
{
56+
if (!(c.ContextData.TryGetValue(typeof(PagingOptions).FullName!, out var obj) &&
57+
obj is PagingOptions pagingOptions))
58+
{
59+
pagingOptions = default;
60+
}
61+
62+
var descriptor = ObjectFieldDescriptor.From(c, d);
63+
descriptor.AddPagingArguments(
64+
pagingOptions.AllowBackwardPagination ?? AllowBackwardPagination);
65+
descriptor.CreateDefinition();
66+
});
67+
5368
descriptor
5469
.Extend()
5570
.OnBeforeCreate(
@@ -76,7 +91,22 @@ public static IInterfaceFieldDescriptor UsePaging(
7691
}
7792

7893
descriptor
79-
.AddPagingArguments()
94+
.Extend()
95+
.OnBeforeCreate((c, d) =>
96+
{
97+
if (!(c.ContextData.TryGetValue(typeof(PagingOptions).FullName!, out var obj) &&
98+
obj is PagingOptions pagingOptions))
99+
{
100+
pagingOptions = default;
101+
}
102+
103+
var descriptor = InterfaceFieldDescriptor.From(c, d);
104+
descriptor.AddPagingArguments(
105+
pagingOptions.AllowBackwardPagination ?? AllowBackwardPagination);
106+
descriptor.CreateDefinition();
107+
});
108+
109+
descriptor
80110
.Extend()
81111
.OnBeforeCreate(
82112
(c, d) => d.Type = CreateConnectionTypeRef(c, d.Member, type, options));
@@ -85,33 +115,49 @@ public static IInterfaceFieldDescriptor UsePaging(
85115
}
86116

87117
public static IObjectFieldDescriptor AddPagingArguments(
88-
this IObjectFieldDescriptor descriptor)
118+
this IObjectFieldDescriptor descriptor,
119+
bool allowBackwardPagination = true)
89120
{
90121
if (descriptor == null)
91122
{
92123
throw new ArgumentNullException(nameof(descriptor));
93124
}
94125

95-
return descriptor
126+
descriptor
96127
.Argument(CursorPagingArgumentNames.First, a => a.Type<IntType>())
97-
.Argument(CursorPagingArgumentNames.After, a => a.Type<StringType>())
98-
.Argument(CursorPagingArgumentNames.Last, a => a.Type<IntType>())
99-
.Argument(CursorPagingArgumentNames.Before, a => a.Type<StringType>());
128+
.Argument(CursorPagingArgumentNames.After, a => a.Type<StringType>());
129+
130+
if (allowBackwardPagination)
131+
{
132+
descriptor
133+
.Argument(CursorPagingArgumentNames.Last, a => a.Type<IntType>())
134+
.Argument(CursorPagingArgumentNames.Before, a => a.Type<StringType>());
135+
}
136+
137+
return descriptor;
100138
}
101139

102140
public static IInterfaceFieldDescriptor AddPagingArguments(
103-
this IInterfaceFieldDescriptor descriptor)
141+
this IInterfaceFieldDescriptor descriptor,
142+
bool allowBackwardPagination = true)
104143
{
105144
if (descriptor == null)
106145
{
107146
throw new ArgumentNullException(nameof(descriptor));
108147
}
109148

110-
return descriptor
149+
descriptor
111150
.Argument(CursorPagingArgumentNames.First, a => a.Type<IntType>())
112-
.Argument(CursorPagingArgumentNames.After, a => a.Type<StringType>())
113-
.Argument(CursorPagingArgumentNames.Last, a => a.Type<IntType>())
114-
.Argument(CursorPagingArgumentNames.Before, a => a.Type<StringType>());
151+
.Argument(CursorPagingArgumentNames.After, a => a.Type<StringType>());
152+
153+
if (allowBackwardPagination)
154+
{
155+
descriptor
156+
.Argument(CursorPagingArgumentNames.Last, a => a.Type<IntType>())
157+
.Argument(CursorPagingArgumentNames.Before, a => a.Type<StringType>());
158+
}
159+
160+
return descriptor;
115161
}
116162

117163
private static ITypeReference CreateConnectionTypeRef(

‎src/HotChocolate/Core/src/Types.CursorPagination/Extensions/UsePagingAttribute.cs

+21-7
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ public sealed class UsePagingAttribute : DescriptorAttribute
1313
private int? _defaultPageSize;
1414
private int? _maxPageSize;
1515
private bool? _includeTotalCount;
16+
private bool? _allowBackwardPagination;
1617

1718
/// <summary>
1819
/// Applies the offset paging middleware to the annotated property.
@@ -28,11 +29,13 @@ public UsePagingAttribute(Type? type = null)
2829
/// <summary>
2930
/// The schema type representation of the item type.
3031
/// </summary>
31-
32-
public Type? SchemaType
33-
{
34-
[Obsolete("Use Type.")] get => Type;
35-
[Obsolete("Use the new constructor.")] set => Type = value;
32+
33+
public Type? SchemaType
34+
{
35+
[Obsolete("Use Type.")]
36+
get => Type;
37+
[Obsolete("Use the new constructor.")]
38+
set => Type = value;
3639
}
3740

3841
/// <summary>
@@ -67,6 +70,15 @@ public bool IncludeTotalCount
6770
set => _includeTotalCount = value;
6871
}
6972

73+
/// <summary>
74+
/// Allow backward paging using <c>last</c> and <c>before</c>
75+
/// </summary>
76+
public bool AllowBackwardPagination
77+
{
78+
get => _allowBackwardPagination ?? PagingDefaults.AllowBackwardPagination;
79+
set => _allowBackwardPagination = value;
80+
}
81+
7082
protected override void TryConfigure(
7183
IDescriptorContext context,
7284
IDescriptor descriptor,
@@ -82,7 +94,8 @@ protected override void TryConfigure(
8294
{
8395
DefaultPageSize = _defaultPageSize,
8496
MaxPageSize = _maxPageSize,
85-
IncludeTotalCount = _includeTotalCount
97+
IncludeTotalCount = _includeTotalCount,
98+
AllowBackwardPagination = AllowBackwardPagination
8699
});
87100
}
88101
else if (descriptor is IInterfaceFieldDescriptor ifd)
@@ -93,7 +106,8 @@ protected override void TryConfigure(
93106
{
94107
DefaultPageSize = _defaultPageSize,
95108
MaxPageSize = _maxPageSize,
96-
IncludeTotalCount = _includeTotalCount
109+
IncludeTotalCount = _includeTotalCount,
110+
AllowBackwardPagination = AllowBackwardPagination
97111
});
98112
}
99113
}

‎src/HotChocolate/Core/src/Types/Types/Pagination/PagingDefaults.cs

+2
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,7 @@ public static class PagingDefaults
77
public const int MaxPageSize = 50;
88

99
public const bool IncludeTotalCount = false;
10+
11+
public const bool AllowBackwardPagination = true;
1012
}
1113
}

‎src/HotChocolate/Core/src/Types/Types/Pagination/PagingOptions.cs

+5
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,10 @@ public struct PagingOptions
2020
/// shall be included into the paging result type.
2121
/// </summary>
2222
public bool? IncludeTotalCount { get; set; }
23+
24+
/// <summary>
25+
/// Defines if backward pagination is allowed or deactivated
26+
/// </summary>
27+
public bool? AllowBackwardPagination { get; set; }
2328
}
2429
}

‎src/HotChocolate/Core/test/Types.CursorPagination.Tests/IntegrationTests.cs

+34-1
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,39 @@ public async Task Attribute_Interface_With_Paging_Field()
611611
schema.Print().MatchSnapshot();
612612
}
613613

614+
[Fact]
615+
public async Task Deactivate_BackwardPagination()
616+
{
617+
IRequestExecutor executor =
618+
await new ServiceCollection()
619+
.AddGraphQL()
620+
.AddQueryType<QueryType>()
621+
.SetPagingOptions(new PagingOptions { AllowBackwardPagination = false })
622+
.Services
623+
.BuildServiceProvider()
624+
.GetRequestExecutorAsync();
625+
626+
executor.Schema.Print().MatchSnapshot();
627+
}
628+
629+
[Fact]
630+
public async Task Deactivate_BackwardPagination_Interface()
631+
{
632+
Snapshot.FullName();
633+
634+
ISchema schema =
635+
await new ServiceCollection()
636+
.AddGraphQL()
637+
.AddQueryType<QueryAttr>()
638+
.SetPagingOptions(new PagingOptions { AllowBackwardPagination = false })
639+
.AddInterfaceType<ISome>(d => d.Field(t => t.ExplicitType()).UsePaging())
640+
.Services
641+
.BuildServiceProvider()
642+
.GetSchemaAsync();
643+
644+
schema.Print().MatchSnapshot();
645+
}
646+
614647
public class QueryType : ObjectType<Query>
615648
{
616649
protected override void Configure(IObjectTypeDescriptor<Query> descriptor)
@@ -755,7 +788,7 @@ public MockExecutable(IQueryable<T> source)
755788
_source = source;
756789
}
757790

758-
public object Source =>_source;
791+
public object Source => _source;
759792

760793
public ValueTask<IList> ToListAsync(CancellationToken cancellationToken)
761794
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
schema {
2+
query: Query
3+
}
4+
5+
type Foo {
6+
bar: String!
7+
}
8+
9+
"A connection to a list of items."
10+
type FooConnection {
11+
"Information to aid in pagination."
12+
pageInfo: PageInfo!
13+
"A list of edges."
14+
edges: [FooEdge!]
15+
"A flattened list of the nodes."
16+
nodes: [[Foo!]!]
17+
totalCount: Int!
18+
}
19+
20+
"An edge in a connection."
21+
type FooEdge {
22+
"A cursor for use in pagination."
23+
cursor: String!
24+
"The item at the end of the edge."
25+
node: [Foo!]!
26+
}
27+
28+
"Information about pagination in a connection."
29+
type PageInfo {
30+
"Indicates whether more edges exist following the set defined by the clients arguments."
31+
hasNextPage: Boolean!
32+
"Indicates whether more edges exist prior the set defined by the clients arguments."
33+
hasPreviousPage: Boolean!
34+
"When paginating backwards, the cursor to continue."
35+
startCursor: String
36+
"When paginating forwards, the cursor to continue."
37+
endCursor: String
38+
}
39+
40+
type Query {
41+
letters(first: Int after: String): StringConnection
42+
explicitType(first: Int after: String): StringConnection
43+
nestedObjectList(first: Int after: String): FooConnection
44+
}
45+
46+
"A connection to a list of items."
47+
type StringConnection {
48+
"Information to aid in pagination."
49+
pageInfo: PageInfo!
50+
"A list of edges."
51+
edges: [StringEdge!]
52+
"A flattened list of the nodes."
53+
nodes: [String!]
54+
}
55+
56+
"An edge in a connection."
57+
type StringEdge {
58+
"A cursor for use in pagination."
59+
cursor: String!
60+
"The item at the end of the edge."
61+
node: String!
62+
}
63+
64+
"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`."
65+
directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT
66+
67+
"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`."
68+
directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! "Streamed when true." if: Boolean!) on FIELD
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
schema {
2+
query: QueryAttr
3+
}
4+
5+
interface ISome {
6+
explicitType(first: Int after: String): StringConnection
7+
}
8+
9+
type Foo {
10+
bar: String!
11+
}
12+
13+
"A connection to a list of items."
14+
type FooConnection {
15+
"Information to aid in pagination."
16+
pageInfo: PageInfo!
17+
"A list of edges."
18+
edges: [FooEdge!]
19+
"A flattened list of the nodes."
20+
nodes: [[Foo!]!]
21+
totalCount: Int!
22+
}
23+
24+
"An edge in a connection."
25+
type FooEdge {
26+
"A cursor for use in pagination."
27+
cursor: String!
28+
"The item at the end of the edge."
29+
node: [Foo!]!
30+
}
31+
32+
"Information about pagination in a connection."
33+
type PageInfo {
34+
"Indicates whether more edges exist following the set defined by the clients arguments."
35+
hasNextPage: Boolean!
36+
"Indicates whether more edges exist prior the set defined by the clients arguments."
37+
hasPreviousPage: Boolean!
38+
"When paginating backwards, the cursor to continue."
39+
startCursor: String
40+
"When paginating forwards, the cursor to continue."
41+
endCursor: String
42+
}
43+
44+
type QueryAttr {
45+
nestedObjectList(first: Int after: String): FooConnection
46+
letters(first: Int after: String): StringConnection
47+
explicitType(first: Int after: String): StringConnection
48+
}
49+
50+
"A connection to a list of items."
51+
type StringConnection {
52+
"Information to aid in pagination."
53+
pageInfo: PageInfo!
54+
"A list of edges."
55+
edges: [StringEdge!]
56+
"A flattened list of the nodes."
57+
nodes: [String!]
58+
}
59+
60+
"An edge in a connection."
61+
type StringEdge {
62+
"A cursor for use in pagination."
63+
cursor: String!
64+
"The item at the end of the edge."
65+
node: String!
66+
}
67+
68+
"The `@defer` directive may be provided for fragment spreads and inline fragments to inform the executor to delay the execution of the current fragment to indicate deprioritization of the current fragment. A query with `@defer` directive will cause the request to potentially return multiple responses, where non-deferred data is delivered in the initial response and data deferred is delivered in a subsequent response. `@include` and `@skip` take precedence over `@defer`."
69+
directive @defer("If this argument label has a value other than null, it will be passed on to the result of this defer directive. This label is intended to give client applications a way to identify to which fragment a deferred result belongs to." label: String "Deferred when true." if: Boolean) on FRAGMENT_SPREAD | INLINE_FRAGMENT
70+
71+
"The `@stream` directive may be provided for a field of `List` type so that the backend can leverage technology such as asynchronous iterators to provide a partial list in the initial response, and additional list items in subsequent responses. `@include` and `@skip` take precedence over `@stream`."
72+
directive @stream("If this argument label has a value other than null, it will be passed on to the result of this stream directive. This label is intended to give client applications a way to identify to which fragment a streamed result belongs to." label: String "The initial elements that shall be send down to the consumer." initialCount: Int! "Streamed when true." if: Boolean!) on FIELD

0 commit comments

Comments
 (0)
Please sign in to comment.