Skip to content
This repository has been archived by the owner on Dec 12, 2024. It is now read-only.

Commit

Permalink
Support pagination for submissions (#537)
Browse files Browse the repository at this point in the history
* Support submission pagination

* spec

* PR comments
  • Loading branch information
andresuribe87 authored Jun 15, 2023
1 parent c8541a8 commit e3550d6
Show file tree
Hide file tree
Showing 18 changed files with 406 additions and 128 deletions.
14 changes: 14 additions & 0 deletions doc/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1611,6 +1611,10 @@ definitions:
type: object
pkg_server_router.ListSubmissionResponse:
properties:
nextPageToken:
description: Pagination token to retrieve the next page of results. If the
value is "", it means no further results for the request.
type: string
submissions:
items:
$ref: '#/definitions/github_com_tbd54566975_ssi-service_pkg_service_presentation_model.Submission'
Expand Down Expand Up @@ -3417,6 +3421,16 @@ paths:
in: query
name: filter
type: string
- description: Hint to the server of the maximum elements to return. More may
be returned. When not set, the server will return all elements.
in: query
name: pageSize
type: number
- description: Used to indicate to the server to return a specific page of the
list results. Must match a previous requests' `nextPageToken`.
in: query
name: pageToken
type: string
produces:
- application/json
responses:
Expand Down
136 changes: 136 additions & 0 deletions pkg/server/pagination/pagination.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package pagination

import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"reflect"
"strconv"

"github.com/gin-gonic/gin"
"github.com/goccy/go-json"
"github.com/sirupsen/logrus"
"github.com/tbd54566975/ssi-service/pkg/server/framework"
"github.com/tbd54566975/ssi-service/pkg/service/common"
)

type PageToken struct {
EncodedQuery string
NextPageToken string
}

const (
PageSizeParam = "pageSize"
PageTokenParam = "pageToken"
)

// ParsePaginationParams reads the PageSizeParam and PageTokenParam from the URL parameters and populates the passed in
// pageRequest. The value encoded in PageTokenParam is assumed to be the base64url encoding of a PageToken. It is an
// error for the query params to be different from the query params encoded in the PageToken. Any error during the
// execution is responded to using the passed in gin.Context. The return value corresponds to whether there was an
// error within the function.
func ParsePaginationParams(c *gin.Context, pageRequest *PageRequest) bool {
pageSizeStr := framework.GetParam(c, PageSizeParam)

if pageSizeStr != nil {
pageSize, err := strconv.Atoi(*pageSizeStr)
if err != nil {
errMsg := fmt.Sprintf("list DIDs by method request encountered a problem with the %q query param", PageSizeParam)
framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return true
}
if pageSize <= 0 {
errMsg := fmt.Sprintf("'%s' must be greater than 0", PageSizeParam)
framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return true
}
pageRequest.PageSize = &pageSize
}

queryPageToken := framework.GetParam(c, PageTokenParam)
if queryPageToken != nil {
errMsg := "token value cannot be decoded"
tokenData, err := base64.RawURLEncoding.DecodeString(*queryPageToken)
if err != nil {
framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return true
}
var pageToken PageToken
if err := json.Unmarshal(tokenData, &pageToken); err != nil {
framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return true
}
pageTokenValues, err := url.ParseQuery(pageToken.EncodedQuery)
if err != nil {
framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return true
}

query := pageTokenQuery(c)
if !reflect.DeepEqual(pageTokenValues, query) {
logrus.Warnf("expected query from token to be equal to query from request. token: %v\nrequest%v", pageTokenValues, query)
framework.LoggingRespondErrMsg(c, "page token must be for the same query", http.StatusBadRequest)
return true
}
pageRequest.PageToken = &pageToken.NextPageToken
}
return false
}

func pageTokenQuery(c *gin.Context) url.Values {
query := c.Request.URL.Query()
delete(query, PageTokenParam)
delete(query, PageSizeParam)
return query
}

// MaybeSetNextPageToken encodes the serviceNextPageToken and the URL query params into a base64url string. The encoded
// string is assigned to what respNextPageToken is pointing to. respNextPageToken cannot be nil. Any error during the
// execution is responded to using the passed in gin.Context. The return value corresponds to whether there was an error
// within the function.
func MaybeSetNextPageToken(c *gin.Context, serviceNextPageToken string, respNextPageToken *string) bool {
if serviceNextPageToken != "" {
tokenQuery := pageTokenQuery(c)
pageToken := PageToken{
EncodedQuery: tokenQuery.Encode(),
NextPageToken: serviceNextPageToken,
}
nextPageTokenData, err := json.Marshal(pageToken)
if err != nil {
framework.LoggingRespondErrWithMsg(c, err, "marshalling page token", http.StatusInternalServerError)
return true
}
encodedToken := base64.RawURLEncoding.EncodeToString(nextPageTokenData)
*respNextPageToken = encodedToken
}
return false
}

// PageRequest contains the parameters sent in the request.
type PageRequest struct {
// PageSize is the value associated with PageSizeParam. A nil value means it was not present in the query. When the parameter
// is absent, all items in the collection are included in the response.
PageSize *int `json:"pageSize,omitempty"`

// PageToken is the value associated with PageTokenParam. A nil value means it was not present in the query.
PageToken *string `json:"pageToken,omitempty"`
}

func (r *PageRequest) ToServicePage() *common.Page {
const allPages = -1
page := common.Page{
Size: allPages,
}
if r == nil {
return &page
}

if r.PageSize != nil {
page.Size = *r.PageSize
}
if r.PageToken != nil {
page.Token = *r.PageToken
}
return &page
}
85 changes: 13 additions & 72 deletions pkg/server/router/did.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
package router

import (
"encoding/base64"
"fmt"
"net/http"
"net/url"
"reflect"
"strconv"

"github.com/TBD54566975/ssi-sdk/crypto"
Expand All @@ -14,7 +11,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/goccy/go-json"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/tbd54566975/ssi-service/pkg/server/pagination"

"github.com/tbd54566975/ssi-service/internal/util"
"github.com/tbd54566975/ssi-service/pkg/server/framework"
Expand All @@ -23,11 +20,9 @@ import (
)

const (
MethodParam = "method"
IDParam = "id"
DeletedParam = "deleted"
PageSizeParam = "pageSize"
PageTokenParam = "pageToken"
MethodParam = "method"
IDParam = "id"
DeletedParam = "deleted"
)

// DIDRouter represents the dependencies required to instantiate a DID-HTTP service
Expand Down Expand Up @@ -240,11 +235,6 @@ type GetDIDsRequest struct {
Filter string `json:"filter,omitempty"`
}

type PageToken struct {
EncodedQuery string
NextPageToken string
}

// ListDIDsByMethod godoc
//
// @Summary List DIDs
Expand Down Expand Up @@ -281,47 +271,15 @@ func (dr DIDRouter) ListDIDsByMethod(c *gin.Context) {
}
// TODO(gabe) check if the method is supported, to tell whether this is a bad req or internal error
// TODO(gabe) differentiate between internal errors and not found DIDs
getDIDsRequest := did.ListDIDsRequest{Method: didsdk.Method(*method), Deleted: getIsDeleted}

pageSizeStr := framework.GetParam(c, PageSizeParam)

if pageSizeStr != nil {
pageSize, err := strconv.Atoi(*pageSizeStr)
if err != nil {
errMsg := fmt.Sprintf("list DIDs by method request encountered a problem with the %q query param", PageSizeParam)
framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return
}
getDIDsRequest.PageSize = &pageSize
getDIDsRequest := did.ListDIDsRequest{
Method: didsdk.Method(*method),
Deleted: getIsDeleted,
}

queryPageToken := framework.GetParam(c, PageTokenParam)
if queryPageToken != nil {
errMsg := "token value cannot be decoded"
tokenData, err := base64.RawURLEncoding.DecodeString(*queryPageToken)
if err != nil {
framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return
}
var pageToken PageToken
if err := json.Unmarshal(tokenData, &pageToken); err != nil {
framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return
}
pageTokenValues, err := url.ParseQuery(pageToken.EncodedQuery)
if err != nil {
framework.LoggingRespondErrMsg(c, errMsg, http.StatusBadRequest)
return
}

query := pageTokenQuery(c)
if !reflect.DeepEqual(pageTokenValues, query) {
logrus.Warnf("expected query from token to be equal to query from request. token: %v\nrequest%v", pageTokenValues, query)
framework.LoggingRespondErrMsg(c, "page token must be for the same query", http.StatusBadRequest)
return
}
getDIDsRequest.PageToken = &pageToken.NextPageToken
var pageRequest pagination.PageRequest
if pagination.ParsePaginationParams(c, &pageRequest) {
return
}
getDIDsRequest.PageRequest = pageRequest.ToServicePage()

listResp, err := dr.service.ListDIDsByMethod(c, getDIDsRequest)
if err != nil {
Expand All @@ -333,29 +291,12 @@ func (dr DIDRouter) ListDIDsByMethod(c *gin.Context) {
resp := ListDIDsByMethodResponse{
DIDs: listResp.DIDs,
}
if listResp.NextPageToken != "" {
tokenQuery := pageTokenQuery(c)
pageToken := PageToken{
EncodedQuery: tokenQuery.Encode(),
NextPageToken: listResp.NextPageToken,
}
nextPageTokenData, err := json.Marshal(pageToken)
if err != nil {
framework.LoggingRespondErrWithMsg(c, err, "marshalling page token", http.StatusInternalServerError)
return
}
resp.NextPageToken = base64.RawURLEncoding.EncodeToString(nextPageTokenData)
if pagination.MaybeSetNextPageToken(c, listResp.NextPageToken, &resp.NextPageToken) {
return
}
framework.Respond(c, resp, http.StatusOK)
}

func pageTokenQuery(c *gin.Context) url.Values {
query := c.Request.URL.Query()
delete(query, PageTokenParam)
delete(query, PageSizeParam)
return query
}

type ResolveDIDResponse struct {
ResolutionMetadata *resolution.Metadata `json:"didResolutionMetadata,omitempty"`
DIDDocument *didsdk.Document `json:"didDocument"`
Expand Down
16 changes: 10 additions & 6 deletions pkg/server/router/did_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/TBD54566975/ssi-sdk/crypto"
didsdk "github.com/TBD54566975/ssi-sdk/did"
"github.com/stretchr/testify/assert"
"github.com/tbd54566975/ssi-service/pkg/service/common"
"github.com/tbd54566975/ssi-service/pkg/testutil"

"github.com/tbd54566975/ssi-service/config"
Expand All @@ -32,7 +33,6 @@ func TestDIDRouter(t *testing.T) {

for _, test := range testutil.TestDatabases {
t.Run(test.Name, func(t *testing.T) {

// TODO: Fix pagesize issue on redis - https://github.com/TBD54566975/ssi-service/issues/538
if !strings.Contains(test.Name, "Redis") {
t.Run("List DIDs supports paging", func(tt *testing.T) {
Expand All @@ -50,8 +50,10 @@ func TestDIDRouter(t *testing.T) {
one := 1
listDIDsResponse1, err := didService.ListDIDsByMethod(context.Background(),
did.ListDIDsRequest{
Method: didsdk.KeyMethod,
PageSize: &one,
Method: didsdk.KeyMethod,
PageRequest: &common.Page{
Size: one,
},
})

assert.NoError(tt, err)
Expand All @@ -60,9 +62,11 @@ func TestDIDRouter(t *testing.T) {

listDIDsResponse2, err := didService.ListDIDsByMethod(context.Background(),
did.ListDIDsRequest{
Method: didsdk.KeyMethod,
PageSize: &one,
PageToken: &listDIDsResponse1.NextPageToken,
Method: didsdk.KeyMethod,
PageRequest: &common.Page{
Size: one,
Token: listDIDsResponse1.NextPageToken,
},
})

assert.NoError(tt, err)
Expand Down
Loading

0 comments on commit e3550d6

Please sign in to comment.