@@ -568,6 +568,156 @@ to send to the client.
568
568
569
569
570
570
571
+ [[execution.pagination]]
572
+ === Pagination
573
+
574
+ The https://relay.dev/graphql/connections.htm[GraphQL Cursor Connection specification]
575
+ defines a mechanism for efficient navigation of large result sets by returning a limited
576
+ subset of items at a time. Each item is assigned a unique cursor that a client can use to
577
+ request the next items after or the previous items before the cursor reference, as a way of
578
+ navigating forward or backward.
579
+
580
+ The spec calls this pattern "Connections", and each schema type whose name ends on
581
+ "Connection" is considered a _Connection Type_ and represents a paginated result set.
582
+ Each `Connection` contains "edges" where an `EdgeType` is a wrapper around the actual item
583
+ and its cursor. There is also a `PageInfo` object with flags for whether you can navigate
584
+ further forward and backward and the cursors of the start and end items in the set.
585
+
586
+
587
+ [[execution.pagination.type.definitions]]
588
+ ==== Connection Type Definitions
589
+
590
+ `Connection` type definitions must be repeated for every type that needs pagination, adding
591
+ boilerplate and noise to the schema. To address this, Spring for GraphQL provides the
592
+ `ConnectionTypeDefinitionConfigurer` that adds these types on startup, if not already
593
+ present in the parsed schema files.
594
+
595
+ That means you can declare `Connection` fields, but leave out their declaration:
596
+
597
+ [source,graphql,indent=0,subs="verbatim,quotes"]
598
+ ----
599
+ Query {
600
+ books: BookConnection
601
+ }
602
+
603
+ type Book {
604
+ id: ID!
605
+ title: String!
606
+ }
607
+ ----
608
+
609
+ Then configure the `ConnectionTypeDefinitionConfigurer`:
610
+
611
+ [source,java,indent=0,subs="verbatim,quotes"]
612
+ ----
613
+ GraphQlSource.schemaResourceBuilder()
614
+ .schemaResources(..)
615
+ .typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer)
616
+ ----
617
+
618
+ The following type definitions are added on startup to the schema:
619
+
620
+ [source,graphql,indent=0,subs="verbatim,quotes"]
621
+ ----
622
+ type BookConnection {
623
+ edges: [BookEdge]!
624
+ pageInfo: PageInfo!
625
+ }
626
+
627
+ type BookEdge {
628
+ node: Book!
629
+ cursor: String!
630
+ }
631
+
632
+ type PageInfo {
633
+ hasPreviousPage: Boolean!
634
+ hasNextPage: Boolean!
635
+ startCursor: String
636
+ endCursor: String
637
+ }
638
+ ----
639
+
640
+
641
+ [[execution.pagination.adapters]]
642
+ ==== Connection Adapters
643
+
644
+ Once <<execution.pagination.type.definitions>> are available in the schema, you also need
645
+ equivalent Java types. GraphQL Java provides those, including generic `Connection` and
646
+ `Edge` types, as well as `PageInfo`.
647
+
648
+ One option is to populate and return `Connection` directly from your controller method or
649
+ `DataFetcher`. However, this is boilerplate work, to wrap each item, create cursors, and
650
+ so on. Moreover, you may already have an underlying pagination mechanism such as when
651
+ using Spring Data repositories.
652
+
653
+ To make this transparent, Spring for GraphQL has a `ConnectionAdapter` contract to adapt
654
+ any container of items to `Connection`. This is applied through a
655
+ `ConnectionFieldTypeVisitor` that looks for any `Connection` field, decorates the
656
+ registered `DataFetcher`, and adapts its return values.
657
+
658
+ For example:
659
+
660
+ [source,java,indent=0,subs="verbatim,quotes"]
661
+ ----
662
+ ConnectionAdapter adapter = ... ;
663
+ GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(adapter)) // <1>
664
+
665
+ GraphQlSource.schemaResourceBuilder()
666
+ .schemaResources(..)
667
+ .typeDefinitionConfigurer(..)
668
+ .typeVisitors(List.of(visitor)) // <2>
669
+ ----
670
+
671
+ <1> Create type visitor with one or more `Connection` adapters.
672
+ <2> Resister the type visitor.
673
+
674
+ There are <<data.scroll.sort,built-in>> ``ConnectionAdapter``s for the Spring Data
675
+ pagination types `Window` and `Slice`. You can also create your own custom adapter.
676
+
677
+ `ConnectionAdapter` implementations rely on a <<execution.pagination.cursor.strategy>> to create a cursor for
678
+ each returned item. , and the same strategy is also used subsequently to decode the cursor
679
+ to support the <<controllers.schema-mapping.subrange>> controller method argument .
680
+
681
+
682
+ [[execution.pagination.cursor.strategy]]
683
+ ==== Cursor Strategy
684
+
685
+ `CursorStrategy` is a contract to create a String cursor for an item to reflect its
686
+ position within a large result set, e.g. based on an offset or key set.
687
+ <<execution.pagination.adapters>> use this to create a cursor for returned items.
688
+
689
+ The strategy also helps to decode a cursor back to an item position. For this to work,
690
+ you need to declare a `CursorStrategy` bean, and ensure that annotated controllers are
691
+ <<controllers-declaration, enabled>>.
692
+
693
+ `CursorEncoder` is a related, supporting strategy to encode and decode cursors to make
694
+ them opaque to clients. `EncodingCursorStrategy` combines `CursorStrategy` with a
695
+ `CursorEncoder`. There is a built-in `Base64CursorEncoder`.
696
+
697
+ There is a <<data.scroll.sort,built-in>> `CursorStrategy` for the Spring Data `ScrollPosition`.
698
+
699
+
700
+ [[execution.pagination.arguments]]
701
+ ==== Arguments
702
+
703
+ Controller methods can declare a <<controllers.schema-mapping.subrange>>, or a
704
+ `ScrollSubange` method argument, to handle requests for forward or backward pagination.
705
+ The method argument resolver is configured for use when a
706
+ <<execution.pagination.cursor.strategy>> bean is declared in Spring configuration.
707
+
708
+
709
+ [[execution.pagination.sort.strategy]]
710
+ ==== Sort
711
+
712
+ Pagination depends on a stable sort order. There is no standard for how to declare sort
713
+ related GraphQL input arguments. You can keep it as an internal detail with a default
714
+ sort, or if it you need to expose it, then you'll need to extract the sort details from
715
+ GraphQL arguments.
716
+
717
+ There is partial, <<data.scroll.sort,built-in>> support for to create a Spring Data
718
+ `Sort`, with the help of a `SortStrategy`, and inject that into a controller method.
719
+
720
+
571
721
[[execution.batching]]
572
722
=== Batch Loading
573
723
@@ -1048,6 +1198,47 @@ required fields (or columns) are part of the database query result.
1048
1198
1049
1199
1050
1200
1201
+ [[data.scroll.sort]]
1202
+ === Scroll and Sort
1203
+
1204
+ As explained in <<execution.pagination>>, the GraphQL Cursor Connection spec defines a
1205
+ mechanism for pagination with the `Connection`, `Edge`, and `PageInfo` schema type, while
1206
+ GraphQL Java provides the equivalent Java type representations.
1207
+
1208
+ Spring for GraphQL has built-in ``ConnectionAdapter``s to adapt the Spring Data pagination
1209
+ types `Window` and `Slice` transparently. You can configure that as follows:
1210
+
1211
+ [source,java,indent=0,subs="verbatim,quotes"]
1212
+ ----
1213
+ CursorStrategy<ScrollPosition> strategy = CursorStrategy.withEncoder(
1214
+ new ScrollPositionCursorStrategy(),
1215
+ CursorEncoder.base64()); // <1>
1216
+
1217
+ GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(
1218
+ new WindowConnectionAdapter(strategy),
1219
+ new SliceConnectionAdapter(strategy))); // <2>
1220
+
1221
+ GraphQlSource.schemaResourceBuilder()
1222
+ .schemaResources(..)
1223
+ .typeDefinitionConfigurer(..)
1224
+ .typeVisitors(List.of(visitor)); // <3>
1225
+ ----
1226
+
1227
+ <1> Create strategy to convert `ScrollPosition` to a Base64 encoded cursor.
1228
+ <2> Create type visitor to adapt `Window` and `Slice` returned from ``DataFetcher``s.
1229
+ <3> Register the type visitor.
1230
+
1231
+ On the request side, a controller method can declare a
1232
+ <<controllers.schema-mapping.subrange,ScrollSubrange>> method argument to paginate forward
1233
+ or backward. For this to work, you must declare a <<execution.pagination.cursor.strategy>>
1234
+ supports `ScrollPosition` as a bean.
1235
+
1236
+ Spring for GraphQL defines a `SortStrategy` to create `Sort` from GraphQL arguments.
1237
+ `AbstractSortStrategy` implements the contract with abstract methods to extract the sort
1238
+ direction and properties. To enable support for `Sort` as a controller method argument,
1239
+ you need to declare a `SortStrategy` bean.
1240
+
1241
+
1051
1242
1052
1243
[[controllers]]
1053
1244
== Annotated Controllers
@@ -1218,6 +1409,16 @@ See <<controllers.schema-mapping.projectedpayload.argument>>.
1218
1409
1219
1410
See <<controllers.schema-mapping.source>>.
1220
1411
1412
+ | `Subrange` and `ScrollSubrange`
1413
+ | For access to pagination arguments.
1414
+
1415
+ See <<execution.pagination>>, <<data.scroll.sort>>, <<controllers.schema-mapping.subrange>>.
1416
+
1417
+ | `Sort`
1418
+ | For access to sort details.
1419
+
1420
+ See <<execution.pagination>>, <<data.scroll.sort>>, <<controllers.schema-mapping.sort>>.
1421
+
1221
1422
| `DataLoader`
1222
1423
| For access to a `DataLoader` in the `DataLoaderRegistry`.
1223
1424
@@ -1455,6 +1656,51 @@ given a list of source/parent books objects.
1455
1656
====
1456
1657
1457
1658
1659
+ [[controllers.schema-mapping.subrange]]
1660
+ ==== `Subrange`
1661
+
1662
+ When there is a <<execution.pagination.cursor.strategy>> bean in Spring configuration,
1663
+ controller methods support a `Subrange<P>` argument where `<P>` is a relative position
1664
+ converted from a cursor. For Spring Data, `ScrollSubrange` exposes `ScrollPosition`.
1665
+ For example:
1666
+
1667
+ [source,java,indent=0,subs="verbatim,quotes"]
1668
+ ----
1669
+ @Controller
1670
+ public class BookController {
1671
+
1672
+ @QueryMapping
1673
+ public Window<Book> books(ScrollSubrange subrange) {
1674
+ ScrollPosition position = subrange.position().orElse(OffsetScrollPosition.initial())
1675
+ int count = subrange.count().orElse(20);
1676
+ // ...
1677
+ }
1678
+
1679
+ }
1680
+ ----
1681
+
1682
+
1683
+ [[controllers.schema-mapping.sort]]
1684
+ ==== `Sort`
1685
+
1686
+ When there is a <<data.scroll.sort,SortStrategy>> bean in Spring configuration, controller
1687
+ methods support `Sort` as a method argument. For example:
1688
+
1689
+ [source,java,indent=0,subs="verbatim,quotes"]
1690
+ ----
1691
+ @Controller
1692
+ public class BookController {
1693
+
1694
+ @QueryMapping
1695
+ public Window<Book> books(Optional<Sort> optionalSort) {
1696
+ Sort sort = optionalSort.orElse(Sort.by(..));
1697
+ }
1698
+
1699
+ }
1700
+ ----
1701
+
1702
+
1703
+
1458
1704
[[controllers.schema-mapping.data-loader]]
1459
1705
==== `DataLoader`
1460
1706
0 commit comments