-
Notifications
You must be signed in to change notification settings - Fork 254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add GraphQL endpoint #228
Add GraphQL endpoint #228
Changes from 5 commits
7fa6dec
5ad76d3
3faef53
feee34f
8c7b727
1226519
24e796a
94d3d6b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,149 @@ | ||
package goshopify | ||
|
||
import ( | ||
"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(string, interface{}, interface{}) (int, 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 *graphQLExtension `json:"extensions"` | ||
} | ||
|
||
type graphQLExtension 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 | ||
// Returns the number of attempts required to perform the request | ||
func (s *GraphQLServiceOp) Query(q string, vars, resp interface{}) (int, 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("graphql.json", data, &gr) | ||
// internal attempts count towards outer total | ||
attempts += s.client.attempts | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I looked at the code again and I think this is a problem. I think I'd want to remove This might be getting a bit much for this PR, to simplify you could, as first step, replace the An alternate option would be to make the retry in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I went with the |
||
|
||
var ra float64 | ||
weirdian2k3 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
if gr.Extensions != nil { | ||
ra = gr.Extensions.Cost.RetryAfterSeconds() | ||
s.client.RateLimits.GraphQLCost = &gr.Extensions.Cost | ||
s.client.RateLimits.RetryAfterSeconds = ra | ||
} | ||
|
||
if len(gr.Errors) > 0 { | ||
re := 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 attempts, RateLimitError{ | ||
RetryAfter: int(math.Ceil(ra)), | ||
ResponseError: ResponseError{ | ||
Status: 200, | ||
Message: err.Message, | ||
}, | ||
} | ||
} | ||
|
||
// only need to retry graphql throttled retries | ||
doRetry = true | ||
} | ||
|
||
re.Errors = append(re.Errors, err.Message) | ||
} | ||
|
||
if doRetry { | ||
wait := time.Duration(math.Ceil(ra)) * time.Second | ||
s.client.log.Debugf("rate limited waiting %s", wait.String()) | ||
time.Sleep(wait) | ||
continue | ||
} | ||
|
||
err = re | ||
} | ||
|
||
return attempts, 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 | ||
oliver006 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
return 0 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's worth mentioning that
Query()
will retry throttled requests but not e..g any other type of 500 error.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To keep the concept of retry more defined, I added this comment to the
WithRetry
method