@@ -568,6 +568,156 @@ to send to the client.
568568
569569
570570
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+
571721[[execution.batching]]
572722=== Batch Loading
573723
@@ -1048,6 +1198,47 @@ required fields (or columns) are part of the database query result.
10481198
10491199
10501200
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+
10511242
10521243[[controllers]]
10531244== Annotated Controllers
@@ -1218,6 +1409,16 @@ See <<controllers.schema-mapping.projectedpayload.argument>>.
12181409
12191410See <<controllers.schema-mapping.source>>.
12201411
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+
12211422| `DataLoader`
12221423| For access to a `DataLoader` in the `DataLoaderRegistry`.
12231424
@@ -1455,6 +1656,51 @@ given a list of source/parent books objects.
14551656====
14561657
14571658
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+
14581704[[controllers.schema-mapping.data-loader]]
14591705==== `DataLoader`
14601706
0 commit comments