diff --git a/compose/rest.yaml b/compose/rest.yaml index 90ba78f16..ce0c5df37 100644 --- a/compose/rest.yaml +++ b/compose/rest.yaml @@ -505,15 +505,9 @@ endpoints: - type: uint name: limit title: Limit - - type: uint - name: offset - title: Offset - - type: uint - name: page - title: Page number (1-based) - - type: uint - name: perPage - title: Returned items per page (default 50) + - type: string + name: pageCursor + title: Page cursor - type: string name: sort title: Sort items @@ -903,15 +897,9 @@ endpoints: - type: uint name: limit title: Limit - - type: uint - name: offset - title: Offset - - type: uint - name: page - title: Page number (1-based) - - type: uint - name: perPage - title: Returned items per page (default 50) + - type: string + name: pageCursor + title: Page cursor - name: read path: "/{attachmentID}" method: GET diff --git a/compose/rest/attachment.go b/compose/rest/attachment.go index e057b51c7..2f036255c 100644 --- a/compose/rest/attachment.go +++ b/compose/rest/attachment.go @@ -51,8 +51,6 @@ func (ctrl Attachment) List(ctx context.Context, r *request.AttachmentList) (int ModuleID: r.ModuleID, RecordID: r.RecordID, FieldName: r.FieldName, - - //PageFilter: rh.Paging(r), } set, filter, err := ctrl.attachment.With(ctx).Find(f) diff --git a/compose/rest/record.go b/compose/rest/record.go index aa3c41137..5415d4ea3 100644 --- a/compose/rest/record.go +++ b/compose/rest/record.go @@ -78,14 +78,14 @@ func (ctrl *Record) List(ctx context.Context, r *request.RecordList) (interface{ m *types.Module err error - rf = types.RecordFilter{ + f = types.RecordFilter{ NamespaceID: r.NamespaceID, ModuleID: r.ModuleID, Deleted: filter.State(r.Deleted), } ) - if err = rf.Sort.Set(r.Sort); err != nil { + if err = f.Sort.Set(r.Sort); err != nil { return nil, err } @@ -95,14 +95,22 @@ func (ctrl *Record) List(ctx context.Context, r *request.RecordList) (interface{ if r.Query != "" { // Query param takes preference - rf.Query = r.Query + f.Query = r.Query } else if r.Filter != "" { // Backward compatibility // Filter param is deprecated - rf.Query = r.Filter + f.Query = r.Filter } - rr, filter, err := ctrl.record.With(ctx).Find(rf) + if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil { + return nil, err + } + + if f.Sorting, err = filter.NewSorting(r.Sort); err != nil { + return nil, err + } + + rr, filter, err := ctrl.record.With(ctx).Find(f) return ctrl.makeFilterPayload(ctx, m, rr, &filter, err) } diff --git a/compose/rest/request/attachment.go b/compose/rest/request/attachment.go index b08f86ff4..51dd4d9a6 100644 --- a/compose/rest/request/attachment.go +++ b/compose/rest/request/attachment.go @@ -75,20 +75,10 @@ type ( // Limit Limit uint - // Offset GET parameter + // PageCursor GET parameter // - // Offset - Offset uint - - // Page GET parameter - // - // Page number (1-based) - Page uint - - // PerPage GET parameter - // - // Returned items per page (default 50) - PerPage uint + // Page cursor + PageCursor string } AttachmentRead struct { @@ -232,9 +222,7 @@ func (r AttachmentList) Auditable() map[string]interface{} { "recordID": r.RecordID, "fieldName": r.FieldName, "limit": r.Limit, - "offset": r.Offset, - "page": r.Page, - "perPage": r.PerPage, + "pageCursor": r.PageCursor, } } @@ -284,18 +272,8 @@ func (r AttachmentList) GetLimit() uint { } // Auditable returns all auditable/loggable parameters -func (r AttachmentList) GetOffset() uint { - return r.Offset -} - -// Auditable returns all auditable/loggable parameters -func (r AttachmentList) GetPage() uint { - return r.Page -} - -// Auditable returns all auditable/loggable parameters -func (r AttachmentList) GetPerPage() uint { - return r.PerPage +func (r AttachmentList) GetPageCursor() string { + return r.PageCursor } // Fill processes request and fills internal variables @@ -357,20 +335,8 @@ func (r *AttachmentList) Fill(req *http.Request) (err error) { return err } } - if val, ok := tmp["offset"]; ok && len(val) > 0 { - r.Offset, err = payload.ParseUint(val[0]), nil - if err != nil { - return err - } - } - if val, ok := tmp["page"]; ok && len(val) > 0 { - r.Page, err = payload.ParseUint(val[0]), nil - if err != nil { - return err - } - } - if val, ok := tmp["perPage"]; ok && len(val) > 0 { - r.PerPage, err = payload.ParseUint(val[0]), nil + if val, ok := tmp["pageCursor"]; ok && len(val) > 0 { + r.PageCursor, err = val[0], nil if err != nil { return err } diff --git a/compose/rest/request/record.go b/compose/rest/request/record.go index e8d8e2070..c3109e872 100644 --- a/compose/rest/request/record.go +++ b/compose/rest/request/record.go @@ -88,20 +88,10 @@ type ( // Limit Limit uint - // Offset GET parameter + // PageCursor GET parameter // - // Offset - Offset uint - - // Page GET parameter - // - // Page number (1-based) - Page uint - - // PerPage GET parameter - // - // Returned items per page (default 50) - PerPage uint + // Page cursor + PageCursor string // Sort GET parameter // @@ -519,9 +509,7 @@ func (r RecordList) Auditable() map[string]interface{} { "filter": r.Filter, "deleted": r.Deleted, "limit": r.Limit, - "offset": r.Offset, - "page": r.Page, - "perPage": r.PerPage, + "pageCursor": r.PageCursor, "sort": r.Sort, } } @@ -557,18 +545,8 @@ func (r RecordList) GetLimit() uint { } // Auditable returns all auditable/loggable parameters -func (r RecordList) GetOffset() uint { - return r.Offset -} - -// Auditable returns all auditable/loggable parameters -func (r RecordList) GetPage() uint { - return r.Page -} - -// Auditable returns all auditable/loggable parameters -func (r RecordList) GetPerPage() uint { - return r.PerPage +func (r RecordList) GetPageCursor() string { + return r.PageCursor } // Auditable returns all auditable/loggable parameters @@ -617,20 +595,8 @@ func (r *RecordList) Fill(req *http.Request) (err error) { return err } } - if val, ok := tmp["offset"]; ok && len(val) > 0 { - r.Offset, err = payload.ParseUint(val[0]), nil - if err != nil { - return err - } - } - if val, ok := tmp["page"]; ok && len(val) > 0 { - r.Page, err = payload.ParseUint(val[0]), nil - if err != nil { - return err - } - } - if val, ok := tmp["perPage"]; ok && len(val) > 0 { - r.PerPage, err = payload.ParseUint(val[0]), nil + if val, ok := tmp["pageCursor"]; ok && len(val) > 0 { + r.PageCursor, err = val[0], nil if err != nil { return err } diff --git a/compose/types/attachment.go b/compose/types/attachment.go index e06be5029..be051a21b 100644 --- a/compose/types/attachment.go +++ b/compose/types/attachment.go @@ -3,6 +3,7 @@ package types import ( "database/sql/driver" "encoding/json" + "github.com/cortezaproject/corteza-server/pkg/filter" "time" "github.com/pkg/errors" @@ -42,6 +43,9 @@ type ( // // Store then loads additional resources to satisfy the paging parameters Check func(*Attachment) (bool, error) + + // Standard helpers for paging and sorting + filter.Paging } attachmentImageMeta struct { diff --git a/pkg/filter/pagination.go b/pkg/filter/pagination.go index 01fa3caf2..b039649e0 100644 --- a/pkg/filter/pagination.go +++ b/pkg/filter/pagination.go @@ -85,7 +85,7 @@ func (p *PagingCursor) String() string { } func (p *PagingCursor) MarshalJSON() ([]byte, error) { - return json.Marshal(struct { + buf, err := json.Marshal(struct { K []string V []interface{} D []bool @@ -96,11 +96,26 @@ func (p *PagingCursor) MarshalJSON() ([]byte, error) { p.desc, p.Reverse, }) + + if err != nil { + return nil, err + } + + std := base64.StdEncoding + dbuf := make([]byte, std.EncodedLen(len(buf))) + std.Encode(dbuf, buf) + + return append([]byte{'"'}, append(dbuf, '"')...), nil +} + +func (p *PagingCursor) Encode() string { + b, _ := p.MarshalJSON() + return string(b) } func (p *PagingCursor) UnmarshalJSON(in []byte) error { var ( - aux *struct { + aux struct { K []string V []interface{} D []bool @@ -108,7 +123,7 @@ func (p *PagingCursor) UnmarshalJSON(in []byte) error { } ) - if err := json.Unmarshal(in, aux); err != nil { + if err := json.Unmarshal(in, &aux); err != nil { return err } @@ -120,6 +135,20 @@ func (p *PagingCursor) UnmarshalJSON(in []byte) error { return nil } +func (p *PagingCursor) Decode(cursor string) error { + b, err := base64.StdEncoding.DecodeString(cursor) + if err != nil { + return err + } + + err = p.UnmarshalJSON(b) + if err != nil { + return err + } + + return err +} + func parseCursor(in string) (p *PagingCursor, err error) { if len(in) == 0 { return nil, nil @@ -130,7 +159,8 @@ func parseCursor(in string) (p *PagingCursor, err error) { return nil, fmt.Errorf("could not decode cursor: %w", err) } - if err = json.Unmarshal(buf, p); err != nil { + p = &PagingCursor{} + if err = p.UnmarshalJSON(buf); err != nil { return nil, fmt.Errorf("could not decode cursor: %w", err) } diff --git a/tests/system/user_test.go b/tests/system/user_test.go index 21a51bad3..3b8dcdcff 100644 --- a/tests/system/user_test.go +++ b/tests/system/user_test.go @@ -90,6 +90,61 @@ func TestUserListAll(t *testing.T) { End() } +func TestUserListWithPaging(t *testing.T) { + h := newHelper(t) + h.clearUsers() + + h.secCtx() + + seedCount := 40 + for i := 0; i < seedCount; i++ { + h.createUserWithEmail(h.randEmail()) + } + + h.allow(types.UserRBACResource.AppendWildcard(), "read") + + var aux = struct { + Response struct { + Filter struct { + NextPage *string + PrevPage *string + } + } + }{} + + h.apiInit(). + Get("/users/"). + Query("limit", "13"). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + Assert(jsonpath.Present(`$.response.filter`)). + Assert(jsonpath.Present(`$.response.set`)). + Assert(jsonpath.Len(`$.response.set`, 13)). + Assert(jsonpath.Present(`$.response.filter.nextPage`)). + End(). + JSON(&aux) + + h.a.NotNil(aux.Response.Filter.NextPage) + + h.apiInit(). + Debug(). + Get("/users/"). + Query("limit", "13"). + Query("pageCursor", *aux.Response.Filter.NextPage). + Expect(t). + Status(http.StatusOK). + Assert(jsonpath.Len(`$.response.set`, 13)). + Assert(jsonpath.Present(`$.response.filter.prevPage`)). + Assert(jsonpath.Present(`$.response.filter.nextPage`)). + End(). + JSON(&aux) + + h.a.NotNil(aux.Response.Filter.PrevPage) + h.a.NotNil(aux.Response.Filter.NextPage) + +} + func TestUserList_filterForbidden(t *testing.T) { h := newHelper(t) h.clearUsers()