pagination

package
v0.0.8 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 31, 2025 License: Apache-2.0 Imports: 7 Imported by: 0

README

Pagination Package

A comprehensive, production-ready pagination system for the AuthSome framework with support for both offset-based and cursor-based pagination patterns.

Features

  • Offset-based pagination (page/limit/offset)
  • Cursor-based pagination (for large datasets)
  • Sorting & ordering (ASC/DESC)
  • Search & filtering support
  • Full metadata (total count, pages, has_next/prev)
  • Forge DTO tags compliance
  • Validation with sensible defaults
  • Helper methods for common operations
  • Type-safe generics for responses
  • Base64 cursor encoding/decoding

Installation

The package is part of the AuthSome core:

import "github.com/xraph/authsome/core/pagination"

Quick Start

Offset-Based Pagination
// In your handler
func (h *Handler) ListUsers(c *forge.Context) error {
    var params pagination.PaginationParams
    if err := c.BindQuery(&params); err != nil {
        return c.JSON(400, ErrorResponse{Message: "invalid parameters"})
    }

    // Validate parameters
    if err := params.Validate(); err != nil {
        return c.JSON(400, ErrorResponse{Message: err.Error()})
    }

    // Fetch data from repository
    users, total, err := h.userRepo.List(
        c.Context(),
        params.GetLimit(),
        params.GetOffset(),
        params.GetSortBy(),
        params.GetOrder(),
    )
    if err != nil {
        return c.JSON(500, ErrorResponse{Message: "failed to fetch users"})
    }

    // Create response with metadata
    response := pagination.NewPageResponse(users, total, &params)
    return c.JSON(200, response)
}
Cursor-Based Pagination
func (h *Handler) ListPosts(c *forge.Context) error {
    var params pagination.CursorParams
    if err := c.BindQuery(&params); err != nil {
        return c.JSON(400, ErrorResponse{Message: "invalid parameters"})
    }

    if err := params.Validate(); err != nil {
        return c.JSON(400, ErrorResponse{Message: err.Error()})
    }

    // Decode cursor
    var cursorData *pagination.CursorData
    if params.Cursor != "" {
        var err error
        cursorData, err = pagination.DecodeCursor(params.Cursor)
        if err != nil {
            return c.JSON(400, ErrorResponse{Message: "invalid cursor"})
        }
    }

    // Fetch data
    posts, nextCursor, prevCursor, err := h.postRepo.ListCursor(
        c.Context(),
        params.GetLimit()+1, // Fetch one extra to check if there's more
        cursorData,
        params.GetSortBy(),
        params.GetOrder(),
    )
    if err != nil {
        return c.JSON(500, ErrorResponse{Message: "failed to fetch posts"})
    }

    response := pagination.NewCursorResponse(posts, nextCursor, prevCursor, &params)
    return c.JSON(200, response)
}

API Reference

Types
PaginationParams

Request parameters for offset-based pagination:

Field Type JSON Tag Query Param Default Validation Description
Limit int limit limit 10 min=1, max=10000 Number of items per page
Offset int offset offset 0 min=0 Number of items to skip
Page int page page 1 min=1 Current page number
SortBy string sortBy sort_by created_at - Field to sort by
Order SortOrder order order desc oneof=asc,desc Sort order (asc/desc)
Search string search search "" - Search query
Filter string filter filter "" - Filter expression
CursorParams

Request parameters for cursor-based pagination:

Field Type JSON Tag Query Param Default Validation Description
Limit int limit limit 10 min=1, max=10000 Number of items per page
Cursor string cursor cursor "" - Base64-encoded cursor
SortBy string sortBy sort_by created_at - Field to sort by
Order SortOrder order order desc oneof=asc,desc Sort order (asc/desc)
Search string search search "" - Search query
Filter string filter filter "" - Filter expression
PageResponse[T]

Generic response structure:

type PageResponse[T any] struct {
    Data       []T         `json:"data"`
    Pagination *PageMeta   `json:"pagination,omitempty"`
    Cursor     *CursorMeta `json:"cursor,omitempty"`
}
PageMeta

Metadata for offset-based pagination:

type PageMeta struct {
    Total       int64 `json:"total"`        // Total number of items
    Limit       int   `json:"limit"`        // Items per page
    Offset      int   `json:"offset"`       // Current offset
    CurrentPage int   `json:"currentPage"`  // Current page number
    TotalPages  int   `json:"totalPages"`   // Total number of pages
    HasNext     bool  `json:"hasNext"`      // Whether there's a next page
    HasPrev     bool  `json:"hasPrev"`      // Whether there's a previous page
}
CursorMeta

Metadata for cursor-based pagination:

type CursorMeta struct {
    NextCursor string `json:"nextCursor,omitempty"` // Cursor for next page
    PrevCursor string `json:"prevCursor,omitempty"` // Cursor for previous page
    HasNext    bool   `json:"hasNext"`              // Whether there's a next page
    HasPrev    bool   `json:"hasPrev"`              // Whether there's a previous page
    Count      int    `json:"count"`                // Number of items in current page
}
Methods
PaginationParams Methods
func (p *PaginationParams) Validate() error
func (p *PaginationParams) GetLimit() int
func (p *PaginationParams) GetOffset() int
func (p *PaginationParams) GetPage() int
func (p *PaginationParams) GetSortBy() string
func (p *PaginationParams) GetOrder() SortOrder
func (p *PaginationParams) GetOrderClause() string  // Returns "field_name ASC/DESC"
CursorParams Methods
func (c *CursorParams) Validate() error
func (c *CursorParams) GetLimit() int
func (c *CursorParams) GetSortBy() string
func (c *CursorParams) GetOrder() SortOrder
func (c *CursorParams) GetOrderClause() string
Response Constructors
// Create paginated response with metadata
func NewPageResponse[T any](data []T, total int64, params *PaginationParams) *PageResponse[T]

