-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Expand file tree
/
Copy pathprojects_batch_mutation.go
More file actions
132 lines (113 loc) · 3.76 KB
/
Copy pathprojects_batch_mutation.go
File metadata and controls
132 lines (113 loc) · 3.76 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
package github
import (
"context"
"errors"
"fmt"
"reflect"
"sync"
"github.com/shurcooL/githubv4"
)
const batchMutationWireChunkSize = 20
type batchMutationKind int
const (
batchMutationUpdate batchMutationKind = iota
batchMutationClear
)
func (k batchMutationKind) fieldName() string {
if k == batchMutationClear {
return "clearProjectV2ItemFieldValue"
}
return "updateProjectV2ItemFieldValue"
}
type projectV2ItemMutationResult struct {
ProjectV2Item struct {
ID string
FullDatabaseID string `graphql:"fullDatabaseId"`
} `graphql:"projectV2Item"`
}
type reflectedMutationTypeKey struct {
kind batchMutationKind
size int
}
var reflectedMutationTypeCache sync.Map
// Reflected types are cached only by operation and chunk size to bound
// reflect.StructOf's runtime cache; positional names and tags keep request data
// out of type identities. The pinned Client.Mutate binds its third argument to
// $input, so item0 uses $input and later aliases use $input1, $input2, ...
// supplied through the variables map.
func buildAliasedMutationType(kind batchMutationKind, size int) reflect.Type {
key := reflectedMutationTypeKey{kind: kind, size: size}
if cached, ok := reflectedMutationTypeCache.Load(key); ok {
return cached.(reflect.Type)
}
resultType := reflect.TypeFor[projectV2ItemMutationResult]()
fields := make([]reflect.StructField, size)
for i := range size {
varName := "input"
if i > 0 {
varName = fmt.Sprintf("input%d", i)
}
fields[i] = reflect.StructField{
Name: fmt.Sprintf("Item%d", i),
Type: resultType,
Tag: reflect.StructTag(fmt.Sprintf(`graphql:"item%d: %s(input: $%s)"`, i, kind.fieldName(), varName)),
}
}
t := reflect.StructOf(fields)
actual, _ := reflectedMutationTypeCache.LoadOrStore(key, t)
return actual.(reflect.Type)
}
type mutationAliasOutcome struct {
// Populated confirms this alias returned a project item, even when the
// response also contains GraphQL errors.
Populated bool
NodeID string
FullDatabaseID string
}
// The pinned client decodes partial data before returning GraphQL errors but
// discards errors[].path. Populated aliases confirm writes; unpopulated aliases
// remain unknown and must not be retried individually.
func executeAliasedMutation(ctx context.Context, gqlClient *githubv4.Client, kind batchMutationKind, inputs []githubv4.Input) ([]mutationAliasOutcome, error) {
if len(inputs) == 0 {
return nil, nil
}
if len(inputs) > batchMutationWireChunkSize {
return nil, fmt.Errorf("internal error: chunk of %d exceeds wire chunk size %d", len(inputs), batchMutationWireChunkSize)
}
mutationType := buildAliasedMutationType(kind, len(inputs))
mutationPtr := reflect.New(mutationType)
var variables map[string]any
if len(inputs) > 1 {
variables = make(map[string]any, len(inputs)-1)
for i := 1; i < len(inputs); i++ {
variables[fmt.Sprintf("input%d", i)] = inputs[i]
}
}
mutateErr := gqlClient.Mutate(ctx, mutationPtr.Interface(), inputs[0], variables)
outcomes := make([]mutationAliasOutcome, len(inputs))
elem := mutationPtr.Elem()
for i := range inputs {
result, ok := elem.Field(i).Interface().(projectV2ItemMutationResult)
if !ok || result.ProjectV2Item.ID == "" {
continue
}
outcomes[i] = mutationAliasOutcome{
Populated: true,
NodeID: result.ProjectV2Item.ID,
FullDatabaseID: result.ProjectV2Item.FullDatabaseID,
}
}
return outcomes, mutateErr
}
// The pinned client's GraphQL response error type is unexported; transport and
// decoding failures must remain distinguishable.
func isGraphQLResponseError(err error) bool {
for err != nil {
errType := reflect.TypeOf(err)
if errType.PkgPath() == "github.com/shurcooL/graphql" && errType.Name() == "errors" {
return true
}
err = errors.Unwrap(err)
}
return false
}