Skip to content

Commit ec8320e

Browse files
zwickCopilot
andcommitted
Refine Project field resolution
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 1d574277-3735-4356-a753-8354aa6b537c
1 parent cb5f660 commit ec8320e

2 files changed

Lines changed: 184 additions & 208 deletions

File tree

pkg/github/projects_resolver.go

Lines changed: 123 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -70,33 +70,36 @@ type projectFieldsQueryUser struct {
7070
} `graphql:"user(login: $owner)"`
7171
}
7272

73-
// projectFieldsConnection is a paginated list of project fields. We select `id`
74-
// to discriminate the union variant and `databaseId` for the numeric ID REST needs.
73+
type projectV2FieldSelection struct {
74+
ID githubv4.ID
75+
DatabaseID githubv4.Int `graphql:"databaseId"`
76+
Name githubv4.String
77+
DataType githubv4.String
78+
}
79+
80+
type projectV2SingleSelectOptionSelection struct {
81+
ID githubv4.String
82+
Name githubv4.String
83+
}
84+
85+
type projectV2SingleSelectFieldSelection struct {
86+
ID githubv4.ID
87+
DatabaseID githubv4.Int `graphql:"databaseId"`
88+
Name githubv4.String
89+
DataType githubv4.String
90+
Options []projectV2SingleSelectOptionSelection
91+
}
92+
93+
// projectFieldsNode selects `id` to discriminate the union variant and
94+
// `databaseId` for the numeric ID REST needs.
95+
type projectFieldsNode struct {
96+
ProjectV2Field projectV2FieldSelection `graphql:"... on ProjectV2Field"`
97+
ProjectV2IterationField projectV2FieldSelection `graphql:"... on ProjectV2IterationField"`
98+
ProjectV2SingleSelectField projectV2SingleSelectFieldSelection `graphql:"... on ProjectV2SingleSelectField"`
99+
}
100+
75101
type projectFieldsConnection struct {
76-
Nodes []struct {
77-
ProjectV2Field struct {
78-
ID githubv4.ID
79-
DatabaseID githubv4.Int `graphql:"databaseId"`
80-
Name githubv4.String
81-
DataType githubv4.String
82-
} `graphql:"... on ProjectV2Field"`
83-
ProjectV2IterationField struct {
84-
ID githubv4.ID
85-
DatabaseID githubv4.Int `graphql:"databaseId"`
86-
Name githubv4.String
87-
DataType githubv4.String
88-
} `graphql:"... on ProjectV2IterationField"`
89-
ProjectV2SingleSelectField struct {
90-
ID githubv4.ID
91-
DatabaseID githubv4.Int `graphql:"databaseId"`
92-
Name githubv4.String
93-
DataType githubv4.String
94-
Options []struct {
95-
ID githubv4.String
96-
Name githubv4.String
97-
}
98-
} `graphql:"... on ProjectV2SingleSelectField"`
99-
}
102+
Nodes []projectFieldsNode
100103
PageInfo PageInfoFragment
101104
}
102105

@@ -117,52 +120,77 @@ type projectFieldsWithIssueFieldsQueryUser struct {
117120
}
118121

119122
type projectFieldsWithIssueFieldsConnection struct {
120-
Nodes []struct {
121-
ProjectV2Field struct {
122-
ID githubv4.ID
123-
DatabaseID githubv4.Int `graphql:"databaseId"`
124-
Name githubv4.String
125-
DataType githubv4.String
126-
IsIssueField githubv4.Boolean
127-
IssueField projectIssueFieldMetadata
128-
} `graphql:"... on ProjectV2Field"`
129-
ProjectV2IterationField struct {
130-
ID githubv4.ID
131-
DatabaseID githubv4.Int `graphql:"databaseId"`
132-
Name githubv4.String
133-
DataType githubv4.String
134-
} `graphql:"... on ProjectV2IterationField"`
135-
ProjectV2SingleSelectField struct {
136-
ID githubv4.ID
137-
DatabaseID githubv4.Int `graphql:"databaseId"`
138-
Name githubv4.String
139-
DataType githubv4.String
140-
IsIssueField githubv4.Boolean
141-
IssueField projectIssueFieldMetadata
142-
Options []struct {
143-
ID githubv4.String
144-
Name githubv4.String
145-
}
146-
} `graphql:"... on ProjectV2SingleSelectField"`
147-
}
123+
Nodes []projectFieldsWithIssueFieldsNode
148124
PageInfo PageInfoFragment
149125
}
150126

