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 }