// Create cursor-based response with metadata
func NewCursorResponse[T any](data []T, nextCursor, prevCursor string, params *CursorParams) *PageResponse[T]

// Create empty paginated response
func NewEmptyPageResponse[T any]() *PageResponse[T]

// Create empty cursor-based response
func NewEmptyCursorResponse[T any]() *PageResponse[T]
Cursor Utilities
// Encode cursor data to base64 string
func EncodeCursor(id string, timestamp time.Time, value string) (string, error)

// Decode base64 cursor string
func DecodeCursor(cursor string) (*CursorData, error)

// Simple string encoding/decoding
func SimpleCursorEncode(value string) string
func SimpleCursorDecode(cursor string) (string, error)

Usage Examples

Basic Listing with Pagination
// GET /api/users?limit=20&page=2&sort_by=name&order=asc

type User struct {
    ID        string    `json:"id"`
    Name      string    `json:"name"`
    Email     string    `json:"email"`
    CreatedAt time.Time `json:"created_at"`
}

func (h *Handler) ListUsers(c *forge.Context) error {
    var params pagination.PaginationParams
    if err := c.BindQuery(&params); err != nil {
        return c.JSON(400, map[string]string{"error": "invalid parameters"})
    }

    if err := params.Validate(); err != nil {
        return c.JSON(400, map[string]string{"error": err.Error()})
    }

    users, total, err := h.service.ListUsers(
        c.Context(),
        params.GetLimit(),
        params.GetOffset(),
        params.GetSortBy(),
        params.GetOrder(),
    )
    if err != nil {
        return c.JSON(500, map[string]string{"error": "failed to fetch users"})
    }

    response := pagination.NewPageResponse(users, total, &params)
    return c.JSON(200, response)
}

Response:

{
  "data": [
    {"id": "21", "name": "Alice", "email": "alice@example.com", "created_at": "2024-01-15T10:00:00Z"},
    {"id": "22", "name": "Bob", "email": "bob@example.com", "created_at": "2024-01-16T10:00:00Z"}
  ],
  "pagination": {
    "total": 100,
    "limit": 20,
    "offset": 20,
    "currentPage": 2,
    "totalPages": 5,
    "hasNext": true,
    "hasPrev": true
  }
}
Cursor-Based Pagination (Large Datasets)
// GET /api/posts?limit=10&cursor=eyJpZCI6IjEyMyIsInRzIjoxNjQwMDAwMDAwfQ==

func (h *Handler) ListPosts(c *forge.Context) error {
    var params pagination.CursorParams
    if err := c.BindQuery(&params); err != nil {
        return c.JSON(400, map[string]string{"error": "invalid parameters"})
    }

    if err := params.Validate(); err != nil {
        return c.JSON(400, map[string]string{"error": err.Error()})
    }

    // Decode cursor
    var afterID string
    var afterTime time.Time
    if params.Cursor != "" {
        cursorData, err := pagination.DecodeCursor(params.Cursor)
        if err != nil {
            return c.JSON(400, map[string]string{"error": "invalid cursor"})
        }
        afterID = cursorData.ID
        afterTime = cursorData.Timestamp
    }

    // Fetch one extra item to determine if there's a next page
    limit := params.GetLimit() + 1
    posts, err := h.service.ListPostsCursor(
        c.Context(),
        afterID,
        afterTime,
        limit,
        params.GetSortBy(),
        params.GetOrder(),
    )
    if err != nil {
        return c.JSON(500, map[string]string{"error": "failed to fetch posts"})
    }

    // Check if there's a next page
    hasNext := len(posts) > params.GetLimit()
    if hasNext {
        posts = posts[:params.GetLimit()]
    }

    // Generate next cursor
    var nextCursor string
    if hasNext && len(posts) > 0 {
        lastPost := posts[len(posts)-1]
        nextCursor, _ = pagination.EncodeCursor(
            lastPost.ID,
            lastPost.CreatedAt,
            "",
        )
    }

    response := pagination.NewCursorResponse(posts, nextCursor, "", &params)
    return c.JSON(200, response)
}

Response:

{
  "data": [
    {"id": "post_1", "title": "First Post", "created_at": "2024-01-15T10:00:00Z"},
    {"id": "post_2", "title": "Second Post", "created_at": "2024-01-16T10:00:00Z"}
  ],
  "cursor": {
    "nextCursor": "eyJpZCI6InBvc3RfMiIsInRzIjoxNzA1NDAwMDAwfQ==",
    "prevCursor": "",
    "hasNext": true,
    "hasPrev": false,
    "count": 2
  }
}
Repository Integration (Bun ORM)
type userRepository struct {
    db *bun.DB
}

