graph

package module
v1.0.9 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 11, 2025 License: MIT Imports: 17 Imported by: 0

README

go-graph

A modern, secure GraphQL handler for Go with built-in authentication, validation, and an intuitive builder API.

Features

  • 🚀 Zero Config Start - Default hello world schema included
  • 🔧 Type-Safe Resolvers - Compile-time type safety with generic resolvers
  • 🎯 Type-Safe Arguments - Automatic argument parsing with NewArgsResolver
  • 🏗️ Fluent Builder API - Clean, intuitive schema construction
  • 🔐 Built-in Auth - Automatic Bearer token extraction
  • 🛡️ Security First - Query depth, complexity, and introspection protection
  • 🧹 Response Sanitization - Remove field suggestions from errors
  • 🎭 Middleware System - Built-in logging, auth, caching + custom middleware support
  • Framework Agnostic - Works with net/http, Gin, or any framework
  • High Performance - ~60μs per request, 100k+ RPS capable

Built on top of graphql-go.

Installation

go get github.com/paulmanoni/go-graph

Quick Start

Option 1: Default Schema (Zero Config)

Start immediately with a built-in hello world schema:

package main

import (
    "log"
    "net/http"
    "github.com/paulmanoni/go-graph"
)

func main() {
    // No schema needed! Includes default hello query & echo mutation
    handler := graph.NewHTTP(&graph.GraphContext{
        Playground: true,
        DEBUG:      true,
    })

    http.Handle("/graphql", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

Test it:

# Query
{ hello }

# Mutation
mutation { echo(message: "test") }

Use the fluent builder API for clean, type-safe schema construction:

package main

import (
    "log"
    "net/http"
    "github.com/paulmanoni/go-graph"
)

type User struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

// Define your queries with type-safe resolvers
func getHello() graph.QueryField {
    return graph.NewResolver[string]("hello").
        WithResolver(func(p graph.ResolveParams) (*string, error) {
            msg := "Hello, World!"
            return &msg, nil
        }).BuildQuery()
}

func getUser() graph.QueryField {
    return graph.NewResolver[User]("user").
        WithArgs(graphql.FieldConfigArgument{
            "id": &graphql.ArgumentConfig{
                Type: graphql.String,
            },
        }).
        WithResolver(func(p graph.ResolveParams) (*User, error) {
            id, _ := graph.GetArgString(p, "id")
            return &User{ID: id, Name: "Alice"}, nil
        }).BuildQuery()
}

func main() {
    handler := graph.NewHTTP(&graph.GraphContext{
        SchemaParams: &graph.SchemaBuilderParams{
            QueryFields: []graph.QueryField{
                getHello(),
                getUser(),
            },
            MutationFields: []graph.MutationField{},
        },
        Playground: true,
        DEBUG:      false,
    })

    http.Handle("/graphql", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}
Option 3: Custom Schema

Bring your own graphql-go schema:

import "github.com/graphql-go/graphql"

schema, _ := graphql.NewSchema(graphql.SchemaConfig{
    Query: graphql.NewObject(graphql.ObjectConfig{
        Name: "Query",
        Fields: graphql.Fields{
            "hello": &graphql.Field{
                Type: graphql.String,
                Resolve: func(p graph.ResolveParams) (interface{}, error) {
                    return "world", nil
                },
            },
        },
    }),
})

handler := graph.NewHTTP(&graph.GraphContext{
    Schema:     &schema,
    Playground: true,
})

Authentication

Automatic Bearer Token Extraction

Token is automatically extracted from Authorization: Bearer <token> header and available in all resolvers:

handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{
        QueryFields: []graph.QueryField{
            getProtectedQuery(),
        },
    },

    // Optional: Fetch user details from token
    UserDetailsFn: func(token string) (interface{}, error) {
        // Validate JWT, query database, etc.
        user, err := validateAndGetUser(token)
        return user, err
    },
})

Access in resolvers:

func getProtectedQuery() graph.QueryField {
    return graph.NewResolver[User]("me").
        WithResolver(func(p graph.ResolveParams) (*User, error) {
            // Get token
            token, err := graph.GetRootString(p, "token")
            if err != nil {
                return nil, fmt.Errorf("authentication required")
            }

            // Get user details (if UserDetailsFn provided)
            var user User
            if err := graph.GetRootInfo(p, "details", &user); err != nil {
                return nil, err
            }

            return &user, nil
        }).BuildQuery()
}
Custom Token Extraction

Extract tokens from cookies, custom headers, or query params:

handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{...},

    TokenExtractorFn: func(r *http.Request) string {
        // From cookie
        if cookie, err := r.Cookie("auth_token"); err == nil {
            return cookie.Value
        }

        // From custom header
        if apiKey := r.Header.Get("X-API-Key"); apiKey != "" {
            return apiKey
        }

        // From query param
        return r.URL.Query().Get("token")
    },

    UserDetailsFn: func(token string) (interface{}, error) {
        return getUserByToken(token)
    },
})

Security Features

Production Setup

Enable all security features for production:

handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams:       &graph.SchemaBuilderParams{...},
    DEBUG:              false,  // Enable security features
    EnableValidation:   true,   // Validate queries
    EnableSanitization: true,   // Sanitize errors
    Playground:         false,  // Disable playground

    UserDetailsFn: func(token string) (interface{}, error) {
        return validateAndGetUser(token)
    },
})
Validation Rules (when EnableValidation: true)
  • Max Query Depth: 10 levels
  • Max Aliases: 4 per query
  • Max Complexity: 200
  • Introspection: Disabled (blocks __schema and __type)
Response Sanitization (when EnableSanitization: true)

Removes field suggestions from error messages:

Before:

{
  "errors": [{
    "message": "Cannot query field \"nam\". Did you mean \"name\"?"
  }]
}

After:

{
  "errors": [{
    "message": "Cannot query field \"nam\"."
  }]
}
Debug Mode

Use DEBUG: true during development to skip all validation and sanitization:

handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{...},
    DEBUG:        true,  // Disables validation & sanitization
    Playground:   true,  // Enable playground for testing
})

Helper Functions

Extracting Arguments
// String argument
name, err := graph.GetArgString(p, "name")

// Int argument
age, err := graph.GetArgInt(p, "age")

// Bool argument
active, err := graph.GetArgBool(p, "active")

// Complex type
var input CreateUserInput
err := graph.GetArg(p, "input", &input)
Accessing Root Values
// Get token
token, err := graph.GetRootString(p, "token")

// Get user details
var user User
err := graph.GetRootInfo(p, "details", &user)

Type-Safe Resolvers

The WithResolver method provides compile-time type safety by accepting a function that returns *T instead of interface{}:

// ✅ Type-safe - returns *User
graph.NewResolver[User]("user").
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        id, _ := graph.GetArgString(p, "id")
        user := db.GetUserByID(id)  // Most ORMs return *User
        return user, nil             // No type assertions needed!
    }).BuildQuery()

// ✅ Works with lists - returns *[]User
graph.NewResolver[User]("users").
    AsList().
    WithResolver(func(p graph.ResolveParams) (*[]User, error) {
        users := db.ListUsers()
        return &users, nil
    }).BuildQuery()

// ✅ Works with primitives - returns *string
graph.NewResolver[string]("message").
    WithResolver(func(p graph.ResolveParams) (*string, error) {
        msg := "Hello!"
        return &msg, nil
    }).BuildQuery()

Benefits:

  • ✅ No type assertions or casts needed
  • ✅ Compiler catches type mismatches at build time
  • ✅ Better IDE autocomplete and refactoring
  • ✅ Cleaner, more readable code
  • ✅ Works with pointers (can return nil for not found)
Real-World Example
type Post struct {
    ID       int    `json:"id"`
    Title    string `json:"title"`
    AuthorID int    `json:"authorId"`
}

