Compare commits

...
69 Commits
Author SHA1 Message Date
lukaszraczylo 9a58df7a54 Speeding the queries up. 2024-05-04 21:17:14 +01:00
lukaszraczylo 4e60a90ce4 Update dependencies. 2024-05-04 19:49:47 +01:00
lukaszraczylo 2f4c8cc5d8 Replace jsoniter with goccy/go-json 2024-02-05 16:01:18 +00:00
lukaszraczylo 154233df14 Add support for the gzipped response. 2023-12-13 23:19:11 +00:00
lukaszraczylo 29afbd08fd Micro improvements on connection handling. 2023-12-13 22:44:57 +00:00
lukaszraczylo a107ee953b Force the string value for the header setting. 2023-12-13 09:18:26 +00:00
lukaszraczylo 58094f85c7 fixup! fixup! Update dependencies Fix go mod version in mod file. 2023-12-12 23:03:58 +00:00
lukaszraczylo ac957ac95b fixup! Update dependencies Fix go mod version in mod file. 2023-12-12 22:47:19 +00:00
lukaszraczylo ef4ab45424 Update dependencies
Fix go mod version in mod file.
2023-12-12 21:41:34 +00:00
lukaszraczylo 4fc826c1f6 Complete rewrite of the library. 2023-12-12 21:30:26 +00:00
lukaszraczylo 3217edf37f Update struct alignment. 2023-10-24 11:40:17 +01:00
lukaszraczylo ae96059f61 Update so the cache uses compression. 2023-10-20 11:25:57 +01:00
lukaszraczylo 2ce7016d7e Update logging to the common logging format for all the projects. 2023-10-19 15:19:21 +01:00
lukaszraczylo 9da7c9bec9 fixup! Add self-sufficient cache and abandon outdated library. 2023-10-13 20:38:17 +01:00
lukaszraczylo ca13be5ef6 Add self-sufficient cache and abandon outdated library. 2023-10-13 20:35:20 +01:00
lukaszraczylo 1df2ae5d16 Update dependencies. 2023-10-12 16:29:50 +01:00
lukaszraczylo 39f489210b Update dependencies. 2023-09-06 09:26:06 +01:00
lukaszraczylo 21c9119bcb Add management of the retries via headers. 2023-04-10 03:12:39 +01:00
lukaszraczylo a7b385f0b7 Do not cache mutations. 2023-04-10 01:46:25 +01:00
lukaszraczylo 7d3abf5464 Replace allegro/bigcache with something lighter.
In high frequency environment the allegro bigcache caused significant
memory usage overhead. This commit replaces it with something much lighter.
2023-04-08 21:36:11 +01:00
lukaszraczylo 7af4e1f96a Memleak fix 2023-04-08 20:45:05 +01:00
lukaszraczylo 7623410bfe fixup! fixup! ... 2023-04-08 06:20:11 +01:00
lukaszraczylo 8e26d79df7 fixup! ... 2023-04-08 05:58:11 +01:00
lukaszraczylo aa25c43795 ... 2023-04-08 05:44:23 +01:00
lukaszraczylo dfb082ad51 fixup! fixup! fixup! Avoid unnecessary copying of the whole struct. 2023-04-08 05:19:57 +01:00
lukaszraczylo ce736958c6 fixup! fixup! Avoid unnecessary copying of the whole struct. 2023-04-08 05:18:30 +01:00
lukaszraczylo 4472041b21 fixup! Avoid unnecessary copying of the whole struct. 2023-04-08 05:17:09 +01:00
lukaszraczylo b81e612f51 Avoid unnecessary copying of the whole struct. 2023-04-08 05:14:14 +01:00
lukaszraczylo 3c508eaf15 fixup! fixup! Finally - fixed the cache version for single query if its enabled via header. 2023-04-08 03:34:32 +01:00
lukaszraczylo 4d0c04cb26 fixup! Finally - fixed the cache version for single query if its enabled via header. 2023-04-08 03:29:35 +01:00
lukaszraczylo 55182f2f3d Finally - fixed the cache version for single query if its enabled via header. 2023-04-08 03:25:33 +01:00
lukaszraczylo 841c136e71 fixup! fixup! Add randomisation test for cache concurrency. 2023-04-08 03:14:53 +01:00
lukaszraczylo 833517f35c fixup! Add randomisation test for cache concurrency. 2023-04-08 03:13:30 +01:00
lukaszraczylo 07e4f75a0a Add randomisation test for cache concurrency. 2023-04-08 03:05:55 +01:00
lukaszraczylo b8044c5d67 Fix concurrency issue. 2023-04-08 03:01:19 +01:00
lukaszraczylo cf3076d701 Update documentation. 2023-04-08 02:11:46 +01:00
lukaszraczylo 5a222aa316 fixup! Add missing retry mechanism. I have no idea how this was missed, but ho hum. 2023-04-08 02:09:18 +01:00
lukaszraczylo d843238d46 Add missing retry mechanism.
I have no idea how this was missed, but ho hum.
2023-04-08 01:59:28 +01:00
lukaszraczylo b9cd614ae4 Revert default output to string for backwards compatibility. 2023-04-08 01:38:55 +01:00
lukaszraczylo eef4e39611 Add ability to set the output for query. 2023-04-08 01:38:29 +01:00
lukaszraczylo d764980390 Add mapstring output and update the README. 2023-04-08 01:36:56 +01:00
lukaszraczylo 689b1efc43 Bugfixes + ability to set values per query. 2023-04-08 00:54:48 +01:00
lukaszraczylo 23f88a91d2 Update dependencies. 2023-04-07 17:42:29 +01:00
lukaszraczylo 664a692c2d Fix test for error message. 2023-04-07 16:06:57 +01:00
lukaszraczylo 2a628a7da7 Add retry mechanism. 2023-04-07 15:45:43 +01:00
lukaszraczylo e0913f8824 New minor release after complete rewrite.
Brings neat and testable codebase with minimum allocations in mind.
2023-04-07 15:27:44 +01:00
lukaszraczylo 6a8f99f6ef Micro optimisations. 2023-03-31 16:00:39 +01:00
lukaszraczylo a5c4643a5e Update dependencies and use realigned structs. 2023-03-31 15:31:42 +01:00
lukaszraczylo 42e1afbf77 Fix the ancient bug where client wasn't able to use non-https endpoints. 2023-02-26 11:38:20 +00:00
lukaszraczylo 9e8df7e297 Clean the dependencies. 2023-02-26 10:53:38 +00:00
lukaszraczylo 405d159eb9 Use dialer to automagically pick between https and http endpoints. 2023-02-26 10:53:06 +00:00
lukaszraczylo b3e594fc4f Update documentation. 2023-02-03 15:59:44 +00:00
lukaszraczylo 32d9e0bd80 Update dependencies. 2023-02-02 10:38:48 +00:00
lukaszraczylo e706b07488 fixup! Add cache per query. 2022-12-20 10:30:04 +00:00
lukaszraczylo 32091fc8cd Add cache per query. 2022-12-20 10:26:05 +00:00
lukaszraczylo edc695fe2b Update dependencies. 2022-12-19 11:25:18 +00:00
lukaszraczylo bd0c5ec373 Update logging package to bring a bit of a speed improvement. 2022-12-19 11:22:58 +00:00
lukaszraczylo 952749d7ff Update dependencies, switch got avast retry. 2022-12-05 12:22:12 +00:00
lukaszraczylo 277ef21263 fixup! Update dependencies. 2022-12-05 12:09:42 +00:00
lukaszraczylo 4980aeb28a Update dependencies. 2022-12-05 11:58:28 +00:00
lukaszraczylo 91f65ebd46 Update dependencies. 2022-10-17 14:47:28 +01:00
lukaszraczylo d5655ae2fa Well.. This escalated quickly. 2022-06-08 19:33:34 +01:00
lukaszraczylo 9b65ba069f Change retry library to something more useful. 2022-06-08 19:07:26 +01:00
lukaszraczylo feed2f36c2 fixup! fixup! fixup! fixup! fixup! Add retries functionality. 2022-06-08 17:36:47 +01:00
lukaszraczylo 966d9ccd0a fixup! fixup! fixup! fixup! Add retries functionality. 2022-06-08 15:51:29 +01:00
lukaszraczylo 3232498e94 fixup! fixup! fixup! Add retries functionality. 2022-06-08 15:42:28 +01:00
lukaszraczylo 3cda8e267b fixup! fixup! Add retries functionality. 2022-06-08 15:40:49 +01:00
lukaszraczylo 1393f4d7cf fixup! Add retries functionality. 2022-06-08 13:48:09 +01:00
lukaszraczylo d4328fdbb7 Add retries functionality. 2022-06-08 13:25:06 +01:00
27 changed files with 1441 additions and 686 deletions
+1 -1
View File
@@ -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
View File
@@ -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 \
+65 -24
View File
@@ -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,9 +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`
* `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
@@ -43,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
@@ -77,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()
+148
View File
@@ -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)
}
+148
View File
@@ -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))
})
}
}
+117
View File
@@ -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
}
+16 -16
View File
@@ -1,27 +1,27 @@
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/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.15
github.com/rs/zerolog v1.26.1
github.com/stretchr/testify v1.7.1
golang.org/x/net v0.0.0-20220526153639-5463443f8c37
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.13 // 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
github.com/tidwall/gjson v1.14.0 // 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/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.19.0 // indirect
golang.org/x/text v0.15.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+39 -63
View File
@@ -1,73 +1,49 @@
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/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.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/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.26.1 h1:/ihwxqH+4z8UxyI70wM1z9yCvkWcfz/a3mj48k/Zngc=
github.com/rs/zerolog v1.26.1/go.mod h1:/wSSJWX7lVrsOwlbyTRSOJvqRlc+WjWlfes+CiJ+tmc=
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/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/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-20210809222454-d867a43fc93e/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.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.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=
-105
View File
@@ -1,105 +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
}
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(),
}
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
View File
@@ -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
View File
@@ -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
View File
@@ -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())
})
}
}
+54
View File
@@ -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
}
+96
View File
@@ -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
}
+31
View File
@@ -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")
+36
View File
@@ -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
}
+9
View File
@@ -0,0 +1,9 @@
package gql
import "testing"
func Benchmark_newConnection(b *testing.B) {
for i := 0; i < b.N; i++ {
NewConnection()
}
}
+21
View File
@@ -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)
})
}
}
-77
View File
@@ -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)
}
+75 -87
View File
@@ -1,118 +1,106 @@
package gql
import (
"bytes"
"crypto/md5"
"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"`
}
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
}
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
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")
}
defer io.Copy(ioutil.Discard, httpResponse.Body)
defer httpResponse.Body.Close()
b.Logger.Debug("Compiled query", map[string]interface{}{"query": compiledQuery})
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")
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")
}
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
}
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()})
}
}
returnedValue, err := q.decodeResponse(rv)
return returnedValue, err
}
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
View File
@@ -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 -170
View File
@@ -1,196 +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 listUserBots {
tbl_bots(limit: 1) {
bot_name
}
}`,
queryVariables: nil,
},
wantResult: `{"tbl_bots":[{"bot_name":"ThetaGuardianBot"}]}`,
wantErr: false,
},
{
name: "Valid query, with cache empty",
endpoint: "https://hasura.local/v1/graphql",
isLocal: true,
cache_enabled: true,
args: args{
queryContent: `query listUserBots {
tbl_bots(limit: 1) {
bot_name
}
}`,
queryVariables: nil,
},
wantResult: `{"tbl_bots":[{"bot_name":"ThetaGuardianBot"}]}`,
wantErr: false,
},
{
name: "Valid query, with cache filled",
endpoint: "https://hasura.local/v1/graphql",
isLocal: true,
cache_enabled: true,
args: args{
queryContent: `query listUserBots {
tbl_bots(limit: 1) {
bot_name
}
}`,
queryVariables: nil,
},
wantResult: `{"tbl_bots":[{"bot_name":"ThetaGuardianBot"}]}`,
wantErr: false,
},
{
name: "Invalid query",
endpoint: "https://hasura.local/v1/graphql",
isLocal: true,
args: args{
queryContent: `query listUserBots {
tbl_botz(limit: 1) {
bot_name
}
}`,
queryVariables: nil,
},
wantResult: ``,
wantErr: true,
},
{
name: "Valid query to https endpoint",
endpoint: "https://web-dev.telegram-bot.app/v1/graphql",
isLocal: false,
args: args{
queryContent: `query packages_prices {
available_packages: tbl_available_packages(where: {enabled: {_eq: true}}) {
package_name
package_discount
package_price
package_size
package_type
}
}`,
queryVariables: nil,
},
wantResult: `{"available_packages":[{"package_discount":0,"package_name":"media_1000","package_price":750,"package_size":1000,"package_type":"media"},{"package_discount":5,"package_name":"media_5000","package_price":3563,"package_size":5000,"package_type":"media"},{"package_discount":10,"package_name":"media_10000","package_price":6750,"package_size":10000,"package_type":"media"},{"package_discount":15,"package_name":"media_25000","package_price":15938,"package_size":25000,"package_type":"media"},{"package_discount":0,"package_name":"voice_500","package_price":1000,"package_size":500,"package_type":"voice"},{"package_discount":10,"package_name":"voice_1000","package_price":1800,"package_size":1000,"package_type":"voice"},{"package_discount":15,"package_name":"voice_2500","package_price":4250,"package_size":2500,"package_type":"voice"}]}`,
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)
})
}
}
+37
View File
@@ -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
View File
@@ -1,8 +1,9 @@
version: 1
force:
major: 1
minor: 0
minor: 2
patch: 0
commit: 3217edf37fd1765c4f2594e3ba122a6a9c251296
wording:
minor:
- minor-release
+46
View File
@@ -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"`
}
+27
View File
@@ -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))
}