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 }