// Type-safe query
func getPost() graph.QueryField {
    return graph.NewResolver[Post]("post").
        WithArgs(graphql.FieldConfigArgument{
            "id": &graphql.ArgumentConfig{Type: graphql.Int},
        }).
        WithResolver(func(p graph.ResolveParams) (*Post, error) {
            id, err := graph.GetArgInt(p, "id")
            if err != nil {
                return nil, err
            }

            post, err := postService.GetByID(id)
            if err != nil {
                return nil, err
            }

            // Return *Post directly - no type assertions!
            return post, nil
        }).BuildQuery()
}

// Type-safe list query
func getPosts() graph.QueryField {
    return graph.NewResolver[Post]("posts").
        AsList().
        WithResolver(func(p graph.ResolveParams) (*[]Post, error) {
            posts, err := postService.List()
            if err != nil {
                return nil, err
            }
            return &posts, nil
        }).BuildQuery()
}

// Type-safe mutation
func createPost() graph.MutationField {
    type CreatePostInput struct {
        Title    string `json:"title"`
        AuthorID int    `json:"authorId"`
    }

    return graph.NewResolver[Post]("createPost").
        WithInputObject(CreatePostInput{}).
        WithResolver(func(p graph.ResolveParams) (*Post, error) {
            var input CreatePostInput
            if err := graph.GetArg(p, "input", &input); err != nil {
                return nil, err
            }

            return postService.Create(input.Title, input.AuthorID)
        }).BuildMutation()
}

Type-Safe Arguments with NewArgsResolver

NewArgsResolver provides compile-time type safety for both the return value AND arguments. The resolver function receives typed arguments directly, eliminating the need for manual argument extraction.

Basic Usage
// Struct arguments - auto-generates GraphQL args from struct fields
type GetUserArgs struct {
    ID int `json:"id" graphql:"id,required" description:"User ID"`
}

func getUser() graph.QueryField {
    return graph.NewArgsResolver[User, GetUserArgs]("user").
        WithResolver(func(ctx context.Context, p graph.ResolveParams, args GetUserArgs) (*User, error) {
            // args.ID is already parsed and type-safe!
            return userService.GetByID(args.ID)
        }).BuildQuery()
}

// Primitive arguments - requires field name
func echo() graph.MutationField {
    return graph.NewArgsResolver[string, string]("echo", "message").
        WithResolver(func(ctx context.Context, p graph.ResolveParams, args string) (*string, error) {
            // args is the message string directly
            return &args, nil
        }).BuildMutation()
}
Resolver Function Signature

The WithResolver method accepts a function with three parameters:

func(ctx context.Context, p graph.ResolveParams, args A) (*T, error)
  • ctx context.Context - Request context (can be nil, defaults to Background)
  • p graph.ResolveParams - Full GraphQL resolve parameters (for advanced use cases)
  • args A - Typed arguments parsed and validated
Anonymous Struct Naming

Anonymous nested structs are automatically given meaningful names based on the parent type:

type MessageArgs struct {
    Input struct {
        Message string `json:"message"`
        Name    string `json:"name"`
    } `json:"input"`
}

// The anonymous Input struct becomes "MessageArgsInput" in GraphQL schema
func sendMessage() graph.MutationField {
    return graph.NewArgsResolver[string, MessageArgs]("sendMessage").
        WithResolver(func(ctx context.Context, p graph.ResolveParams, args MessageArgs) (*string, error) {
            response := fmt.Sprintf("Hello %s: %s", args.Input.Name, args.Input.Message)
            return &response, nil
        }).BuildMutation()
}

GraphQL Schema Generated:

type Mutation {
  sendMessage(input: MessageArgsInput!): String
}

input MessageArgsInput {
  message: String
  name: String
}
Benefits
  • No manual argument extraction - Arguments are parsed and typed automatically
  • Compile-time safety - Both args and return type are type-checked
  • Context access - Explicit context.Context parameter
  • Full params access - Access to graph.ResolveParams when needed
  • Auto-generated schema - Arguments converted to GraphQL types automatically
  • Meaningful type names - Anonymous structs named after parent type
Comparison with NewResolver

NewResolver - Manual argument extraction:

graph.NewResolver[User]("user").
    WithArgs(graphql.FieldConfigArgument{
        "id": &graphql.ArgumentConfig{Type: graphql.Int},
    }).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        id, err := graph.GetArgInt(p, "id")  // Manual extraction
        if err != nil {
            return nil, err
        }
        return userService.GetByID(id)
    }).BuildQuery()

NewArgsResolver - Type-safe arguments:

type GetUserArgs struct {
    ID int `json:"id" graphql:"id,required"`
}

graph.NewArgsResolver[User, GetUserArgs]("user").
    WithResolver(func(ctx context.Context, p graph.ResolveParams, args GetUserArgs) (*User, error) {
        return userService.GetByID(args.ID)  // Direct access, type-safe!
    }).BuildQuery()
Advanced Examples

With validation tags:

type CreatePostArgs struct {
    Title   string `json:"title" graphql:"title,required" description:"Post title (required)"`
    Content string `json:"content" description:"Post content"`
    Tags    []string `json:"tags" description:"Post tags"`
}

func createPost() graph.MutationField {
    return graph.NewArgsResolver[Post, CreatePostArgs]("createPost").
        WithResolver(func(ctx context.Context, p graph.ResolveParams, args CreatePostArgs) (*Post, error) {
            // All fields are already validated and typed
            post, err := postService.Create(args.Title, args.Content, args.Tags)
            if err != nil {
                return nil, err
            }
            return post, nil
        }).BuildMutation()
}

With authentication (using middleware):

type UpdateUserArgs struct {
    ID   int    `json:"id" graphql:"id,required"`
    Name string `json:"name" graphql:"name,required"`
}

func updateUser() graph.MutationField {
    return graph.NewArgsResolver[User, UpdateUserArgs]("updateUser").
        WithMiddleware(graph.AuthMiddleware("admin")).  // Require admin role
        WithResolver(func(ctx context.Context, p graph.ResolveParams, args UpdateUserArgs) (*User, error) {
            // Auth already validated by middleware
            // Use typed args directly
            return userService.Update(args.ID, args.Name)
        }).BuildMutation()
}

With manual token extraction:

type UpdateUserArgs struct {
    ID   int    `json:"id" graphql:"id,required"`
    Name string `json:"name" graphql:"name,required"`
}

func updateUser() graph.MutationField {
    return graph.NewArgsResolver[User, UpdateUserArgs]("updateUser").
        WithResolver(func(ctx context.Context, p graph.ResolveParams, args UpdateUserArgs) (*User, error) {
            // Extract auth token from root
            token, err := graph.GetRootString(p, "token")
            if err != nil {
                return nil, fmt.Errorf("authentication required")
            }

            // Use typed args directly
            return userService.Update(token, args.ID, args.Name)
        }).BuildMutation()
}

Middleware

The library provides a powerful middleware system for adding cross-cutting concerns like authentication, logging, caching, and more to your resolvers.

Resolver Middleware

Apply middleware to the entire resolver using WithMiddleware(). Middleware functions are applied in the order they're added (first added = outermost layer):

graph.NewResolver[User]("user").
    WithMiddleware(graph.LoggingMiddleware).
    WithMiddleware(graph.AuthMiddleware("admin")).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        id, _ := graph.GetArgInt(p, "id")
        return userService.GetByID(id)
    }).BuildQuery()

Execution flow:

  1. LoggingMiddleware (starts timer)
  2. AuthMiddleware (checks permissions)
  3. Your resolver (executes business logic)
  4. AuthMiddleware (returns)
  5. LoggingMiddleware (logs duration)
Built-in Middleware
LoggingMiddleware

Logs resolver execution time to stdout:

