Skip to content

derhoerit/gorm-cursor-paginator

 
 

Repository files navigation

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

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/pilagod/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/pilagod/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

About

A paginator doing cursor-based pagination based on GORM

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages

  • Go 99.4%
  • Makefile 0.6%