Pagination¶
Viaduct implements the Relay Connection specification, providing cursor-based pagination with built-in builder utilities.
Schema¶
Define your connection and edge types using the @connection and @edge directives:
type FilmConnection @connection {
edges: [FilmEdge!]!
pageInfo: PageInfo!
}
type FilmEdge @edge {
node: Film!
cursor: String!
}
Fields that return a connection accept standard Relay pagination arguments:
type Character implements Node {
id: ID!
name: String!
films(first: Int, after: String): FilmConnection!
}
Building Connection Responses¶
Connection builders provide three strategies depending on your backend:
fromList — in-memory list¶
Use when your resolver has the full dataset. Viaduct handles slicing and cursor encoding automatically:
@Resolver
class CharacterFilmsResolver : CharacterResolvers.Films() {
override suspend fun resolve(ctx: Context): FilmConnection {
val allFilms = filmRepository.getFilmsForCharacter(ctx.getObjectValue().getIdOrThrow().internalID)
return FilmConnection.Builder(ctx)
.fromList(allFilms) { film -> Film.Builder(ctx).title(film.title).build() }
.build()
}
}
package com.example.starwars.modules.filmography.characters.queries;
/** Resolves Relay-style character pagination. */
@Resolver
@Prototype
public final class AllCharactersConnectionQueryResolver
extends QueryResolvers.AllCharactersConnection {
private final CharacterRepository characterRepository;
@Inject
public AllCharactersConnectionQueryResolver(CharacterRepository characterRepository) {
this.characterRepository = characterRepository;
}
@Override
public CompletableFuture<CharactersConnection> resolve(Context context) {
return CompletableFuture.completedFuture(
CharactersConnection.builder(context)
.fromList(
characterRepository.findAll(),
character -> new CharacterBuilder(context).build(character))
.build());
}
}
The Java demo uses allCharactersConnection for this strategy. Generated builders use Type.builder(context) and a Java mapping function; the context supplies the pagination arguments.
fromSlice — offset/limit backend¶
Use when your backend accepts offset and limit. Over-fetch by one to detect hasNextPage:
@Resolver
class CharacterFilmsResolver : CharacterResolvers.Films() {
override suspend fun resolve(ctx: Context): FilmConnection {
val (offset, limit) = ctx.arguments.toOffsetLimit()
val fetched = filmService.getFilms(offset, limit + 1)
return FilmConnection.Builder(ctx)
.fromSlice(fetched.take(limit), hasNextPage = fetched.size > limit) { film ->
Film.Builder(ctx).title(film.title).build()
}
.build()
}
}
package com.example.starwars.modules.universe.starships.queries;
/** Resolves forward pagination for starships using a limit-plus-one slice. */
@Resolver
@Prototype
public final class AllStarshipsConnectionQueryResolver
extends QueryResolvers.AllStarshipsConnection {
private final StarshipsRepository starshipsRepository;
@Inject
public AllStarshipsConnectionQueryResolver(StarshipsRepository starshipsRepository) {
this.starshipsRepository = starshipsRepository;
}
@Override
public CompletableFuture<StarshipsConnection> resolve(Context context) {
OffsetLimit offsetLimit = context.getArguments().toOffsetLimit();
List<Starship> allStarships = starshipsRepository.findAll();
int fromIndex = Math.min(offsetLimit.offset(), allStarships.size());
int toIndex = Math.min(allStarships.size(), fromIndex + offsetLimit.limit() + 1);
List<Starship> slicePlusOne = allStarships.subList(fromIndex, toIndex);
boolean hasNextPage = slicePlusOne.size() > offsetLimit.limit();
return CompletableFuture.completedFuture(
StarshipsConnection.builder(context)
.fromSlice(
slicePlusOne, hasNextPage, starship -> new StarshipBuilder(context).build(starship))
.build());
}
}
context.getArguments().toOffsetLimit() returns an OffsetLimit. Pass the slice and hasNextPage to the Java builder. Over-fetch by one to detect the next page.
fromEdges — native cursors or edge metadata¶
Use when your backend returns native cursors, or when your edge type carries extra fields beyond node and cursor:
@Resolver
class CharacterFilmsResolver : CharacterResolvers.Films() {
override suspend fun resolve(ctx: Context): FilmConnection {
val response = filmService.getFilms(cursor = ctx.arguments.after, limit = ctx.arguments.first ?: 20)
return FilmConnection.Builder(ctx)
.fromEdges(
edges = response.films.map { film ->
FilmEdge.Builder(ctx)
.node(Film.Builder(ctx).title(film.title).build())
.cursor(film.cursor)
.build()
},
hasNextPage = response.hasMore
)
.build()
}
}
package com.example.starwars.modules.universe.species.queries;
/** Resolves multidirectional species pagination using manually constructed edges. */
@Resolver
@Prototype
public final class AllSpeciesConnectionQueryResolver extends QueryResolvers.AllSpeciesConnection {
private final SpeciesRepository speciesRepository;
@Inject
public AllSpeciesConnectionQueryResolver(SpeciesRepository speciesRepository) {
this.speciesRepository = speciesRepository;
}
@Override
public CompletableFuture<SpeciesConnection> resolve(Context context) {
OffsetLimit offsetLimit =
context.getArguments().requiresTotalCountForOffsetLimit()
? context
.getArguments()
.toOffsetLimit(speciesRepository.count(), ConnectionArguments.DEFAULT_PAGE_SIZE)
: context.getArguments().toOffsetLimit();
List<Species> slicePlusOne =
speciesRepository.findSome(offsetLimit.limit() + 1, offsetLimit.offset());
boolean hasNextPage = slicePlusOne.size() > offsetLimit.limit();
boolean hasPreviousPage = offsetLimit.offset() > 0;
int edgeCount = Math.min(offsetLimit.limit(), slicePlusOne.size());
List<SpeciesEdge> edges = new ArrayList<>(edgeCount);
for (int index = 0; index < edgeCount; index++) {
edges.add(
SpeciesEdge.builder(context)
.node(new SpeciesBuilder(context).build(slicePlusOne.get(index)))
.cursor(OffsetCursor.fromOffset(offsetLimit.offset() + index).getValue())
.build());
}
return CompletableFuture.completedFuture(
SpeciesConnection.builder(context).fromEdges(edges, hasNextPage, hasPreviousPage).build());
}
}
The Java demo constructs SpeciesEdge objects and calls fromEdges(edges, hasNextPage, hasPreviousPage). For backward pagination its argument conversion uses the total item count.
Full Documentation¶
For the complete pagination reference — argument types, toOffsetLimit(), cursor encoding, and PageInfo handling — see the Pagination developer docs.