mirror of
https://github.com/lukaszraczylo/go-simple-graphql.git
synced 2026-10-13 00:48:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b314a25549 | ||
|
|
240929a808 |
@@ -37,15 +37,52 @@ Therefore, I present you the simple client to which you can copy & paste your gr
|
|||||||
* `GRAPHQL_CACHE_TTL` - Cache TTL in seconds for SELECT type of queries. Default: `5`
|
* `GRAPHQL_CACHE_TTL` - Cache TTL in seconds for SELECT type of queries. Default: `5`
|
||||||
* `GRAPHQL_OUTPUT` - Output format. Default: `string`, available: `byte`, `string`, `mapstring`
|
* `GRAPHQL_OUTPUT` - Output format. Default: `string`, available: `byte`, `string`, `mapstring`
|
||||||
* `LOG_LEVEL` - Logging level. Default: `info` available: `debug`, `info`, `warn`, `error`
|
* `LOG_LEVEL` - Logging level. Default: `info` available: `debug`, `info`, `warn`, `error`
|
||||||
* `GRAPHQL_RETRIES_ENABLE` - Should retries be enabled? Default: `true`
|
* `GRAPHQL_RETRIES_ENABLE` - Should retries be enabled? Default: `false`
|
||||||
* `GRAPHQL_RETRIES_NUMBER` - Number of retries: Default: `1`
|
* `GRAPHQL_RETRIES_NUMBER` - Number of retries: Default: `3`
|
||||||
* `GRAPHQL_RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250`
|
* `GRAPHQL_RETRIES_DELAY` - Delay in retries in milliseconds. Default: `250`
|
||||||
|
* `GRAPHQL_RETRIES_PATTERNS` - Comma-separated error patterns that trigger retries. Default: `postgres,connection,timeout,transaction,could not,temporarily unavailable,deadlock`
|
||||||
|
|
||||||
### Modifiers on the fly
|
### Modifiers on the fly
|
||||||
|
|
||||||
* `gql.SetEndpoint('your-endpoint-url')` - modifies endpoint, without the need to set the environment variable
|
* `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
|
* `gql.SetOutput('byte')` - modifies output format, without the need to set the environment variable
|
||||||
|
|
||||||
|
### Retries
|
||||||
|
|
||||||
|
The library supports automatic retries for both HTTP-level errors and GraphQL errors that match configurable patterns.
|
||||||
|
|
||||||
|
**Retry behavior:**
|
||||||
|
- **HTTP errors** (status codes outside 200-299): Always retried if retries are enabled
|
||||||
|
- **GraphQL errors**: Only retried if the error message contains one of the configured patterns
|
||||||
|
- **Non-matching errors**: Fail immediately without retrying (e.g., validation errors, field not found)
|
||||||
|
|
||||||
|
**Configuring retryable patterns:**
|
||||||
|
|
||||||
|
By default, errors containing these patterns will trigger retries:
|
||||||
|
- `postgres` - Database connection/transaction errors
|
||||||
|
- `connection` - Network connection issues
|
||||||
|
- `timeout` - Request timeout errors
|
||||||
|
- `transaction` - Database transaction conflicts
|
||||||
|
- `could not` - Generic transient failures
|
||||||
|
- `temporarily unavailable` - Service unavailability
|
||||||
|
- `deadlock` - Database deadlocks
|
||||||
|
|
||||||
|
You can customize these patterns using the `GRAPHQL_RETRIES_PATTERNS` environment variable:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GRAPHQL_RETRIES_PATTERNS="postgres,connection,timeout,custom-error-pattern"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example:** To handle flaky Hasura database connections, enable retries:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export GRAPHQL_RETRIES_ENABLE=true
|
||||||
|
export GRAPHQL_RETRIES_NUMBER=3
|
||||||
|
export GRAPHQL_RETRIES_DELAY=250
|
||||||
|
```
|
||||||
|
|
||||||
|
This will retry GraphQL queries up to 3 times when postgres transaction errors occur, with exponential backoff starting at 250ms.
|
||||||
|
|
||||||
### Cache
|
### Cache
|
||||||
|
|
||||||
You have two options to enable the cache:
|
You have two options to enable the cache:
|
||||||
|
|||||||
+78
-19
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
@@ -32,6 +33,47 @@ func max(a, b int) int {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isRetryableError checks if GraphQL errors contain patterns that indicate a transient failure
|
||||||
|
// that should be retried (e.g., database connection issues, timeouts, transaction conflicts)
|
||||||
|
func isRetryableError(errors []struct {
|
||||||
|
Message interface{} `json:"message"`
|
||||||
|
}, patterns []string) bool {
|
||||||
|
if len(errors) == 0 || len(patterns) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, err := range errors {
|
||||||
|
// Try to extract error message as string
|
||||||
|
var msgStr string
|
||||||
|
switch msg := err.Message.(type) {
|
||||||
|
case string:
|
||||||
|
msgStr = msg
|
||||||
|
case map[string]interface{}:
|
||||||
|
// Handle nested error structures (e.g., {"message": "...", "extensions": {...}})
|
||||||
|
if nestedMsg, ok := msg["message"].(string); ok {
|
||||||
|
msgStr = nestedMsg
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// If we can't convert to string, try fmt.Sprint as fallback
|
||||||
|
msgStr = fmt.Sprint(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
if msgStr == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if error message contains any of the configured retry patterns (case-insensitive)
|
||||||
|
msgLower := strings.ToLower(msgStr)
|
||||||
|
for _, pattern := range patterns {
|
||||||
|
if strings.Contains(msgLower, strings.ToLower(pattern)) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// Shared HTTP transport with optimized settings
|
// Shared HTTP transport with optimized settings
|
||||||
defaultTransport = &http.Transport{
|
defaultTransport = &http.Transport{
|
||||||
@@ -331,6 +373,38 @@ func (qe *QueryExecutor) executeQuery() ([]byte, error) {
|
|||||||
return fmt.Errorf("error unmarshalling HTTP response: %w", err)
|
return fmt.Errorf("error unmarshalling HTTP response: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for GraphQL errors in the response
|
||||||
|
if len(queryResult.Errors) > 0 {
|
||||||
|
// Determine if this is a retryable error based on configured patterns
|
||||||
|
if isRetryableError(queryResult.Errors, qe.retries_patterns) {
|
||||||
|
// Log as warning since we'll retry
|
||||||
|
qe.Logger.Warning(&libpack_logger.LogMessage{
|
||||||
|
Message: "Retryable GraphQL error detected",
|
||||||
|
Pairs: map[string]interface{}{"errors": queryResult.Errors},
|
||||||
|
})
|
||||||
|
// Return error to trigger retry mechanism
|
||||||
|
return fmt.Errorf("retryable error executing query: %s", queryResult.Errors)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Non-retryable error - log and fail immediately without retrying
|
||||||
|
qe.Logger.Error(&libpack_logger.LogMessage{
|
||||||
|
Message: "Non-retryable GraphQL error",
|
||||||
|
Pairs: map[string]interface{}{"errors": queryResult.Errors},
|
||||||
|
})
|
||||||
|
// Use retry.Unrecoverable to skip remaining retry attempts
|
||||||
|
return retry.Unrecoverable(fmt.Errorf("error executing query: %s", queryResult.Errors))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for null data in the response
|
||||||
|
if queryResult.Data == nil {
|
||||||
|
qe.Logger.Error(&libpack_logger.LogMessage{
|
||||||
|
Message: "GraphQL query returned no data",
|
||||||
|
Pairs: map[string]interface{}{"error": "data field is null"},
|
||||||
|
})
|
||||||
|
// Null data is typically not retryable (indicates query structure issue)
|
||||||
|
return retry.Unrecoverable(errors.New("error executing query: no data"))
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
retry.OnRetry(func(n uint, err error) {
|
retry.OnRetry(func(n uint, err error) {
|
||||||
@@ -346,33 +420,18 @@ func (qe *QueryExecutor) executeQuery() ([]byte, error) {
|
|||||||
retry.LastErrorOnly(true),
|
retry.LastErrorOnly(true),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
qe.Logger.Debug(&libpack_logger.LogMessage{
|
qe.Logger.Error(&libpack_logger.LogMessage{
|
||||||
Message: "Error executing HTTP request",
|
Message: "Query execution failed after retries",
|
||||||
Pairs: map[string]interface{}{"error": err.Error()},
|
Pairs: map[string]interface{}{"error": err.Error()},
|
||||||
})
|
})
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(queryResult.Errors) > 0 {
|
// At this point, we have successfully executed the query and validated the response
|
||||||
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
|
// Marshal queryResult.Data
|
||||||
jsonData, err := json.Marshal(queryResult.Data)
|
jsonData, err := json.Marshal(queryResult.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
qe.Logger.Debug(&libpack_logger.LogMessage{
|
qe.Logger.Error(&libpack_logger.LogMessage{
|
||||||
Message: "Error marshalling query result",
|
Message: "Error marshalling query result",
|
||||||
Pairs: map[string]interface{}{"error": err.Error(), "data": queryResult.Data},
|
Pairs: map[string]interface{}{"error": err.Error(), "data": queryResult.Data},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package gql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gookit/goutil/envutil"
|
"github.com/gookit/goutil/envutil"
|
||||||
@@ -9,6 +10,22 @@ import (
|
|||||||
logging "github.com/lukaszraczylo/go-simple-graphql/logging"
|
logging "github.com/lukaszraczylo/go-simple-graphql/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// parseRetryPatterns parses a comma-separated string of retry patterns into a slice
|
||||||
|
func parseRetryPatterns(patterns string) []string {
|
||||||
|
if patterns == "" {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
parts := strings.Split(patterns, ",")
|
||||||
|
result := make([]string, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
trimmed := strings.TrimSpace(p)
|
||||||
|
if trimmed != "" {
|
||||||
|
result = append(result, trimmed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func NewConnection() (b *BaseClient) {
|
func NewConnection() (b *BaseClient) {
|
||||||
// Read LOG_LEVEL environment variable
|
// Read LOG_LEVEL environment variable
|
||||||
logLevelStr := envutil.Getenv("LOG_LEVEL", "info")
|
logLevelStr := envutil.Getenv("LOG_LEVEL", "info")
|
||||||
@@ -30,14 +47,16 @@ func NewConnection() (b *BaseClient) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
b = &BaseClient{
|
b = &BaseClient{
|
||||||
endpoint: envutil.Getenv("GRAPHQL_ENDPOINT", "https://api.github.com/graphql"),
|
endpoint: envutil.Getenv("GRAPHQL_ENDPOINT", "https://api.github.com/graphql"),
|
||||||
responseType: envutil.Getenv("GRAPHQL_OUTPUT", "string"),
|
responseType: envutil.Getenv("GRAPHQL_OUTPUT", "string"),
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
cache: cache.New(time.Duration(envutil.GetInt("GRAPHQL_CACHE_TTL", 5)) * time.Second),
|
cache: cache.New(time.Duration(envutil.GetInt("GRAPHQL_CACHE_TTL", 5)) * time.Second),
|
||||||
cache_global: envutil.GetBool("GRAPHQL_CACHE_ENABLED", false),
|
cache_global: envutil.GetBool("GRAPHQL_CACHE_ENABLED", false),
|
||||||
retries_enable: envutil.GetBool("GRAPHQL_RETRIES_ENABLE", false),
|
retries_enable: envutil.GetBool("GRAPHQL_RETRIES_ENABLE", false),
|
||||||
retries_delay: time.Duration(envutil.GetInt("GRAPHQL_RETRIES_DELAY", 250) * int(time.Millisecond)),
|
retries_delay: time.Duration(envutil.GetInt("GRAPHQL_RETRIES_DELAY", 250) * int(time.Millisecond)),
|
||||||
retries_number: envutil.GetInt("GRAPHQL_RETRIES_NUMBER", 3),
|
retries_number: envutil.GetInt("GRAPHQL_RETRIES_NUMBER", 3),
|
||||||
|
retries_patterns: parseRetryPatterns(envutil.Getenv("GRAPHQL_RETRIES_PATTERNS", "postgres,connection,timeout,transaction,could not,temporarily unavailable,deadlock")),
|
||||||
|
minify_queries: envutil.GetBool("GRAPHQL_MINIFY_QUERIES", true), // Default: enabled for production efficiency
|
||||||
}
|
}
|
||||||
b.client = b.createHttpClient()
|
b.client = b.createHttpClient()
|
||||||
b.Logger.Debug(&logging.LogMessage{
|
b.Logger.Debug(&logging.LogMessage{
|
||||||
@@ -63,3 +82,13 @@ func (b *BaseClient) SetOutput(responseType string) {
|
|||||||
func (b *BaseClient) SetHTTPClient(client *http.Client) {
|
func (b *BaseClient) SetHTTPClient(client *http.Client) {
|
||||||
b.client = client
|
b.client = client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseClient) SetQueryMinification(enabled bool) {
|
||||||
|
b.minify_queries = enabled
|
||||||
|
b.Logger.Debug(&logging.LogMessage{
|
||||||
|
Message: "GraphQL query minification setting updated",
|
||||||
|
Pairs: map[string]interface{}{
|
||||||
|
"minify_queries": enabled,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ func TestPerformanceRegression(t *testing.T) {
|
|||||||
}
|
}
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
// Should complete 10k hashes in under 25ms (accounts for race detection + coverage overhead)
|
// Should complete 10k hashes in under 40ms (accounts for race detection + coverage overhead)
|
||||||
if duration > 25*time.Millisecond {
|
if duration > 40*time.Millisecond {
|
||||||
t.Errorf("Hash performance regression: 10k hashes took %v, expected < 25ms", duration)
|
t.Errorf("Hash performance regression: 10k hashes took %v, expected < 40ms", duration)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -41,9 +41,9 @@ func TestPerformanceRegression(t *testing.T) {
|
|||||||
}
|
}
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
// Should complete 10k buffer operations in under 25ms (accounts for race detection + coverage overhead)
|
// Should complete 10k buffer operations in under 30ms (accounts for race detection + coverage overhead)
|
||||||
if duration > 25*time.Millisecond {
|
if duration > 30*time.Millisecond {
|
||||||
t.Errorf("Buffer pool performance regression: 10k operations took %v, expected < 25ms", duration)
|
t.Errorf("Buffer pool performance regression: 10k operations took %v, expected < 30ms", duration)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -105,9 +105,9 @@ func TestPerformanceRegression(t *testing.T) {
|
|||||||
}
|
}
|
||||||
duration := time.Since(start)
|
duration := time.Since(start)
|
||||||
|
|
||||||
// Should complete 1k query compilations in under 50ms
|
// Should complete 1k query compilations in under 65ms (accounts for minification overhead)
|
||||||
if duration > 50*time.Millisecond {
|
if duration > 65*time.Millisecond {
|
||||||
t.Errorf("Query compilation performance regression: 1k operations took %v, expected < 50ms", duration)
|
t.Errorf("Query compilation performance regression: 1k operations took %v, expected < 65ms", duration)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package gql
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
"github.com/gookit/goutil"
|
"github.com/gookit/goutil"
|
||||||
@@ -9,6 +10,129 @@ import (
|
|||||||
libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging"
|
libpack_logger "github.com/lukaszraczylo/go-simple-graphql/logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// minifyGraphQLQuery removes unnecessary whitespace from GraphQL queries while preserving string literals
|
||||||
|
func minifyGraphQLQuery(query string) string {
|
||||||
|
if query == "" {
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pre-allocate with estimated final size (typically 70-80% of original)
|
||||||
|
var result strings.Builder
|
||||||
|
result.Grow(len(query) * 3 / 4)
|
||||||
|
|
||||||
|
inString := false
|
||||||
|
stringChar := byte(0) // Track whether we're in single or double quotes
|
||||||
|
queryBytes := []byte(query) // Convert to bytes once for faster access
|
||||||
|
length := len(queryBytes)
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for i < length {
|
||||||
|
char := queryBytes[i]
|
||||||
|
|
||||||
|
// Handle string literals - preserve everything inside quotes
|
||||||
|
if !inString && (char == '"' || char == '\'') {
|
||||||
|
inString = true
|
||||||
|
stringChar = char
|
||||||
|
result.WriteByte(char)
|
||||||
|
i++
|
||||||
|
} else if inString && char == stringChar {
|
||||||
|
// Check if this quote is escaped (optimized version)
|
||||||
|
escaped := false
|
||||||
|
backslashCount := 0
|
||||||
|
for j := i - 1; j >= 0 && queryBytes[j] == '\\'; j-- {
|
||||||
|
backslashCount++
|
||||||
|
}
|
||||||
|
escaped = backslashCount%2 == 1
|
||||||
|
|
||||||
|
if !escaped {
|
||||||
|
inString = false
|
||||||
|
stringChar = 0
|
||||||
|
}
|
||||||
|
result.WriteByte(char)
|
||||||
|
i++
|
||||||
|
} else if inString {
|
||||||
|
// Inside string literal - preserve everything including whitespace
|
||||||
|
result.WriteByte(char)
|
||||||
|
i++
|
||||||
|
} else if isWhitespace(char) {
|
||||||
|
// Handle whitespace outside strings - optimized version
|
||||||
|
// Skip all consecutive whitespace
|
||||||
|
for i < length && isWhitespace(queryBytes[i]) {
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
if i < length && result.Len() > 0 {
|
||||||
|
// Get previous character from result buffer
|
||||||
|
resultStr := result.String()
|
||||||
|
prevChar := resultStr[len(resultStr)-1]
|
||||||
|
nextChar := queryBytes[i]
|
||||||
|
|
||||||
|
// Optimized space preservation logic
|
||||||
|
needsSpace := false
|
||||||
|
|
||||||
|
// Between alphanumeric characters (field names, variables, types)
|
||||||
|
if isAlphaNumeric(prevChar) && isAlphaNumeric(nextChar) {
|
||||||
|
needsSpace = true
|
||||||
|
} else if prevChar == '}' && isAlphaNumeric(nextChar) {
|
||||||
|
// Between closing brace and field name: "} field_name"
|
||||||
|
needsSpace = true
|
||||||
|
} else if prevChar == ':' && (isAlphaNumeric(nextChar) || nextChar == '$' || nextChar == '"' || nextChar == '\'' || nextChar == '{') {
|
||||||
|
// Around colons in type definitions and arguments: "$var: Type", "field: value"
|
||||||
|
needsSpace = true
|
||||||
|
} else if (isAlphaNumeric(prevChar) || prevChar == '_') && nextChar == ':' {
|
||||||
|
needsSpace = true
|
||||||
|
} else if prevChar == ',' && nextChar == '$' {
|
||||||
|
// After commas in argument lists: "arg1: value1, arg2: value2" - only for variables
|
||||||
|
needsSpace = true
|
||||||
|
} else if prevChar == ',' && isAlphaNumeric(nextChar) {
|
||||||
|
// Special case: space after comma when followed by field that has colon
|
||||||
|
// Look ahead to see if this field has a colon (limited lookahead for performance)
|
||||||
|
colonFound := false
|
||||||
|
lookAheadLimit := min(i+15, length) // Reduced lookahead for performance
|
||||||
|
for j := i + 1; j < lookAheadLimit; j++ {
|
||||||
|
if queryBytes[j] == ':' {
|
||||||
|
colonFound = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if !isAlphaNumeric(queryBytes[j]) && queryBytes[j] != '_' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if colonFound {
|
||||||
|
needsSpace = true
|
||||||
|
}
|
||||||
|
} else if (prevChar == '!' && isAlphaNumeric(nextChar)) || (isAlphaNumeric(prevChar) && nextChar == '!') {
|
||||||
|
// Around exclamation marks in type definitions: "Type !"
|
||||||
|
needsSpace = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if needsSpace {
|
||||||
|
result.WriteByte(' ')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Regular character - just copy it
|
||||||
|
result.WriteByte(char)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// isAlphaNumeric checks if a character is alphanumeric or underscore
|
||||||
|
func isAlphaNumeric(char byte) bool {
|
||||||
|
return (char >= 'a' && char <= 'z') ||
|
||||||
|
(char >= 'A' && char <= 'Z') ||
|
||||||
|
(char >= '0' && char <= '9') ||
|
||||||
|
char == '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
// isWhitespace checks if a character is whitespace
|
||||||
|
func isWhitespace(char byte) bool {
|
||||||
|
return char == ' ' || char == '\t' || char == '\n' || char == '\r'
|
||||||
|
}
|
||||||
|
|
||||||
func (b *BaseClient) convertToJSON(v any) []byte {
|
func (b *BaseClient) convertToJSON(v any) []byte {
|
||||||
// Estimate size based on query structure for better buffer selection
|
// Estimate size based on query structure for better buffer selection
|
||||||
estimatedSize := 512 // Base size
|
estimatedSize := 512 // Base size
|
||||||
@@ -112,9 +236,31 @@ func (b *BaseClient) compileQuery(queryPartials ...any) *Query {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct query object once
|
// Apply query minification if enabled (default: true)
|
||||||
|
finalQuery := query
|
||||||
|
if b.minify_queries {
|
||||||
|
originalSize := len(query)
|
||||||
|
minifiedQuery := minifyGraphQLQuery(query)
|
||||||
|
minifiedSize := len(minifiedQuery)
|
||||||
|
|
||||||
|
// Log the minification results if there was a reduction
|
||||||
|
if originalSize != minifiedSize {
|
||||||
|
b.Logger.Debug(&libpack_logger.LogMessage{
|
||||||
|
Message: "GraphQL query minified",
|
||||||
|
Pairs: map[string]interface{}{
|
||||||
|
"original_size": originalSize,
|
||||||
|
"minified_size": minifiedSize,
|
||||||
|
"size_reduction": originalSize - minifiedSize,
|
||||||
|
"reduction_pct": float64(originalSize-minifiedSize) / float64(originalSize) * 100,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
finalQuery = minifiedQuery
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct query object once with final query
|
||||||
q := &Query{
|
q := &Query{
|
||||||
Query: query,
|
Query: finalQuery,
|
||||||
Variables: variables,
|
Variables: variables,
|
||||||
}
|
}
|
||||||
q.JsonQuery = b.convertToJSON(q)
|
q.JsonQuery = b.convertToJSON(q)
|
||||||
|
|||||||
@@ -0,0 +1,410 @@
|
|||||||
|
package gql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
assertions "github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMinifyGraphQLQuery(t *testing.T) {
|
||||||
|
assert := assertions.New(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty_query",
|
||||||
|
input: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "simple_query_no_whitespace",
|
||||||
|
input: "query{viewer{login}}",
|
||||||
|
expected: "query{viewer{login}}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query_with_newlines_and_tabs",
|
||||||
|
input: `query {
|
||||||
|
viewer {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
expected: "query{viewer{login}}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mutation_with_variables",
|
||||||
|
input: `mutation storeMessage($content: String!, $id: Int!) {
|
||||||
|
insert_message(content: $content, id: $id) {
|
||||||
|
id
|
||||||
|
created_at
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
expected: "mutation storeMessage($content: String!, $id: Int!){insert_message(content: $content, id: $id){id created_at}}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query_with_string_literals_preserved",
|
||||||
|
input: `mutation storeMessage($description: String!) {
|
||||||
|
insert_message(description: $description, content: "This is a test\nwith newlines\tand tabs") {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
expected: `mutation storeMessage($description: String!){insert_message(description: $description, content: "This is a test\nwith newlines\tand tabs"){id}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complex_query_with_nested_fields",
|
||||||
|
input: `query Dragons {
|
||||||
|
dragons {
|
||||||
|
name
|
||||||
|
type
|
||||||
|
abilities {
|
||||||
|
name
|
||||||
|
power
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
expected: "query Dragons{dragons{name type abilities{name power}}}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query_with_single_quotes",
|
||||||
|
input: `mutation test($value: String!) {
|
||||||
|
insert_test(value: $value, note: 'Single quoted string with\nnewlines') {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
expected: `mutation test($value: String!){insert_test(value: $value, note: 'Single quoted string with\nnewlines'){id}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query_with_escaped_quotes",
|
||||||
|
input: `mutation test($value: String!) {
|
||||||
|
insert_test(value: $value, note: "String with \"escaped quotes\" inside") {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
expected: `mutation test($value: String!){insert_test(value: $value, note: "String with \"escaped quotes\" inside"){id}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "production_case_from_trailing_garbage_test",
|
||||||
|
input: `mutation storeProcessedMessage($content: jsonb, $telegram_msg_id: bigint!, $hasMedia: Boolean!, $urls: jsonb, $groupID: bigint!, $userID: bigint!, $entities: jsonb) {
|
||||||
|
insert_tg_messages_one(object: {content: $content, telegram_msg_id: $telegram_msg_id, has_media: $hasMedia, urls: $urls, group_id: $groupID, user_id: $userID, entities: $entities}) {
|
||||||
|
id
|
||||||
|
tg_group {
|
||||||
|
tg_group_admins_aggregate(where: {user_id: {_eq: $userID}}) {
|
||||||
|
aggregate {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set_spam_ai
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
expected: "mutation storeProcessedMessage($content: jsonb, $telegram_msg_id: bigint!, $hasMedia: Boolean!, $urls: jsonb, $groupID: bigint!, $userID: bigint!, $entities: jsonb){insert_tg_messages_one(object: {content: $content,telegram_msg_id: $telegram_msg_id, has_media: $hasMedia, urls: $urls, group_id: $groupID, user_id: $userID, entities: $entities}){id tg_group{tg_group_admins_aggregate(where: {user_id: {_eq: $userID}}){aggregate{count}} set_spam_ai}}}",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query_with_mixed_quotes_and_escapes",
|
||||||
|
input: `mutation test {
|
||||||
|
insert_test(
|
||||||
|
single: 'Single quote with "double" inside',
|
||||||
|
double: "Double quote with 'single' inside",
|
||||||
|
escaped: "Escaped \"double\" quotes"
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
expected: `mutation test{insert_test(single: 'Single quote with "double" inside', double: "Double quote with 'single' inside", escaped: "Escaped \"double\" quotes"){id}}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query_with_unicode_in_strings",
|
||||||
|
input: `mutation storeMessage($user_name: String!) {
|
||||||
|
insert_message(
|
||||||
|
user_name: $user_name,
|
||||||
|
description: "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈"
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
expected: `mutation storeMessage($user_name: String!){insert_message(user_name: $user_name, description: "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈"){id}}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := minifyGraphQLQuery(tt.input)
|
||||||
|
assert.Equal(tt.expected, result, "Minified query should match expected output")
|
||||||
|
|
||||||
|
// Verify that the result doesn't contain unnecessary whitespace
|
||||||
|
assert.False(strings.Contains(result, "\n"), "Result should not contain newlines")
|
||||||
|
assert.False(strings.Contains(result, "\t"), "Result should not contain tabs")
|
||||||
|
|
||||||
|
// Verify that string literals are preserved (if they exist in expected)
|
||||||
|
if strings.Contains(tt.expected, `\n`) {
|
||||||
|
assert.True(strings.Contains(result, `\n`), "Newlines in string literals should be preserved")
|
||||||
|
}
|
||||||
|
if strings.Contains(tt.expected, `\t`) {
|
||||||
|
assert.True(strings.Contains(result, `\t`), "Tabs in string literals should be preserved")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMinifyGraphQLQuerySizeReduction(t *testing.T) {
|
||||||
|
assert := assertions.New(t)
|
||||||
|
|
||||||
|
// Test with a heavily formatted query
|
||||||
|
heavyQuery := `
|
||||||
|
mutation storeProcessedMessage(
|
||||||
|
$content: jsonb,
|
||||||
|
$telegram_msg_id: bigint!,
|
||||||
|
$hasMedia: Boolean!,
|
||||||
|
$urls: jsonb,
|
||||||
|
$groupID: bigint!,
|
||||||
|
$userID: bigint!,
|
||||||
|
$entities: jsonb
|
||||||
|
) {
|
||||||
|
insert_tg_messages_one(
|
||||||
|
object: {
|
||||||
|
content: $content,
|
||||||
|
telegram_msg_id: $telegram_msg_id,
|
||||||
|
has_media: $hasMedia,
|
||||||
|
urls: $urls,
|
||||||
|
group_id: $groupID,
|
||||||
|
user_id: $userID,
|
||||||
|
entities: $entities
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
tg_group {
|
||||||
|
tg_group_admins_aggregate(
|
||||||
|
where: {
|
||||||
|
user_id: {
|
||||||
|
_eq: $userID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
aggregate {
|
||||||
|
count
|
||||||
|
}
|
||||||
|
}
|
||||||
|
set_spam_ai
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
originalSize := len(heavyQuery)
|
||||||
|
minified := minifyGraphQLQuery(heavyQuery)
|
||||||
|
minifiedSize := len(minified)
|
||||||
|
|
||||||
|
t.Logf("Original size: %d bytes", originalSize)
|
||||||
|
t.Logf("Minified size: %d bytes", minifiedSize)
|
||||||
|
t.Logf("Size reduction: %d bytes (%.1f%%)", originalSize-minifiedSize, float64(originalSize-minifiedSize)/float64(originalSize)*100)
|
||||||
|
|
||||||
|
// Should achieve significant size reduction
|
||||||
|
assert.True(minifiedSize < originalSize, "Minified query should be smaller than original")
|
||||||
|
reductionPercent := float64(originalSize-minifiedSize) / float64(originalSize) * 100
|
||||||
|
assert.True(reductionPercent > 30, "Should achieve at least 30% size reduction for heavily formatted queries")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBaseClientQueryMinification(t *testing.T) {
|
||||||
|
assert := assertions.New(t)
|
||||||
|
|
||||||
|
t.Run("should_minify_queries_by_default", func(t *testing.T) {
|
||||||
|
client := CreateTestClient()
|
||||||
|
|
||||||
|
// Verify minification is enabled by default
|
||||||
|
assert.True(client.minify_queries, "Query minification should be enabled by default")
|
||||||
|
|
||||||
|
query := `query {
|
||||||
|
viewer {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
variables := map[string]interface{}{}
|
||||||
|
|
||||||
|
compiledQuery := client.compileQuery(query, variables)
|
||||||
|
assert.NotNil(compiledQuery)
|
||||||
|
|
||||||
|
// The compiled query should be minified (no newlines/tabs)
|
||||||
|
assert.False(strings.Contains(compiledQuery.Query, "\n"), "Compiled query should not contain newlines")
|
||||||
|
assert.False(strings.Contains(compiledQuery.Query, "\t"), "Compiled query should not contain tabs")
|
||||||
|
assert.Equal("query{viewer{login}}", compiledQuery.Query)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should_respect_minification_disabled", func(t *testing.T) {
|
||||||
|
client := CreateTestClient()
|
||||||
|
client.SetQueryMinification(false)
|
||||||
|
|
||||||
|
// Verify minification is disabled
|
||||||
|
assert.False(client.minify_queries, "Query minification should be disabled")
|
||||||
|
|
||||||
|
query := `query {
|
||||||
|
viewer {
|
||||||
|
login
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
variables := map[string]interface{}{}
|
||||||
|
|
||||||
|
compiledQuery := client.compileQuery(query, variables)
|
||||||
|
assert.NotNil(compiledQuery)
|
||||||
|
|
||||||
|
// The compiled query should NOT be minified (preserve original formatting)
|
||||||
|
assert.Equal(query, compiledQuery.Query, "Query should remain unchanged when minification is disabled")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should_handle_string_literals_correctly", func(t *testing.T) {
|
||||||
|
client := CreateTestClient()
|
||||||
|
|
||||||
|
query := `mutation test($value: String!) {
|
||||||
|
insert_test(
|
||||||
|
value: $value,
|
||||||
|
note: "This is a test\nwith newlines\tand tabs"
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"value": "test",
|
||||||
|
}
|
||||||
|
|
||||||
|
compiledQuery := client.compileQuery(query, variables)
|
||||||
|
assert.NotNil(compiledQuery)
|
||||||
|
|
||||||
|
// String literals should preserve their content
|
||||||
|
assert.Contains(compiledQuery.Query, `"This is a test\nwith newlines\tand tabs"`, "String literals should be preserved")
|
||||||
|
// But the query structure should be minified
|
||||||
|
assert.False(strings.Contains(strings.Replace(compiledQuery.Query, `"This is a test\nwith newlines\tand tabs"`, "", 1), "\n"), "Query structure should be minified")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should_demonstrate_size_reduction_with_production_case", func(t *testing.T) {
|
||||||
|
client := CreateTestClient()
|
||||||
|
|
||||||
|
// Use the production case from trailing garbage test
|
||||||
|
variables := map[string]interface{}{
|
||||||
|
"group_description": "This a group for serious buyers and sellers\nOf uber eats and Door dash account \nRippers are not allowed in this group😈",
|
||||||
|
"user_name": "2eba巴结",
|
||||||
|
}
|
||||||
|
|
||||||
|
originalQuery := `mutation storeMessage(
|
||||||
|
$group_description: String!,
|
||||||
|
$user_name: String!
|
||||||
|
) {
|
||||||
|
insert_message(
|
||||||
|
group_description: $group_description,
|
||||||
|
user_name: $user_name
|
||||||
|
) {
|
||||||
|
id
|
||||||
|
created_at
|
||||||
|
updated_at
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
|
||||||
|
// Test with minification enabled
|
||||||
|
client.SetQueryMinification(true)
|
||||||
|
compiledQueryMinified := client.compileQuery(originalQuery, variables)
|
||||||
|
minifiedJsonSize := len(compiledQueryMinified.JsonQuery)
|
||||||
|
|
||||||
|
// Test with minification disabled
|
||||||
|
client.SetQueryMinification(false)
|
||||||
|
compiledQueryOriginal := client.compileQuery(originalQuery, variables)
|
||||||
|
originalJsonSize := len(compiledQueryOriginal.JsonQuery)
|
||||||
|
|
||||||
|
t.Logf("Original JSON size: %d bytes", originalJsonSize)
|
||||||
|
t.Logf("Minified JSON size: %d bytes", minifiedJsonSize)
|
||||||
|
t.Logf("Size reduction: %d bytes", originalJsonSize-minifiedJsonSize)
|
||||||
|
|
||||||
|
// Should achieve size reduction
|
||||||
|
assert.True(minifiedJsonSize < originalJsonSize, "Minified JSON should be smaller")
|
||||||
|
|
||||||
|
// Both should be valid JSON
|
||||||
|
assert.True(len(compiledQueryMinified.JsonQuery) > 0, "Minified query should produce valid JSON")
|
||||||
|
assert.True(len(compiledQueryOriginal.JsonQuery) > 0, "Original query should produce valid JSON")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsAlphaNumeric(t *testing.T) {
|
||||||
|
assert := assertions.New(t)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
char byte
|
||||||
|
expected bool
|
||||||
|
}{
|
||||||
|
{'a', true},
|
||||||
|
{'z', true},
|
||||||
|
{'A', true},
|
||||||
|
{'Z', true},
|
||||||
|
{'0', true},
|
||||||
|
{'9', true},
|
||||||
|
{'_', true},
|
||||||
|
{' ', false},
|
||||||
|
{'\n', false},
|
||||||
|
{'\t', false},
|
||||||
|
{'{', false},
|
||||||
|
{'}', false},
|
||||||
|
{'(', false},
|
||||||
|
{')', false},
|
||||||
|
{':', false},
|
||||||
|
{'"', false},
|
||||||
|
{'\'', false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(string(tt.char), func(t *testing.T) {
|
||||||
|
result := isAlphaNumeric(tt.char)
|
||||||
|
assert.Equal(tt.expected, result, "isAlphaNumeric should correctly identify character type")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryMinificationEnvironmentVariable(t *testing.T) {
|
||||||
|
assert := assertions.New(t)
|
||||||
|
|
||||||
|
t.Run("should_respect_GRAPHQL_MINIFY_QUERIES_env_var", func(t *testing.T) {
|
||||||
|
// This test would require setting environment variables
|
||||||
|
// For now, we'll test the default behavior
|
||||||
|
client := NewConnection()
|
||||||
|
|
||||||
|
// Should be enabled by default
|
||||||
|
assert.True(client.minify_queries, "Query minification should be enabled by default")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryMinificationEdgeCases(t *testing.T) {
|
||||||
|
assert := assertions.New(t)
|
||||||
|
|
||||||
|
t.Run("should_handle_empty_query", func(t *testing.T) {
|
||||||
|
result := minifyGraphQLQuery("")
|
||||||
|
assert.Equal("", result, "Empty query should remain empty")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should_handle_query_with_only_whitespace", func(t *testing.T) {
|
||||||
|
result := minifyGraphQLQuery(" \n\t \n ")
|
||||||
|
assert.Equal("", result, "Query with only whitespace should become empty")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should_handle_nested_quotes", func(t *testing.T) {
|
||||||
|
query := `mutation test {
|
||||||
|
insert_test(value: "String with \"nested quotes\" and 'mixed' quotes") {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
result := minifyGraphQLQuery(query)
|
||||||
|
expected := `mutation test{insert_test(value: "String with \"nested quotes\" and 'mixed' quotes"){id}}`
|
||||||
|
assert.Equal(expected, result, "Should handle nested quotes correctly")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("should_preserve_spaces_between_keywords", func(t *testing.T) {
|
||||||
|
query := `query test($var: String!) {
|
||||||
|
field(where: {id: {_eq: $var}}) {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
result := minifyGraphQLQuery(query)
|
||||||
|
|
||||||
|
// Should preserve necessary spaces
|
||||||
|
assert.Contains(result, "query test", "Should preserve space between 'query' and 'test'")
|
||||||
|
assert.Contains(result, "$var: String!", "Should preserve space in variable declaration")
|
||||||
|
assert.Contains(result, "_eq: $var", "Should preserve space around operators")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -124,7 +124,7 @@ func (suite *QueryOptimizationTestSuite) TestDoubleCompilationFix() {
|
|||||||
// This should not cause double compilation
|
// This should not cause double compilation
|
||||||
compiledQuery := suite.client.compileQuery(query, variables)
|
compiledQuery := suite.client.compileQuery(query, variables)
|
||||||
suite.NotNil(compiledQuery)
|
suite.NotNil(compiledQuery)
|
||||||
suite.Equal(query, compiledQuery.Query)
|
suite.Equal("query GetUser($id: ID!){user(id: $id){id name}}", compiledQuery.Query)
|
||||||
|
|
||||||
// Variables should still contain the flags at this point (they're cleaned in Query method)
|
// Variables should still contain the flags at this point (they're cleaned in Query method)
|
||||||
suite.Contains(compiledQuery.Variables, "gqlcache")
|
suite.Contains(compiledQuery.Variables, "gqlcache")
|
||||||
|
|||||||
+5
-5
@@ -60,8 +60,8 @@ func (suite *Tests) TestBaseClient_compileQuery() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantQ: &Query{
|
wantQ: &Query{
|
||||||
Query: `query { viewer { login } }`,
|
Query: `query{viewer{login}}`,
|
||||||
JsonQuery: []byte(`{"query":"query { viewer { login } }"}`),
|
JsonQuery: []byte(`{"query":"query{viewer{login}}"}`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -76,12 +76,12 @@ func (suite *Tests) TestBaseClient_compileQuery() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
wantQ: &Query{
|
wantQ: &Query{
|
||||||
Query: `query { viewer { login } }`,
|
Query: `query{viewer{login}}`,
|
||||||
Variables: map[string]interface{}{
|
Variables: map[string]interface{}{
|
||||||
"login": "lukaszraczylo",
|
"login": "lukaszraczylo",
|
||||||
"first": 10,
|
"first": 10,
|
||||||
},
|
},
|
||||||
JsonQuery: []byte(`{"variables":{"first":10,"login":"lukaszraczylo"},"query":"query { viewer { login } }"}`),
|
JsonQuery: []byte(`{"variables":{"first":10,"login":"lukaszraczylo"},"query":"query{viewer{login}}"}`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -231,7 +231,7 @@ func (suite *Tests) TestBaseClient_compileQuery_errors() {
|
|||||||
|
|
||||||
result := b.compileQuery("query { test }", "invalid_variables")
|
result := b.compileQuery("query { test }", "invalid_variables")
|
||||||
assert.NotNil(result)
|
assert.NotNil(result)
|
||||||
assert.Equal("query { test }", result.Query)
|
assert.Equal("query{test}", result.Query)
|
||||||
assert.Nil(result.Variables)
|
assert.Nil(result.Variables)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-10
@@ -9,16 +9,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type BaseClient struct {
|
type BaseClient struct {
|
||||||
cache *cache.Cache
|
cache *cache.Cache
|
||||||
Logger *logging.Logger
|
Logger *logging.Logger
|
||||||
client *http.Client
|
client *http.Client
|
||||||
endpoint string
|
endpoint string
|
||||||
responseType string
|
responseType string
|
||||||
retries_delay time.Duration
|
retries_delay time.Duration
|
||||||
retries_number int
|
retries_number int
|
||||||
MaxGoRoutines int
|
retries_patterns []string // Error patterns that should trigger retries
|
||||||
cache_global bool
|
MaxGoRoutines int
|
||||||
retries_enable bool
|
cache_global bool
|
||||||
|
retries_enable bool
|
||||||
|
minify_queries bool // Enable GraphQL query minification (default: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
|
|||||||
+5
-4
@@ -60,9 +60,10 @@ func CleanupTestCache() {
|
|||||||
// CreateTestClient creates a test client with shared resources
|
// CreateTestClient creates a test client with shared resources
|
||||||
func CreateTestClient() *BaseClient {
|
func CreateTestClient() *BaseClient {
|
||||||
return &BaseClient{
|
return &BaseClient{
|
||||||
Logger: GetTestLogger(),
|
Logger: GetTestLogger(),
|
||||||
cache: GetTestCache(),
|
cache: GetTestCache(),
|
||||||
endpoint: "https://example.com/graphql",
|
endpoint: "https://example.com/graphql",
|
||||||
responseType: "mapstring",
|
responseType: "mapstring",
|
||||||
|
minify_queries: true, // Enable query minification by default for tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func TestTrailingGarbageReproduction(t *testing.T) {
|
|||||||
return v
|
return v
|
||||||
},
|
},
|
||||||
queryMod: func(q string) string { return q },
|
queryMod: func(q string) string { return q },
|
||||||
expectSize: 356,
|
expectSize: 350,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "with_additional_headers_in_variables",
|
name: "with_additional_headers_in_variables",
|
||||||
@@ -424,7 +424,7 @@ func TestPotentialRootCauses(t *testing.T) {
|
|||||||
|
|
||||||
// The JSON should be the same regardless of Content-Type header
|
// The JSON should be the same regardless of Content-Type header
|
||||||
assert.True(json.Valid(compiledQuery.JsonQuery))
|
assert.True(json.Valid(compiledQuery.JsonQuery))
|
||||||
assert.Equal(356, len(compiledQuery.JsonQuery)) // Should be consistent
|
assert.Equal(350, len(compiledQuery.JsonQuery)) // Updated for improved minification
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user