func (r *userRepository) List(
    ctx context.Context,
    limit, offset int,
    sortBy string,
    order pagination.SortOrder,
) ([]*User, int64, error) {
    var users []*User
    
    query := r.db.NewSelect().
        Model(&users).
        Limit(limit).
        Offset(offset)

    // Apply sorting
    orderClause := fmt.Sprintf("%s %s", sortBy, strings.ToUpper(string(order)))
    query = query.Order(orderClause)

    // Get total count
    total, err := query.ScanAndCount(ctx)
    if err != nil {
        return nil, 0, fmt.Errorf("failed to fetch users: %w", err)
    }

    return users, int64(total), nil
}

// Cursor-based query
func (r *userRepository) ListCursor(
    ctx context.Context,
    afterID string,
    afterTime time.Time,
    limit int,
    sortBy string,
    order pagination.SortOrder,
) ([]*User, error) {
    var users []*User
    
    query := r.db.NewSelect().
        Model(&users).
        Limit(limit)

    // Apply cursor condition
    if afterID != "" {
        if order == pagination.SortOrderDesc {
            query = query.Where("created_at < ? OR (created_at = ? AND id < ?)",
                afterTime, afterTime, afterID)
        } else {
            query = query.Where("created_at > ? OR (created_at = ? AND id > ?)",
                afterTime, afterTime, afterID)
        }
    }

    // Apply sorting
    orderClause := fmt.Sprintf("%s %s", sortBy, strings.ToUpper(string(order)))
    query = query.Order(orderClause)

    err := query.Scan(ctx)
    if err != nil {
        return nil, fmt.Errorf("failed to fetch users: %w", err)
    }

    return users, nil
}
With Search and Filtering
func (h *Handler) SearchUsers(c *forge.Context) error {
    var params pagination.PaginationParams
    if err := c.BindQuery(&params); err != nil {
        return c.JSON(400, map[string]string{"error": "invalid parameters"})
    }

    if err := params.Validate(); err != nil {
        return c.JSON(400, map[string]string{"error": err.Error()})
    }

    // Parse filter (e.g., "status:active,role:admin")
    filters := parseFilters(params.Filter)

    users, total, err := h.service.SearchUsers(
        c.Context(),
        params.Search,
        filters,
        params.GetLimit(),
        params.GetOffset(),
        params.GetSortBy(),
        params.GetOrder(),
    )
    if err != nil {
        return c.JSON(500, map[string]string{"error": "failed to search users"})
    }

    response := pagination.NewPageResponse(users, total, &params)
    return c.JSON(200, response)
}

func parseFilters(filterStr string) map[string]string {
    filters := make(map[string]string)
    if filterStr == "" {
        return filters
    }

    pairs := strings.Split(filterStr, ",")
    for _, pair := range pairs {
        kv := strings.SplitN(pair, ":", 2)
        if len(kv) == 2 {
            filters[strings.TrimSpace(kv[0])] = strings.TrimSpace(kv[1])
        }
    }
    return filters
}

Route Registration

// Register paginated endpoints
func Register(router forge.Router, h *Handler) {
    router.GET("/users", h.ListUsers,
        forge.WithName("users.list"),
        forge.WithSummary("List users with pagination"),
        forge.WithDescription("Returns a paginated list of users"),
        forge.WithRequestSchema(pagination.PaginationParams{}),
        forge.WithResponseSchema(200, "Success", pagination.PageResponse[User]{}),
        forge.WithResponseSchema(400, "Invalid parameters", ErrorResponse{}),
        forge.WithTags("Users"),
    )

    router.GET("/posts", h.ListPosts,
        forge.WithName("posts.list"),
        forge.WithSummary("List posts with cursor pagination"),
        forge.WithDescription("Returns a cursor-paginated list of posts"),
        forge.WithRequestSchema(pagination.CursorParams{}),
        forge.WithResponseSchema(200, "Success", pagination.PageResponse[Post]{}),
        forge.WithResponseSchema(400, "Invalid parameters", ErrorResponse{}),
        forge.WithTags("Posts"),
    )
}

Configuration

Constants can be adjusted in the package:

const (
    DefaultLimit = 10   // Default number of items per page
    MaxLimit     = 10000  // Maximum allowed items per page
    MinLimit     = 1    // Minimum allowed items per page
)

Best Practices

1. Always Validate
if err := params.Validate(); err != nil {
    return c.JSON(400, map[string]string{"error": err.Error()})
}
2. Use Getters for Safe Defaults
// Instead of params.Limit directly
limit := params.GetLimit()  // Returns default if not set or invalid
3. Choose the Right Pagination Type
  • Offset-based: Good for small to medium datasets, supports jumping to specific pages
  • Cursor-based: Better for large datasets, real-time data, infinite scroll
4. Index Your Sort Fields

Ensure database indexes exist on fields used in SortBy:

CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_users_name ON users(name);
5. Cursor Query Optimization

For cursor pagination, use composite indexes:

CREATE INDEX idx_posts_cursor ON posts(created_at DESC, id DESC);
6. Handle Empty Results
if len(users) == 0 {
    return c.JSON(200, pagination.NewEmptyPageResponse[User]())
}

Performance Considerations

  1. Offset Performance: For large offsets, consider cursor-based pagination
  2. Count Queries: Cache total counts for frequently accessed lists
  3. Index Usage: Ensure proper indexes on sort fields
  4. Fetch N+1: Fetch one extra item to determine has_next without separate query