graph.NewResolver[Post]("post").
    WithMiddleware(graph.LoggingMiddleware).
    WithResolver(func(p graph.ResolveParams) (*Post, error) {
        return postService.GetByID(id)
    }).BuildQuery()

// Output: Field post resolved in 2.5ms
AuthMiddleware

Requires a specific user role from context:

graph.NewResolver[User]("adminUser").
    WithMiddleware(graph.AuthMiddleware("admin")).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        return userService.GetAdmin()
    }).BuildQuery()
CacheMiddleware

Caches resolver results based on a custom key function:

graph.NewResolver[Product]("product").
    WithMiddleware(graph.CacheMiddleware(func(p graph.ResolveParams) string {
        id, _ := graph.GetArgInt(p, "id")
        return fmt.Sprintf("product:%d", id)
    })).
    WithResolver(func(p graph.ResolveParams) (*Product, error) {
        // Only executes on cache miss
        id, _ := graph.GetArgInt(p, "id")
        return productService.GetByID(id)
    }).BuildQuery()
Custom Middleware

Create custom middleware by implementing the FieldMiddleware function signature:

// FieldMiddleware wraps a resolver with additional functionality
type FieldMiddleware func(next FieldResolveFn) FieldResolveFn

// Example: Rate limiting middleware
func RateLimitMiddleware(limit int) graph.FieldMiddleware {
    requests := make(map[string]int)
    var mu sync.Mutex

    return func(next graph.FieldResolveFn) graph.FieldResolveFn {
        return func(p graph.ResolveParams) (interface{}, error) {
            // Extract user ID from context
            userID := p.Context.Value("userID").(string)

            mu.Lock()
            if requests[userID] >= limit {
                mu.Unlock()
                return nil, fmt.Errorf("rate limit exceeded")
            }
            requests[userID]++
            mu.Unlock()

            return next(p)
        }
    }
}

// Usage
graph.NewResolver[User]("user").
    WithMiddleware(RateLimitMiddleware(100)).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        return userService.GetByID(id)
    }).BuildQuery()
Middleware Patterns
Stacking Multiple Middleware

Chain multiple middleware for complex behavior:

graph.NewResolver[User]("user").
    WithMiddleware(graph.LoggingMiddleware).           // 1. Log timing
    WithMiddleware(graph.AuthMiddleware("user")).      // 2. Check auth
    WithMiddleware(RateLimitMiddleware(100)).          // 3. Rate limit
    WithMiddleware(graph.CacheMiddleware(cacheKeyFn)). // 4. Cache results
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        return userService.GetByID(id)
    }).BuildQuery()
Field-Level Middleware

Apply middleware to specific fields within a type:

graph.NewResolver[User]("users").
    AsList().
    WithFieldMiddleware("email", graph.AuthMiddleware("admin")).
    WithFieldMiddleware("salary", graph.AuthMiddleware("hr")).
    WithResolver(func(p graph.ResolveParams) (*[]User, error) {
        return userService.List(), nil
    }).BuildQuery()
Permission Middleware (Convenience Method)

WithPermission() is a convenience wrapper around WithMiddleware() for authorization:

graph.NewResolver[User]("deleteUser").
    AsMutation().
    WithPermission(graph.AuthMiddleware("admin")).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        id, _ := graph.GetArgInt(p, "id")
        return userService.Delete(id)
    }).BuildMutation()
Advanced Middleware Examples
Context Injection Middleware
func InjectDependencies(db *gorm.DB, cache *redis.Client) graph.FieldMiddleware {
    return func(next graph.FieldResolveFn) graph.FieldResolveFn {
        return func(p graph.ResolveParams) (interface{}, error) {
            ctx := p.Context
            ctx = context.WithValue(ctx, "db", db)
            ctx = context.WithValue(ctx, "cache", cache)

            // Create new params with injected context
            newParams := p
            newParams.Context = ctx

            return next(newParams)
        }
    }
}
Error Handling Middleware
func ErrorHandlingMiddleware(next graph.FieldResolveFn) graph.FieldResolveFn {
    return func(p graph.ResolveParams) (interface{}, error) {
        result, err := next(p)
        if err != nil {
            // Log error
            log.Printf("Error in %s: %v", p.Info.FieldName, err)

            // Transform error for user
            return nil, fmt.Errorf("an error occurred: please contact support")
        }
        return result, nil
    }
}
Performance Tracking Middleware
func MetricsMiddleware(metrics *prometheus.Registry) graph.FieldMiddleware {
    return func(next graph.FieldResolveFn) graph.FieldResolveFn {
        return func(p graph.ResolveParams) (interface{}, error) {
            start := time.Now()
            result, err := next(p)
            duration := time.Since(start)

            // Record metrics
            fieldName := fmt.Sprintf("%s.%s", p.Info.ParentType.Name(), p.Info.FieldName)
            recordMetric(metrics, fieldName, duration, err != nil)

            return result, err
        }
    }
}
Middleware Best Practices
  1. Order matters: Place authentication before caching, logging outermost
  2. Keep middleware pure: Avoid side effects when possible
  3. Use context for data: Pass request-scoped data via context
  4. Handle errors gracefully: Always return meaningful errors
  5. Measure performance: Use logging/metrics middleware to track slow resolvers
  6. Batch database queries: Use dataloaders to prevent N+1 queries

Framework Integration

With Gin
import (
    "github.com/gin-gonic/gin"
    "github.com/paulmanoni/go-graph"
)

func main() {
    r := gin.Default()

    handler := graph.NewHTTP(&graph.GraphContext{
        SchemaParams:     &graph.SchemaBuilderParams{...},
        EnableValidation: true,
    })

    r.POST("/graphql", gin.WrapF(handler))
    r.GET("/graphql", gin.WrapF(handler))

    r.Run(":8080")
}
With Chi
import (
    "github.com/go-chi/chi/v5"
    "github.com/paulmanoni/go-graph"
)

func main() {
    r := chi.NewRouter()

    handler := graph.NewHTTP(&graph.GraphContext{
        SchemaParams: &graph.SchemaBuilderParams{...},
    })

    r.Handle("/graphql", handler)

    http.ListenAndServe(":8080", r)
}
With Standard net/http
handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{...},
})

http.Handle("/graphql", handler)
http.ListenAndServe(":8080", nil)

API Reference

NewHTTP(graphCtx *GraphContext) http.HandlerFunc

Creates a standard HTTP handler with validation and sanitization support.

GraphContext Configuration
Field Type Default Description
Schema *graphql.Schema nil Custom GraphQL schema (Option 3)
SchemaParams *SchemaBuilderParams nil Builder params (Option 2)
Playground bool false Enable GraphQL Playground
Pretty bool false Pretty-print JSON responses
DEBUG bool false Skip validation/sanitization
EnableValidation bool false Enable query validation
EnableSanitization bool false Enable error sanitization
TokenExtractorFn func(*http.Request) string Bearer token Custom token extraction
UserDetailsFn func(string) (interface{}, error) nil Fetch user from token
RootObjectFn func(context.Context, *http.Request) map[string]interface{} nil Custom root setup

Note: If both Schema and SchemaParams are nil, a default hello world schema is used.

SchemaBuilderParams
type SchemaBuilderParams struct {
    QueryFields    []QueryField
    MutationFields []MutationField
}

Examples

See the examples directory for complete working examples:

  • main.go - Full example with authentication

Performance Benchmarks

Comprehensive benchmarks are included to measure performance across all package operations.

Running Benchmarks
# Run all benchmarks
go test -bench=. -benchmem

# Run specific benchmark
go test -bench=BenchmarkExtractBearerToken -benchmem

# Run with longer duration for more accurate results
go test -bench=. -benchmem -benchtime=5s

# Save results for comparison
go test -bench=. -benchmem > bench_results.txt
Benchmark Results

