Skip to content

Commit

Permalink
feat: add pagination validation
Browse files Browse the repository at this point in the history
  • Loading branch information
robinmuhia committed Nov 20, 2024
1 parent e4a0654 commit be71a4d
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 46 deletions.
24 changes: 5 additions & 19 deletions firestore.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,20 +37,6 @@ func SuffixCollection(c string) string {
return fmt.Sprintf("%v_bewell_%v", c, GetFirestoreEnvironmentSuffix())
}

// ValidatePaginationParameters ensures that the supplied pagination parameters make sense
func ValidatePaginationParameters(pagination *PaginationInput) error {
if pagination == nil {
return nil // not having pagination is not a fatal error
}

// if `first` is specified, `last` cannot be specified
if pagination.First > 0 && pagination.Last > 0 {
return fmt.Errorf("if `first` is specified for pagination, `last` cannot be specified")
}

return nil
}

// OpString translates between an Operation enum value and the appropriate firestore
// query operator
func OpString(op enumutils.Operation) (string, error) {
Expand Down Expand Up @@ -165,16 +151,16 @@ func QueryNodes(
// pagination
pageSize := DefaultPageSize
if pagination != nil {
if pagination.First > 0 {
if pagination.First != nil {
if pagination.After != "" {
query = query.OrderBy("id", firestore.Asc).StartAfter(pagination.After).Limit(pagination.First)
query = query.OrderBy("id", firestore.Asc).StartAfter(pagination.After).Limit(*pagination.First)
}
pageSize = pagination.First
pageSize = *pagination.First
query = query.Limit(pageSize)
}
if pagination.Last > 0 {
if pagination.Last != nil {
if pagination.Before != "" {
query = query.OrderBy("id", firestore.Asc).EndAt(pagination.Before).LimitToLast(pagination.Last)
query = query.OrderBy("id", firestore.Asc).EndAt(pagination.Before).LimitToLast(*pagination.Last)
}
}
}
Expand Down
27 changes: 13 additions & 14 deletions firestore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,46 +56,42 @@ func Test_validatePaginationParameters(t *testing.T) {
}{
"first_last_specified": {
pagination: &fb.PaginationInput{
First: first,
Last: last,
First: &first,
Last: &last,
},
expectError: true,
expectedErrorMessage: "if `first` is specified for pagination, `last` cannot be specified",
expectedErrorMessage: "cannot provide both first and last",
},
"first_only": {
pagination: &fb.PaginationInput{
First: first,
First: &first,
},
expectError: false,
},
"last_only": {
pagination: &fb.PaginationInput{
Last: last,
Last: &last,
},
expectError: false,
},
"first_and_after": {
pagination: &fb.PaginationInput{
First: first,
First: &first,
After: after,
},
expectError: false,
},
"last_and_before": {
pagination: &fb.PaginationInput{
Last: last,
Last: &last,
Before: before,
},
expectError: false,
},
"nil_pagination": {
pagination: nil,
expectError: false,
},
}
for name, tc := range tests {
t.Run(name, func(t *testing.T) {
err := fb.ValidatePaginationParameters(tc.pagination)
err := tc.pagination.Validate()
if tc.expectError {
assert.NotNil(t, err)
assert.Contains(t, err.Error(), tc.expectedErrorMessage)
Expand Down Expand Up @@ -609,6 +605,9 @@ func TestRetrieveNode(t *testing.T) {
}

func TestQueryNodes(t *testing.T) {
first := 10
last := 1

ctx := context.Background()
node := &fb.Model{
Name: "test model instance",
Expand Down Expand Up @@ -657,7 +656,7 @@ func TestQueryNodes(t *testing.T) {
args: args{
ctx: ctx,
pagination: &fb.PaginationInput{
First: 10,
First: &first,
After: id,
},
filter: nil,
Expand All @@ -671,7 +670,7 @@ func TestQueryNodes(t *testing.T) {
args: args{
ctx: ctx,
pagination: &fb.PaginationInput{
Last: 1,
Last: &last,
Before: id,
},
filter: nil,
Expand Down
13 changes: 9 additions & 4 deletions helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,16 @@ func GetAPIPaginationParams(pagination *PaginationInput) (url.Values, error) {
// `first` will supersede `last`
var err error
pageSize := DefaultRESTAPIPageSize
if pagination.Last > 0 {
pageSize = pagination.Last

err = pagination.Validate()
if err != nil {
return url.Values{}, nil
}
if pagination.First > 0 {
pageSize = pagination.First

if pagination.Last != nil {
pageSize = *pagination.Last
} else if pagination.First != nil {
pageSize = *pagination.First
}

// For these "pass through APIs", "after" and "before" should be parseable as ints
Expand Down
47 changes: 38 additions & 9 deletions model.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,57 @@ import (
"github.com/savannahghi/enumutils"
)

const defaultPageCount = 10

// QueryParam is an interface used for filter and sort parameters
type QueryParam interface {
ToURLValues() (values url.Values)
}

// PaginationInput represents paging parameters
type PaginationInput struct {
First int `json:"first"`
Last int `json:"last"`
First *int `json:"first"`
Last *int `json:"last"`
After string `json:"after"`
Before string `json:"before"`
}

//IsEntity ...
// IsEntity ...
func (p PaginationInput) IsEntity() {}

func (p *PaginationInput) Validate() error {
if p.First != nil && p.Last != nil {
return fmt.Errorf("cannot provide both first and last")
}

if p.First != nil {
first := *p.First
if first <= 0 {
return fmt.Errorf("first cannot be less than 0")
}
}

if p.Last != nil {
last := *p.Last
if last <= 0 {
return fmt.Errorf("last cannot be less than 0")
}
}

if p.First == nil && p.Last == nil {
limit := defaultPageCount
p.First = &limit
}

return nil
}

// SortInput is a generic container for strongly typed sorting parameters
type SortInput struct {
SortBy []*SortParam `json:"sortBy"`
}

//IsEntity ...
// IsEntity ...
func (s SortInput) IsEntity() {}

// SortParam represents a single field sort parameter
Expand All @@ -41,7 +70,7 @@ type SortParam struct {
SortOrder enumutils.SortOrder `json:"sortOrder"`
}

//IsEntity ...
// IsEntity ...
func (s SortParam) IsEntity() {}

// FilterInput is s generic container for strongly type filter parameters
Expand All @@ -50,7 +79,7 @@ type FilterInput struct {
FilterBy []*FilterParam `json:"filterBy"`
}

//IsEntity ...
// IsEntity ...
func (f FilterInput) IsEntity() {}

// FilterParam represents a single field filter parameter
Expand All @@ -61,7 +90,7 @@ type FilterParam struct {
FieldValue interface{} `json:"fieldValue"`
}

//IsEntity ...
// IsEntity ...
func (f FilterParam) IsEntity() {}

// FirebaseRefreshResponse is used to (de)serialize the results of a successful Firebase token refresh
Expand Down Expand Up @@ -114,7 +143,7 @@ type PageInfo struct {
EndCursor *string `json:"endCursor"`
}

//IsEntity ...
// IsEntity ...
func (p PageInfo) IsEntity() {}

// NewString returns a pointer to the supplied string.
Expand Down Expand Up @@ -170,7 +199,7 @@ func (c *Model) SetID(id string) {
c.ID = id
}

//IsEntity ...
// IsEntity ...
func (c Model) IsEntity() {}

// LoginResponse is used to (de)serialize the result of a successful login
Expand Down

0 comments on commit be71a4d

Please sign in to comment.