mirror of
https://github.com/lukaszraczylo/go-simple-graphql.git
synced 2026-10-12 00:45:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
569867d04b | ||
|
|
9a58df7a54 | ||
|
|
4e60a90ce4 | ||
|
|
2f4c8cc5d8 | ||
|
|
154233df14
|
||
|
|
29afbd08fd
|
||
|
|
a107ee953b
|
||
|
|
58094f85c7
|
||
|
|
ac957ac95b
|
||
|
|
ef4ab45424
|
||
|
|
4fc826c1f6
|
||
|
|
3217edf37f
|
||
|
|
ae96059f61
|
||
|
|
2ce7016d7e
|
||
|
|
9da7c9bec9
|
||
|
|
ca13be5ef6
|
||
|
|
1df2ae5d16
|
||
|
|
39f489210b
|
||
|
|
21c9119bcb
|
||
|
|
a7b385f0b7
|
||
|
|
7d3abf5464
|
||
|
|
7af4e1f96a
|
||
|
|
7623410bfe
|
||
|
|
8e26d79df7
|
||
|
|
aa25c43795
|
||
|
|
dfb082ad51
|
||
|
|
ce736958c6
|
||
|
|
4472041b21
|
||
|
|
b81e612f51
|
||
|
|
3c508eaf15
|
||
|
|
4d0c04cb26
|
||
|
|
55182f2f3d
|
||
|
|
841c136e71
|
||
|
|
833517f35c
|
||
|
|
07e4f75a0a
|
||
|
|
b8044c5d67
|
||
|
|
cf3076d701
|
||
|
|
5a222aa316
|
||
|
|
d843238d46
|
||
|
|
b9cd614ae4
|
||
|
|
eef4e39611
|
||
|
|
d764980390
|
||
|
|
689b1efc43
|
||
|
|
23f88a91d2
|
||
|
|
664a692c2d
|
||
|
|
2a628a7da7
|
||
|
|
e0913f8824
|
||
|
|
6a8f99f6ef
|
||
|
|
a5c4643a5e
|
||
|
|
42e1afbf77
|
||
|
|
9e8df7e297
|
||
|
|
405d159eb9
|
||
|
|
b3e594fc4f
|
||
|
|
32d9e0bd80
|
||
|
|
e706b07488
|
||
|
|
32091fc8cd
|
||
|
|
edc695fe2b
|
||
|
|
bd0c5ec373
|
||
|
|
952749d7ff
|
||
|
|
277ef21263
|
||
|
|
4980aeb28a
|
||
|
|
91f65ebd46
|
||
|
|
d5655ae2fa
|
||
|
|
9b65ba069f
|
||
|
|
feed2f36c2
|
||
|
|
966d9ccd0a
|
||
|
|
3232498e94
|
@@ -12,6 +12,6 @@ jobs:
|
||||
shared:
|
||||
uses: telegram-bot-app/ci-scripts/.github/workflows/library-test-publish.yaml@main
|
||||
with:
|
||||
enable-code-scans: true
|
||||
enable-code-scans: false
|
||||
secrets:
|
||||
ghcr-token: ${{ secrets.GHCR_TOKEN }}
|
||||
@@ -11,7 +11,7 @@ help: ## display this help
|
||||
|
||||
.PHONY: test
|
||||
test: ## run tests on library
|
||||
@LOG_LEVEL=debug go test $(ADDITIONAL_BUILD_FLAGS) -v -cover ./...
|
||||
@LOG_LEVEL=debug go test $(ADDITIONAL_BUILD_FLAGS) -v -cover ./... -race
|
||||
|
||||
.PHONY: test-packages
|
||||
test-packages: ## run tests on packages
|
||||
@@ -23,6 +23,7 @@ all: test-packages test
|
||||
.PHONY: update
|
||||
update: ## update dependencies
|
||||
@go get -u -v ./...
|
||||
@go mod tidy -v
|
||||
|
||||
sonar:
|
||||
sonar-scanner \
|
||||
|
||||
@@ -9,7 +9,10 @@ Ps. It's Hasura friendly.
|
||||
- [Features](#features)
|
||||
- [Usage example](#usage-example)
|
||||
- [Environment variables](#environment-variables)
|
||||
- [Modifiers on the fly](#modifiers-on-the-fly)
|
||||
- [Cache](#cache)
|
||||
- [Example reader code](#example-reader-code)
|
||||
- [Tips](#tips)
|
||||
- [Working with results](#working-with-results)
|
||||
|
||||
## Reasoning
|
||||
@@ -30,12 +33,38 @@ Therefore, I present you the simple client to which you can copy & paste your gr
|
||||
### Environment variables
|
||||
|
||||
* `GRAPHQL_ENDPOINT` - Your GraphQL endpoint. Default: `http://127.0.0.1:9090/v1/graphql`
|
||||
* `GRAPHQL_CACHE` - Should the query cache be enabled? Default: `false`
|
||||
* `GRAPHQL_CACHE_ENABLED` - Should the query cache be enabled? Default: `false`
|
||||
* `GRAPHQL_CACHE_TTL` - Cache TTL in seconds for SELECT type of queries. Default: `5`
|
||||
* `LOG_LEVEL` - Logging level. Default: `info`
|
||||
* `RETRIES_ENABLE` - Should retries be enabled? Default: `false`
|
||||
* `RETRIES_NUMBER` - Number of retries: Default: `1`
|
||||
* `RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250`
|
||||
* `GRAPHQL_OUTPUT` - Output format. Default: `string`, available: `byte`, `string`, `mapstring`
|
||||
* `LOG_LEVEL` - Logging level. Default: `info` available: `debug`, `info`, `warn`, `error`
|
||||
* `GRAPHQL_RETRIES_ENABLE` - Should retries be enabled? Default: `true`
|
||||
* `GRAPHQL_RETRIES_NUMBER` - Number of retries: Default: `1`
|
||||
* `GRAPHQL_RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250`
|
||||
|
||||
### Modifiers on the fly
|
||||
|
||||
* `gql.SetEndpoint('your-endpoint-url')` - modifies endpoint, without the need to set the environment variable
|
||||
* `gql.SetOutput('byte')` - modifies output format, without the need to set the environment variable
|
||||
|
||||
### Cache
|
||||
|
||||
You have two options to enable the cache:
|
||||
|
||||
* Use `GRAPHQL_CACHE_ENABLED` environment variable which will enable the cache globally. It may be desired if you want to use the cache for all queries.
|
||||
* Add `gqlcache: true` header for your query which will enable the cache for this query only with `GRAPHQL_CACHE_TTL` TTL.
|
||||
* You can check the list of supported per-query modifiers below
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
// following values passed as headers will modify behaviour of the query
|
||||
// and disregard settings provided via environment variables
|
||||
headers := map[string]interface{}{
|
||||
...
|
||||
"gqlcache": true, // sets the cache as on for this query only
|
||||
"gqlretries": false, // disables retries for this query only
|
||||
}
|
||||
```
|
||||
|
||||
### Example reader code
|
||||
|
||||
@@ -46,32 +75,39 @@ import (
|
||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||
)
|
||||
|
||||
headers := map[string]interface{}{
|
||||
"x-hasura-user-id": 37,
|
||||
"x-hasura-user-uuid": "bde1962e-b42e-1212-ac10-d43fa27f44a5",
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"fileHash": "123deadc0w321",
|
||||
}
|
||||
|
||||
query := `query searchFileKnown($fileHash: String) {
|
||||
tbl_file_scans(where: {file_hash: {_eq: $fileHash}}) {
|
||||
racy
|
||||
violence
|
||||
virus
|
||||
func main() {
|
||||
headers := map[string]interface{}{
|
||||
"x-hasura-user-id": 37,
|
||||
"x-hasura-user-uuid": "bde1962e-b42e-1212-ac10-d43fa27f44a5",
|
||||
}
|
||||
}`
|
||||
|
||||
gql := graphql.NewConnection()
|
||||
result, err := gql.Query(query, variables, headers)
|
||||
if err != nil {
|
||||
fmt.Println("Query error", err)
|
||||
return
|
||||
variables := map[string]interface{}{
|
||||
"fileHash": "123deadc0w321",
|
||||
}
|
||||
|
||||
query := `query searchFileKnown($fileHash: String) {
|
||||
tbl_file_scans(where: {file_hash: {_eq: $fileHash}}) {
|
||||
racy
|
||||
violence
|
||||
virus
|
||||
}
|
||||
}`
|
||||
|
||||
gql := graphql.NewConnection()
|
||||
result, err := gql.Query(query, variables, headers)
|
||||
if err != nil {
|
||||
fmt.Println("Query error", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(result)
|
||||
}
|
||||
fmt.Println(result)
|
||||
```
|
||||
|
||||
### Tips
|
||||
|
||||
* Connection handler ( `gql := graphql.NewConnection()` ) should be created once and reused in the application especially if you run dozens of queries per second. It will allow you also to use cache and http2 to its full potential.
|
||||
|
||||
|
||||
**Result**
|
||||
|
||||
```json
|
||||
@@ -80,7 +116,9 @@ fmt.Println(result)
|
||||
|
||||
## Working with results
|
||||
|
||||
I'm using an amazing library [tidwall/gjson](https://github.com/tidwall/gjson) to parse the results and extract the information required in further steps and I strongly recommend this approach as the easiest and close to painless, for example:
|
||||
Currently attempting to switch to the fork of the [`ask` library](https://github.com/lukaszraczylo/ask)
|
||||
|
||||
Before, I used an amazing library [tidwall/gjson](https://github.com/tidwall/gjson) to parse the results and extract the information required in further steps and I strongly recommend this approach as the easiest and close to painless, for example:
|
||||
|
||||
```go
|
||||
result := gjson.Get(result, "tbl_user_group_admins.0.is_admin").Bool()
|
||||
|
||||
Vendored
+153
@@ -0,0 +1,153 @@
|
||||
package libpack_cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CacheEntry struct {
|
||||
ExpiresAt time.Time
|
||||
Value []byte
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
entries sync.Map
|
||||
globalTTL time.Duration
|
||||
compressPool sync.Pool
|
||||
decompressPool sync.Pool
|
||||
sync.RWMutex // Reintroduced to provide lock methods
|
||||
}
|
||||
|
||||
func New(globalTTL time.Duration) *Cache {
|
||||
cache := &Cache{
|
||||
globalTTL: globalTTL,
|
||||
compressPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
w := gzip.NewWriter(nil)
|
||||
return w
|
||||
},
|
||||
},
|
||||
decompressPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
// Ensure that new is returning a new reader initialized with an empty byte buffer
|
||||
r, _ := gzip.NewReader(bytes.NewReader([]byte{}))
|
||||
return r
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
go cache.cleanupRoutine(globalTTL)
|
||||
return cache
|
||||
}
|
||||
|
||||
func (c *Cache) cleanupRoutine(globalTTL time.Duration) {
|
||||
ticker := time.NewTicker(globalTTL / 2)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
c.CleanExpiredEntries()
|
||||
}
|
||||
}
|
||||
func (c *Cache) Set(key string, value []byte, ttl time.Duration) {
|
||||
c.Lock() // use the lock
|
||||
defer c.Unlock()
|
||||
|
||||
expiresAt := time.Now().Add(ttl)
|
||||
|
||||
compressedValue, err := c.compress(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
entry := CacheEntry{
|
||||
Value: compressedValue,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
c.entries.Store(key, entry)
|
||||
}
|
||||
|
||||
func (c *Cache) Get(key string) ([]byte, bool) {
|
||||
c.RLock() // use the read lock
|
||||
defer c.RUnlock()
|
||||
|
||||
entry, ok := c.entries.Load(key)
|
||||
if !ok || entry.(CacheEntry).ExpiresAt.Before(time.Now()) {
|
||||
return nil, false
|
||||
}
|
||||
compressedValue := entry.(CacheEntry).Value
|
||||
value, err := c.decompress(compressedValue)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return value, true
|
||||
}
|
||||
|
||||
func (c *Cache) Delete(key string) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
|
||||
_, ok := c.entries.Load(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
c.entries.Delete(key)
|
||||
}
|
||||
|
||||
func (c *Cache) CleanExpiredEntries() {
|
||||
now := time.Now()
|
||||
c.entries.Range(func(key, value interface{}) bool {
|
||||
entry := value.(CacheEntry)
|
||||
if entry.ExpiresAt.Before(now) {
|
||||
c.entries.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Cache) compress(data []byte) ([]byte, error) {
|
||||
w := c.compressPool.Get().(*gzip.Writer)
|
||||
defer c.compressPool.Put(w)
|
||||
|
||||
var buf bytes.Buffer
|
||||
w.Reset(&buf)
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (c *Cache) decompress(data []byte) ([]byte, error) {
|
||||
r, ok := c.decompressPool.Get().(*gzip.Reader)
|
||||
if !ok || r == nil {
|
||||
// If r is nil or type assertion fails, create a new gzip.Reader
|
||||
var err error
|
||||
r, err = gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err // Handle the error if gzip.NewReader fails
|
||||
}
|
||||
} else {
|
||||
// Reset the existing reader with new data
|
||||
if err := r.Reset(bytes.NewReader(data)); err != nil {
|
||||
return nil, err // Handle the error if Reset fails
|
||||
}
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Ensure the reader is returned to the pool
|
||||
defer c.decompressPool.Put(r)
|
||||
|
||||
// Read all the data from the reader
|
||||
decompressedData, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err // Handle the error if reading fails
|
||||
}
|
||||
return decompressedData, nil
|
||||
}
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
package libpack_cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Assume that New function initializes the cache and it is defined somewhere in the libpack_cache package.
|
||||
|
||||
func BenchmarkCacheSet(b *testing.B) {
|
||||
cache := New(30 * time.Second) // Initializing the cache with a TTL of 30 seconds
|
||||
key := "benchmark-key"
|
||||
value := []byte("benchmark-value")
|
||||
|
||||
b.ResetTimer() // Reset the timer to exclude the setup time from the benchmark
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.Set(key, value, 5*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCacheGet(b *testing.B) {
|
||||
cache := New(30 * time.Second) // Initializing the cache
|
||||
key := "benchmark-key"
|
||||
value := []byte("benchmark-value")
|
||||
cache.Set(key, value, 5*time.Second) // Pre-set a value to retrieve
|
||||
|
||||
b.ResetTimer() // Start timing
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = cache.Get(key)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCacheExpire(b *testing.B) {
|
||||
key := "benchmark-expire-key"
|
||||
value := []byte("benchmark-value")
|
||||
ttl := 5 * time.Millisecond // Setting a short TTL for quick expiration
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache := New(30 * time.Second)
|
||||
cache.Set(key, value, ttl)
|
||||
time.Sleep(ttl) // Wait for the key to expire
|
||||
_, _ = cache.Get(key)
|
||||
}
|
||||
}
|
||||
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
package libpack_cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type CacheTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) SetupTest() {
|
||||
}
|
||||
|
||||
func TestCachingTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(CacheTestSuite))
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) Test_New() {
|
||||
suite.T().Run("should return a new cache", func(t *testing.T) {
|
||||
cache := New(2 * time.Second)
|
||||
suite.NotNil(cache)
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) Test_CacheUse() {
|
||||
cache := New(30 * time.Second)
|
||||
tests := []struct {
|
||||
name string
|
||||
cache_value string
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
cache_value: "test1-123",
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
cache_value: "test2-123",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
cache.Set(tt.name, []byte(tt.name), 5*time.Second)
|
||||
c, ok := cache.Get(tt.name)
|
||||
suite.Equal(true, ok)
|
||||
suite.Equal(tt.name, string(c))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) Test_CacheDelete() {
|
||||
cache := New(30 * time.Second)
|
||||
tests := []struct {
|
||||
name string
|
||||
cache_value string
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
cache_value: "test1-123",
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
cache_value: "test2-123",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
cache.Set(tt.name, []byte(tt.name), 5*time.Second)
|
||||
c, ok := cache.Get(tt.name)
|
||||
suite.Equal(true, ok)
|
||||
suite.Equal(tt.name, string(c))
|
||||
cache.Delete(tt.name)
|
||||
c, ok = cache.Get(tt.name)
|
||||
suite.Equal(false, ok)
|
||||
suite.Equal("", string(c))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) Test_CacheExpire() {
|
||||
cache := New(30 * time.Second)
|
||||
tests := []struct {
|
||||
name string
|
||||
cache_value string
|
||||
ttl time.Duration
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
cache_value: "test1-123",
|
||||
ttl: 2 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
cache_value: "test2-123",
|
||||
ttl: 5 * time.Second,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
cache.Set(tt.name, []byte(tt.name), tt.ttl)
|
||||
c, ok := cache.Get(tt.name)
|
||||
suite.Equal(true, ok)
|
||||
suite.Equal(tt.name, string(c))
|
||||
time.Sleep(tt.ttl)
|
||||
c, ok = cache.Get(tt.name)
|
||||
suite.Equal(false, ok)
|
||||
suite.Equal("", string(c))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
func (qe *QueryExecutor) executeQuery() ([]byte, error) {
|
||||
httpRequest, err := http.NewRequest(http.MethodPost, qe.endpoint, bytes.NewBuffer(qe.Query))
|
||||
if err != nil {
|
||||
qe.Logger.Error("Can't create HTTP request", map[string]interface{}{"error": err})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range qe.Headers {
|
||||
httpRequest.Header.Set(key, fmt.Sprint(value))
|
||||
}
|
||||
|
||||
retriesMax := 1
|
||||
if qe.Retries {
|
||||
qe.Logger.Debug("Retries enabled", map[string]interface{}{"retries": qe.retries_number})
|
||||
retriesMax = qe.retries_number
|
||||
} else {
|
||||
qe.Logger.Debug("Retries disabled", map[string]interface{}{"retries": 1})
|
||||
}
|
||||
|
||||
var queryResult queryResults
|
||||
err = retry.Do(
|
||||
func() error {
|
||||
httpResponse, err := qe.client.Do(httpRequest)
|
||||
if err != nil {
|
||||
qe.Logger.Debug("Error executing HTTP request", map[string]interface{}{"error": err.Error()})
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
_, err := io.Copy(io.Discard, httpResponse.Body)
|
||||
if err != nil {
|
||||
qe.Logger.Debug("Error discarding HTTP response body", map[string]interface{}{"error": err.Error()})
|
||||
}
|
||||
httpResponse.Body.Close()
|
||||
}()
|
||||
|
||||
if httpResponse.StatusCode < http.StatusOK || httpResponse.StatusCode >= http.StatusNoContent {
|
||||
return fmt.Errorf("HTTP error - status code: %s for %s", httpResponse.Status, httpRequest.URL)
|
||||
}
|
||||
|
||||
var reader io.ReadCloser
|
||||
encoding := httpResponse.Header.Get("Content-Encoding")
|
||||
switch encoding {
|
||||
case "gzip":
|
||||
reader, err = gzip.NewReader(httpResponse.Body)
|
||||
if err != nil {
|
||||
qe.Logger.Debug("Error creating gzip reader", map[string]interface{}{"error": err.Error()})
|
||||
return fmt.Errorf("error creating gzip reader: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
default:
|
||||
reader = httpResponse.Body
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
qe.Logger.Debug("Error reading HTTP response", map[string]interface{}{"error": err.Error()})
|
||||
return fmt.Errorf("error reading HTTP response: %w", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &queryResult)
|
||||
if err != nil {
|
||||
qe.Logger.Debug("Error unmarshalling HTTP response", map[string]interface{}{"error": err.Error()})
|
||||
return fmt.Errorf("error unmarshalling HTTP response: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
retry.OnRetry(func(n uint, err error) {
|
||||
qe.Logger.Warning("Retrying query", map[string]interface{}{"error": err.Error(), "attempt": n})
|
||||
}),
|
||||
retry.Attempts(uint(retriesMax)),
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.Delay(time.Duration(qe.retries_delay)),
|
||||
retry.LastErrorOnly(true),
|
||||
)
|
||||
if err != nil {
|
||||
qe.Logger.Debug("Error executing HTTP request", map[string]interface{}{"error": err.Error()})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(queryResult.Errors) > 0 {
|
||||
qe.Logger.Debug("Error executing query", map[string]interface{}{"error": queryResult.Errors})
|
||||
return nil, fmt.Errorf("error executing query: %s", queryResult.Errors)
|
||||
}
|
||||
|
||||
if queryResult.Data == nil {
|
||||
qe.Logger.Debug("Error executing query", map[string]interface{}{"error": "no data"})
|
||||
return nil, errors.New("error executing query: no data")
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(queryResult.Data)
|
||||
if err != nil {
|
||||
qe.Logger.Debug("Error marshalling query result", map[string]interface{}{"error": err.Error(), "data": queryResult.Data})
|
||||
return nil, fmt.Errorf("error marshalling query result: %w. Data: %s", err, queryResult.Data)
|
||||
}
|
||||
|
||||
if qe.CacheKey != "no-cache" {
|
||||
qe.cache.Set(qe.CacheKey, jsonData, qe.CacheTTL)
|
||||
}
|
||||
|
||||
return jsonData, nil
|
||||
}
|
||||
@@ -1,31 +1,26 @@
|
||||
module github.com/lukaszraczylo/go-simple-graphql
|
||||
|
||||
go 1.18
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/allegro/bigcache/v3 v3.0.2
|
||||
github.com/avast/retry-go v3.0.0+incompatible
|
||||
github.com/buger/jsonparser v1.1.1
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/lukaszraczylo/pandati v0.0.15
|
||||
github.com/rs/zerolog v1.27.0
|
||||
github.com/stretchr/testify v1.7.1
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
|
||||
github.com/avast/retry-go/v4 v4.6.0
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/gookit/goutil v0.6.15
|
||||
github.com/rs/zerolog v1.32.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/net v0.25.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/tidwall/gjson v1.14.1 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/wI2L/jsondiff v0.2.0 // indirect
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.7.0 // indirect
|
||||
golang.org/x/sys v0.20.0 // indirect
|
||||
golang.org/x/term v0.20.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,92 +1,47 @@
|
||||
github.com/allegro/bigcache/v3 v3.0.2 h1:AKZCw+5eAaVyNTBmI2fgyPVJhHkdWder3O9IrprcQfI=
|
||||
github.com/allegro/bigcache/v3 v3.0.2/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
|
||||
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
|
||||
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
|
||||
github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
|
||||
github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
|
||||
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/avast/retry-go/v4 v4.6.0 h1:K9xNA+KeB8HHc2aWFuLb25Offp+0iVRXEvFx8IinRJA=
|
||||
github.com/avast/retry-go/v4 v4.6.0/go.mod h1:gvWlPhBVsvBbLkVGDg/KwvBv0bEkCOLRRSHKIr2PyOE=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk=
|
||||
github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/lukaszraczylo/pandati v0.0.15 h1:1BNtbmhIQbKgs1Gm9WlH3CMms+TgkgxZcU7utHbjloY=
|
||||
github.com/lukaszraczylo/pandati v0.0.15/go.mod h1:5E0mqlNgdOps6FW2X4tPilr+3B8s9JGWFgA6ZVFixyQ=
|
||||
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
|
||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||
github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo=
|
||||
github.com/gookit/goutil v0.6.15/go.mod h1:qdKdYEHQdEtyH+4fNdQNZfJHhI0jUZzHxQVAV3DaMDY=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
|
||||
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
|
||||
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
|
||||
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tidwall/gjson v1.14.0 h1:6aeJ0bzojgWLa82gDQHcx3S0Lr/O51I9bJ5nv6JFx5w=
|
||||
github.com/tidwall/gjson v1.14.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.14.1 h1:iymTbGkQBhveq21bEvAQ81I0LEBork8BFe1CUZXdyuo=
|
||||
github.com/tidwall/gjson v1.14.1/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/wI2L/jsondiff v0.2.0 h1:dE00WemBa1uCjrzQUUTE/17I6m5qAaN0EMFOg2Ynr/k=
|
||||
github.com/wI2L/jsondiff v0.2.0/go.mod h1:axTcwtBkY4TsKuV+RgoMhHyHKKFRI6nnjRLi8LLYQnA=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37 h1:lUkvobShwKsOesNfWWlCS5q7fnbG1MEliIzwu886fn8=
|
||||
golang.org/x/net v0.0.0-20220526153639-5463443f8c37/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a h1:dGzPydgVsqGcTRVwiLJ1jVbufYwmzD3LfVPLKsKg+0k=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0=
|
||||
github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E=
|
||||
golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE=
|
||||
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA=
|
||||
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
// Package go-simple-graphql is a wrapper for GraphQL queries execution.
|
||||
// It's sole purpose is to allow developer to execute queries without specific fields / types mapping
|
||||
// which can be really confusing and even impossible whilst dealing with Hasura custom generated types.
|
||||
//
|
||||
// Library supports advanced error reporting on unsuccessful queries.
|
||||
// Library also uses HTTP2 for communication with GraphQL API if it's supported by server
|
||||
|
||||
// Environment variables:
|
||||
// GRAPHQL_ENDPOINT - GraphQL endpoint to use. Default: http://127.0.0.1:9090/v1/graphql
|
||||
// LOG_LEVEL - Log level to use. Default: INFO
|
||||
// Available log levels: DEBUG, INFO, WARN, ERROR, FATAL
|
||||
|
||||
package gql
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/allegro/bigcache/v3"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/lukaszraczylo/go-simple-graphql/pkg/logging"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var (
|
||||
json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
)
|
||||
|
||||
type GraphQL struct {
|
||||
Endpoint string
|
||||
HttpClient *http.Client
|
||||
Log *logging.LogConfig
|
||||
Cache bool // Enable caching for read queries
|
||||
CacheStore *bigcache.BigCache
|
||||
RetriesEnable bool
|
||||
RetriesNumber int
|
||||
RetriesDelay int
|
||||
}
|
||||
|
||||
func pickGraphqlEndpoint() (graphqlEndpoint string) {
|
||||
value, present := os.LookupEnv("GRAPHQL_ENDPOINT")
|
||||
if present && value != "" {
|
||||
graphqlEndpoint = value
|
||||
} else {
|
||||
graphqlEndpoint = "http://127.0.0.1:9090/v1/graphql"
|
||||
fmt.Println("Setting default endpoint", graphqlEndpoint)
|
||||
}
|
||||
return graphqlEndpoint
|
||||
}
|
||||
|
||||
func setCacheTTL() int {
|
||||
value, present := os.LookupEnv("GRAPHQL_CACHE_TTL")
|
||||
if present && value != "" {
|
||||
i, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
panic("Invalid value for query cache ttl")
|
||||
}
|
||||
return int(i)
|
||||
} else {
|
||||
return 5
|
||||
}
|
||||
}
|
||||
|
||||
func setCacheEnabled() bool {
|
||||
value, present := os.LookupEnv("GRAPHQL_CACHE")
|
||||
if present && value != "" {
|
||||
b, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
panic("Invalid value for query cache")
|
||||
}
|
||||
return b
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func NewConnection() *GraphQL {
|
||||
g := GraphQL{
|
||||
Endpoint: pickGraphqlEndpoint(),
|
||||
HttpClient: &http.Client{
|
||||
Transport: &http2.Transport{
|
||||
ReadIdleTimeout: 30 * time.Second,
|
||||
DisableCompression: false,
|
||||
AllowHTTP: true,
|
||||
PingTimeout: 10 * time.Second,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
Log: logging.NewLogger(),
|
||||
Cache: setCacheEnabled(),
|
||||
CacheStore: setupCache(),
|
||||
RetriesEnable: false,
|
||||
RetriesNumber: 1,
|
||||
RetriesDelay: 250,
|
||||
}
|
||||
var err error
|
||||
retriesEnable, retriesEnableExists := os.LookupEnv("RETRIES_ENABLE")
|
||||
if retriesEnableExists {
|
||||
g.RetriesEnable, err = strconv.ParseBool(retriesEnable)
|
||||
if err != nil {
|
||||
panic("Invalid value for RETRIES_ENABLE")
|
||||
}
|
||||
retriesNumber, retriesNumberExists := os.LookupEnv("RETRIES_NUMBER")
|
||||
if !retriesNumberExists {
|
||||
panic("RETRIES_NUMBER environment variable is not set but RETRIES_ENABLE is")
|
||||
}
|
||||
g.RetriesNumber, err = strconv.Atoi(retriesNumber)
|
||||
if err != nil {
|
||||
panic("Invalid value for RETRIES_NUMBER")
|
||||
}
|
||||
retriesDelay, retriesDelayExists := os.LookupEnv("RETRIES_DELAY")
|
||||
if !retriesDelayExists {
|
||||
panic("RETRIES_DELAY environment variable is not set but RETRIES_ENABLE is")
|
||||
}
|
||||
g.RetriesDelay, err = strconv.Atoi(retriesDelay)
|
||||
}
|
||||
return &g
|
||||
}
|
||||
|
||||
func setupCache() *bigcache.BigCache {
|
||||
cache, err := bigcache.NewBigCache(bigcache.DefaultConfig(time.Duration(setCacheTTL()) * time.Second))
|
||||
if err != nil {
|
||||
panic("Error creating cache: " + err.Error())
|
||||
}
|
||||
return cache
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func mockGraphQLServerResponses(responseQuery string) (*httptest.Server, string) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Write([]byte(responseQuery))
|
||||
}))
|
||||
serverURL := server.URL + "/v1/graphql"
|
||||
return server, serverURL
|
||||
}
|
||||
|
||||
type TestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TestSuite) SetupTest() {
|
||||
os.Unsetenv("GRAPHQL_ENDPOINT")
|
||||
}
|
||||
|
||||
func TestSuiteRun(t *testing.T) {
|
||||
suite.Run(t, new(TestSuite))
|
||||
}
|
||||
|
||||
func (suite *TestSuite) TestNewConnection() {
|
||||
type args struct {
|
||||
endpoint string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *GraphQL
|
||||
}{
|
||||
{
|
||||
name: "New connection: Env variable endpoint",
|
||||
args: args{
|
||||
endpoint: "http://localhost:8080/graphql",
|
||||
},
|
||||
want: &GraphQL{
|
||||
Endpoint: "http://localhost:8080/graphql",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "New connection: No env variable endpoint",
|
||||
args: args{},
|
||||
want: &GraphQL{
|
||||
Endpoint: "http://127.0.0.1:9090/v1/graphql",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
os.Setenv("GRAPHQL_ENDPOINT", tt.args.endpoint)
|
||||
got := NewConnection()
|
||||
assert.Equal(t, tt.want.Endpoint, got.Endpoint)
|
||||
})
|
||||
}
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
func searchForKeysInMapStringInterface(msi map[string]interface{}, key string) (value any) {
|
||||
if msi == nil {
|
||||
return nil
|
||||
}
|
||||
return msi[key]
|
||||
}
|
||||
|
||||
func calculateHash(query *Query) string {
|
||||
return strutil.Md5(query.JsonQuery)
|
||||
}
|
||||
|
||||
func (b *BaseClient) cacheLookup(hash string) []byte {
|
||||
obj, _ := b.cache.Get(hash)
|
||||
return obj
|
||||
}
|
||||
|
||||
func (b *BaseClient) decodeResponse(response []byte) (any, error) {
|
||||
switch b.responseType {
|
||||
case "mapstring":
|
||||
var result map[string]interface{}
|
||||
err := json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
b.Logger.Error("Can't decode response into mapstring", map[string]interface{}{"error": err.Error()})
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
case "string":
|
||||
return string(response), nil
|
||||
case "byte":
|
||||
return response, nil
|
||||
default:
|
||||
b.Logger.Error("Can't decode response;", map[string]interface{}{"error": "unknown response type"})
|
||||
return nil, fmt.Errorf("Can't decode response - unknown response type specified")
|
||||
}
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func (suite *Tests) Test_searchForKeysInMapStringInterface() {
|
||||
type args struct {
|
||||
msi map[string]interface{}
|
||||
key string
|
||||
}
|
||||
tests := []struct {
|
||||
wantValue any
|
||||
args args
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "Test_searchForKeysInMapStringInterface_exists",
|
||||
args: args{
|
||||
msi: map[string]interface{}{
|
||||
"gqlcache": true,
|
||||
"test": "test2",
|
||||
},
|
||||
key: "test",
|
||||
},
|
||||
wantValue: "test2",
|
||||
},
|
||||
{
|
||||
name: "Test_searchForKeysInMapStringInterface_not_exists",
|
||||
args: args{
|
||||
msi: map[string]interface{}{
|
||||
"gqlcache": true,
|
||||
"test": "test2",
|
||||
},
|
||||
key: "test-new",
|
||||
},
|
||||
wantValue: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(tt.wantValue, searchForKeysInMapStringInterface(tt.args.msi, tt.args.key))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *Tests) TestBaseClient_decodeResponse() {
|
||||
type args struct {
|
||||
response []byte
|
||||
}
|
||||
tests := []struct {
|
||||
want any
|
||||
name string
|
||||
setType string
|
||||
wantType string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "TestBaseClient_decodeResponse_mapstring",
|
||||
args: args{
|
||||
response: []byte(`{"data":{"viewer":{"login":"lukaszraczylo"}}}`),
|
||||
},
|
||||
want: map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"login": "lukaszraczylo",
|
||||
},
|
||||
},
|
||||
},
|
||||
setType: "mapstring",
|
||||
wantType: "map[string]interface {}",
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_decodeResponse_string",
|
||||
args: args{
|
||||
response: []byte(`{"data":{"viewer":{"login":"lukaszraczylo"}}}`),
|
||||
},
|
||||
want: `{"data":{"viewer":{"login":"lukaszraczylo"}}}`,
|
||||
setType: "string",
|
||||
wantType: "string",
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_decodeResponse_byte",
|
||||
args: args{
|
||||
response: []byte(`{"data":{"viewer":{"login":"lukaszraczylo"}}}`),
|
||||
},
|
||||
want: []byte(`{"data":{"viewer":{"login":"lukaszraczylo"}}}`),
|
||||
setType: "byte",
|
||||
wantType: "[]uint8",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
b := NewConnection()
|
||||
b.SetOutput(tt.setType)
|
||||
got, err := b.decodeResponse(tt.args.response)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("decodeResponse() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
assert.Equal(tt.wantType, reflect.TypeOf(got).String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
func (b *BaseClient) createHttpClient() (http_client *http.Client) {
|
||||
httpTransport := &http.Transport{
|
||||
MaxIdleConns: 512,
|
||||
MaxConnsPerHost: 512,
|
||||
MaxIdleConnsPerHost: 512,
|
||||
IdleConnTimeout: 15 * time.Second,
|
||||
ResponseHeaderTimeout: 15 * time.Second,
|
||||
DisableKeepAlives: false,
|
||||
DisableCompression: false,
|
||||
}
|
||||
|
||||
if strings.HasPrefix(b.endpoint, "http://") {
|
||||
http_client = &http.Client{
|
||||
Timeout: 15 * time.Second,
|
||||
Transport: httpTransport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
b.Logger.Debug("Using HTTP/1.1 over http", nil)
|
||||
} else if strings.HasPrefix(b.endpoint, "https://") {
|
||||
tlsClientConfig := &tls.Config{}
|
||||
if strings.HasPrefix(b.endpoint, "https://") {
|
||||
tlsClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
http2Transport := &http2.Transport{
|
||||
AllowHTTP: true,
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
ReadIdleTimeout: 15 * time.Second,
|
||||
PingTimeout: 15 * time.Second,
|
||||
}
|
||||
http_client = &http.Client{
|
||||
Transport: http2Transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
b.Logger.Debug("Using HTTP/2 over https", nil)
|
||||
} else {
|
||||
b.Logger.Critical("Invalid endpoint - neither http or https", nil)
|
||||
}
|
||||
return http_client
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
package libpack_logging
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gookit/goutil/envutil"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type LogConfig struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
baseLogger zerolog.Logger
|
||||
|
||||
eventPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(zerolog.Event)
|
||||
},
|
||||
}
|
||||
|
||||
fieldMapPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make(map[string]interface{})
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
zerolog.TimeFieldFormat = time.RFC3339
|
||||
zerolog.MessageFieldName = "short_message"
|
||||
zerolog.TimestampFieldName = "timestamp"
|
||||
zerolog.LevelFieldName = "level"
|
||||
zerolog.LevelFatalValue = "critical"
|
||||
|
||||
baseLogger = zerolog.New(os.Stdout).With().Timestamp().Logger()
|
||||
|
||||
switch logLevel := envutil.Getenv("LOG_LEVEL", "info"); logLevel {
|
||||
case "debug":
|
||||
baseLogger = baseLogger.Level(zerolog.DebugLevel)
|
||||
case "warn":
|
||||
baseLogger = baseLogger.Level(zerolog.WarnLevel)
|
||||
case "error":
|
||||
baseLogger = baseLogger.Level(zerolog.ErrorLevel)
|
||||
default:
|
||||
baseLogger = baseLogger.Level(zerolog.InfoLevel)
|
||||
}
|
||||
}
|
||||
|
||||
func NewLogger() *LogConfig {
|
||||
return &LogConfig{logger: baseLogger}
|
||||
}
|
||||
|
||||
func (lw *LogConfig) log(w io.Writer, level zerolog.Level, message string, fields map[string]interface{}) {
|
||||
logger := lw.logger.Output(w)
|
||||
event := logger.WithLevel(level).CallerSkipFrame(3)
|
||||
|
||||
for k, val := range fields {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
event = event.Str(k, v)
|
||||
case int:
|
||||
event = event.Int(k, v)
|
||||
case float64:
|
||||
event = event.Float64(k, v)
|
||||
default:
|
||||
event = event.Interface(k, val)
|
||||
}
|
||||
}
|
||||
|
||||
event.Msg(message)
|
||||
}
|
||||
|
||||
func (lw *LogConfig) logWithLevel(level zerolog.Level, message string, fields map[string]interface{}) {
|
||||
if lw.logger.GetLevel() <= level {
|
||||
w := os.Stdout
|
||||
if level >= zerolog.ErrorLevel {
|
||||
w = os.Stderr
|
||||
}
|
||||
lw.log(w, level, message, fields)
|
||||
}
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Debug(message string, fields map[string]interface{}) {
|
||||
lw.logWithLevel(zerolog.DebugLevel, message, fields)
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Info(message string, fields map[string]interface{}) {
|
||||
lw.logWithLevel(zerolog.InfoLevel, message, fields)
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Warning(message string, fields map[string]interface{}) {
|
||||
lw.logWithLevel(zerolog.WarnLevel, message, fields)
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Error(message string, fields map[string]interface{}) {
|
||||
lw.logWithLevel(zerolog.ErrorLevel, message, fields)
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Critical(message string, fields map[string]interface{}) {
|
||||
lw.logWithLevel(zerolog.FatalLevel, message, fields)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Helper function to get a new fields map from the pool
|
||||
func getFieldsMap() map[string]interface{} {
|
||||
return fieldMapPool.Get().(map[string]interface{})
|
||||
}
|
||||
|
||||
// Helper function to put a used fields map back into the pool
|
||||
func putFieldsMap(fields map[string]interface{}) {
|
||||
for k := range fields {
|
||||
delete(fields, k)
|
||||
}
|
||||
fieldMapPool.Put(fields)
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package libpack_logging
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkNewLogger(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
NewLogger()
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkInfoLog(b *testing.B) {
|
||||
oldEnv := os.Getenv("LOG_LEVEL")
|
||||
os.Setenv("LOG_LEVEL", "info")
|
||||
oldStdout := os.Stdout
|
||||
oldStderr := os.Stderr
|
||||
os.Stdout, _ = os.Open(os.DevNull)
|
||||
os.Stderr, _ = os.Open(os.DevNull)
|
||||
defer func() {
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
os.Setenv("LOG_LEVEL", oldEnv)
|
||||
}()
|
||||
|
||||
testsLogger := NewLogger()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
testsLogger.Info("test", map[string]interface{}{"test": "test"})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gookit/goutil/envutil"
|
||||
cache "github.com/lukaszraczylo/go-simple-graphql/cache"
|
||||
logging "github.com/lukaszraczylo/go-simple-graphql/logging"
|
||||
)
|
||||
|
||||
func NewConnection() (b *BaseClient) {
|
||||
b = &BaseClient{
|
||||
endpoint: envutil.Getenv("GRAPHQL_ENDPOINT", "https://api.github.com/graphql"),
|
||||
responseType: envutil.Getenv("GRAPHQL_OUTPUT", "string"),
|
||||
Logger: logging.NewLogger(),
|
||||
cache: cache.New(time.Duration(envutil.GetInt("GRAPHQL_CACHE_TTL", 5)) * time.Second),
|
||||
cache_global: envutil.GetBool("GRAPHQL_CACHE_ENABLED", false),
|
||||
retries_enable: envutil.GetBool("GRAPHQL_RETRIES_ENABLE", false),
|
||||
retries_delay: time.Duration(envutil.GetInt("GRAPHQL_RETRIES_DELAY", 250) * int(time.Millisecond)),
|
||||
retries_number: envutil.GetInt("GRAPHQL_RETRIES_NUMBER", 3),
|
||||
}
|
||||
b.client = b.createHttpClient()
|
||||
b.Logger.Debug("Created new GraphQL client connection", map[string]interface{}{"values": b})
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *BaseClient) SetEndpoint(endpoint string) {
|
||||
b.endpoint = endpoint
|
||||
}
|
||||
|
||||
func (b *BaseClient) SetOutput(responseType string) {
|
||||
// allowed are byte, string, mapstring
|
||||
// check if responseType is allowed
|
||||
// TODO: implement
|
||||
b.responseType = responseType
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package gql
|
||||
|
||||
import "testing"
|
||||
|
||||
func Benchmark_newConnection(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
NewConnection()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func (suite *Tests) TestNewConnection() {
|
||||
tests := []struct {
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "TestNewConnection",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
got := NewConnection()
|
||||
assert.NotNil(suite.T(), got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/lukaszraczylo/pandati"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type LogConfig struct {
|
||||
DefaultOutput *os.File
|
||||
Logger zerolog.Logger
|
||||
}
|
||||
|
||||
func NewLogger() *LogConfig {
|
||||
log := new(LogConfig)
|
||||
switch dll := os.Getenv("LOG_LEVEL"); dll {
|
||||
case "debug":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "warn":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "error":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
default:
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
}
|
||||
zerolog.TimeFieldFormat = time.RFC3339
|
||||
zerolog.MessageFieldName = "short_message"
|
||||
zerolog.TimestampFieldName = "timestamp"
|
||||
zerolog.LevelFatalValue = "critical"
|
||||
log.Logger = log.Logger.With().Timestamp().Logger()
|
||||
stdlog.SetFlags(0)
|
||||
stdlog.SetOutput(log.Logger)
|
||||
return log
|
||||
}
|
||||
|
||||
func (l *LogConfig) Critical(message string, v ...map[string]interface{}) {
|
||||
w := l.Logger.Output(os.Stderr)
|
||||
log(w.Fatal(), message, v...)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Error(message string, v ...map[string]interface{}) {
|
||||
w := l.Logger.Output(os.Stderr)
|
||||
log(w.Error(), message, v...)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Warning(message string, v ...map[string]interface{}) {
|
||||
w := l.Logger.Output(os.Stdout)
|
||||
log(w.Warn(), message, v...)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Info(message string, v ...map[string]interface{}) {
|
||||
w := l.Logger.Output(os.Stdout)
|
||||
log(w.Info(), message, v...)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Debug(message string, v ...map[string]interface{}) {
|
||||
w := l.Logger.Output(os.Stdout)
|
||||
log(w.Debug(), message, v...)
|
||||
}
|
||||
|
||||
func log(contextLog *zerolog.Event, message string, v ...map[string]interface{}) {
|
||||
if len(v) > 0 {
|
||||
for _, m := range v {
|
||||
for k, v := range m {
|
||||
if !pandati.IsZero(v) && !pandati.IsZero(k) {
|
||||
contextLog.Str(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// contextLog.Str("_stack_trace", pandati.Trace())
|
||||
contextLog.Msg(message)
|
||||
}
|
||||
@@ -1,373 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type LoggingTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
var (
|
||||
testsLogger *LogConfig
|
||||
)
|
||||
|
||||
type stdoutCapture struct {
|
||||
oldStdout *os.File
|
||||
readPipe *os.File
|
||||
}
|
||||
|
||||
func (sc *stdoutCapture) StartCapture() {
|
||||
sc.oldStdout = os.Stdout
|
||||
sc.readPipe, os.Stdout, _ = os.Pipe()
|
||||
}
|
||||
|
||||
func (sc *stdoutCapture) StopCapture() (string, error) {
|
||||
if sc.oldStdout == nil || sc.readPipe == nil {
|
||||
return "", errors.New("StartCapture not called before StopCapture on Stdout")
|
||||
}
|
||||
os.Stdout.Close()
|
||||
os.Stdout = sc.oldStdout
|
||||
bytes, err := ioutil.ReadAll(sc.readPipe)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
type stderrCapture struct {
|
||||
oldStderr *os.File
|
||||
readPipe *os.File
|
||||
}
|
||||
|
||||
func (sc *stderrCapture) StartCapture() {
|
||||
sc.oldStderr = os.Stderr
|
||||
sc.readPipe, os.Stderr, _ = os.Pipe()
|
||||
}
|
||||
|
||||
func (sc *stderrCapture) StopCapture() (string, error) {
|
||||
if sc.oldStderr == nil || sc.readPipe == nil {
|
||||
return "", errors.New("StartCapture not called before StopCapture on Stderr")
|
||||
}
|
||||
os.Stderr.Close()
|
||||
os.Stderr = sc.oldStderr
|
||||
bytes, err := ioutil.ReadAll(sc.readPipe)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func (suite *LoggingTestSuite) SetupTest() {
|
||||
}
|
||||
|
||||
func TestLoggingTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(LoggingTestSuite))
|
||||
}
|
||||
|
||||
func (suite *LoggingTestSuite) TestLogConfig_AllHandlers() {
|
||||
type args struct {
|
||||
message string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantLevel string
|
||||
wantMessage string
|
||||
envMinLogLevel string
|
||||
stdOutExpect bool
|
||||
stdErrExpect bool
|
||||
loggerType string
|
||||
}{
|
||||
{
|
||||
name: "Test log: Error",
|
||||
loggerType: "Error",
|
||||
args: args{
|
||||
message: "This is a error message",
|
||||
},
|
||||
wantLevel: "error",
|
||||
wantMessage: "This is a error message",
|
||||
stdErrExpect: true,
|
||||
stdOutExpect: false,
|
||||
},
|
||||
{
|
||||
name: "Test log: Warning",
|
||||
loggerType: "Warning",
|
||||
args: args{
|
||||
message: "This is a warning message",
|
||||
},
|
||||
wantLevel: "warn",
|
||||
wantMessage: "This is a warning message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
envMinLogLevel: "info",
|
||||
},
|
||||
{
|
||||
name: "Test log: Warning | Min level: Debug",
|
||||
loggerType: "Warning",
|
||||
args: args{
|
||||
message: "This is a warning message",
|
||||
},
|
||||
wantLevel: "warn",
|
||||
wantMessage: "This is a warning message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
envMinLogLevel: "debug",
|
||||
},
|
||||
{
|
||||
name: "Test log: Info",
|
||||
loggerType: "Info",
|
||||
args: args{
|
||||
message: "This is a info message",
|
||||
},
|
||||
wantLevel: "info",
|
||||
wantMessage: "This is a info message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
},
|
||||
{
|
||||
name: "Test log: Info | Min level: Warn",
|
||||
loggerType: "Info",
|
||||
args: args{
|
||||
message: "This is a info message",
|
||||
},
|
||||
wantLevel: "",
|
||||
wantMessage: "",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: false,
|
||||
envMinLogLevel: "warn",
|
||||
},
|
||||
{
|
||||
name: "Test log: Warning | Min level: Warn",
|
||||
loggerType: "Warning",
|
||||
args: args{
|
||||
message: "This is a warning message",
|
||||
},
|
||||
wantLevel: "warn",
|
||||
wantMessage: "This is a warning message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
envMinLogLevel: "warn",
|
||||
},
|
||||
{
|
||||
name: "Test log: Warning | Min level: Error",
|
||||
loggerType: "Warning",
|
||||
args: args{
|
||||
message: "This is an error message",
|
||||
},
|
||||
wantLevel: "",
|
||||
wantMessage: "",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: false,
|
||||
envMinLogLevel: "error",
|
||||
},
|
||||
{
|
||||
name: "Test log: Debug | Min level: Debug",
|
||||
loggerType: "Debug",
|
||||
args: args{
|
||||
message: "This is a debug message",
|
||||
},
|
||||
wantLevel: "debug",
|
||||
wantMessage: "This is a debug message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
envMinLogLevel: "debug",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
if tt.envMinLogLevel != "" {
|
||||
os.Setenv("LOG_LEVEL", tt.envMinLogLevel)
|
||||
defer os.Unsetenv("LOG_LEVEL")
|
||||
}
|
||||
testsLogger = NewLogger()
|
||||
|
||||
captureStdout := stdoutCapture{}
|
||||
captureStdout.StartCapture()
|
||||
captureStderr := stderrCapture{}
|
||||
captureStderr.StartCapture()
|
||||
|
||||
reflect.ValueOf(testsLogger).MethodByName(tt.loggerType).Call([]reflect.Value{reflect.ValueOf(tt.args.message)})
|
||||
|
||||
stdoutOut, err := captureStdout.StopCapture()
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
}
|
||||
|
||||
stderrOut, err := captureStderr.StopCapture()
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
}
|
||||
|
||||
if tt.stdErrExpect && !tt.stdOutExpect {
|
||||
gotLvl, gotMsg, err := getResponseValues(stderrOut, "short_message")
|
||||
suite.NoError(err, "Failed in [STDERR]: "+tt.name)
|
||||
suite.Equal(tt.wantLevel, gotLvl, "Failed in [STDERR]: "+tt.name)
|
||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDERR]: "+tt.name)
|
||||
suite.Equal("", stdoutOut, "Failed in [STDERR]: "+tt.name)
|
||||
}
|
||||
if tt.stdOutExpect && !tt.stdErrExpect {
|
||||
gotLvl, gotMsg, err := getResponseValues(stdoutOut, "short_message")
|
||||
suite.NoError(err, "Failed in [STDOUT]: "+tt.name)
|
||||
suite.Equal(tt.wantLevel, gotLvl, "Failed in [STDOUT]: "+tt.name)
|
||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDOUT]: "+tt.name)
|
||||
suite.Equal("", stderrOut, "Failed in [STDOUT]: "+tt.name)
|
||||
}
|
||||
if !tt.stdErrExpect && !tt.stdOutExpect {
|
||||
suite.Equal("", stderrOut, "Failed in [NEITHER]: "+tt.name)
|
||||
suite.Equal("", stdoutOut, "Failed in [NEITHER]: "+tt.name)
|
||||
}
|
||||
os.Unsetenv("LOG_LEVEL")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *LoggingTestSuite) TestFullMessage() {
|
||||
type args struct {
|
||||
message string
|
||||
extraFields map[string]interface{}
|
||||
}
|
||||
extraFields := make(map[string]interface{})
|
||||
extraFields["_full_message"] = "full message"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantLevel string
|
||||
wantMessage string
|
||||
envMinLogLevel string
|
||||
stdOutExpect bool
|
||||
stdErrExpect bool
|
||||
loggerType string
|
||||
}{
|
||||
{
|
||||
name: "Test log: Error",
|
||||
loggerType: "Error",
|
||||
args: args{
|
||||
message: "This is a error message",
|
||||
extraFields: extraFields,
|
||||
},
|
||||
wantLevel: "error",
|
||||
wantMessage: extraFields["_full_message"].(string),
|
||||
stdErrExpect: true,
|
||||
stdOutExpect: false,
|
||||
},
|
||||
{
|
||||
name: "Test log: Info",
|
||||
loggerType: "Info",
|
||||
args: args{
|
||||
message: "This is a info message",
|
||||
extraFields: extraFields,
|
||||
},
|
||||
wantMessage: extraFields["_full_message"].(string),
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
if tt.envMinLogLevel != "" {
|
||||
os.Setenv("LOG_LEVEL", tt.envMinLogLevel)
|
||||
defer os.Unsetenv("LOG_LEVEL")
|
||||
}
|
||||
testsLogger = NewLogger()
|
||||
|
||||
captureStdout := stdoutCapture{}
|
||||
captureStdout.StartCapture()
|
||||
captureStderr := stderrCapture{}
|
||||
captureStderr.StartCapture()
|
||||
|
||||
reflect.ValueOf(testsLogger).MethodByName(tt.loggerType).Call([]reflect.Value{
|
||||
reflect.ValueOf(tt.args.message),
|
||||
reflect.ValueOf(tt.args.extraFields),
|
||||
})
|
||||
|
||||
stdoutOut, err := captureStdout.StopCapture()
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
}
|
||||
|
||||
stderrOut, err := captureStderr.StopCapture()
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
}
|
||||
|
||||
if tt.stdErrExpect && !tt.stdOutExpect {
|
||||
_, gotMsg, err := getResponseValues(stderrOut, "_full_message")
|
||||
suite.NoError(err, "Failed in [STDERR]: "+tt.name)
|
||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDERR]: "+tt.name)
|
||||
}
|
||||
if tt.stdOutExpect && !tt.stdErrExpect {
|
||||
_, gotMsg, err := getResponseValues(stdoutOut, "_full_message")
|
||||
suite.NoError(err, "Failed in [STDOUT]: "+tt.name)
|
||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDOUT]: "+tt.name)
|
||||
}
|
||||
os.Unsetenv("LOG_LEVEL")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getResponseValues(t *testing.T) {
|
||||
type args struct {
|
||||
sourceJson string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantGotLvl string
|
||||
wantGotMsg string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test with json",
|
||||
args: args{
|
||||
sourceJson: `{"level": "debug", "short_message": "hello world"`,
|
||||
},
|
||||
wantGotLvl: "debug",
|
||||
wantGotMsg: "hello world",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Test with json, wrong message field",
|
||||
args: args{
|
||||
sourceJson: `{"level": "debug", "message": "hello world"`,
|
||||
},
|
||||
wantGotLvl: "debug",
|
||||
wantGotMsg: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotGotLvl, gotGotMsg, err := getResponseValues(tt.args.sourceJson, "short_message")
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getResponseValues() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotGotLvl != tt.wantGotLvl {
|
||||
t.Errorf("getResponseValues() gotGotLvl = %v, want %v", gotGotLvl, tt.wantGotLvl)
|
||||
}
|
||||
if gotGotMsg != tt.wantGotMsg {
|
||||
t.Errorf("getResponseValues() gotGotMsg = %v, want %v", gotGotMsg, tt.wantGotMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getResponseValues(sourceJson string, key string) (gotLvl, gotMsg string, err error) {
|
||||
gotLvl, err = jsonparser.GetString([]byte(sourceJson), "level")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
gotMsg, err = jsonparser.GetString([]byte(sourceJson), key)
|
||||
return
|
||||
}
|
||||
@@ -1,138 +1,106 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
retry "github.com/avast/retry-go"
|
||||
"github.com/lukaszraczylo/pandati"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/gookit/goutil"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
)
|
||||
|
||||
type requestBase struct {
|
||||
Query string `json:"query"`
|
||||
Variables interface{} `json:"variables"`
|
||||
}
|
||||
|
||||
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 (b *BaseClient) convertToJSON(v any) []byte {
|
||||
jsonData, 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
|
||||
b.Logger.Error("Can't convert to JSON", map[string]interface{}{"error": err.Error()})
|
||||
return nil
|
||||
}
|
||||
return j2, err
|
||||
return jsonData
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var body []byte
|
||||
var queryResult *queryResults
|
||||
queryHash := fmt.Sprintf("%x", md5.Sum(query))
|
||||
|
||||
if g.Cache {
|
||||
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 string(entry), nil
|
||||
}
|
||||
} else {
|
||||
g.Log.Debug("Unable to find the query in the cache", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
|
||||
func (b *BaseClient) compileQuery(queryPartials ...any) *Query {
|
||||
q := &Query{}
|
||||
for _, partial := range queryPartials {
|
||||
switch val := partial.(type) {
|
||||
case string:
|
||||
q.Query = val
|
||||
case map[string]interface{}:
|
||||
q.Variables = val
|
||||
}
|
||||
}
|
||||
|
||||
httpRequest, err := http.NewRequest("POST", g.Endpoint, bytes.NewBuffer(query))
|
||||
//httpRequest.Header.Add("Content-Type", "application/json")
|
||||
for header, value := range queryHeaders {
|
||||
httpRequest.Header.Add(fmt.Sprintf("%v", header), fmt.Sprintf("%v", value))
|
||||
if q.Query == "" {
|
||||
b.Logger.Error("Can't compile query", map[string]interface{}{"error": "query is empty"})
|
||||
return nil
|
||||
}
|
||||
q.JsonQuery = b.convertToJSON(q)
|
||||
return q
|
||||
}
|
||||
|
||||
var httpResponse *http.Response
|
||||
|
||||
err = retry.Do(
|
||||
func() error {
|
||||
httpResponse, err = g.HttpClient.Do(httpRequest)
|
||||
if err != nil {
|
||||
g.Log.Error("Unable to send the query", map[string]interface{}{"_error": err.Error()})
|
||||
return err
|
||||
}
|
||||
if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 {
|
||||
return errors.New(fmt.Sprintf("%v", httpResponse.StatusCode))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
retry.Attempts(uint(g.RetriesNumber)),
|
||||
retry.OnRetry(
|
||||
func(n uint, err error) {
|
||||
g.Log.Debug("Retrying the query", map[string]interface{}{"_attempt": n, "_error": err.Error()})
|
||||
time.Sleep(time.Duration(g.RetriesDelay) * time.Millisecond)
|
||||
},
|
||||
),
|
||||
)
|
||||
func (b *BaseClient) Query(query string, variables map[string]interface{}, headers map[string]interface{}) (any, error) {
|
||||
compiledQuery := b.compileQuery(query, variables)
|
||||
if compiledQuery.JsonQuery == nil {
|
||||
b.Logger.Error("Can't compile query", map[string]interface{}{"error": "query is empty"})
|
||||
return nil, fmt.Errorf("can't compile query")
|
||||
}
|
||||
b.Logger.Debug("Compiled query", map[string]interface{}{"query": compiledQuery})
|
||||
|
||||
defer io.Copy(ioutil.Discard, httpResponse.Body)
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
body, err = ioutil.ReadAll(httpResponse.Body)
|
||||
if err != nil {
|
||||
g.Log.Critical("Unable to read the response", map[string]interface{}{"_error": err.Error()})
|
||||
return "", err
|
||||
enableCache, enableRetries, recompileRequired := compiledQuery.parseHeadersAndVariables(headers)
|
||||
if recompileRequired {
|
||||
compiledQuery = b.compileQuery(query, variables)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &queryResult)
|
||||
if err != nil {
|
||||
g.Log.Error("Unable to unmarshal the query", map[string]interface{}{"_error": err.Error()})
|
||||
return "", err
|
||||
var queryHash string
|
||||
if (enableCache || b.cache_global) && strutil.HasPrefix(compiledQuery.Query, "query") {
|
||||
b.Logger.Debug("Cache enabled", nil)
|
||||
queryHash = calculateHash(compiledQuery)
|
||||
cachedValue := b.cacheLookup(queryHash)
|
||||
if cachedValue != nil {
|
||||
b.Logger.Debug("Cache hit", map[string]interface{}{"query": compiledQuery})
|
||||
return cachedValue, nil
|
||||
}
|
||||
b.Logger.Debug("Cache miss", map[string]interface{}{"query": compiledQuery})
|
||||
}
|
||||
|
||||
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[0].Message)
|
||||
if enableRetries || b.retries_enable {
|
||||
b.Logger.Debug("Retries enabled", nil)
|
||||
}
|
||||
|
||||
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")
|
||||
q := &QueryExecutor{
|
||||
BaseClient: b,
|
||||
Query: compiledQuery.JsonQuery,
|
||||
Headers: headers,
|
||||
CacheKey: func() string {
|
||||
if queryHash != "" {
|
||||
return queryHash
|
||||
}
|
||||
return "no-cache"
|
||||
}(),
|
||||
Retries: enableRetries || b.retries_enable,
|
||||
}
|
||||
defer func() { q = nil }()
|
||||
|
||||
responseContent, err = json.MarshalToString(queryResult.Data)
|
||||
rv, err := q.executeQuery()
|
||||
if err != nil {
|
||||
g.Log.Error("Invalid data result", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_result": responseContent, "_response_code": httpResponse.StatusCode})
|
||||
return "", err
|
||||
b.Logger.Error("Error executing query", map[string]interface{}{"error": err.Error()})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
returnedValue, err := q.decodeResponse(rv)
|
||||
return returnedValue, 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, []byte(responseContent))
|
||||
if err != nil {
|
||||
g.Log.Error("Unable to cache the query", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
|
||||
}
|
||||
}
|
||||
func (q *Query) parseHeadersAndVariables(headers map[string]interface{}) (enableCache, enableRetries, recompileRequired bool) {
|
||||
enableCache, _ = goutil.ToBool(searchForKeysInMapStringInterface(headers, "gqlcache"))
|
||||
enableRetries, _ = goutil.ToBool(searchForKeysInMapStringInterface(headers, "gqlretries"))
|
||||
|
||||
if q.Variables != nil {
|
||||
varEnableCache, _ := goutil.ToBool(searchForKeysInMapStringInterface(q.Variables, "gqlcache"))
|
||||
varEnableRetries, _ := goutil.ToBool(searchForKeysInMapStringInterface(q.Variables, "gqlretries"))
|
||||
enableCache = enableCache || varEnableCache
|
||||
enableRetries = enableRetries || varEnableRetries
|
||||
delete(q.Variables, "gqlcache")
|
||||
delete(q.Variables, "gqlretries")
|
||||
recompileRequired = true
|
||||
}
|
||||
|
||||
return
|
||||
return enableCache, enableRetries, recompileRequired
|
||||
}
|
||||
|
||||
+19
-65
@@ -1,69 +1,23 @@
|
||||
package gql
|
||||
|
||||
// import (
|
||||
// "testing"
|
||||
// )
|
||||
import "testing"
|
||||
|
||||
// func Benchmark_GraphQL_Query(t *testing.B) {
|
||||
// if testing.Short() {
|
||||
// t.Skip("Skipping test in short / CI mode")
|
||||
// }
|
||||
// type args struct {
|
||||
// queryContent string
|
||||
// queryVariables interface{}
|
||||
// }
|
||||
// tests := []struct {
|
||||
// name string
|
||||
// endpoint string
|
||||
// isLocal bool
|
||||
// args args
|
||||
// wantResult string
|
||||
// mockedReply string
|
||||
// wantErr bool
|
||||
// }{
|
||||
// {
|
||||
// name: "Valid query",
|
||||
// endpoint: "http://hasura.local/v1/graphql",
|
||||
// isLocal: false,
|
||||
// args: args{
|
||||
// queryContent: `query listUserBots {
|
||||
// tbl_bots(limit: 1) {
|
||||
// bot_name
|
||||
// }
|
||||
// }`,
|
||||
// queryVariables: nil,
|
||||
// },
|
||||
// wantResult: `{"tbl_bots":[{"bot_name":"littleMentionBot"}]}`,
|
||||
// mockedReply: `{"data":{"tbl_bots":[{"bot_name":"littleMentionBot"}]}}`,
|
||||
// wantErr: false,
|
||||
// },
|
||||
// {
|
||||
// name: "Invalid query",
|
||||
// endpoint: "http://hasura.local/v1/graphql",
|
||||
// isLocal: false,
|
||||
// args: args{
|
||||
// queryContent: `query listUserBots {
|
||||
// tbl_botz(limit: 1) {
|
||||
// bot_name
|
||||
// }
|
||||
// }`,
|
||||
// queryVariables: nil,
|
||||
// },
|
||||
// mockedReply: `{"errors":[{"message":"Unknown field \"tbl_botz\" on type \"Query\""}]}`,
|
||||
// wantResult: ``,
|
||||
// wantErr: true,
|
||||
// },
|
||||
// }
|
||||
func Benchmark_compileQuery(bn *testing.B) {
|
||||
b := NewConnection()
|
||||
bn.ResetTimer()
|
||||
for i := 0; i < bn.N; i++ {
|
||||
b.compileQuery(
|
||||
`query { viewer { login } }`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// for _, tt := range tests {
|
||||
// t.Run(tt.name, func(t *testing.B) {
|
||||
// g := NewConnection()
|
||||
// server, serverURL := mockGraphQLServerResponses(tt.mockedReply)
|
||||
// g.Endpoint = serverURL
|
||||
// for n := 0; n < t.N; n++ {
|
||||
// g.Query(tt.args.queryContent, tt.args.queryVariables, nil)
|
||||
// }
|
||||
// server.Close()
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
func Benchmark_convertToJson(bn *testing.B) {
|
||||
b := NewConnection()
|
||||
bn.ResetTimer()
|
||||
for i := 0; i < bn.N; i++ {
|
||||
b.convertToJSON(
|
||||
`query { viewer { login } }`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+292
-175
@@ -1,201 +1,318 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func (suite *TestSuite) Test_GraphQL_queryBuilder() {
|
||||
|
||||
func (suite *Tests) TestBaseClient_convertToJSON() {
|
||||
type args struct {
|
||||
queryContent string
|
||||
queryVariables interface{}
|
||||
v any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
wantErr bool
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
}{
|
||||
{
|
||||
name: "Valid query",
|
||||
name: "TestBaseClient_convertToJSON_query",
|
||||
args: args{
|
||||
queryContent: `query listUserBots { tbl_bots { bot_name } }`,
|
||||
queryVariables: nil,
|
||||
v: `query { viewer { login } }`,
|
||||
},
|
||||
want: []byte(`{"query":"query listUserBots { tbl_bots { bot_name } }","variables":null}`),
|
||||
wantErr: false,
|
||||
want: []byte(`"query { viewer { login } }"`),
|
||||
},
|
||||
{
|
||||
name: "Valid query with variables",
|
||||
name: "TestBaseClient_convertToJSON_variables",
|
||||
args: args{
|
||||
queryContent: `query listUserBots { tbl_bots { bot_name } }`,
|
||||
queryVariables: map[string]interface{}{"user_id": 1},
|
||||
},
|
||||
want: []byte(`{"query":"query listUserBots { tbl_bots { bot_name } }","variables":{"user_id":1}}`),
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
q := NewConnection()
|
||||
got, gotErr := q.queryBuilder(tt.args.queryContent, tt.args.queryVariables)
|
||||
assert.Equal(t, tt.want, got)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, gotErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TestSuite) Test_GraphQL_Query() {
|
||||
if testing.Short() {
|
||||
suite.T().Skip("Skipping test in short / CI mode")
|
||||
}
|
||||
g := NewConnection()
|
||||
|
||||
type args struct {
|
||||
queryContent string
|
||||
queryVariables interface{}
|
||||
queryHeaders map[string]interface{}
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
isLocal bool
|
||||
cache_enabled bool
|
||||
args args
|
||||
wantResult string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid query, no cache",
|
||||
endpoint: "https://hasura.local/v1/graphql",
|
||||
isLocal: true,
|
||||
cache_enabled: false,
|
||||
args: args{
|
||||
queryContent: `query packages_prices {
|
||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}`,
|
||||
queryVariables: nil,
|
||||
},
|
||||
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid query, with cache empty",
|
||||
endpoint: "https://hasura.local/v1/graphql",
|
||||
isLocal: true,
|
||||
cache_enabled: true,
|
||||
args: args{
|
||||
queryContent: `query packages_prices {
|
||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}`,
|
||||
queryVariables: nil,
|
||||
},
|
||||
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid query, with cache filled",
|
||||
endpoint: "https://hasura.local/v1/graphql",
|
||||
isLocal: true,
|
||||
cache_enabled: true,
|
||||
args: args{
|
||||
queryContent: `query packages_prices {
|
||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}`,
|
||||
queryVariables: nil,
|
||||
},
|
||||
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid query",
|
||||
endpoint: "https://hasura.local/v1/graphql",
|
||||
isLocal: true,
|
||||
args: args{
|
||||
queryContent: `query packages_pricez {
|
||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}`,
|
||||
queryVariables: nil,
|
||||
},
|
||||
wantResult: ``,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Valid query to https endpoint",
|
||||
endpoint: "https://telegram-bot.app/v1/graphql",
|
||||
isLocal: false,
|
||||
args: args{
|
||||
queryContent: `query packages_prices {
|
||||
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
|
||||
aggregate {
|
||||
count
|
||||
}
|
||||
}
|
||||
}`,
|
||||
queryVariables: nil,
|
||||
},
|
||||
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Valid query to github endpoint",
|
||||
endpoint: "https://api.github.com/graphql",
|
||||
isLocal: false,
|
||||
args: args{
|
||||
queryContent: `query {
|
||||
repository(name: "semver-generator", owner: "lukaszraczylo", followRenames: true) {
|
||||
releases(last: 2) {
|
||||
nodes {
|
||||
tag {
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}`,
|
||||
queryVariables: nil,
|
||||
queryHeaders: map[string]interface{}{
|
||||
"Authorization": "Bearer " + os.Getenv("GITHUB_TOKEN"),
|
||||
v: map[string]interface{}{
|
||||
"login": "lukaszraczylo",
|
||||
"first": 10,
|
||||
},
|
||||
},
|
||||
wantResult: `{"repository":{"releases":{"nodes":[`,
|
||||
wantErr: false,
|
||||
want: []byte(`{"first":10,"login":"lukaszraczylo"}`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
// if tt.isLocal {
|
||||
// os.Setenv("GRAPHQL_ENDPOINT", tt.endpoint)
|
||||
// }
|
||||
g.Endpoint = tt.endpoint
|
||||
g.Cache = tt.cache_enabled
|
||||
gotResult, gotErr := g.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, gotErr)
|
||||
b := NewConnection()
|
||||
if got := b.convertToJSON(tt.args.v); !reflect.DeepEqual(got, tt.want) {
|
||||
assert.Equal(tt.want, got)
|
||||
}
|
||||
assert.Contains(t, gotResult, tt.wantResult)
|
||||
})
|
||||
}
|
||||
fmt.Println(g.CacheStore.Capacity())
|
||||
}
|
||||
|
||||
func (suite *Tests) TestBaseClient_compileQuery() {
|
||||
type args struct {
|
||||
query_partials []any
|
||||
}
|
||||
tests := []struct {
|
||||
wantQ *Query
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "TestBaseClient_compileQuery_query",
|
||||
args: args{
|
||||
query_partials: []any{
|
||||
`query { viewer { login } }`,
|
||||
},
|
||||
},
|
||||
wantQ: &Query{
|
||||
Query: `query { viewer { login } }`,
|
||||
JsonQuery: []byte(`{"query":"query { viewer { login } }"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_compileQuery_with_variables",
|
||||
args: args{
|
||||
query_partials: []any{
|
||||
`query { viewer { login } }`,
|
||||
map[string]interface{}{
|
||||
"login": "lukaszraczylo",
|
||||
"first": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantQ: &Query{
|
||||
Query: `query { viewer { login } }`,
|
||||
Variables: map[string]interface{}{
|
||||
"login": "lukaszraczylo",
|
||||
"first": 10,
|
||||
},
|
||||
JsonQuery: []byte(`{"variables":{"first":10,"login":"lukaszraczylo"},"query":"query { viewer { login } }"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_compileQuery_variables_without_query",
|
||||
args: args{
|
||||
query_partials: []any{
|
||||
map[string]interface{}{
|
||||
"login": "lukaszraczylo",
|
||||
"first": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantQ: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
b := NewConnection()
|
||||
got := b.compileQuery(tt.args.query_partials...)
|
||||
assert.Equal(tt.wantQ, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *Tests) TestBaseClient_Query() {
|
||||
type args struct {
|
||||
variables map[string]interface{}
|
||||
headers map[string]interface{}
|
||||
query string
|
||||
}
|
||||
tests := []struct {
|
||||
args args
|
||||
wantReturned_value any
|
||||
name string
|
||||
graphQLServer string
|
||||
wantType string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "TestBaseClient_Query_spacex_working",
|
||||
graphQLServer: "https://spacex-production.up.railway.app/",
|
||||
args: args{
|
||||
query: `query Dragons {
|
||||
dragons {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
variables: nil,
|
||||
headers: map[string]interface{}{
|
||||
"x-apollo-operation-name": "Dragons/github.com/lukaszraczylo/go-simple-graphql",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
wantReturned_value: string(`{"dragons":[{"name":"Dragon 1"},{"name":"Dragon 2"}]}`),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_Query_spacex_invalid_query",
|
||||
graphQLServer: "https://spacex-production.up.railway.app/",
|
||||
args: args{
|
||||
query: `query Dragons {
|
||||
dragons {
|
||||
name
|
||||
}
|
||||
`,
|
||||
variables: nil,
|
||||
headers: map[string]interface{}{
|
||||
"x-apollo-operation-name": "Dragons/github.com/lukaszraczylo/go-simple-graphql",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
wantReturned_value: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_Query_tgbotapp_working",
|
||||
graphQLServer: "https://telegram-bot.app/v1/graphql",
|
||||
args: args{
|
||||
query: `query {
|
||||
__type(name: "Query") {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantReturned_value: string(`{"__type":null}`),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_Query_tgbotapp_invalid_query",
|
||||
graphQLServer: "https://telegram-bot.app/v1/graphql",
|
||||
args: args{
|
||||
query: `query {
|
||||
__type(name: "Query") {
|
||||
name
|
||||
}
|
||||
`,
|
||||
},
|
||||
wantReturned_value: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_Query_tgbotapp_invalid_wrong_field",
|
||||
graphQLServer: "https://telegram-bot.app/v1/graphql",
|
||||
args: args{
|
||||
query: `query {
|
||||
__type(name: "Query") {
|
||||
name2
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantReturned_value: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_Query_tgbotapp_wrong_url",
|
||||
graphQLServer: "https://telegram-bot.app/v0/graphql",
|
||||
args: args{
|
||||
query: `query {
|
||||
__type(name: "Query") {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantReturned_value: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_Query_tgbotapp_valid_query_mapstring",
|
||||
graphQLServer: "https://telegram-bot.app/v1/graphql",
|
||||
wantType: "mapstring",
|
||||
args: args{
|
||||
query: `query {
|
||||
__type(name: "Query") {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantReturned_value: map[string]interface{}{"__type": nil},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_Query_tgbotapp_valid_query_byte",
|
||||
graphQLServer: "https://telegram-bot.app/v1/graphql",
|
||||
wantType: "byte",
|
||||
args: args{
|
||||
query: `query {
|
||||
__type(name: "Query") {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantReturned_value: []byte(`{"__type":null}`),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_Query_tgbotapp_valid_query_string",
|
||||
graphQLServer: "https://telegram-bot.app/v1/graphql",
|
||||
wantType: "string",
|
||||
args: args{
|
||||
query: `query {
|
||||
__type(name: "Query") {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantReturned_value: string(`{"__type":null}`),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_Query_tgbotapp_valid_query_invalid_type",
|
||||
graphQLServer: "https://telegram-bot.app/v1/graphql",
|
||||
wantType: "potato",
|
||||
args: args{
|
||||
query: `query {
|
||||
__type(name: "Query") {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
},
|
||||
wantReturned_value: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_Query_tgbotapp_valid_query_cache",
|
||||
graphQLServer: "https://telegram-bot.app/v1/graphql",
|
||||
args: args{
|
||||
query: `query {
|
||||
__type(name: "Query") {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
variables: map[string]interface{}{
|
||||
"gqlcache": true,
|
||||
},
|
||||
},
|
||||
wantReturned_value: string(`{"__type":null}`),
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_Query_tgbotapp_valid_query_with_retry",
|
||||
graphQLServer: "https://telegram-bot.app/v0/graphql",
|
||||
wantType: "byte",
|
||||
args: args{
|
||||
query: `query {
|
||||
__type(name: "Query") {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
variables: map[string]interface{}{
|
||||
"gqlretries": true,
|
||||
},
|
||||
},
|
||||
wantReturned_value: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
b := NewConnection()
|
||||
if tt.graphQLServer != "" {
|
||||
b.SetEndpoint(tt.graphQLServer)
|
||||
}
|
||||
if tt.wantType != "" {
|
||||
b.SetOutput(tt.wantType)
|
||||
}
|
||||
gotReturned_value, err := b.Query(tt.args.query, tt.args.variables, tt.args.headers)
|
||||
if tt.wantErr {
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
assert.Equal(tt.wantReturned_value, gotReturned_value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||
)
|
||||
|
||||
func main() {
|
||||
gql := graphql.NewConnection()
|
||||
|
||||
query := `query MyQuery {
|
||||
bots {
|
||||
bot_name
|
||||
ddd
|
||||
}
|
||||
}`
|
||||
_, err := gql.Query(query, nil, nil)
|
||||
if err != nil {
|
||||
fmt.Println("Error returned from query:", err)
|
||||
}
|
||||
|
||||
query = `query MyQuery {
|
||||
badwords(distinct_on: word) {
|
||||
word
|
||||
}
|
||||
}`
|
||||
gql.Query(query, nil, nil)
|
||||
|
||||
// Repeated to use the cached response
|
||||
query = `query MyQuery {
|
||||
badwords(distinct_on: word) {
|
||||
word
|
||||
}
|
||||
}`
|
||||
gql.Query(query, nil, nil)
|
||||
}
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
version: 1
|
||||
force:
|
||||
major: 1
|
||||
minor: 0
|
||||
minor: 2
|
||||
patch: 0
|
||||
commit: 3217edf37fd1765c4f2594e3ba122a6a9c251296
|
||||
wording:
|
||||
minor:
|
||||
- minor-release
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
cache "github.com/lukaszraczylo/go-simple-graphql/cache"
|
||||
logging "github.com/lukaszraczylo/go-simple-graphql/logging"
|
||||
)
|
||||
|
||||
type BaseClient struct {
|
||||
cache *cache.Cache
|
||||
Logger *logging.LogConfig
|
||||
client *http.Client
|
||||
endpoint string
|
||||
responseType string
|
||||
retries_delay time.Duration
|
||||
retries_number int
|
||||
MaxGoRoutines int
|
||||
cache_global bool
|
||||
retries_enable bool
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||
Query string `json:"query,omitempty"`
|
||||
JsonQuery []byte `json:"jsonQuery,omitempty"`
|
||||
}
|
||||
|
||||
type QueryExecutor struct {
|
||||
Result any
|
||||
Error error
|
||||
*BaseClient
|
||||
Headers map[string]interface{}
|
||||
CacheKey string
|
||||
Query []byte
|
||||
CacheTTL time.Duration
|
||||
Retries bool
|
||||
}
|
||||
|
||||
type queryResults struct {
|
||||
Data interface{} `json:"data"`
|
||||
Errors []struct {
|
||||
Message interface{} `json:"message"`
|
||||
} `json:"errors"`
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assertions "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type Tests struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
var (
|
||||
assert *assertions.Assertions
|
||||
)
|
||||
|
||||
func (suite *Tests) SetupTest() {
|
||||
assert = assertions.New(suite.T())
|
||||
}
|
||||
|
||||
func (suite *Tests) BeforeTest(suiteName, testName string) {
|
||||
}
|
||||
|
||||
func TestSuite(t *testing.T) {
|
||||
suite.Run(t, new(Tests))
|
||||
}
|
||||
Reference in New Issue
Block a user