Gqlgen GraphQL Resolver Cursor Pagination

Implement Relay-style cursor pagination with gqlgen resolvers, resolving edges and pageInfo from a keyset-ordered store.

GoData Fetching

Code

snippet
package main

// gqlgen resolver implementing Relay-style GraphQL cursor pagination

import (
	"context"
	"fmt"
	"time"
)

type Order struct {
	ID        string    `json:"id"`
	CreatedAt time.Time `json:"createdAt"`
	Total     int       `json:"total"`
}

type OrderEdge struct {
	Cursor string `json:"cursor"`
	Node   Order  `json:"node"`
}

type PageInfo struct {
	HasNextPage bool   `json:"hasNextPage"`
	EndCursor   string `json:"endCursor"`
}

type OrderConnection struct {
	Edges    []OrderEdge `json:"edges"`
	PageInfo PageInfo    `json:"pageInfo"`
}

type Resolver struct {
	orders []Order
}

func (r *Resolver) Orders(
	ctx context.Context, first *int, after *string,
) (*OrderConnection, error) {
	limit := 20
	if first != nil && *first > 0 && *first <= 100 {
		limit = *first
	}

	// 存储按时间倒序,游标即 createdAt 的 ISO 串
	afterTime := time.Time{}
	if after != nil {
		parsed, err := time.Parse(time.RFC3339, *after)
		if err != nil {
			return nil, fmt.Errorf("invalid cursor: %w", err)
		}
		afterTime = parsed
	}

	edges := make([]OrderEdge, 0, limit)
	for _, o := range r.orders {
		if !afterTime.IsZero() && !o.CreatedAt.After(afterTime) {
			continue // 跳过游标之前(含锚点)的记录
		}
		if len(edges) == limit {
			break
		}
		edges = append(edges, OrderEdge{
			Cursor: o.CreatedAt.Format(time.RFC3339),
			Node:   o,
		})
	}

	hasNext := false
	lastCursor := ""
	if len(edges) > 0 {
		last := edges[len(edges)-1].Node.CreatedAt
		lastCursor = edges[len(edges)-1].Cursor
		for _, o := range r.orders {
			if o.CreatedAt.After(last) {
				hasNext = true
				break
			}
		}
	}

	return &OrderConnection{
		Edges: edges,
		PageInfo: PageInfo{HasNextPage: hasNext, EndCursor: lastCursor},
	}, nil
}