Performance metrics on Apple M1 Pro (results will vary by hardware):

Core Operations
Operation Time/op Allocations Description
Token Extraction ~31 ns 0 allocs Bearer token from header
Type Registration ~14 ns 0 allocs Object type caching
GetArgString ~10 ns 0 allocs Extract string argument
GetArgInt ~10 ns 0 allocs Extract int argument
GetArgBool ~10 ns 0 allocs Extract bool argument
GetRootString ~10 ns 0 allocs Extract root string value
Schema Building
Operation Time/op Allocations Description
Simple Schema ~9 μs 122 allocs Default hello/echo schema
Complex Schema ~10 μs 147 allocs Multiple types with nesting
Schema from Context ~8-10 μs 109-136 allocs Build from GraphContext
Query Validation
Operation Time/op Allocations Description
Simple Query ~700 ns 27 allocs Basic field selection
Complex Query ~3.2 μs 103 allocs Nested 3 levels deep
Deep Query ~2.2 μs 72 allocs Nested 5+ levels
With Aliases ~3.9 μs 130 allocs Multiple field aliases
Depth Calculation ~6-16 ns 0 allocs AST traversal
Alias Counting ~20 ns 0 allocs AST analysis
Complexity Calc ~13 ns 0 allocs Complexity scoring
HTTP Handler Performance
Operation Time/op Allocations Description
Debug Mode ~28 μs 439 allocs No validation/sanitization
With Validation ~28 μs 478 allocs Query validation enabled
With Sanitization ~34 μs 607 allocs Error sanitization enabled
With Auth ~27 μs 443 allocs Token + user details fetch
Complete Stack ~60 μs 966 allocs All features enabled
GET Request ~29 μs 436 allocs Query string parsing
Resolver Creation
Operation Time/op Allocations Description
Simple Resolver ~234 ns 5 allocs Basic type resolver
With Arguments ~349 ns 9 allocs Field arguments included
List Resolver ~186 ns 5 allocs Array type resolver
Paginated ~230 ns 5 allocs Pagination wrapper
With Input Object ~411 ns 10 allocs Input type generation
Type-Safe Arguments (NewArgsResolver)
Operation Time/op Allocations Description
Struct Args Creation ~874 ns 17 allocs Create resolver with struct args
Primitive Args Creation ~372 ns 11 allocs Create resolver with primitive args
Nested Struct Args Creation ~581 ns 15 allocs Create resolver with nested structs
List Resolver Creation ~544 ns 14 allocs Create list resolver with args
Execute Struct Args ~259 ns 5 allocs Execute resolver with struct args
Execute Primitive Args ~66 ns 2 allocs Execute resolver with primitive args
Execute Nested Structs ~397 ns 6 allocs Execute resolver with nested structs
Execute With Context ~158 ns 4 allocs Execute with context.Context
Generate Args From Type ~1.3 μs 22 allocs Auto-generate GraphQL args from struct
Map Args to Struct ~457 ns 7 allocs Parse and map GraphQL args to Go struct
Map Nested Struct ~346 ns 4 allocs Parse nested struct arguments
Advanced Features
Operation Time/op Allocations Description
GetRootInfo ~742 ns 12 allocs Complex type extraction
GetArg (Complex) ~1.1 μs 15 allocs Struct argument parsing
Response Sanitization ~5.4 μs 80 allocs Regex error cleaning
Cached Field Resolver ~5.6 ns 0 allocs Cache hit scenario
Response Write ~3.4 ns 0 allocs Buffer write operation
Concurrency Performance
Operation Time/op Allocations Description
Parallel HTTP Requests ~17 μs 440 allocs Concurrent request handling
Parallel Schema Build ~3 μs 104 allocs Concurrent schema creation
Parallel Type Registration ~145 ns 0 allocs Thread-safe type caching
Key Takeaways
  • Zero-allocation primitives: Token extraction and utility functions have zero heap allocations
  • Fast validation: Query validation adds minimal overhead (~700ns-4μs depending on complexity)
  • Type-safe arguments: NewArgsResolver execution is blazing fast (~66ns for primitives, ~259ns for structs)
  • Efficient type generation: Auto-generating GraphQL args from structs adds minimal overhead (~1.3μs one-time cost)
  • Efficient caching: Type registration uses read-write locks for optimal concurrent access
  • Predictable performance: End-to-end request handling is consistently under 100μs
  • Production ready: Complete stack with all security features runs at ~60μs per request
Optimization Tips
  1. Enable caching: Type registration is cached automatically - registered types are reused
  2. Use DEBUG mode wisely: Validation adds ~0-1μs overhead, only disable in development
  3. Minimize complexity: Keep query depth under 10 levels for optimal validation performance
  4. Batch operations: Use concurrent requests for multiple independent queries
  5. Profile your resolvers: The handler overhead is minimal (~30μs), optimize resolver logic first

High Load Performance Analysis

Is This Package Production-Ready for High Traffic?

Yes, absolutely. The benchmarks demonstrate excellent performance characteristics for high-load scenarios:

Throughput Capacity

Based on the benchmark results:

  • Handler overhead: ~60 μs per request (complete stack with all security features)
  • Theoretical capacity: ~16,600 requests/second per core
  • Multi-core scaling: On an 8-core system, potentially 100,000+ RPS (handler only)
Real-World Considerations
  1. Handler overhead is negligible: At 60 μs, the GraphQL handler represents a tiny fraction of total request time

    Example breakdown (not measured, for illustration):
    - GraphQL handler:        60 μs   (0.06%)  ← Measured
    - Database query:      50,000 μs  (50.00%) ← Example
    - External API calls:  45,000 μs  (45.00%) ← Example
    - Business logic:       4,940 μs   (4.94%) ← Example
    Total:               ~100,000 μs  (100 ms)
    
  2. Zero-allocation critical paths: Token extraction and argument parsing have 0 heap allocations, minimizing GC pressure

  3. Thread-safe design: Parallel benchmarks show excellent concurrent performance (17 μs vs 28 μs sequential)

  4. Predictable latency: Performance is consistent - no spikes or unpredictable behavior

Tested Load Scenarios

The package handles these scenarios efficiently:

Scenario Handler Overhead Notes
Simple queries ~28 μs Basic CRUD operations
Complex nested queries ~28 μs 3-5 levels deep
With authentication ~27 μs Token + user details
Full security stack ~60 μs Validation + sanitization + auth
Concurrent requests ~17 μs/req Parallel processing
Production Deployment Recommendations

For high-load production environments:

✅ Do:

  • Enable all security features (EnableValidation, EnableSanitization) - overhead is minimal
  • Use connection pooling for databases (your resolvers, not this package)
  • Implement resolver-level caching for expensive operations
  • Monitor resolver performance (this is where bottlenecks occur)
  • Use load balancing across multiple instances
  • Consider rate limiting at the API gateway level

⚠️ Bottlenecks will be in your code, not this package:

  • Database queries (typically 1-100+ ms)
  • External API calls (typically 10-500+ ms)
  • Complex business logic
  • N+1 query problems (use dataloader pattern)

❌ Don't:

  • Disable security features for "performance" - they add negligible overhead
  • Skip validation in production - the ~1 μs cost is worth it
  • Worry about handler performance - optimize your resolvers first
Memory Efficiency
  • Complete stack: 966 allocations per request (~62 KB)
  • Debug mode: 439 allocations per request (~33 KB)
  • GC impact: Minimal on modern Go runtimes (1.18+)
  • Memory footprint: Low even at 10,000+ concurrent requests
Proven Scalability

The benchmarks show:

  • Linear scaling: No performance degradation with concurrency
  • Type caching: Registered types reused (0 allocs after initial registration)
  • Lock contention: Minimal (RWMutex on type registry)
When NOT to Use This Package

