diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index 8d50f979..908cda43 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -568,6 +568,156 @@ to send to the client. +[[execution.pagination]] +=== Pagination + +The https://relay.dev/graphql/connections.htm[GraphQL Cursor Connection specification] +defines a mechanism for efficient navigation of large result sets by returning a limited +subset of items at a time. Each item is assigned a unique cursor that a client can use to +request the next items after or the previous items before the cursor reference, as a way of +navigating forward or backward. + +The spec calls this pattern "Connections", and each schema type whose name ends on +"Connection" is considered a _Connection Type_ and represents a paginated result set. +Each `Connection` contains "edges" where an `EdgeType` is a wrapper around the actual item +and its cursor. There is also a `PageInfo` object with flags for whether you can navigate +further forward and backward and the cursors of the start and end items in the set. + + +[[execution.pagination.type.definitions]] +==== Connection Type Definitions + +`Connection` type definitions must be repeated for every type that needs pagination, adding +boilerplate and noise to the schema. To address this, Spring for GraphQL provides the +`ConnectionTypeDefinitionConfigurer` that adds these types on startup, if not already +present in the parsed schema files. + +That means you can declare `Connection` fields, but leave out their declaration: + +[source,graphql,indent=0,subs="verbatim,quotes"] +---- + Query { + books: BookConnection + } + + type Book { + id: ID! + title: String! + } +---- + +Then configure the `ConnectionTypeDefinitionConfigurer`: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +GraphQlSource.schemaResourceBuilder() + .schemaResources(..) + .typeDefinitionConfigurer(new ConnectionTypeDefinitionConfigurer) +---- + +The following type definitions are added on startup to the schema: + +[source,graphql,indent=0,subs="verbatim,quotes"] +---- + type BookConnection { + edges: [BookEdge]! + pageInfo: PageInfo! + } + + type BookEdge { + node: Book! + cursor: String! + } + + type PageInfo { + hasPreviousPage: Boolean! + hasNextPage: Boolean! + startCursor: String + endCursor: String + } +---- + + +[[execution.pagination.adapters]] +==== Connection Adapters + +Once <> are available in the schema, you also need +equivalent Java types. GraphQL Java provides those, including generic `Connection` and +`Edge` types, as well as `PageInfo`. + +One option is to populate and return `Connection` directly from your controller method or +`DataFetcher`. However, this is boilerplate work, to wrap each item, create cursors, and +so on. Moreover, you may already have an underlying pagination mechanism such as when +using Spring Data repositories. + +To make this transparent, Spring for GraphQL has a `ConnectionAdapter` contract to adapt +any container of items to `Connection`. This is applied through a +`ConnectionFieldTypeVisitor` that looks for any `Connection` field, decorates the +registered `DataFetcher`, and adapts its return values. + +For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +ConnectionAdapter adapter = ... ; +GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of(adapter)) // <1> + +GraphQlSource.schemaResourceBuilder() + .schemaResources(..) + .typeDefinitionConfigurer(..) + .typeVisitors(List.of(visitor)) // <2> +---- + +<1> Create type visitor with one or more `Connection` adapters. +<2> Resister the type visitor. + +There are <> ``ConnectionAdapter``s for the Spring Data +pagination types `Window` and `Slice`. You can also create your own custom adapter. + +`ConnectionAdapter` implementations rely on a <> to create a cursor for +each returned item. , and the same strategy is also used subsequently to decode the cursor +to support the <> controller method argument . + + +[[execution.pagination.cursor.strategy]] +==== Cursor Strategy + +`CursorStrategy` is a contract to create a String cursor for an item to reflect its +position within a large result set, e.g. based on an offset or key set. +<> use this to create a cursor for returned items. + +The strategy also helps to decode a cursor back to an item position. For this to work, +you need to declare a `CursorStrategy` bean, and ensure that annotated controllers are +<>. + +`CursorEncoder` is a related, supporting strategy to encode and decode cursors to make +them opaque to clients. `EncodingCursorStrategy` combines `CursorStrategy` with a +`CursorEncoder`. There is a built-in `Base64CursorEncoder`. + +There is a <> `CursorStrategy` for the Spring Data `ScrollPosition`. + + +[[execution.pagination.arguments]] +==== Arguments + +Controller methods can declare a <>, or a +`ScrollSubange` method argument, to handle requests for forward or backward pagination. +The method argument resolver is configured for use when a +<> bean is declared in Spring configuration. + + +[[execution.pagination.sort.strategy]] +==== Sort + +Pagination depends on a stable sort order. There is no standard for how to declare sort +related GraphQL input arguments. You can keep it as an internal detail with a default +sort, or if it you need to expose it, then you'll need to extract the sort details from +GraphQL arguments. + +There is partial, <> support for to create a Spring Data +`Sort`, with the help of a `SortStrategy`, and inject that into a controller method. + + [[execution.batching]] === Batch Loading @@ -1048,6 +1198,47 @@ required fields (or columns) are part of the database query result. +[[data.scroll.sort]] +=== Scroll and Sort + +As explained in <>, the GraphQL Cursor Connection spec defines a +mechanism for pagination with the `Connection`, `Edge`, and `PageInfo` schema type, while +GraphQL Java provides the equivalent Java type representations. + +Spring for GraphQL has built-in ``ConnectionAdapter``s to adapt the Spring Data pagination +types `Window` and `Slice` transparently. You can configure that as follows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +CursorStrategy strategy = CursorStrategy.withEncoder( + new ScrollPositionCursorStrategy(), + CursorEncoder.base64()); // <1> + +GraphQLTypeVisitor visitor = ConnectionFieldTypeVisitor.create(List.of( + new WindowConnectionAdapter(strategy), + new SliceConnectionAdapter(strategy))); // <2> + +GraphQlSource.schemaResourceBuilder() + .schemaResources(..) + .typeDefinitionConfigurer(..) + .typeVisitors(List.of(visitor)); // <3> +---- + +<1> Create strategy to convert `ScrollPosition` to a Base64 encoded cursor. +<2> Create type visitor to adapt `Window` and `Slice` returned from ``DataFetcher``s. +<3> Register the type visitor. + +On the request side, a controller method can declare a +<> method argument to paginate forward +or backward. For this to work, you must declare a <> +supports `ScrollPosition` as a bean. + +Spring for GraphQL defines a `SortStrategy` to create `Sort` from GraphQL arguments. +`AbstractSortStrategy` implements the contract with abstract methods to extract the sort +direction and properties. To enable support for `Sort` as a controller method argument, +you need to declare a `SortStrategy` bean. + + [[controllers]] == Annotated Controllers @@ -1218,6 +1409,16 @@ See <>. See <>. +| `Subrange` and `ScrollSubrange` +| For access to pagination arguments. + +See <>, <>, <>. + +| `Sort` +| For access to sort details. + +See <>, <>, <>. + | `DataLoader` | For access to a `DataLoader` in the `DataLoaderRegistry`. @@ -1455,6 +1656,51 @@ given a list of source/parent books objects. ==== +[[controllers.schema-mapping.subrange]] +==== `Subrange` + +When there is a <> bean in Spring configuration, +controller methods support a `Subrange