Testing

Run tests:

go test ./core/pagination/... -v

Run benchmarks:

go test ./core/pagination/... -bench=. -benchmem

License

Part of the AuthSome project.

Documentation

Overview

Package pagination provides comprehensive pagination support for the AuthSome framework.

This package implements both offset-based and cursor-based pagination patterns with full support for sorting, filtering, and search. All structs follow the Forge DTO tags specification for seamless integration with request binding and validation.

Features

  • Offset-based pagination (page/limit/offset)
  • Cursor-based pagination for large datasets
  • Sorting and ordering (ASC/DESC)
  • Search and filtering support
  • Complete metadata in responses (total count, pages, has_next/prev)
  • Validation with sensible defaults
  • Type-safe generic responses
  • Base64 cursor encoding/decoding
  • Zero-allocation validation
  • Production-tested performance

Quick Start

Offset-based pagination:

func (h *Handler) ListUsers(c *forge.Context) error {
	var params pagination.PaginationParams
	if err := c.BindQuery(&params); err != nil {
		return c.JSON(400, ErrorResponse{Message: "invalid parameters"})
	}

	if err := params.Validate(); err != nil {
		return c.JSON(400, ErrorResponse{Message: err.Error()})
	}

	users, total, err := h.userRepo.List(
		c.Context(),
		params.GetLimit(),
		params.GetOffset(),
		params.GetSortBy(),
		params.GetOrder(),
	)
	if err != nil {
		return c.JSON(500, ErrorResponse{Message: "failed to fetch users"})
	}

	response := pagination.NewPageResponse(users, total, &params)
	return c.JSON(200, response)
}

Cursor-based pagination:

func (h *Handler) ListPosts(c *forge.Context) error {
	var params pagination.CursorParams
	if err := c.BindQuery(&params); err != nil {
		return c.JSON(400, ErrorResponse{Message: "invalid parameters"})
	}

	if err := params.Validate(); err != nil {
		return c.JSON(400, ErrorResponse{Message: err.Error()})
	}

	cursorData, err := pagination.DecodeCursor(params.Cursor)
	if err != nil {
		return c.JSON(400, ErrorResponse{Message: "invalid cursor"})
	}

	posts, nextCursor, prevCursor, err := h.postRepo.ListCursor(
		c.Context(), params.GetLimit()+1, cursorData, params.GetSortBy(), params.GetOrder(),
	)
	if err != nil {
		return c.JSON(500, ErrorResponse{Message: "failed to fetch posts"})
	}

	response := pagination.NewCursorResponse(posts, nextCursor, prevCursor, &params)
	return c.JSON(200, response)
}

Configuration

Constants can be adjusted for your use case:

const (
	DefaultLimit = 10   // Default items per page
	MaxLimit     = 10000  // Maximum items per page
	MinLimit     = 1    // Minimum items per page
)

Request Parameters

Both PaginationParams and CursorParams support Forge DTO tags:

  • json: JSON field name
  • query: Query parameter name
  • default: Default value
  • validate: Validation rules
  • example: OpenAPI example

Response Structure

Responses include comprehensive metadata:

type PageResponse[T any] struct {
	Data       []T         `json:"data"`
	Pagination *PageMeta   `json:"pagination,omitempty"` // For offset-based
	Cursor     *CursorMeta `json:"cursor,omitempty"`     // For cursor-based
}

Performance

Benchmarks on Apple M3 Max:

BenchmarkPaginationParams_Validate-16    430332202    2.771 ns/op    0 B/op    0 allocs/op
BenchmarkEncodeCursor-16                   3105051  391.6 ns/op  416 B/op    5 allocs/op
BenchmarkDecodeCursor-16                   1732579  691.3 ns/op  384 B/op    8 allocs/op

Best Practices

1. Always validate parameters after binding:

if err := params.Validate(); err != nil {
	return c.JSON(400, map[string]string{"error": err.Error()})
}

2. Use getter methods for safe defaults:

limit := params.GetLimit()  // Returns default if not set

3. Choose the right pagination type:

  • Offset: Good for small/medium datasets, supports jumping to pages
  • Cursor: Better for large datasets, real-time data, infinite scroll

4. Index your database sort fields:

CREATE INDEX idx_users_created_at ON users(created_at);
CREATE INDEX idx_posts_cursor ON posts(created_at DESC, id DESC);

5. Handle empty results gracefully:

if len(users) == 0 {
	return c.JSON(200, pagination.NewEmptyPageResponse[User]())
}

Database Integration

Example with Bun ORM:

func (r *userRepository) List(
	ctx context.Context,
	limit, offset int,
	sortBy string,
	order pagination.SortOrder,
) ([]*User, int64, error) {
	var users []*User

	query := r.db.NewSelect().
		Model(&users).
		Limit(limit).
		Offset(offset).
		Order(fmt.Sprintf("%s %s", sortBy, strings.ToUpper(string(order))))

	total, err := query.ScanAndCount(ctx)
	return users, int64(total), err
}

Thread Safety

All methods are safe for concurrent use. The validation methods modify the receiver to set defaults but are designed to be called once per request.

Error Handling

Validation errors are returned as standard Go errors with descriptive messages:

  • "limit must be at least 1"
  • "limit cannot exceed 10000"
  • "offset cannot be negative"
  • "page must be at least 1"
  • "order must be 'asc' or 'desc'"