This package may not be suitable if:

  • You need sub-10 μs total latency (extremely rare requirement)
  • You're running on severely resource-constrained environments (embedded systems)
  • You need custom validation rules beyond depth/complexity/aliases
  • You require subscription support (this package focuses on queries/mutations)
Conclusion

This package is excellent for high-load production environments. The 60 μs overhead is negligible compared to typical resolver operations. Your performance bottlenecks will be in your business logic, database queries, and external API calls - not in this GraphQL handler.

For reference:

  • 100 RPS: Trivial (1% CPU on single core)
  • 1,000 RPS: Easy (10% CPU on single core)
  • 10,000 RPS: Manageable (multi-core, normal load)
  • 100,000 RPS: Achievable (horizontal scaling + optimization)
  • ⚠️ 1,000,000 RPS: Requires distributed architecture (but handler isn't the bottleneck)

The handler is not your problem. Focus on optimizing your resolvers.

License

MIT

Documentation

Overview

Package graph provides a modern, secure GraphQL handler for Go with built-in authentication, validation, and an intuitive builder API.

Built on top of graphql-go (github.com/graphql-go/graphql), this package simplifies GraphQL server development with sensible defaults while maintaining full flexibility.

Features

  • Zero Config Start: Default hello world schema included
  • Fluent Builder API: Clean, type-safe schema construction
  • Built-in Authentication: Automatic Bearer token extraction
  • Security First: Query depth, complexity, and introspection protection
  • Response Sanitization: Remove field suggestions from errors
  • Framework Agnostic: Works with net/http, Gin, Chi, or any HTTP framework

Quick Start

Start immediately with the default schema:

import "github.com/paulmanoni/graph"

func main() {
    handler := graph.NewHTTP(&graph.GraphContext{
        Playground: true,
        DEBUG:      true,
    })
    http.Handle("/graphql", handler)
    http.ListenAndServe(":8080", nil)
}

Builder Pattern

Use the fluent builder API for clean schema construction:

func getUser() graph.QueryField {
    return graph.NewResolver[User]("user").
        WithArgs(graphql.FieldConfigArgument{
            "id": &graphql.ArgumentConfig{Type: graphql.String},
        }).
        WithResolver(func(p graphql.ResolveParams) (interface{}, error) {
            id, _ := graph.GetArgString(p, "id")
            return User{ID: id, Name: "Alice"}, nil
        }).BuildQuery()
}

Authentication

Automatic Bearer token extraction with optional user details fetching:

handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{
        QueryFields: []graph.QueryField{getProtectedQuery()},
    },
    UserDetailsFn: func(token string) (interface{}, error) {
        return validateAndGetUser(token)
    },
})

Access token in resolvers:

func getProtectedQuery() graph.QueryField {
    return graph.NewResolver[User]("me").
        WithResolver(func(p graphql.ResolveParams) (interface{}, error) {
            token, err := graph.GetRootString(p, "token")
            if err != nil {
                return nil, fmt.Errorf("authentication required")
            }
            // Use token...
        }).BuildQuery()
}

Security

Enable security features for production:

handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams:       &graph.SchemaBuilderParams{...},
    DEBUG:              false,  // Enable security features
    EnableValidation:   true,   // Max depth: 10, Max aliases: 4, Max complexity: 200
    EnableSanitization: true,   // Remove field suggestions from errors
    Playground:         false,  // Disable playground in production
})

Helper Functions

Extract arguments safely:

name, err := graph.GetArgString(p, "name")
age, err := graph.GetArgInt(p, "age")
active, err := graph.GetArgBool(p, "active")

Access root values:

token, err := graph.GetRootString(p, "token")
var user User
err := graph.GetRootInfo(p, "details", &user)

For more information, see https://github.com/paulmanoni/graph

Index

Constants

View Source
const SpringShortLayout = "2006-01-02T15:04"

SpringShortLayout is the time format used by Spring Boot for DateTime serialization. Format: yyyy-MM-dd'T'HH:mm (e.g., "2024-01-15T14:30")

Variables

View Source
var DateTime = graphql.NewScalar(graphql.ScalarConfig{
	Name:        "DateTime",
	Description: "The `DateTime` scalar type formatted as yyyy-MM-dd'T'HH:mm",
	Serialize:   serializeDateTime,
	ParseValue:  unserializeDateTime,
	ParseLiteral: func(valueAST ast.Value) interface{} {
		if v, ok := valueAST.(*ast.StringValue); ok {
			return unserializeDateTime(v.Value)
		}
		return nil
	},
})

DateTime is a GraphQL scalar type for date-time values. It uses the Spring Boot date format: yyyy-MM-dd'T'HH:mm (e.g., "2024-01-15T14:30"). All times are automatically converted to UTC.

Usage in struct fields:

type Event struct {
    Name      string    `json:"name"`
    StartTime time.Time `json:"startTime"` // Will use DateTime scalar
}

The scalar automatically handles:

  • Serialization: time.Time → "2024-01-15T14:30"
  • Deserialization: "2024-01-15T14:30" → time.Time
  • UTC conversion for all values

Functions

func AsyncFieldResolver

func AsyncFieldResolver(resolver graphql.FieldResolveFn) graphql.FieldResolveFn

AsyncFieldResolver executes a resolver asynchronously

func CachedFieldResolver

func CachedFieldResolver(cacheKey func(graphql.ResolveParams) string, resolver graphql.FieldResolveFn) graphql.FieldResolveFn

CachedFieldResolver caches field results with a key function

func ConditionalResolver

func ConditionalResolver(condition func(graphql.ResolveParams) bool, ifTrue, ifFalse graphql.FieldResolveFn) graphql.FieldResolveFn

ConditionalResolver resolves based on a condition

func DataTransformResolver

func DataTransformResolver(transform func(interface{}) interface{}) graphql.FieldResolveFn

DataTransformResolver applies a transformation to a field value

func ExtractBearerToken

func ExtractBearerToken(r *http.Request) string

ExtractBearerToken extracts the Bearer token from the Authorization header. It performs case-insensitive matching for the "Bearer " prefix and trims whitespace.

Returns an empty string if:

  • The Authorization header is missing
  • The header doesn't start with "Bearer " (case-insensitive)
  • The token value is empty

Example:

// Authorization: Bearer abc123xyz
token := graph.ExtractBearerToken(r) // Returns: "abc123xyz"

func GenerateArgsFromStruct

func GenerateArgsFromStruct[T any]() graphql.FieldConfigArgument

func GenerateGraphQLFields

func GenerateGraphQLFields[T any]() graphql.Fields

func GenerateGraphQLObject

func GenerateGraphQLObject[T any](name string) *graphql.Object

func GenerateInputObject

func GenerateInputObject[T any](name string) *graphql.InputObject

func GetArg

func GetArg(p ResolveParams, key string, target interface{}) error

GetArg safely extracts a value from p.Args and unmarshals it into the target. This is useful for extracting complex types like structs, slices, or maps.

The function handles:

  • Primitive types (string, int, bool) with optimized direct assignment
  • Complex types using JSON marshaling/unmarshaling for type conversion
  • Type mismatches with descriptive error messages

Returns an error if:

  • The argument key doesn't exist
  • Type conversion fails

Example:

var input CreateUserInput
if err := graph.GetArg(p, "input", &input); err != nil {
    return nil, err
}
// Use input.Name, input.Email, etc.

func GetArgBool

func GetArgBool(p ResolveParams, key string) (bool, error)

GetArgBool safely extracts a bool argument from p.Args. Returns an error if the argument doesn't exist or is not a boolean.

Example:

active, err := graph.GetArgBool(p, "active")

func GetArgInt

func GetArgInt(p ResolveParams, key string) (int, error)

GetArgInt safely extracts an int argument from p.Args. Handles both int and float64 types (JSON numbers are parsed as float64). Returns an error if the argument doesn't exist or is not a number.

