Skip to content

Commit

Permalink
Allow the after cursor to be an empty string (same as offset: 0)
Browse files Browse the repository at this point in the history
  • Loading branch information
christeredvartsen committed Nov 26, 2024
1 parent cb7565d commit 5070070
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 5 deletions.
205 changes: 205 additions & 0 deletions integration_tests/cursor.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
Test.gql("forward pagination", function(t)
local fetchTeams = function(after)
t.query(string.format([[
query {
teams(first:5 after:"%s") {
pageInfo {
hasNextPage
endCursor
}
edges {
node {
slug
}
cursor
}
}
}
]], after))
end

fetchTeams("")
t.check {
data = {
teams = {
pageInfo = {
hasNextPage = true,
endCursor = Save("endCursor"),
},
edges = {
{
node = {
slug = "slug-1",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-10",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-11",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-12",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-13",
},
cursor = Save("lastNodeInPageCursor"),
},
},
},
},
}

assert(State.lastNodeInPageCursor == State.endCursor, "lastNodeInPageCursor is not equal to endCursor")

fetchTeams(State.endCursor)
t.check {
data = {
teams = {
pageInfo = {
hasNextPage = true,
endCursor = Save("endCursor"),
},
edges = {
{
node = {
slug = "slug-14",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-15",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-16",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-17",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-18",
},
cursor = Save("lastNodeInPageCursor"),
},
},
},
},
}

assert(State.lastNodeInPageCursor == State.endCursor, "lastNodeInPageCursor is not equal to endCursor")

fetchTeams(State.endCursor)
t.check {
data = {
teams = {
pageInfo = {
hasNextPage = true,
endCursor = Save("endCursor"),
},
edges = {
{
node = {
slug = "slug-19",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-2",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-20",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-3",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-4",
},
cursor = Save("lastNodeInPageCursor"),
},

},
},
},
}

assert(State.lastNodeInPageCursor == State.endCursor, "lastNodeInPageCursor is not equal to endCursor")

fetchTeams(State.endCursor)
t.check {
data = {
teams = {
pageInfo = {
hasNextPage = false,
endCursor = Save("endCursor"),
},
edges = {
{
node = {
slug = "slug-5",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-6",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-7",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-8",
},
cursor = Ignore(),
},
{
node = {
slug = "slug-9",
},
cursor = Save("lastNodeInPageCursor"),
},
},
},
},
}

assert(State.lastNodeInPageCursor == State.endCursor, "lastNodeInPageCursor is not equal to endCursor")
end)
14 changes: 12 additions & 2 deletions internal/graph/pagination/cursor.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strconv"

"github.com/btcsuite/btcutil/base58"
"github.com/nais/api/internal/graph/apierror"
)

var cursorVersions = map[string]func(c *Cursor, i []byte) error{
Expand All @@ -17,6 +18,11 @@ var cursorVersions = map[string]func(c *Cursor, i []byte) error{

type Cursor struct {
Offset int `json:"offset"`
empty bool
}

func (c *Cursor) valid() bool {
return c != nil && !c.empty
}

func (c Cursor) MarshalGQLContext(_ context.Context, w io.Writer) error {
Expand All @@ -31,14 +37,18 @@ func (c Cursor) MarshalGQLContext(_ context.Context, w io.Writer) error {

func (c *Cursor) UnmarshalGQLContext(_ context.Context, v interface{}) error {
if s, ok := v.(string); ok {
if s == "" {
c.empty = true
return nil
}
b := base58.Decode(s)
version, cursor, ok := bytes.Cut(b, []byte{':'})
if !ok {
return fmt.Errorf("invalid cursor format")
return apierror.Errorf("Unsupported cursor format")
}
parseCursor, ok := cursorVersions[string(version)]
if !ok {
return fmt.Errorf("unsupported cursor version")
return apierror.Errorf("Unsupported cursor version: %q", string(version))
}
if err := parseCursor(c, cursor); err != nil {
return err
Expand Down
3 changes: 2 additions & 1 deletion internal/graph/pagination/cursor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
)

func TestCursor_MarshalGQLContext(t *testing.T) {
Expand Down Expand Up @@ -58,7 +59,7 @@ func TestCursor_UnmarshalGQLContext(t *testing.T) {
t.Fatal(err)
}

if diff := cmp.Diff(tc.expected, c); diff != "" {
if diff := cmp.Diff(tc.expected, c, cmpopts.IgnoreUnexported(Cursor{})); diff != "" {
t.Errorf("diff: -want +got\n%s", diff)
}
})
Expand Down
6 changes: 4 additions & 2 deletions internal/graph/pagination/relay.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ func ParsePage(first *int, after *Cursor, last *int, before *Cursor) (*Paginatio
}

switch {
case first != nil && before != nil:
case first != nil && before.valid():
return nil, apierror.Errorf("first and before cannot be used together")
case last != nil && after != nil:
return nil, apierror.Errorf("last and after cannot be used together")
case last != nil && before == nil:
return nil, apierror.Errorf("last must be used with before")
case before != nil && before.empty:
return nil, apierror.Errorf("before cannot be empty")
}

if first != nil {
Expand All @@ -62,7 +64,7 @@ func ParsePage(first *int, after *Cursor, last *int, before *Cursor) (*Paginatio
p.limit = l
}

if after != nil {
if after != nil && !after.empty {
p.offset = after.Offset + 1
} else if before != nil {
p.offset = before.Offset - int(p.Limit())
Expand Down

0 comments on commit 5070070

Please sign in to comment.