Files
end 683b0ddbf4 docs: add package godoc and clarify public API
Prepare the extracted library for external consumption with grouped
godoc, extension point docs, and setup requirements. Wire
ErrBadCursorString into decode paths and drop redundant Paginate*Conds
aliases.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-29 17:14:05 -07:00

121 lines
3.4 KiB
Go

package cursor
import (
"fmt"
"gitea.auvem.com/go-toolkit/dbx"
)
// PageInfo holds standard pagination information, including cursors for the
// start and end of the page, and flags indicating whether there are more pages
// before or after the current page.
//
// Modeled after the PageInfo type in the GraphQL Relay specification.
type PageInfo struct {
// The cursor string for the last item in the current page.
EndCursor *string `json:"endCursor,omitempty"`
// Whether there are more items after the current page.
HasNextPage bool `json:"hasNextPage"`
// Whether there are more items before the current page.
HasPreviousPage bool `json:"hasPreviousPage"`
// The cursor string for the first item in the current page.
StartCursor *string `json:"startCursor,omitempty"`
}
// CursorFunc builds a [GenericCursor] for a connection node.
type CursorFunc[T any] = func(*T) (GenericCursor, error)
// Edge represents a single edge in a connection, containing a node of type T
// and a cursor string for pagination.
type Edge[T any] struct {
Node *T
Cursor string
}
// Connection represents a paginated list of edges, along with page information.
type Connection[T any] struct {
Edges []*Edge[T]
PageInfo *PageInfo
TotalCount int
}
// BuildEdges constructs a Connection object from a list of items of type T,
// using the provided Queryable interface for database operations and a
// count function to determine the total number of items. It also uses a
// cursor function to generate cursors for each item in the list.
func BuildEdges[T any](
sqlo dbx.Queryable,
countFn QueryCountFn,
list []*T,
cursorFunc CursorFunc[T],
) (
conn *Connection[T],
err error,
) {
// Create edges for the connection
edges := make([]*Edge[T], len(list))
for i, item := range list {
if item == nil {
return nil, fmt.Errorf("item at index %d is nil", i)
}
c, err := cursorFunc(item)
if err != nil {
return nil, fmt.Errorf("failed to get cursor for item at index %d: %w", i, err)
}
cstr, err := c.Encode()
if err != nil {
return nil, fmt.Errorf("failed to encode cursor for item at index %d: %w", i, err)
}
edges[i] = &Edge[T]{
Node: item,
Cursor: cstr,
}
}
// Create page info and connection object
var pageInfo PageInfo
var countResult QueryCountResult
if len(edges) > 0 {
// Set start and end cursor strings
pageInfo.StartCursor = &edges[0].Cursor
pageInfo.EndCursor = &edges[len(edges)-1].Cursor
// Fetch total count. Comparisons use composite tuple logic when the cursor
// encodes both order and index columns.
startCursor, err := cursorFunc(edges[0].Node)
if err != nil {
return nil, fmt.Errorf("failed to get start cursor: %w", err)
}
endCursor, err := cursorFunc(edges[len(edges)-1].Node)
if err != nil {
return nil, fmt.Errorf("failed to get end cursor: %w", err)
}
if countResult, err = countFn(sqlo, startCursor, endCursor); err != nil {
return nil, fmt.Errorf("failed to query count: %w", err)
}
}
pageInfo.HasNextPage = countResult.After > 0
pageInfo.HasPreviousPage = countResult.Before > 0
return &Connection[T]{
Edges: edges,
PageInfo: &pageInfo,
TotalCount: countResult.Total,
}, nil
}
// ExtractNodes extracts the nodes from a Connection object and returns them
// as a slice of pointers to type T.
func ExtractNodes[T any](conn *Connection[T]) []*T {
nodes := make([]*T, len(conn.Edges))
for i, edge := range conn.Edges {
nodes[i] = edge.Node
}
return nodes
}