Compare commits

...
40 Commits
Author SHA1 Message Date
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
24 changed files with 1957 additions and 730 deletions
+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 -27
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,12 +33,38 @@ Therefore, I present you the simple client to which you can copy & paste your gr
### Environment variables
* `GRAPHQL_ENDPOINT` - Your GraphQL endpoint. Default: `http://127.0.0.1:9090/v1/graphql`
* `GRAPHQL_CACHE` - Should the query cache be enabled? Default: `false`
* `GRAPHQL_CACHE_ENABLED` - Should the query cache be enabled? Default: `false`
* `GRAPHQL_CACHE_TTL` - Cache TTL in seconds for SELECT type of queries. Default: `5`
* `LOG_LEVEL` - Logging level. Default: `info`
* `RETRIES_ENABLE` - Should retries be enabled? Default: `false`
* `RETRIES_NUMBER` - Number of retries: Default: `1`
* `RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250`
* `GRAPHQL_OUTPUT` - Output format. Default: `string`, available: `byte`, `string`, `mapstring`
* `LOG_LEVEL` - Logging level. Default: `info` available: `debug`, `info`, `warn`, `error`
* `GRAPHQL_RETRIES_ENABLE` - Should retries be enabled? Default: `true`
* `GRAPHQL_RETRIES_NUMBER` - Number of retries: Default: `1`
* `GRAPHQL_RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250`
### Modifiers on the fly
* `gql.SetEndpoint('your-endpoint-url')` - modifies endpoint, without the need to set the environment variable
* `gql.SetOutput('byte')` - modifies output format, without the need to set the environment variable
### Cache
You have two options to enable the cache:
* Use `GRAPHQL_CACHE_ENABLED` environment variable which will enable the cache globally. It may be desired if you want to use the cache for all queries.
* Add `gqlcache: true` header for your query which will enable the cache for this query only with `GRAPHQL_CACHE_TTL` TTL.
* You can check the list of supported per-query modifiers below
Example:
```go
// following values passed as headers will modify behaviour of the query
// and disregard settings provided via environment variables
headers := map[string]interface{}{
...
"gqlcache": true, // sets the cache as on for this query only
"gqlretries": false, // disables retries for this query only
}
```
### Example reader code
@@ -46,32 +75,39 @@ import (
graphql "github.com/lukaszraczylo/go-simple-graphql"
)
headers := map[string]interface{}{
"x-hasura-user-id": 37,
"x-hasura-user-uuid": "bde1962e-b42e-1212-ac10-d43fa27f44a5",
}
variables := map[string]interface{}{
"fileHash": "123deadc0w321",
}
query := `query searchFileKnown($fileHash: String) {
tbl_file_scans(where: {file_hash: {_eq: $fileHash}}) {
racy
violence
virus
func main() {
headers := map[string]interface{}{
"x-hasura-user-id": 37,
"x-hasura-user-uuid": "bde1962e-b42e-1212-ac10-d43fa27f44a5",
}
}`
gql := graphql.NewConnection()
result, err := gql.Query(query, variables, headers)
if err != nil {
fmt.Println("Query error", err)
return
variables := map[string]interface{}{
"fileHash": "123deadc0w321",
}
query := `query searchFileKnown($fileHash: String) {
tbl_file_scans(where: {file_hash: {_eq: $fileHash}}) {
racy
violence
virus
}
}`
gql := graphql.NewConnection()
result, err := gql.Query(query, variables, headers)
if err != nil {
fmt.Println("Query error", err)
return
}
fmt.Println(result)
}
fmt.Println(result)
```
### Tips
* Connection handler ( `gql := graphql.NewConnection()` ) should be created once and reused in the application especially if you run dozens of queries per second. It will allow you also to use cache and http2 to its full potential.
**Result**
```json
@@ -80,7 +116,9 @@ fmt.Println(result)
## Working with results
I'm using an amazing library [tidwall/gjson](https://github.com/tidwall/gjson) to parse the results and extract the information required in further steps and I strongly recommend this approach as the easiest and close to painless, for example:
Currently attempting to switch to the fork of the [`ask` library](https://github.com/lukaszraczylo/ask)
Before, I used an amazing library [tidwall/gjson](https://github.com/tidwall/gjson) to parse the results and extract the information required in further steps and I strongly recommend this approach as the easiest and close to painless, for example:
```go
result := gjson.Get(result, "tbl_user_group_admins.0.is_admin").Bool()
+124
View File
@@ -0,0 +1,124 @@
package gql
import (
"crypto/tls"
"fmt"
"time"
"github.com/akyoto/cache"
"github.com/lukaszraczylo/go-simple-graphql/utils/logger"
"golang.org/x/net/http2"
"log"
"net/http"
"os"
"strings"
"github.com/gookit/goutil/envutil"
)
func NewConnection() *BaseClient {
b := &BaseClient{
client: &http.Client{},
MaxGoRoutines: -1,
LoggingLevel: logger.Warn,
LoggerColorful: true,
}
if b.LoggerWriter == nil {
b.LoggerWriter = log.New(os.Stdout, "", log.LstdFlags)
}
b.endpoint = envutil.Getenv("GRAPHQL_ENDPOINT", "https://api.github.com/graphql")
b.responseType = envutil.Getenv("GRAPHQL_OUTPUT", "string")
b.validate = envutil.GetBool("GRAPHQL_VALIDATE", false)
b.cache.enabled = envutil.GetBool("GRAPHQL_CACHE_ENABLED", true)
b.cache.ttl = envutil.GetInt("GRAPHQL_CACHE_TTL", 60)
b.retries.enabled = envutil.GetBool("GRAPHQL_RETRIES_ENABLED", true)
b.retries.max = envutil.GetInt("GRAPHQL_RETRIES_MAX", 1)
b.retries.delay = envutil.GetInt("GRAPHQL_RETRIES_DELAY", 300)
b.enableCache()
b_tmp_log_level := envutil.Getenv("LOG_LEVEL", "info")
switch b_tmp_log_level {
case "silent":
b.LoggingLevel = logger.Silent
case "error":
b.LoggingLevel = logger.Error
case "warn":
b.LoggingLevel = logger.Warn
case "info":
b.LoggingLevel = logger.Info
case "debug":
b.LoggingLevel = logger.Debug
}
b.Logger = NewLogger(b.LoggerWriter, logger.Config{
Colorful: b.LoggerColorful,
LogLevel: b.LoggingLevel,
})
var httpClient *http.Client
httpTransport := &http.Transport{
MaxIdleConns: 10,
IdleConnTimeout: 15 * time.Second,
ResponseHeaderTimeout: 15 * time.Second,
DisableKeepAlives: false,
}
if strings.HasPrefix(b.endpoint, "http://") {
httpClient = &http.Client{
Transport: httpTransport,
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
} else {
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,
}
httpClient = &http.Client{
Transport: http2Transport,
}
}
b.client = httpClient
b.Logger.Debug(b, "graphQL client initialized;", "endpoint", b.endpoint, "responseType", b.responseType, "validate", b.validate, "cache", b.cache.enabled, "cacheTTL", b.cache.ttl, "maxGoRoutines", b.MaxGoRoutines, "loggingLevel", b_tmp_log_level, "loggerColorful", b.LoggerColorful)
b.Logger.Info(b, "graphQL client initialized;")
return b
}
func (b *BaseClient) SetEndpoint(endpoint string) {
b.endpoint = endpoint
}
func (b *BaseClient) SetOutput(output string) {
b.responseType = output
}
func (b *BaseClient) enableCache() {
var err error
b.cache.client = cache.New(time.Duration(b.cache.ttl) * time.Second * 2)
if err != nil {
fmt.Println(">> Error while creating cache client;", "error", err.Error())
panic(err)
}
}
func (b *BaseClient) disableCache() {
b.Logger.Debug(b, "Disabling cache")
b.cache.enabled = false
}
+155
View File
@@ -0,0 +1,155 @@
package gql
import (
"os"
"testing"
"github.com/lukaszraczylo/go-simple-graphql/utils/logger"
"github.com/stretchr/testify/assert"
)
func TestNewConnection(t *testing.T) {
tests := []struct {
want *BaseClient
cache bool
endpoint string
output string
name string
}{
{
name: "Test NewConnection https",
endpoint: "https://api.github.com/graphql",
output: "string",
cache: true,
want: &BaseClient{
endpoint: "https://api.github.com/graphql",
responseType: "string",
},
},
{
name: "Test NewConnection http",
endpoint: "http://api.github.com/graphql",
output: "mapstring",
cache: false,
want: &BaseClient{
endpoint: "http://api.github.com/graphql",
responseType: "mapstring",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// temporarily set GRAPHQL_ENDPOINT env variable to https://api.github.com/graphql
// and GRAPHQL_OUTPUT to string
current := os.Getenv("GRAPHQL_ENDPOINT")
defer os.Setenv("GRAPHQL_ENDPOINT", current)
os.Setenv("GRAPHQL_ENDPOINT", tt.endpoint)
got := NewConnection()
got.SetEndpoint(tt.endpoint)
got.SetOutput(tt.output)
if tt.cache {
got.enableCache()
} else {
got.disableCache()
}
assert.Equal(t, tt.want.endpoint, got.endpoint)
assert.Equal(t, tt.want.responseType, got.responseType)
assert.Equal(t, tt.cache, got.cache.enabled)
})
}
}
func TestNewConnectionLogLevels(t *testing.T) {
tests := []struct {
want *BaseClient
name string
env string
}{
{
name: "Test NewConnection with log level set to debug",
env: "debug",
want: &BaseClient{
endpoint: "https://api.github.com/graphql",
responseType: "string",
LoggingLevel: logger.Debug,
},
},
{
name: "Test NewConnection with log level set to info",
env: "info",
want: &BaseClient{
endpoint: "https://api.github.com/graphql",
responseType: "string",
LoggingLevel: logger.Info,
},
},
{
name: "Test NewConnection with log level set to warn",
env: "warn",
want: &BaseClient{
endpoint: "https://api.github.com/graphql",
responseType: "string",
LoggingLevel: logger.Warn,
},
},
{
name: "Test NewConnection with log level set to error",
env: "error",
want: &BaseClient{
endpoint: "https://api.github.com/graphql",
responseType: "string",
LoggingLevel: logger.Error,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// temporarily set GRAPHQL_ENDPOINT env variable to https://api.github.com/graphql
// and GRAPHQL_OUTPUT to string
current := os.Getenv("GRAPHQL_ENDPOINT")
defer os.Setenv("GRAPHQL_ENDPOINT", current)
os.Setenv("GRAPHQL_ENDPOINT", "https://api.github.com/graphql")
current_log_level := os.Getenv("LOG_LEVEL")
defer os.Setenv("LOG_LEVEL", current_log_level)
os.Setenv("LOG_LEVEL", tt.env)
got := NewConnection()
assert.Equal(t, tt.want.endpoint, got.endpoint)
assert.Equal(t, tt.want.responseType, got.responseType)
assert.Equal(t, tt.want.LoggingLevel, got.LoggingLevel)
})
}
}
func TestBaseClient_SetEndpoint(t *testing.T) {
type fields struct {
}
type args struct {
endpoint string
}
tests := []struct {
name string
fields fields
args args
want string
}{
{
name: "Test SetEndpoint",
args: args{
endpoint: "https://potato/graphql",
},
want: "https://potato/graphql",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := NewConnection()
b.SetEndpoint(tt.args.endpoint)
assert.Equal(t, tt.want, b.endpoint)
})
}
}
+14
View File
@@ -0,0 +1,14 @@
package gql
func (c *BaseClient) cacheLookup(hash string) []byte {
if c.cache.client != nil {
obj, found := c.cache.client.Get(hash)
if found {
c.Logger.Debug(c, "Cache hit;", "hash", hash)
return obj.([]byte)
}
// if error is not nil it means that hash is not present in cache
c.Logger.Debug(c, "Cache miss;", "hash", hash)
}
return nil
}
+16 -18
View File
@@ -1,31 +1,29 @@
module github.com/lukaszraczylo/go-simple-graphql
go 1.18
go 1.20
require (
github.com/allegro/bigcache/v3 v3.1.0
github.com/avast/retry-go v3.0.0+incompatible
github.com/akyoto/cache v1.0.6
github.com/avast/retry-go/v4 v4.5.0
github.com/gookit/goutil v0.6.12
github.com/json-iterator/go v1.1.12
github.com/lukaszraczylo/pandati v0.0.15
github.com/rs/zerolog v1.28.0
github.com/sethvargo/go-retry v0.2.4
github.com/stretchr/testify v1.7.1
golang.org/x/net v0.4.0
github.com/stretchr/testify v1.8.4
golang.org/x/net v0.17.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/imdario/mergo v0.3.13 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/wI2L/jsondiff v0.3.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect
gopkg.in/yaml.v3 v3.0.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9 // indirect
golang.org/x/sync v0.4.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/term v0.13.0 // indirect
golang.org/x/text v0.13.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+39 -46
View File
@@ -1,62 +1,55 @@
github.com/allegro/bigcache/v3 v3.1.0 h1:H2Vp8VOvxcrB91o86fUSVJFqeuz8kpyyB02eH3bSzwk=
github.com/allegro/bigcache/v3 v3.1.0/go.mod h1:aPyh7jEvrog9zAwx5N7+JUQX5dZTSGpxF1LAR4dr35I=
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/akyoto/cache v1.0.6 h1:5XGVVYoi2i+DZLLPuVIXtsNIJ/qaAM16XT0LaBaXd2k=
github.com/akyoto/cache v1.0.6/go.mod h1:WfxTRqKhfgAG71Xh6E3WLpjhBtZI37O53G4h5s+3iM4=
github.com/avast/retry-go/v4 v4.5.0 h1:QoRAZZ90cj5oni2Lsgl2GW8mNTnUCnmpx/iKpwVisHg=
github.com/avast/retry-go/v4 v4.5.0/go.mod h1:7hLEXp0oku2Nir2xBAsg0PTphp9z71bN5Aq1fboC3+I=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/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/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.12 h1:73vPUcTtVGXbhSzBOFcnSB1aJl7Jq9np3RAE50yIDZc=
github.com/gookit/goutil v0.6.12/go.mod h1:g6krlFib8xSe3G1h02IETowOtrUGpAmetT8IevDpvpM=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/lukaszraczylo/pandati v0.0.15 h1:1BNtbmhIQbKgs1Gm9WlH3CMms+TgkgxZcU7utHbjloY=
github.com/lukaszraczylo/pandati v0.0.15/go.mod h1:5E0mqlNgdOps6FW2X4tPilr+3B8s9JGWFgA6ZVFixyQ=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
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.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/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.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/sethvargo/go-retry v0.2.4 h1:T+jHEQy/zKJf5s95UkguisicE0zuF9y7+/vgz08Ocec=
github.com/sethvargo/go-retry v0.2.4/go.mod h1:1afjQuvh7s4gflMObvjLPaWgluLLyhA1wmVZ6KLpICw=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
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.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/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/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/wI2L/jsondiff v0.3.0 h1:iTzQ9u/d86GE9RsBzVHX88f2EA1vQUboHwLhSQFc1s4=
github.com/wI2L/jsondiff v0.3.0/go.mod h1:y1IMzNNjlSsk3IUoJdRJO7VRBtzMvRgyo4Vu0LdHpTc=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
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-20221126150942-6ab00d035af9 h1:yZNXmy+j/JpX19vZkVktWqAo7Gny4PBWYYK3zskGpx4=
golang.org/x/exp v0.0.0-20221126150942-6ab00d035af9/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
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/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-140
View File
@@ -1,140 +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"
bigcache "github.com/allegro/bigcache/v3"
jsoniter "github.com/json-iterator/go"
"github.com/lukaszraczylo/go-simple-graphql/pkg/logging"
retry "github.com/sethvargo/go-retry"
"golang.org/x/net/http2"
)
var (
json = jsoniter.ConfigCompatibleWithStandardLibrary
)
type GraphQL struct {
Endpoint string
HttpClient *http.Client
Log *logging.LogConfig
Cache bool // Enable caching for read queries
CacheStore *bigcache.BigCache
RetriesEnable bool
RetriesNumber int
RetriesDelay time.Duration
BackoffSetup retry.Backoff
}
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: 5 * time.Second,
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
},
},
Log: logging.NewLogger(),
Cache: setCacheEnabled(),
CacheStore: setupCache(),
RetriesEnable: false,
RetriesNumber: 1,
RetriesDelay: 250,
BackoffSetup: nil,
}
var err error
retriesEnable, retriesEnableExists := os.LookupEnv("RETRIES_ENABLE")
if retriesEnableExists {
g.RetriesEnable, err = strconv.ParseBool(retriesEnable)
if err != nil {
panic("Invalid value for RETRIES_ENABLE")
}
retriesNumber, retriesNumberExists := os.LookupEnv("RETRIES_NUMBER")
if retriesNumberExists {
g.RetriesNumber, err = strconv.Atoi(retriesNumber)
if err != nil {
panic("Invalid value for RETRIES_NUMBER")
}
}
retriesDelay, retriesDelayExists := os.LookupEnv("RETRIES_DELAY")
if retriesDelayExists {
retDel, err := strconv.Atoi(retriesDelay)
if err != nil {
panic("Invalid value for RETRIES_DELAY")
}
g.RetriesDelay = time.Duration(retDel) * time.Millisecond
}
}
g.BackoffSetup = retry.NewExponential(g.RetriesDelay)
g.BackoffSetup = retry.WithMaxRetries(uint64(g.RetriesNumber), g.BackoffSetup)
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)
})
}
}
+141
View File
@@ -0,0 +1,141 @@
package gql
import (
"fmt"
"reflect"
"time"
"github.com/lukaszraczylo/go-simple-graphql/utils/logger"
"github.com/gookit/goutil/timex"
)
func NewLogger(writer Writer, config logger.Config) Logger {
colorfulPrint := "%s[%s]%s %%s %%s"
closeColor, debugColor, infoColor, warnColor, errorColor := "\033[0m", "\033[1;34m", "\033[1;32m", "\033[1;33m", "\033[1;31m"
if !config.Colorful {
closeColor, debugColor, infoColor, warnColor, errorColor = "", "", "", "", ""
}
debugStr := fmt.Sprintf(colorfulPrint, debugColor, "DEBUG", closeColor)
infoStr := fmt.Sprintf(colorfulPrint, infoColor, "INFO", closeColor)
warnStr := fmt.Sprintf(colorfulPrint, warnColor, "WARN", closeColor)
errStr := fmt.Sprintf(colorfulPrint, errorColor, "ERROR", closeColor)
return &logging{
Writer: writer,
Config: config,
infoStr: infoStr,
warnStr: warnStr,
errStr: errStr,
debugStr: debugStr,
}
}
func setDate() string {
tx := time.Now()
return timex.DateFormat(tx, "Y-m-d H:I:S")
}
func (l logging) Debug(client *BaseClient, msg string, data ...any) {
if l.LogLevel >= logger.Debug {
t := setDate()
msg, data = censorSensitiveInfo(client, msg, data...)
if client != nil {
l.Printf(l.debugStr, fmt.Sprintf("[%s] %s", t, msg), sprint(data...))
} else {
l.Printf(l.debugStr, msg, sprint(data...))
}
}
}
func (l logging) Info(client *BaseClient, msg string, data ...any) {
if l.LogLevel >= logger.Info {
t := setDate()
msg, data = censorSensitiveInfo(client, msg, data...)
if client != nil {
l.Printf(l.infoStr, fmt.Sprintf("[%s] %s", t, msg), sprint(data...))
} else {
l.Printf(l.infoStr, msg, sprint(data...))
}
}
}
func (l logging) Warn(client *BaseClient, msg string, data ...any) {
if l.LogLevel >= logger.Warn {
t := setDate()
msg, data = censorSensitiveInfo(client, msg, data...)
if client != nil {
l.Printf(l.warnStr, fmt.Sprintf("[%s] %s", t, msg), sprint(data...))
} else {
l.Printf(l.warnStr, msg, sprint(data...))
}
}
}
func (l logging) Error(client *BaseClient, msg string, data ...any) {
if l.LogLevel >= logger.Error {
t := setDate()
msg, data = censorSensitiveInfo(client, msg, data...)
if client != nil {
l.Printf(l.errStr, fmt.Sprintf("[%s] %s", t, msg), sprint(data...))
} else {
l.Printf(l.errStr, msg, sprint(data...))
}
}
}
type Logger interface {
Debug(*BaseClient, string, ...any)
Info(*BaseClient, string, ...any)
Warn(*BaseClient, string, ...any)
Error(*BaseClient, string, ...any)
}
type Writer interface {
Printf(string, ...any)
}
type logging struct {
Writer
debugStr string
infoStr string
warnStr string
errStr string
logger.Config
}
func censorSensitiveInfo(client *BaseClient, a string, b ...any) (string, []any) {
// if client == nil || client.Token == "" {
// return a, b
// }
// tokenFixed := strings.Split(client.Token, ":")[1]
// for t := range b {
// if b[t] != nil {
// rV := reflect.TypeOf(b[t]).Kind()
// if rV == reflect.String {
// b[t] = strings.ReplaceAll(b[t].(string), tokenFixed, strings.Repeat("X", len(tokenFixed)))
// }
// }
// }
// a = strings.ReplaceAll(a, tokenFixed, strings.Repeat("X", len(tokenFixed)))
// return a, b
return a, b
}
func sprint(a ...any) string {
for t := range a {
if a[t] != nil {
rV := reflect.TypeOf(a[t]).Kind()
if rV == reflect.Ptr {
a[t] = reflect.ValueOf(a[t]).Elem().Interface()
}
rV = reflect.TypeOf(a[t]).Kind()
if rV == reflect.Struct || rV == reflect.Map || rV == reflect.Slice {
a[t], _ = logger.Serialize(a[t])
}
}
}
r := fmt.Sprintln(a...)
r = r[:len(r)-1]
return r
}
-72
View File
@@ -1,72 +0,0 @@
package logging
import (
"fmt"
stdlog "log"
"os"
"time"
"github.com/rs/zerolog"
)
type LogConfig struct {
DefaultOutput *os.File
Logger zerolog.Logger
}
func NewLogger() *LogConfig {
log := new(LogConfig)
dll := os.Getenv("LOG_LEVEL")
if dll == "debug" {
zerolog.SetGlobalLevel(zerolog.DebugLevel)
} else if dll == "warn" {
zerolog.SetGlobalLevel(zerolog.WarnLevel)
} else if dll == "error" {
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
} else {
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) Log(level zerolog.Level, message string, v ...map[string]interface{}) {
contextLog := l.Logger.WithLevel(level)
if len(v) > 0 {
for _, m := range v {
for k, v := range m {
if v != nil && k != "" {
contextLog.Str(k, fmt.Sprintf("%v", v))
}
}
}
}
// contextLog.Str("_stack_trace", pandati.Trace())
contextLog.Msg(message)
}
func (l *LogConfig) Critical(message string, v ...map[string]interface{}) {
l.Log(zerolog.FatalLevel, message, v...)
}
func (l *LogConfig) Error(message string, v ...map[string]interface{}) {
l.Log(zerolog.ErrorLevel, message, v...)
}
func (l *LogConfig) Warning(message string, v ...map[string]interface{}) {
l.Log(zerolog.WarnLevel, message, v...)
}
func (l *LogConfig) Info(message string, v ...map[string]interface{}) {
l.Log(zerolog.InfoLevel, message, v...)
}
func (l *LogConfig) Debug(message string, v ...map[string]interface{}) {
l.Log(zerolog.DebugLevel, message, v...)
}
+121 -103
View File
@@ -1,134 +1,152 @@
package gql
import (
"bytes"
"crypto/md5"
"errors"
"context"
"fmt"
"io/ioutil"
"net/http"
"strconv"
retry "github.com/avast/retry-go"
"github.com/lukaszraczylo/pandati"
"github.com/gookit/goutil/strutil"
"github.com/lukaszraczylo/go-simple-graphql/utils/helpers"
jsoniter "github.com/json-iterator/go"
)
type requestBase struct {
Query string `json:"query"`
Variables interface{} `json:"variables"`
}
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type queryResults struct {
Data interface{} `json:"data"`
Errors []struct {
Message interface{} `json:"message"`
} `json:"errors"`
}
func (g *GraphQL) queryBuilder(queryContent string, queryVariables interface{}) ([]byte, error) {
var qb = &requestBase{
Query: queryContent,
Variables: queryVariables,
}
j2, err := json.Marshal(qb)
func (c *BaseClient) convertToJson(v any) []byte {
json, err := json.Marshal(v)
if err != nil {
g.Log.Critical("Unable to marshal the query", map[string]interface{}{"_error": err.Error(), "_query": queryContent, "_variables": queryVariables})
return []byte{}, err
c.Logger.Error(c, "Can't convert to json;", "error", err.Error())
return nil
}
return j2, err
return json
}
func (g *GraphQL) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (responseContent string, err error) {
g.Log.Debug("Query details", map[string]interface{}{"_query": queryContent, "_variables": queryVariables})
query, err := g.queryBuilder(queryContent, queryVariables)
if err != nil {
g.Log.Error("Unable to build the query", map[string]interface{}{"_error": err.Error()})
return "", err
}
func (c *BaseClient) NewQuery(q ...any) *Query {
query := &Query{}
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
if len(q) > 0 {
for _, v := range q {
switch val := v.(type) {
case string:
query.query = val
case map[string]interface{}:
query.variables = val
case context.Context:
query.context = val
}
} else {
g.Log.Debug("Unable to find the query in the cache", map[string]interface{}{"_query": queryHash, "_error": err.Error()})
}
}
httpRequest, err := http.NewRequest("POST", g.Endpoint, bytes.NewBuffer(query))
// httpRequest.Header.Set("Content-Type", "application/json")
if err != nil {
g.Log.Error("Unable to create the request", map[string]interface{}{"_error": err.Error()})
return "", err
query.compiledQuery = c.convertToJson(request{
Query: query.query,
Variables: query.variables,
})
c.Logger.Debug(c, "Clearing previously prepared variables and query")
query.variables = nil
if query.context == nil {
query.context = context.Background()
}
for header, value := range queryHeaders {
httpRequest.Header.Add(fmt.Sprintf("%v", header), fmt.Sprintf("%v", value))
if c.validate {
c.Logger.Warn(c, "Validating query is not active")
}
var httpResponse *http.Response
query.query = ""
c.Logger.Debug(c, "Query prepared;", "query", helpers.BytesToString(query.compiledQuery))
err = retry.Do(
func() error {
httpResponse, err = g.HttpClient.Do(httpRequest)
return query
}
if err != nil {
g.Log.Debug("Unable to send the query RETRY", map[string]interface{}{"_error": err.Error()})
return err
}
defer httpResponse.Body.Close()
// Query is a function that sends a query to the server
// It takes 3 arguments:
// 1. queryContent: the query string
// 2. queryVariables: the variables for the query
// 3. queryHeaders: the headers for the query
// It looks a bit weird because of the backward compatibility
if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 {
return err
}
func (c *BaseClient) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (any, error) {
body, err = ioutil.ReadAll(httpResponse.Body)
if err != nil {
g.Log.Debug("Unable to read the response", map[string]interface{}{"_error": err.Error()})
return err
}
compiledQuery := c.NewQuery(queryContent, queryVariables)
parseQueryHeaders, enabledCache, headersModified, enabledRetries := compiledQuery.parseQueryHeaders(queryHeaders)
err = json.Unmarshal(body, &queryResult)
if err != nil {
g.Log.Debug("Unable to unmarshal the query", map[string]interface{}{"_error": err.Error()})
return err
}
var should_cache, retries_enabled bool
var queryHash string
if headersModified {
should_cache = enabledCache
retries_enabled = enabledRetries
} else {
should_cache = c.cache.enabled
retries_enabled = c.retries.enabled
}
// if queryContent does not start with `query` then we don't want to cache it
// because it's probably a mutation or subscription.
if !strutil.HasPrefix(queryContent, "query") {
should_cache = false
}
if should_cache {
queryHash = strutil.Md5(fmt.Sprintf("%s-%+v", compiledQuery.compiledQuery, queryHeaders))
}
q := &queryExecutor{
client: c,
query: compiledQuery.compiledQuery,
headers: parseQueryHeaders,
context: context.Background(),
should_cache: should_cache,
retries_enabled: retries_enabled,
hash: queryHash,
}
q.execute()
defer q.done()
return q.result.data, q.result.errors
}
func (c *BaseClient) decodeResponse(jsonData []byte) any {
switch c.responseType {
case "mapstring":
var response map[string]interface{}
err := json.Unmarshal(jsonData, &response)
if err != nil {
c.Logger.Error(c, "Error while converting to map[string]interface{};", "error", err.Error())
return nil
},
)
if !pandati.IsZero(queryResult.Errors) {
g.Log.Error("Query returned error", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_error": fmt.Sprintf("%v", queryResult.Errors), "_response_code": httpResponse.StatusCode})
return "", fmt.Errorf("%v", queryResult.Errors)
}
if pandati.IsZero(queryResult.Data) {
g.Log.Error("Query returned no data", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_response_code": httpResponse.StatusCode})
return "", errors.New("Query returned no data")
}
responseContent, err = json.MarshalToString(queryResult.Data)
if err != nil {
g.Log.Error("Invalid data result", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_result": responseContent, "_response_code": httpResponse.StatusCode})
return "", 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()})
}
}
return response
case "string":
return helpers.BytesToString(jsonData)
case "byte":
return jsonData
default:
c.Logger.Error(c, "Unknown response type", "response", c.responseType)
return nil
}
}
return
func (q *Query) parseQueryHeaders(queryHeaders map[string]interface{}) (returnHeaders map[string]interface{}, cache_enabled bool, headers_modified bool, retries_enabled bool) {
returnHeaders = make(map[string]interface{})
var err error
for k, v := range queryHeaders {
if k == "gqlcache" {
cache_enabled, err = strconv.ParseBool(fmt.Sprintf("%v", v))
if err != nil {
panic(fmt.Sprintf("Unable to parse gqlcache value %s", err.Error()))
}
headers_modified = true
continue
}
if k == "gqlretries" {
retries_enabled, _ = strconv.ParseBool(fmt.Sprintf("%v", v))
continue
}
returnHeaders[k] = v
}
return returnHeaders, cache_enabled, headers_modified, retries_enabled
}
-69
View File
@@ -1,69 +0,0 @@
package gql
// 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,
// },
// }
// 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()
// })
// }
// }
+538 -187
View File
@@ -1,219 +1,570 @@
package gql
import (
"fmt"
"os"
"math/rand"
"reflect"
"testing"
"github.com/stretchr/testify/assert"
)
func (suite *TestSuite) Test_GraphQL_queryBuilder() {
func TestBaseClient_convertToJson(t *testing.T) {
type fields struct {
}
type args struct {
queryContent string
queryVariables interface{}
v any
}
tests := []struct {
name string
name string
fields fields
args args
want []byte
}{
{
name: "Test convertToJson",
fields: fields{},
args: args{
v: map[string]interface{}{
"query": "query { hello }",
},
},
want: []byte(`{"query":"query { hello }"}`),
},
{
name: "Test convertToJson with variables",
fields: fields{},
args: args{
v: map[string]interface{}{
"query": "query { hello }",
"variables": map[string]interface{}{"name": "John"},
},
},
want: []byte(`{"query":"query { hello }","variables":{"name":"John"}}`),
},
{
name: "Test NewQuery with modifier",
fields: fields{},
args: args{
v: map[string]interface{}{
"query": "query @cached(ttl: 120) { hello }",
"variables": map[string]interface{}{"name": "John"},
},
},
want: []byte(`{"query":"query @cached(ttl: 120) { hello }","variables":{"name":"John"}}`),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &BaseClient{}
if got := c.convertToJson(tt.args.v); !reflect.DeepEqual(got, tt.want) {
t.Errorf("BaseClient.convertToJson() = %v, want %v", got, tt.want)
}
})
}
}
func TestBaseClient_NewQuery(t *testing.T) {
type fields struct {
}
type args struct {
q []any
}
tests := []struct {
fields fields
want *Query
name string
args args
}{
{
name: "Test NewQuery",
fields: fields{},
args: args{
q: []any{
"query { hello }",
},
},
want: &Query{
compiledQuery: []byte(`{"variables":null,"query":"query { hello }"}`),
},
},
{
name: "Test NewQuery with variables",
fields: fields{},
args: args{
q: []any{
"query { hello }",
map[string]interface{}{"name": "John"},
},
},
want: &Query{
compiledQuery: []byte(`{"variables":{"name":"John"},"query":"query { hello }"}`),
},
},
{
name: "Test NewQuery with modifier",
fields: fields{},
args: args{
q: []any{
"query @cached(ttl: 120) { hello }",
map[string]interface{}{"name": "John"},
},
},
want: &Query{
compiledQuery: []byte(`{"variables":{"name":"John"},"query":"query @cached(ttl: 120) { hello }"}`),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewConnection()
gotQuery := c.NewQuery(tt.args.q...)
assert.Equal(t, tt.want.query, gotQuery.query)
assert.Equal(t, tt.want.variables, gotQuery.variables)
assert.Equal(t, tt.want.compiledQuery, gotQuery.compiledQuery)
})
}
}
func TestBaseClient_decodeResponse(t *testing.T) {
type fields struct {
responseType string
}
type args struct {
jsonData []byte
}
tests := []struct {
fields fields
want any
name string
args args
}{
{
name: "Test decodeResponse - string",
fields: fields{
responseType: "string",
},
args: args{
jsonData: []byte(`{"data":{"hello":"world"}}`),
},
want: `{"data":{"hello":"world"}}`,
},
{
name: "Test decodeResponse - byte",
fields: fields{
responseType: "byte",
},
args: args{
jsonData: []byte(`{"data":{"hello":"world"}}`),
},
want: []byte(`{"data":{"hello":"world"}}`),
},
{
name: "Test decodeResponse - mapstring",
fields: fields{
responseType: "mapstring",
},
args: args{
jsonData: []byte(`{"data":{"hello":"world"}}`),
},
want: map[string]interface{}{
"data": map[string]interface{}{
"hello": "world",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewConnection()
c.responseType = tt.fields.responseType
if got := c.decodeResponse(tt.args.jsonData); !reflect.DeepEqual(got, tt.want) {
t.Errorf("BaseClient.decodeResponse() = %v, want %v", got, tt.want)
}
})
}
}
func TestBaseClient_Query(t *testing.T) {
type fields struct {
graphql_endpoint string
repeat int
}
type args struct {
queryVariables interface{}
queryHeaders map[string]interface{}
queryContent string
}
tests := []struct {
want any
args args
want []byte
name string
fields fields
wantErr bool
}{
{
name: "Valid query",
args: args{
queryContent: `query listUserBots { tbl_bots { bot_name } }`,
queryVariables: nil,
name: "Test Query - failing",
fields: fields{
graphql_endpoint: "https://spacex-production.up.railway.app/",
},
want: []byte(`{"query":"query listUserBots { tbl_bots { bot_name } }","variables":null}`),
wantErr: false,
args: args{
queryContent: "query { hello }",
},
want: ``,
wantErr: true,
},
{
name: "Valid query with variables",
args: args{
queryContent: `query listUserBots { tbl_bots { bot_name } }`,
queryVariables: map[string]interface{}{"user_id": 1},
name: "Test Query - success",
fields: fields{
graphql_endpoint: "https://spacex-production.up.railway.app/",
},
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://telegram-bot.app/v1/graphql",
isLocal: true,
cache_enabled: false,
args: args{
queryContent: `query packages_prices {
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
aggregate {
count
}
queryContent: `query Dragons {
dragons {
name
}
}`,
queryVariables: nil,
},
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
wantErr: false,
},
{
name: "Valid query, no cache #2",
endpoint: "https://telegram-bot.app/v1/graphql",
isLocal: true,
cache_enabled: false,
args: args{
queryContent: `query packages_prices {
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
aggregate {
count
}
}
}`,
queryVariables: nil,
},
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
wantErr: false,
},
{
name: "Valid query, with cache empty",
endpoint: "https://telegram-bot.app/v1/graphql",
isLocal: true,
cache_enabled: true,
args: args{
queryContent: `query packages_prices {
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
aggregate {
count
}
}
}`,
queryVariables: nil,
},
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
wantErr: false,
},
{
name: "Valid query, with cache filled",
endpoint: "https://telegram-bot.app/v1/graphql",
isLocal: true,
cache_enabled: true,
args: args{
queryContent: `query packages_prices {
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
aggregate {
count
}
}
}`,
queryVariables: nil,
},
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
wantErr: false,
},
{
name: "Invalid query",
endpoint: "https://telegram-bot.app/v1/graphql",
isLocal: true,
args: args{
queryContent: `query packages_pricez {
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
aggregate {
count
}
}`,
queryVariables: nil,
},
wantResult: ``,
wantErr: true,
},
{
name: "Valid query to https endpoint",
endpoint: "https://telegram-bot.app/v1/graphql",
isLocal: false,
args: args{
queryContent: `query packages_prices {
sub_group_packages_aggregate(where: {enabled: {_eq: true}}) {
aggregate {
count
}
}
}`,
queryVariables: nil,
},
wantResult: `{"sub_group_packages_aggregate":{"aggregate":{"count":4}}}`,
wantErr: false,
},
{
name: "Valid query to github endpoint",
endpoint: "https://api.github.com/graphql",
isLocal: false,
args: args{
queryContent: `query {
repository(name: "semver-generator", owner: "lukaszraczylo", followRenames: true) {
releases(last: 2) {
nodes {
tag {
name
}
}
}
}
}`,
queryVariables: nil,
queryHeaders: map[string]interface{}{
"Authorization": "Bearer " + os.Getenv("GITHUB_TOKEN"),
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
"content-type": "application/json",
},
},
wantResult: `{"repository":{"releases":{"nodes":[`,
wantErr: false,
wantErr: false,
want: `{"dragons":[{"name":"Dragon 1"},{"name":"Dragon 2"}]}`,
},
{
name: "Test Query - success",
fields: fields{
repeat: 2,
graphql_endpoint: "https://spacex-production.up.railway.app/",
},
args: args{
queryContent: `query Dragons {
dragons {
name
first_flight
}
}`,
queryHeaders: map[string]interface{}{
"x-apollo-operation-name": "Missions-Potato-Test-Golang-Cached",
"content-type": "application/json",
},
},
wantErr: false,
want: `{"dragons":[{"first_flight":"2010-12-08","name":"Dragon 1"},{"first_flight":"2019-03-02","name":"Dragon 2"}]}`,
},
}
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)
t.Run(tt.name, func(t *testing.T) {
c := NewConnection()
if tt.fields.graphql_endpoint != "" {
c.endpoint = tt.fields.graphql_endpoint
}
repeat := 1
if tt.fields.repeat != 0 {
repeat = tt.fields.repeat
}
for i := 0; i < repeat; i++ {
got, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
if err != nil && tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
}
}
})
}
}
/// benchmarks
func BenchmarkBaseClient_convertToJson(b *testing.B) {
c := NewConnection()
query := c.NewQuery("query { hello }")
for i := 0; i < b.N; i++ {
_ = c.convertToJson(query)
}
}
func BenchmarkNewQuery(b *testing.B) {
c := NewConnection()
for i := 0; i < b.N; i++ {
_ = c.NewQuery("query { hello }")
}
}
func BenchmarkDecodeResponse(b *testing.B) {
c := NewConnection()
for i := 0; i < b.N; i++ {
_ = c.decodeResponse([]byte(`{"data":{"hello":"world"}}`))
}
}
func BenchmarkQueryNoCache(b *testing.B) {
c := NewConnection()
c.cache.enabled = false
for i := 0; i < b.N; i++ {
_, _ = c.Query(`query MyQuery {
bots {
bot_name
}
}`, nil, nil)
}
}
func BenchmarkQueryWithCache(b *testing.B) {
c := NewConnection()
c.cache.enabled = true
for i := 0; i < b.N; i++ {
_, _ = c.Query(`query MyQuery {
bots {
bot_name
}
}`, nil, nil)
}
}
func TestBaseClient_parseQueryHeaders(t *testing.T) {
type fields struct {
}
type args struct {
queryHeaders map[string]interface{}
}
tests := []struct {
fields fields
args args
wantReturnHeaders map[string]interface{}
name string
}{
{
name: "Test parseQueryHeaders - no change",
fields: fields{},
args: args{
queryHeaders: map[string]interface{}{
"x-test-header": "test",
"x-test-header-next": true,
},
},
wantReturnHeaders: map[string]interface{}{
"x-test-header": "test",
"x-test-header-next": true,
},
},
{
name: "Test parseQueryHeaders - lib-headers removed",
fields: fields{},
args: args{
queryHeaders: map[string]interface{}{
"x-test-header": "test",
"gqlcache": true,
},
},
wantReturnHeaders: map[string]interface{}{
"x-test-header": "test",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
q := &Query{}
if gotReturnHeaders, _, _, _ := q.parseQueryHeaders(tt.args.queryHeaders); !reflect.DeepEqual(gotReturnHeaders, tt.wantReturnHeaders) {
t.Errorf("BaseClient.parseQueryHeaders() = %v, want %v", gotReturnHeaders, tt.wantReturnHeaders)
}
})
}
}
func TestBaseClient_QueryCache(t *testing.T) {
type fields struct {
graphql_endpoint string
cache_enabled bool
}
type args struct {
queryVariables interface{}
queryHeaders map[string]interface{}
queryContent string
}
tests := []struct {
want any
args args
name string
fields fields
wantErr bool
}{
{
name: "Test QueryCache - enabled",
fields: fields{
graphql_endpoint: "https://spacex-production.up.railway.app/",
cache_enabled: true,
},
args: args{
queryContent: `query Dragons {
dragons {
name
}
}`,
queryHeaders: map[string]interface{}{
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
"content-type": "application/json",
},
},
},
{
name: "Test QueryCache - disabled",
fields: fields{
graphql_endpoint: "https://spacex-production.up.railway.app/",
cache_enabled: false,
},
args: args{
queryContent: `query Dragons {
dragons {
name
}
}`,
queryHeaders: map[string]interface{}{
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
"content-type": "application/json",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewConnection()
c.cache.enabled = tt.fields.cache_enabled
if tt.fields.graphql_endpoint != "" {
c.endpoint = tt.fields.graphql_endpoint
}
_, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
if !tt.wantErr {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
})
}
}
func TestBaseClient_QueryCacheRandomizedRace(t *testing.T) {
type fields struct {
graphql_endpoint string
cache_enabled bool
}
type args struct {
queryVariables interface{}
queryHeaders map[string]interface{}
queryContent string
}
tests := []struct {
want any
args args
name string
fields fields
wantErr bool
}{
{
name: "Test QueryCache - enabled",
fields: fields{
graphql_endpoint: "https://spacex-production.up.railway.app/",
},
args: args{
queryContent: `query Dragons {
dragons {
name
}
}`,
queryHeaders: map[string]interface{}{
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
"content-type": "application/json",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewConnection()
if tt.fields.graphql_endpoint != "" {
c.endpoint = tt.fields.graphql_endpoint
}
for i := 0; i < 10; i++ {
c.cache.enabled = rand.Intn(2) == 0
go func() {
_, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
if !tt.wantErr {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
}()
}
})
}
}
func TestBaseClient_QueryCacheRandomizedRaceViaHeader(t *testing.T) {
type fields struct {
graphql_endpoint string
cache_enabled bool
}
type args struct {
queryVariables interface{}
queryHeaders map[string]interface{}
queryContent string
}
tests := []struct {
want any
args args
name string
fields fields
wantErr bool
}{
{
name: "Test QueryCache - enabled",
fields: fields{
graphql_endpoint: "https://spacex-production.up.railway.app/",
},
args: args{
queryContent: `query Dragons {
dragons {
name
}
}`,
queryHeaders: map[string]interface{}{
"x-apollo-operation-name": "Missions-Potato-Test-Golang",
"content-type": "application/json",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := NewConnection()
if tt.fields.graphql_endpoint != "" {
c.endpoint = tt.fields.graphql_endpoint
}
for i := 0; i < 10; i++ {
go func() {
tt.args.queryHeaders["gqlcache"] = rand.Intn(2) == 0
_, err := c.Query(tt.args.queryContent, tt.args.queryVariables, tt.args.queryHeaders)
if !tt.wantErr {
assert.NoError(t, err)
} else {
assert.Error(t, err)
}
}()
}
assert.Contains(t, gotResult, tt.wantResult)
})
}
fmt.Println(g.CacheStore.Capacity())
}
+115
View File
@@ -0,0 +1,115 @@
package gql
import (
"bytes"
"fmt"
"io"
"net/http"
"time"
"github.com/avast/retry-go/v4"
)
func (c *BaseClient) executeQuery(query []byte, headers any) (result any, err error) {
var queryResult queryResults
httpRequest, err := http.NewRequest("POST", c.endpoint, bytes.NewBuffer(query))
if err != nil {
c.Logger.Error(c, "Error while creating http request;", "error", err.Error())
return
}
for key, value := range headers.(map[string]interface{}) {
httpRequest.Header.Add(key, fmt.Sprintf("%s", value))
}
var retries_available = c.retries.max
if !c.retries.enabled {
retries_available = 1
}
var httpResponse *http.Response
err = retry.Do(
func() error {
httpResponse, err = c.client.Do(httpRequest)
if err != nil {
c.Logger.Error(c, "Error while executing http request;", "error", err.Error())
return err
}
defer io.Copy(io.Discard, httpResponse.Body) // equivalent to `cp body /dev/null`
defer httpResponse.Body.Close()
if httpResponse.StatusCode <= 200 && httpResponse.StatusCode >= 204 {
return err
}
body, err := io.ReadAll(httpResponse.Body)
if err != nil {
c.Logger.Error(c, "Error while reading http response;", "error", err.Error())
return err
}
err = json.Unmarshal(body, &queryResult)
if err != nil {
c.Logger.Error(c, "Error while unmarshalling http response;", "error", err.Error())
return err
}
return nil
},
retry.OnRetry(func(n uint, err error) {
c.Logger.Error(c, "Retrying query", "error", err.Error())
}),
retry.Attempts(uint(retries_available)),
retry.DelayType(retry.BackOffDelay),
retry.Delay(time.Duration(c.retries.delay)*time.Second),
retry.LastErrorOnly(true),
)
if len(queryResult.Errors) > 0 {
return nil, fmt.Errorf("%v", queryResult.Errors)
}
return queryResult.Data, nil
}
func (q *queryExecutor) execute() {
if q.should_cache {
cachedResponse := q.client.cacheLookup(q.hash)
if cachedResponse != nil {
q.client.Logger.Debug(q.client, "Found cached response")
q.result.data = q.client.decodeResponse(cachedResponse)
return // return cached response
} else {
q.client.Logger.Debug(q.client, "No cached response found")
}
}
if q.retries_enabled {
q.client.Logger.Debug(q.client, "Retries enabled")
q.client.retries.enabled = true
}
response, err := q.client.executeQuery(q.query, q.headers)
if err != nil {
q.client.Logger.Error(q.client, "Error while executing query;", "error", err.Error())
q.result.errors = err
return
}
jsonData, err := json.Marshal(response)
if err != nil {
q.client.Logger.Error(q.client, "Error while converting to json;", "error", err.Error())
q.result.errors = err
return
}
if q.should_cache {
q.client.cache.client.Set(q.hash, jsonData, time.Duration(q.client.cache.ttl)*time.Second)
}
q.result.data = q.client.decodeResponse(jsonData)
}
func (q *queryExecutor) done() {
q = nil
}
+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: 1
patch: 0
commit: 6a8f99f6ef8f65a303dad74c272ff12bf9fe526f
wording:
minor:
- minor-release
+70
View File
@@ -0,0 +1,70 @@
package gql
import (
"context"
"net/http"
"github.com/akyoto/cache"
"github.com/lukaszraczylo/go-simple-graphql/utils/logger"
)
type cacheStore struct {
client *cache.Cache
ttl int
enabled bool
}
type retriesConfig struct {
enabled bool
max int
delay int
}
type BaseClient struct {
Logger Logger
LoggerWriter Writer
client *http.Client
endpoint string
responseType string
cache cacheStore
MaxGoRoutines int
LoggingLevel logger.LogLevel
LoggerColorful bool
validate bool
retries retriesConfig
}
type Query struct {
context context.Context
variables map[string]any `json:"variables"`
query string `json:"query"`
compiledQuery []byte `json:"compiledQuery"`
}
type queryExecutor struct {
result queryExecutorResult
context context.Context
client *BaseClient
headers map[string]interface{}
hash string
query []byte
should_cache bool
retries_enabled bool
}
type queryExecutorResult struct {
data interface{}
errors error
}
type request struct {
Variables any `json:"variables"`
Query string `json:"query"`
}
type queryResults struct {
Data interface{} `json:"data"`
Errors []struct {
Message interface{} `json:"message"`
} `json:"errors"`
}
+13
View File
@@ -0,0 +1,13 @@
package helpers
import (
"unsafe"
)
func StringToBytes(s string) []byte {
return unsafe.Slice(unsafe.StringData(s), len(s))
}
func BytesToString(b []byte) string {
return unsafe.String(unsafe.SliceData(b), len(b))
}
+70
View File
@@ -0,0 +1,70 @@
package helpers
import (
"reflect"
"testing"
)
func TestStringToBytes(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want []byte
}{
{
name: "TestStringToBytes",
args: args{
s: "hello world",
},
want: []byte("hello world"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := StringToBytes(tt.args.s); !reflect.DeepEqual(got, tt.want) {
t.Errorf("StringToBytes() = %v, want %v", got, tt.want)
}
})
}
}
func TestBytesToString(t *testing.T) {
type args struct {
b []byte
}
tests := []struct {
name string
want string
args args
}{
{
name: "TestBytesToString",
args: args{
b: []byte("hello world"),
},
want: "hello world",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := BytesToString(tt.args.b); got != tt.want {
t.Errorf("BytesToString() = %v, want %v", got, tt.want)
}
})
}
}
func BenchmarkStringToBytes(b *testing.B) {
for i := 0; i < b.N; i++ {
StringToBytes("hello world")
}
}
func BenchmarkBytesToString(b *testing.B) {
for i := 0; i < b.N; i++ {
BytesToString([]byte("hello world"))
}
}
+66
View File
@@ -0,0 +1,66 @@
package logger
import (
"encoding/json"
"reflect"
)
const (
reset = "\033[0m"
redBold = "\033[1;31m"
greenBold = "\033[1;32m"
yellowBold = "\033[1;33m"
blueBold = "\033[1;34m"
)
type LogLevel int
const (
Silent LogLevel = iota
Error
Warn
Info
Debug
)
type Config struct {
Colorful bool
LogLevel LogLevel
}
func Serialize(v interface{}) ([]byte, error) {
b, err := json.Marshal(internalSerialize(v))
if err != nil {
return nil, err
}
return b, nil
}
func internalSerialize(v interface{}) interface{} {
if v != nil {
switch reflect.TypeOf(v).Kind() {
case reflect.Struct:
r := make(map[string]interface{})
name := reflect.TypeOf(v).Name()
if name != "" {
r["_"] = reflect.TypeOf(v).String()
}
for i := 0; i < reflect.ValueOf(v).NumField(); i++ {
r[reflect.TypeOf(v).Field(i).Name] = internalSerialize(reflect.ValueOf(v).Field(i).Interface())
}
return r
case reflect.Ptr:
if !reflect.ValueOf(v).IsNil() {
return internalSerialize(reflect.ValueOf(v).Elem().Interface())
}
case reflect.Slice:
var r []interface{}
tmpSlice := reflect.ValueOf(v)
for i := 0; i < tmpSlice.Len(); i++ {
r = append(r, internalSerialize(tmpSlice.Index(i).Interface()))
}
return r
}
}
return v
}
+84
View File
@@ -0,0 +1,84 @@
package logger
import "testing"
type Person struct {
Name string
Age int
}
func TestSerializeStruct(t *testing.T) {
person := Person{Name: "Alice", Age: 30}
expected := `{"_":"logger.Person","Name":"Alice","Age":30}`
actual, err := Serialize(person)
if err != nil {
t.Errorf("Error: %v", err)
}
if actual != expected {
t.Errorf("Expected: %v\nActual: %v", expected, actual)
}
}
func TestSerializeNilPointer(t *testing.T) {
var person *Person
expected := `null`
actual, err := Serialize(person)
if err != nil {
t.Errorf("Error: %v", err)
}
if actual != expected {
t.Errorf("Expected: %v\nActual: %v", expected, actual)
}
}
func TestSerializeNonNilPointer(t *testing.T) {
person := &Person{Name: "Alice", Age: 30}
expected := `{"_":"logger.Person","Name":"Alice","Age":30}`
actual, err := Serialize(person)
if err != nil {
t.Errorf("Error: %v", err)
}
if actual != expected {
t.Errorf("Expected: %v\nActual: %v", expected, actual)
}
}
func TestSerializeSlice(t *testing.T) {
people := []Person{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 40},
}
expected := `[{"_":"logger.Person","Name":"Alice","Age":30},{"_":"logger.Person","Name":"Bob","Age":40}]`
actual, err := Serialize(people)
if err != nil {
t.Errorf("Error: %v", err)
}
if actual != expected {
t.Errorf("Expected: %v\nActual: %v", expected, actual)
}
}
func TestSerializeSliceOfPointers(t *testing.T) {
people := []*Person{
{Name: "Alice", Age: 30},
{Name: "Bob", Age: 40},
}
expected := `[{"_":"logger.Person","Name":"Alice","Age":30},{"_":"logger.Person","Name":"Bob","Age":40}]`
actual, err := Serialize(people)
if err != nil {
t.Errorf("Error: %v", err)
}
if actual != expected {
t.Errorf("Expected: %v\nActual: %v", expected, actual)
}
}
func TestSerializeUnsupportedType(t *testing.T) {
type UnsupportedType chan int
unsupportedType := make(UnsupportedType)
_, err := Serialize(unsupportedType)
if err == nil {
t.Errorf("Expected an error, but got nil")
}
}
+121
View File
@@ -0,0 +1,121 @@
package ordered_map
import (
"bytes"
jsoniter "github.com/json-iterator/go"
)
var json = jsoniter.ConfigCompatibleWithStandardLibrary
type OrderedMap struct {
values map[string]interface{}
keys []string
}
func New() *OrderedMap {
o := OrderedMap{}
o.keys = []string{}
o.values = map[string]interface{}{}
return &o
}
func (o *OrderedMap) Set(key string, value interface{}) {
_, exists := o.values[key]
if !exists {
o.keys = append(o.keys, key)
}
o.values[key] = value
}
func (o OrderedMap) MarshalJSON() ([]byte, error) {
var buf bytes.Buffer
buf.WriteByte('{')
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", "")
for i, k := range o.keys {
if i > 0 {
buf.WriteByte(',')
}
buf.WriteByte('"')
buf.WriteString(k)
buf.WriteString(`":`)
customEncoder := &customJsonEncoder{buf: &buf}
if err := customEncoder.Encode(o.values[k]); err != nil {
return nil, err
}
}
buf.WriteByte('}')
return buf.Bytes(), nil
}
type customJsonEncoder struct {
buf *bytes.Buffer
}
func (e *customJsonEncoder) Encode(v interface{}) error {
if v == nil {
e.buf.WriteString("null")
return nil
}
switch val := v.(type) {
case *OrderedMap:
data, err := json.Marshal(val.values)
if err != nil {
return err
}
e.buf.Write(data)
default:
data, err := json.Marshal(val)
if err != nil {
return err
}
e.buf.Write(data)
}
return nil
}
func (o OrderedMap) Keys() []string {
return o.keys
}
func (o OrderedMap) Values() []interface{} {
var values []interface{}
for _, k := range o.keys {
values = append(values, o.values[k])
}
return values
}
func (o *OrderedMap) Delete(key string) {
delete(o.values, key)
for i, k := range o.keys {
if k == key {
o.keys = append(o.keys[:i], o.keys[i+1:]...)
break
}
}
}
func (o OrderedMap) Exists(key string) bool {
_, exists := o.values[key]
return exists
}
func (o *OrderedMap) UnmarshalJSON(data []byte) error {
var values map[string]interface{}
err := json.Unmarshal(data, &values)
if err != nil {
return err
}
o.keys = []string{}
o.values = map[string]interface{}{}
for k, v := range values {
o.Set(k, v)
}
return nil
}
+164
View File
@@ -0,0 +1,164 @@
package ordered_map
import (
"reflect"
"testing"
)
func TestNew(t *testing.T) {
tests := []struct {
want *OrderedMap
name string
}{
{
name: "TestNew",
want: &OrderedMap{
keys: []string{},
values: map[string]interface{}{},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := New(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("New() = %v, want %v", got, tt.want)
}
})
}
}
func TestOrderedMap_Set(t *testing.T) {
type fields struct {
values map[string]interface{}
keys []string
}
type args struct {
value interface{}
key string
}
tests := []struct {
args args
name string
fields fields
}{
{
name: "TestOrderedMap_Set",
fields: fields{
values: map[string]interface{}{},
keys: []string{},
},
args: args{},
},
{
name: "TestOrderedMap_Set2",
fields: fields{
values: map[string]interface{}{
"hello": "world",
},
keys: []string{"hello"},
},
args: args{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := &OrderedMap{
values: tt.fields.values,
keys: tt.fields.keys,
}
o.Set(tt.args.key, tt.args.value)
for _, key := range o.keys {
if _, ok := o.values[key]; !ok {
t.Errorf("OrderedMap.Set() key not found in values map")
}
}
})
}
}
func TestOrderedMap_MarshalJSON(t *testing.T) {
type fields struct {
values map[string]interface{}
keys []string
}
tests := []struct {
name string
fields fields
want []byte
wantErr bool
}{
{
name: "TestOrderedMap_MarshalJSON",
fields: fields{
values: map[string]interface{}{
"hello": "world",
},
keys: []string{"hello"},
},
want: []byte(`{"hello":"world"}`),
wantErr: false,
},
{
name: "TestOrderedMap_MarshalJSON2",
fields: fields{
values: map[string]interface{}{
"hello": "world",
"foo": "bar",
},
keys: []string{"hello", "foo"},
},
want: []byte(`{"hello":"world","foo":"bar"}`),
wantErr: false,
},
{
name: "TestOrderedMap_MarshalJSON_with_error",
fields: fields{
values: map[string]interface{}{
"hello": "world",
"foo": make(chan int),
},
keys: []string{"hello", "foo"},
},
want: nil,
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
o := OrderedMap{
values: tt.fields.values,
keys: tt.fields.keys,
}
got, err := o.MarshalJSON()
if (err != nil) != tt.wantErr {
t.Errorf("OrderedMap.MarshalJSON() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("OrderedMap.MarshalJSON() = %v, want %v", got, tt.want)
}
})
}
}
func BenchmarkOrderedMapTest(b *testing.B) {
m := New()
for i := 0; i < b.N; i++ {
m.Set("hello", "world")
}
}
func BenchmarkOrderedMarshalJSON(b *testing.B) {
m := New()
for i := 0; i < b.N; i++ {
m.Set("hello", "world")
m.MarshalJSON()
}
}
func BenchmarkMapTest(b *testing.B) {
m := make(map[string]interface{})
for i := 0; i < b.N; i++ {
m["hello"] = "world"
}
}