From 60f438dfc6b22dd90746296a196e11451176e61e Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Tue, 5 May 2020 13:42:17 -0400 Subject: [PATCH] API: initial groundwork for cursors --- api/database/ql.go | 43 ++++--- api/go.sum | 1 + api/graph/generated/generated.go | 208 +++++++++++++++++++++---------- api/graph/model/cursor.go | 2 + api/graph/model/models_gen.go | 5 + api/graph/model/repository.go | 2 +- api/graph/model/util.go | 71 ----------- api/graph/schema.graphqls | 19 ++- api/graph/schema.resolvers.go | 21 +++- 9 files changed, 208 insertions(+), 164 deletions(-) delete mode 100644 api/graph/model/util.go diff --git a/api/database/ql.go b/api/database/ql.go index 6498652..d026449 100644 --- a/api/database/ql.go +++ b/api/database/ql.go @@ -10,13 +10,34 @@ import ( "git.sr.ht/~sircmpwn/gqlgen/graphql" ) -func ColumnsFor(ctx context.Context, alias string, - colMap map[string]string) []string { - +func collectFields(ctx context.Context) []graphql.CollectedField { var fields []graphql.CollectedField if graphql.GetFieldContext(ctx) != nil { fields = graphql.CollectFieldsCtx(ctx, nil) - } else { + + octx := graphql.GetOperationContext(ctx) + for _, col := range fields { + if col.Name == "results" { + // This endpoint is using the cursor pattern; the columns we + // actually need to filter with are nested into the results + // field. + fields = graphql.CollectFields(octx, col.SelectionSet, nil) + break + } + } + + sort.Slice(fields, func(a, b int) bool { + return fields[a].Name < fields[b].Name + }) + } + return fields +} + +func ColumnsFor(ctx context.Context, alias string, + colMap map[string]string) []string { + + fields := collectFields(ctx) + if len(fields) == 0 { // Collect all fields if we are not in an active graphql context for qlCol, _ := range colMap { fields = append(fields, graphql.CollectedField{ @@ -25,10 +46,6 @@ func ColumnsFor(ctx context.Context, alias string, } } - sort.Slice(fields, func(a, b int) bool { - return fields[a].Name < fields[b].Name - }) - var columns []string for _, qlCol := range fields { if sqlCol, ok := colMap[qlCol.Name]; ok { @@ -47,10 +64,8 @@ func ColumnsFor(ctx context.Context, alias string, func FieldsFor(ctx context.Context, colMap map[string]interface{}) []interface{} { - var qlFields []graphql.CollectedField - if graphql.GetFieldContext(ctx) != nil { - qlFields = graphql.CollectFieldsCtx(ctx, nil) - } else { + qlFields := collectFields(ctx) + if len(qlFields) == 0 { // Collect all fields if we are not in an active graphql context for qlCol, _ := range colMap { qlFields = append(qlFields, graphql.CollectedField{ @@ -59,10 +74,6 @@ func FieldsFor(ctx context.Context, } } - sort.Slice(qlFields, func(a, b int) bool { - return qlFields[a].Name < qlFields[b].Name - }) - var fields []interface{} for _, qlField := range qlFields { if field, ok := colMap[qlField.Name]; ok { diff --git a/api/go.sum b/api/go.sum index d770192..f334f00 100644 --- a/api/go.sum +++ b/api/go.sum @@ -2,6 +2,7 @@ git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3 h1:4wDp4BKF7NQqoh7 git.sr.ht/~sircmpwn/getopt v0.0.0-20191230200459-23622cc906b3/go.mod h1:wMEGFFFNuPos7vHmWXfszqImLppbc0wEhh6JBfJIUgw= git.sr.ht/~sircmpwn/git.sr.ht v0.0.0-20200405134845-b8fbf5bf484f h1:SW8+xV65kcga0rHmQbnZkLG36yp286BcbVOdQTVo1m8= git.sr.ht/~sircmpwn/git.sr.ht v0.0.0-20200413150414-046cd382d7b7 h1:PYRTIcsHR5W+aPn98OCC73ly528uw5o/4Z3b5Rvc7vA= +git.sr.ht/~sircmpwn/git.sr.ht v0.0.0-20200430231646-4014870a700b h1:xHyh0xixoMog7F1kqOG55daFfK0uso98i0lQ/D4dSM8= git.sr.ht/~sircmpwn/gqlgen v0.0.0-20200412134447-57d7234737d4 h1:J/Sb88htNHzZaN6ZEF8BnRWj3LzYoTrOL4WRhZEEiQE= git.sr.ht/~sircmpwn/gqlgen v0.0.0-20200412134447-57d7234737d4/go.mod h1:W1cijL2EqAyL1eo1WAJ3ijNVkZM2okpYyCF5TRu1VfI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= diff --git a/api/graph/generated/generated.go b/api/graph/generated/generated.go index a610385..9fe062e 100644 --- a/api/graph/generated/generated.go +++ b/api/graph/generated/generated.go @@ -121,7 +121,6 @@ type ComplexityRoot struct { Repository struct { AccessControlList func(childComplexity int, cursor *model.Cursor) int Created func(childComplexity int) int - Cursor func(childComplexity int) int Description func(childComplexity int) int File func(childComplexity int, revspec *string, path string) int Head func(childComplexity int) int @@ -138,6 +137,11 @@ type ComplexityRoot struct { Visibility func(childComplexity int) int } + RepositoryCursor struct { + Cursor func(childComplexity int) int + Results func(childComplexity int) int + } + Signature struct { Email func(childComplexity int) int Name func(childComplexity int) int @@ -210,7 +214,7 @@ type QueryResolver interface { Me(ctx context.Context) (*model.User, error) Cursor(ctx context.Context, filter model.Filter) (*model.Cursor, error) User(ctx context.Context, username string) (*model.User, error) - Repositories(ctx context.Context, cursor *model.Cursor) ([]*model.Repository, error) + Repositories(ctx context.Context, cursor *model.Cursor) (*model.RepositoryCursor, error) Repository(ctx context.Context, id int) (*model.Repository, error) RepositoryByName(ctx context.Context, name string) (*model.Repository, error) RepositoryByOwner(ctx context.Context, owner string, repo string) (*model.Repository, error) @@ -225,7 +229,7 @@ type TreeResolver interface { Entries(ctx context.Context, obj *model.Tree, cursor *model.Cursor) ([]*model.TreeEntry, error) } type UserResolver interface { - Repositories(ctx context.Context, obj *model.User, cursor *model.Cursor) ([]*model.Repository, error) + Repositories(ctx context.Context, obj *model.User, cursor *model.Cursor) (*model.RepositoryCursor, error) } type executableSchema struct { @@ -649,13 +653,6 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Repository.Created(childComplexity), true - case "Repository.cursor": - if e.complexity.Repository.Cursor == nil { - break - } - - return e.complexity.Repository.Cursor(childComplexity), true - case "Repository.description": if e.complexity.Repository.Description == nil { break @@ -784,6 +781,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Repository.Visibility(childComplexity), true + case "RepositoryCursor.cursor": + if e.complexity.RepositoryCursor.Cursor == nil { + break + } + + return e.complexity.RepositoryCursor.Cursor(childComplexity), true + + case "RepositoryCursor.results": + if e.complexity.RepositoryCursor.Results == nil { + break + } + + return e.complexity.RepositoryCursor.Results(childComplexity), true + case "Signature.email": if e.complexity.Signature.Email == nil { break @@ -1158,7 +1169,7 @@ interface Entity { canonicalName: String! # A list of repositories owned by this entity - repositories(cursor: Cursor): [Repository]! + repositories(cursor: Cursor): RepositoryCursor } # A registered user @@ -1174,7 +1185,7 @@ type User implements Entity { bio: String # A list of repositories owned by this user - repositories(cursor: Cursor): [Repository]! + repositories(cursor: Cursor): RepositoryCursor } # A git repository @@ -1187,9 +1198,6 @@ type Repository { description: String visibility: Visibility! - # Pass this into the same endpoint to receive a new page of results - cursor: Cursor - # If this repository was cloned from another, this will be set to the # original clone URL upstreamUrl: String @@ -1235,6 +1243,16 @@ type Repository { revparse_single(revspec: String!): Object } +# A cursor for enumerating a list of repositories +# +# If there are additional results available, the cursor object may be passed +# back into the same endpoint to retrieve another page. If the cursor is null, +# there are no remaining results to return. +type RepositoryCursor { + results: [Repository]! + cursor: Cursor +} + # Access Control List entry type ACL { id: Int! @@ -1382,7 +1400,7 @@ type Query { # will be to return all repositories that the user either (1) has been given # explicit access to via ACLs or (2) has implicit access to either by # ownership or group membership. - repositories(cursor: Cursor): [Repository]! + repositories(cursor: Cursor): RepositoryCursor # Returns a specific repository repository(id: Int!): Repository @@ -3284,14 +3302,11 @@ func (ec *executionContext) _Query_repositories(ctx context.Context, field graph return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } - res := resTmp.([]*model.Repository) + res := resTmp.(*model.RepositoryCursor) fc.Result = res - return ec.marshalNRepository2ᚕᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐRepository(ctx, field.Selections, res) + return ec.marshalORepositoryCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐRepositoryCursor(ctx, field.Selections, res) } func (ec *executionContext) _Query_repository(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) { @@ -3811,37 +3826,6 @@ func (ec *executionContext) _Repository_visibility(ctx context.Context, field gr return ec.marshalNVisibility2gitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐVisibility(ctx, field.Selections, res) } -func (ec *executionContext) _Repository_cursor(ctx context.Context, field graphql.CollectedField, obj *model.Repository) (ret graphql.Marshaler) { - defer func() { - if r := recover(); r != nil { - ec.Error(ctx, ec.Recover(ctx, r)) - ret = graphql.Null - } - }() - fc := &graphql.FieldContext{ - Object: "Repository", - Field: field, - Args: nil, - IsMethod: false, - } - - ctx = graphql.WithFieldContext(ctx, fc) - resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { - ctx = rctx // use context from middleware stack in children - return obj.Cursor, nil - }) - if err != nil { - ec.Error(ctx, err) - return graphql.Null - } - if resTmp == nil { - return graphql.Null - } - res := resTmp.(*model.Cursor) - fc.Result = res - return ec.marshalOCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐCursor(ctx, field.Selections, res) -} - func (ec *executionContext) _Repository_upstreamUrl(ctx context.Context, field graphql.CollectedField, obj *model.Repository) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -4182,6 +4166,71 @@ func (ec *executionContext) _Repository_revparse_single(ctx context.Context, fie return ec.marshalOObject2gitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐObject(ctx, field.Selections, res) } +func (ec *executionContext) _RepositoryCursor_results(ctx context.Context, field graphql.CollectedField, obj *model.RepositoryCursor) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "RepositoryCursor", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Results, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + if !graphql.HasFieldError(ctx, fc) { + ec.Errorf(ctx, "must not be null") + } + return graphql.Null + } + res := resTmp.([]*model.Repository) + fc.Result = res + return ec.marshalNRepository2ᚕᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐRepository(ctx, field.Selections, res) +} + +func (ec *executionContext) _RepositoryCursor_cursor(ctx context.Context, field graphql.CollectedField, obj *model.RepositoryCursor) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "RepositoryCursor", + Field: field, + Args: nil, + IsMethod: false, + } + + ctx = graphql.WithFieldContext(ctx, fc) + resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) { + ctx = rctx // use context from middleware stack in children + return obj.Cursor, nil + }) + if err != nil { + ec.Error(ctx, err) + return graphql.Null + } + if resTmp == nil { + return graphql.Null + } + res := resTmp.(*model.Cursor) + fc.Result = res + return ec.marshalOCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐCursor(ctx, field.Selections, res) +} + func (ec *executionContext) _Signature_name(ctx context.Context, field graphql.CollectedField, obj *model.Signature) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -5266,14 +5315,11 @@ func (ec *executionContext) _User_repositories(ctx context.Context, field graphq return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } - res := resTmp.([]*model.Repository) + res := resTmp.(*model.RepositoryCursor) fc.Result = res - return ec.marshalNRepository2ᚕᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐRepository(ctx, field.Selections, res) + return ec.marshalORepositoryCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐRepositoryCursor(ctx, field.Selections, res) } func (ec *executionContext) _Version_major(ctx context.Context, field graphql.CollectedField, obj *model.Version) (ret graphql.Marshaler) { @@ -6991,9 +7037,6 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr } }() res = ec._Query_repositories(ctx, field) - if res == graphql.Null { - atomic.AddUint32(&invalids, 1) - } return res }) case "repository": @@ -7130,8 +7173,6 @@ func (ec *executionContext) _Repository(ctx context.Context, sel ast.SelectionSe if out.Values[i] == graphql.Null { atomic.AddUint32(&invalids, 1) } - case "cursor": - out.Values[i] = ec._Repository_cursor(ctx, field, obj) case "upstreamUrl": out.Values[i] = ec._Repository_upstreamUrl(ctx, field, obj) case "accessControlList": @@ -7191,6 +7232,35 @@ func (ec *executionContext) _Repository(ctx context.Context, sel ast.SelectionSe return out } +var repositoryCursorImplementors = []string{"RepositoryCursor"} + +func (ec *executionContext) _RepositoryCursor(ctx context.Context, sel ast.SelectionSet, obj *model.RepositoryCursor) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, repositoryCursorImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("RepositoryCursor") + case "results": + out.Values[i] = ec._RepositoryCursor_results(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "cursor": + out.Values[i] = ec._RepositoryCursor_cursor(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var signatureImplementors = []string{"Signature"} func (ec *executionContext) _Signature(ctx context.Context, sel ast.SelectionSet, obj *model.Signature) graphql.Marshaler { @@ -7470,9 +7540,6 @@ func (ec *executionContext) _User(ctx context.Context, sel ast.SelectionSet, obj } }() res = ec._User_repositories(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&invalids, 1) - } return res }) default: @@ -8700,6 +8767,17 @@ func (ec *executionContext) marshalORepository2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋg return ec._Repository(ctx, sel, v) } +func (ec *executionContext) marshalORepositoryCursor2gitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐRepositoryCursor(ctx context.Context, sel ast.SelectionSet, v model.RepositoryCursor) graphql.Marshaler { + return ec._RepositoryCursor(ctx, sel, &v) +} + +func (ec *executionContext) marshalORepositoryCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐRepositoryCursor(ctx context.Context, sel ast.SelectionSet, v *model.RepositoryCursor) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._RepositoryCursor(ctx, sel, v) +} + func (ec *executionContext) unmarshalOString2string(ctx context.Context, v interface{}) (string, error) { return graphql.UnmarshalString(v) } diff --git a/api/graph/model/cursor.go b/api/graph/model/cursor.go index cf945fd..4a8955c 100644 --- a/api/graph/model/cursor.go +++ b/api/graph/model/cursor.go @@ -37,5 +37,7 @@ func (cur Cursor) MarshalGQL(w io.Writer) { if err != nil { panic(err) } + w.Write([]byte("\"")) w.Write(crypto.Encrypt(data)) + w.Write([]byte("\"")) } diff --git a/api/graph/model/models_gen.go b/api/graph/model/models_gen.go index d44c166..b3d9e28 100644 --- a/api/graph/model/models_gen.go +++ b/api/graph/model/models_gen.go @@ -43,6 +43,11 @@ type RepoInput struct { Visibility *Visibility `json:"visibility"` } +type RepositoryCursor struct { + Results []*Repository `json:"results"` + Cursor *Cursor `json:"cursor"` +} + type Signature struct { Name string `json:"name"` Email string `json:"email"` diff --git a/api/graph/model/repository.go b/api/graph/model/repository.go index 3dff4b6..501a4fe 100644 --- a/api/graph/model/repository.go +++ b/api/graph/model/repository.go @@ -72,7 +72,7 @@ func (r *Repository) As(alias string) database.Selectable { } func (r *Repository) Fields(ctx context.Context) []interface{} { - fields := FieldsFor(ctx, map[string]interface{}{ + fields := database.FieldsFor(ctx, map[string]interface{}{ "id": &r.ID, "created": &r.Created, "updated": &r.Updated, diff --git a/api/graph/model/util.go b/api/graph/model/util.go deleted file mode 100644 index 771af24..0000000 --- a/api/graph/model/util.go +++ /dev/null @@ -1,71 +0,0 @@ -package model - -import ( - "context" - "sort" - - "git.sr.ht/~sircmpwn/gqlgen/graphql" - "github.com/vektah/gqlparser/v2/ast" -) - -func ColumnsFor(ctx context.Context, - colMap map[string]string, tbl string) []string { - - var fields []graphql.CollectedField - if graphql.GetFieldContext(ctx) != nil { - fields = graphql.CollectFieldsCtx(ctx, nil) - } else { - // Collect all fields if we are not in an active graphql context - for qlCol, _ := range colMap { - fields = append(fields, graphql.CollectedField{ - &ast.Field{Name: qlCol}, nil, - }) - } - } - - sort.Slice(fields, func(a, b int) bool { - return fields[a].Name < fields[b].Name - }) - - var columns []string - for _, qlCol := range fields { - if sqlCol, ok := colMap[qlCol.Name]; ok { - if tbl != "" { - columns = append(columns, tbl+"."+sqlCol) - } else { - columns = append(columns, sqlCol) - } - } - } - - return columns -} - -func FieldsFor(ctx context.Context, - colMap map[string]interface{}) []interface{} { - - var qlFields []graphql.CollectedField - if graphql.GetFieldContext(ctx) != nil { - qlFields = graphql.CollectFieldsCtx(ctx, nil) - } else { - // Collect all fields if we are not in an active graphql context - for qlCol, _ := range colMap { - qlFields = append(qlFields, graphql.CollectedField{ - &ast.Field{Name: qlCol}, nil, - }) - } - } - - sort.Slice(qlFields, func(a, b int) bool { - return qlFields[a].Name < qlFields[b].Name - }) - - var fields []interface{} - for _, qlField := range qlFields { - if field, ok := colMap[qlField.Name]; ok { - fields = append(fields, field) - } - } - - return fields -} diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index fa5d7b4..8a34953 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -41,7 +41,7 @@ interface Entity { canonicalName: String! # A list of repositories owned by this entity - repositories(cursor: Cursor): [Repository]! + repositories(cursor: Cursor): RepositoryCursor } # A registered user @@ -57,7 +57,7 @@ type User implements Entity { bio: String # A list of repositories owned by this user - repositories(cursor: Cursor): [Repository]! + repositories(cursor: Cursor): RepositoryCursor } # A git repository @@ -70,9 +70,6 @@ type Repository { description: String visibility: Visibility! - # Pass this into the same endpoint to receive a new page of results - cursor: Cursor - # If this repository was cloned from another, this will be set to the # original clone URL upstreamUrl: String @@ -118,6 +115,16 @@ type Repository { revparse_single(revspec: String!): Object } +# A cursor for enumerating a list of repositories +# +# If there are additional results available, the cursor object may be passed +# back into the same endpoint to retrieve another page. If the cursor is null, +# there are no remaining results to return. +type RepositoryCursor { + results: [Repository]! + cursor: Cursor +} + # Access Control List entry type ACL { id: Int! @@ -265,7 +272,7 @@ type Query { # will be to return all repositories that the user either (1) has been given # explicit access to via ACLs or (2) has implicit access to either by # ownership or group membership. - repositories(cursor: Cursor): [Repository]! + repositories(cursor: Cursor): RepositoryCursor # Returns a specific repository repository(id: Int!): Repository diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 0d045d6..bcc0c3f 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -8,6 +8,7 @@ import ( "database/sql" "fmt" "sort" + "strconv" "strings" "git.sr.ht/~sircmpwn/git.sr.ht/api/auth" @@ -78,7 +79,7 @@ func (r *queryResolver) User(ctx context.Context, username string) (*model.User, return loaders.ForContext(ctx).UsersByName.Load(username) } -func (r *queryResolver) Repositories(ctx context.Context, cursor *model.Cursor) ([]*model.Repository, error) { +func (r *queryResolver) Repositories(ctx context.Context, cursor *model.Cursor) (*model.RepositoryCursor, error) { var ( err error rows *sql.Rows @@ -103,7 +104,8 @@ func (r *queryResolver) Repositories(ctx context.Context, cursor *model.Cursor) } repos = append(repos, &repo) } - return repos, nil + // TODO: Cursor + return &model.RepositoryCursor{repos, nil}, nil } func (r *queryResolver) Repository(ctx context.Context, id int) (*model.Repository, error) { @@ -157,7 +159,7 @@ func (r *treeResolver) Entries(ctx context.Context, obj *model.Tree, cursor *mod panic(fmt.Errorf("not implemented")) } -func (r *userResolver) Repositories(ctx context.Context, obj *model.User, cursor *model.Cursor) ([]*model.Repository, error) { +func (r *userResolver) Repositories(ctx context.Context, obj *model.User, cursor *model.Cursor) (*model.RepositoryCursor, error) { var ( err error rows *sql.Rows @@ -168,7 +170,7 @@ func (r *userResolver) Repositories(ctx context.Context, obj *model.User, cursor From(`repository repo`). Where(`repo.owner_id = ?`, obj.ID). OrderBy(`id DESC`). - Limit(25) + Limit(26) if rows, err = query.RunWith(r.DB).QueryContext(ctx); err != nil { panic(err) } @@ -181,7 +183,16 @@ func (r *userResolver) Repositories(ctx context.Context, obj *model.User, cursor } repos = append(repos, &repo) } - return repos, nil + if len(repos) > 25 { + cursor = &model.Cursor{ + Count: 25, + Next: strconv.Itoa(repos[len(repos)-1].ID), + OrderBy: `id DESC`, + Search: "", + } + repos = repos[:25] + } + return &model.RepositoryCursor{repos, cursor}, nil } // Mutation returns generated.MutationResolver implementation. -- 2.38.4