mirror of
https://github.com/lukaszraczylo/go-simple-graphql.git
synced 2026-10-12 00:45:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
|
||
|
|
3cda8e267b
|
||
|
|
1393f4d7cf
|
||
|
|
d4328fdbb7
|
||
|
|
4ac9f132a2
|
||
|
|
fb04e68b8d
|
||
|
|
40652c3fdd
|
||
|
|
f360a5cf7e
|
||
|
|
2b09da7685
|
||
|
|
aac6ca113d
|
||
|
|
c8ed2290b4
|
||
|
|
d1406eeac8
|
||
|
|
ea7ec683ef
|
||
|
|
5cd22318f1
|
||
|
|
13955da995
|
||
|
|
cd5d75feff
|
@@ -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 }}
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
*.out
|
||||
*.test
|
||||
coverage.txt
|
||||
coverage.txt
|
||||
*.DS_Store
|
||||
@@ -11,11 +11,23 @@ help: ## display this help
|
||||
|
||||
.PHONY: test
|
||||
test: ## run tests on library
|
||||
@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
|
||||
@go test -v -cover ./pkg/...
|
||||
|
||||
.PHONY: all
|
||||
all: test-packages test
|
||||
all: test-packages test
|
||||
|
||||
.PHONY: update
|
||||
update: ## update dependencies
|
||||
@go get -u -v ./...
|
||||
@go mod tidy -v
|
||||
|
||||
sonar:
|
||||
sonar-scanner \
|
||||
-Dsonar.projectKey=golang-simple-graphql \
|
||||
-Dsonar.sources=. \
|
||||
-Dsonar.host.url=http://localhost:9000 \
|
||||
-Dsonar.login=555ba78b315faa07b3231a3be0f3cbb24ab33814
|
||||
@@ -9,13 +9,16 @@ 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
|
||||
|
||||
I've tried to run few graphQL clients with hasura, all of them required conversion of the data into
|
||||
the appropriate structures, causing issues with non-existing types ( thanks to Hasura ), for example `bigint` which was difficult to export.
|
||||
I've tried to run a few GraphQL clients with Hasura, all of them required conversion of the data into
|
||||
the appropriate structures, causing issues with non-existing types ( thanks to Hasura ), for example, `bigint` which was difficult to export.
|
||||
Therefore, I present you the simple client to which you can copy & paste your graphQL query, variables and you are good to go.
|
||||
|
||||
## Features
|
||||
@@ -23,13 +26,45 @@ Therefore, I present you the simple client to which you can copy & paste your gr
|
||||
* Executing GraphQL queries as they are, without types declaration
|
||||
* HTTP2 support!
|
||||
* Support for additional headers
|
||||
* Query cache
|
||||
|
||||
## Usage example
|
||||
|
||||
### Environment variables
|
||||
|
||||
* `GRAPHQL_ENDPOINT` - Your GraphQL endpoint. Default: `http://127.0.0.1:9090/v1/graphql`
|
||||
* `LOG_LEVEL` - Logging level. Default: `info`
|
||||
* `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`
|
||||
* `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
|
||||
|
||||
@@ -37,35 +72,42 @@ Therefore, I present you the simple client to which you can copy & paste your gr
|
||||
```go
|
||||
import (
|
||||
fmt
|
||||
gql "github.com/lukaszraczylo/simple-gql-client"
|
||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||
)
|
||||
|
||||
headers := map[string]interface{}{
|
||||
"x-hasura-user-id": 37,
|
||||
"x-hasura-user-uuid": "bde3262e-b42e-4151-ac10-d43f0bef44a5",
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"fileHash": "123deadc0w321",
|
||||
}
|
||||
var query = `query searchFileKnown($fileHash: String) {
|
||||
tbl_file_scans(where: {file_hash: {_eq: $fileHash}}) {
|
||||
porn
|
||||
racy
|
||||
violence
|
||||
virus
|
||||
func main() {
|
||||
headers := map[string]interface{}{
|
||||
"x-hasura-user-id": 37,
|
||||
"x-hasura-user-uuid": "bde1962e-b42e-1212-ac10-d43fa27f44a5",
|
||||
}
|
||||
}
|
||||
|
||||
graphql := NewConnection()
|
||||
result, err := graphql.Query(query, variables, nil)
|
||||
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
|
||||
@@ -74,10 +116,12 @@ 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 information required in further steps and I strongly recommend this approach as the easiest and close to painless.
|
||||
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()
|
||||
result := gjson.Get(result, "tbl_user_group_admins.0.is_admin").Bool()
|
||||
if result {
|
||||
fmt.Println("User is an admin")
|
||||
}
|
||||
|
||||
Vendored
+148
@@ -0,0 +1,148 @@
|
||||
package libpack_cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"flag"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CacheEntry struct {
|
||||
ExpiresAt time.Time
|
||||
Value []byte
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
bytePool sync.Pool
|
||||
entries sync.Map
|
||||
globalTTL time.Duration
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
func New(globalTTL time.Duration) *Cache {
|
||||
cache := &Cache{
|
||||
globalTTL: globalTTL,
|
||||
}
|
||||
|
||||
// Initialize the byte pool.
|
||||
cache.bytePool.New = func() interface{} {
|
||||
return make([]byte, 0)
|
||||
}
|
||||
|
||||
// Start the cache cleanup.
|
||||
if flag.Lookup("test.v") == nil {
|
||||
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()
|
||||
defer c.Unlock()
|
||||
expiresAt := time.Now().Add(ttl)
|
||||
|
||||
compressedValue, err := c.compress(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Get a byte slice from the pool and ensure it's properly sized.
|
||||
b := c.bytePool.Get().([]byte)
|
||||
if cap(b) < len(compressedValue) {
|
||||
b = make([]byte, len(compressedValue))
|
||||
} else {
|
||||
b = b[:len(compressedValue)]
|
||||
}
|
||||
|
||||
copy(b, compressedValue)
|
||||
|
||||
entry := CacheEntry{
|
||||
Value: b,
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
c.entries.Store(key, entry)
|
||||
}
|
||||
|
||||
func (c *Cache) Get(key string) ([]byte, bool) {
|
||||
c.RLock()
|
||||
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()
|
||||
|
||||
entry, ok := c.entries.Load(key)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// Return the byte slice to the pool.
|
||||
c.bytePool.Put(entry.(CacheEntry).Value)
|
||||
|
||||
// Delete the entry from the cache.
|
||||
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) {
|
||||
// Return the byte slice to the pool.
|
||||
c.bytePool.Put(entry.Value)
|
||||
|
||||
// Delete the entry from the cache.
|
||||
c.entries.Delete(key)
|
||||
}
|
||||
|
||||
// Return true to continue iterating over the map.
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (c *Cache) compress(data []byte) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
w := gzip.NewWriter(&buf)
|
||||
_, err := w.Write(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (c *Cache) decompress(data []byte) ([]byte, error) {
|
||||
r, err := gzip.NewReader(bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer r.Close()
|
||||
return io.ReadAll(r)
|
||||
}
|
||||
Vendored
+148
@@ -0,0 +1,148 @@
|
||||
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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) Test_CacheCleanExpiredEntries() {
|
||||
cache := New(5 * 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))
|
||||
cache.CleanExpiredEntries()
|
||||
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,22 +1,27 @@
|
||||
module github.com/lukaszraczylo/go-simple-graphql
|
||||
|
||||
go 1.17
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/avast/retry-go/v4 v4.6.0
|
||||
github.com/buger/jsonparser v1.1.1
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/lukaszraczylo/pandati v0.0.10
|
||||
github.com/rs/zerolog v1.25.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6
|
||||
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.24.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // 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
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // 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.19.0 // indirect
|
||||
golang.org/x/text v0.15.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,62 +1,49 @@
|
||||
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/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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
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.10 h1:ZXmLjxb/UYRBVyAKwXmUcJgZ2FYNwGY4YDJsQQMYAXI=
|
||||
github.com/lukaszraczylo/pandati v0.0.10/go.mod h1:1lE/x4X4LfO1yerAQoiR7AVb/wIabH/zITDGnqTdyH8=
|
||||
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/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.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II=
|
||||
github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=
|
||||
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.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.3.5/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/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-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6 h1:Z04ewVs7JhXaYkmDhBERPi41gnltfQpMWDnTnQbaCqk=
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
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-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/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 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
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.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
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.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
|
||||
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
|
||||
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.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
|
||||
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
|
||||
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.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/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,64 +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"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 NewConnection() *GraphQL {
|
||||
return &GraphQL{
|
||||
Endpoint: pickGraphqlEndpoint(),
|
||||
HttpClient: &http.Client{
|
||||
Timeout: time.Second * 30,
|
||||
Transport: &http2.Transport{
|
||||
DisableCompression: false,
|
||||
AllowHTTP: true,
|
||||
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||
return net.Dial(network, addr)
|
||||
},
|
||||
},
|
||||
},
|
||||
Log: logging.NewLogger(),
|
||||
}
|
||||
}
|
||||
-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")
|
||||
} 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")
|
||||
} else {
|
||||
b.Logger.Critical("Invalid endpoint - neither http or https")
|
||||
}
|
||||
return http_client
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package libpack_logging
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gookit/goutil/envutil"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type LogConfig struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
var baseLogger zerolog.Logger
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
func NewLogger() *LogConfig {
|
||||
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)
|
||||
}
|
||||
|
||||
return &LogConfig{logger: baseLogger}
|
||||
}
|
||||
|
||||
func (lw *LogConfig) log(w io.Writer, level zerolog.Level, message string, v map[string]interface{}) {
|
||||
e := lw.logger.With().Logger()
|
||||
e = e.Output(w)
|
||||
event := e.WithLevel(level).CallerSkipFrame(3)
|
||||
for k, val := range v {
|
||||
switch v := val.(type) {
|
||||
case string:
|
||||
event.Str(k, v)
|
||||
case int:
|
||||
event.Int(k, v)
|
||||
case float64:
|
||||
event.Float64(k, v)
|
||||
default:
|
||||
event.Interface(k, val)
|
||||
}
|
||||
}
|
||||
event.Msg(message)
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Debug(message string, v ...map[string]interface{}) {
|
||||
if !lw.logger.Debug().Enabled() {
|
||||
return
|
||||
}
|
||||
lw.log(os.Stdout, zerolog.DebugLevel, message, mergeMaps(v))
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Info(message string, v ...map[string]interface{}) {
|
||||
if !lw.logger.Info().Enabled() {
|
||||
return
|
||||
}
|
||||
lw.log(os.Stdout, zerolog.InfoLevel, message, mergeMaps(v))
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Warning(message string, v ...map[string]interface{}) {
|
||||
lw.log(os.Stdout, zerolog.WarnLevel, message, mergeMaps(v))
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Error(message string, v ...map[string]interface{}) {
|
||||
lw.log(os.Stderr, zerolog.ErrorLevel, message, mergeMaps(v))
|
||||
}
|
||||
|
||||
func (lw *LogConfig) Critical(message string, v ...map[string]interface{}) {
|
||||
lw.log(os.Stderr, zerolog.FatalLevel, message, mergeMaps(v))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func mergeMaps(maps []map[string]interface{}) map[string]interface{} {
|
||||
result := make(map[string]interface{})
|
||||
for _, m := range maps {
|
||||
for k, v := range m {
|
||||
result[k] = v
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
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()
|
||||
for i := 0; i < b.N; i++ {
|
||||
testsLogger.Info("test", map[string]interface{}{"test": "test"})
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package logging
|
||||
package libpack_logging
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
@@ -35,7 +35,7 @@ func (sc *stdoutCapture) StopCapture() (string, error) {
|
||||
}
|
||||
os.Stdout.Close()
|
||||
os.Stdout = sc.oldStdout
|
||||
bytes, err := ioutil.ReadAll(sc.readPipe)
|
||||
bytes, err := io.ReadAll(sc.readPipe)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -58,7 +58,7 @@ func (sc *stderrCapture) StopCapture() (string, error) {
|
||||
}
|
||||
os.Stderr.Close()
|
||||
os.Stderr = sc.oldStderr
|
||||
bytes, err := ioutil.ReadAll(sc.readPipe)
|
||||
bytes, err := io.ReadAll(sc.readPipe)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -82,9 +82,9 @@ func (suite *LoggingTestSuite) TestLogConfig_AllHandlers() {
|
||||
wantLevel string
|
||||
wantMessage string
|
||||
envMinLogLevel string
|
||||
loggerType string
|
||||
stdOutExpect bool
|
||||
stdErrExpect bool
|
||||
loggerType string
|
||||
}{
|
||||
{
|
||||
name: "Test log: Error",
|
||||
@@ -183,7 +183,7 @@ func (suite *LoggingTestSuite) TestLogConfig_AllHandlers() {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
suite.Run(tt.name, func() {
|
||||
if tt.envMinLogLevel != "" {
|
||||
os.Setenv("LOG_LEVEL", tt.envMinLogLevel)
|
||||
defer os.Unsetenv("LOG_LEVEL")
|
||||
@@ -232,21 +232,21 @@ func (suite *LoggingTestSuite) TestLogConfig_AllHandlers() {
|
||||
|
||||
func (suite *LoggingTestSuite) TestFullMessage() {
|
||||
type args struct {
|
||||
message string
|
||||
extraFields map[string]interface{}
|
||||
message string
|
||||
}
|
||||
extraFields := make(map[string]interface{})
|
||||
extraFields["_full_message"] = "full message"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
name string
|
||||
wantLevel string
|
||||
wantMessage string
|
||||
envMinLogLevel string
|
||||
loggerType string
|
||||
stdOutExpect bool
|
||||
stdErrExpect bool
|
||||
loggerType string
|
||||
}{
|
||||
{
|
||||
name: "Test log: Error",
|
||||
@@ -274,7 +274,7 @@ func (suite *LoggingTestSuite) TestFullMessage() {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
suite.Run(tt.name, func() {
|
||||
if tt.envMinLogLevel != "" {
|
||||
os.Setenv("LOG_LEVEL", tt.envMinLogLevel)
|
||||
defer os.Unsetenv("LOG_LEVEL")
|
||||
@@ -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,95 +1,106 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"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"`
|
||||
func (b *BaseClient) convertToJSON(v any) []byte {
|
||||
jsonData, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
b.Logger.Error("Can't convert to JSON", map[string]interface{}{"error": err.Error()})
|
||||
return nil
|
||||
}
|
||||
return jsonData
|
||||
}
|
||||
|
||||
type queryResults struct {
|
||||
Data interface{} `json:"data"`
|
||||
Errors []struct {
|
||||
Message interface{} `json:"message"`
|
||||
} `json:"errors"`
|
||||
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
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
func (g *GraphQL) queryBuilder(queryContent string, queryVariables interface{}) ([]byte, error) {
|
||||
var qb = &requestBase{
|
||||
Query: queryContent,
|
||||
Variables: queryVariables,
|
||||
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})
|
||||
|
||||
enableCache, enableRetries, recompileRequired := compiledQuery.parseHeadersAndVariables(headers)
|
||||
if recompileRequired {
|
||||
compiledQuery = b.compileQuery(query, variables)
|
||||
}
|
||||
|
||||
// j := new(bytes.Buffer)
|
||||
j2, err := json.Marshal(qb)
|
||||
if err != nil {
|
||||
g.Log.Critical("Unable to marshal the query", map[string]interface{}{"_error": err.Error(), "_query": queryContent, "_variables": queryVariables})
|
||||
return []byte{}, err
|
||||
var queryHash string
|
||||
if (enableCache || b.cache_global) && strutil.HasPrefix(compiledQuery.Query, "query") {
|
||||
b.Logger.Debug("Cache enabled")
|
||||
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 err = json.Compact(j, j2); err != nil {
|
||||
// g.Log.Critical("Unable to compact the query", map[string]interface{}{"error": err.Error()})
|
||||
// return []byte{}, err
|
||||
// }
|
||||
return j2, err
|
||||
|
||||
if enableRetries || b.retries_enable {
|
||||
b.Logger.Debug("Retries enabled")
|
||||
}
|
||||
|
||||
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 }()
|
||||
|
||||
rv, err := q.executeQuery()
|
||||
if err != nil {
|
||||
b.Logger.Error("Error executing query", map[string]interface{}{"error": err.Error()})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
returnedValue, err := q.decodeResponse(rv)
|
||||
return returnedValue, err
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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))
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
var queryResult *queryResults
|
||||
err = json.Unmarshal(body, &queryResult)
|
||||
if err != nil {
|
||||
g.Log.Error("Unable to unmarshal the query", map[string]interface{}{"_error": err.Error()})
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !pandati.IsZero(queryResult.Errors) {
|
||||
g.Log.Error("Query returned error", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_error": fmt.Sprintf("%v", queryResult.Errors)})
|
||||
return "", fmt.Errorf("%v", queryResult.Errors[0].Message)
|
||||
}
|
||||
|
||||
if pandati.IsZero(queryResult.Data) {
|
||||
g.Log.Error("Query returned no data", map[string]interface{}{"_query": queryContent, "_variables": queryVariables})
|
||||
return "", errors.New("Query returned no data")
|
||||
}
|
||||
|
||||
responseContent, err = json.MarshalToString(queryResult.Data)
|
||||
if err != nil {
|
||||
g.Log.Error("Invalid data result", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_result": responseContent})
|
||||
return "", err
|
||||
}
|
||||
|
||||
return
|
||||
return enableCache, enableRetries, recompileRequired
|
||||
}
|
||||
|
||||
+16
-62
@@ -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")
|
||||
func Benchmark_compileQuery(bn *testing.B) {
|
||||
b := NewConnection()
|
||||
bn.ResetTimer()
|
||||
for i := 0; i < bn.N; i++ {
|
||||
b.compileQuery(
|
||||
`query { viewer { login } }`,
|
||||
)
|
||||
}
|
||||
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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
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 } }`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+271
-66
@@ -1,113 +1,318 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"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},
|
||||
v: map[string]interface{}{
|
||||
"login": "lukaszraczylo",
|
||||
"first": 10,
|
||||
},
|
||||
},
|
||||
want: []byte(`{"query":"query listUserBots { tbl_bots { bot_name } }","variables":{"user_id":1}}`),
|
||||
wantErr: false,
|
||||
want: []byte(`{"first":10,"login":"lukaszraczylo"}`),
|
||||
},
|
||||
}
|
||||
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)
|
||||
b := NewConnection()
|
||||
if got := b.convertToJSON(tt.args.v); !reflect.DeepEqual(got, tt.want) {
|
||||
assert.Equal(tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TestSuite) Test_GraphQL_Query() {
|
||||
if testing.Short() {
|
||||
suite.T().Skip("Skipping test in short / CI mode")
|
||||
}
|
||||
func (suite *Tests) TestBaseClient_compileQuery() {
|
||||
type args struct {
|
||||
queryContent string
|
||||
queryVariables interface{}
|
||||
query_partials []any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
isLocal bool
|
||||
args args
|
||||
wantResult string
|
||||
wantErr bool
|
||||
wantQ *Query
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "Valid query",
|
||||
endpoint: "http://hasura.local/v1/graphql",
|
||||
isLocal: true,
|
||||
name: "TestBaseClient_compileQuery_query",
|
||||
args: args{
|
||||
queryContent: `query listUserBots {
|
||||
tbl_bots(limit: 1) {
|
||||
bot_name
|
||||
}
|
||||
}`,
|
||||
queryVariables: nil,
|
||||
query_partials: []any{
|
||||
`query { viewer { login } }`,
|
||||
},
|
||||
},
|
||||
wantQ: &Query{
|
||||
Query: `query { viewer { login } }`,
|
||||
JsonQuery: []byte(`{"query":"query { viewer { login } }"}`),
|
||||
},
|
||||
wantResult: `{"tbl_bots":[{"bot_name":"littleMentionBot"}]}`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid query",
|
||||
endpoint: "http://hasura.local/v1/graphql",
|
||||
isLocal: true,
|
||||
name: "TestBaseClient_compileQuery_with_variables",
|
||||
args: args{
|
||||
queryContent: `query listUserBots {
|
||||
tbl_botz(limit: 1) {
|
||||
bot_name
|
||||
}
|
||||
}`,
|
||||
queryVariables: nil,
|
||||
query_partials: []any{
|
||||
`query { viewer { login } }`,
|
||||
map[string]interface{}{
|
||||
"login": "lukaszraczylo",
|
||||
"first": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantResult: ``,
|
||||
wantErr: true,
|
||||
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) {
|
||||
if tt.isLocal {
|
||||
os.Setenv("GRAPHQL_ENDPOINT", tt.endpoint)
|
||||
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)
|
||||
}
|
||||
g := NewConnection()
|
||||
gotResult, gotErr := g.Query(tt.args.queryContent, tt.args.queryVariables, nil)
|
||||
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(t, gotErr)
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
assert.Equal(t, tt.wantResult, gotResult)
|
||||
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