forked from getconversio/go-shopify
-
Notifications
You must be signed in to change notification settings - Fork 256
/
graphql.go
150 lines (121 loc) · 3.68 KB
/
graphql.go
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
package goshopify
import (
"context"
"math"
"time"
)
// GraphQLService is an interface to interact with the graphql endpoint
// of the Shopify API
// See https://shopify.dev/docs/admin-api/graphql/reference
type GraphQLService interface {
Query(context.Context, string, interface{}, interface{}) error
}
// GraphQLServiceOp handles communication with the graphql endpoint of
// the Shopify API.
type GraphQLServiceOp struct {
client *Client
}
type graphQLResponse struct {
Data interface{} `json:"data"`
Errors []graphQLError `json:"errors"`
Extensions *graphQLExtensions `json:"extensions"`
}
type graphQLExtensions struct {
Cost GraphQLCost `json:"cost"`
}
// GraphQLCost represents the cost of the graphql query
type GraphQLCost struct {
RequestedQueryCost int `json:"requestedQueryCost"`
ActualQueryCost *int `json:"actualQueryCost"`
ThrottleStatus GraphQLThrottleStatus `json:"throttleStatus"`
}
// GraphQLThrottleStatus represents the status of the shop's rate limit points
type GraphQLThrottleStatus struct {
MaximumAvailable float64 `json:"maximumAvailable"`
CurrentlyAvailable float64 `json:"currentlyAvailable"`
RestoreRate float64 `json:"restoreRate"`
}
type graphQLError struct {
Message string `json:"message"`
Extensions *graphQLErrorExtensions `json:"extensions"`
Locations []graphQLErrorLocation `json:"locations"`
}
type graphQLErrorExtensions struct {
Code string
Documentation string
}
const (
graphQLErrorCodeThrottled = "THROTTLED"
)
type graphQLErrorLocation struct {
Line int `json:"line"`
Column int `json:"column"`
}
// Query creates a graphql query against the Shopify API
// the "data" portion of the response is unmarshalled into resp
func (s *GraphQLServiceOp) Query(ctx context.Context, q string, vars, resp interface{}) error {
data := struct {
Query string `json:"query"`
Variables interface{} `json:"variables"`
}{
Query: q,
Variables: vars,
}
attempts := 0
for {
gr := graphQLResponse{
Data: resp,
}
err := s.client.Post(ctx, "graphql.json", data, &gr)
// internal attempts count towards outer total
attempts += 1
var retryAfterSecs float64
if gr.Extensions != nil {
retryAfterSecs = gr.Extensions.Cost.RetryAfterSeconds()
s.client.RateLimits.GraphQLCost = &gr.Extensions.Cost
s.client.RateLimits.RetryAfterSeconds = retryAfterSecs
}
if len(gr.Errors) > 0 {
responseError := ResponseError{Status: 200}
var doRetry bool
for _, err := range gr.Errors {
if err.Extensions != nil && err.Extensions.Code == graphQLErrorCodeThrottled {
if attempts >= s.client.retries {
return RateLimitError{
RetryAfter: int(math.Ceil(retryAfterSecs)),
ResponseError: ResponseError{
Status: 200,
Message: err.Message,
},
}
}
// only need to retry graphql throttled retries
doRetry = true
}
responseError.Errors = append(responseError.Errors, err.Message)
}
if doRetry {
wait := time.Duration(math.Ceil(retryAfterSecs)) * time.Second
s.client.log.Debugf("rate limited waiting %s", wait.String())
time.Sleep(wait)
continue
}
err = responseError
}
return err
}
}
// RetryAfterSeconds returns the estimated retry after seconds based on
// the requested query cost and throttle status
func (c GraphQLCost) RetryAfterSeconds() float64 {
var diff float64
if c.ActualQueryCost != nil {
diff = c.ThrottleStatus.CurrentlyAvailable - float64(*c.ActualQueryCost)
} else {
diff = c.ThrottleStatus.CurrentlyAvailable - float64(c.RequestedQueryCost)
}
if diff < 0 {
return -diff / c.ThrottleStatus.RestoreRate
}
return 0
}