` argument where `

` is a relative position +converted from a cursor. For Spring Data, `ScrollSubrange` exposes `ScrollPosition`. +For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Controller +public class BookController { + + @QueryMapping + public Window books(ScrollSubrange subrange) { + ScrollPosition position = subrange.position().orElse(OffsetScrollPosition.initial()) + int count = subrange.count().orElse(20); + // ... + } + +} +---- + + +[[controllers.schema-mapping.sort]] +==== `Sort` + +When there is a <> bean in Spring configuration, controller +methods support `Sort` as a method argument. For example: + +[source,java,indent=0,subs="verbatim,quotes"] +---- +@Controller +public class BookController { + + @QueryMapping + public Window books(Optional optionalSort) { + Sort sort = optionalSort.orElse(Sort.by(..)); + } + +} +---- + + + [[controllers.schema-mapping.data-loader]] ==== `DataLoader` diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/Base64CursorEncoder.java b/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/Base64CursorEncoder.java index af5d2da4..6fc8a063 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/Base64CursorEncoder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/Base64CursorEncoder.java @@ -25,6 +25,8 @@ import java.util.Base64; /** * {@link CursorEncoder} that applies Base 64 encoding and decoding. * + *

To create an instance, use {@link CursorEncoder#base64()}. + * * @author Rossen Stoyanchev * @since 1.2 */ diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/EncodingCursorStrategy.java b/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/EncodingCursorStrategy.java index 8114fd08..31809a5d 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/EncodingCursorStrategy.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/EncodingCursorStrategy.java @@ -22,8 +22,8 @@ import org.springframework.util.Assert; * Decorator for a {@link CursorStrategy} that applies a {@link CursorEncoder} * to the cursor String to make it opaque for external use. * - *

Use {@link CursorStrategy#withEncoder(CursorStrategy, CursorEncoder)} to - * decorate a {@code CursorStrategy}. + *

To create an instance, use + * {@link CursorStrategy#withEncoder(CursorStrategy, CursorEncoder)}. * * @author Rossen Stoyanchev * @since 1.2 diff --git a/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/NoOpCursorEncoder.java b/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/NoOpCursorEncoder.java index c4443edd..59afd738 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/NoOpCursorEncoder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/data/pagination/NoOpCursorEncoder.java @@ -19,6 +19,8 @@ package org.springframework.graphql.data.pagination; /** * {@link CursorEncoder} that leaves the cursor value unchanged. * + *

To create an instance, use {@link CursorEncoder#noOpEncoder()}. + * * @author Rossen Stoyanchev * @since 1.2 */