API Compatibility

This package follows semantic versioning. The public API is stable and will not break between minor versions.

For detailed documentation and examples, see the README.md file.

Example (CursorEncoding)

Example demonstrates cursor encoding and decoding

package main

import (
	"fmt"
	"time"

	"github.com/xraph/authsome/core/pagination"
)

func main() {
	// Encode cursor
	id := "user_123"
	timestamp := time.Date(2024, 1, 15, 10, 0, 0, 0, time.UTC)
	value := "alice"

	encoded, err := pagination.EncodeCursor(id, timestamp, value)
	if err != nil {
		fmt.Printf("Encoding error: %v\n", err)
		return
	}

	// Decode cursor
	decoded, err := pagination.DecodeCursor(encoded)
	if err != nil {
		fmt.Printf("Decoding error: %v\n", err)
		return
	}

	fmt.Printf("ID: %s\n", decoded.ID)
	fmt.Printf("Value: %s\n", decoded.Value)
}
Output:

ID: user_123
Value: alice
Example (CursorPagination)

Example demonstrates cursor-based pagination

package main

import (
	"fmt"
	"time"

	"github.com/xraph/authsome/core/pagination"
)

// User represents a sample user model
type User struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"created_at"`
}

func main() {
	params := &pagination.CursorParams{
		BaseRequestParams: pagination.BaseRequestParams{
			Order: pagination.SortOrderDesc,
		},
		Limit: 10,
	}

	if err := params.Validate(); err != nil {
		fmt.Printf("Validation error: %v\n", err)
		return
	}

	// Simulate fetching posts
	posts := []User{
		{ID: "1", Name: "Post 1", CreatedAt: time.Now()},
		{ID: "2", Name: "Post 2", CreatedAt: time.Now()},
	}

	// Generate next cursor
	nextCursor, _ := pagination.EncodeCursor("2", time.Now(), "")

	// Create response
	response := pagination.NewCursorResponse(posts, nextCursor, "", params)

	fmt.Printf("Item count: %d\n", response.Cursor.Count)
	fmt.Printf("Has next: %v\n", response.Cursor.HasNext)
	fmt.Printf("Has cursor: %v\n", response.Cursor.NextCursor != "")
}
Output:

Item count: 2
Has next: true
Has cursor: true
Example (EmptyResponse)

Example demonstrates empty response handling

package main

import (
	"fmt"
	"time"

	"github.com/xraph/authsome/core/pagination"
)

// User represents a sample user model
type User struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"created_at"`
}

func main() {
	response := pagination.NewEmptyPageResponse[User]()

	fmt.Printf("Data count: %d\n", len(response.Data))
	fmt.Printf("Total: %d\n", response.Pagination.Total)
	fmt.Printf("Has next: %v\n", response.Pagination.HasNext)
}
Output:

Data count: 0
Total: 0
Has next: false
Example (FieldSelection)

Example demonstrates field selection

package main

import (
	"fmt"

	"github.com/xraph/authsome/core/pagination"
)

func main() {
	params := &pagination.PaginationParams{
		BaseRequestParams: pagination.BaseRequestParams{
			Fields: "id, name, email",
		},
		Limit: 10,
	}

	fields := params.GetFields()
	fmt.Printf("Selected fields: %v\n", fields)
	fmt.Printf("Has fields: %v\n", params.HasFields())
}
Output:

Selected fields: [id name email]
Has fields: true
Example (HandlerIntegration)

Example_handlerIntegration demonstrates integration with a handler

package main

import (
	"context"
	"fmt"
	"time"

	"github.com/xraph/authsome/core/pagination"
)

