code.vegaprotocol.io/vega@v0.79.0/datanode/entities/pagination.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package entities
    17  
    18  import (
    19  	"encoding/base64"
    20  
    21  	"code.vegaprotocol.io/vega/libs/ptr"
    22  	v2 "code.vegaprotocol.io/vega/protos/data-node/api/v2"
    23  
    24  	"github.com/pkg/errors"
    25  )
    26  
    27  const (
    28  	DefaultPageSize int32 = 1000
    29  	maxPageSize     int32 = 5000
    30  )
    31  
    32  var ErrCursorOverflow = errors.Errorf("pagination limit must be in range 0-%d", maxPageSize)
    33  
    34  type Pagination interface{}
    35  
    36  type Cursor struct {
    37  	cursor string
    38  }
    39  
    40  func NewCursor(cursor string) *Cursor {
    41  	return &Cursor{
    42  		cursor: cursor,
    43  	}
    44  }
    45  
    46  func (c *Cursor) Encode() string {
    47  	if c.cursor == "" {
    48  		return ""
    49  	}
    50  
    51  	return base64.StdEncoding.EncodeToString([]byte(c.cursor))
    52  }
    53  
    54  func (c *Cursor) Decode(value string) error {
    55  	if value == "" {
    56  		return errors.New("cursor is empty")
    57  	}
    58  
    59  	cursor, err := base64.StdEncoding.DecodeString(value)
    60  	if err != nil {
    61  		return errors.Wrap(err, "failed to decode cursor")
    62  	}
    63  
    64  	c.cursor = string(cursor)
    65  
    66  	return nil
    67  }
    68  
    69  func (c *Cursor) IsSet() bool {
    70  	return c.cursor != ""
    71  }
    72  
    73  func (c *Cursor) Value() string {
    74  	return c.cursor
    75  }
    76  
    77  type CursorPagination struct {
    78  	Pagination
    79  	Forward     *CursorOffset
    80  	Backward    *CursorOffset
    81  	NewestFirst bool
    82  }
    83  
    84  func (p CursorPagination) HasForward() bool {
    85  	return p.Forward != nil
    86  }
    87  
    88  func (p CursorPagination) HasBackward() bool {
    89  	return p.Backward != nil
    90  }
    91  
    92  func NewCursorPagination(first *int32, after *string, last *int32, before *string, newestFirst bool) (CursorPagination, error) {
    93  	return CursorPaginationFromProto(&v2.Pagination{
    94  		First:       first,
    95  		After:       after,
    96  		Last:        last,
    97  		Before:      before,
    98  		NewestFirst: &newestFirst,
    99  	})
   100  }
   101  
   102  func CursorPaginationFromProto(cp *v2.Pagination) (CursorPagination, error) {
   103  	if cp == nil || (cp != nil && cp.First == nil && cp.Last == nil && cp.After == nil && cp.Before == nil) {
   104  		if cp != nil && cp.NewestFirst != nil {
   105  			return DefaultCursorPagination(*cp.NewestFirst), nil
   106  		}
   107  		return DefaultCursorPagination(true), nil
   108  	}
   109  
   110  	var after, before Cursor
   111  	var err error
   112  	var forwardOffset, backwardOffset *CursorOffset
   113  
   114  	if cp.Before != nil && cp.After != nil {
   115  		return CursorPagination{}, errors.New("cannot set both a before and after cursor")
   116  	}
   117  
   118  	if cp.First != nil {
   119  		if *cp.First < 0 || *cp.First > maxPageSize {
   120  			return CursorPagination{}, ErrCursorOverflow
   121  		}
   122  		forwardOffset = &CursorOffset{
   123  			Limit: cp.First,
   124  		}
   125  		// Proto cursors should be encoded values, so we want to decode them in order to use them
   126  		if cp.After != nil {
   127  			if err = after.Decode(*cp.After); err != nil {
   128  				return CursorPagination{}, errors.Wrap(err, "failed to decode after cursor")
   129  			}
   130  
   131  			forwardOffset.Cursor = &after
   132  		}
   133  	} else if cp.Last != nil {
   134  		if *cp.Last < 0 || *cp.Last > maxPageSize {
   135  			return CursorPagination{}, ErrCursorOverflow
   136  		}
   137  		backwardOffset = &CursorOffset{
   138  			Limit: cp.Last,
   139  		}
   140  		// Proto cursors should be encoded values, so we want to decode them in order to use them
   141  		if cp.Before != nil {
   142  			if err = before.Decode(*cp.Before); err != nil {
   143  				return CursorPagination{}, errors.Wrap(err, "failed to decode before cursor")
   144  			}
   145  			backwardOffset.Cursor = &before
   146  		}
   147  	} else if cp.After != nil {
   148  		// Have an 'after' cursor but no page size ('first') so use default
   149  		if err = after.Decode(*cp.After); err != nil {
   150  			return CursorPagination{}, errors.Wrap(err, "failed to decode after cursor")
   151  		}
   152  
   153  		forwardOffset = &CursorOffset{
   154  			Limit:  ptr.From(DefaultPageSize),
   155  			Cursor: &after,
   156  		}
   157  	} else if cp.Before != nil {
   158  		// Have an 'before' cursor but no page size ('first') so use default
   159  		if err = before.Decode(*cp.Before); err != nil {
   160  			return CursorPagination{}, errors.Wrap(err, "failed to decode before cursor")
   161  		}
   162  
   163  		backwardOffset = &CursorOffset{
   164  			Limit:  ptr.From(DefaultPageSize),
   165  			Cursor: &before,
   166  		}
   167  	}
   168  
   169  	// Default the sort order to return the newest records first if no sort order is provided
   170  	newestFirst := true
   171  	if cp.NewestFirst != nil {
   172  		newestFirst = *cp.NewestFirst
   173  	}
   174  
   175  	pagination := CursorPagination{
   176  		Forward:     forwardOffset,
   177  		Backward:    backwardOffset,
   178  		NewestFirst: newestFirst,
   179  	}
   180  
   181  	if err = validatePagination(pagination); err != nil {
   182  		return CursorPagination{}, err
   183  	}
   184  
   185  	return pagination, nil
   186  }
   187  
   188  func DefaultCursorPagination(newestFirst bool) CursorPagination {
   189  	return CursorPagination{
   190  		Forward: &CursorOffset{
   191  			Limit: ptr.From(DefaultPageSize),
   192  		},
   193  		NewestFirst: newestFirst,
   194  	}
   195  }
   196  
   197  type CursorOffset struct {
   198  	Limit  *int32
   199  	Cursor *Cursor
   200  }
   201  
   202  func (o CursorOffset) IsSet() bool {
   203  	return o.Limit != nil
   204  }
   205  
   206  func (o CursorOffset) HasCursor() bool {
   207  	return o.Cursor != nil && o.Cursor.IsSet()
   208  }
   209  
   210  func validatePagination(pagination CursorPagination) error {
   211  	if pagination.HasForward() && pagination.HasBackward() {
   212  		return errors.New("cannot provide both forward and backward cursors")
   213  	}
   214  
   215  	var cursorOffset CursorOffset
   216  
   217  	if pagination.HasForward() {
   218  		cursorOffset = *pagination.Forward
   219  	} else if pagination.HasBackward() {
   220  		cursorOffset = *pagination.Backward
   221  	} else {
   222  		// no pagination is provided is okay
   223  		return nil
   224  	}
   225  
   226  	limit := *cursorOffset.Limit
   227  	if limit <= 0 || limit > maxPageSize {
   228  		return ErrCursorOverflow
   229  	}
   230  
   231  	return nil
   232  }
   233  
   234  type PageInfo struct {
   235  	HasNextPage     bool
   236  	HasPreviousPage bool
   237  	StartCursor     string
   238  	EndCursor       string
   239  }
   240  
   241  func (p PageInfo) ToProto() *v2.PageInfo {
   242  	return &v2.PageInfo{
   243  		HasNextPage:     p.HasNextPage,
   244  		HasPreviousPage: p.HasPreviousPage,
   245  		StartCursor:     p.StartCursor,
   246  		EndCursor:       p.EndCursor,
   247  	}
   248  }