Example:

age, err := graph.GetArgInt(p, "age")

func GetArgString

func GetArgString(p ResolveParams, key string) (string, error)

GetArgString safely extracts a string argument from p.Args. Returns an error if the argument doesn't exist or is not a string.

Example:

name, err := graph.GetArgString(p, "name")

func GetRootInfo

func GetRootInfo(p ResolveParams, key string, target interface{}) error

GetRootInfo safely extracts a value from p.Info.RootValue and unmarshals it into the target. This is commonly used to retrieve user details set by UserDetailsFn in the GraphContext.

The function handles:

  • Primitive types (string, int) with optimized direct assignment
  • Complex types using JSON marshaling/unmarshaling for type conversion
  • Type mismatches with descriptive error messages

Returns an error if:

  • Root value is nil or not a map
  • The key doesn't exist in the root value
  • Type conversion fails

Example:

// In your resolver
var user UserDetails
if err := graph.GetRootInfo(p, "details", &user); err != nil {
    return nil, fmt.Errorf("authentication required")
}
// Use user.ID, user.Email, etc.

func GetRootString

func GetRootString(p ResolveParams, key string) (string, error)

GetRootString safely extracts a string value from p.Info.RootValue. This is commonly used to retrieve the authentication token.

Returns an error if:

  • Root value is nil or not a map
  • The key doesn't exist in the root value
  • The value is not a string

Example:

// Get authentication token
token, err := graph.GetRootString(p, "token")
if err != nil {
    return nil, fmt.Errorf("authentication required")
}
// Validate token...

func GetTypeName

func GetTypeName[T any]() string

func LazyFieldResolver

func LazyFieldResolver(fieldName string, loader func(interface{}) (interface{}, error)) graphql.FieldResolveFn

LazyFieldResolver loads a field only when requested

func New

func New(graphCtx GraphContext) (*handler.Handler, error)

New creates a GraphQL handler from the provided GraphContext. It builds the schema and sets up authentication with token extraction and user details.

The handler automatically:

  • Extracts tokens using TokenExtractorFn (defaults to Bearer token extraction)
  • Fetches user details using UserDetailsFn if provided
  • Adds token and details to the root value for access in resolvers

Returns an error if schema building fails.

Example:

handler, err := graph.New(graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{
        QueryFields: []graph.QueryField{getUserQuery()},
    },
    Playground: true,
})

func NewHTTP

func NewHTTP(graphCtx *GraphContext) http.HandlerFunc

NewHTTP creates a standard http.HandlerFunc with built-in validation and sanitization support. This is the recommended way to create a GraphQL handler for production use.

The handler is fully compatible with net/http and any HTTP framework (Gin, Chi, Echo, etc.). If graphCtx is nil, defaults to DEBUG mode with Playground enabled.

Behavior:

  • In DEBUG mode (DEBUG: true): Skips all validation and sanitization for easier development
  • In production (DEBUG: false): Enables validation and sanitization based on configuration
  • Panics during initialization if schema building fails (fail-fast approach)

Security Features (when DEBUG: false):

  • EnableValidation: Validates query depth (max 10), aliases (max 4), complexity (max 200), and blocks introspection
  • EnableSanitization: Removes field suggestions from error messages to prevent information disclosure

Example:

// Development setup
handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{
        QueryFields: []graph.QueryField{getUserQuery()},
    },
    DEBUG:      true,
    Playground: true,
})

// Production setup
handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams:       &graph.SchemaBuilderParams{...},
    DEBUG:              false,
    EnableValidation:   true,
    EnableSanitization: true,
    Playground:         false,
    UserDetailsFn: func(token string) (interface{}, error) {
        return validateToken(token)
    },
})

http.Handle("/graphql", handler)
http.ListenAndServe(":8080", nil)

func RegisterObjectType

func RegisterObjectType(name string, typeFactory func() *graphql.Object) *graphql.Object

RegisterObjectType registers a GraphQL object type in the global registry Returns existing type if already registered, otherwise creates and registers new type

func ValidateGraphQLQuery

func ValidateGraphQLQuery(queryString string, schema *graphql.Schema) error

ValidateGraphQLQuery validates a GraphQL query against security rules. This function implements multiple layers of protection against malicious or expensive queries.

Validation Rules:

  • Max Query Depth: 10 levels (prevents deeply nested queries)
  • Max Aliases: 4 per query (prevents alias-based DoS attacks)
  • Max Complexity: 200 (prevents computationally expensive queries)
  • Introspection: Blocked (__schema and __type queries are rejected)

Returns an error if:

  • Query depth exceeds 10 levels
  • Query contains more than 4 aliases
  • Query complexity exceeds 200
  • Query contains __schema or __type introspection fields
  • Query parsing fails (though parsing errors are allowed to pass through)

Example usage:

if err := graph.ValidateGraphQLQuery(queryString, schema); err != nil {
    // Reject query with HTTP 400
    return fmt.Errorf("invalid query: %w", err)
}
// Query is safe to execute

Enable this in production with GraphContext.EnableValidation = true.

Types

type FieldConfig

type FieldConfig struct {
	Resolver          graphql.FieldResolveFn
	Description       string
	Args              graphql.FieldConfigArgument
	DeprecationReason string
}

type FieldGenerator

type FieldGenerator[T any] struct {
	// contains filtered or unexported fields
}

func NewFieldGenerator

func NewFieldGenerator[T any]() *FieldGenerator[T]

type FieldMiddleware

type FieldMiddleware func(next FieldResolveFn) FieldResolveFn

FieldMiddleware wraps a field resolver with additional functionality (auth, logging, caching, etc.)

func AuthMiddleware

func AuthMiddleware(requiredRole string) FieldMiddleware

AuthMiddleware requires a specific user role

func CacheMiddleware

func CacheMiddleware(cacheKey func(ResolveParams) string) FieldMiddleware

CacheMiddleware caches field results based on a key function

type FieldResolveFn

type FieldResolveFn func(p ResolveParams) (interface{}, error)

func LoggingMiddleware

func LoggingMiddleware(next FieldResolveFn) FieldResolveFn

LoggingMiddleware logs field resolution time

type GenericTypeInfo

type GenericTypeInfo struct {
	IsGeneric     bool
	IsWrapper     bool
	BaseTypeName  string
	ElementType   reflect.Type
	WrapperFields map[string]reflect.Type
}

GenericTypeInfo holds information about a generic type

type GraphContext

type GraphContext struct {
	// Schema: Provide either Schema OR SchemaParams (not both)
	// If both are nil, a default "hello world" schema will be created
	Schema *graphql.Schema

	// SchemaParams: Alternative to Schema - will be built automatically
	// If nil and Schema is also nil, defaults to hello world query/mutation
	SchemaParams *SchemaBuilderParams

	// Pretty: Pretty-print JSON responses
	Pretty bool

	// GraphiQL: Enable GraphiQL interface (deprecated, use Playground instead)
	GraphiQL bool

	// Playground: Enable GraphQL Playground interface
	Playground bool

	// DEBUG mode skips validation and sanitization for easier development
	// Default: false (validation enabled)
	DEBUG bool

	// RootObjectFn: Custom function to set up root object for each request
	// Called before token extraction and user details fetching
	RootObjectFn func(ctx context.Context, r *http.Request) map[string]interface{}

	// TokenExtractorFn: Custom token extraction from request
	// If not provided, default Bearer token extraction will be used
	TokenExtractorFn func(*http.Request) string

	// UserDetailsFn: Custom user details fetching based on token
	// If not provided, user details will not be added to rootValue
	// The details are accessible in resolvers via GetRootInfo(p, "details", &user)
	UserDetailsFn func(token string) (interface{}, error)

	// EnableValidation: Enable query validation (depth, complexity, introspection checks)
	// Default: false (validation disabled)
	// When enabled: Max depth=10, Max aliases=4, Max complexity=200, Introspection blocked
	EnableValidation bool

	// EnableSanitization: Enable response sanitization (removes field suggestions from errors)
	// Default: false (sanitization disabled)
	// Prevents information disclosure by removing "Did you mean X?" suggestions
	EnableSanitization bool
}

