ChilliCream / ChilliCream/graphql-platform
Simplify cursor-over-offset pagination
Nobody has claimed this yet.
- Dominant language
- C#
- Stars
- 5.8k
- Forks
- 810
- Avg merge
- 15h 39m
- Merged PRs (30d)
- 98
Description
Product
Hot Chocolate
Is your feature request related to a problem?
The HotChocolate pagination docs highly recommends cursors/connections, even if the underlying system uses offset-based pagination.
However, HotChocolate does not make this easy. Specifically, I need to write the following helper code myself, which could have been part of HotChocolate (with some modifications regarding possible edge cases and optional pagination). It mimics the cursor format HotChocolate uses if you use [UsePaging] with IEnumerable (i.e., encoded indexes).
public record OffsetPaginationArgs(int Offset, int Limit);
public static class OffsetPaginationHelper
{
public static string EncodeCursor(int offset)
{
return Convert.ToBase64String(Encoding.UTF8.GetBytes(offset.ToString()));
}
public static int DecodeCursor(string argumentName, string cursor)
{
try
{
return int.Parse(Encoding.UTF8.GetString(Convert.FromBase64String(cursor)));
}
catch (Exception e)
{
throw new GraphQLException(
ErrorBuilder
.New()
.SetMessage("The cursor specified in `{0}` has an invalid format.", argumentName)
.SetExtension("argument", argumentName)
.SetExtension("cursor", cursor)
.SetCode(ErrorCodes.Paging.InvalidCursor)
.Build()
);
}
}
public static OffsetPaginationArgs ParseOffset(int first, string? after)
{
var offset = 0;
if (after != null)
offset = DecodeCursor("after", after) + 1;
// +1 extra since we must know if there are more
var limit = first + 1;
return new OffsetPaginationArgs(offset, limit);
}
public static async Task<Connection<TNode>> QueryAsync<TNode>(int first, string? after,
Func<OffsetPaginationArgs, Task<IReadOnlyCollection<TNode>>> query)
{
var args = ParseOffset(first, after);
var nodes = await query(args);
var toReturn = nodes.Take(args.Limit - 1);
var edges = toReturn.Select((n, i) => new Edge<TNode>(n, EncodeCursor(args.Offset + i))).ToArray();
var pageInfo =
new ConnectionPageInfo(
nodes.Count > args.Limit - 1,
args.Offset > 0,
edges.FirstOrDefault()?.Cursor,
edges.LastOrDefault()?.Cursor
);
return new Connection<TNode>(edges, pageInfo);
}
}
With the helper code above in place, usage is very simple:
public class Query
{
[UsePaging]
public async Task<Connection<int>> GetNumbers(int first, string? after, [Service] IntRepository repository)
{
return await OffsetPaginationHelper.QueryAsync(first, after,
args => repository.GetAllNumbers(args.Offset, args.Limit));
}
}
Note that the helper code above does not support backwards pagination, since this is generally not easy to support for offset-based pagination. There is also no support for including the total count; the helper code could of course be extended to support that if desired. Alternatively, if ConnectionPageInfo.HasPreviousPage
The solution you'd like
For the above helper code, or something similar, to be part of HotChocolate.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the HotChocolate pagination documentation and the existing [UsePaging] behavior for IEnumerable, then compare it with the proposed OffsetPaginationHelper. Done means HotChocolate provides a supported way to expose cursor-based connections over offset-backed data, including the described forward-pagination behavior and invalid-cursor handling.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, graphql
- Domain
- api, backend-api-design
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100