No description
  • Go 99.4%
  • Makefile 0.6%
Find a file
2025-05-15 07:04:11 -07:00
.github PCI change to public checklist 2025-05-14 16:23:16 -07:00
cursor solution for paginating aggregated model 2021-07-02 15:37:25 -07:00
example Revert back to gorm v1 2021-05-20 17:27:06 -07:00
internal/util solution for paginating aggregated model 2021-07-02 15:37:25 -07:00
META.d fix: heimdall owner (#5) 2024-10-31 08:56:37 -04:00
paginator small refactoring 2021-07-06 11:22:40 -07:00
.gitignore add example 2020-02-16 11:20:57 +08:00
.travis.yml fix travis 2021-04-12 11:35:11 +08:00
docker-compose.yml fix join query bug and refine cursor interface 2019-06-22 03:05:15 +08:00
go.mod Upgrade YAML library version. (#8) 2025-02-21 14:17:46 -06:00
go.sum Upgrade YAML library version. (#8) 2025-02-21 14:17:46 -06:00
LICENSE update README 2021-05-07 17:10:22 +08:00
Makefile restructure package 2021-04-12 11:32:53 +08:00
README.md Revert back to gorm v1 2021-05-20 17:27:06 -07:00

gorm-cursor-paginator Build Status Coverage Status Go Report Card Codacy Badge

A paginator doing cursor-based pagination based on GORM

This doc is for v2, which uses GORM v2. If you are using GORM v1, please checkout v1 doc.

Features

  • Query extendable.
  • Multiple paging keys.
  • Paging rule customization (e.g., order, SQL representation) for each key.
  • GORM column tag supported.
  • Error handling enhancement.
  • Exporting cursor module for advanced usage.

Installation

go get -u github.com/hashicorp/gorm-cursor-paginator/v2

Usage By Example

Given a User model:

type User struct {
    ID          int
    JoinedAt    time.Time `gorm:"column:created_at"`
}

We need to construct a paginator.Paginator based on fields of User struct. First we import paginator:

import (
   "github.com/hashicorp/gorm-cursor-paginator/v2/paginator"
)

Then we can start configuring paginator.Paginator, here are some useful patterns:

// configure paginator with paginator.Config and paginator.Option
func UserPaginator(
    cursor paginator.Cursor, 
    order *paginator.Order,
    limit *int,
) *paginator.Paginator {
    opts := []paginator.Option{
        &paginator.Config{
            // keys should be ordered by ordering priority
            Keys: []string{"ID", "JoinedAt"}, // default: []string{"ID"}
            Limit: 5, // default: 10
            Order: paginator.ASC, // default: DESC
        },
    }
    if limit != nil {
        opts = append(opts, paginator.WithLimit(*limit))
    }
    if order != nil {
        opts = append(opts, paginator.WithOrder(*order))
    }
    if cursor.After != nil {
        opts = append(opts, paginator.WithAfter(*cursor.After))
    }
    if cursor.Before != nil {
        opts = append(opts, paginator.WithBefore(*cursor.Before))
    }
    return paginator.New(opts...)
}

// configure paginator with setters
func UserPaginator(
    cursor paginator.Cursor,
    order *paginator.Order, 
    limit *int,
) *paginator.Paginator {
    p := paginator.New(
        paginator.WithKeys("ID", "JoinedAt"),
        paginator.WithLimit(5),
        paginator.WithOrder(paginator.ASC),
    )
    if order != nil {
        p.SetOrder(*order)
    }
    if limit != nil {
        p.SetLimit(*limit)
    }
    if cursor.After != nil {
        p.SetAfter(*cursor.After)
    }
    if cursor.Before != nil {
        p.SetBefore(*cursor.Before)
    }
    return p
}

If you need fine grained setting for each key, you can use paginator.Rule:

SQLRepr is especially useful when you have JOIN or table alias in your SQL query. If SQLRepr is not specified, paginator will use the table name from paginated model, plus table key derived by below rules to form the SQL query:

  1. Search GORM tag column on struct field.
  2. If tag not found, convert struct field name to snake case.
func UserPaginator(/* ... */) {
    opts := []paginator.Option{
        &paginator.Config{
            Rules: []paginator.Rule{
                {
                    Key: "ID",
                },
                {
                    Key: "JoinedAt",
                    Order: paginator.ASC,
                    SQLRepr: "users.created_at",
                },
            },
            Limit: 5,
            Order: paginator.DESC, // outer order will apply to keys without order specified, in this example is the key "ID".
        },
    }
    // ...
    return paginator.New(opts...)
}

After setup, you can start paginating with GORM:

func FindUsers(db *gorm.DB, query Query) ([]User, paginator.Cursor, error) {
    var users []User

    // extend query before paginating
    stmt := db.
        Select(/* fields */).
        Joins(/* joins */).
        Where(/* queries */)

    // find users with pagination
    result, cursor, err := UserPaginator(/* config */).Paginate(stmt, &users)

    // this is paginator error, e.g., invalid cursor
    if err != nil {
        return nil, paginator.Cursor{}, err
    }

    // this is gorm error
    if result.Error != nil {
        return nil, paginator.Cursor{}, result.Error
    }

    return users, cursor, nil
}

The second value returned from paginator.Paginator.Paginate is a paginator.Cursor, which is a re-exported struct from cursor.Cursor:

type Cursor struct {
    After  *string `json:"after" query:"after"`
    Before *string `json:"before" query:"before"`
}

That's all! Enjoy paginating in the GORM world. 🎉

For more paginating examples, please checkout exmaple/main.go and paginator/paginator_paginate_test.go

For manually encoding/decoding cursor exmaples, please checkout cursor/encoding_test.go

Known Issues

  1. Please make sure you're not paginating by nullable fields. Nullable values would occur NULLS { FIRST | LAST } problems. Current workaround recommended is to select only non-null fields for paginating, or filter null values beforehand:

    stmt = db.Where("nullable_field IS NOT NULL")
    

License

© Cyan Ho (pilagod), 2018-NOW

Released under the MIT License