New minor release after complete rewrite.

Brings neat and testable codebase with minimum allocations in mind.
This commit is contained in:
2023-04-07 15:27:44 +01:00
parent 6a8f99f6ef
commit e0913f8824
26 changed files with 1417 additions and 1129 deletions
+88 -112
View File
@@ -1,141 +1,117 @@
package gql
import (
"bytes"
"crypto/md5"
"errors"
"fmt"
"io/ioutil"
"net/http"
"context"
retry "github.com/avast/retry-go"
"github.com/lukaszraczylo/pandati"
"github.com/lukaszraczylo/go-simple-graphql/utils/helpers"
"github.com/gookit/goutil/strutil"
jsoniter "github.com/json-iterator/go"
)
type requestBase struct {
Variables interface{} `json:"variables"`
Query string `json:"query"`
}
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type queryResults struct {
Data interface{} `json:"data"`
Errors []struct {
Message interface{} `json:"message"`
} `json:"errors"`
}
func (g *GraphQL) queryBuilder(queryContent string, queryVariables interface{}) ([]byte, error) {
var qb = &requestBase{
Query: queryContent,
Variables: queryVariables,
}
j2, err := json.Marshal(qb)
func (c *BaseClient) convertToJson(v any) []byte {
json, err := json.Marshal(v)
if err != nil {
g.Log.Critical("Unable to marshal the query", map[string]interface{}{"_error": err.Error(), "_query": queryContent, "_variables": queryVariables})
return []byte{}, err
c.Logger.Error(c, "Can't convert to json;", "error", err.Error())
return nil
}
return j2, err
return json
}
func (g *GraphQL) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (responseContent string, err error) {
g.Log.Debug("Query details", map[string]interface{}{"_query": queryContent, "_variables": queryVariables})
query, err := g.queryBuilder(queryContent, queryVariables)
if err != nil {
g.Log.Error("Unable to build the query", map[string]interface{}{"_error": err.Error()})
return "", err
func (c *BaseClient) NewQuery(q ...any) *Query {
query := &Query{}
if len(q) > 0 {
for _, v := range q {
switch val := v.(type) {
case string:
query.query = val
case map[string]interface{}:
query.variables = val
case context.Context:
query.context = val
}
}
}
var body []byte
var queryResult queryResults
query.compiledQuery = c.convertToJson(request{
Query: query.query,
Variables: query.variables,
})
c.Logger.Debug(c, "Clearing previously prepared variables and query")
query.variables = nil
if query.context == nil {
query.context = context.Background()
}
if c.validate {
c.Logger.Warn(c, "Validating query is not active")
}
query.query = ""
c.Logger.Debug(c, "Query prepared;", "query", helpers.BytesToString(query.compiledQuery))
return query
}
// Query is a function that sends a query to the server
// It takes 3 arguments:
// 1. queryContent: the query string
// 2. queryVariables: the variables for the query
// 3. queryHeaders: the headers for the query
// It looks a bit weird because of the backward compatibility
func (c *BaseClient) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (any, error) {
query := c.NewQuery(queryContent, queryVariables)
var queryHash string
var cachedResponse []byte
cacheEnabled := queryHeaders["gqlcache"] == true
// If header 'gqlcache' is set to true, we will enforce the cache for the query
if cacheEnabled || g.Cache {
g.Log.Debug("Forced cache for query", map[string]interface{}{"_query": queryContent, "_headers": queryHeaders})
g.Cache = true
queryHash = fmt.Sprintf("%x", md5.Sum(query))
if c.cache.enabled {
queryHash = strutil.Md5(query.compiledQuery)
c.Logger.Debug(c, "Hash calculated;", "hash:", queryHash)
g.Log.Debug("Checking the cache for the query", map[string]interface{}{"_query": queryHash})
if entry, entryInfo, err := g.CacheStore.GetWithInfo(queryHash); err == nil {
g.Log.Debug("Found the query in the cache", map[string]interface{}{"_query": queryHash})
if pandati.IsZero(entryInfo.EntryStatus) {
return pandati.BytesToString(entry), nil
}
cachedResponse = c.cacheLookup(queryHash)
if cachedResponse != nil {
c.Logger.Debug(c, "Found cached response")
return c.decodeResponse(cachedResponse), nil
} else {
g.Log.Debug("Unable to find the query in the cache", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
c.Logger.Debug(c, "No cached response found")
}
}
httpRequest, err := http.NewRequest("POST", g.Endpoint, bytes.NewBuffer(query))
// httpRequest.Header.Set("Content-Type", "application/json")
response, err := c.executeQuery(query.compiledQuery, queryHeaders)
if err != nil {
g.Log.Error("Unable to create the request", map[string]interface{}{"_error": err.Error()})
return "", err
c.Logger.Error(c, "Error while executing query;", "error", err.Error())
return nil, err
}
for header, value := range queryHeaders {
httpRequest.Header.Add(header, fmt.Sprintf("%v", value))
}
var httpResponse *http.Response
err = retry.Do(
func() error {
g.Log.Debug("Sending the query")
httpResponse, err = g.HttpClient.Do(httpRequest)
if err != nil {
g.Log.Debug("Unable to send the query RETRY", map[string]interface{}{"_error": err.Error()})
return err
}
defer httpResponse.Body.Close()
if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 {
return err
}
body, err = ioutil.ReadAll(httpResponse.Body)
if err != nil {
g.Log.Debug("Unable to read the response", map[string]interface{}{"_error": err.Error()})
return err
}
err = json.Unmarshal(body, &queryResult)
if err != nil {
g.Log.Debug("Unable to unmarshal the query", map[string]interface{}{"_error": err.Error()})
return err
}
return nil
},
)
if !pandati.IsZero(queryResult.Errors) {
g.Log.Error("Query returned error", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_error": fmt.Sprintf("%v", queryResult.Errors), "_response_code": httpResponse.StatusCode})
return "", fmt.Errorf("%v", queryResult.Errors)
}
if pandati.IsZero(queryResult.Data) {
g.Log.Error("Query returned no data", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_response_code": httpResponse.StatusCode})
return "", errors.New("Query returned no data")
}
responseContent, err = json.MarshalToString(queryResult.Data)
jsonData, err := json.Marshal(response)
if err != nil {
g.Log.Error("Invalid data result", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_result": responseContent, "_response_code": httpResponse.StatusCode})
return "", err
c.Logger.Error(c, "Error while converting to json;", "error", err.Error())
return nil, err
}
if g.Cache {
g.Log.Debug("Caching the query", map[string]interface{}{"_query": queryHash})
if queryContent[0:5] == "query" {
err = g.CacheStore.Set(queryHash, pandati.StringToBytes(responseContent))
if err != nil {
g.Log.Error("Unable to cache the query", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
}
}
if c.cache.enabled {
c.cache.client.Set(queryHash, jsonData)
}
return
return c.decodeResponse(jsonData), err
}
func (c *BaseClient) decodeResponse(jsonData []byte) any {
switch c.responseType {
case "string":
return helpers.BytesToString(jsonData)
case "byte":
return jsonData
default:
c.Logger.Error(c, "Unknown response type", "response", c.responseType)
return nil
}
}