GraphContext configures a GraphQL handler with schema, authentication, and security settings.

Schema Configuration (choose one):

  • Schema: Use a pre-built graphql.Schema
  • SchemaParams: Use the builder pattern with QueryFields and MutationFields
  • Neither: A default "hello world" schema will be created

Security Modes:

  • DEBUG mode (DEBUG: true): Disables all validation and sanitization for development
  • Production mode (DEBUG: false): Enables validation and sanitization based on configuration flags

Authentication:

  • TokenExtractorFn: Extract tokens from requests (defaults to Bearer token extraction)
  • UserDetailsFn: Fetch user details from the extracted token
  • RootObjectFn: Custom root object setup for advanced use cases

Example Development Setup:

ctx := &graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{
        QueryFields: []graph.QueryField{getUserQuery()},
    },
    DEBUG:      true,
    Playground: true,
}

Example Production Setup:

ctx := &graph.GraphContext{
    SchemaParams:       &graph.SchemaBuilderParams{...},
    DEBUG:              false,
    EnableValidation:   true,  // Max depth: 10, Max aliases: 4, Max complexity: 200
    EnableSanitization: true,  // Remove field suggestions from errors
    Playground:         false,
    UserDetailsFn: func(token string) (interface{}, error) {
        return validateJWT(token)
    },
}

type JSONTime

type JSONTime time.Time

JSONTime is a custom time type that handles flexible JSON time formats. It supports both RFC3339 strings and array formats commonly used in some APIs.

Supported input formats:

  • RFC3339 string: "2024-01-15T14:30:00Z"
  • Array format: [year, month, day, hour, minute, second, nanosecond]
  • Minimum array: [2024, 1, 15] (time components default to 0)

Output format:

  • Always RFC3339 string: "2024-01-15T14:30:00Z"

Example usage:

type Event struct {
    Name      string         `json:"name"`
    StartTime graph.JSONTime `json:"startTime"`
}

// Accepts: {"startTime": "2024-01-15T14:30:00Z"}
// Accepts: {"startTime": [2024, 1, 15, 14, 30, 0]}
// Outputs: {"startTime": "2024-01-15T14:30:00Z"}

func (JSONTime) MarshalJSON

func (t JSONTime) MarshalJSON() ([]byte, error)

MarshalJSON implements json.Marshaler interface. Serializes JSONTime to RFC3339 format string.

func (JSONTime) Time

func (t JSONTime) Time() time.Time

Time converts JSONTime back to the standard time.Time type. This is useful when you need to perform time operations or comparisons.

Example:

var event Event
json.Unmarshal(data, &event)
standardTime := event.StartTime.Time()
// Now use standard time operations
if standardTime.After(time.Now()) { ... }

func (*JSONTime) UnmarshalJSON

func (t *JSONTime) UnmarshalJSON(data []byte) error

UnmarshalJSON implements json.Unmarshaler interface. Deserializes from either RFC3339 string or array format.

Array format: [year, month, day, hour?, minute?, second?, nanosecond?] Missing time components default to 0. All times are assumed to be UTC.

type MutationField

type MutationField interface {
	// Serve returns the GraphQL field configuration
	Serve() *graphql.Field

	// Name returns the field name used in the GraphQL schema
	Name() string
}

MutationField represents a GraphQL mutation field with its configuration. Implementations must provide both the field configuration and its name.

Use NewResolver to create MutationField instances:

mutation := graph.NewResolver[User]("createUser").
    WithInputObject(CreateUserInput{}).
    WithResolver(...).
    BuildMutation()

type PageInfo

type PageInfo struct {
	HasNextPage     bool   `json:"hasNextPage" description:"Whether there are more pages"`
	HasPreviousPage bool   `json:"hasPreviousPage" description:"Whether there are previous pages"`
	StartCursor     string `json:"startCursor" description:"Cursor for the first item"`
	EndCursor       string `json:"endCursor" description:"Cursor for the last item"`
}

PageInfo contains pagination information

type PaginatedResponse

type PaginatedResponse[T any] struct {
	Items      []T      `json:"items" description:"List of items"`
	TotalCount int      `json:"totalCount" description:"Total number of items"`
	PageInfo   PageInfo `json:"pageInfo" description:"Pagination information"`
}

PaginatedResponse represents a paginated response structure

type PaginationArgs

type PaginationArgs struct {
	First  *int    `json:"first" description:"Number of items to fetch"`
	After  *string `json:"after" description:"Cursor to start after"`
	Last   *int    `json:"last" description:"Number of items to fetch from end"`
	Before *string `json:"before" description:"Cursor to start before"`
}

PaginationArgs contains pagination arguments

type QueryField

type QueryField interface {
	// Serve returns the GraphQL field configuration
	Serve() *graphql.Field

	// Name returns the field name used in the GraphQL schema
	Name() string
}

QueryField represents a GraphQL query field with its configuration. Implementations must provide both the field configuration and its name.

Use NewResolver to create QueryField instances:

query := graph.NewResolver[User]("user").
    WithArgs(...).
    WithResolver(...).
    BuildQuery()

type ResolveParams

type ResolveParams graphql.ResolveParams

type SchemaBuilder

type SchemaBuilder struct {
	// contains filtered or unexported fields
}

SchemaBuilder builds GraphQL schemas from QueryFields and MutationFields. Use NewSchemaBuilder to create an instance and Build() to generate the schema.

func NewSchemaBuilder

func NewSchemaBuilder(params SchemaBuilderParams) *SchemaBuilder

NewSchemaBuilder creates a new schema builder with the provided query and mutation fields.

Example:

params := graph.SchemaBuilderParams{
    QueryFields:    []graph.QueryField{getUserQuery()},
    MutationFields: []graph.MutationField{createUserMutation()},
}
builder := graph.NewSchemaBuilder(params)
schema, err := builder.Build()

func (*SchemaBuilder) Build

func (sb *SchemaBuilder) Build() (graphql.Schema, error)

Build constructs and returns a graphql.Schema from the configured fields. It creates Query and Mutation root types based on the provided fields.

Returns an error if:

  • Schema construction fails due to type conflicts
  • Field configurations are invalid

The schema can have:

  • Only queries (no mutations)
  • Only mutations (no queries)
  • Both queries and mutations
  • Neither (empty schema)

type SchemaBuilderParams

type SchemaBuilderParams struct {
	// QueryFields: List of query fields to include in the schema
	QueryFields []QueryField `group:"query_fields"`

	// MutationFields: List of mutation fields to include in the schema
	MutationFields []MutationField `group:"mutation_fields"`
}

SchemaBuilderParams configures the fields for building a GraphQL schema. Use this with NewSchemaBuilder to construct schemas using the builder pattern.

Example:

params := graph.SchemaBuilderParams{
    QueryFields: []graph.QueryField{
        getUserQuery(),
        listUsersQuery(),
    },
    MutationFields: []graph.MutationField{
        createUserMutation(),
        updateUserMutation(),
    },
}
schema, err := graph.NewSchemaBuilder(params).Build()

type TypedArgsResolver

type TypedArgsResolver[T any, A any] struct {
	// contains filtered or unexported fields
}

TypedArgsResolver provides type-safe argument handling

func NewArgsResolver

func NewArgsResolver[T any, A any](name string, argName ...string) *TypedArgsResolver[T, A]

func (*TypedArgsResolver[T, A]) AsList

func (r *TypedArgsResolver[T, A]) AsList() *TypedArgsResolver[T, A]

