From c60c1685116d00dfb128c19f464aa10b452c8f25 Mon Sep 17 00:00:00 2001 From: Drew DeVault Date: Tue, 12 May 2020 11:24:13 -0400 Subject: [PATCH] api: improve repository references cursor support --- api/graph/generated/generated.go | 152 ++++++++++++++++++++++++++++--- api/graph/model/models_gen.go | 5 + api/graph/schema.graphqls | 16 +++- api/graph/schema.resolvers.go | 94 +++++++++++-------- 4 files changed, 211 insertions(+), 56 deletions(-) diff --git a/api/graph/generated/generated.go b/api/graph/generated/generated.go index fa28c96..18d8a85 100644 --- a/api/graph/generated/generated.go +++ b/api/graph/generated/generated.go @@ -123,6 +123,11 @@ type ComplexityRoot struct { Target func(childComplexity int) int } + ReferenceCursor struct { + Cursor func(childComplexity int) int + Results func(childComplexity int) int + } + Repository struct { AccessControlList func(childComplexity int, cursor *model.Cursor) int Created func(childComplexity int) int @@ -231,7 +236,7 @@ type RepositoryResolver interface { Owner(ctx context.Context, obj *model.Repository) (model.Entity, error) AccessControlList(ctx context.Context, obj *model.Repository, cursor *model.Cursor) (*model.ACLCursor, error) - References(ctx context.Context, obj *model.Repository, cursor *model.Cursor) ([]*model.Reference, error) + References(ctx context.Context, obj *model.Repository, cursor *model.Cursor) (*model.ReferenceCursor, error) } type TreeResolver interface { Entries(ctx context.Context, obj *model.Tree, cursor *model.Cursor) ([]*model.TreeEntry, error) @@ -644,6 +649,20 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in return e.complexity.Reference.Target(childComplexity), true + case "ReferenceCursor.cursor": + if e.complexity.ReferenceCursor.Cursor == nil { + break + } + + return e.complexity.ReferenceCursor.Cursor(childComplexity), true + + case "ReferenceCursor.results": + if e.complexity.ReferenceCursor.Results == nil { + break + } + + return e.complexity.ReferenceCursor.Results(childComplexity), true + case "Repository.accessControlList": if e.complexity.Repository.AccessControlList == nil { break @@ -1218,10 +1237,7 @@ type Repository { ## Plumbing API: # Returns a list of references for this repository - # - # glob: an optional string to filter the list of references, e.g. for tags - # use "refs/tags/*", or leave null to enumerate all references - references(cursor: Cursor): [Reference]! + references(cursor: Cursor): ReferenceCursor # Returns a list of objects for this repository by their IDs (using fully # qualified git object IDs, 40 character hex strings) @@ -1273,6 +1289,17 @@ type ACLCursor { cursor: Cursor } + +# A cursor for enumerating a list of references +# +# 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 ReferenceCursor { + results: [Reference]! + cursor: Cursor +} + # Access Control List entry type ACL { id: Int! @@ -3637,6 +3664,71 @@ func (ec *executionContext) _Reference_follow(ctx context.Context, field graphql return ec.marshalOObject2gitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐObject(ctx, field.Selections, res) } +func (ec *executionContext) _ReferenceCursor_results(ctx context.Context, field graphql.CollectedField, obj *model.ReferenceCursor) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ReferenceCursor", + 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.Reference) + fc.Result = res + return ec.marshalNReference2ᚕᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐReference(ctx, field.Selections, res) +} + +func (ec *executionContext) _ReferenceCursor_cursor(ctx context.Context, field graphql.CollectedField, obj *model.ReferenceCursor) (ret graphql.Marshaler) { + defer func() { + if r := recover(); r != nil { + ec.Error(ctx, ec.Recover(ctx, r)) + ret = graphql.Null + } + }() + fc := &graphql.FieldContext{ + Object: "ReferenceCursor", + 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_id(ctx context.Context, field graphql.CollectedField, obj *model.Repository) (ret graphql.Marshaler) { defer func() { if r := recover(); r != nil { @@ -3972,14 +4064,11 @@ func (ec *executionContext) _Repository_references(ctx context.Context, field gr return graphql.Null } if resTmp == nil { - if !graphql.HasFieldError(ctx, fc) { - ec.Errorf(ctx, "must not be null") - } return graphql.Null } - res := resTmp.([]*model.Reference) + res := resTmp.(*model.ReferenceCursor) fc.Result = res - return ec.marshalNReference2ᚕᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐReference(ctx, field.Selections, res) + return ec.marshalOReferenceCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐReferenceCursor(ctx, field.Selections, res) } func (ec *executionContext) _Repository_objects(ctx context.Context, field graphql.CollectedField, obj *model.Repository) (ret graphql.Marshaler) { @@ -7200,6 +7289,35 @@ func (ec *executionContext) _Reference(ctx context.Context, sel ast.SelectionSet return out } +var referenceCursorImplementors = []string{"ReferenceCursor"} + +func (ec *executionContext) _ReferenceCursor(ctx context.Context, sel ast.SelectionSet, obj *model.ReferenceCursor) graphql.Marshaler { + fields := graphql.CollectFields(ec.OperationContext, sel, referenceCursorImplementors) + + out := graphql.NewFieldSet(fields) + var invalids uint32 + for i, field := range fields { + switch field.Name { + case "__typename": + out.Values[i] = graphql.MarshalString("ReferenceCursor") + case "results": + out.Values[i] = ec._ReferenceCursor_results(ctx, field, obj) + if out.Values[i] == graphql.Null { + invalids++ + } + case "cursor": + out.Values[i] = ec._ReferenceCursor_cursor(ctx, field, obj) + default: + panic("unknown field " + strconv.Quote(field.Name)) + } + } + out.Dispatch() + if invalids > 0 { + return graphql.Null + } + return out +} + var repositoryImplementors = []string{"Repository"} func (ec *executionContext) _Repository(ctx context.Context, sel ast.SelectionSet, obj *model.Repository) graphql.Marshaler { @@ -7274,9 +7392,6 @@ func (ec *executionContext) _Repository(ctx context.Context, sel ast.SelectionSe } }() res = ec._Repository_references(ctx, field, obj) - if res == graphql.Null { - atomic.AddUint32(&invalids, 1) - } return res }) case "objects": @@ -8839,6 +8954,17 @@ func (ec *executionContext) marshalOReference2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgi return ec._Reference(ctx, sel, v) } +func (ec *executionContext) marshalOReferenceCursor2gitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐReferenceCursor(ctx context.Context, sel ast.SelectionSet, v model.ReferenceCursor) graphql.Marshaler { + return ec._ReferenceCursor(ctx, sel, &v) +} + +func (ec *executionContext) marshalOReferenceCursor2ᚖgitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐReferenceCursor(ctx context.Context, sel ast.SelectionSet, v *model.ReferenceCursor) graphql.Marshaler { + if v == nil { + return graphql.Null + } + return ec._ReferenceCursor(ctx, sel, v) +} + func (ec *executionContext) unmarshalORepoInput2gitᚗsrᚗhtᚋאsircmpwnᚋgitᚗsrᚗhtᚋapiᚋgraphᚋmodelᚐRepoInput(ctx context.Context, v interface{}) (model.RepoInput, error) { return ec.unmarshalInputRepoInput(ctx, v) } diff --git a/api/graph/model/models_gen.go b/api/graph/model/models_gen.go index b29873e..06ea0b2 100644 --- a/api/graph/model/models_gen.go +++ b/api/graph/model/models_gen.go @@ -34,6 +34,11 @@ type Filter struct { OrderBy *string `json:"orderBy"` } +type ReferenceCursor struct { + Results []*Reference `json:"results"` + Cursor *Cursor `json:"cursor"` +} + type RepoInput struct { Name string `json:"name"` Description *string `json:"description"` diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls index 1ca83d8..7484e94 100644 --- a/api/graph/schema.graphqls +++ b/api/graph/schema.graphqls @@ -80,10 +80,7 @@ type Repository { ## Plumbing API: # Returns a list of references for this repository - # - # glob: an optional string to filter the list of references, e.g. for tags - # use "refs/tags/*", or leave null to enumerate all references - references(cursor: Cursor): [Reference]! + references(cursor: Cursor): ReferenceCursor # Returns a list of objects for this repository by their IDs (using fully # qualified git object IDs, 40 character hex strings) @@ -135,6 +132,17 @@ type ACLCursor { cursor: Cursor } + +# A cursor for enumerating a list of references +# +# 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 ReferenceCursor { + results: [Reference]! + cursor: Cursor +} + # Access Control List entry type ACL { id: Int! diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go index 862dd9c..1f23de0 100644 --- a/api/graph/schema.resolvers.go +++ b/api/graph/schema.resolvers.go @@ -18,6 +18,40 @@ import ( "github.com/go-git/go-git/v5/plumbing" ) +func (r *aCLResolver) Repository(ctx context.Context, obj *model.ACL) (*model.Repository, error) { + // XXX This could be moved into a loader, but it's unlikely to be a + // frequently utilized endpoint, so I'm not especially interested in the + // extra work/cruft. + repo := (&model.Repository{}).As(`repo`) + query := database. + Select(ctx, repo). + From(`repository repo`). + Join(`access acl ON acl.repo_id = repo.id`). + Where(`acl.id = ?`, obj.ID) + row := query.RunWith(r.DB).QueryRow() + if err := row.Scan(repo.Fields(ctx)...); err != nil { + panic(err) + } + return repo, nil +} + +func (r *aCLResolver) Entity(ctx context.Context, obj *model.ACL) (model.Entity, error) { + // XXX This could be moved into a loader, but it's unlikely to be a + // frequently utilized endpoint, so I'm not especially interested in the + // extra work/cruft. + user := (&model.User{}).As(`u`) + query := database. + Select(ctx, user). + From(`"user" u`). + Join(`access acl ON acl.user_id = u.id`). + Where(`acl.id = ?`, obj.ID) + row := query.RunWith(r.DB).QueryRow() + if err := row.Scan(user.Fields(ctx)...); err != nil { + panic(err) + } + return user, nil +} + func (r *mutationResolver) CreateRepository(ctx context.Context, params *model.RepoInput) (*model.Repository, error) { panic(fmt.Errorf("createRepository: not implemented")) } @@ -127,59 +161,41 @@ func (r *repositoryResolver) AccessControlList(ctx context.Context, obj *model.R return &model.ACLCursor{acls, cursor}, nil } -func (r *aCLResolver) Repository(ctx context.Context, obj *model.ACL) (*model.Repository, error) { - // XXX This could be moved into a loader, but it's unlikely to be a - // frequently utilized endpoint, so I'm not especially interested in the - // extra work/cruft. - repo := (&model.Repository{}).As(`repo`) - query := database. - Select(ctx, repo). - From(`repository repo`). - Join(`access acl ON acl.repo_id = repo.id`). - Where(`acl.id = ?`, obj.ID) - row := query.RunWith(r.DB).QueryRow() - if err := row.Scan(repo.Fields(ctx)...); err != nil { - panic(err) - } - return repo, nil -} - -func (r *aCLResolver) Entity(ctx context.Context, obj *model.ACL) (model.Entity, error) { - // XXX This could be moved into a loader, but it's unlikely to be a - // frequently utilized endpoint, so I'm not especially interested in the - // extra work/cruft. - user := (&model.User{}).As(`u`) - query := database. - Select(ctx, user). - From(`"user" u`). - Join(`access acl ON acl.user_id = u.id`). - Where(`acl.id = ?`, obj.ID) - row := query.RunWith(r.DB).QueryRow() - if err := row.Scan(user.Fields(ctx)...); err != nil { - panic(err) - } - return user, nil -} - -func (r *repositoryResolver) References(ctx context.Context, obj *model.Repository, cursor *model.Cursor) ([]*model.Reference, error) { +func (r *repositoryResolver) References(ctx context.Context, obj *model.Repository, cursor *model.Cursor) (*model.ReferenceCursor, error) { iter, err := obj.Repo().References() if err != nil { return nil, err } defer iter.Close() + + if cursor == nil { + cursor = model.NewCursor(nil) + } + var refs []*model.Reference iter.ForEach(func(ref *plumbing.Reference) error { refs = append(refs, &model.Reference{obj.Repo(), ref}) return nil }) - // TODO: Implement globbing + sort.SliceStable(refs, func(i, j int) bool { return refs[i].Name() < refs[j].Name() }) - if len(refs) > 25 { - refs = refs[:25] + + // TODO: Implement cursor globbing/next + + if len(refs) > cursor.Count { + cursor = &model.Cursor{ + Count: cursor.Count, + Next: refs[len(refs)-1].Name(), + Search: cursor.Search, + } + refs = refs[:cursor.Count] + } else { + cursor = nil } - return refs, nil + + return &model.ReferenceCursor{refs, cursor}, nil } func (r *treeResolver) Entries(ctx context.Context, obj *model.Tree, cursor *model.Cursor) ([]*model.TreeEntry, error) { -- 2.38.4