← 中文资源

简体中文版本

Gqlgen GraphQL 游标分页解析器

用 gqlgen 实现 Relay 连接规范的游标分页:first 控制页大小、after 作为时间锚点过滤、edges 与 pageInfo 按规范组装。

阅读英文原文

示例说明

Relay 连接规范是 GraphQL 分页的事实标准:edges 承载数据与游标,pageInfo 声明续页信息。解析器把这两个字段从键序存储里组装出来——first 参数约束页大小(夹在 1 到 100 之间防御恶意大页),after 游标解码为时间锚点,过滤出严格晚于锚点的记录再截断。

技术要点

  • first 参数夹在安全区间防深拉取
  • 游标解码失败立即报错
  • hasNextPage 由锚点后剩余记录判定

代码解读

Orders 的过滤循环跳过锚点之前的订单,凑满 limit 即止;hasNextPage 再扫一遍判断锚点之后是否还有记录——多取判定的替代实现。EndCursor 取本页末条的时间串。结构体的 json 标签与 GraphQL schema 字段一一对应,gqlgen 的模型绑定直接复用。

使用建议

游标可加密签名防客户端构造任意锚点。排序键与游标键必须一致且建索引。深层翻页建议换搜索或筛选交互。

代码示例

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
}