127+
type projectFieldsWithIssueFieldsNode struct {
128+
ProjectV2Field struct {
129+
projectV2FieldSelection
130+
IsIssueField githubv4.Boolean
131+
IssueField projectIssueFieldMetadata
132+
} `graphql:"... on ProjectV2Field"`
133+
ProjectV2IterationField projectV2FieldSelection `graphql:"... on ProjectV2IterationField"`
134+
ProjectV2SingleSelectField struct {
135+
projectV2SingleSelectFieldSelection
136+
IsIssueField githubv4.Boolean
137+
IssueField projectIssueFieldMetadata
138+
} `graphql:"... on ProjectV2SingleSelectField"`
139+
}
140+
141+
func projectFieldsQueryVariables(owner string, projectNumber int, after *githubv4.String) map[string]any {
142+
vars := map[string]any{
143+
"owner": githubv4.String(owner),
144+
"projectNumber": githubv4.Int(int32(projectNumber)), //nolint:gosec // Project numbers are small
145+
"first": githubv4.Int(resolverFieldsPageSize),
146+
"after": (*githubv4.String)(nil),
147+
}
148+
if after != nil {
149+
vars["after"] = after
150+
}
151+
return vars
152+
}
153+
154+
func resolvedProjectField(node projectFieldsNode) (ResolvedField, bool) {
155+
switch {
156+
case node.ProjectV2SingleSelectField.ID != nil:
157+
options := make([]ResolvedFieldOption, 0, len(node.ProjectV2SingleSelectField.Options))
158+
for _, option := range node.ProjectV2SingleSelectField.Options {
159+
options = append(options, ResolvedFieldOption{ID: string(option.ID), Name: string(option.Name)})
160+
}
161+
return ResolvedField{
162+
ID: fmt.Sprintf("%d", node.ProjectV2SingleSelectField.DatabaseID),
163+
NodeID: fmt.Sprintf("%v", node.ProjectV2SingleSelectField.ID),
164+
Name: string(node.ProjectV2SingleSelectField.Name),
165+
DataType: string(node.ProjectV2SingleSelectField.DataType),
166+
Options: options,
167+
}, true
168+
case node.ProjectV2IterationField.ID != nil:
169+
return ResolvedField{
170+
ID: fmt.Sprintf("%d", node.ProjectV2IterationField.DatabaseID),
171+
NodeID: fmt.Sprintf("%v", node.ProjectV2IterationField.ID),
172+
Name: string(node.ProjectV2IterationField.Name),
173+
DataType: string(node.ProjectV2IterationField.DataType),
174+
}, true
175+
case node.ProjectV2Field.ID != nil:
176+
return ResolvedField{
177+
ID: fmt.Sprintf("%d", node.ProjectV2Field.DatabaseID),
178+
NodeID: fmt.Sprintf("%v", node.ProjectV2Field.ID),
179+
Name: string(node.ProjectV2Field.Name),
180+
DataType: string(node.ProjectV2Field.DataType),
181+
}, true
182+
default:
183+
return ResolvedField{}, false
184+
}
185+
}
186+
151187
// listAllProjectFields fetches every field on a project, paginating as needed.
152188
func listAllProjectFields(ctx context.Context, gqlClient *githubv4.Client, owner, ownerType string, projectNumber int) ([]ResolvedField, error) {
153189
all := []ResolvedField{}
154190
var after *githubv4.String
155191

156192
for {
157-
vars := map[string]any{
158-
"owner": githubv4.String(owner),
159-
"projectNumber": githubv4.Int(int32(projectNumber)), //nolint:gosec // Project numbers are small
160-
"first": githubv4.Int(resolverFieldsPageSize),
161-
"after": (*githubv4.String)(nil),
162-
}
163-
if after != nil {
164-
vars["after"] = after
165-
}
193+
vars := projectFieldsQueryVariables(owner, projectNumber, after)
166194

167195
var conn projectFieldsConnection
168196
if ownerType == "org" {
@@ -179,34 +207,9 @@ func listAllProjectFields(ctx context.Context, gqlClient *githubv4.Client, owner
179207
conn = q.User.ProjectV2.Fields
180208
}
181209

182-
for _, n := range conn.Nodes {
183-
switch {
184-
case n.ProjectV2SingleSelectField.ID != nil:
185-
opts := make([]ResolvedFieldOption, 0, len(n.ProjectV2SingleSelectField.Options))
186-
for _, o := range n.ProjectV2SingleSelectField.Options {
187-
opts = append(opts, ResolvedFieldOption{ID: string(o.ID), Name: string(o.Name)})
188-
}
189-
all = append(all, ResolvedField{
190-
ID: fmt.Sprintf("%d", n.ProjectV2SingleSelectField.DatabaseID),
191-
NodeID: fmt.Sprintf("%v", n.ProjectV2SingleSelectField.ID),
192-
Name: string(n.ProjectV2SingleSelectField.Name),
193-
DataType: string(n.ProjectV2SingleSelectField.DataType),
194-
Options: opts,
195-
})
196-
case n.ProjectV2IterationField.ID != nil:
197-
all = append(all, ResolvedField{
198-
ID: fmt.Sprintf("%d", n.ProjectV2IterationField.DatabaseID),
199-
NodeID: fmt.Sprintf("%v", n.ProjectV2IterationField.ID),
200-
Name: string(n.ProjectV2IterationField.Name),
201-
DataType: string(n.ProjectV2IterationField.DataType),
202-
})
203-
case n.ProjectV2Field.ID != nil:
204-
all = append(all, ResolvedField{
205-
ID: fmt.Sprintf("%d", n.ProjectV2Field.DatabaseID),
206-
NodeID: fmt.Sprintf("%v", n.ProjectV2Field.ID),
207-
Name: string(n.ProjectV2Field.Name),
208-
DataType: string(n.ProjectV2Field.DataType),
209-
})
210+
for _, node := range conn.Nodes {
211+
if field, ok := resolvedProjectField(node); ok {
212+
all = append(all, field)
210213
}
211214
}
212215

@@ -234,15 +237,7 @@ func listAllProjectFieldsWithIssueFieldMetadata(ctx context.Context, gqlClient *
234237
ctx = ghcontext.WithGraphQLFeatures(ctx, "issue_fields")
235238

236239
for {
237-
vars := map[string]any{
238-
"owner": githubv4.String(owner),
239-
"projectNumber": githubv4.Int(int32(projectNumber)), //nolint:gosec // Project numbers are small
240-
"first": githubv4.Int(resolverFieldsPageSize),
241-
"after": (*githubv4.String)(nil),
242-
}
243-
if after != nil {
244-
vars["after"] = after
245-
}
240+
vars := projectFieldsQueryVariables(owner, projectNumber, after)
246241

247242
var conn projectFieldsWithIssueFieldsConnection
248243
if ownerType == "org" {
@@ -259,47 +254,28 @@ func listAllProjectFieldsWithIssueFieldMetadata(ctx context.Context, gqlClient *
259254
conn = q.User.ProjectV2.Fields
260255
}
261256

262-
for _, n := range conn.Nodes {
263-
switch {
264-
case n.ProjectV2SingleSelectField.ID != nil:
265-
opts := make([]ResolvedFieldOption, 0, len(n.ProjectV2SingleSelectField.Options))
266-
for _, o := range n.ProjectV2SingleSelectField.Options {
267-
opts = append(opts, ResolvedFieldOption{ID: string(o.ID), Name: string(o.Name)})
268-
}
269-
issueFieldNodeID := ""
270-
if n.ProjectV2SingleSelectField.IsIssueField {
271-
opts = opts[:0]
272-
issueFieldNodeID = issueFieldNodeIDForType(n.ProjectV2SingleSelectField.DataType, n.ProjectV2SingleSelectField.IssueField)
273-
for _, o := range n.ProjectV2SingleSelectField.IssueField.SingleSelect.Options {
274-
opts = append(opts, ResolvedFieldOption{ID: fmt.Sprintf("%v", o.ID), Name: string(o.Name)})
275-
}
257+
for _, node := range conn.Nodes {
258+
field, ok := resolvedProjectField(projectFieldsNode{
259+
ProjectV2Field: node.ProjectV2Field.projectV2FieldSelection,
260+
ProjectV2IterationField: node.ProjectV2IterationField,
261+
ProjectV2SingleSelectField: node.ProjectV2SingleSelectField.projectV2SingleSelectFieldSelection,
262+
})
263+
if !ok {
264+
continue
265+
}
266+
267+
if field.DataType == "SINGLE_SELECT" && bool(node.ProjectV2SingleSelectField.IsIssueField) {
268+
field.IsIssueField = true
269+
field.IssueFieldNodeID = issueFieldNodeIDForType(field.DataType, node.ProjectV2SingleSelectField.IssueField)
270+
field.Options = field.Options[:0]
271+
for _, option := range node.ProjectV2SingleSelectField.IssueField.SingleSelect.Options {
272+
field.Options = append(field.Options, ResolvedFieldOption{ID: fmt.Sprintf("%v", option.ID), Name: string(option.Name)})
276273
}
277-
all = append(all, ResolvedField{
278-
ID: fmt.Sprintf("%d", n.ProjectV2SingleSelectField.DatabaseID),
279-
NodeID: fmt.Sprintf("%v", n.ProjectV2SingleSelectField.ID),
280-
Name: string(n.ProjectV2SingleSelectField.Name),
281-
DataType: string(n.ProjectV2SingleSelectField.DataType),
282-
Options: opts,
283-
IsIssueField: bool(n.ProjectV2SingleSelectField.IsIssueField),
284-
IssueFieldNodeID: issueFieldNodeID,
285-
})
286-
case n.ProjectV2IterationField.ID != nil:
287-
all = append(all, ResolvedField{
288-
ID: fmt.Sprintf("%d", n.ProjectV2IterationField.DatabaseID),
289-
NodeID: fmt.Sprintf("%v", n.ProjectV2IterationField.ID),
290-
Name: string(n.ProjectV2IterationField.Name),
291-
DataType: string(n.ProjectV2IterationField.DataType),
292-
})
293-
case n.ProjectV2Field.ID != nil:
294-
all = append(all, ResolvedField{
295-
ID: fmt.Sprintf("%d", n.ProjectV2Field.DatabaseID),
296-
NodeID: fmt.Sprintf("%v", n.ProjectV2Field.ID),
297-
Name: string(n.ProjectV2Field.Name),
298-
DataType: string(n.ProjectV2Field.DataType),
299-
IsIssueField: bool(n.ProjectV2Field.IsIssueField),
300-
IssueFieldNodeID: issueFieldNodeIDForType(n.ProjectV2Field.DataType, n.ProjectV2Field.IssueField),
301-
})
274+
} else if bool(node.ProjectV2Field.IsIssueField) {
275+
field.IsIssueField = true
276+
field.IssueFieldNodeID = issueFieldNodeIDForType(field.DataType, node.ProjectV2Field.IssueField)
302277
}
278+
all = append(all, field)
303279
}
304280

305281
if !bool(conn.PageInfo.HasNextPage) {
@@ -328,12 +304,18 @@ func issueFieldSchemaUnavailable(err error) bool {
328304
return true
329305
}
330306
}
307+
for _, typeName := range []string{"IssueFieldText", "IssueFieldNumber", "IssueFieldDate", "IssueFieldSingleSelect"} {
308+
if strings.Contains(message, fmt.Sprintf("No such type %s, so it can't be a fragment condition", typeName)) ||
309+
strings.Contains(message, fmt.Sprintf(`Unknown type "%s".`, typeName)) {
310+
return true
311+
}
312+
}
331313
return false
332314
}
333315

334-
func issueFieldNodeIDForType(dataType githubv4.String, metadata projectIssueFieldMetadata) string {
316+
func issueFieldNodeIDForType(dataType string, metadata projectIssueFieldMetadata) string {
335317
var id githubv4.ID
336-
switch strings.ToUpper(string(dataType)) {
318+
switch strings.ToUpper(dataType) {
337319
case "TEXT":
338320
id = metadata.Text.ID
339321
case "NUMBER":
@@ -349,13 +331,6 @@ func issueFieldNodeIDForType(dataType githubv4.String, metadata projectIssueFiel
349331
return fmt.Sprintf("%v", id)
350332
}
351333

352-
// resolveProjectFieldByName resolves a field by display name. Returns a
353-
// structured error on not-found, ambiguous, or wrong-data-type (when
354-
// expectedDataType is set) so the agent can self-correct.
355-
func resolveProjectFieldByName(ctx context.Context, gqlClient *githubv4.Client, owner, ownerType string, projectNumber int, fieldName, expectedDataType string) (*ResolvedField, error) {
356-
return resolveProjectFieldByNameWithLister(ctx, gqlClient, owner, ownerType, projectNumber, fieldName, expectedDataType, listAllProjectFields)
357-
}
358-
359334
func resolveProjectFieldForUpdateByName(ctx context.Context, gqlClient *githubv4.Client, owner, ownerType string, projectNumber int, fieldName, expectedDataType string) (*ResolvedField, error) {
360335
return resolveProjectFieldByNameWithLister(ctx, gqlClient, owner, ownerType, projectNumber, fieldName, expectedDataType, listAllProjectFieldsForUpdate)
361336
}

0 commit comments

Comments
 (0)