mirror of
https://github.com/lukaszraczylo/go-simple-graphql.git
synced 2026-10-12 00:45:31 +00:00
Compare commits
114
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7f9999928 | ||
|
|
0c5902e1c6 | ||
|
|
1fd4fe4f73 | ||
|
|
e84eff3233 | ||
|
|
3d89a77565 | ||
|
|
4a4b24fa45 | ||
|
|
d2453fe2f5 | ||
|
|
28eec46f4c | ||
|
|
63e449ef77 | ||
|
|
50cfc5f98d | ||
|
|
4a575ac9c8 | ||
|
|
f54cedaeb3 | ||
|
|
1080083384 | ||
|
|
941aadb869 | ||
|
|
8174ae6f9c | ||
|
|
992a29da85 | ||
|
|
ecf2c06401 | ||
|
|
fb0785229c | ||
|
|
8ac446db41 | ||
|
|
586af64d70 | ||
|
|
50c1033ad1 | ||
|
|
7a791965ca | ||
|
|
05b057c67c | ||
|
|
9c729d814a | ||
|
|
9d5e2122b8 | ||
|
|
a19f1d257a | ||
|
|
563c952c78 | ||
|
|
2f259a4b3f | ||
|
|
957857537a | ||
|
|
4ea4bbb6cc | ||
|
|
36ef16ad3d | ||
|
|
42170f9b8e | ||
|
|
4f438b7eac | ||
|
|
569867d04b | ||
|
|
9a58df7a54 | ||
|
|
4e60a90ce4 | ||
|
|
2f4c8cc5d8 | ||
|
|
154233df14
|
||
|
|
29afbd08fd
|
||
|
|
a107ee953b
|
||
|
|
58094f85c7
|
||
|
|
ac957ac95b
|
||
|
|
ef4ab45424
|
||
|
|
4fc826c1f6
|
||
|
|
3217edf37f
|
||
|
|
ae96059f61
|
||
|
|
2ce7016d7e
|
||
|
|
9da7c9bec9
|
||
|
|
ca13be5ef6
|
||
|
|
1df2ae5d16
|
||
|
|
39f489210b
|
||
|
|
21c9119bcb
|
||
|
|
a7b385f0b7
|
||
|
|
7d3abf5464
|
||
|
|
7af4e1f96a
|
||
|
|
7623410bfe
|
||
|
|
8e26d79df7
|
||
|
|
aa25c43795
|
||
|
|
dfb082ad51
|
||
|
|
ce736958c6
|
||
|
|
4472041b21
|
||
|
|
b81e612f51
|
||
|
|
3c508eaf15
|
||
|
|
4d0c04cb26
|
||
|
|
55182f2f3d
|
||
|
|
841c136e71
|
||
|
|
833517f35c
|
||
|
|
07e4f75a0a
|
||
|
|
b8044c5d67
|
||
|
|
cf3076d701
|
||
|
|
5a222aa316
|
||
|
|
d843238d46
|
||
|
|
b9cd614ae4
|
||
|
|
eef4e39611
|
||
|
|
d764980390
|
||
|
|
689b1efc43
|
||
|
|
23f88a91d2
|
||
|
|
664a692c2d
|
||
|
|
2a628a7da7
|
||
|
|
e0913f8824
|
||
|
|
6a8f99f6ef
|
||
|
|
a5c4643a5e
|
||
|
|
42e1afbf77
|
||
|
|
9e8df7e297
|
||
|
|
405d159eb9
|
||
|
|
b3e594fc4f
|
||
|
|
32d9e0bd80
|
||
|
|
e706b07488
|
||
|
|
32091fc8cd
|
||
|
|
edc695fe2b
|
||
|
|
bd0c5ec373
|
||
|
|
952749d7ff
|
||
|
|
277ef21263
|
||
|
|
4980aeb28a
|
||
|
|
91f65ebd46
|
||
|
|
d5655ae2fa
|
||
|
|
9b65ba069f
|
||
|
|
feed2f36c2
|
||
|
|
966d9ccd0a
|
||
|
|
3232498e94
|
||
|
|
3cda8e267b
|
||
|
|
1393f4d7cf
|
||
|
|
d4328fdbb7
|
||
|
|
4ac9f132a2
|
||
|
|
fb04e68b8d
|
||
|
|
40652c3fdd
|
||
|
|
f360a5cf7e
|
||
|
|
2b09da7685
|
||
|
|
aac6ca113d
|
||
|
|
c8ed2290b4
|
||
|
|
d1406eeac8
|
||
|
|
ea7ec683ef
|
||
|
|
5cd22318f1
|
||
|
|
13955da995
|
@@ -0,0 +1,73 @@
|
||||
name: Autoupdate go.mod and go.sum
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: "0 3 * * *"
|
||||
|
||||
env:
|
||||
GO_VERSION: ">=1.21"
|
||||
|
||||
jobs:
|
||||
# This job is responsible for preparation of the build
|
||||
# environment variables.
|
||||
prepare:
|
||||
name: Preparing build context
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
id: cache
|
||||
with:
|
||||
go-version: ${{env.GO_VERSION}}
|
||||
cache-dependency-path: "**/*.sum"
|
||||
|
||||
- name: Go get dependencies
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
go get ./...
|
||||
|
||||
# This job is responsible for running tests and linting the codebase
|
||||
test:
|
||||
name: "Unit testing"
|
||||
runs-on: ubuntu-latest
|
||||
container: golang:1
|
||||
needs: [prepare]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0 # Ensure full history is checked out
|
||||
token: ${{ secrets.GHCR_TOKEN }}
|
||||
|
||||
- name: Install Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{env.GO_VERSION}}
|
||||
cache-dependency-path: "**/*.sum"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install ca-certificates make -y
|
||||
update-ca-certificates
|
||||
go mod tidy
|
||||
go get -u -v ./...
|
||||
go mod tidy -v
|
||||
|
||||
- name: Run unit tests
|
||||
run: |
|
||||
CI_RUN=${CI} make test
|
||||
git config --global --add safe.directory /__w/go-simple-graphql/go-simple-graphql
|
||||
|
||||
- name: Commit changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: "Update go.mod and go.sum"
|
||||
commit_options: "--no-verify --signoff"
|
||||
file_pattern: "go.mod go.sum"
|
||||
@@ -12,6 +12,6 @@ jobs:
|
||||
shared:
|
||||
uses: telegram-bot-app/ci-scripts/.github/workflows/library-test-publish.yaml@main
|
||||
with:
|
||||
enable-code-scans: true
|
||||
enable-code-scans: false
|
||||
secrets:
|
||||
ghcr-token: ${{ secrets.GHCR_TOKEN }}
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
*.out
|
||||
*.test
|
||||
coverage.txt
|
||||
coverage.txt
|
||||
*.DS_Store
|
||||
@@ -11,11 +11,23 @@ help: ## display this help
|
||||
|
||||
.PHONY: test
|
||||
test: ## run tests on library
|
||||
@go test $(ADDITIONAL_BUILD_FLAGS) -v -cover ./...
|
||||
@LOG_LEVEL=debug go test $(ADDITIONAL_BUILD_FLAGS) -v -cover ./... -race
|
||||
|
||||
.PHONY: test-packages
|
||||
test-packages: ## run tests on packages
|
||||
@go test -v -cover ./pkg/...
|
||||
|
||||
.PHONY: all
|
||||
all: test-packages test
|
||||
all: test-packages test
|
||||
|
||||
.PHONY: update
|
||||
update: ## update dependencies
|
||||
@go get -u -v ./...
|
||||
@go mod tidy -v
|
||||
|
||||
sonar:
|
||||
sonar-scanner \
|
||||
-Dsonar.projectKey=golang-simple-graphql \
|
||||
-Dsonar.sources=. \
|
||||
-Dsonar.host.url=http://localhost:9000 \
|
||||
-Dsonar.login=555ba78b315faa07b3231a3be0f3cbb24ab33814
|
||||
@@ -9,13 +9,16 @@ Ps. It's Hasura friendly.
|
||||
- [Features](#features)
|
||||
- [Usage example](#usage-example)
|
||||
- [Environment variables](#environment-variables)
|
||||
- [Modifiers on the fly](#modifiers-on-the-fly)
|
||||
- [Cache](#cache)
|
||||
- [Example reader code](#example-reader-code)
|
||||
- [Tips](#tips)
|
||||
- [Working with results](#working-with-results)
|
||||
|
||||
## Reasoning
|
||||
|
||||
I've tried to run few graphQL clients with hasura, all of them required conversion of the data into
|
||||
the appropriate structures, causing issues with non-existing types ( thanks to Hasura ), for example `bigint` which was difficult to export.
|
||||
I've tried to run a few GraphQL clients with Hasura, all of them required conversion of the data into
|
||||
the appropriate structures, causing issues with non-existing types ( thanks to Hasura ), for example, `bigint` which was difficult to export.
|
||||
Therefore, I present you the simple client to which you can copy & paste your graphQL query, variables and you are good to go.
|
||||
|
||||
## Features
|
||||
@@ -23,13 +26,45 @@ Therefore, I present you the simple client to which you can copy & paste your gr
|
||||
* Executing GraphQL queries as they are, without types declaration
|
||||
* HTTP2 support!
|
||||
* Support for additional headers
|
||||
* Query cache
|
||||
|
||||
## Usage example
|
||||
|
||||
### Environment variables
|
||||
|
||||
* `GRAPHQL_ENDPOINT` - Your GraphQL endpoint. Default: `http://127.0.0.1:9090/v1/graphql`
|
||||
* `LOG_LEVEL` - Logging level. Default: `info`
|
||||
* `GRAPHQL_CACHE_ENABLED` - Should the query cache be enabled? Default: `false`
|
||||
* `GRAPHQL_CACHE_TTL` - Cache TTL in seconds for SELECT type of queries. Default: `5`
|
||||
* `GRAPHQL_OUTPUT` - Output format. Default: `string`, available: `byte`, `string`, `mapstring`
|
||||
* `LOG_LEVEL` - Logging level. Default: `info` available: `debug`, `info`, `warn`, `error`
|
||||
* `GRAPHQL_RETRIES_ENABLE` - Should retries be enabled? Default: `true`
|
||||
* `GRAPHQL_RETRIES_NUMBER` - Number of retries: Default: `1`
|
||||
* `GRAPHQL_RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250`
|
||||
|
||||
### Modifiers on the fly
|
||||
|
||||
* `gql.SetEndpoint('your-endpoint-url')` - modifies endpoint, without the need to set the environment variable
|
||||
* `gql.SetOutput('byte')` - modifies output format, without the need to set the environment variable
|
||||
|
||||
### Cache
|
||||
|
||||
You have two options to enable the cache:
|
||||
|
||||
* Use `GRAPHQL_CACHE_ENABLED` environment variable which will enable the cache globally. It may be desired if you want to use the cache for all queries.
|
||||
* Add `gqlcache: true` header for your query which will enable the cache for this query only with `GRAPHQL_CACHE_TTL` TTL.
|
||||
* You can check the list of supported per-query modifiers below
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
// following values passed as headers will modify behaviour of the query
|
||||
// and disregard settings provided via environment variables
|
||||
headers := map[string]interface{}{
|
||||
...
|
||||
"gqlcache": true, // sets the cache as on for this query only
|
||||
"gqlretries": false, // disables retries for this query only
|
||||
}
|
||||
```
|
||||
|
||||
### Example reader code
|
||||
|
||||
@@ -37,35 +72,42 @@ Therefore, I present you the simple client to which you can copy & paste your gr
|
||||
```go
|
||||
import (
|
||||
fmt
|
||||
gql "github.com/lukaszraczylo/simple-gql-client"
|
||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||
)
|
||||
|
||||
headers := map[string]interface{}{
|
||||
"x-hasura-user-id": 37,
|
||||
"x-hasura-user-uuid": "bde3262e-b42e-4151-ac10-d43f0bef44a5",
|
||||
}
|
||||
|
||||
variables := map[string]interface{}{
|
||||
"fileHash": "123deadc0w321",
|
||||
}
|
||||
var query = `query searchFileKnown($fileHash: String) {
|
||||
tbl_file_scans(where: {file_hash: {_eq: $fileHash}}) {
|
||||
porn
|
||||
racy
|
||||
violence
|
||||
virus
|
||||
func main() {
|
||||
headers := map[string]interface{}{
|
||||
"x-hasura-user-id": 37,
|
||||
"x-hasura-user-uuid": "bde1962e-b42e-1212-ac10-d43fa27f44a5",
|
||||
}
|
||||
}
|
||||
|
||||
graphql := NewConnection()
|
||||
result, err := graphql.Query(query, variables, nil)
|
||||
if err != nil {
|
||||
fmt.Println("Query error", err)
|
||||
return
|
||||
variables := map[string]interface{}{
|
||||
"fileHash": "123deadc0w321",
|
||||
}
|
||||
|
||||
query := `query searchFileKnown($fileHash: String) {
|
||||
tbl_file_scans(where: {file_hash: {_eq: $fileHash}}) {
|
||||
racy
|
||||
violence
|
||||
virus
|
||||
}
|
||||
}`
|
||||
|
||||
gql := graphql.NewConnection()
|
||||
result, err := gql.Query(query, variables, headers)
|
||||
if err != nil {
|
||||
fmt.Println("Query error", err)
|
||||
return
|
||||
}
|
||||
fmt.Println(result)
|
||||
}
|
||||
fmt.Println(result)
|
||||
```
|
||||
|
||||
### Tips
|
||||
|
||||
* Connection handler ( `gql := graphql.NewConnection()` ) should be created once and reused in the application especially if you run dozens of queries per second. It will allow you also to use cache and http2 to its full potential.
|
||||
|
||||
|
||||
**Result**
|
||||
|
||||
```json
|
||||
@@ -74,10 +116,12 @@ fmt.Println(result)
|
||||
|
||||
## Working with results
|
||||
|
||||
I'm using an amazing library [tidwall/gjson](https://github.com/tidwall/gjson) to parse the results and extract information required in further steps and I strongly recommend this approach as the easiest and close to painless.
|
||||
Currently attempting to switch to the fork of the [`ask` library](https://github.com/lukaszraczylo/ask)
|
||||
|
||||
Before, I used an amazing library [tidwall/gjson](https://github.com/tidwall/gjson) to parse the results and extract the information required in further steps and I strongly recommend this approach as the easiest and close to painless, for example:
|
||||
|
||||
```go
|
||||
result = gjson.Get(result, "tbl_user_group_admins.0.is_admin").Bool()
|
||||
result := gjson.Get(result, "tbl_user_group_admins.0.is_admin").Bool()
|
||||
if result {
|
||||
fmt.Println("User is an admin")
|
||||
}
|
||||
|
||||
Vendored
+180
@@ -0,0 +1,180 @@
|
||||
package libpack_cache
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CacheEntry struct {
|
||||
ExpiresAt time.Time
|
||||
Value []byte
|
||||
}
|
||||
|
||||
const shardCount = 256 // Must be power of 2
|
||||
|
||||
type shard struct {
|
||||
entries map[string]CacheEntry
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
compressPool sync.Pool
|
||||
decompressPool sync.Pool
|
||||
shards [shardCount]*shard
|
||||
globalTTL time.Duration
|
||||
}
|
||||
|
||||
// getShard returns the appropriate shard for a given key
|
||||
func (c *Cache) getShard(key string) *shard {
|
||||
hash := fnv.New32a()
|
||||
hash.Write([]byte(key))
|
||||
return c.shards[hash.Sum32()%shardCount]
|
||||
}
|
||||
|
||||
func New(globalTTL time.Duration) *Cache {
|
||||
cache := &Cache{
|
||||
globalTTL: globalTTL,
|
||||
compressPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
w := gzip.NewWriter(nil)
|
||||
return w
|
||||
},
|
||||
},
|
||||
decompressPool: sync.Pool{
|
||||
New: func() interface{} {
|
||||
r, _ := gzip.NewReader(bytes.NewReader([]byte{}))
|
||||
return r
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize shards
|
||||
for i := 0; i < shardCount; i++ {
|
||||
cache.shards[i] = &shard{
|
||||
entries: make(map[string]CacheEntry),
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
shard := c.getShard(key)
|
||||
shard.Lock()
|
||||
defer shard.Unlock()
|
||||
|
||||
compressedValue, err := c.compress(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
shard.entries[key] = CacheEntry{
|
||||
Value: compressedValue,
|
||||
ExpiresAt: time.Now().Add(ttl),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) Get(key string) ([]byte, bool) {
|
||||
shard := c.getShard(key)
|
||||
shard.RLock()
|
||||
entry, ok := shard.entries[key]
|
||||
if !ok {
|
||||
shard.RUnlock()
|
||||
return nil, false
|
||||
}
|
||||
|
||||
if entry.ExpiresAt.Before(time.Now()) {
|
||||
shard.RUnlock()
|
||||
// Clean up expired entry in background
|
||||
go func() {
|
||||
shard.Lock()
|
||||
delete(shard.entries, key)
|
||||
shard.Unlock()
|
||||
}()
|
||||
return nil, false
|
||||
}
|
||||
shard.RUnlock()
|
||||
|
||||
value, err := c.decompress(entry.Value)
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return value, true
|
||||
}
|
||||
|
||||
func (c *Cache) Delete(key string) {
|
||||
shard := c.getShard(key)
|
||||
shard.Lock()
|
||||
delete(shard.entries, key)
|
||||
shard.Unlock()
|
||||
}
|
||||
|
||||
func (c *Cache) CleanExpiredEntries() {
|
||||
now := time.Now()
|
||||
for _, shard := range c.shards {
|
||||
shard.Lock()
|
||||
for key, entry := range shard.entries {
|
||||
if entry.ExpiresAt.Before(now) {
|
||||
delete(shard.entries, key)
|
||||
}
|
||||
}
|
||||
shard.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) compress(data []byte) ([]byte, error) {
|
||||
w := c.compressPool.Get().(*gzip.Writer)
|
||||
defer c.compressPool.Put(w)
|
||||
|
||||
var buf bytes.Buffer
|
||||
w.Reset(&buf)
|
||||
if _, err := w.Write(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (c *Cache) decompress(data []byte) ([]byte, error) {
|
||||
r, ok := c.decompressPool.Get().(*gzip.Reader)
|
||||
if !ok || r == nil {
|
||||
// If r is nil or type assertion fails, create a new gzip.Reader
|
||||
var err error
|
||||
r, err = gzip.NewReader(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err // Handle the error if gzip.NewReader fails
|
||||
}
|
||||
} else {
|
||||
// Reset the existing reader with new data
|
||||
if err := r.Reset(bytes.NewReader(data)); err != nil {
|
||||
return nil, err // Handle the error if Reset fails
|
||||
}
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
// Ensure the reader is returned to the pool
|
||||
defer c.decompressPool.Put(r)
|
||||
|
||||
// Read all the data from the reader
|
||||
decompressedData, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err // Handle the error if reading fails
|
||||
}
|
||||
return decompressedData, nil
|
||||
}
|
||||
Vendored
+46
@@ -0,0 +1,46 @@
|
||||
package libpack_cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Assume that New function initializes the cache and it is defined somewhere in the libpack_cache package.
|
||||
|
||||
func BenchmarkCacheSet(b *testing.B) {
|
||||
cache := New(30 * time.Second) // Initializing the cache with a TTL of 30 seconds
|
||||
key := "benchmark-key"
|
||||
value := []byte("benchmark-value")
|
||||
|
||||
b.ResetTimer() // Reset the timer to exclude the setup time from the benchmark
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache.Set(key, value, 5*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCacheGet(b *testing.B) {
|
||||
cache := New(30 * time.Second) // Initializing the cache
|
||||
key := "benchmark-key"
|
||||
value := []byte("benchmark-value")
|
||||
cache.Set(key, value, 5*time.Second) // Pre-set a value to retrieve
|
||||
|
||||
b.ResetTimer() // Start timing
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = cache.Get(key)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCacheExpire(b *testing.B) {
|
||||
key := "benchmark-expire-key"
|
||||
value := []byte("benchmark-value")
|
||||
ttl := 5 * time.Millisecond // Setting a short TTL for quick expiration
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
cache := New(30 * time.Second)
|
||||
cache.Set(key, value, ttl)
|
||||
time.Sleep(ttl) // Wait for the key to expire
|
||||
_, _ = cache.Get(key)
|
||||
}
|
||||
}
|
||||
Vendored
+112
@@ -0,0 +1,112 @@
|
||||
package libpack_cache
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type CacheTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) SetupTest() {
|
||||
}
|
||||
|
||||
func TestCachingTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(CacheTestSuite))
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) Test_New() {
|
||||
suite.T().Run("should return a new cache", func(t *testing.T) {
|
||||
cache := New(2 * time.Second)
|
||||
suite.NotNil(cache)
|
||||
})
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) Test_CacheUse() {
|
||||
cache := New(30 * time.Second)
|
||||
tests := []struct {
|
||||
name string
|
||||
cache_value string
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
cache_value: "test1-123",
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
cache_value: "test2-123",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
cache.Set(tt.name, []byte(tt.name), 5*time.Second)
|
||||
c, ok := cache.Get(tt.name)
|
||||
suite.Equal(true, ok)
|
||||
suite.Equal(tt.name, string(c))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) Test_CacheDelete() {
|
||||
cache := New(30 * time.Second)
|
||||
tests := []struct {
|
||||
name string
|
||||
cache_value string
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
cache_value: "test1-123",
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
cache_value: "test2-123",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
cache.Set(tt.name, []byte(tt.name), 5*time.Second)
|
||||
c, ok := cache.Get(tt.name)
|
||||
suite.Equal(true, ok)
|
||||
suite.Equal(tt.name, string(c))
|
||||
cache.Delete(tt.name)
|
||||
c, ok = cache.Get(tt.name)
|
||||
suite.Equal(false, ok)
|
||||
suite.Equal("", string(c))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *CacheTestSuite) Test_CacheExpire() {
|
||||
cache := New(30 * time.Second)
|
||||
tests := []struct {
|
||||
name string
|
||||
cache_value string
|
||||
ttl time.Duration
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
cache_value: "test1-123",
|
||||
ttl: 2 * time.Second,
|
||||
},
|
||||
{
|
||||
name: "test2",
|
||||
cache_value: "test2-123",
|
||||
ttl: 5 * time.Second,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
cache.Set(tt.name, []byte(tt.name), tt.ttl)
|
||||
c, ok := cache.Get(tt.name)
|
||||
suite.Equal(true, ok)
|
||||
suite.Equal(tt.name, string(c))
|
||||
time.Sleep(tt.ttl)
|
||||
c, ok = cache.Get(tt.name)
|
||||
suite.Equal(false, ok)
|
||||
suite.Equal("", string(c))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
This is a dummy change for PR #1
|
||||
@@ -0,0 +1 @@
|
||||
This is a dummy change for PR #2
|
||||
@@ -0,0 +1 @@
|
||||
This is a dummy change for PR #3
|
||||
@@ -0,0 +1 @@
|
||||
This is a dummy change for PR #4
|
||||
@@ -0,0 +1,175 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/avast/retry-go/v4"
|
||||
"github.com/goccy/go-json"
|
||||
libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging"
|
||||
)
|
||||
|
||||
var (
|
||||
// Shared HTTP transport with optimized settings
|
||||
defaultTransport = &http.Transport{
|
||||
MaxIdleConns: 100,
|
||||
MaxIdleConnsPerHost: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
DisableCompression: false, // Enable compression for responses
|
||||
ForceAttemptHTTP2: true,
|
||||
}
|
||||
|
||||
// Shared HTTP client with timeouts
|
||||
defaultClient = &http.Client{
|
||||
Transport: defaultTransport,
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
)
|
||||
|
||||
func (qe *QueryExecutor) executeQuery() ([]byte, error) {
|
||||
// Reuse buffer from pool to avoid allocations
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
defer bufferPool.Put(buf)
|
||||
buf.Reset()
|
||||
|
||||
buf.Write(qe.Query)
|
||||
|
||||
httpRequest, err := http.NewRequest(http.MethodPost, qe.endpoint, nil)
|
||||
if err != nil {
|
||||
qe.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't create HTTP request",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for key, value := range qe.Headers {
|
||||
httpRequest.Header.Set(key, fmt.Sprint(value))
|
||||
}
|
||||
// Set Content-Type header if not already set
|
||||
if httpRequest.Header.Get("Content-Type") == "" {
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
retriesMax := 1
|
||||
if qe.Retries {
|
||||
retriesMax = qe.retries_number
|
||||
}
|
||||
|
||||
var queryResult queryResults
|
||||
err = retry.Do(
|
||||
func() error {
|
||||
// Reset buffer before each retry
|
||||
buf.Reset()
|
||||
buf.Write(qe.Query)
|
||||
|
||||
// Set the body of the request
|
||||
httpRequest.Body = io.NopCloser(bytes.NewReader(buf.Bytes()))
|
||||
httpRequest.ContentLength = int64(buf.Len())
|
||||
|
||||
// Use default client if custom client is not set
|
||||
client := qe.client
|
||||
if client == nil {
|
||||
client = defaultClient
|
||||
}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
io.Copy(io.Discard, httpResponse.Body)
|
||||
httpResponse.Body.Close()
|
||||
}()
|
||||
|
||||
if httpResponse.StatusCode < http.StatusOK || httpResponse.StatusCode >= http.StatusMultipleChoices {
|
||||
return fmt.Errorf("HTTP error - status code: %s for %s", httpResponse.Status, httpRequest.URL)
|
||||
}
|
||||
|
||||
var reader io.Reader
|
||||
encoding := httpResponse.Header.Get("Content-Encoding")
|
||||
if encoding == "gzip" {
|
||||
gzipReader, err := gzip.NewReader(httpResponse.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating gzip reader: %w", err)
|
||||
}
|
||||
defer gzipReader.Close()
|
||||
reader = gzipReader
|
||||
} else {
|
||||
reader = httpResponse.Body
|
||||
}
|
||||
|
||||
// Use buffer pool for reading response
|
||||
respBuf := bufferPool.Get().(*bytes.Buffer)
|
||||
respBuf.Reset()
|
||||
defer bufferPool.Put(respBuf)
|
||||
|
||||
_, err = io.Copy(respBuf, reader)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error reading HTTP response: %w", err)
|
||||
}
|
||||
|
||||
// Unmarshal response directly from buffer
|
||||
err = json.Unmarshal(respBuf.Bytes(), &queryResult)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unmarshalling HTTP response: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
retry.OnRetry(func(n uint, err error) {
|
||||
qe.Logger.Warning(&libpack_logger.LogMessage{
|
||||
Message: "Retrying query",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "attempt": n + 1},
|
||||
})
|
||||
}),
|
||||
retry.Attempts(uint(retriesMax)),
|
||||
retry.DelayType(retry.BackOffDelay),
|
||||
retry.Delay(time.Duration(qe.retries_delay)),
|
||||
retry.MaxDelay(10 * time.Second),
|
||||
retry.LastErrorOnly(true),
|
||||
)
|
||||
if err != nil {
|
||||
qe.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Error executing HTTP request",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(queryResult.Errors) > 0 {
|
||||
qe.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Error executing query",
|
||||
Pairs: map[string]interface{}{"error": queryResult.Errors},
|
||||
})
|
||||
return nil, fmt.Errorf("error executing query: %s", queryResult.Errors)
|
||||
}
|
||||
|
||||
if queryResult.Data == nil {
|
||||
qe.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Error executing query",
|
||||
Pairs: map[string]interface{}{"error": "no data"},
|
||||
})
|
||||
return nil, errors.New("error executing query: no data")
|
||||
}
|
||||
|
||||
// Marshal queryResult.Data
|
||||
jsonData, err := json.Marshal(queryResult.Data)
|
||||
if err != nil {
|
||||
qe.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Error marshalling query result",
|
||||
Pairs: map[string]interface{}{"error": err.Error(), "data": queryResult.Data},
|
||||
})
|
||||
return nil, fmt.Errorf("error marshalling query result: %w. Data: %s", err, queryResult.Data)
|
||||
}
|
||||
|
||||
if qe.CacheKey != "no-cache" {
|
||||
qe.cache.Set(qe.CacheKey, jsonData, qe.CacheTTL)
|
||||
}
|
||||
|
||||
return jsonData, nil
|
||||
}
|
||||
@@ -1,22 +1,24 @@
|
||||
module github.com/lukaszraczylo/go-simple-graphql
|
||||
|
||||
go 1.17
|
||||
go 1.21
|
||||
|
||||
require (
|
||||
github.com/buger/jsonparser v1.1.1
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/lukaszraczylo/pandati v0.0.10
|
||||
github.com/rs/zerolog v1.25.0
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6
|
||||
github.com/avast/retry-go/v4 v4.6.0
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/goccy/go-reflect v1.2.0
|
||||
github.com/gookit/goutil v0.6.18
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/net v0.34.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/imdario/mergo v0.3.12 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/gookit/color v1.5.4 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
golang.org/x/text v0.3.6 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sync v0.11.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/term v0.29.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -1,62 +1,34 @@
|
||||
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/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/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.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU=
|
||||
github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/lukaszraczylo/pandati v0.0.10 h1:ZXmLjxb/UYRBVyAKwXmUcJgZ2FYNwGY4YDJsQQMYAXI=
|
||||
github.com/lukaszraczylo/pandati v0.0.10/go.mod h1:1lE/x4X4LfO1yerAQoiR7AVb/wIabH/zITDGnqTdyH8=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-reflect v1.2.0 h1:O0T8rZCuNmGXewnATuKYnkL0xm6o8UNOJZd/gOkb9ms=
|
||||
github.com/goccy/go-reflect v1.2.0/go.mod h1:n0oYZn8VcV2CkWTxi8B9QjkCoq6GTtCEdfmR66YhFtE=
|
||||
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.18 h1:MUVj0G16flubWT8zYVicIuisUiHdgirPAkmnfD2kKgw=
|
||||
github.com/gookit/goutil v0.6.18/go.mod h1:AY/5sAwKe7Xck+mEbuxj0n/bc3qwrGNe3Oeulln7zBA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/rs/zerolog v1.25.0 h1:Rj7XygbUHKUlDPcVdoLyR91fJBsduXj5fRxyqIQj/II=
|
||||
github.com/rs/zerolog v1.25.0/go.mod h1:7KHcEGe0QZPOm2IE4Kpb5rTh6n1h2hIgS5OOnu1rUaI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6 h1:Z04ewVs7JhXaYkmDhBERPi41gnltfQpMWDnTnQbaCqk=
|
||||
golang.org/x/net v0.0.0-20210929193557-e81a3d93ecf6/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
github.com/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.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
|
||||
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
|
||||
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
// Package go-simple-graphql is a wrapper for GraphQL queries execution.
|
||||
// It's sole purpose is to allow developer to execute queries without specific fields / types mapping
|
||||
// which can be really confusing and even impossible whilst dealing with Hasura custom generated types.
|
||||
//
|
||||
// Library supports advanced error reporting on unsuccessful queries.
|
||||
// Library also uses HTTP2 for communication with GraphQL API if it's supported by server
|
||||
|
||||
// Environment variables:
|
||||
// GRAPHQL_ENDPOINT - GraphQL endpoint to use. Default: http://127.0.0.1:9090/v1/graphql
|
||||
// LOG_LEVEL - Log level to use. Default: INFO
|
||||
// Available log levels: DEBUG, INFO, WARN, ERROR, FATAL
|
||||
|
||||
package gql
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/lukaszraczylo/go-simple-graphql/pkg/logging"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
var (
|
||||
json = jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
)
|
||||
|
||||
type GraphQL struct {
|
||||
Endpoint string
|
||||
HttpClient *http.Client
|
||||
Log *logging.LogConfig
|
||||
}
|
||||
|
||||
func pickGraphqlEndpoint() (graphqlEndpoint string) {
|
||||
value, present := os.LookupEnv("GRAPHQL_ENDPOINT")
|
||||
if present && value != "" {
|
||||
graphqlEndpoint = value
|
||||
} else {
|
||||
graphqlEndpoint = "http://127.0.0.1:9090/v1/graphql"
|
||||
fmt.Println("Setting default endpoint", graphqlEndpoint)
|
||||
}
|
||||
return graphqlEndpoint
|
||||
}
|
||||
|
||||
func NewConnection() *GraphQL {
|
||||
return &GraphQL{
|
||||
Endpoint: pickGraphqlEndpoint(),
|
||||
HttpClient: &http.Client{
|
||||
Transport: &http2.Transport{
|
||||
DisableCompression: false,
|
||||
AllowHTTP: true,
|
||||
PingTimeout: 5 * time.Second,
|
||||
DialTLS: func(network, addr string, cfg *tls.Config) (net.Conn, error) {
|
||||
return net.Dial(network, addr)
|
||||
},
|
||||
},
|
||||
},
|
||||
Log: logging.NewLogger(),
|
||||
}
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
func mockGraphQLServerResponses(responseQuery string) (*httptest.Server, string) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||
rw.Write([]byte(responseQuery))
|
||||
}))
|
||||
serverURL := server.URL + "/v1/graphql"
|
||||
return server, serverURL
|
||||
}
|
||||
|
||||
type TestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
func (suite *TestSuite) SetupTest() {
|
||||
os.Unsetenv("GRAPHQL_ENDPOINT")
|
||||
}
|
||||
|
||||
func TestSuiteRun(t *testing.T) {
|
||||
suite.Run(t, new(TestSuite))
|
||||
}
|
||||
|
||||
func (suite *TestSuite) TestNewConnection() {
|
||||
type args struct {
|
||||
endpoint string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *GraphQL
|
||||
}{
|
||||
{
|
||||
name: "New connection: Env variable endpoint",
|
||||
args: args{
|
||||
endpoint: "http://localhost:8080/graphql",
|
||||
},
|
||||
want: &GraphQL{
|
||||
Endpoint: "http://localhost:8080/graphql",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "New connection: No env variable endpoint",
|
||||
args: args{},
|
||||
want: &GraphQL{
|
||||
Endpoint: "http://127.0.0.1:9090/v1/graphql",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
os.Setenv("GRAPHQL_ENDPOINT", tt.args.endpoint)
|
||||
got := NewConnection()
|
||||
assert.Equal(t, tt.want.Endpoint, got.Endpoint)
|
||||
})
|
||||
}
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging"
|
||||
)
|
||||
|
||||
func searchForKeysInMapStringInterface(msi map[string]interface{}, key string) interface{} {
|
||||
if msi == nil {
|
||||
return nil
|
||||
}
|
||||
return msi[key]
|
||||
}
|
||||
|
||||
func calculateHash(query *Query) string {
|
||||
hash := md5.Sum(query.JsonQuery)
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
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(&libpack_logger.LogMessage{
|
||||
Message: "Can't decode response into mapstring",
|
||||
Pairs: 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(&libpack_logger.LogMessage{
|
||||
Message: "Can't decode response",
|
||||
Pairs: map[string]interface{}{"error": "unknown response type"},
|
||||
})
|
||||
return nil, fmt.Errorf("can't decode response - unknown response type specified")
|
||||
}
|
||||
}
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/goccy/go-reflect"
|
||||
)
|
||||
|
||||
func (suite *Tests) Test_searchForKeysInMapStringInterface() {
|
||||
type args struct {
|
||||
msi map[string]interface{}
|
||||
key string
|
||||
}
|
||||
tests := []struct {
|
||||
wantValue any
|
||||
args args
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "Test_searchForKeysInMapStringInterface_exists",
|
||||
args: args{
|
||||
msi: map[string]interface{}{
|
||||
"gqlcache": true,
|
||||
"test": "test2",
|
||||
},
|
||||
key: "test",
|
||||
},
|
||||
wantValue: "test2",
|
||||
},
|
||||
{
|
||||
name: "Test_searchForKeysInMapStringInterface_not_exists",
|
||||
args: args{
|
||||
msi: map[string]interface{}{
|
||||
"gqlcache": true,
|
||||
"test": "test2",
|
||||
},
|
||||
key: "test-new",
|
||||
},
|
||||
wantValue: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(tt.wantValue, searchForKeysInMapStringInterface(tt.args.msi, tt.args.key))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *Tests) TestBaseClient_decodeResponse() {
|
||||
type args struct {
|
||||
response []byte
|
||||
}
|
||||
tests := []struct {
|
||||
want any
|
||||
name string
|
||||
setType string
|
||||
wantType string
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "TestBaseClient_decodeResponse_mapstring",
|
||||
args: args{
|
||||
response: []byte(`{"data":{"viewer":{"login":"lukaszraczylo"}}}`),
|
||||
},
|
||||
want: map[string]interface{}{
|
||||
"data": map[string]interface{}{
|
||||
"viewer": map[string]interface{}{
|
||||
"login": "lukaszraczylo",
|
||||
},
|
||||
},
|
||||
},
|
||||
setType: "mapstring",
|
||||
wantType: "map[string]interface {}",
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_decodeResponse_string",
|
||||
args: args{
|
||||
response: []byte(`{"data":{"viewer":{"login":"lukaszraczylo"}}}`),
|
||||
},
|
||||
want: `{"data":{"viewer":{"login":"lukaszraczylo"}}}`,
|
||||
setType: "string",
|
||||
wantType: "string",
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_decodeResponse_byte",
|
||||
args: args{
|
||||
response: []byte(`{"data":{"viewer":{"login":"lukaszraczylo"}}}`),
|
||||
},
|
||||
want: []byte(`{"data":{"viewer":{"login":"lukaszraczylo"}}}`),
|
||||
setType: "byte",
|
||||
wantType: "[]uint8",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
b := NewConnection()
|
||||
b.SetOutput(tt.setType)
|
||||
got, err := b.decodeResponse(tt.args.response)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("decodeResponse() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
assert.Equal(tt.wantType, reflect.TypeOf(got).String())
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging"
|
||||
"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(&libpack_logger.LogMessage{
|
||||
Message: "Using HTTP/1.1 over http",
|
||||
Pairs: nil,
|
||||
})
|
||||
} else if strings.HasPrefix(b.endpoint, "https://") {
|
||||
tlsClientConfig := &tls.Config{}
|
||||
if strings.HasPrefix(b.endpoint, "https://") {
|
||||
tlsClientConfig.InsecureSkipVerify = true
|
||||
}
|
||||
http2Transport := &http2.Transport{
|
||||
AllowHTTP: true,
|
||||
TLSClientConfig: tlsClientConfig,
|
||||
ReadIdleTimeout: 15 * time.Second,
|
||||
PingTimeout: 15 * time.Second,
|
||||
}
|
||||
http_client = &http.Client{
|
||||
Transport: http2Transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
b.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Using HTTP/2 over https",
|
||||
Pairs: nil,
|
||||
})
|
||||
} else {
|
||||
b.Logger.Critical(&libpack_logger.LogMessage{
|
||||
Message: "Invalid endpoint - neither http or https",
|
||||
})
|
||||
}
|
||||
return http_client
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package libpack_logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
const (
|
||||
_ = iota
|
||||
LEVEL_DEBUG
|
||||
LEVEL_INFO
|
||||
LEVEL_WARN
|
||||
LEVEL_ERROR
|
||||
LEVEL_FATAL
|
||||
)
|
||||
|
||||
var LevelNames = [...]string{
|
||||
"none",
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error",
|
||||
"fatal",
|
||||
}
|
||||
|
||||
const (
|
||||
defaultFormat = time.RFC3339
|
||||
defaultMinLevel = LEVEL_INFO
|
||||
defaultShowCaller = false
|
||||
)
|
||||
|
||||
var defaultOutput = os.Stdout
|
||||
|
||||
type Logger struct {
|
||||
output io.Writer
|
||||
format string
|
||||
minLogLevel int
|
||||
showCaller bool
|
||||
}
|
||||
|
||||
type LogMessage struct {
|
||||
output io.Writer
|
||||
Pairs map[string]any
|
||||
Message string
|
||||
}
|
||||
|
||||
func (m *LogMessage) String() string {
|
||||
return m.Message
|
||||
}
|
||||
|
||||
var fieldNames = map[string]string{
|
||||
"timestamp": "timestamp",
|
||||
"level": "level",
|
||||
"message": "message",
|
||||
}
|
||||
|
||||
func New() *Logger {
|
||||
return &Logger{
|
||||
format: defaultFormat,
|
||||
minLogLevel: defaultMinLevel,
|
||||
output: defaultOutput,
|
||||
showCaller: defaultShowCaller,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) SetOutput(output io.Writer) *Logger {
|
||||
l.output = output
|
||||
return l
|
||||
}
|
||||
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() any {
|
||||
return new(bytes.Buffer)
|
||||
},
|
||||
}
|
||||
|
||||
var defaultPairs = make(map[string]any)
|
||||
|
||||
func GetLogLevel(level string) int {
|
||||
for i, name := range LevelNames {
|
||||
if name == strings.ToLower(level) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return defaultMinLevel
|
||||
}
|
||||
|
||||
func (l *Logger) log(level int, m *LogMessage) {
|
||||
if m.Pairs == nil {
|
||||
m.Pairs = defaultPairs
|
||||
}
|
||||
|
||||
m.Pairs[fieldNames["timestamp"]] = time.Now().Format(l.format)
|
||||
m.Pairs[fieldNames["level"]] = LevelNames[level]
|
||||
m.Pairs[fieldNames["message"]] = m.Message
|
||||
|
||||
if l.showCaller {
|
||||
m.Pairs["caller"] = getCaller()
|
||||
}
|
||||
|
||||
buffer := bufferPool.Get().(*bytes.Buffer)
|
||||
defer bufferPool.Put(buffer)
|
||||
buffer.Reset()
|
||||
|
||||
var encoder = json.NewEncoder(buffer)
|
||||
err := encoder.Encode(m.Pairs)
|
||||
if err != nil {
|
||||
fmt.Println("Error marshalling log message:", err)
|
||||
return
|
||||
}
|
||||
|
||||
// if not running in test - use stderr and stdout, otherwise - use logger's output setting
|
||||
if flag.Lookup("test.v") != nil {
|
||||
m.output = os.Stdout
|
||||
if level >= LEVEL_ERROR {
|
||||
m.output = os.Stderr
|
||||
}
|
||||
}
|
||||
|
||||
// Use logger's output setting instead of os.Stdout or os.Stderr
|
||||
l.output.Write(buffer.Bytes())
|
||||
}
|
||||
|
||||
func (l *Logger) Debug(m *LogMessage) {
|
||||
if l.shouldLog(LEVEL_DEBUG) {
|
||||
l.log(LEVEL_DEBUG, m)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Info(m *LogMessage) {
|
||||
if l.shouldLog(LEVEL_INFO) {
|
||||
l.log(LEVEL_INFO, m)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Warn(m *LogMessage) {
|
||||
if l.shouldLog(LEVEL_WARN) {
|
||||
l.log(LEVEL_WARN, m)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Warning(m *LogMessage) {
|
||||
l.Warn(m)
|
||||
}
|
||||
|
||||
func (l *Logger) Error(m *LogMessage) {
|
||||
if l.shouldLog(LEVEL_ERROR) {
|
||||
l.log(LEVEL_ERROR, m)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Fatal(m *LogMessage) {
|
||||
if l.shouldLog(LEVEL_FATAL) {
|
||||
l.log(LEVEL_FATAL, m)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Critical(m *LogMessage) {
|
||||
l.Fatal(m)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func (l *Logger) shouldLog(level int) bool {
|
||||
return level >= l.minLogLevel
|
||||
}
|
||||
|
||||
func (l *Logger) SetFormat(format string) *Logger {
|
||||
l.format = format
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Logger) SetMinLogLevel(level int) *Logger {
|
||||
l.minLogLevel = level
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Logger) SetFieldName(field, name string) *Logger {
|
||||
fieldNames[field] = name
|
||||
return l
|
||||
}
|
||||
|
||||
func (l *Logger) SetShowCaller(show bool) *Logger {
|
||||
l.showCaller = show
|
||||
return l
|
||||
}
|
||||
|
||||
func getCaller() string {
|
||||
_, file, line, ok := runtime.Caller(3)
|
||||
if !ok {
|
||||
return "unknown:0"
|
||||
}
|
||||
file = filepath.Base(file)
|
||||
return fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
package libpack_logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Benchmark_NewLogger(b *testing.B) {
|
||||
type triggers struct {
|
||||
ModFormat struct {
|
||||
Format string
|
||||
}
|
||||
ModLevel struct {
|
||||
Level int
|
||||
}
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
triggers triggers
|
||||
}{
|
||||
{
|
||||
name: "BenchmarkNew",
|
||||
},
|
||||
{
|
||||
name: "BenchmarkNewChangeTimeFormat",
|
||||
triggers: triggers{
|
||||
ModFormat: struct{ Format string }{
|
||||
Format: time.RFC3339Nano,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BenchmarkNewChangeLogLevel",
|
||||
triggers: triggers{
|
||||
ModLevel: struct{ Level int }{
|
||||
Level: LEVEL_DEBUG,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "BenchmarkNewChangeTimeFormatAndLogLevel",
|
||||
triggers: triggers{
|
||||
ModFormat: struct{ Format string }{
|
||||
Format: time.RFC3339Nano,
|
||||
},
|
||||
ModLevel: struct{ Level int }{
|
||||
Level: LEVEL_DEBUG,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
b.Run(tt.name, func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
got := New()
|
||||
|
||||
if tt.triggers.ModFormat.Format != "" {
|
||||
got.SetFormat(tt.triggers.ModFormat.Format)
|
||||
}
|
||||
|
||||
if tt.triggers.ModLevel.Level != 0 {
|
||||
got.SetMinLogLevel(tt.triggers.ModLevel.Level)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_Log_Debug(b *testing.B) {
|
||||
output := &bytes.Buffer{}
|
||||
logger := New().SetMinLogLevel(LEVEL_DEBUG).SetOutput(output)
|
||||
msg := &LogMessage{
|
||||
Message: "debug message",
|
||||
Pairs: make(map[string]any),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Debug(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_Log_Info(b *testing.B) {
|
||||
output := &bytes.Buffer{}
|
||||
logger := New().SetMinLogLevel(LEVEL_INFO).SetOutput(output)
|
||||
msg := &LogMessage{
|
||||
Message: "info message",
|
||||
Pairs: make(map[string]any),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Info(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_Log_Warn(b *testing.B) {
|
||||
output := &bytes.Buffer{}
|
||||
logger := New().SetMinLogLevel(LEVEL_WARN).SetOutput(output)
|
||||
msg := &LogMessage{
|
||||
Message: "warn message",
|
||||
Pairs: make(map[string]any),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Warn(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_Log_Error(b *testing.B) {
|
||||
output := &bytes.Buffer{}
|
||||
logger := New().SetMinLogLevel(LEVEL_ERROR).SetOutput(output)
|
||||
msg := &LogMessage{
|
||||
Message: "error message",
|
||||
Pairs: map[string]any{"key": "value"},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_Log_Fatal(b *testing.B) {
|
||||
output := &bytes.Buffer{}
|
||||
logger := New().SetMinLogLevel(LEVEL_FATAL).SetOutput(output)
|
||||
msg := &LogMessage{
|
||||
Message: "fatal message",
|
||||
Pairs: make(map[string]any),
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
logger.Fatal(msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package libpack_logger
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
assertions "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type LoggerTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
var (
|
||||
assert *assertions.Assertions
|
||||
)
|
||||
|
||||
func (suite *LoggerTestSuite) BeforeTest(suiteName, testName string) {
|
||||
}
|
||||
|
||||
func (suite *LoggerTestSuite) SetupTest() {
|
||||
assert = assertions.New(suite.T())
|
||||
}
|
||||
|
||||
// TearDownTest is run after each test to clean up
|
||||
func (suite *LoggerTestSuite) TearDownTest() {
|
||||
}
|
||||
|
||||
func TestSuite(t *testing.T) {
|
||||
suite.Run(t, new(LoggerTestSuite))
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
package libpack_logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/goccy/go-reflect"
|
||||
)
|
||||
|
||||
func (suite *LoggerTestSuite) Test_LogMessageString() {
|
||||
msg := &LogMessage{
|
||||
Message: "test message",
|
||||
}
|
||||
|
||||
assert.Equal("test message", msg.String())
|
||||
}
|
||||
|
||||
func callLoggerMethod(logger *Logger, methodName string, message *LogMessage) {
|
||||
// Get the method by name using reflection
|
||||
method := reflect.ValueOf(logger).MethodByName(methodName)
|
||||
if method.IsValid() {
|
||||
// Call the method with the message as an argument
|
||||
method.Call([]reflect.Value{reflect.ValueOf(message)})
|
||||
} else {
|
||||
fmt.Printf("Method %s does not exist on Logger\n", methodName)
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *LoggerTestSuite) Test_LogsLevelsPrint() {
|
||||
output := &bytes.Buffer{}
|
||||
logger := New().SetOutput(output)
|
||||
|
||||
tests := []struct {
|
||||
pairs map[string]any
|
||||
name string
|
||||
method string
|
||||
message string
|
||||
loggerMinLevel int
|
||||
messageLogLevel int
|
||||
wantOutput bool
|
||||
}{
|
||||
{
|
||||
name: "Log: Debug, Level: Debug - no pairs",
|
||||
method: "Debug",
|
||||
loggerMinLevel: LEVEL_DEBUG,
|
||||
messageLogLevel: LEVEL_DEBUG,
|
||||
message: "debug message",
|
||||
wantOutput: true,
|
||||
},
|
||||
{
|
||||
name: "Log: Info, Level: Info - one pair",
|
||||
method: "Info",
|
||||
loggerMinLevel: LEVEL_INFO,
|
||||
messageLogLevel: LEVEL_INFO,
|
||||
message: "info message",
|
||||
pairs: map[string]any{
|
||||
"key": "value",
|
||||
},
|
||||
wantOutput: true,
|
||||
},
|
||||
{
|
||||
name: "Log: Info, Level: Warn - with pairs",
|
||||
method: "Info",
|
||||
loggerMinLevel: LEVEL_WARN,
|
||||
messageLogLevel: LEVEL_INFO,
|
||||
message: "warn message",
|
||||
pairs: map[string]any{
|
||||
"key1": "value1",
|
||||
"key2": "value2",
|
||||
},
|
||||
wantOutput: false,
|
||||
},
|
||||
{
|
||||
name: "Log: Warn, Level: Info - with 500 pairs",
|
||||
method: "Warn",
|
||||
loggerMinLevel: LEVEL_INFO,
|
||||
messageLogLevel: LEVEL_WARN,
|
||||
message: "warn message with 500 pairs",
|
||||
pairs: func() map[string]any {
|
||||
pairs := make(map[string]any)
|
||||
for i := 0; i < 500; i++ {
|
||||
pairs[fmt.Sprintf("key%d", i)] = fmt.Sprintf("value%d", i)
|
||||
}
|
||||
return pairs
|
||||
}(),
|
||||
wantOutput: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
msg := &LogMessage{
|
||||
Message: tt.message,
|
||||
Pairs: tt.pairs,
|
||||
}
|
||||
output.Reset()
|
||||
|
||||
// Set logger's minimum log level
|
||||
logger.SetMinLogLevel(tt.loggerMinLevel)
|
||||
fmt.Println("Logger min log level:", LevelNames[logger.minLogLevel])
|
||||
|
||||
// Call the logging method
|
||||
callLoggerMethod(logger, tt.method, msg)
|
||||
|
||||
logOutput := output.String()
|
||||
fmt.Println("Output:", logOutput)
|
||||
|
||||
if tt.wantOutput {
|
||||
var loggedMessage map[string]any
|
||||
err := json.Unmarshal([]byte(logOutput), &loggedMessage)
|
||||
if err != nil {
|
||||
t.Fatalf("Error unmarshalling log message: %v\nLog output: %s", err, logOutput)
|
||||
}
|
||||
|
||||
if !containsLogMessage(logOutput, tt.message) {
|
||||
t.Errorf("Expected log message %q, but got %q", tt.message, logOutput)
|
||||
}
|
||||
assert.Equal(LevelNames[tt.messageLogLevel], loggedMessage["level"])
|
||||
if tt.pairs != nil {
|
||||
for k, v := range tt.pairs {
|
||||
assert.Equal(v, loggedMessage[k])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert.Equal("", logOutput)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func containsLogMessage(logOutput, expectedMessage string) bool {
|
||||
return bytes.Contains([]byte(logOutput), []byte(expectedMessage))
|
||||
}
|
||||
|
||||
func (suite *LoggerTestSuite) Test_SetFormat() {
|
||||
logger := New().SetFormat(time.RFC3339Nano)
|
||||
|
||||
assert.Equal(time.RFC3339Nano, logger.format)
|
||||
}
|
||||
|
||||
func (suite *LoggerTestSuite) Test_SetMinLogLevel() {
|
||||
logger := New().SetMinLogLevel(LEVEL_DEBUG)
|
||||
|
||||
assert.Equal(LEVEL_DEBUG, logger.minLogLevel)
|
||||
}
|
||||
|
||||
func (suite *LoggerTestSuite) Test_ShouldLog() {
|
||||
logger := New().SetMinLogLevel(LEVEL_WARN)
|
||||
|
||||
assert.True(logger.shouldLog(LEVEL_WARN))
|
||||
assert.True(logger.shouldLog(LEVEL_ERROR))
|
||||
assert.False(logger.shouldLog(LEVEL_INFO))
|
||||
assert.False(logger.shouldLog(LEVEL_DEBUG))
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"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.New(),
|
||||
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(&logging.LogMessage{
|
||||
Message: "Created new GraphQL client connection",
|
||||
Pairs: 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
|
||||
}
|
||||
|
||||
func (b *BaseClient) SetHTTPClient(client *http.Client) {
|
||||
b.client = client
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package gql
|
||||
|
||||
import "testing"
|
||||
|
||||
func Benchmark_newConnection(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
NewConnection()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func (suite *Tests) TestNewConnection() {
|
||||
tests := []struct {
|
||||
name string
|
||||
}{
|
||||
{
|
||||
name: "TestNewConnection",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
got := NewConnection()
|
||||
assert.NotNil(suite.T(), got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// mock_server.go
|
||||
package gql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
func StartMockServer() *httptest.Server {
|
||||
// Create your handler
|
||||
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Your existing handler code
|
||||
bodyBytes, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Variables map[string]interface{} `json:"variables"`
|
||||
Query string `json:"query"`
|
||||
}
|
||||
err = json.Unmarshal(bodyBytes, &req)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
// Simple validation to check for unbalanced braces as a syntax error
|
||||
if !strings.HasPrefix(strings.TrimSpace(req.Query), "query") || !strings.HasSuffix(strings.TrimSpace(req.Query), "}") {
|
||||
resp := []byte(`{"errors":[{"message":"Syntax Error: Invalid query"}]}`)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write(resp)
|
||||
return
|
||||
}
|
||||
|
||||
var resp []byte
|
||||
if strings.Contains(req.Query, "viewer") {
|
||||
resp = []byte(`{"data":{"viewer":{"login":"mockuser"}}}`)
|
||||
} else if strings.Contains(req.Query, "dragons") {
|
||||
resp = []byte(`{"data":{"dragons":[{"name":"Mock Dragon 1"},{"name":"Mock Dragon 2"}]}}`)
|
||||
} else {
|
||||
resp = []byte(`{"errors":[{"message":"Unknown query"}]}`)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(resp)
|
||||
})
|
||||
|
||||
// Create an unstarted server
|
||||
server := httptest.NewUnstartedServer(handler)
|
||||
|
||||
// Configure the server to support HTTP/2
|
||||
http2.ConfigureServer(server.Config, &http2.Server{})
|
||||
server.TLS = server.Config.TLSConfig
|
||||
|
||||
// Start the server with TLS
|
||||
server.StartTLS()
|
||||
return server
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/lukaszraczylo/pandati"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type LogConfig struct {
|
||||
DefaultOutput *os.File
|
||||
Logger zerolog.Logger
|
||||
}
|
||||
|
||||
func NewLogger() *LogConfig {
|
||||
log := new(LogConfig)
|
||||
switch dll := os.Getenv("LOG_LEVEL"); dll {
|
||||
case "debug":
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
case "warn":
|
||||
zerolog.SetGlobalLevel(zerolog.WarnLevel)
|
||||
case "error":
|
||||
zerolog.SetGlobalLevel(zerolog.ErrorLevel)
|
||||
default:
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
}
|
||||
zerolog.TimeFieldFormat = time.RFC3339
|
||||
zerolog.MessageFieldName = "short_message"
|
||||
zerolog.TimestampFieldName = "timestamp"
|
||||
zerolog.LevelFatalValue = "critical"
|
||||
log.Logger = log.Logger.With().Timestamp().Logger()
|
||||
stdlog.SetFlags(0)
|
||||
stdlog.SetOutput(log.Logger)
|
||||
return log
|
||||
}
|
||||
|
||||
func (l *LogConfig) Critical(message string, v ...map[string]interface{}) {
|
||||
w := l.Logger.Output(os.Stderr)
|
||||
log(w.Fatal(), message, v...)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Error(message string, v ...map[string]interface{}) {
|
||||
w := l.Logger.Output(os.Stderr)
|
||||
log(w.Error(), message, v...)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Warning(message string, v ...map[string]interface{}) {
|
||||
w := l.Logger.Output(os.Stdout)
|
||||
log(w.Warn(), message, v...)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Info(message string, v ...map[string]interface{}) {
|
||||
w := l.Logger.Output(os.Stdout)
|
||||
log(w.Info(), message, v...)
|
||||
}
|
||||
|
||||
func (l *LogConfig) Debug(message string, v ...map[string]interface{}) {
|
||||
w := l.Logger.Output(os.Stdout)
|
||||
log(w.Debug(), message, v...)
|
||||
}
|
||||
|
||||
func log(contextLog *zerolog.Event, message string, v ...map[string]interface{}) {
|
||||
if len(v) > 0 {
|
||||
for _, m := range v {
|
||||
for k, v := range m {
|
||||
if !pandati.IsZero(v) && !pandati.IsZero(k) {
|
||||
contextLog.Str(k, fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// contextLog.Str("_stack_trace", pandati.Trace())
|
||||
contextLog.Msg(message)
|
||||
}
|
||||
@@ -1,373 +0,0 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/buger/jsonparser"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type LoggingTestSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
var (
|
||||
testsLogger *LogConfig
|
||||
)
|
||||
|
||||
type stdoutCapture struct {
|
||||
oldStdout *os.File
|
||||
readPipe *os.File
|
||||
}
|
||||
|
||||
func (sc *stdoutCapture) StartCapture() {
|
||||
sc.oldStdout = os.Stdout
|
||||
sc.readPipe, os.Stdout, _ = os.Pipe()
|
||||
}
|
||||
|
||||
func (sc *stdoutCapture) StopCapture() (string, error) {
|
||||
if sc.oldStdout == nil || sc.readPipe == nil {
|
||||
return "", errors.New("StartCapture not called before StopCapture on Stdout")
|
||||
}
|
||||
os.Stdout.Close()
|
||||
os.Stdout = sc.oldStdout
|
||||
bytes, err := ioutil.ReadAll(sc.readPipe)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
type stderrCapture struct {
|
||||
oldStderr *os.File
|
||||
readPipe *os.File
|
||||
}
|
||||
|
||||
func (sc *stderrCapture) StartCapture() {
|
||||
sc.oldStderr = os.Stderr
|
||||
sc.readPipe, os.Stderr, _ = os.Pipe()
|
||||
}
|
||||
|
||||
func (sc *stderrCapture) StopCapture() (string, error) {
|
||||
if sc.oldStderr == nil || sc.readPipe == nil {
|
||||
return "", errors.New("StartCapture not called before StopCapture on Stderr")
|
||||
}
|
||||
os.Stderr.Close()
|
||||
os.Stderr = sc.oldStderr
|
||||
bytes, err := ioutil.ReadAll(sc.readPipe)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func (suite *LoggingTestSuite) SetupTest() {
|
||||
}
|
||||
|
||||
func TestLoggingTestSuite(t *testing.T) {
|
||||
suite.Run(t, new(LoggingTestSuite))
|
||||
}
|
||||
|
||||
func (suite *LoggingTestSuite) TestLogConfig_AllHandlers() {
|
||||
type args struct {
|
||||
message string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantLevel string
|
||||
wantMessage string
|
||||
envMinLogLevel string
|
||||
stdOutExpect bool
|
||||
stdErrExpect bool
|
||||
loggerType string
|
||||
}{
|
||||
{
|
||||
name: "Test log: Error",
|
||||
loggerType: "Error",
|
||||
args: args{
|
||||
message: "This is a error message",
|
||||
},
|
||||
wantLevel: "error",
|
||||
wantMessage: "This is a error message",
|
||||
stdErrExpect: true,
|
||||
stdOutExpect: false,
|
||||
},
|
||||
{
|
||||
name: "Test log: Warning",
|
||||
loggerType: "Warning",
|
||||
args: args{
|
||||
message: "This is a warning message",
|
||||
},
|
||||
wantLevel: "warn",
|
||||
wantMessage: "This is a warning message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
envMinLogLevel: "info",
|
||||
},
|
||||
{
|
||||
name: "Test log: Warning | Min level: Debug",
|
||||
loggerType: "Warning",
|
||||
args: args{
|
||||
message: "This is a warning message",
|
||||
},
|
||||
wantLevel: "warn",
|
||||
wantMessage: "This is a warning message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
envMinLogLevel: "debug",
|
||||
},
|
||||
{
|
||||
name: "Test log: Info",
|
||||
loggerType: "Info",
|
||||
args: args{
|
||||
message: "This is a info message",
|
||||
},
|
||||
wantLevel: "info",
|
||||
wantMessage: "This is a info message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
},
|
||||
{
|
||||
name: "Test log: Info | Min level: Warn",
|
||||
loggerType: "Info",
|
||||
args: args{
|
||||
message: "This is a info message",
|
||||
},
|
||||
wantLevel: "",
|
||||
wantMessage: "",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: false,
|
||||
envMinLogLevel: "warn",
|
||||
},
|
||||
{
|
||||
name: "Test log: Warning | Min level: Warn",
|
||||
loggerType: "Warning",
|
||||
args: args{
|
||||
message: "This is a warning message",
|
||||
},
|
||||
wantLevel: "warn",
|
||||
wantMessage: "This is a warning message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
envMinLogLevel: "warn",
|
||||
},
|
||||
{
|
||||
name: "Test log: Warning | Min level: Error",
|
||||
loggerType: "Warning",
|
||||
args: args{
|
||||
message: "This is an error message",
|
||||
},
|
||||
wantLevel: "",
|
||||
wantMessage: "",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: false,
|
||||
envMinLogLevel: "error",
|
||||
},
|
||||
{
|
||||
name: "Test log: Debug | Min level: Debug",
|
||||
loggerType: "Debug",
|
||||
args: args{
|
||||
message: "This is a debug message",
|
||||
},
|
||||
wantLevel: "debug",
|
||||
wantMessage: "This is a debug message",
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
envMinLogLevel: "debug",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
if tt.envMinLogLevel != "" {
|
||||
os.Setenv("LOG_LEVEL", tt.envMinLogLevel)
|
||||
defer os.Unsetenv("LOG_LEVEL")
|
||||
}
|
||||
testsLogger = NewLogger()
|
||||
|
||||
captureStdout := stdoutCapture{}
|
||||
captureStdout.StartCapture()
|
||||
captureStderr := stderrCapture{}
|
||||
captureStderr.StartCapture()
|
||||
|
||||
reflect.ValueOf(testsLogger).MethodByName(tt.loggerType).Call([]reflect.Value{reflect.ValueOf(tt.args.message)})
|
||||
|
||||
stdoutOut, err := captureStdout.StopCapture()
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
}
|
||||
|
||||
stderrOut, err := captureStderr.StopCapture()
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
}
|
||||
|
||||
if tt.stdErrExpect && !tt.stdOutExpect {
|
||||
gotLvl, gotMsg, err := getResponseValues(stderrOut, "short_message")
|
||||
suite.NoError(err, "Failed in [STDERR]: "+tt.name)
|
||||
suite.Equal(tt.wantLevel, gotLvl, "Failed in [STDERR]: "+tt.name)
|
||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDERR]: "+tt.name)
|
||||
suite.Equal("", stdoutOut, "Failed in [STDERR]: "+tt.name)
|
||||
}
|
||||
if tt.stdOutExpect && !tt.stdErrExpect {
|
||||
gotLvl, gotMsg, err := getResponseValues(stdoutOut, "short_message")
|
||||
suite.NoError(err, "Failed in [STDOUT]: "+tt.name)
|
||||
suite.Equal(tt.wantLevel, gotLvl, "Failed in [STDOUT]: "+tt.name)
|
||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDOUT]: "+tt.name)
|
||||
suite.Equal("", stderrOut, "Failed in [STDOUT]: "+tt.name)
|
||||
}
|
||||
if !tt.stdErrExpect && !tt.stdOutExpect {
|
||||
suite.Equal("", stderrOut, "Failed in [NEITHER]: "+tt.name)
|
||||
suite.Equal("", stdoutOut, "Failed in [NEITHER]: "+tt.name)
|
||||
}
|
||||
os.Unsetenv("LOG_LEVEL")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *LoggingTestSuite) TestFullMessage() {
|
||||
type args struct {
|
||||
message string
|
||||
extraFields map[string]interface{}
|
||||
}
|
||||
extraFields := make(map[string]interface{})
|
||||
extraFields["_full_message"] = "full message"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantLevel string
|
||||
wantMessage string
|
||||
envMinLogLevel string
|
||||
stdOutExpect bool
|
||||
stdErrExpect bool
|
||||
loggerType string
|
||||
}{
|
||||
{
|
||||
name: "Test log: Error",
|
||||
loggerType: "Error",
|
||||
args: args{
|
||||
message: "This is a error message",
|
||||
extraFields: extraFields,
|
||||
},
|
||||
wantLevel: "error",
|
||||
wantMessage: extraFields["_full_message"].(string),
|
||||
stdErrExpect: true,
|
||||
stdOutExpect: false,
|
||||
},
|
||||
{
|
||||
name: "Test log: Info",
|
||||
loggerType: "Info",
|
||||
args: args{
|
||||
message: "This is a info message",
|
||||
extraFields: extraFields,
|
||||
},
|
||||
wantMessage: extraFields["_full_message"].(string),
|
||||
stdErrExpect: false,
|
||||
stdOutExpect: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
if tt.envMinLogLevel != "" {
|
||||
os.Setenv("LOG_LEVEL", tt.envMinLogLevel)
|
||||
defer os.Unsetenv("LOG_LEVEL")
|
||||
}
|
||||
testsLogger = NewLogger()
|
||||
|
||||
captureStdout := stdoutCapture{}
|
||||
captureStdout.StartCapture()
|
||||
captureStderr := stderrCapture{}
|
||||
captureStderr.StartCapture()
|
||||
|
||||
reflect.ValueOf(testsLogger).MethodByName(tt.loggerType).Call([]reflect.Value{
|
||||
reflect.ValueOf(tt.args.message),
|
||||
reflect.ValueOf(tt.args.extraFields),
|
||||
})
|
||||
|
||||
stdoutOut, err := captureStdout.StopCapture()
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
}
|
||||
|
||||
stderrOut, err := captureStderr.StopCapture()
|
||||
if err != nil {
|
||||
suite.T().Fatal(err)
|
||||
}
|
||||
|
||||
if tt.stdErrExpect && !tt.stdOutExpect {
|
||||
_, gotMsg, err := getResponseValues(stderrOut, "_full_message")
|
||||
suite.NoError(err, "Failed in [STDERR]: "+tt.name)
|
||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDERR]: "+tt.name)
|
||||
}
|
||||
if tt.stdOutExpect && !tt.stdErrExpect {
|
||||
_, gotMsg, err := getResponseValues(stdoutOut, "_full_message")
|
||||
suite.NoError(err, "Failed in [STDOUT]: "+tt.name)
|
||||
suite.Equal(tt.wantMessage, gotMsg, "Failed in [STDOUT]: "+tt.name)
|
||||
}
|
||||
os.Unsetenv("LOG_LEVEL")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getResponseValues(t *testing.T) {
|
||||
type args struct {
|
||||
sourceJson string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantGotLvl string
|
||||
wantGotMsg string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Test with json",
|
||||
args: args{
|
||||
sourceJson: `{"level": "debug", "short_message": "hello world"`,
|
||||
},
|
||||
wantGotLvl: "debug",
|
||||
wantGotMsg: "hello world",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Test with json, wrong message field",
|
||||
args: args{
|
||||
sourceJson: `{"level": "debug", "message": "hello world"`,
|
||||
},
|
||||
wantGotLvl: "debug",
|
||||
wantGotMsg: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotGotLvl, gotGotMsg, err := getResponseValues(tt.args.sourceJson, "short_message")
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("getResponseValues() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotGotLvl != tt.wantGotLvl {
|
||||
t.Errorf("getResponseValues() gotGotLvl = %v, want %v", gotGotLvl, tt.wantGotLvl)
|
||||
}
|
||||
if gotGotMsg != tt.wantGotMsg {
|
||||
t.Errorf("getResponseValues() gotGotMsg = %v, want %v", gotGotMsg, tt.wantGotMsg)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getResponseValues(sourceJson string, key string) (gotLvl, gotMsg string, err error) {
|
||||
gotLvl, err = jsonparser.GetString([]byte(sourceJson), "level")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
gotMsg, err = jsonparser.GetString([]byte(sourceJson), key)
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
// Pre-sized buffer pool for better performance
|
||||
bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
// Pre-allocate buffer with 4KB capacity
|
||||
b := bytes.NewBuffer(make([]byte, 0, 4096))
|
||||
return b
|
||||
},
|
||||
}
|
||||
|
||||
// Pre-allocate error maps to reduce allocations
|
||||
errPairsPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return make(map[string]interface{}, 1)
|
||||
},
|
||||
}
|
||||
)
|
||||
@@ -2,94 +2,164 @@ package gql
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/lukaszraczylo/pandati"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/gookit/goutil"
|
||||
"github.com/gookit/goutil/strutil"
|
||||
libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging"
|
||||
)
|
||||
|
||||
type requestBase struct {
|
||||
Query string `json:"query"`
|
||||
Variables interface{} `json:"variables"`
|
||||
func (b *BaseClient) convertToJSON(v any) []byte {
|
||||
buf := bufferPool.Get().(*bytes.Buffer)
|
||||
defer bufferPool.Put(buf)
|
||||
buf.Reset()
|
||||
|
||||
// Use json.NewEncoder for better performance with buffers
|
||||
enc := json.NewEncoder(buf)
|
||||
enc.SetEscapeHTML(false) // Reduce unnecessary escaping
|
||||
|
||||
if err := enc.Encode(v); err != nil {
|
||||
errPairs := errPairsPool.Get().(map[string]interface{})
|
||||
errPairs["error"] = err.Error()
|
||||
b.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't convert to JSON",
|
||||
Pairs: errPairs,
|
||||
})
|
||||
errPairsPool.Put(errPairs)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the buffer bytes directly, trimming the trailing newline
|
||||
bytes := buf.Bytes()
|
||||
if len(bytes) > 0 && bytes[len(bytes)-1] == '\n' {
|
||||
bytes = bytes[:len(bytes)-1]
|
||||
}
|
||||
|
||||
// Make a copy of the bytes since the buffer will be reused
|
||||
result := make([]byte, len(bytes))
|
||||
copy(result, bytes)
|
||||
return result
|
||||
}
|
||||
|
||||
type queryResults struct {
|
||||
Data interface{} `json:"data"`
|
||||
Errors []struct {
|
||||
Message interface{} `json:"message"`
|
||||
} `json:"errors"`
|
||||
func (b *BaseClient) compileQuery(queryPartials ...any) *Query {
|
||||
var query string
|
||||
var variables map[string]interface{}
|
||||
|
||||
// Pre-allocate the query with an estimated size
|
||||
if len(queryPartials) > 0 {
|
||||
if str, ok := queryPartials[0].(string); ok {
|
||||
query = str
|
||||
}
|
||||
}
|
||||
|
||||
// Only allocate variables map if we have more than one partial
|
||||
if len(queryPartials) > 1 {
|
||||
if vars, ok := queryPartials[1].(map[string]interface{}); ok {
|
||||
variables = vars
|
||||
}
|
||||
}
|
||||
|
||||
if query == "" {
|
||||
errPairs := errPairsPool.Get().(map[string]interface{})
|
||||
errPairs["error"] = "query is empty"
|
||||
b.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't compile query",
|
||||
Pairs: errPairs,
|
||||
})
|
||||
errPairsPool.Put(errPairs)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Construct query object once
|
||||
q := &Query{
|
||||
Query: query,
|
||||
Variables: variables,
|
||||
}
|
||||
q.JsonQuery = b.convertToJSON(q)
|
||||
return q
|
||||
}
|
||||
|
||||
func (g *GraphQL) queryBuilder(queryContent string, queryVariables interface{}) ([]byte, error) {
|
||||
var qb = &requestBase{
|
||||
Query: queryContent,
|
||||
Variables: queryVariables,
|
||||
func (b *BaseClient) Query(query string, variables map[string]interface{}, headers map[string]interface{}) (any, error) {
|
||||
compiledQuery := b.compileQuery(query, variables)
|
||||
if compiledQuery.JsonQuery == nil {
|
||||
b.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Can't compile query",
|
||||
Pairs: map[string]interface{}{"error": "query is empty"},
|
||||
})
|
||||
return nil, fmt.Errorf("can't compile query")
|
||||
}
|
||||
b.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Compiled query",
|
||||
Pairs: map[string]interface{}{"query": compiledQuery},
|
||||
})
|
||||
|
||||
enableCache, enableRetries, recompileRequired := compiledQuery.parseHeadersAndVariables(headers)
|
||||
if recompileRequired {
|
||||
compiledQuery = b.compileQuery(query, variables)
|
||||
}
|
||||
|
||||
// j := new(bytes.Buffer)
|
||||
j2, err := json.Marshal(qb)
|
||||
if err != nil {
|
||||
g.Log.Critical("Unable to marshal the query", map[string]interface{}{"_error": err.Error(), "_query": queryContent, "_variables": queryVariables})
|
||||
return []byte{}, err
|
||||
var queryHash string
|
||||
if (enableCache || b.cache_global) && strutil.HasPrefix(compiledQuery.Query, "query") {
|
||||
b.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Cache enabled",
|
||||
Pairs: nil,
|
||||
})
|
||||
queryHash = calculateHash(compiledQuery)
|
||||
if cachedValue := b.cacheLookup(queryHash); cachedValue != nil {
|
||||
b.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Cache hit",
|
||||
Pairs: map[string]interface{}{"query": compiledQuery},
|
||||
})
|
||||
return b.decodeResponse(cachedValue)
|
||||
}
|
||||
b.Logger.Debug(&libpack_logger.LogMessage{
|
||||
Message: "Cache miss",
|
||||
Pairs: map[string]interface{}{"query": compiledQuery},
|
||||
})
|
||||
}
|
||||
// if err = json.Compact(j, j2); err != nil {
|
||||
// g.Log.Critical("Unable to compact the query", map[string]interface{}{"error": err.Error()})
|
||||
// return []byte{}, err
|
||||
// }
|
||||
return j2, err
|
||||
|
||||
q := &QueryExecutor{
|
||||
BaseClient: b,
|
||||
Query: compiledQuery.JsonQuery,
|
||||
Headers: headers,
|
||||
CacheKey: func() string {
|
||||
if queryHash != "" {
|
||||
return queryHash
|
||||
}
|
||||
return "no-cache"
|
||||
}(),
|
||||
Retries: enableRetries || b.retries_enable,
|
||||
}
|
||||
|
||||
rv, err := q.executeQuery()
|
||||
if err != nil {
|
||||
b.Logger.Error(&libpack_logger.LogMessage{
|
||||
Message: "Error executing query",
|
||||
Pairs: map[string]interface{}{"error": err.Error()},
|
||||
})
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.decodeResponse(rv)
|
||||
}
|
||||
|
||||
func (g *GraphQL) Query(queryContent string, queryVariables interface{}, queryHeaders map[string]interface{}) (responseContent string, err error) {
|
||||
g.Log.Debug("Query details", map[string]interface{}{"_query": queryContent, "_variables": queryVariables})
|
||||
query, err := g.queryBuilder(queryContent, queryVariables)
|
||||
if err != nil {
|
||||
g.Log.Error("Unable to build the query", map[string]interface{}{"_error": err.Error()})
|
||||
return "", err
|
||||
}
|
||||
httpRequest, err := http.NewRequest("POST", g.Endpoint, bytes.NewBuffer(query))
|
||||
httpRequest.Header.Add("Content-Type", "application/json")
|
||||
for header, value := range queryHeaders {
|
||||
httpRequest.Header.Add(fmt.Sprintf("%v", header), fmt.Sprintf("%v", value))
|
||||
func (q *Query) parseHeadersAndVariables(headers map[string]interface{}) (enableCache, enableRetries, recompileRequired bool) {
|
||||
enableCache, _ = goutil.ToBool(searchForKeysInMapStringInterface(headers, "gqlcache"))
|
||||
enableRetries, _ = goutil.ToBool(searchForKeysInMapStringInterface(headers, "gqlretries"))
|
||||
|
||||
if q.Variables != nil {
|
||||
varEnableCache, _ := goutil.ToBool(searchForKeysInMapStringInterface(q.Variables, "gqlcache"))
|
||||
varEnableRetries, _ := goutil.ToBool(searchForKeysInMapStringInterface(q.Variables, "gqlretries"))
|
||||
enableCache = enableCache || varEnableCache
|
||||
enableRetries = enableRetries || varEnableRetries
|
||||
|
||||
if varEnableCache || varEnableRetries {
|
||||
delete(q.Variables, "gqlcache")
|
||||
delete(q.Variables, "gqlretries")
|
||||
recompileRequired = true
|
||||
}
|
||||
}
|
||||
|
||||
httpResponse, err := g.HttpClient.Do(httpRequest)
|
||||
if err != nil {
|
||||
g.Log.Error("Unable to send the query", map[string]interface{}{"_error": err.Error()})
|
||||
return "", err
|
||||
}
|
||||
defer io.Copy(ioutil.Discard, httpResponse.Body)
|
||||
defer httpResponse.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(httpResponse.Body)
|
||||
if err != nil {
|
||||
g.Log.Critical("Unable to read the response", map[string]interface{}{"_error": err.Error()})
|
||||
return "", err
|
||||
}
|
||||
var queryResult *queryResults
|
||||
err = json.Unmarshal(body, &queryResult)
|
||||
if err != nil {
|
||||
g.Log.Error("Unable to unmarshal the query", map[string]interface{}{"_error": err.Error()})
|
||||
return "", err
|
||||
}
|
||||
|
||||
if !pandati.IsZero(queryResult.Errors) {
|
||||
g.Log.Error("Query returned error", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_error": fmt.Sprintf("%v", queryResult.Errors)})
|
||||
return "", fmt.Errorf("%v", queryResult.Errors[0].Message)
|
||||
}
|
||||
|
||||
if pandati.IsZero(queryResult.Data) {
|
||||
g.Log.Error("Query returned no data", map[string]interface{}{"_query": queryContent, "_variables": queryVariables})
|
||||
return "", errors.New("Query returned no data")
|
||||
}
|
||||
|
||||
responseContent, err = json.MarshalToString(queryResult.Data)
|
||||
if err != nil {
|
||||
g.Log.Error("Invalid data result", map[string]interface{}{"_query": queryContent, "_variables": queryVariables, "_result": responseContent})
|
||||
return "", err
|
||||
}
|
||||
|
||||
return
|
||||
return enableCache, enableRetries, recompileRequired
|
||||
}
|
||||
|
||||
+89
-59
@@ -4,66 +4,96 @@ 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_compileQueryWithVariables(bn *testing.B) {
|
||||
b := NewConnection()
|
||||
variables := map[string]interface{}{
|
||||
"var1": "value1",
|
||||
}
|
||||
bn.ResetTimer()
|
||||
for i := 0; i < bn.N; i++ {
|
||||
b.compileQuery(
|
||||
`query { viewer { login } }`,
|
||||
variables,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_convertToJson(bn *testing.B) {
|
||||
b := NewConnection()
|
||||
query := `query { viewer { login } }`
|
||||
bn.ResetTimer()
|
||||
for i := 0; i < bn.N; i++ {
|
||||
b.convertToJSON(query)
|
||||
}
|
||||
}
|
||||
func Benchmark_Query(bn *testing.B) {
|
||||
mockServer := StartMockServer()
|
||||
bn.Cleanup(func() {
|
||||
mockServer.Close()
|
||||
})
|
||||
|
||||
b := NewConnection()
|
||||
b.SetEndpoint(mockServer.URL)
|
||||
query := "query { viewer { login } }"
|
||||
variables := map[string]interface{}{
|
||||
"var1": "value1",
|
||||
}
|
||||
headers := map[string]interface{}{
|
||||
"Authorization": "Bearer token",
|
||||
"gqlcache": "true",
|
||||
}
|
||||
bn.ResetTimer()
|
||||
for i := 0; i < bn.N; i++ {
|
||||
_, _ = b.Query(query, variables, headers)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_QueryNoCacheNoRetry(bn *testing.B) {
|
||||
mockServer := StartMockServer()
|
||||
bn.Cleanup(func() {
|
||||
mockServer.Close()
|
||||
})
|
||||
|
||||
b := NewConnection()
|
||||
b.SetEndpoint(mockServer.URL)
|
||||
query := `query { viewer { login } }`
|
||||
variables := map[string]interface{}{
|
||||
"var1": "value1",
|
||||
}
|
||||
headers := map[string]interface{}{
|
||||
"Authorization": "Bearer token",
|
||||
}
|
||||
bn.ResetTimer()
|
||||
for i := 0; i < bn.N; i++ {
|
||||
_, _ = b.Query(query, variables, headers)
|
||||
}
|
||||
}
|
||||
|
||||
func Benchmark_QueryWithEmptyVariables(bn *testing.B) {
|
||||
mockServer := StartMockServer()
|
||||
bn.Cleanup(func() {
|
||||
mockServer.Close()
|
||||
})
|
||||
|
||||
b := NewConnection()
|
||||
b.SetEndpoint(mockServer.URL)
|
||||
query := `query { viewer { login } }`
|
||||
headers := map[string]interface{}{
|
||||
"Authorization": "Bearer token",
|
||||
"gqlcache": "true",
|
||||
}
|
||||
bn.ResetTimer()
|
||||
for i := 0; i < bn.N; i++ {
|
||||
_, _ = b.Query(query, nil, headers)
|
||||
}
|
||||
}
|
||||
|
||||
+136
-65
@@ -1,113 +1,184 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/goccy/go-reflect"
|
||||
)
|
||||
|
||||
func (suite *TestSuite) Test_GraphQL_queryBuilder() {
|
||||
|
||||
func (suite *Tests) TestBaseClient_convertToJSON() {
|
||||
type args struct {
|
||||
queryContent string
|
||||
queryVariables interface{}
|
||||
v any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
wantErr bool
|
||||
name string
|
||||
args args
|
||||
want []byte
|
||||
}{
|
||||
{
|
||||
name: "Valid query",
|
||||
name: "TestBaseClient_convertToJSON_query",
|
||||
args: args{
|
||||
queryContent: `query listUserBots { tbl_bots { bot_name } }`,
|
||||
queryVariables: nil,
|
||||
v: `query { viewer { login } }`,
|
||||
},
|
||||
want: []byte(`{"query":"query listUserBots { tbl_bots { bot_name } }","variables":null}`),
|
||||
wantErr: false,
|
||||
want: []byte(`"query { viewer { login } }"`),
|
||||
},
|
||||
{
|
||||
name: "Valid query with variables",
|
||||
name: "TestBaseClient_convertToJSON_variables",
|
||||
args: args{
|
||||
queryContent: `query listUserBots { tbl_bots { bot_name } }`,
|
||||
queryVariables: map[string]interface{}{"user_id": 1},
|
||||
v: map[string]interface{}{
|
||||
"login": "lukaszraczylo",
|
||||
"first": 10,
|
||||
},
|
||||
},
|
||||
want: []byte(`{"query":"query listUserBots { tbl_bots { bot_name } }","variables":{"user_id":1}}`),
|
||||
wantErr: false,
|
||||
want: []byte(`{"first":10,"login":"lukaszraczylo"}`),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
q := NewConnection()
|
||||
got, gotErr := q.queryBuilder(tt.args.queryContent, tt.args.queryVariables)
|
||||
assert.Equal(t, tt.want, got)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, gotErr)
|
||||
b := NewConnection()
|
||||
if got := b.convertToJSON(tt.args.v); !reflect.DeepEqual(got, tt.want) {
|
||||
assert.Equal(tt.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *TestSuite) Test_GraphQL_Query() {
|
||||
if testing.Short() {
|
||||
suite.T().Skip("Skipping test in short / CI mode")
|
||||
}
|
||||
func (suite *Tests) TestBaseClient_compileQuery() {
|
||||
type args struct {
|
||||
queryContent string
|
||||
queryVariables interface{}
|
||||
query_partials []any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
endpoint string
|
||||
isLocal bool
|
||||
args args
|
||||
wantResult string
|
||||
wantErr bool
|
||||
wantQ *Query
|
||||
name string
|
||||
args args
|
||||
}{
|
||||
{
|
||||
name: "Valid query",
|
||||
endpoint: "http://hasura.local/v1/graphql",
|
||||
isLocal: true,
|
||||
name: "TestBaseClient_compileQuery_query",
|
||||
args: args{
|
||||
queryContent: `query listUserBots {
|
||||
tbl_bots(limit: 1) {
|
||||
bot_name
|
||||
}
|
||||
}`,
|
||||
queryVariables: nil,
|
||||
query_partials: []any{
|
||||
`query { viewer { login } }`,
|
||||
},
|
||||
},
|
||||
wantQ: &Query{
|
||||
Query: `query { viewer { login } }`,
|
||||
JsonQuery: []byte(`{"query":"query { viewer { login } }"}`),
|
||||
},
|
||||
wantResult: `{"tbl_bots":[{"bot_name":"littleMentionBot"}]}`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid query",
|
||||
endpoint: "http://hasura.local/v1/graphql",
|
||||
isLocal: true,
|
||||
name: "TestBaseClient_compileQuery_with_variables",
|
||||
args: args{
|
||||
queryContent: `query listUserBots {
|
||||
tbl_botz(limit: 1) {
|
||||
bot_name
|
||||
}
|
||||
}`,
|
||||
queryVariables: nil,
|
||||
query_partials: []any{
|
||||
`query { viewer { login } }`,
|
||||
map[string]interface{}{
|
||||
"login": "lukaszraczylo",
|
||||
"first": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantResult: ``,
|
||||
wantErr: true,
|
||||
wantQ: &Query{
|
||||
Query: `query { viewer { login } }`,
|
||||
Variables: map[string]interface{}{
|
||||
"login": "lukaszraczylo",
|
||||
"first": 10,
|
||||
},
|
||||
JsonQuery: []byte(`{"variables":{"first":10,"login":"lukaszraczylo"},"query":"query { viewer { login } }"}`),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_compileQuery_variables_without_query",
|
||||
args: args{
|
||||
query_partials: []any{
|
||||
map[string]interface{}{
|
||||
"login": "lukaszraczylo",
|
||||
"first": 10,
|
||||
},
|
||||
},
|
||||
},
|
||||
wantQ: nil,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
if tt.isLocal {
|
||||
os.Setenv("GRAPHQL_ENDPOINT", tt.endpoint)
|
||||
b := NewConnection()
|
||||
got := b.compileQuery(tt.args.query_partials...)
|
||||
assert.Equal(tt.wantQ, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *Tests) TestBaseClient_Query() {
|
||||
type args struct {
|
||||
variables map[string]interface{}
|
||||
headers map[string]interface{}
|
||||
query string
|
||||
}
|
||||
tests := []struct {
|
||||
args args
|
||||
wantReturned_value any
|
||||
name string
|
||||
wantType string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "TestBaseClient_Query_viewer",
|
||||
args: args{
|
||||
query: "query { viewer { login } }",
|
||||
variables: nil,
|
||||
headers: map[string]interface{}{
|
||||
"x-apollo-operation-name": "ViewerQuery",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
wantReturned_value: `{"viewer":{"login":"mockuser"}}`, // Adjusted expected value
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "TestBaseClient_Query_dragons",
|
||||
args: args{
|
||||
query: `query Dragons {
|
||||
dragons {
|
||||
name
|
||||
}
|
||||
}`,
|
||||
variables: nil,
|
||||
headers: map[string]interface{}{
|
||||
"x-apollo-operation-name": "DragonsQuery",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
wantReturned_value: `{"dragons":[{"name":"Mock Dragon 1"},{"name":"Mock Dragon 2"}]}`, // Adjusted expected value
|
||||
wantErr: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "TestBaseClient_Query_invalid_query",
|
||||
args: args{
|
||||
query: "query { potato { login } ",
|
||||
variables: nil,
|
||||
headers: map[string]interface{}{
|
||||
"x-apollo-operation-name": "InvalidQuery",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
},
|
||||
wantReturned_value: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
suite.T().Run(tt.name, func(t *testing.T) {
|
||||
b := NewConnection()
|
||||
b.SetEndpoint(mockServer.URL)
|
||||
b.SetHTTPClient(mockServer.Client())
|
||||
if tt.wantType != "" {
|
||||
b.SetOutput(tt.wantType)
|
||||
}
|
||||
g := NewConnection()
|
||||
gotResult, gotErr := g.Query(tt.args.queryContent, tt.args.queryVariables, nil)
|
||||
gotReturned_value, err := b.Query(tt.args.query, tt.args.variables, tt.args.headers)
|
||||
if tt.wantErr {
|
||||
assert.Error(t, gotErr)
|
||||
assert.Error(err)
|
||||
} else {
|
||||
assert.NoError(err)
|
||||
}
|
||||
assert.Equal(t, tt.wantResult, gotResult)
|
||||
assert.Equal(tt.wantReturned_value, gotReturned_value)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
graphql "github.com/lukaszraczylo/go-simple-graphql"
|
||||
)
|
||||
|
||||
func main() {
|
||||
gql := graphql.NewConnection()
|
||||
|
||||
query := `query MyQuery {
|
||||
bots {
|
||||
bot_name
|
||||
ddd
|
||||
}
|
||||
}`
|
||||
_, err := gql.Query(query, nil, nil)
|
||||
if err != nil {
|
||||
fmt.Println("Error returned from query:", err)
|
||||
}
|
||||
|
||||
query = `query MyQuery {
|
||||
badwords(distinct_on: word) {
|
||||
word
|
||||
}
|
||||
}`
|
||||
gql.Query(query, nil, nil)
|
||||
|
||||
// Repeated to use the cached response
|
||||
query = `query MyQuery {
|
||||
badwords(distinct_on: word) {
|
||||
word
|
||||
}
|
||||
}`
|
||||
gql.Query(query, nil, nil)
|
||||
}
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
version: 1
|
||||
force:
|
||||
major: 1
|
||||
minor: 0
|
||||
minor: 2
|
||||
patch: 0
|
||||
commit: 3217edf37fd1765c4f2594e3ba122a6a9c251296
|
||||
wording:
|
||||
minor:
|
||||
- minor-release
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
cache "github.com/lukaszraczylo/go-simple-graphql/cache"
|
||||
logging "github.com/lukaszraczylo/go-simple-graphql/logging"
|
||||
)
|
||||
|
||||
type BaseClient struct {
|
||||
cache *cache.Cache
|
||||
Logger *logging.Logger
|
||||
client *http.Client
|
||||
endpoint string
|
||||
responseType string
|
||||
retries_delay time.Duration
|
||||
retries_number int
|
||||
MaxGoRoutines int
|
||||
cache_global bool
|
||||
retries_enable bool
|
||||
}
|
||||
|
||||
type Query struct {
|
||||
Variables map[string]interface{} `json:"variables,omitempty"`
|
||||
Query string `json:"query,omitempty"`
|
||||
JsonQuery []byte `json:"jsonQuery,omitempty"`
|
||||
}
|
||||
|
||||
type QueryExecutor struct {
|
||||
Result any
|
||||
Error error
|
||||
*BaseClient
|
||||
Headers map[string]interface{}
|
||||
CacheKey string
|
||||
Query []byte
|
||||
CacheTTL time.Duration
|
||||
Retries bool
|
||||
}
|
||||
|
||||
type queryResults struct {
|
||||
Data interface{} `json:"data"`
|
||||
Errors []struct {
|
||||
Message interface{} `json:"message"`
|
||||
} `json:"errors"`
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package gql
|
||||
|
||||
import (
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
assertions "github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
type Tests struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
var (
|
||||
assert *assertions.Assertions
|
||||
mockServer *httptest.Server
|
||||
)
|
||||
|
||||
func (suite *Tests) SetupSuite() {
|
||||
// Start mock GraphQL server
|
||||
mockServer = StartMockServer()
|
||||
}
|
||||
|
||||
func (suite *Tests) TearDownSuite() {
|
||||
// Close the mock server
|
||||
if mockServer != nil {
|
||||
mockServer.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (suite *Tests) SetupTest() {
|
||||
assert = assertions.New(suite.T())
|
||||
}
|
||||
|
||||
func (suite *Tests) BeforeTest(suiteName, testName string) {
|
||||
}
|
||||
|
||||
func TestSuite(t *testing.T) {
|
||||
suite.Run(t, new(Tests))
|
||||
}
|
||||
Reference in New Issue
Block a user