AsList configures the resolver to return a list of items

func (*TypedArgsResolver[T, A]) AsPaginated

func (r *TypedArgsResolver[T, A]) AsPaginated() *TypedArgsResolver[T, A]

AsPaginated configures the resolver to return paginated results

func (*TypedArgsResolver[T, A]) BuildMutation

func (r *TypedArgsResolver[T, A]) BuildMutation() MutationField

BuildMutation builds and returns a MutationField

func (*TypedArgsResolver[T, A]) BuildQuery

func (r *TypedArgsResolver[T, A]) BuildQuery() QueryField

BuildQuery builds and returns a QueryField

func (*TypedArgsResolver[T, A]) WithDescription

func (r *TypedArgsResolver[T, A]) WithDescription(desc string) *TypedArgsResolver[T, A]

WithDescription sets the field description

func (*TypedArgsResolver[T, A]) WithResolver

func (r *TypedArgsResolver[T, A]) WithResolver(resolver func(ctx context.Context, p ResolveParams, args A) (*T, error)) *TypedArgsResolver[T, A]

WithResolver sets a type-safe resolver with typed arguments and context support

Example usage:

type GetPostArgs struct {
	ID int `graphql:"id,required"`
}

resolver.WithArgs[GetPostArgs]().
	WithResolver(func(ctx context.Context, args GetPostArgs) (*Post, error) {
		return postService.GetByID(args.ID)
	})

type UnifiedResolver

type UnifiedResolver[T any] struct {
	// contains filtered or unexported fields
}

UnifiedResolver handles all GraphQL resolver scenarios with field-level customization

func NewResolver

func NewResolver[T any](name string) *UnifiedResolver[T]

func (*UnifiedResolver[T]) AsList

func (r *UnifiedResolver[T]) AsList() *UnifiedResolver[T]

Query Configuration

func (*UnifiedResolver[T]) AsMutation

func (r *UnifiedResolver[T]) AsMutation() *UnifiedResolver[T]

Mutation Configuration

func (*UnifiedResolver[T]) AsPaginated

func (r *UnifiedResolver[T]) AsPaginated() *UnifiedResolver[T]

func (*UnifiedResolver[T]) Build

func (r *UnifiedResolver[T]) Build() interface{}

func (*UnifiedResolver[T]) BuildMutation

func (r *UnifiedResolver[T]) BuildMutation() MutationField

func (*UnifiedResolver[T]) BuildQuery

func (r *UnifiedResolver[T]) BuildQuery() QueryField

Build Methods

func (*UnifiedResolver[T]) Name

func (r *UnifiedResolver[T]) Name() string

Interface Implementation

func (*UnifiedResolver[T]) Serve

func (r *UnifiedResolver[T]) Serve() *graphql.Field

func (*UnifiedResolver[T]) WithArgs

func (*UnifiedResolver[T]) WithArgsFromStruct

func (r *UnifiedResolver[T]) WithArgsFromStruct(structType interface{}) *UnifiedResolver[T]

func (*UnifiedResolver[T]) WithAsyncField

func (r *UnifiedResolver[T]) WithAsyncField(fieldName string, resolver graphql.FieldResolveFn) *UnifiedResolver[T]

func (*UnifiedResolver[T]) WithCachedField

func (r *UnifiedResolver[T]) WithCachedField(fieldName string, cacheKeyFunc func(graphql.ResolveParams) string, resolver graphql.FieldResolveFn) *UnifiedResolver[T]

func (*UnifiedResolver[T]) WithComputedField

func (r *UnifiedResolver[T]) WithComputedField(name string, fieldType graphql.Output, resolver graphql.FieldResolveFn) *UnifiedResolver[T]

func (*UnifiedResolver[T]) WithCustomField

func (r *UnifiedResolver[T]) WithCustomField(name string, field *graphql.Field) *UnifiedResolver[T]

func (*UnifiedResolver[T]) WithDescription

func (r *UnifiedResolver[T]) WithDescription(desc string) *UnifiedResolver[T]

Basic Configuration

func (*UnifiedResolver[T]) WithFieldMiddleware

func (r *UnifiedResolver[T]) WithFieldMiddleware(fieldName string, middleware FieldMiddleware) *UnifiedResolver[T]

func (*UnifiedResolver[T]) WithFieldResolver

func (r *UnifiedResolver[T]) WithFieldResolver(fieldName string, resolver graphql.FieldResolveFn) *UnifiedResolver[T]

Field-Level Customization

func (*UnifiedResolver[T]) WithFieldResolvers

func (r *UnifiedResolver[T]) WithFieldResolvers(overrides map[string]graphql.FieldResolveFn) *UnifiedResolver[T]

func (*UnifiedResolver[T]) WithInputObject

func (r *UnifiedResolver[T]) WithInputObject(inputType interface{}) *UnifiedResolver[T]

func (*UnifiedResolver[T]) WithInputObjectFieldName

func (r *UnifiedResolver[T]) WithInputObjectFieldName(name string) *UnifiedResolver[T]

func (*UnifiedResolver[T]) WithInputObjectNullable

func (r *UnifiedResolver[T]) WithInputObjectNullable() *UnifiedResolver[T]

func (*UnifiedResolver[T]) WithLazyField

func (r *UnifiedResolver[T]) WithLazyField(fieldName string, loader func(interface{}) (interface{}, error)) *UnifiedResolver[T]

Utility Methods for Field Configuration

func (*UnifiedResolver[T]) WithMiddleware

func (r *UnifiedResolver[T]) WithMiddleware(middleware FieldMiddleware) *UnifiedResolver[T]

WithMiddleware adds middleware to the main resolver. Middleware functions are applied in the order they are added (first added = outermost layer). This is the foundation for all resolver-level middleware (auth, logging, caching, etc.).

Example usage:

NewResolver[User]("user").
	WithMiddleware(LoggingMiddleware).
	WithMiddleware(AuthMiddleware("admin")).
	WithResolver(func(p ResolveParams) (*User, error) {
		return userService.GetByID(p.Args["id"].(int))
	}).
	BuildQuery()

func (*UnifiedResolver[T]) WithPermission

func (r *UnifiedResolver[T]) WithPermission(middleware FieldMiddleware) *UnifiedResolver[T]

WithPermission adds permission middleware to the resolver (similar to Python @permission_classes decorator) This is now just a convenience wrapper around WithMiddleware for backwards compatibility

func (*UnifiedResolver[T]) WithResolver

func (r *UnifiedResolver[T]) WithResolver(resolver func(p ResolveParams) (*T, error)) *UnifiedResolver[T]

WithResolver sets a type-safe resolver function that returns *T instead of interface{} This provides better type safety and eliminates the need for type assertions or casts

Example usage:

NewResolver[User]("user").
	WithResolver(func(p graph.ResolveParams) (*User, error) {
		id, _ := GetArgInt(p, "id")
		return userService.GetByID(id)
	}).BuildQuery()

NewResolver[User]("users").
	AsList().
	WithResolver(func(p graph.ResolveParams) (*[]User, error) {
		users := userService.List()
		return &users, nil
	}).BuildQuery()

NewResolver[string]("hello").
	WithResolver(func(p graph.ResolveParams) (*string, error) {
		msg := "Hello, World!"
		return &msg, nil
	}).BuildQuery()

func (*UnifiedResolver[T]) WithTypedResolver

func (r *UnifiedResolver[T]) WithTypedResolver(typedResolver interface{}) *UnifiedResolver[T]

Typed Resolver Support - allows direct struct parameters instead of graphql.ResolveParams

Example usage:

func resolveUser(args GetUserArgs) (*User, error) {
    return &User{ID: args.ID, Name: "User"}, nil
}

NewResolver[User]("user", "User").
    WithTypedResolver(resolveUser).
    BuildQuery()

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL