Repository: TBD54566975/ssi-service
Pull request #638 (2023-08-04 03:42:54)

Add pagination to schemas
# Overview
This is part of #349 

# Description
Simple wiring and reusing all the existing components.

# How Has This Been Tested?
Added an integration test.


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/638?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) Report
> Merging [#638](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/638?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (0e9ec7a) into [main](https://app.codecov.io/gh/TBD54566975/ssi-service/commit/028dbf93be843864a9db56e4375d85bff148ca87?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) (028dbf9) will **decrease** coverage by `0.05%`.
> The diff coverage is `0.00%`.

> :exclamation: Current head 0e9ec7a differs from pull request most recent head 9ec8ac5. Consider uploading reports for the commit 9ec8ac5 to get more accurate results

```diff
@@            Coverage Diff             @@
##             main     #638      +/-   ##
==========================================
- Coverage   25.35%   25.31%   -0.05%     
==========================================
  Files          56       56              
  Lines        6219     6229      +10     
==========================================
  Hits         1577     1577              
- Misses       4367     4377      +10     
  Partials      275      275              
```


| [Files Changed](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/638?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975) | Coverage Δ | |
|---|---|---|
| [pkg/server/router/schema.go](https://app.codecov.io/gh/TBD54566975/ssi-service/pull/638?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=TBD54566975#diff-cGtnL3NlcnZlci9yb3V0ZXIvc2NoZW1hLmdv) | `6.72% <0.00%> (-0.62%)` | :arrow_down: |



Patch:
diff --git a/doc/swagger.yaml b/doc/swagger.yaml
index 93b92bb..5bda830 100644
--- a/doc/swagger.yaml
+++ b/doc/swagger.yaml
@@ -1824,6 +1824,10 @@ definitions:
     type: object
   pkg_server_router.ListSchemasResponse:
     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
       schemas:
         description: Schemas is the list of all schemas the service holds
         items:
@@ -4049,6 +4053,17 @@ paths:
       consumes:
       - application/json
       description: List Credential Schemas stored by the service
+      parameters:
+      - 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:
diff --git a/pkg/server/router/schema.go b/pkg/server/router/schema.go
index 6fafac2..13a91ec 100644
--- a/pkg/server/router/schema.go
+++ b/pkg/server/router/schema.go
@@ -8,9 +8,9 @@ import (
 	"github.com/TBD54566975/ssi-sdk/did"
 	"github.com/gin-gonic/gin"
 	"github.com/pkg/errors"
-
 	"github.com/tbd54566975/ssi-service/internal/keyaccess"
 	"github.com/tbd54566975/ssi-service/pkg/server/framework"
+	"github.com/tbd54566975/ssi-service/pkg/server/pagination"
 	svcframework "github.com/tbd54566975/ssi-service/pkg/service/framework"
 	"github.com/tbd54566975/ssi-service/pkg/service/schema"
 )
@@ -185,6 +185,9 @@ func (sr SchemaRouter) GetSchema(c *gin.Context) {
 type ListSchemasResponse struct {
 	// Schemas is the list of all schemas the service holds
 	Schemas []GetSchemaResponse `json:"schemas,omitempty"`
+
+	// Pagination token to retrieve the next page of results. If the value is "", it means no further results for the request.
+	NextPageToken string `json:"nextPageToken"`
 }
 
 // ListSchemas godoc
@@ -194,11 +197,20 @@ type ListSchemasResponse struct {
 //	@Tags			Schemas
 //	@Accept			json
 //	@Produce		json
-//	@Success		200	{object}	ListSchemasResponse
-//	@Failure		500	{string}	string	"Internal server error"
+//	@Param			pageSize	query		number	false	"Hint to the server of the maximum elements to return. More may be returned. When not set, the server will return all elements."
+//	@Param			pageToken	query		string	false	"Used to indicate to the server to return a specific page of the list results. Must match a previous requests' `nextPageToken`."
+//	@Success		200			{object}	ListSchemasResponse
+//	@Failure		500			{string}	string	"Internal server error"
 //	@Router			/v1/schemas [get]
 func (sr SchemaRouter) ListSchemas(c *gin.Context) {
-	gotSchemas, err := sr.service.ListSchemas(c)
+	var pageRequest pagination.PageRequest
+	if pagination.ParsePaginationQueryValues(c, &pageRequest) {
+		return
+	}
+
+	gotSchemas, err := sr.service.ListSchemas(c, schema.ListSchemasRequest{
+		PageRequest: &pageRequest,
+	})
 	if err != nil {
 		errMsg := "could not list schemas"
 		framework.LoggingRespondErrWithMsg(c, err, errMsg, http.StatusInternalServerError)
@@ -218,6 +230,10 @@ func (sr SchemaRouter) ListSchemas(c *gin.Context) {
 	}
 
 	resp := ListSchemasResponse{Schemas: schemas}
+
+	if pagination.MaybeSetNextPageToken(c, gotSchemas.NextPageToken, &resp.NextPageToken) {
+		return
+	}
 	framework.Respond(c, resp, http.StatusOK)
 }
 
diff --git a/pkg/service/schema/model.go b/pkg/service/schema/model.go
index 0a18eac..ab78fd5 100644
--- a/pkg/service/schema/model.go
+++ b/pkg/service/schema/model.go
@@ -3,6 +3,7 @@ package schema
 import (
 	"github.com/TBD54566975/ssi-sdk/credential/schema"
 	"github.com/TBD54566975/ssi-sdk/util"
+	"github.com/tbd54566975/ssi-service/pkg/server/pagination"
 	"github.com/tbd54566975/ssi-service/pkg/service/common"
 
 	"github.com/tbd54566975/ssi-service/internal/keyaccess"
@@ -40,8 +41,13 @@ type CreateSchemaResponse struct {
 	CredentialSchema *keyaccess.JWT          `json:"credentialSchema,omitempty"`
 }
 
+type ListSchemasRequest struct {
+	PageRequest *pagination.PageRequest
+}
+
 type ListSchemasResponse struct {
-	Schemas []GetSchemaResponse `json:"schemas,omitempty"`
+	Schemas       []GetSchemaResponse `json:"schemas,omitempty"`
+	NextPageToken string              `json:"nextPageToken,omitempty"`
 }
 
 type GetSchemaRequest struct {
diff --git a/pkg/service/schema/service.go b/pkg/service/schema/service.go
index 77c1a8c..7927d90 100644
--- a/pkg/service/schema/service.go
+++ b/pkg/service/schema/service.go
@@ -195,15 +195,15 @@ func (s Service) signCredentialSchema(ctx context.Context, cred credential.Verif
 	return credToken, nil
 }
 
-func (s Service) ListSchemas(ctx context.Context) (*ListSchemasResponse, error) {
+func (s Service) ListSchemas(ctx context.Context, request ListSchemasRequest) (*ListSchemasResponse, error) {
 	logrus.Debug("listing all schemas")
 
-	storedSchemas, err := s.storage.ListSchemas(ctx)
+	storedSchemas, err := s.storage.ListSchemas(ctx, *request.PageRequest.ToServicePage())
 	if err != nil {
 		return nil, sdkutil.LoggingErrorMsg(err, "error getting schemas")
 	}
-	schemas := make([]GetSchemaResponse, 0, len(storedSchemas))
-	for _, stored := range storedSchemas {
+	schemas := make([]GetSchemaResponse, 0, len(storedSchemas.Schemas))
+	for _, stored := range storedSchemas.Schemas {
 		schemas = append(schemas, GetSchemaResponse{
 			ID:               stored.ID,
 			Type:             stored.Type,
@@ -212,7 +212,7 @@ func (s Service) ListSchemas(ctx context.Context) (*ListSchemasResponse, error)
 		})
 	}
 
-	return &ListSchemasResponse{Schemas: schemas}, nil
+	return &ListSchemasResponse{Schemas: schemas, NextPageToken: storedSchemas.NextPageToken}, nil
 }
 
 func (s Service) GetSchema(ctx context.Context, request GetSchemaRequest) (*GetSchemaResponse, error) {
diff --git a/pkg/service/schema/storage.go b/pkg/service/schema/storage.go
index 5c62157..07a86f7 100644
--- a/pkg/service/schema/storage.go
+++ b/pkg/service/schema/storage.go
@@ -3,12 +3,12 @@ package schema
 import (
 	"context"
 
+	"github.com/TBD54566975/ssi-sdk/credential/schema"
 	"github.com/TBD54566975/ssi-sdk/util"
 	"github.com/goccy/go-json"
 	"github.com/pkg/errors"
 	"github.com/sirupsen/logrus"
-
-	"github.com/TBD54566975/ssi-sdk/credential/schema"
+	"github.com/tbd54566975/ssi-service/pkg/service/common"
 
 	"github.com/tbd54566975/ssi-service/internal/keyaccess"
 	"github.com/tbd54566975/ssi-service/pkg/storage"
@@ -18,6 +18,11 @@ const (
 	namespace = "schema"
 )
 
+type StoredSchemas struct {
+	Schemas       []StoredSchema
+	NextPageToken string
+}
+
 type StoredSchema struct {
 	ID               string                  `json:"id"`
 	Type             schema.VCJSONSchemaType `json:"type"`
@@ -64,15 +69,13 @@ func (s *Storage) GetSchema(ctx context.Context, id string) (*StoredSchema, erro
 }
 
 // ListSchemas attempts to get all stored schemas. It will return those it can even if it has trouble with some.
-func (s *Storage) ListSchemas(ctx context.Context) ([]StoredSchema, error) {
-	gotSchemas, err := s.db.ReadAll(ctx, namespace)
+func (s *Storage) ListSchemas(ctx context.Context, page common.Page) (*StoredSchemas, error) {
+	token, size := page.ToStorageArgs()
+	gotSchemas, nextPageToken, err := s.db.ReadPage(ctx, namespace, token, size)
 	if err != nil {
-		return nil, util.LoggingErrorMsg(err, "could not list schemas")
-	}
-	if len(gotSchemas) == 0 {
-		logrus.Info("no schemas to list")
-		return nil, nil
+		return nil, errors.Wrap(err, "reading page of schemas")
 	}
+
 	stored := make([]StoredSchema, 0, len(gotSchemas))
 	for _, schemaBytes := range gotSchemas {
 		var nextSchema StoredSchema
@@ -82,7 +85,10 @@ func (s *Storage) ListSchemas(ctx context.Context) ([]StoredSchema, error) {
 		}
 		stored = append(stored, nextSchema)
 	}
-	return stored, nil
+	return &StoredSchemas{
+		Schemas:       stored,
+		NextPageToken: nextPageToken,
+	}, nil
 }
 
 func (s *Storage) DeleteSchema(ctx context.Context, id string) error {


Test patch:
diff --git a/integration/pagination_integration_test.go b/integration/pagination_integration_test.go
new file mode 100644
index 0000000..91f08a6
--- /dev/null
+++ b/integration/pagination_integration_test.go
@@ -0,0 +1,49 @@
+package integration
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestListSchemaIntegration(t *testing.T) {
+	if testing.Short() {
+		t.Skip("skipping integration test")
+	}
+
+	// In this test we create a couple of schemas, fetch them all using pagination, and validate that the created ones
+	// were returned in any of the results.
+	schema1, err := CreateKYCSchema()
+	assert.NoError(t, err)
+
+	schema1ID, err := getJSONElement(schema1, "$.id")
+	assert.NoError(t, err)
+
+	schema2, err := CreateKYCSchema()
+	assert.NoError(t, err)
+
+	schema2ID, err := getJSONElement(schema2, "$.id")
+	assert.NoError(t, err)
+
+	schemasPage, err := get(endpoint + version + "schemas?pageSize=1")
+	assert.NoError(t, err)
+
+	var allSchemaIDs string
+	allSchemaIDs = schemasPage
+
+	nextPageToken, err := getJSONElement(schemasPage, "$.nextPageToken")
+	assert.NoError(t, err)
+
+	for nextPageToken != "" {
+		schemasPage, err := get(endpoint + version + "schemas?pageSize=1&pageToken=" + nextPageToken)
+		assert.NoError(t, err)
+
+		allSchemaIDs += schemasPage
+
+		nextPageToken, err = getJSONElement(schemasPage, "$.nextPageToken")
+		assert.NoError(t, err)
+	}
+
+	assert.Contains(t, allSchemaIDs, schema1ID)
+	assert.Contains(t, allSchemaIDs, schema2ID)
+}
diff --git a/pkg/server/router/schema_test.go b/pkg/server/router/schema_test.go
index 9ec3384..cc3cc8c 100644
--- a/pkg/server/router/schema_test.go
+++ b/pkg/server/router/schema_test.go
@@ -50,7 +50,7 @@ func TestSchemaRouter(t *testing.T) {
 				assert.Equal(tt, framework.StatusReady, schemaService.Status().Status)
 
 				// get all schemas (none)
-				gotSchemas, err := schemaService.ListSchemas(context.Background())
+				gotSchemas, err := schemaService.ListSchemas(context.Background(), schema.ListSchemasRequest{})
 				assert.NoError(tt, err)
 				assert.Empty(tt, gotSchemas.Schemas)
 
@@ -74,7 +74,7 @@ func TestSchemaRouter(t *testing.T) {
 				assert.EqualValues(tt, createdSchema.Schema, gotSchema.Schema)
 
 				// get all schemas, expect one
-				gotSchemas, err = schemaService.ListSchemas(context.Background())
+				gotSchemas, err = schemaService.ListSchemas(context.Background(), schema.ListSchemasRequest{})
 				assert.NoError(tt, err)
 				assert.NotEmpty(tt, gotSchemas.Schemas)
 				assert.Len(tt, gotSchemas.Schemas, 1)
@@ -88,7 +88,7 @@ func TestSchemaRouter(t *testing.T) {
 				assert.Equal(tt, credschema.JSONSchema2023Type, createdSchema.Type)
 
 				// get all schemas, expect two
-				gotSchemas, err = schemaService.ListSchemas(context.Background())
+				gotSchemas, err = schemaService.ListSchemas(context.Background(), schema.ListSchemasRequest{})
 				assert.NoError(tt, err)
 				assert.NotEmpty(tt, gotSchemas.Schemas)
 				assert.Len(tt, gotSchemas.Schemas, 2)
@@ -101,7 +101,7 @@ func TestSchemaRouter(t *testing.T) {
 				assert.NoError(tt, err)
 
 				// get all schemas, expect one
-				gotSchemas, err = schemaService.ListSchemas(context.Background())
+				gotSchemas, err = schemaService.ListSchemas(context.Background(), schema.ListSchemasRequest{})
 				assert.NoError(tt, err)
 				assert.NotEmpty(tt, gotSchemas.Schemas)
 				assert.Len(tt, gotSchemas.Schemas, 1)