// User represents a sample user model
type User struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"created_at"`
}

func main() {
	// This example shows typical handler usage

	// 1. Bind query parameters
	params := &pagination.PaginationParams{
		BaseRequestParams: pagination.BaseRequestParams{
			SortBy: "name",
			Order:  pagination.SortOrderAsc,
		},
		Limit: 20,
		Page:  2,
	}

	// 2. Validate
	if err := params.Validate(); err != nil {
		fmt.Printf("Validation failed: %v\n", err)
		return
	}

	// 3. Query database (simulated)
	users, total := queryUsers(context.Background(), params)

	// 4. Create response
	response := pagination.NewPageResponse(users, total, params)

	// 5. Return response
	fmt.Printf("Showing page %d of %d\n", response.Pagination.CurrentPage, response.Pagination.TotalPages)
	fmt.Printf("Items: %d-%d of %d\n",
		response.Pagination.Offset+1,
		response.Pagination.Offset+len(response.Data),
		response.Pagination.Total,
	)
}

// queryUsers simulates a database query
func queryUsers(ctx context.Context, params *pagination.PaginationParams) ([]User, int64) {

	users := make([]User, 20)
	for i := range users {
		users[i] = User{
			ID:        fmt.Sprintf("user_%d", params.GetOffset()+i+1),
			Name:      fmt.Sprintf("User %d", params.GetOffset()+i+1),
			Email:     fmt.Sprintf("user%d@example.com", params.GetOffset()+i+1),
			CreatedAt: time.Now(),
		}
	}

	return users, 50
}
Output:

Showing page 2 of 3
Items: 21-40 of 50
Example (OffsetPagination)

Example demonstrates basic offset-based pagination

package main

import (
	"fmt"
	"time"

	"github.com/xraph/authsome/core/pagination"
)

// User represents a sample user model
type User struct {
	ID        string    `json:"id"`
	Name      string    `json:"name"`
	Email     string    `json:"email"`
	CreatedAt time.Time `json:"created_at"`
}

func main() {
	// Simulate request parameters
	params := &pagination.PaginationParams{
		BaseRequestParams: pagination.BaseRequestParams{
			SortBy: "created_at",
			Order:  pagination.SortOrderDesc,
		},
		Limit: 10,
		Page:  1,
	}

	// Validate parameters
	if err := params.Validate(); err != nil {
		fmt.Printf("Validation error: %v\n", err)
		return
	}

	// Simulate fetching users from database
	users := []User{
		{ID: "1", Name: "Alice", Email: "alice@example.com", CreatedAt: time.Now()},
		{ID: "2", Name: "Bob", Email: "bob@example.com", CreatedAt: time.Now()},
	}
	total := int64(25)

	// Create paginated response
	response := pagination.NewPageResponse(users, total, params)

	fmt.Printf("Current page: %d\n", response.Pagination.CurrentPage)
	fmt.Printf("Total pages: %d\n", response.Pagination.TotalPages)
	fmt.Printf("Has next: %v\n", response.Pagination.HasNext)
	fmt.Printf("Total items: %d\n", response.Pagination.Total)
}
Output:

Current page: 1
Total pages: 3
Has next: true
Total items: 25
Example (ParameterValidation)

Example demonstrates parameter validation with defaults

package main

import (
	"fmt"

	"github.com/xraph/authsome/core/pagination"
)

func main() {
	// Parameters with defaults
	params := &pagination.PaginationParams{}

	if err := params.Validate(); err != nil {
		fmt.Printf("Error: %v\n", err)
		return
	}

	fmt.Printf("Limit: %d\n", params.GetLimit())
	fmt.Printf("Page: %d\n", params.GetPage())
	fmt.Printf("Order: %s\n", params.GetOrder())
}
Output:

Limit: 10
Page: 1
Order: desc
Example (SqlOrderClause)

Example demonstrates SQL ORDER BY clause generation

package main

import (
	"fmt"

	"github.com/xraph/authsome/core/pagination"
)

func main() {
	params := &pagination.PaginationParams{
		BaseRequestParams: pagination.BaseRequestParams{
			SortBy: "name",
			Order:  pagination.SortOrderAsc,
		},
	}

	orderClause := params.GetOrderClause()
	fmt.Printf("ORDER BY %s\n", orderClause)
}
Output:

ORDER BY name ASC

Index

Examples

Constants

View Source
const (
	DefaultLimit = 10
	MaxLimit     = 10000
	MinLimit     = 1
)

Constants for pagination limits

Variables

This section is empty.

Functions

func Apply

func Apply(query *bun.SelectQuery, params *PaginationParams) *bun.SelectQuery

Apply applies all standard parameters (limit, offset, order) to a query

func ApplyAll

func ApplyAll(query *bun.SelectQuery, params *PaginationParams, searchFields ...string) *bun.SelectQuery

ApplyAll applies all parameters including search and filters

func ApplyBase

func ApplyBase(query *bun.SelectQuery, params *BaseRequestParams, searchFields ...string) *bun.SelectQuery

ApplyBase applies sorting, searching, filtering, and field selection from BaseRequestParams This is useful for non-paginated requests that still need sorting/filtering

func ApplyCursorPagination

func ApplyCursorPagination(query *bun.SelectQuery, params *CursorParams, cursorField, timestampField string) (*bun.SelectQuery, error)

ApplyCursorPagination applies cursor-based pagination to a query

func ApplyWithFilters

func ApplyWithFilters(query *bun.SelectQuery, params *PaginationParams) *bun.SelectQuery

ApplyWithFilters applies standard parameters plus filters to a query

func ApplyWithSearch

func ApplyWithSearch(query *bun.SelectQuery, params *PaginationParams, searchFields ...string) *bun.SelectQuery

ApplyWithSearch applies standard parameters plus search to a query

func EncodeCursor

func EncodeCursor(id string, timestamp time.Time, value string) (string, error)

EncodeCursor encodes cursor data into a base64 string

func ParseFilters

func ParseFilters(filterStr string) map[string]string

ParseFilters parses a filter string into a map Format: "key1:value1,key2:value2" Example: "status:active,role:admin"

func ScanAndCount

func ScanAndCount[T any](ctx context.Context, query *bun.SelectQuery, dest *[]T) (int64, error)

ScanAndCount is a helper that executes a query and returns results with total count This is useful for offset-based pagination

func SimpleCursorDecode

func SimpleCursorDecode(cursor string) (string, error)

SimpleCursorDecode decodes a simple string cursor

func SimpleCursorEncode

func SimpleCursorEncode(value string) string

SimpleCursorEncode encodes a simple string cursor

Types

type BaseRequestParams

type BaseRequestParams struct {
	SortBy string    `json:"sortBy" query:"sortBy" default:"created_at" example:"created_at" optional:"true"`
	Order  SortOrder `json:"order" query:"order" default:"desc" validate:"oneof=asc desc" example:"desc" optional:"true"`
	Search string    `json:"search" query:"search" default:"" example:"john" optional:"true"`
	Filter string    `json:"filter" query:"filter" default:"" example:"status:active" optional:"true"`
	Fields string    `json:"fields" query:"fields" default:"" example:"id,name,email" optional:"true"`
}

BaseRequestParams contains common request parameters for sorting, searching, and filtering Can be used in both paginated and non-paginated requests

func (*BaseRequestParams) GetFields

func (b *BaseRequestParams) GetFields() []string

GetFields returns the parsed list of fields to select Returns nil if no fields specified (select all)

func (*BaseRequestParams) GetOrder

func (b *BaseRequestParams) GetOrder() SortOrder

GetOrder returns the sort order with fallback

func (*BaseRequestParams) GetOrderClause

func (b *BaseRequestParams) GetOrderClause() string

GetOrderClause returns SQL ORDER BY clause

func (*BaseRequestParams) GetSortBy

func (b *BaseRequestParams) GetSortBy() string

GetSortBy returns the sort field with fallback

func (*BaseRequestParams) HasFields

func (b *BaseRequestParams) HasFields() bool

HasFields returns true if field selection is specified

func (*BaseRequestParams) Validate

func (b *BaseRequestParams) Validate() error

Validate validates and normalizes base request parameters

type CursorData

type CursorData struct {
	ID        string    `json:"id"`
	Timestamp time.Time `json:"ts"`
	Value     string    `json:"val,omitempty"` // For sorting by fields other than timestamp
}

CursorData represents the data encoded in a cursor

func DecodeCursor

func DecodeCursor(cursor string) (*CursorData, error)

DecodeCursor decodes a base64 cursor string back to CursorData

type CursorMeta

type CursorMeta struct {
	NextCursor string `json:"nextCursor,omitempty" example:"eyJpZCI6IjEyMyIsInRzIjoxNjQwMDAwMDAwfQ=="`
	PrevCursor string `json:"prevCursor,omitempty" example:"eyJpZCI6IjEwMCIsInRzIjoxNjM5OTAwMDAwfQ=="`
	HasNext    bool   `json:"hasNext" example:"true"`
	HasPrev    bool   `json:"hasPrev" example:"false"`
	Count      int    `json:"count" example:"10"`
}

CursorMeta contains cursor-based pagination metadata

type CursorParams

type CursorParams struct {
	BaseRequestParams
	Limit  int    `json:"limit" query:"limit" default:"10" validate:"min=1,max=10000" example:"10" optional:"true"`
	Cursor string `json:"cursor" query:"cursor" default:"" example:"eyJpZCI6IjEyMyIsInRzIjoxNjQwMDAwMDAwfQ==" optional:"true"`
}

CursorParams represents cursor-based pagination parameters

func (*CursorParams) GetLimit

func (c *CursorParams) GetLimit() int

GetLimit returns the limit for cursor pagination

func (*CursorParams) GetOrder

func (c *CursorParams) GetOrder() SortOrder

GetOrder returns the sort order with fallback

func (*CursorParams) GetOrderClause

func (c *CursorParams) GetOrderClause() string

GetOrderClause returns SQL ORDER BY clause

func (*CursorParams) GetSortBy

func (c *CursorParams) GetSortBy() string

GetSortBy returns the sort field with fallback

func (*CursorParams) Validate

func (c *CursorParams) Validate() error

Validate validates cursor pagination parameters

type PageMeta

type PageMeta struct {
	Total       int64 `json:"total" example:"1000"`
	Limit       int   `json:"limit" example:"10"`
	Offset      int   `json:"offset" example:"0"`
	CurrentPage int   `json:"currentPage" example:"1"`
	TotalPages  int   `json:"totalPages" example:"100"`
	HasNext     bool  `json:"hasNext" example:"true"`
	HasPrev     bool  `json:"hasPrev" example:"false"`
}

PageMeta contains offset-based pagination metadata

type PageResponse

type PageResponse[T any] struct {
	Data       []T         `json:"data"`
	Pagination *PageMeta   `json:"pagination,omitempty"`
	Cursor     *CursorMeta `json:"cursor,omitempty"`
}

PageResponse represents a paginated response with metadata

func NewCursorResponse

func NewCursorResponse[T any](data []T, nextCursor, prevCursor string, params *CursorParams) *PageResponse[T]

NewCursorResponse creates a new cursor-based paginated response

func NewEmptyCursorResponse

func NewEmptyCursorResponse[T any]() *PageResponse[T]

NewEmptyCursorResponse creates an empty cursor-based response

func NewEmptyPageResponse

func NewEmptyPageResponse[T any]() *PageResponse[T]

NewEmptyPageResponse creates an empty paginated response

func NewPageResponse

func NewPageResponse[T any](data []T, total int64, params *PaginationParams) *PageResponse[T]

NewPageResponse creates a new paginated response

type Pagination added in v0.0.3

type Pagination struct {
	Page       int `json:"page"`
	PageSize   int `json:"pageSize"`
	TotalItems int `json:"totalItems"`
	TotalPages int `json:"totalPages"`
}

Pagination is a simple pagination response struct for use in services

func (*Pagination) HasNext added in v0.0.3

func (p *Pagination) HasNext() bool

HasNext returns true if there are more pages

func (*Pagination) HasPrev added in v0.0.3

func (p *Pagination) HasPrev() bool

HasPrev returns true if there are previous pages

func (*Pagination) ToPageMeta added in v0.0.3

func (p *Pagination) ToPageMeta() *PageMeta

ToPageMeta converts Pagination to PageMeta for compatibility

type PaginationParams

type PaginationParams struct {
	BaseRequestParams
	Limit  int `json:"limit" query:"limit" default:"10" validate:"min=1,max=10000" example:"10" optional:"true"`
	Offset int `json:"offset" query:"offset" default:"0" validate:"min=0" example:"0" optional:"true"`
	Page   int `json:"page" query:"page" default:"1" validate:"min=1" example:"1" optional:"true"`
}

PaginationParams represents offset-based pagination request parameters

func (*PaginationParams) GetLimit

func (p *PaginationParams) GetLimit() int

GetLimit returns the limit with fallback to default

func (*PaginationParams) GetOffset

func (p *PaginationParams) GetOffset() int

GetOffset returns the calculated offset

Example

ExamplePaginationParams_GetOffset demonstrates offset calculation

package main

import (
	"fmt"

	"github.com/xraph/authsome/core/pagination"
)

func main() {
	// Using page number
	params := &pagination.PaginationParams{
		Limit: 10,
		Page:  3,
	}

	offset := params.GetOffset()
	fmt.Printf("Page 3 offset: %d\n", offset)
}
Output:

Page 3 offset: 20

func (*PaginationParams) GetOrder

func (p *PaginationParams) GetOrder() SortOrder

GetOrder returns the sort order with fallback

func (*PaginationParams) GetOrderClause

func (p *PaginationParams) GetOrderClause() string

GetOrderClause returns SQL ORDER BY clause

func (*PaginationParams) GetPage

func (p *PaginationParams) GetPage() int

GetPage returns the current page number

Example

ExamplePaginationParams_GetPage demonstrates page calculation

package main

import (
	"fmt"

	"github.com/xraph/authsome/core/pagination"
)

func main() {
	// Using offset
	params := &pagination.PaginationParams{
		Limit:  10,
		Offset: 50,
	}

	page := params.GetPage()
	fmt.Printf("Offset 50 page: %d\n", page)
}
Output:

Offset 50 page: 6

func (*PaginationParams) GetSortBy

func (p *PaginationParams) GetSortBy() string

GetSortBy returns the sort field with fallback

func (*PaginationParams) Validate

func (p *PaginationParams) Validate() error

Validate validates and normalizes pagination parameters

type QueryBuilder

type QueryBuilder struct {
	// contains filtered or unexported fields
}

QueryBuilder provides methods to apply pagination parameters to Bun queries

func NewQueryBuilder

func NewQueryBuilder(params interface{}) *QueryBuilder

NewQueryBuilder creates a new QueryBuilder from pagination parameters

func (*QueryBuilder) ApplyCursor

func (qb *QueryBuilder) ApplyCursor(query *bun.SelectQuery, cursorData *CursorData, cursorField, timestampField string) *bun.SelectQuery

ApplyCursor applies cursor-based pagination to a Bun query cursorData should be obtained from DecodeCursor() cursorField is the field to use for cursor comparison (default: "id") timestampField is the timestamp field (default: "created_at")

func (*QueryBuilder) ApplyFields

func (qb *QueryBuilder) ApplyFields(query *bun.SelectQuery) *bun.SelectQuery

ApplyFields applies field selection to a Bun query Only selects specified fields if Fields parameter is set

func (*QueryBuilder) ApplyFilters

func (qb *QueryBuilder) ApplyFilters(query *bun.SelectQuery, filters map[string]string) *bun.SelectQuery

ApplyFilters applies parsed filters to a Bun query filters should be from ParseFilters() It applies exact match filters as WHERE conditions

func (*QueryBuilder) ApplyLimit

func (qb *QueryBuilder) ApplyLimit(query *bun.SelectQuery) *bun.SelectQuery

ApplyLimit applies the limit parameter to a Bun query Note: BaseRequestParams does not have a Limit field (not all requests need pagination)

func (*QueryBuilder) ApplyOffset

func (qb *QueryBuilder) ApplyOffset(query *bun.SelectQuery) *bun.SelectQuery

ApplyOffset applies the offset parameter to a Bun query Only works with PaginationParams (not cursor-based)

func (*QueryBuilder) ApplyOrder

func (qb *QueryBuilder) ApplyOrder(query *bun.SelectQuery) *bun.SelectQuery

ApplyOrder applies the order parameter to a Bun query

func (*QueryBuilder) ApplySearch

func (qb *QueryBuilder) ApplySearch(query *bun.SelectQuery, searchFields ...string) *bun.SelectQuery

ApplySearch applies a search filter to a Bun query searchFields should be the column names to search in Example: ApplySearch(query, "name", "email", "username") Note: Uses LOWER() for case-insensitive search (works with all databases)

func (*QueryBuilder) ApplyToQuery

func (qb *QueryBuilder) ApplyToQuery(query *bun.SelectQuery) *bun.SelectQuery

ApplyToQuery applies all pagination parameters to a Bun query This is a convenience method that calls ApplyLimit, ApplyOffset, ApplyOrder, and ApplyFields

type SortOrder

type SortOrder string

SortOrder represents the sort direction

const (
	SortOrderAsc  SortOrder = "asc"
	SortOrderDesc SortOrder = "desc"
)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL