ChilliCream / ChilliCream/graphql-platform

Simplify cursor-over-offset pagination

Open
#6,906 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

🌶️ hot chocolate Area: Data
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.