Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
335 changes: 316 additions & 19 deletions api/graphql/generated/generated.go

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions api/graphql/gqlgen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ models:
resolver: true
reactionGroups:
resolver: true
# databaseId narrows the int64 db id to Int; activeLockReason maps the raw
# reason string onto the LockReason enum; viewerCanUpdate needs the viewer.
databaseId:
resolver: true
activeLockReason:
resolver: true
viewerCanUpdate:
resolver: true
PullRequest:
fields:
commits:
Expand Down
67 changes: 63 additions & 4 deletions api/graphql/issue.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

83 changes: 83 additions & 0 deletions api/graphql/issue_fields_contract_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package graphql_test

import (
"encoding/json"
"testing"
)

// TestIssueRemainderFields covers R02-16: an Issue serves databaseId,
// activeLockReason, isPinned, viewerCanUpdate, reactionGroups, and an assignees
// connection that carries pageInfo.
func TestIssueRemainderFields(t *testing.T) {
srv, token := issueServer(t)

q := `query($owner:String!,$name:String!,$number:Int!){
repository(owner:$owner,name:$name){
issue(number:$number){
databaseId
activeLockReason
isPinned
viewerCanUpdate
reactionGroups { content users { totalCount } }
assignees(first: 10) { totalCount pageInfo { hasNextPage } nodes { login } }
}
}
}`
got := post(t, srv, token, q, map[string]any{"owner": "octocat", "name": "hello", "number": 1})
var env struct {
Data struct {
Repository struct {
Issue struct {
DatabaseID *int `json:"databaseId"`
ActiveLockReason *string `json:"activeLockReason"`
IsPinned bool `json:"isPinned"`
ViewerCanUpdate bool `json:"viewerCanUpdate"`
ReactionGroups []struct {
Content string `json:"content"`
} `json:"reactionGroups"`
Assignees struct {
TotalCount int `json:"totalCount"`
PageInfo struct {
HasNextPage bool `json:"hasNextPage"`
} `json:"pageInfo"`
Nodes []struct {
Login string `json:"login"`
} `json:"nodes"`
} `json:"assignees"`
} `json:"issue"`
} `json:"repository"`
} `json:"data"`
Errors []struct {
Message string `json:"message"`
} `json:"errors"`
}
if err := json.Unmarshal(got, &env); err != nil {
t.Fatalf("unmarshal: %v, body %s", err, got)
}
if len(env.Errors) != 0 {
t.Fatalf("errors = %v, body %s", env.Errors, got)
}
iss := env.Data.Repository.Issue

if iss.DatabaseID == nil || *iss.DatabaseID <= 0 {
t.Errorf("databaseId = %v, want a positive db id", iss.DatabaseID)
}
if iss.ActiveLockReason != nil {
t.Errorf("activeLockReason = %v, want null on an unlocked issue", *iss.ActiveLockReason)
}
if iss.IsPinned {
t.Errorf("isPinned = true, want false")
}
// The token belongs to octocat, who owns the repo and authored the issue, so
// the viewer can update it.
if !iss.ViewerCanUpdate {
t.Errorf("viewerCanUpdate = false, want true for the owner/author")
}
if iss.ReactionGroups == nil {
t.Errorf("reactionGroups = null, want a (possibly empty) array")
}
// assignees must carry pageInfo (R02-16): the connection used to omit it.
if iss.Assignees.PageInfo.HasNextPage {
t.Errorf("assignees.pageInfo.hasNextPage = true, want false for a full single page")
}
}
2 changes: 1 addition & 1 deletion api/graphql/pull.resolvers.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions api/graphql/schema/issue.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -73,12 +73,21 @@ interface Actor {
type Issue implements Node {
id: ID!
number: Int!
# databaseId is the issue's integer database id, the legacy REST `id`.
databaseId: Int
title: String!
body: String!
state: IssueState!
stateReason: IssueStateReason
url: URI!
locked: Boolean!
# activeLockReason is why the conversation was locked, null when unlocked or
# locked without a reason.
activeLockReason: LockReason
# isPinned reports whether the issue is pinned to the repository.
isPinned: Boolean!
# viewerCanUpdate reports whether the requesting viewer can edit this issue.
viewerCanUpdate: Boolean!
closed: Boolean!
author: Actor
createdAt: DateTime!
Expand Down Expand Up @@ -158,6 +167,7 @@ type Milestone implements Node {
# UserConnection is the Relay connection over users (assignees, etc.).
type UserConnection {
nodes: [User]
pageInfo: PageInfo!
totalCount: Int!
}

Expand Down
14 changes: 8 additions & 6 deletions presenter/gqlissue.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,14 @@ func (b *URLBuilder) GQLIssue(owner, repo string, iss *domain.Issue, format node
Labels: b.gqlLabelConnection(owner, repo, iss.Labels, format),
Assignees: b.GQLUserConnection(iss.Assignees, format),
Milestone: b.GQLMilestone(owner, repo, iss.Milestone, format),
Comments: &gqlmodel.IssueCommentConnection{TotalCount: int32(iss.CommentsCount), PageInfo: &gqlmodel.PageInfo{}},
ReactionGroups: []gqlmodel.ReactionGroup{}, // Githome does not store reactions
RepoOwner: owner,
RepoName: repo,
PK: iss.PK,
UserPK: iss.UserPK,
Comments: &gqlmodel.IssueCommentConnection{TotalCount: int32(iss.CommentsCount), PageInfo: &gqlmodel.PageInfo{}},
ReactionGroups: gqlReactionGroups(iss.Reactions),
RepoOwner: owner,
RepoName: repo,
PK: iss.PK,
UserPK: iss.UserPK,
DatabaseID: iss.ID,
ActiveLockReason: iss.ActiveLockReason,
}
if sr := issueStateReason(iss.StateReason); sr != nil {
out.StateReason = sr
Expand Down
10 changes: 10 additions & 0 deletions presenter/gqlmodel/issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ type Issue struct {
Comments *IssueCommentConnection
ReactionGroups []ReactionGroup // emoji reaction counts; always non-nil (empty when none)

// DatabaseID is the issue's integer database id (the legacy REST id),
// served as databaseId by the resolver.
DatabaseID int64
// ActiveLockReason is GitHub's raw lock-reason string, nil when unlocked or
// locked without a reason; the resolver maps it onto the LockReason enum.
ActiveLockReason *string
// IsPinned reports whether the issue is pinned to the repository. Githome
// does not model pinned issues yet, so it is always false.
IsPinned bool

// RepoOwner and RepoName carry the repository coordinates so the comments
// field resolver can page the issue's comments. They are not part of the
// GraphQL schema, so gqlgen ignores them; the presenter fills them.
Expand Down
1 change: 1 addition & 0 deletions presenter/gqlmodel/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ func (User) IsRequestedReviewer() {}
// UserConnection is the Relay connection over a set of users (assignees, etc.).
type UserConnection struct {
Nodes []*User
PageInfo *PageInfo
TotalCount int32
}

Expand Down
4 changes: 2 additions & 2 deletions presenter/gqlrepository.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (b *URLBuilder) GQLRepository(r *domain.Repo, branch *git.Branch, format no
SquashMergeAllowed: true,
RebaseMergeAllowed: true,
RepositoryTopics: b.gqlRepositoryTopics(r),
Watchers: &gqlmodel.UserConnection{Nodes: []*gqlmodel.User{}, TotalCount: 0},
Watchers: &gqlmodel.UserConnection{Nodes: []*gqlmodel.User{}, PageInfo: &gqlmodel.PageInfo{}, TotalCount: 0},
Languages: &gqlmodel.LanguageConnection{Nodes: []*gqlmodel.Language{}, Edges: []*gqlmodel.LanguageEdge{}, PageInfo: &gqlmodel.PageInfo{}, TotalCount: 0, TotalSize: 0},
RepoOwner: r.Owner.Login,
RepoName: r.Name,
Expand Down Expand Up @@ -161,7 +161,7 @@ func (b *URLBuilder) GQLUserConnection(users []*domain.User, format nodeid.Forma
nodes = append(nodes, n)
}
}
return &gqlmodel.UserConnection{Nodes: nodes, TotalCount: int32(len(nodes))}
return &gqlmodel.UserConnection{Nodes: nodes, PageInfo: &gqlmodel.PageInfo{}, TotalCount: int32(len(nodes))}
}

// GQLMilestone renders a domain milestone into the GraphQL Milestone shape.
Expand Down
Loading