graph

package module
v1.2.4 Latest Latest
Warning

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

Go to latest
Published: Apr 22, 2026 License: MIT Imports: 20 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 - Chainable WithArg API with automatic parsing into scalars or structs
  • ✍️ Typed Mutations - NewMutation[T, In] with kind-based builders (Create/Update/Delete/Action/Upsert), Patch[In] for partial updates, and lifecycle hooks
  • 🏗️ 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
  • 🔄 Real-time Subscriptions - WebSocket subscriptions with dual protocol support
  • Framework Agnostic - Works with net/http, Gin, or any framework
  • High Performance - ~30–35 μs per request, optional QueryASTCache drops validation cost to ~141 ns on hits

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").
        WithArg("id", graph.String).
        WithResolver(func(p graph.ResolveParams) (*User, error) {
            id := graph.Get[string](graph.ArgsMap(p.Args), "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 and update context
    UserDetailsFn: func(ctx context.Context, token string) (context.Context, interface{}, error) {
        // Validate JWT, query database, etc.
        user, err := validateAndGetUser(token)
        if err != nil {
            return ctx, nil, err
        }

        // Add values to context (accessible via p.Context.Value() in resolvers)
        ctx = context.WithValue(ctx, "userID", user.ID)
        ctx = context.WithValue(ctx, "roles", user.Roles)

        return ctx, user, nil
    },
})

Access in resolvers:

func getProtectedQuery() graph.QueryField {
    return graph.NewResolver[User]("me").
        WithResolver(func(p graph.ResolveParams) (*User, error) {
            // Option 1: Get values from context (set by UserDetailsFn)
            userID := p.Context.Value("userID").(string)

            // Option 2: Get token directly using generic GetRoot
            token := graph.GetRoot[string](graph.NewRootInfo(p), "token")
            if token == "" {
                return nil, fmt.Errorf("authentication required")
            }

            // Option 3: Get user details struct using generic GetRoot
            user, err := graph.GetRootE[User](graph.NewRootInfo(p), "details")
            if 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(ctx context.Context, token string) (context.Context, interface{}, error) {
        user, err := getUserByToken(token)
        return ctx, user, err
    },
})

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(ctx context.Context, token string) (context.Context, interface{}, error) {
        user, err := validateAndGetUser(token)
        if err != nil {
            return ctx, nil, err
        }
        return ctx, user, nil
    },
})
Validation Rules (Legacy)

For simple validation needs, use EnableValidation: true:

  • Max Query Depth: 10 levels
  • Max Aliases: 4 per query
  • Max Complexity: 200
  • Introspection: Disabled (blocks __schema and __type)

For advanced validation needs, see the Custom Validation Rules section below.

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
})

Custom Validation Rules

The package provides a powerful and flexible validation system that goes beyond simple EnableValidation: true. Create custom validation rules to enforce security policies, authentication requirements, rate limits, and more.

Features
  • 🛡️ Pre-built Security Rules - Max depth, complexity, aliases, introspection blocking, token limits
  • 🔐 Authentication & Authorization - Require auth for operations, role-based and permission-based access control
  • Rate Limiting - Budget-based rate limiting with role bypasses
  • 📦 Preset Collections - SecurityRules, StrictSecurityRules, DevelopmentRules
  • 🎯 Type-Safe - Strongly-typed AuthContext with user information
  • 🔧 Composable - Combine multiple rules and rule sets
  • ⚠️ Multi-Error Support - Collect and return all validation errors at once
  • 🚀 High Performance - Adds minimal overhead (~1-2μs per query)
Quick Start
handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{...},

    // Custom validation rules (replaces EnableValidation)
    ValidationRules: []graph.ValidationRule{
        graph.NewMaxDepthRule(10),
        graph.NewMaxComplexityRule(200),
        graph.NewNoIntrospectionRule(),
        graph.NewRequireAuthRule("mutation", "subscription"),
        graph.NewRoleRules(map[string][]string{
            "deleteUser": {"admin"},
            "viewAuditLog": {"admin", "auditor"},
        }),
    },

    // Fetch user details from token (reuses existing UserDetailsFn)
    UserDetailsFn: func(ctx context.Context, token string) (context.Context, interface{}, error) {
        user, err := validateJWT(token)
        if err != nil {
            return ctx, nil, err
        }
        return ctx, user, nil
    },
})
Security Rules
MaxDepthRule

Prevents deeply nested queries that can cause performance issues:

graph.NewMaxDepthRule(10)  // Max 10 levels deep

Blocks:

{
  level1 {
    level2 {
      level3 {
        # ... more than 10 levels
      }
    }
  }
}
MaxComplexityRule

Limits query computational cost (complexity = number of fields × depth):

graph.NewMaxComplexityRule(200)  // Max complexity of 200

Example: Query with 50 fields at depth 5 = complexity 250 → Rejected

MaxAliasesRule

Prevents alias-based denial-of-service attacks:

graph.NewMaxAliasesRule(4)  // Max 4 aliases per query

Blocks:

{
  u1: user(id: 1) { name }
  u2: user(id: 2) { name }
  u3: user(id: 3) { name }
  u4: user(id: 4) { name }
  u5: user(id: 5) { name }  # 5th alias rejected
}
NoIntrospectionRule

Blocks schema introspection in production:

graph.NewNoIntrospectionRule()

Blocks:

{ __schema { types { name } } }
{ __type(name: "User") { fields { name } } }
MaxTokensRule

Limits total tokens in query (prevents extremely large queries):

graph.NewMaxTokensRule(500)  // Max 500 tokens
Authentication & Authorization Rules
RequireAuthRule

Require authentication for specific operations or fields:

// Require auth for all mutations and subscriptions
graph.NewRequireAuthRule("mutation", "subscription")

// Require auth for specific fields
graph.NewRequireAuthRule("sensitiveData", "adminPanel")

// Combine both
graph.NewRequireAuthRule("mutation", "subscription", "sensitiveData")

Example response when unauthenticated:

{
  "errors": [{
    "message": "mutation operations require authentication",
    "rule": "RequireAuthRule"
  }]
}
RoleRule & RoleRules

Enforce role-based access control:

// Single field rule
graph.NewRoleRule("deleteUser", "admin")

// Multiple roles allowed
graph.NewRoleRule("viewReports", "admin", "manager")

// Batch configuration (preferred for multiple fields)
graph.NewRoleRules(map[string][]string{
    "deleteUser":     {"admin"},
    "deleteAccount":  {"admin"},
    "viewAuditLog":   {"admin", "auditor"},
    "approveOrder":   {"admin", "manager"},
})

Minimal interface requirements:

Your user struct only needs to implement the methods required by the rules you use:

// For RoleRule and RoleRules
type HasRolesInterface interface {
    HasRole(role string) bool
}

// For PermissionRule and PermissionRules
type HasPermissionsInterface interface {
    HasPermission(permission string) bool
}

// For RateLimitRule
type HasIDInterface interface {
    GetID() string
}

// Example user implementation
type User struct {
    ID          string
    Roles       []string
    Permissions []string
}

func (u *User) GetID() string {
    return u.ID
}

func (u *User) HasRole(role string) bool {
    for _, r := range u.Roles {
        if r == role {
            return true
        }
    }
    return false
}

func (u *User) HasPermission(perm string) bool {
    for _, p := range u.Permissions {
        if p == perm {
            return true
        }
    }
    return false
}
PermissionRule & PermissionRules

Fine-grained permission-based access control:

// Single field permission
graph.NewPermissionRule("sensitiveData", "read:sensitive")

// Multiple permissions
graph.NewPermissionRule("exportData", "export:data", "admin:all")

// Batch configuration
graph.NewPermissionRules(map[string][]string{
    "sensitiveData": {"read:sensitive"},
    "exportData":    {"export:data"},
    "adminPanel":    {"admin:access"},
})
BlockedFieldsRule

Block specific fields from being queried:

rule := graph.NewBlockedFieldsRule("internalUsers", "deprecatedField")

// With reasons
rule.BlockField("legacyAPI", "deprecated: use v2 API")
rule.BlockField("internalData", "not available in this version")
Rate Limiting

Budget-based rate limiting with role bypasses:

graph.NewRateLimitRule(
    graph.WithBudgetFunc(getBudgetFromRedis),
    graph.WithCostPerUnit(2),  // Multiply complexity by 2
    graph.WithBypassRoles("admin", "service"),
)

Budget function examples:

// Simple fixed budget
graph.SimpleBudgetFunc(1000)

// Per-user budgets
graph.PerUserBudgetFunc(map[string]int{
    "premium_user": 10000,
    "basic_user":   1000,
}, 500)  // default budget

// Redis-based sliding window
func getRedisBudget(userID string) (int, error) {
    key := fmt.Sprintf("rate_limit:%s", userID)
    remaining, err := redis.Get(ctx, key).Int()
    if err == redis.Nil {
        return 1000, nil  // Reset budget
    }
    return remaining, err
}
Preset Rule Collections

Pre-configured rule sets for common scenarios:

SecurityRules (Default)

Standard security for production:

graph.ValidationRules: graph.SecurityRules
  • Max depth: 10
  • Max complexity: 200
  • Max aliases: 4
  • Introspection: Blocked
StrictSecurityRules

Stricter limits for high-security environments:

graph.ValidationRules: graph.StrictSecurityRules
  • Max depth: 8
  • Max complexity: 150
  • Max aliases: 3
  • Max tokens: 500
  • Introspection: Blocked
DevelopmentRules

Lenient rules for development:

graph.ValidationRules: graph.DevelopmentRules
  • Max depth: 20
  • Max complexity: 500
  • No other restrictions
Combining Rules
CombineRules

Merge multiple rule sets:

rules := graph.CombineRules(
    graph.SecurityRules,
    []graph.ValidationRule{
        graph.NewRequireAuthRule("mutation"),
        graph.NewRoleRules(graph.AdminOnlyFields),
    },
)

handler := graph.NewHTTP(&graph.GraphContext{
    ValidationRules: rules,
})
Preset Role Configurations

Pre-built role configurations for common access patterns:

// Use built-in configurations
graph.NewRoleRules(graph.AdminOnlyFields)
graph.NewRoleRules(graph.ManagerFields)
graph.NewRoleRules(graph.AuditorFields)

// Merge multiple configurations
allRoles := graph.MergeRoleConfigs(
    graph.AdminOnlyFields,
    graph.ManagerFields,
    graph.AuditorFields,
)
graph.NewRoleRules(allRoles)

Built-in role configs:

  • AdminOnlyFields: deleteUser, deleteAccount, viewAuditLog, systemSettings, manageRoles
  • ManagerFields: approveOrder, viewReports, manageTeam, bulkOperations
  • AuditorFields: viewAuditLog, exportLogs, viewAnalytics
Production Example

Complete production setup with security, auth, and rate limiting:

package main

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

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

        // Strict security validation
        ValidationRules: graph.CombineRules(
            graph.StrictSecurityRules,
            []graph.ValidationRule{
                // Authentication
                graph.NewRequireAuthRule("mutation", "subscription"),

                // Role-based access
                graph.NewRoleRules(map[string][]string{
                    "deleteUser":     {"admin"},
                    "viewAuditLog":   {"admin", "auditor"},
                    "approveOrder":   {"admin", "manager"},
                }),

                // Permission-based access
                graph.NewPermissionRules(map[string][]string{
                    "sensitiveData": {"read:sensitive"},
                    "exportData":    {"export:data"},
                }),

                // Rate limiting
                graph.NewRateLimitRule(
                    graph.WithBudgetFunc(getRedisBudget),
                    graph.WithCostPerUnit(2),
                    graph.WithBypassRoles("admin", "service"),
                ),
            },
        ),

        // Fetch user details from JWT token (reuses existing UserDetailsFn)
        UserDetailsFn: func(ctx context.Context, token string) (context.Context, interface{}, error) {
            user, err := validateJWT(token)
            if err != nil {
                return ctx, nil, err  // Invalid token
            }
            return ctx, user, nil  // Returns your user struct that implements minimal interfaces
        },

        // Validation options
        ValidationOptions: &graph.ValidationOptions{
            StopOnFirstError: false,  // Collect all errors
            SkipInDebug:      true,   // Skip in DEBUG mode
        },

        // Other production settings
        EnableSanitization: true,
        Playground:         false,
        DEBUG:              false,
    })

    http.Handle("/graphql", handler)
    http.ListenAndServe(":8080", nil)
}
Custom Validation Rules

Create custom rules by embedding BaseRule:

type IPWhitelistRule struct {
    graph.BaseRule
    allowedIPs []string
}

func NewIPWhitelistRule(ips ...string) graph.ValidationRule {
    return &IPWhitelistRule{
        BaseRule:   graph.NewBaseRule("IPWhitelistRule"),
        allowedIPs: ips,
    }
}

func (r *IPWhitelistRule) Validate(ctx *graph.ValidationContext) error {
    // Access request from context if needed
    // clientIP := getClientIP(ctx.Request)

    // Check IP whitelist
    // if !contains(r.allowedIPs, clientIP) {
    //     return r.NewError("IP not whitelisted")
    // }

    return nil
}

BaseRule provides:

  • NewError(msg string) *ValidationError
  • NewErrorf(format string, args ...interface{}) *ValidationError
  • Enable() / Disable() / Enabled() bool
  • Consistent error formatting
Error Responses
Single Validation Error
{
  "errors": [{
    "message": "query depth 12 exceeds maximum 10",
    "rule": "MaxDepthRule"
  }]
}
Multiple Validation Errors

When StopOnFirstError: false:

{
  "errors": [
    {
      "message": "query depth 12 exceeds maximum 10",
      "rule": "MaxDepthRule"
    },
    {
      "message": "field 'deleteUser' requires one of roles: [admin]",
      "rule": "RoleRules"
    },
    {
      "message": "query cost 250 exceeds available budget 200",
      "rule": "RateLimitRule"
    }
  ]
}
Validation Options

Configure validation behavior:

&graph.ValidationOptions{
    // Collect all errors vs stop on first
    StopOnFirstError: false,

    // Skip validation in DEBUG mode
    SkipInDebug: true,

    // Optional: cache parsed ASTs across requests so repeat queries skip the parser.
    // Sized to an upper bound on the number of distinct queries you expect.
    // When full, the cache is reset wholesale to keep memory bounded.
    QueryCache: graph.NewQueryASTCache(256),
}
Performance Impact

Validation adds minimal overhead (measured on Apple M1 Pro):

Rule Time/op Allocations Description
MaxDepthRule ~1.4 μs 43 allocs Query depth validation
MaxComplexityRule ~1.4 μs 43 allocs Complexity calculation
MaxAliasesRule ~1.6 μs 51 allocs Alias counting
NoIntrospectionRule ~1.1 μs 36 allocs Introspection blocking
RequireAuthRule ~1.0 μs 30 allocs Authentication check
RoleRule ~626 ns 20 allocs Role validation
PermissionRule ~603 ns 20 allocs Permission check
RateLimitRule ~1.2 μs 36 allocs Budget + complexity
SecurityRules (preset) ~1.4 μs 43 allocs Depth + complexity + aliases + introspection
SecurityRules (cached) ~141 ns 1 alloc Same preset via QueryASTCache
StrictSecurityRules ~1.2 μs 36 allocs Stricter limits
Combined rules ~965 ns 29 allocs Multiple custom rules

For a complete HTTP request:

  • Debug mode (no validation): ~31 μs
  • With validation: ~35 μs
  • With sanitization: ~35 μs (41 allocs in the sanitize path itself)
  • With auth: ~30 μs
  • Overhead: ~1–2 μs for validation on a cache miss, ~100 ns on a cache hit
Migration from EnableValidation

Before (simple validation):

handler := graph.NewHTTP(&graph.GraphContext{
    EnableValidation: true,  // Deprecated
})

After (custom rules):

handler := graph.NewHTTP(&graph.GraphContext{
    ValidationRules: graph.SecurityRules,  // Same defaults
})

Or use presets:

// Development
ValidationRules: graph.DevelopmentValidationRules()

// Production
ValidationRules: graph.ProductionValidationRules()

// Custom
ValidationRules: graph.DefaultValidationRules()
Best Practices
  1. Start with presets: Use SecurityRules or StrictSecurityRules as a base
  2. Add auth rules: Layer in RequireAuthRule and RoleRules as needed
  3. Test in development: Use DevelopmentRules with DEBUG: true for development
  4. Collect all errors: Set StopOnFirstError: false for better debugging
  5. Monitor performance: Validation overhead is minimal (~3-5μs total)
  6. Use UserDetailsFn: Centralize auth logic by implementing UserDetailsFn instead of checking tokens in each resolver
  7. Implement minimal interfaces: Only implement the interfaces needed by your validation rules (HasRolesInterface, HasPermissionsInterface, HasIDInterface)

Helper Functions

Accessing Root Values (Generic API)

Use the type-safe generic functions to access root values:

// Get string with zero value on error
token := graph.GetRoot[string](graph.NewRootInfo(p), "token")

// Get string with error handling
token, err := graph.GetRootE[string](graph.NewRootInfo(p), "token")
if err != nil {
    return nil, fmt.Errorf("authentication required")
}

// Get string with default value
token := graph.GetRootOr[string](graph.NewRootInfo(p), "token", "anonymous")

// Get struct with type safety
user := graph.GetRoot[User](graph.NewRootInfo(p), "details")

// Get struct with error handling
user, err := graph.GetRootE[User](graph.NewRootInfo(p), "details")
if err != nil {
    return nil, err
}

// MustGet - panics if not found (use when certain value exists)
user := graph.MustGetRoot[User](graph.NewRootInfo(p), "details")
Function Missing Key Conversion Error Use Case
GetRoot[T] Zero value Zero value Optional values, quick access
GetRootOr[T] Default Default Values with defaults
GetRootE[T] Error Error Required values with validation
MustGetRoot[T] Panic Panic Required values (certain to exist)
Legacy API (Still Available)

The original pointer-based functions are still available:

// Get token (legacy)
token, err := graph.GetRootString(p, "token")

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

Type-Safe Resolvers

Automatic Type Generation with Embedded Struct Support

The package automatically generates GraphQL types from your Go structs, including full support for embedded structs. Fields from embedded structs are automatically flattened to the parent level.

Embedded Struct Example
// Define a base entity with common fields
type BaseEntity struct {
    ID        string     `json:"id"`
    CreatedAt time.Time  `json:"created_at"`
    UpdatedAt *time.Time `json:"updated_at,omitempty"`
}

// Product embeds BaseEntity - fields are automatically flattened
type Product struct {
    BaseEntity
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

// GraphQL type is automatically generated with ALL fields at the same level:
// type Product {
//   id: String
//   created_at: DateTime
//   updated_at: DateTime
//   name: String
//   price: Float
// }

func getProduct() graph.QueryField {
    return graph.NewResolver[Product]("product").
        WithResolver(func(p graph.ResolveParams) (*Product, error) {
            return &Product{
                BaseEntity: BaseEntity{
                    ID:        "123",
                    CreatedAt: time.Now(),
                },
                Name:  "Widget",
                Price: 19.99,
            }, nil
        }).BuildQuery()
}

Embedded Struct Features:

  • Automatic field flattening - No nested objects, all fields at parent level
  • Multiple embedding - Embed multiple structs (e.g., BaseEntity + Metadata)
  • Nested embedding - Multi-level embedding supported (Level1 → Level2 → Level3)
  • Pointer embedding - Works with *BaseEntity as well
  • Field override - Child fields with same name take precedence
  • Works everywhere - Supported in queries, mutations, input objects, and arguments

Performance:

  • Field generation with embedded structs: ~1.2 μs
  • Field resolver execution: ~232 ns
  • Zero runtime overhead after type generation

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").
    WithArg("id", graph.String).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        id := graph.Get[string](graph.ArgsMap(p.Args), "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").
        WithArg("id", graph.Int).
        WithResolver(func(p graph.ResolveParams) (*Post, error) {
            id := graph.Get[int](graph.ArgsMap(p.Args), "id")

            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()
}

Chainable Arguments with WithArg

WithArg provides a fluent API for adding arguments to your resolvers. It supports scalars, structs (including deeply nested), and uses ArgsMap(p.Args) for type-safe argument access.

Basic Usage
// Without args - simple resolver
graph.NewResolver[Message]("hello").
    WithResolver(func(p graph.ResolveParams) (*Message, error) {
        return &Message{Text: "Hello, World!"}, nil
    }).BuildQuery()

// With scalar args - using graph.String, graph.Int, etc.
graph.NewResolver[User]("user").
    WithArg("id", graph.String).
    WithArg("limit", graph.Int).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        id := graph.Get[string](graph.ArgsMap(p.Args), "id")
        limit := graph.GetOr[int](graph.ArgsMap(p.Args), "limit", 10)
        return userService.GetByID(id)
    }).BuildQuery()

// With struct input - auto-generates InputObject
type UserInput struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

graph.NewResolver[User]("createUser").
    WithArg("input", UserInput{}).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        input := graph.Get[UserInput](graph.ArgsMap(p.Args), "input")
        return userService.Create(input.Name, input.Email)
    }).BuildMutation()
Supported Argument Types
Type Usage GraphQL Type
graph.String WithArg("id", graph.String) String
graph.Int WithArg("age", graph.Int) Int
graph.Float WithArg("price", graph.Float) Float
graph.Boolean WithArg("active", graph.Boolean) Boolean
graph.ID WithArg("userId", graph.ID) ID
Zero values WithArg("name", "") String
Structs WithArg("input", UserInput{}) UserInputInput
Args Helper Functions
// Get a value with type safety (returns zero value if missing)
id := graph.Get[string](args, "id")
age := graph.Get[int](args, "age")
active := graph.Get[bool](args, "active")

// Get struct argument with automatic conversion
input := graph.Get[UserInput](args, "input")

// Get with default value
limit := graph.GetOr[int](args, "limit", 10)
name := graph.GetOr[string](args, "name", "Anonymous")

// Get with error handling (returns error if missing or conversion fails)
input, err := graph.GetE[UserInput](args, "input")
if err != nil {
    return nil, fmt.Errorf("invalid input: %w", err)
}

// MustGet panics if missing or conversion fails (use when certain arg exists)
id := graph.MustGet[string](args, "id")

// Check if argument exists
if args.Has("optionalField") {
    // process optional field
}

// Get raw map for complex processing (if needed)
rawArgs := args.Raw()
Using Args with Subscriptions and Custom Resolvers (ArgsMap)

When using subscriptions or custom resolvers that receive p.Args directly (a map[string]interface{}), you can use graph.ArgsMap to wrap it and use the same type-safe getter functions:

// In subscriptions or custom resolvers with graphql.FieldConfigArgument
subscription := graph.NewSubscription[Message]("onMessage").
    WithField("channelID", graphql.String).
    WithResolver(func(ctx context.Context, p graph.ResolveParams) (<-chan *Message, error) {
        // Wrap p.Args with ArgsMap to use type-safe getters
        channelID := graph.Get[string](graph.ArgsMap(p.Args), "channelID")

        // Or with error handling
        channelID, err := graph.GetE[string](graph.ArgsMap(p.Args), "channelID")
        if err != nil {
            return nil, fmt.Errorf("channelID required: %w", err)
        }

        // ... create and return channel
    })

Both graph.Args (from WithArg chainable API) and graph.ArgsMap (wrapping p.Args) implement the ArgsGetter interface, so all getter functions work with both:

Source Usage When to Use
graph.Args graph.Get[T](args, "key") With WithResolver(func(p, args)...)
graph.ArgsMap(p.Args) graph.Get[T](graph.ArgsMap(p.Args), "key") Subscriptions, middlewares, custom resolvers
Error Handling Options
Function Missing Key Conversion Error Use Case
Get[T] Zero value Zero value Optional args, quick access
GetOr[T] Default Default Args with defaults
GetE[T] Error Error Required args with validation
MustGet[T] Panic Panic Required args (certain to exist)

Example with error handling:

graph.NewResolver[User]("createUser").
    WithArg("input", CreateUserInput{}).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        // Explicit error handling
        input, err := graph.GetE[CreateUserInput](graph.ArgsMap(p.Args), "input")
        if err != nil {
            return nil, fmt.Errorf("invalid input: %w", err)
        }
        return userService.Create(input.Name, input.Email)
    }).BuildMutation()
Required Arguments and Defaults
// Required argument (GraphQL NonNull)
graph.NewResolver[User]("user").
    WithArgRequired("id", graph.String).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        id := graph.Get[string](graph.ArgsMap(p.Args), "id") // always present
        return userService.GetByID(id)
    }).BuildQuery()

// Argument with default value
graph.NewResolver[User]("users").
    AsList().
    WithArgDefault("limit", graph.Int, 20).
    WithArgDefault("offset", graph.Int, 0).
    WithResolver(func(p graph.ResolveParams) (*[]User, error) {
        limit := graph.Get[int](graph.ArgsMap(p.Args), "limit")  // 20 if not provided
        offset := graph.Get[int](graph.ArgsMap(p.Args), "offset") // 0 if not provided
        return userService.List(limit, offset)
    }).BuildQuery()
Nested Struct Support

Deeply nested structs are automatically converted to GraphQL InputObjects:

type AddressInput struct {
    Street  string `json:"street"`
    City    string `json:"city"`
    Country string `json:"country"`
}

type UserProfileInput struct {
    Name    string       `json:"name"`
    Age     int          `json:"age"`
    Address AddressInput `json:"address"`
}

// Generates nested InputObjects automatically
graph.NewResolver[User]("createUser").
    WithArg("profile", UserProfileInput{}).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        profile := graph.Get[UserProfileInput](graph.ArgsMap(p.Args), "profile")
        // Access nested data with type safety
        return userService.Create(profile.Name, profile.Address.City)
    }).BuildMutation()
WithResolver Signature

WithResolver accepts a single, simple signature:

func(p graph.ResolveParams) (*T, error)

Access arguments using ArgsMap(p.Args) with the type-safe Get functions:

// Without args
graph.NewResolver[Message]("hello").
    WithResolver(func(p graph.ResolveParams) (*Message, error) {
        return &Message{Text: "Hello!"}, nil
    }).BuildQuery()

// With args - access via ArgsMap(p.Args)
graph.NewResolver[User]("user").
    WithArg("id", graph.String).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        id := graph.Get[string](graph.ArgsMap(p.Args), "id")
        return userService.GetByID(id)
    }).BuildQuery()
Complete Example
type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

type CreateUserInput struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Query with multiple args
func getUsers() graph.QueryField {
    return graph.NewResolver[User]("users").
        AsList().
        WithArg("search", graph.String).
        WithArgDefault("limit", graph.Int, 20).
        WithArgDefault("offset", graph.Int, 0).
        WithResolver(func(p graph.ResolveParams) (*[]User, error) {
            search := graph.GetOr[string](graph.ArgsMap(p.Args), "search", "")
            limit := graph.Get[int](graph.ArgsMap(p.Args), "limit")
            offset := graph.Get[int](graph.ArgsMap(p.Args), "offset")

            users := userService.Search(search, limit, offset)
            return &users, nil
        }).BuildQuery()
}

// Mutation with struct input
func createUser() graph.MutationField {
    return graph.NewResolver[User]("createUser").
        WithArg("input", CreateUserInput{}).
        WithResolver(func(p graph.ResolveParams) (*User, error) {
            input := graph.Get[CreateUserInput](graph.ArgsMap(p.Args), "input")
            return userService.Create(input.Name, input.Email)
        }).BuildMutation()
}

Typed Mutations with NewMutation

NewMutation[T, In] is the recommended way to build mutations. It forces you to pick a kind (Create, Update, Delete, Action, Upsert) before wiring a resolver — each kind gives you the right resolver signature and the right schema shape. Input decoding, lifecycle hooks, and the presence-aware Patch[In] for partial updates are built in. Decode plans and patch field tables are cached per input type, so repeat calls avoid re-walking reflection.

Why kind-based builders?

The kind enforces intent at compile time:

Kind Resolver signature Use case
Create func(ctx, In) (*T, error) Insert a new record
Update func(ctx, Patch[In]) (*T, error) Partial update — Patch tells you which fields the client sent
Delete func(ctx, In) (*T, error) Remove a record (returns the deleted entity, or a tombstone)
Action func(ctx, In) (*T, error) Side-effectful operation that isn't CRUD (sendEmail, rotateKey, etc.)
Upsert func(ctx, Patch[In]) (Result[T], error) Insert-or-update — Result.Created surfaces which path ran

The MutationBuilder itself has no WithResolver or Build; you must transition to a kind builder first. This prevents accidentally using a Create resolver signature for an Update that needs presence information.

Quick Start
type CreateUserInput struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Age   int    `json:"age"`
}

createUser := graph.NewMutation[User, CreateUserInput]("createUser").
    WithDescription("Create a new user").
    Create().
    WithResolver(func(ctx context.Context, in CreateUserInput) (*User, error) {
        return userService.Create(ctx, in)
    }).
    Build()

graph.NewHTTP(&graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{
        MutationFields: []graph.MutationField{createUser},
    },
})

The input type (CreateUserInput) is automatically registered as a GraphQL InputObject named CreateUserInputInput (the "Input" suffix is appended when missing). The output type (User) is registered as a GraphQL Object. Both are cached globally so the same type produces the same GraphQL type across mutations.

Partial Updates with Patch[In]

Update and Upsert resolvers receive Patch[In], which distinguishes omitted from set to zero value. This is crucial for PATCH-style updates where null or missing fields should not overwrite existing data.

type UpdateUserInput struct {
    Name  string `json:"name"`
    Email string `json:"email"`
    Bio   string `json:"bio"`
}

updateUser := graph.NewMutation[User, UpdateUserInput]("updateUser").
    Update().
    WithResolver(func(ctx context.Context, p graph.Patch[UpdateUserInput]) (*User, error) {
        existing, err := userService.Get(ctx, userIDFromCtx(ctx))
        if err != nil {
            return nil, err
        }

        // Only fields actually sent by the client are applied onto `existing`.
        p.Apply(existing)

        // Or inspect manually:
        if p.Has("email") {
            // Email was sent — check uniqueness, etc.
        }

        return userService.Save(ctx, existing)
    }).
    Build()

Patch[In] exposes:

Method Purpose
Get() In The decoded input struct
Has(field string) bool Was this field sent by the client?
Fields() []string All field names the client sent
Presence() PresenceSet Read-only set view
Apply(dst *T) Copy only the present fields onto dst (reflection-cached)
Upserts return Result[T]

Upsert surfaces the insert-vs-update outcome to the client via an auto-generated <MutationName>Payload object with result and created fields:

upsertUser := graph.NewMutation[User, UserInput]("upsertUser").
    Upsert().
    WithResolver(func(ctx context.Context, p graph.Patch[UserInput]) (graph.Result[User], error) {
        u, created, err := userService.Upsert(ctx, p.Get())
        if err != nil {
            return graph.Result[User]{}, err
        }
        return graph.Result[User]{Value: u, Created: created}, nil
    }).
    Build()

GraphQL query:

mutation {
  upsertUser(input: { name: "Ada", email: "ada@example.com" }) {
    created
    result { id name email }
  }
}
Lifecycle Hooks

Implement any of these interfaces on your input type to hook into the mutation pipeline. They run in this order, all before the resolver:

Interface Method Order Failure Code
InputNormalizer Normalize() 1
InputAuthorizer Authorize(ctx) error 2 UNAUTHORIZED
PatchInputValidator (Update/Upsert only) ValidatePatch(ctx, present) error 3 INVALID_INPUT
InputValidator (Create/Delete/Action, or Update/Upsert fallback) Validate(ctx) error 3 INVALID_INPUT
type CreateUserInput struct {
    Name  string `json:"name"`
    Email string `json:"email"`
}

// Trim whitespace and lowercase the email before anything else sees it.
func (in *CreateUserInput) Normalize() {
    in.Name = strings.TrimSpace(in.Name)
    in.Email = strings.ToLower(strings.TrimSpace(in.Email))
}

// Gate the mutation to logged-in users.
func (in *CreateUserInput) Authorize(ctx context.Context) error {
    if _, ok := ctx.Value("userID").(string); !ok {
        return errors.New("login required")
    }
    return nil
}

// Enforce field-level rules after normalization.
func (in *CreateUserInput) Validate(ctx context.Context) error {
    if !strings.Contains(in.Email, "@") {
        return &graph.MutationError{
            Code: graph.CodeInvalidInput, Field: "email", Message: "invalid email",
        }
    }
    return nil
}

For Update, prefer ValidatePatch(ctx, present PresenceSet) error so you can skip rules for fields the client didn't send:

func (in *UpdateUserInput) ValidatePatch(ctx context.Context, present graph.PresenceSet) error {
    if present.Has("email") && !strings.Contains(in.Email, "@") {
        return &graph.MutationError{Code: graph.CodeInvalidInput, Field: "email"}
    }
    return nil
}
Error Model

Return *MutationError (or any error — non-MutationError errors are wrapped as INVALID_INPUT or UNAUTHORIZED depending on which hook failed, or INTERNAL if the resolver itself is unset). Codes map cleanly to GraphQL error extensions:

const (
    CodeInvalidInput ErrorCode = "INVALID_INPUT"
    CodeUnauthorized ErrorCode = "UNAUTHORIZED"
    CodeNotFound     ErrorCode = "NOT_FOUND"
    CodeConflict     ErrorCode = "CONFLICT"
    CodeInternal     ErrorCode = "INTERNAL"
)

MutationError implements Extensions() map[string]any, which graphql-go surfaces as the error's extensions block — clients get a structured { "code": "CONFLICT", "field": "email" } instead of a string they have to regex.

return nil, &graph.MutationError{
    Code:    graph.CodeConflict,
    Field:   "email",
    Message: "email already registered",
}
Middleware, Input Name, Description

All kinds share these configuration calls on the base builder:

graph.NewMutation[User, CreateUserInput]("createUser").
    WithDescription("Register a new user account").
    WithInputName("payload"). // default is "input"
    Use(authMiddleware, auditMiddleware).
    Create().
    WithResolver(createHandler).
    Build()

Middleware wraps the resolver using the same FieldMiddleware type as resolver middleware — see Middleware below.

Performance
Operation Time/op Allocations
Build (Create) ~381 ns 10 allocs
Build (Update) ~380 ns 10 allocs
Build (Action) ~356 ns 10 allocs
Execute (Create) ~302 ns 5 allocs
Execute (Update) ~443 ns 7 allocs
Execute (Delete) ~227 ns 5 allocs
Execute (Action) ~232 ns 5 allocs
Decode Input ~245 ns 3 allocs
Patch.Apply ~93 ns 1 alloc

Decode plans (tag parsing + setter selection per field) and patch field tables (name + struct index) are cached in sync.Maps keyed by reflect.Type. Reflection happens once per input type, ever — the hot path uses the cached plan directly.

Comparison with the older NewResolver[T]().BuildMutation() API

The older pattern still works, but has rough edges the new API fixes:

Concern NewResolver[T].BuildMutation() NewMutation[T, In]
Resolver signature Same for create/update/delete/action Kind-specific — compiler enforces the right shape
Partial updates Manual — you decide per field whether empty means "omitted" or "clear" First-class Patch[In] with Has(field)
Upsert payload DIY — return a map or custom struct Built-in Result[T] + auto-generated <Name>Payload
Lifecycle hooks Scattered across middleware Interface-based: Normalize, Authorize, Validate(Patch)
Error codes String messages Typed MutationError with Extensions()
Input parsing GetArg(p, "input", &in) (re-parse per call) Cached decode plan per type

The old BuildMutation() is still supported for backwards compatibility; new code should prefer NewMutation[T, In].

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").
    WithArg("id", graph.Int).
    WithMiddleware(graph.LoggingMiddleware).
    WithMiddleware(graph.AuthMiddleware("admin")).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        id := graph.Get[int](graph.ArgsMap(p.Args), "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").
    WithArg("id", graph.Int).
    WithMiddleware(graph.CacheMiddleware(func(p graph.ResolveParams) string {
        id := graph.Get[int](graph.ArgsMap(p.Args), "id")
        return fmt.Sprintf("product:%d", id)
    })).
    WithResolver(func(p graph.ResolveParams) (*Product, error) {
        // Only executes on cache miss
        id := graph.Get[int](graph.ArgsMap(p.Args), "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().
    WithArg("id", graph.Int).
    WithPermission(graph.AuthMiddleware("admin")).
    WithResolver(func(p graph.ResolveParams) (*User, error) {
        id := graph.Get[int](graph.ArgsMap(p.Args), "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

GraphQL Subscriptions

Real-time event streaming over WebSocket with type-safe resolvers and middleware support.

Features
  • 🔄 Real-time Events - Stream data to clients over WebSocket
  • 🎯 Type-Safe Resolvers - Generic subscription builders with compile-time safety
  • 🔌 Dual Protocol Support - Works with both graphql-ws (modern) and subscriptions-transport-ws (legacy)
  • 🏗️ Fluent Builder API - Same intuitive API as queries and mutations
  • 🛡️ Middleware Support - Apply authentication, logging, and custom middleware
  • 🔍 Event Filtering - Filter events before sending to clients
  • 📦 Pluggable PubSub - In-memory, Redis, Kafka, or custom backends
  • 🎭 Field-Level Control - Custom resolvers and middleware per field
Quick Start
package main

import (
    "context"
    "encoding/json"
    "net/http"
    "time"
    "github.com/graphql-go/graphql"
    graph "github.com/paulmanoni/go-graph"
)

type MessageEvent struct {
    ID        string    `json:"id"`
    Content   string    `json:"content"`
    Author    string    `json:"author"`
    Timestamp time.Time `json:"timestamp"`
}

func main() {
    // Initialize PubSub system
    pubsub := graph.NewInMemoryPubSub()
    defer pubsub.Close()

    // Create subscription
    messageSubscription := graph.NewSubscription[MessageEvent]("messageAdded").
        WithDescription("Subscribe to new messages").
        WithArgs(graphql.FieldConfigArgument{
            "channelID": &graphql.ArgumentConfig{
                Type: graphql.NewNonNull(graphql.String),
            },
        }).
        WithResolver(func(ctx context.Context, p graph.ResolveParams) (<-chan *MessageEvent, error) {
            channelID := graph.Get[string](graph.ArgsMap(p.Args), "channelID")

            // Create output channel
            events := make(chan *MessageEvent, 10)

            // Subscribe to PubSub topic
            subscription := pubsub.Subscribe(ctx, "messages:"+channelID)

            // Forward events to GraphQL channel
            go func() {
                defer close(events)
                for msg := range subscription {
                    var event MessageEvent
                    json.Unmarshal(msg.Data, &event)
                    events <- &event
                }
            }()

            return events, nil
        }).
        BuildSubscription()

    // Build GraphQL handler with subscriptions
    handler := graph.NewHTTP(&graph.GraphContext{
        SchemaParams: &graph.SchemaBuilderParams{
            QueryFields:        []graph.QueryField{...},
            MutationFields:     []graph.MutationField{...},
            SubscriptionFields: []graph.SubscriptionField{messageSubscription},
        },
        PubSub:              pubsub,
        EnableSubscriptions: true,
        Playground:          true,
    })

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

Test the subscription:

subscription {
  messageAdded(channelID: "general") {
    id
    content
    author
    timestamp
  }
}
PubSub System

The PubSub interface enables pluggable backends for event distribution:

type PubSub interface {
    Publish(ctx context.Context, topic string, data interface{}) error
    Subscribe(ctx context.Context, topic string) <-chan *Message
    Unsubscribe(ctx context.Context, subscriptionID string) error
    Close() error
}
In-Memory PubSub (Development)

Perfect for development and single-instance deployments:

pubsub := graph.NewInMemoryPubSub()
defer pubsub.Close()

// Publish events
ctx := context.Background()
pubsub.Publish(ctx, "messages:general", &MessageEvent{
    ID:      "1",
    Content: "Hello!",
    Author:  "Alice",
})

// Subscribe to events
subscription := pubsub.Subscribe(ctx, "messages:general")
for msg := range subscription {
    // Process message
}
Custom PubSub Backends

Implement the PubSub interface for Redis, Kafka, or other message brokers:

type RedisPubSub struct {
    client *redis.Client
    // ... implementation
}

func (r *RedisPubSub) Publish(ctx context.Context, topic string, data interface{}) error {
    jsonData, _ := json.Marshal(data)
    return r.client.Publish(ctx, topic, jsonData).Err()
}

func (r *RedisPubSub) Subscribe(ctx context.Context, topic string) <-chan *graph.Message {
    // ... implementation
}

// Use with GraphContext
handler := graph.NewHTTP(&graph.GraphContext{
    PubSub: &RedisPubSub{client: redisClient},
    EnableSubscriptions: true,
})
Subscription Resolver API

The NewSubscription[T] builder provides a fluent API for creating type-safe subscriptions:

graph.NewSubscription[EventType]("subscriptionName").
    WithDescription("Description of the subscription").
    WithArgs(graphql.FieldConfigArgument{...}).
    WithResolver(func(ctx context.Context, p graph.ResolveParams) (<-chan *EventType, error) {
        // Return a channel that emits events
    }).
    WithFilter(func(ctx context.Context, data *EventType, p graph.ResolveParams) bool {
        // Filter events before sending to clients
        return true
    }).
    WithMiddleware(graph.AuthMiddleware("user")).
    WithFieldResolver("customField", func(p graph.ResolveParams) (interface{}, error) {
        // Custom resolver for specific fields
    }).
    WithFieldMiddleware("sensitiveField", graph.AuthMiddleware("admin")).
    BuildSubscription()
Basic Subscription
type UserStatusEvent struct {
    UserID    string    `json:"userID"`
    Status    string    `json:"status"`
    Timestamp time.Time `json:"timestamp"`
}

func userStatusSubscription(pubsub graph.PubSub) graph.SubscriptionField {
    return graph.NewSubscription[UserStatusEvent]("userStatusChanged").
        WithDescription("Subscribe to user status changes").
        WithResolver(func(ctx context.Context, p graph.ResolveParams) (<-chan *UserStatusEvent, error) {
            events := make(chan *UserStatusEvent, 10)
            subscription := pubsub.Subscribe(ctx, "user_status")

            go func() {
                defer close(events)
                for msg := range subscription {
                    var event UserStatusEvent
                    json.Unmarshal(msg.Data, &event)
                    events <- &event
                }
            }()

            return events, nil
        }).
        BuildSubscription()
}
With Arguments
func messageSubscription(pubsub graph.PubSub) graph.SubscriptionField {
    return graph.NewSubscription[Message]("messageAdded").
        WithArgs(graphql.FieldConfigArgument{
            "channelID": &graphql.ArgumentConfig{
                Type:        graphql.NewNonNull(graphql.String),
                Description: "Channel to subscribe to",
            },
        }).
        WithResolver(func(ctx context.Context, p graph.ResolveParams) (<-chan *Message, error) {
            channelID := graph.Get[string](graph.ArgsMap(p.Args), "channelID")

            events := make(chan *Message, 10)
            subscription := pubsub.Subscribe(ctx, "messages:"+channelID)

            go func() {
                defer close(events)
                for msg := range subscription {
                    var message Message
                    json.Unmarshal(msg.Data, &message)
                    events <- &message
                }
            }()

            return events, nil
        }).
        BuildSubscription()
}
With Event Filtering

Filter events based on user permissions or subscription criteria:

func messageSubscription(pubsub graph.PubSub) graph.SubscriptionField {
    return graph.NewSubscription[Message]("messageAdded").
        WithArgs(graphql.FieldConfigArgument{
            "channelID": &graphql.ArgumentConfig{Type: graphql.String},
        }).
        WithResolver(func(ctx context.Context, p graph.ResolveParams) (<-chan *Message, error) {
            // Subscribe to all messages
            events := make(chan *Message, 10)
            subscription := pubsub.Subscribe(ctx, "messages")

            go func() {
                defer close(events)
                for msg := range subscription {
                    var message Message
                    json.Unmarshal(msg.Data, &message)
                    events <- &message
                }
            }()

            return events, nil
        }).
        WithFilter(func(ctx context.Context, data *Message, p graph.ResolveParams) bool {
            // Filter by channel ID
            channelID := graph.Get[string](graph.ArgsMap(p.Args), "channelID")
            return data.ChannelID == channelID
        }).
        BuildSubscription()
}
With Authentication Middleware

Protect subscriptions with authentication:

func adminSubscription(pubsub graph.PubSub) graph.SubscriptionField {
    return graph.NewSubscription[AdminEvent]("adminEvents").
        WithDescription("Admin-only event stream").
        WithMiddleware(graph.AuthMiddleware("admin")).
        WithResolver(func(ctx context.Context, p graph.ResolveParams) (<-chan *AdminEvent, error) {
            // Only admins reach here
            events := make(chan *AdminEvent, 10)
            subscription := pubsub.Subscribe(ctx, "admin_events")

            go func() {
                defer close(events)
                for msg := range subscription {
                    var event AdminEvent
                    json.Unmarshal(msg.Data, &event)
                    events <- &event
                }
            }()

            return events, nil
        }).
        BuildSubscription()
}
With Field-Level Customization

Customize how specific fields are resolved:

type MessageEvent struct {
    ID       string `json:"id"`
    Content  string `json:"content"`
    AuthorID string `json:"authorID"`
}

func messageSubscription(pubsub graph.PubSub) graph.SubscriptionField {
    return graph.NewSubscription[MessageEvent]("messageAdded").
        WithResolver(func(ctx context.Context, p graph.ResolveParams) (<-chan *MessageEvent, error) {
            // ... resolver implementation
        }).
        WithFieldResolver("author", func(p graph.ResolveParams) (interface{}, error) {
            // Custom resolver for author field
            event := p.Source.(MessageEvent)
            return userService.GetByID(event.AuthorID), nil
        }).
        WithFieldMiddleware("content", graph.AuthMiddleware("user")).
        BuildSubscription()
}
WebSocket Configuration

Configure WebSocket behavior through GraphContext:

handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{
        SubscriptionFields: []graph.SubscriptionField{...},
    },

    // Enable subscriptions
    EnableSubscriptions: true,

    // PubSub backend
    PubSub: pubsub,

    // Optional: Custom WebSocket path (default: auto-detects)
    WebSocketPath: "/subscriptions",

    // Optional: Custom origin check
    WebSocketCheckOrigin: func(r *http.Request) bool {
        origin := r.Header.Get("Origin")
        return origin == "https://example.com"
    },

    // Optional: Authentication for WebSocket connections
    UserDetailsFn: func(ctx context.Context, token string) (context.Context, interface{}, error) {
        user, err := validateAndGetUser(token)
        return ctx, user, err
    },
})
Authentication in Subscriptions

WebSocket authentication works similarly to HTTP:

handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{
        SubscriptionFields: []graph.SubscriptionField{...},
    },
    EnableSubscriptions: true,
    PubSub:              pubsub,

    // Extract user details from token
    UserDetailsFn: func(ctx context.Context, token string) (context.Context, interface{}, error) {
        user, err := validateJWT(token)
        if err != nil {
            return ctx, nil, err
        }
        return ctx, user, nil
    },
})

Client sends token during connection initialization:

const client = new SubscriptionClient('ws://localhost:8080/graphql', {
  connectionParams: {
    authorization: 'Bearer <token>',
  },
});

Access user details in subscription resolver:

func messageSubscription(pubsub graph.PubSub) graph.SubscriptionField {
    return graph.NewSubscription[Message]("messageAdded").
        WithResolver(func(ctx context.Context, p graph.ResolveParams) (<-chan *Message, error) {
            // Get authenticated user
            var user User
            if err := graph.GetRootInfo(p, "details", &user); err != nil {
                return nil, fmt.Errorf("authentication required")
            }

            // Subscribe to user-specific messages
            events := make(chan *Message, 10)
            subscription := pubsub.Subscribe(ctx, "messages:"+user.ID)

            // ... forward events

            return events, nil
        }).
        BuildSubscription()
}
Publishing Events from Mutations

Trigger subscription events from mutations:

func sendMessageMutation(pubsub graph.PubSub) graph.MutationField {
    type SendMessageInput struct {
        ChannelID string `json:"channelID" graphql:"channelID,required"`
        Content   string `json:"content" graphql:"content,required"`
    }

    return graph.NewResolver[Message]("sendMessage").
        WithInputObject(SendMessageInput{}).
        WithResolver(func(p graph.ResolveParams) (*Message, error) {
            var input SendMessageInput
            graph.GetArg(p, "input", &input)

            // Create message
            msg := &Message{
                ID:        uuid.New().String(),
                Content:   input.Content,
                ChannelID: input.ChannelID,
                Timestamp: time.Now(),
            }

            // Store in database
            db.Create(msg)

            // Publish to subscribers
            ctx := context.Background()
            pubsub.Publish(ctx, "messages:"+input.ChannelID, msg)

            return msg, nil
        }).
        BuildMutation()
}
Protocol Support

The WebSocket handler supports both modern and legacy protocols:

Protocol Support Client
graphql-ws ✅ Full Apollo Client 3+, urql
subscriptions-transport-ws ✅ Full Apollo Client 2, GraphQL Playground

Both protocols work simultaneously - no configuration needed.

Production Considerations
Connection Limits

Monitor and limit concurrent WebSocket connections:

var (
    maxConnections = 10000
    currentConns   int64
)

handler := graph.NewHTTP(&graph.GraphContext{
    EnableSubscriptions: true,
    WebSocketCheckOrigin: func(r *http.Request) bool {
        if atomic.LoadInt64(&currentConns) >= int64(maxConnections) {
            return false
        }
        atomic.AddInt64(&currentConns, 1)
        return true
    },
})
Event Buffering

Use buffered channels to handle bursts:

// Good: Buffered channel prevents blocking
events := make(chan *Message, 100)

// Bad: Unbuffered channel may block publishers
events := make(chan *Message)
Error Handling

Handle errors gracefully in subscription resolvers:

WithResolver(func(ctx context.Context, p graph.ResolveParams) (<-chan *Event, error) {
    // Validate arguments first using type-safe API
    channelID, err := graph.GetE[string](graph.ArgsMap(p.Args), "channelID")
    if err != nil || channelID == "" {
        return nil, fmt.Errorf("channelID required")
    }

    // Create subscription
    events := make(chan *Event, 10)
    subscription := pubsub.Subscribe(ctx, "events:"+channelID)

    go func() {
        defer close(events)
        for msg := range subscription {
            var event Event
            if err := json.Unmarshal(msg.Data, &event); err != nil {
                log.Printf("Failed to unmarshal event: %v", err)
                continue // Skip malformed events
            }
            events <- &event
        }
    }()

    return events, nil
})
Graceful Shutdown

Close all connections during shutdown:

func main() {
    pubsub := graph.NewInMemoryPubSub()

    handler := graph.NewHTTP(&graph.GraphContext{
        PubSub:              pubsub,
        EnableSubscriptions: true,
    })

    server := &http.Server{Addr: ":8080", Handler: handler}

    // Graceful shutdown
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

    go func() {
        <-sigChan
        ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
        defer cancel()

        // Close PubSub (closes all subscriptions)
        pubsub.Close()

        // Shutdown HTTP server
        server.Shutdown(ctx)
    }()

    server.ListenAndServe()
}
Complete Example

See examples/subscription for a complete working example with:

  • Message subscriptions with channel filtering
  • User status change subscriptions
  • Authentication and authorization
  • Mutations that trigger subscription events
  • Background event simulator

Run the example:

cd examples/subscription
go run main.go

Then test subscriptions in GraphQL Playground at http://localhost:8080/graphql

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(context.Context, string) (context.Context, interface{}, error) nil Fetch user from token and optionally update context
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
    SubscriptionFields []SubscriptionField  // Optional: Real-time subscriptions
}

Testing

The library includes comprehensive test coverage including unit tests and benchmarks for all features including queries, mutations, and subscriptions.

Running Unit Tests
# Run all tests
go test -v

# Run tests with coverage
go test -cover

# Generate coverage report
go test -coverprofile=coverage.out
go tool cover -html=coverage.out

# Run specific test
go test -run TestSubscription_WithFilter

# Run tests for a specific file
go test -run Subscription
Subscription Tests

The subscription implementation includes extensive unit tests covering:

  • Basic subscription creation - Creating and configuring subscriptions
  • Event streaming - Testing event channels and data flow
  • Event filtering - Filtering events based on subscription parameters
  • Middleware integration - Applying middleware to subscriptions
  • PubSub integration - Testing with in-memory and custom PubSub backends
  • Context cancellation - Proper cleanup when subscriptions are cancelled
  • Type generation - Auto-generating GraphQL types from Go structs
  • Error handling - Proper error propagation and recovery

Example test run:

# Run all subscription tests
go test -v -run Subscription

# Run specific subscription test
go test -v -run TestSubscription_WithFilter
Writing Tests for Your Resolvers

Here's an example of how to test your custom resolvers:

func TestMySubscription(t *testing.T) {
    type MyEvent struct {
        ID      string `json:"id"`
        Message string `json:"message"`
    }

    sub := graph.NewSubscription[MyEvent]("myEvent").
        WithResolver(func(ctx context.Context, p graph.ResolveParams) (<-chan *MyEvent, error) {
            ch := make(chan *MyEvent, 1)
            ch <- &MyEvent{ID: "1", Message: "test"}
            close(ch)
            return ch, nil
        }).
        BuildSubscription()

    field := sub.Serve()

    // Test subscription execution
    result, err := field.Subscribe(graphql.ResolveParams{
        Context: context.Background(),
    })

    if err != nil {
        t.Fatalf("Subscribe error: %v", err)
    }

    outputCh, ok := result.(<-chan interface{})
    if !ok {
        t.Fatalf("Expected channel, got %T", result)
    }

    // Collect events
    var received []MyEvent
    for event := range outputCh {
        received = append(received, event.(MyEvent))
    }

    if len(received) != 1 {
        t.Errorf("Expected 1 event, got %d", len(received))
    }
}

Examples

See the examples directory for complete working examples:

  • main.go - Full example with authentication
  • subscription/main.go - Real-time subscriptions with WebSocket

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 ~36 ns 0 allocs Bearer token from header
Type Registration ~14 ns 0 allocs Object type caching
Get[string] ~10 ns 0 allocs Extract string argument
Get[int] ~10 ns 0 allocs Extract int argument
Get[bool] ~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 ~10 μs 124 allocs Default hello/echo schema
Complex Schema ~12 μs 148 allocs Multiple types with nesting
With Subscriptions ~16 μs 136 allocs Schema + subscription field
Multiple Subscriptions ~16 μs 125 allocs Schema + multiple subscription fields
Query Validation
Operation Time/op Allocations Description
Simple Query ~816 ns 27 allocs Basic field selection
Complex Query ~3.7 μs 103 allocs Nested 3 levels deep
Deep Query ~2.5 μs 72 allocs Nested 5+ levels
With Aliases ~4.6 μs 130 allocs Multiple field aliases
SecurityRules (uncached) ~1.4–5.6 μs 43 allocs Default security ruleset
SecurityRules (cached) ~141 ns 1 alloc Same ruleset via QueryASTCache
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

QueryASTCache skips the parser on repeat queries — see Query AST Caching below.

HTTP Handler Performance
Operation Time/op Allocations Description
Debug Mode ~31 μs 439 allocs No validation/sanitization
With Validation ~35 μs 466 allocs Query validation enabled
With Sanitization ~35 μs 560 allocs Error sanitization enabled
With Auth ~30 μs 445 allocs Token + user details fetch
GET Request ~29 μs 436 allocs Query string parsing
Parallel (b.RunParallel) ~19 μs 440 allocs Concurrent request handling
Custom Root Object ~29 μs 441 allocs RootObjectFn wired
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
Argument Handling
Operation Time/op Allocations Description
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
Typed Mutations (NewMutation[T, In])
Operation Time/op Allocations Description
Build (Create) ~381 ns 10 allocs Field construction
Build (Update) ~380 ns 10 allocs Field construction
Build (Action) ~356 ns 10 allocs Field construction
Execute (Create) ~302 ns 5 allocs Resolver dispatch
Execute (Update) ~443 ns 7 allocs Resolver dispatch + Patch build
Execute (Delete) ~227 ns 5 allocs Resolver dispatch
Execute (Action) ~232 ns 5 allocs Resolver dispatch
Decode Input ~245 ns 3 allocs Cached plan, per-input decode
Patch.Apply ~93 ns 1 alloc Copy present fields to dst
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 ~3.2 μs 41 allocs Regex error cleaning (pooled regex + error-marker fast path)
Cached Field Resolver ~5.6 ns 0 allocs Cache hit scenario
Response Write ~3.4 ns 0 allocs Buffer write operation
Subscription Performance

Comprehensive benchmarks for real-time subscription operations:

Operation Time/op Allocations Description
Build Subscription ~200–600 ns 5–15 allocs Create subscription resolver
Build Complex Subscription ~800 ns 17 allocs With args, filter, middleware
Execute Subscription ~589 ns 8 allocs Start event streaming
With Filter ~3.6 μs 20 allocs Filter 100 events
With Middleware ~1.4 μs 13 allocs 3 middleware layers
UnmarshalSubscriptionMessage ~300 ns 5 allocs JSON parsing
Event Throughput ~55 μs 2024 allocs 1000 events/subscription (dominated by event payload alloc)
With PubSub ~3.4 μs 16 allocs PubSub integration, context-scoped per iter
Type Generation ~800 ns 15 allocs Complex event type
Concurrent Subscriptions ~1.9 μs 36 allocs Parallel execution
Schema with Subscriptions ~16 μs 136 allocs Multiple subscriptions

Key Observations:

  • Subscription creation is fast (~200-800ns) with minimal allocations
  • Event filtering adds minimal overhead (~1-2μs for 100 events)
  • Type generation for complex events is efficient (~800ns)
  • Concurrent subscription handling performs excellently (~500ns per subscription)
  • PubSub integration adds negligible overhead (~1μs)

Running Subscription Benchmarks:

# Run all subscription benchmarks
go test -bench=BenchmarkSubscription -benchmem

# Run specific subscription benchmark
go test -bench=BenchmarkSubscription_WithFilter -benchmem

# Compare with and without middleware
go test -bench=BenchmarkSubscription_Execute -benchmem
go test -bench=BenchmarkSubscription_WithMiddleware -benchmem
Embedded Struct Field Generation

Performance benchmarks for automatic embedded struct flattening:

Operation Time/op Allocations Description
Generate Fields (Embedded) ~1.2 μs 22 allocs Single embedded struct
Generate Fields (Multiple) ~1.4 μs 28 allocs Multiple embedded structs
Generate Fields (Deep) ~1.7 μs 36 allocs 4-level deep embedding
Generate Fields (No Embedding) ~1.1 μs 20 allocs Baseline comparison
Generate Object (Embedded) ~1.4 μs 24 allocs Object with embedded fields
Generate Input (Embedded) ~1.1 μs 18 allocs Input type with embedding
Generate Args (Embedded) ~1.1 μs 20 allocs Args with embedded fields
Field Resolver (Embedded) ~232 ns 5 allocs Resolve embedded field
Parallel Generation ~779 ns 22 allocs Concurrent field generation
Complex Embedding ~2.1 μs 42 allocs Multiple mixed embeddings

Key Observations:

  • Embedded struct field flattening adds minimal overhead (~100-600ns depending on complexity)
  • Deep nesting (4+ levels) handled efficiently (~1.7μs)
  • Field resolution from embedded structs is extremely fast (~232ns)
  • Parallel generation scales well (~779ns vs ~1.2μs sequential)
  • Complex scenarios with multiple embeddings remain performant (~2.1μs)
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
Parallel Embedded Generation ~779 ns 22 allocs Concurrent embedded field gen
Query AST Caching

ExecuteValidationRules re-parses every incoming query by default. For steady-state traffic where the same queries recur (persisted queries, mobile apps with a fixed query set, internal clients), plug in a QueryASTCache to skip parsing on hits:

graph.NewHTTP(&graph.GraphContext{
    ValidationRules: graph.SecurityRules,
    ValidationOptions: &graph.ValidationOptions{
        QueryCache: graph.NewQueryASTCache(256), // upper bound on distinct queries
    },
    // ...
})

Observed impact on BenchmarkSecurityRules:

ns/op B/op allocs/op
Uncached 1.4–5.6 μs 1672 43
Cached (hit) 141 ns 64 1

That's ~12× faster and ~43× fewer allocations on the cache-hit path. The cache uses an RWMutex-protected map; when max entries is reached the map is swapped for a fresh one (coarse eviction) so memory stays bounded without the overhead of a per-entry LRU. Cached ASTs are read-only — validation rules must not mutate them.

Note: graphql-go's handler still parses the query a second time during execution. The cache eliminates the validation-side parse only. Fully eliminating the double-parse requires invoking graphql.Do directly instead of using handler.Handler.

Key Takeaways
  • Zero-allocation primitives: Token extraction and utility functions have zero heap allocations
  • Fast validation: Query validation adds minimal overhead (~800 ns–4 μs depending on complexity), or ~141 ns with QueryASTCache on a hit
  • Type-safe arguments: WithArg + Get[T](ArgsMap(…)) keep per-call overhead in the low hundreds of ns
  • Efficient type generation: Auto-generating GraphQL args from structs adds minimal overhead (~1.3μs one-time cost)
  • Pooled request/response paths: POST body buffers and the response-sanitization wrapper are reused via sync.Pool, cutting per-request allocation
  • Sanitize-on-error fast path: Responses without an "errors" field skip JSON unmarshal/remarshal entirely
  • Predictable performance: End-to-end request handling is consistently under 100μs
  • Production ready: Complete stack with all security features runs well under 100μs per request
Optimization Tips
  1. Enable the AST cache: Set ValidationOptions.QueryCache to a NewQueryASTCache(n) sized for your distinct-query count — biggest single win for steady-state traffic
  2. Enable type caching: Type registration is cached automatically — registered types are reused
  3. Use DEBUG mode wisely: Validation adds ~1 μs overhead on a cache miss (~0.1 μs on a hit), only disable in development
  4. Minimize complexity: Keep query depth under 10 levels for optimal validation performance
  5. Batch operations: Use concurrent requests for multiple independent queries
  6. 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
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(ctx context.Context, token string) (context.Context, interface{}, error) {
        user, err := validateAndGetUser(token)
        if err != nil {
            return ctx, nil, err
        }
        // Add values to context accessible via p.Context.Value() in resolvers
        ctx = context.WithValue(ctx, "userID", user.ID)
        return ctx, user, nil
    },
})

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 (
	// Client -> Server (graphql-ws)
	MessageTypeConnectionInit = "connection_init"
	MessageTypeSubscribe      = "subscribe"
	MessageTypePing           = "ping"

	// Client -> Server (subscriptions-transport-ws - legacy)
	MessageTypeStart = "start" // Legacy equivalent of "subscribe"
	MessageTypeStop  = "stop"  // Legacy equivalent of "complete"

	// Server -> Client (graphql-ws)
	MessageTypeConnectionAck = "connection_ack"
	MessageTypeNext          = "next"
	MessageTypeError         = "error"
	MessageTypeComplete      = "complete"
	MessageTypePong          = "pong"

	// Server -> Client (subscriptions-transport-ws - legacy)
	MessageTypeData                = "data"             // Legacy equivalent of "next"
	MessageTypeConnectionError     = "connection_error" // Legacy connection error
	MessageTypeConnectionKeepAlive = "ka"               // Legacy keep-alive
)

GraphQL WebSocket Protocol message types Supports both graphql-ws (new) and subscriptions-transport-ws (legacy) protocols

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 (
	String  = graphql.String
	Int     = graphql.Int
	Float   = graphql.Float
	Boolean = graphql.Boolean
	ID      = graphql.ID
)

GraphQL scalar type constants for use with WithArg These mirror Go's type names for intuitive usage:

WithArg("id", graph.String)   // like using 'string'
WithArg("limit", graph.Int)   // like using 'int'
View Source
var (
	// SecurityRules provides standard security validation
	// - Max depth: 10
	// - Max complexity: 200
	// - Max aliases: 4
	// - No introspection
	SecurityRules = []ValidationRule{
		NewMaxDepthRule(10),
		NewMaxComplexityRule(200),
		NewMaxAliasesRule(4),
		NewNoIntrospectionRule(),
	}

	// StrictSecurityRules provides strict security for production
	// - Max depth: 8
	// - Max complexity: 150
	// - Max aliases: 3
	// - Max tokens: 500
	// - No introspection
	StrictSecurityRules = []ValidationRule{
		NewMaxDepthRule(8),
		NewMaxComplexityRule(150),
		NewMaxAliasesRule(3),
		NewMaxTokensRule(500),
		NewNoIntrospectionRule(),
	}

	// DevelopmentRules provides lenient rules for development
	// - Max depth: 20
	// - Max complexity: 500
	DevelopmentRules = []ValidationRule{
		NewMaxDepthRule(20),
		NewMaxComplexityRule(500),
	}
)
View Source
var (
	// AdminOnlyFields - fields that require admin role
	AdminOnlyFields = map[string][]string{
		"deleteUser":     {"admin"},
		"deleteAccount":  {"admin"},
		"viewAuditLog":   {"admin"},
		"systemSettings": {"admin"},
		"manageRoles":    {"admin"},
	}

	// ManagerFields - fields that require admin or manager role
	ManagerFields = map[string][]string{
		"approveOrder":   {"admin", "manager"},
		"viewReports":    {"admin", "manager"},
		"manageTeam":     {"admin", "manager"},
		"bulkOperations": {"admin", "manager"},
	}

	// AuditorFields - fields that require admin or auditor role
	AuditorFields = map[string][]string{
		"viewAuditLog":  {"admin", "auditor"},
		"exportLogs":    {"admin", "auditor"},
		"viewAnalytics": {"admin", "auditor"},
	}
)

Common role configurations for convenience

View Source
var (
	ErrPubSubClosed         = newError("pubsub is closed")
	ErrSubscriptionNotFound = newError("subscription not found")
)

Common errors

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 ExecuteValidationRules added in v1.1.1

func ExecuteValidationRules(
	queryString string,
	schema *graphql.Schema,
	rules []ValidationRule,
	userDetails interface{},
	options *ValidationOptions,
) error

ExecuteValidationRules executes a set of validation rules against a GraphQL query. This is the modern validation system that supports custom rules.

Parameters:

  • queryString: The GraphQL query to validate
  • schema: The GraphQL schema
  • rules: The validation rules to execute
  • authCtx: Authentication context (can be nil if not needed)
  • options: Validation options (can be nil for defaults)

Returns:

  • nil if validation passes
  • *ValidationError for single rule failure
  • *MultiValidationError for multiple rule failures

Example:

rules := []ValidationRule{
    NewMaxDepthRule(10),
    NewRequireAuthRule("mutation"),
    NewRoleRules(AdminOnlyFields),
}
err := ExecuteValidationRules(query, schema, rules, authCtx, nil)

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 Get added in v1.1.7

func Get[T any](a ArgsGetter, name string) T

Get retrieves an argument by name with type safety. Returns zero value if key is missing or conversion fails. Use GetE for explicit error handling.

Works with both graph.Args (from WithArg) and graph.ArgsMap (from p.Args):

// With graph.Args (from WithArg chainable API)
id := graph.Get[string](args, "id")

// With p.Args (from graphql.FieldConfigArgument)
channelID := graph.Get[string](graph.ArgsMap(p.Args), "channelID")

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 GetE added in v1.1.8

func GetE[T any](a ArgsGetter, name string) (T, error)

GetE retrieves an argument by name with type safety and error handling. Returns an error if the key is missing or type conversion fails.

Works with both graph.Args and graph.ArgsMap (p.Args):

// With graph.Args
input, err := graph.GetE[UserInput](args, "input")

// With p.Args
channelID, err := graph.GetE[string](graph.ArgsMap(p.Args), "channelID")

func GetOr added in v1.1.7

func GetOr[T any](a ArgsGetter, name string, defaultVal T) T

GetOr retrieves an argument by name with a default value if not found.

Works with both graph.Args and graph.ArgsMap (p.Args):

// With graph.Args
limit := graph.GetOr[int](args, "limit", 10)

// With p.Args
limit := graph.GetOr[int](graph.ArgsMap(p.Args), "limit", 10)

func GetRoot added in v1.2.2

func GetRoot[T any](r RootInfoGetter, name string) T

GetRoot retrieves a value from root info by name with type safety. Returns zero value if key is missing or conversion fails. Use GetRootE for explicit error handling.

Usage:

user := graph.GetRoot[UserDetails](graph.NewRootInfo(p), "details")
token := graph.GetRoot[string](graph.NewRootInfo(p), "token")

func GetRootE added in v1.2.2

func GetRootE[T any](r RootInfoGetter, name string) (T, error)

GetRootE retrieves a value from root info by name with type safety and error handling. Returns an error if the key is missing or type conversion fails.

Usage:

user, err := graph.GetRootE[UserDetails](graph.NewRootInfo(p), "details")
if err != nil {
    return nil, fmt.Errorf("authentication required: %w", err)
}

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 GetRootOr added in v1.2.2

func GetRootOr[T any](r RootInfoGetter, name string, defaultVal T) T

GetRootOr retrieves a value from root info by name with a default value if not found.

Usage:

token := graph.GetRootOr[string](graph.NewRootInfo(p), "token", "anonymous")
userID := graph.GetRootOr[int](graph.NewRootInfo(p), "userID", 0)

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 MergeRoleConfigs added in v1.1.1

func MergeRoleConfigs(configs ...map[string][]string) map[string][]string

MergeRoleConfigs combines multiple role configurations

Example:

allRoles := MergeRoleConfigs(AdminOnlyFields, ManagerFields, AuditorFields)

func MustGet added in v1.1.8

func MustGet[T any](a ArgsGetter, name string) T

MustGet retrieves an argument by name with type safety. Panics if the key is missing or conversion fails. Use only when you're certain the argument exists and is valid.

Works with both graph.Args and graph.ArgsMap (p.Args).

func MustGetRoot added in v1.2.2

func MustGetRoot[T any](r RootInfoGetter, name string) T

MustGetRoot retrieves a value from root info by name with type safety. Panics if the key is missing or conversion fails. Use only when you're certain the value exists and is valid.

Usage:

user := graph.MustGetRoot[UserDetails](graph.NewRootInfo(p), "details")

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 automatically detects WebSocket upgrade requests and handles them appropriately when subscriptions are enabled (EnableSubscriptions: true).

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)
  • WebSocket upgrade requests are handled when EnableSubscriptions: true

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 without subscriptions:

// 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(ctx context.Context, token string) (context.Context, interface{}, error) {
        user, err := validateToken(token)
        if err != nil {
            return ctx, nil, err
        }
        // Add user ID to context for access in resolvers via p.Context.Value("userID")
        ctx = context.WithValue(ctx, "userID", user.ID)
        return ctx, user, nil
    },
})

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

Example with subscriptions:

pubsub := graph.NewInMemoryPubSub()
defer pubsub.Close()

handler := graph.NewHTTP(&graph.GraphContext{
    SchemaParams: &graph.SchemaBuilderParams{
        QueryFields:        []graph.QueryField{getUserQuery()},
        MutationFields:     []graph.MutationField{createUserMutation()},
        SubscriptionFields: []graph.SubscriptionField{userSubscription(pubsub)},
    },
    PubSub:              pubsub,
    EnableSubscriptions: true,
    DEBUG:               false,
})

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

func NewWebSocketHandler added in v1.1.0

func NewWebSocketHandler(params WebSocketParams) http.HandlerFunc

NewWebSocketHandler creates an HTTP handler for WebSocket connections. This handler upgrades HTTP connections to WebSocket and manages GraphQL subscriptions.

Example:

params := graph.WebSocketParams{
    Schema:      schema,
    PubSub:      pubsub,
    CheckOrigin: func(r *http.Request) bool {
        origin := r.Header.Get("Origin")
        return origin == "https://example.com"
    },
    AuthFn: func(r *http.Request) (interface{}, error) {
        token := ExtractBearerToken(r)
        return validateToken(token)
    },
}

http.Handle("/graphql", graph.NewWebSocketHandler(params))

func PerUserBudgetFunc added in v1.1.1

func PerUserBudgetFunc(budgets map[string]int, defaultBudget int) func(string) (int, error)

PerUserBudgetFunc creates a budget function with per-user budgets Useful for different tier users or testing

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 SimpleBudgetFunc added in v1.1.1

func SimpleBudgetFunc(budget int) func(string) (int, error)

SimpleBudgetFunc creates a simple budget function that returns a fixed budget Useful for testing or simple rate limiting scenarios

func UnmarshalSubscriptionMessage added in v1.1.0

func UnmarshalSubscriptionMessage[T any](msg *Message) (*T, error)

Helper function to unmarshal subscription messages

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 ASTVisitor added in v1.1.1

type ASTVisitor struct {
	EnterField     func(field *ast.Field, ctx *ValidationContext) error
	LeaveField     func(field *ast.Field, ctx *ValidationContext) error
	EnterOperation func(op *ast.OperationDefinition, ctx *ValidationContext) error
	LeaveOperation func(op *ast.OperationDefinition, ctx *ValidationContext) error
	EnterFragment  func(frag *ast.FragmentDefinition, ctx *ValidationContext) error
	LeaveFragment  func(frag *ast.FragmentDefinition, ctx *ValidationContext) error
}

ASTVisitor allows traversing the AST with hooks

type ActionBuilder added in v1.2.3

type ActionBuilder[T any, In any] struct {
	// contains filtered or unexported fields
}

func (*ActionBuilder[T, In]) Build added in v1.2.3

func (b *ActionBuilder[T, In]) Build() MutationField

func (*ActionBuilder[T, In]) WithResolver added in v1.2.3

func (b *ActionBuilder[T, In]) WithResolver(
	fn func(ctx context.Context, in In) (*T, error),
) *ActionBuilder[T, In]

type ArgInfo added in v1.2.4

type ArgInfo struct {
	Name         string          `json:"name"`
	Type         string          `json:"type"` // SDL-style
	Description  string          `json:"description,omitempty"`
	Required     bool            `json:"required,omitempty"`
	DefaultValue any             `json:"defaultValue,omitempty"`
	Validators   []ValidatorInfo `json:"validators,omitempty"`
}

ArgInfo describes one argument on a resolver. Required is true iff the underlying GraphQL type is NonNull. DefaultValue reflects whatever was supplied to WithArgDefault or struct-tag defaults; it's nil otherwise.

type ArgValidator added in v1.2.4

type ArgValidator func(value any) error

ArgValidator is the runtime shape of a validator. Return nil when the value is acceptable; return an error whose message is user-facing.

type Args added in v1.1.7

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

Args provides type-safe access to GraphQL arguments

func NewArgs added in v1.1.7

func NewArgs(raw map[string]interface{}) Args

NewArgs creates an Args instance from a map

func (Args) GetArg added in v1.1.9

func (a Args) GetArg(name string) (interface{}, bool)

GetArg implements ArgsGetter interface

func (Args) Has added in v1.1.7

func (a Args) Has(name string) bool

Has checks if an argument exists

func (Args) Raw added in v1.1.7

func (a Args) Raw() map[string]interface{}

Raw returns the underlying map

type ArgsGetter added in v1.1.9

type ArgsGetter interface {
	GetArg(name string) (interface{}, bool)
}

ArgsGetter is an interface for types that can provide arguments by name. This allows Get[T], GetE[T], GetOr[T], and MustGet[T] to work with both:

  • graph.Args (from WithArg chainable API)
  • graph.ArgsMap (from graphql.FieldConfigArgument / p.Args)

type ArgsMap added in v1.1.9

type ArgsMap map[string]interface{}

ArgsMap wraps map[string]interface{} to implement ArgsGetter. This allows using p.Args directly with Get[T], GetE[T], etc.

Usage:

// In subscription resolvers with graphql.FieldConfigArgument
channelID := graph.Get[string](graph.ArgsMap(p.Args), "channelID")

func (ArgsMap) GetArg added in v1.1.9

func (m ArgsMap) GetArg(name string) (interface{}, bool)

GetArg implements ArgsGetter interface

type BaseRule added in v1.1.1

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

BaseRule provides common functionality for all validation rules All custom rules should embed this struct

func NewBaseRule added in v1.1.1

func NewBaseRule(name string) BaseRule

NewBaseRule creates a new base rule with the given name

func (*BaseRule) Disable added in v1.1.1

func (r *BaseRule) Disable()

func (*BaseRule) Enable added in v1.1.1

func (r *BaseRule) Enable()

func (*BaseRule) Enabled added in v1.1.1

func (r *BaseRule) Enabled() bool

func (*BaseRule) Name added in v1.1.1

func (r *BaseRule) Name() string

func (*BaseRule) NewError added in v1.1.1

func (r *BaseRule) NewError(message string) *ValidationError

NewValidationError creates a validation error for this rule

func (*BaseRule) NewErrorf added in v1.1.1

func (r *BaseRule) NewErrorf(format string, args ...interface{}) *ValidationError

NewErrorf creates a validation error with formatted message

func (*BaseRule) SetEnabled added in v1.1.1

func (r *BaseRule) SetEnabled(enabled bool)

SetEnabled sets the enabled state

type BlockedFieldsRule added in v1.1.1

type BlockedFieldsRule struct {
	BaseRule
	// contains filtered or unexported fields
}

BlockedFieldsRule blocks specific fields from being queried

func (*BlockedFieldsRule) BlockField added in v1.1.1

func (r *BlockedFieldsRule) BlockField(field string, reason string) *BlockedFieldsRule

BlockField adds a field to the blocked list with an optional reason

func (*BlockedFieldsRule) Validate added in v1.1.1

func (r *BlockedFieldsRule) Validate(ctx *ValidationContext) error

type Connection added in v1.1.0

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

Connection represents a single WebSocket connection.

type CreateBuilder added in v1.2.3

type CreateBuilder[T any, In any] struct {
	// contains filtered or unexported fields
}

func (*CreateBuilder[T, In]) Build added in v1.2.3

func (b *CreateBuilder[T, In]) Build() MutationField

func (*CreateBuilder[T, In]) WithResolver added in v1.2.3

func (b *CreateBuilder[T, In]) WithResolver(
	fn func(ctx context.Context, in In) (*T, error),
) *CreateBuilder[T, In]

type DeleteBuilder added in v1.2.3

type DeleteBuilder[T any, In any] struct {
	// contains filtered or unexported fields
}

func (*DeleteBuilder[T, In]) Build added in v1.2.3

func (b *DeleteBuilder[T, In]) Build() MutationField

func (*DeleteBuilder[T, In]) WithResolver added in v1.2.3

func (b *DeleteBuilder[T, In]) WithResolver(
	fn func(ctx context.Context, in In) (*T, error),
) *DeleteBuilder[T, In]

type EchoInput added in v1.2.3

type EchoInput struct {
	Message string `json:"message" graphql:"message,required" description:"Message to echo back"`
}

EchoInput is the input for the default echo mutation.

type ErrorCode added in v1.2.3

type ErrorCode string
const (
	CodeInvalidInput ErrorCode = "INVALID_INPUT"
	CodeUnauthorized ErrorCode = "UNAUTHORIZED"
	CodeNotFound     ErrorCode = "NOT_FOUND"
	CodeConflict     ErrorCode = "CONFLICT"
	CodeInternal     ErrorCode = "INTERNAL"
)

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 FieldInfo added in v1.2.4

type FieldInfo struct {
	Name              string           `json:"name"`
	Description       string           `json:"description,omitempty"`
	Kind              FieldKind        `json:"kind"`
	ReturnType        string           `json:"returnType"` // SDL-style, e.g. "User!" or "[Advert]"
	List              bool             `json:"list,omitempty"`
	Paginated         bool             `json:"paginated,omitempty"`
	Deprecated        bool             `json:"deprecated,omitempty"`
	DeprecationReason string           `json:"deprecationReason,omitempty"`
	Args              []ArgInfo        `json:"args,omitempty"`
	Middlewares       []MiddlewareInfo `json:"middlewares,omitempty"`
	InputObject       *InputObjectInfo `json:"inputObject,omitempty"`
}

FieldInfo is a serializable snapshot of a resolver's declared metadata. It captures everything a tool would need to render documentation or a dashboard view of the field — without calling Serve() or parsing SDL.

func Inspect added in v1.2.4

func Inspect(f any) (FieldInfo, bool)

Inspect returns the FieldInfo for any value implementing Introspectable, or a zero FieldInfo and false otherwise. Use this when you hold a generic QueryField / MutationField interface reference rather than the concrete *UnifiedResolver — it saves the type assertion boilerplate.

type FieldKind added in v1.2.4

type FieldKind string

FieldKind distinguishes query / mutation / subscription at the metadata level.

const (
	FieldKindQuery        FieldKind = "query"
	FieldKindMutation     FieldKind = "mutation"
	FieldKindSubscription FieldKind = "subscription"
)

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

	// PubSub: PubSub system for subscriptions (optional, only needed for subscriptions)
	// Use NewInMemoryPubSub() for development or RedisPubSub for production
	PubSub PubSub

	// EnableSubscriptions: Enable WebSocket support for GraphQL subscriptions
	// Default: false (subscriptions disabled)
	// Requires PubSub to be configured
	EnableSubscriptions bool

	// WebSocketPath: Path for WebSocket endpoint (default: same as HTTP endpoint)
	// If not set, WebSocket connections will be handled on the same path as HTTP
	WebSocketPath string

	// WebSocketCheckOrigin: Custom function to check WebSocket upgrade origin
	// If not provided, all origins are allowed (only use in development!)
	WebSocketCheckOrigin func(r *http.Request) bool

	// 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)
	//
	// The function receives the request context and token, and returns:
	//   - ctx: Updated context with custom values (accessible via p.Context.Value() in resolvers)
	//   - details: User details (accessible via GetRootInfo(p, "details", &user) in resolvers)
	//   - error: Any error during user details fetching
	//
	// Example:
	//
	//	UserDetailsFn: func(ctx context.Context, token string) (context.Context, interface{}, error) {
	//	    user, err := validateJWT(token)
	//	    if err != nil {
	//	        return ctx, nil, err
	//	    }
	//	    // Add user ID to context for access in resolvers via p.Context.Value("userID")
	//	    ctx = context.WithValue(ctx, "userID", user.ID)
	//	    return ctx, user, nil
	//	}
	UserDetailsFn func(ctx context.Context, token string) (context.Context, 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
	// DEPRECATED: Use ValidationRules for more control
	EnableValidation bool

	// ValidationRules: Custom validation rules (takes precedence over EnableValidation)
	// Set to nil or empty slice to disable validation
	// Example:
	//   ValidationRules: []ValidationRule{
	//       NewMaxDepthRule(10),
	//       NewRequireAuthRule("mutation"),
	//       NewRoleRules(map[string][]string{
	//           "deleteUser": {"admin"},
	//       }),
	//   }
	ValidationRules []ValidationRule

	// ValidationOptions: Configure validation behavior (optional)
	// Default: StopOnFirstError=false, SkipInDebug=true
	ValidationOptions *ValidationOptions

	// 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(ctx context.Context, token string) (context.Context, interface{}, error) {
        user, err := validateJWT(token)
        if err != nil {
            return ctx, nil, err
        }
        ctx = context.WithValue(ctx, "userID", user.ID)
        return ctx, user, nil
    },
}

type HasIDInterface added in v1.1.1

type HasIDInterface interface {
	GetID() string
}

HasIDInterface - implement this on your user struct for rate limiting

type HasPermissionsInterface added in v1.1.1

type HasPermissionsInterface interface {
	HasPermission(permission string) bool
}

HasPermissionsInterface - implement this on your user struct for permission-based rules

type HasRolesInterface added in v1.1.1

type HasRolesInterface interface {
	HasRole(role string) bool
}

HasRolesInterface - implement this on your user struct for role-based rules

type InMemoryPubSub added in v1.1.0

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

InMemoryPubSub is a simple in-memory implementation of PubSub. It's suitable for development, testing, and single-instance deployments. For production multi-instance deployments, use RedisPubSub or similar.

func NewInMemoryPubSub added in v1.1.0

func NewInMemoryPubSub() *InMemoryPubSub

NewInMemoryPubSub creates a new in-memory PubSub implementation.

Example:

pubsub := graph.NewInMemoryPubSub()
defer pubsub.Close()

ctx := context.Background()
sub := pubsub.Subscribe(ctx, "events")

go func() {
    for msg := range sub {
        fmt.Println("Event:", string(msg.Data))
    }
}()

pubsub.Publish(ctx, "events", map[string]string{"type": "user_created"})

func (*InMemoryPubSub) Close added in v1.1.0

func (p *InMemoryPubSub) Close() error

Close shuts down the PubSub and closes all active subscriptions.

func (*InMemoryPubSub) Publish added in v1.1.0

func (p *InMemoryPubSub) Publish(ctx context.Context, topic string, data interface{}) error

Publish sends data to all subscribers of the topic. Slow subscribers are skipped to prevent blocking.

func (*InMemoryPubSub) Subscribe added in v1.1.0

func (p *InMemoryPubSub) Subscribe(ctx context.Context, topic string) <-chan *Message

Subscribe creates a subscription to a topic. The subscription is automatically cleaned up when the context is canceled.

func (*InMemoryPubSub) Unsubscribe added in v1.1.0

func (p *InMemoryPubSub) Unsubscribe(ctx context.Context, subscriptionID string) error

Unsubscribe removes a subscription by ID (not commonly used with context-based cleanup).

type InputAuthorizer added in v1.2.3

type InputAuthorizer interface {
	Authorize(ctx context.Context) error
}

InputAuthorizer runs before Validate. Failures surface as UNAUTHORIZED.

type InputNormalizer added in v1.2.3

type InputNormalizer interface{ Normalize() }

InputNormalizer lets an input struct normalize its fields (trim spaces, lowercase emails, etc.) after decoding and before validation.

type InputObjectInfo added in v1.2.4

type InputObjectInfo struct {
	TypeName string `json:"typeName"`
	Nullable bool   `json:"nullable,omitempty"`
}

InputObjectInfo describes the input struct for mutations built with WithInputObject. TypeName is the Go type name; the dashboard can fetch field-level detail via standard graphql-go introspection.

type InputValidator added in v1.2.3

type InputValidator interface {
	Validate(ctx context.Context) error
}

InputValidator runs after Normalize. A non-nil error short-circuits the resolver and is surfaced as INVALID_INPUT unless the returned error is already a *MutationError (in which case its Code is preserved).

type Introspectable added in v1.2.4

type Introspectable interface {
	FieldInfo() FieldInfo
}

Introspectable is implemented by every QueryField, MutationField, and SubscriptionField this library produces. External tooling (dashboards, documentation generators, schema linters) can iterate a schema's fields and read their metadata without parsing Go source.

Because FieldInfo is a plain struct, the output is safe to JSON-serialize, pass across goroutines, and diff between builds.

Typical consumer:

for _, qf := range schemaParams.QueryFields {
    info, ok := graph.Inspect(qf)
    if !ok { continue }
    fmt.Printf("%s(%v) -> %s [mws=%d]\n",
        info.Name, info.Args, info.ReturnType, len(info.Middlewares))
}

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 MaxAliasesRule added in v1.1.1

type MaxAliasesRule struct {
	BaseRule
	// contains filtered or unexported fields
}

MaxAliasesRule validates number of aliases

func (*MaxAliasesRule) Validate added in v1.1.1

func (r *MaxAliasesRule) Validate(ctx *ValidationContext) error

type MaxComplexityRule added in v1.1.1

type MaxComplexityRule struct {
	BaseRule
	// contains filtered or unexported fields
}

MaxComplexityRule validates query complexity

func (*MaxComplexityRule) Validate added in v1.1.1

func (r *MaxComplexityRule) Validate(ctx *ValidationContext) error

type MaxDepthRule added in v1.1.1

type MaxDepthRule struct {
	BaseRule
	// contains filtered or unexported fields
}

MaxDepthRule validates maximum query depth

func (*MaxDepthRule) Validate added in v1.1.1

func (r *MaxDepthRule) Validate(ctx *ValidationContext) error

type MaxTokensRule added in v1.1.1

type MaxTokensRule struct {
	BaseRule
	// contains filtered or unexported fields
}

MaxTokensRule limits query size by token count

func (*MaxTokensRule) Validate added in v1.1.1

func (r *MaxTokensRule) Validate(ctx *ValidationContext) error

type Message added in v1.1.0

type Message struct {
	// Topic is the channel/topic name where this message was published
	Topic string

	// Data is the JSON-encoded payload
	Data []byte
}

Message represents a published message with its topic and data payload.

type MiddlewareInfo added in v1.2.4

type MiddlewareInfo struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
}

MiddlewareInfo is the metadata recorded by WithNamedMiddleware. Middlewares added via plain WithMiddleware have Name == "anonymous".

type MultiValidationError added in v1.1.1

type MultiValidationError struct {
	Errors []error
}

MultiValidationError combines multiple validation errors

func NewMultiValidationError added in v1.1.1

func NewMultiValidationError(errors []error) *MultiValidationError

func (*MultiValidationError) Error added in v1.1.1

func (e *MultiValidationError) Error() string

type MutationBuilder added in v1.2.3

type MutationBuilder[T any, In any] struct {
	// contains filtered or unexported fields
}

MutationBuilder carries config common to every mutation kind. It deliberately exposes no WithResolver or Build — you must call Create/Update/Delete/Action/ Upsert to transition to a kind-specific builder.

func NewMutation added in v1.2.3

func NewMutation[T any, In any](name string) *MutationBuilder[T, In]

NewMutation is the single entry point for building mutations. The returned MutationBuilder cannot produce a GraphQL field on its own — you must pick a kind (Create, Update, Delete, Action, or Upsert) before calling WithResolver and Build. The kind determines the resolver signature.

Example:

graph.NewMutation[User, CreateUserInput]("createUser").
    Create().
    WithResolver(func(ctx context.Context, in CreateUserInput) (*User, error) {
        return userService.Create(ctx, in)
    }).
    Build()

func (*MutationBuilder[T, In]) Action added in v1.2.3

func (b *MutationBuilder[T, In]) Action() *ActionBuilder[T, In]

func (*MutationBuilder[T, In]) Create added in v1.2.3

func (b *MutationBuilder[T, In]) Create() *CreateBuilder[T, In]

func (*MutationBuilder[T, In]) Delete added in v1.2.3

func (b *MutationBuilder[T, In]) Delete() *DeleteBuilder[T, In]

func (*MutationBuilder[T, In]) Update added in v1.2.3

func (b *MutationBuilder[T, In]) Update() *UpdateBuilder[T, In]

func (*MutationBuilder[T, In]) Upsert added in v1.2.3

func (b *MutationBuilder[T, In]) Upsert() *UpsertBuilder[T, In]

func (*MutationBuilder[T, In]) Use added in v1.2.3

func (b *MutationBuilder[T, In]) Use(mw ...FieldMiddleware) *MutationBuilder[T, In]

func (*MutationBuilder[T, In]) WithDescription added in v1.2.3

func (b *MutationBuilder[T, In]) WithDescription(d string) *MutationBuilder[T, In]

func (*MutationBuilder[T, In]) WithInputName added in v1.2.3

func (b *MutationBuilder[T, In]) WithInputName(n string) *MutationBuilder[T, In]

type MutationError added in v1.2.3

type MutationError struct {
	Code    ErrorCode
	Field   string
	Message string
	Cause   error
}

func (*MutationError) Error added in v1.2.3

func (e *MutationError) Error() string

func (*MutationError) Extensions added in v1.2.3

func (e *MutationError) Extensions() map[string]interface{}

func (*MutationError) Unwrap added in v1.2.3

func (e *MutationError) Unwrap() error

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 NoIntrospectionRule added in v1.1.1

type NoIntrospectionRule struct {
	BaseRule
}

NoIntrospectionRule blocks introspection queries

func (*NoIntrospectionRule) Validate added in v1.1.1

func (r *NoIntrospectionRule) Validate(ctx *ValidationContext) error

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 Patch added in v1.2.3

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

Patch wraps a decoded input with the set of fields the client sent. Only Update and Upsert resolvers receive a Patch — the framework constructs it from the raw args, so "omitted" and "set to zero value" are distinguishable.

func (Patch[T]) Apply added in v1.2.3

func (p Patch[T]) Apply(dst *T)

Apply copies only the fields that were present in the request onto dst.

func (Patch[T]) Fields added in v1.2.3

func (p Patch[T]) Fields() []string

func (Patch[T]) Get added in v1.2.3

func (p Patch[T]) Get() T

func (Patch[T]) Has added in v1.2.3

func (p Patch[T]) Has(field string) bool

func (Patch[T]) Presence added in v1.2.3

func (p Patch[T]) Presence() PresenceSet

type PatchInputValidator added in v1.2.3

type PatchInputValidator interface {
	ValidatePatch(ctx context.Context, present PresenceSet) error
}

PatchInputValidator is preferred over InputValidator for Update and Upsert kinds, because it receives the set of fields the client actually sent.

type PermissionRule added in v1.1.1

type PermissionRule struct {
	BaseRule
	// contains filtered or unexported fields
}

PermissionRule validates a single field requires specific permissions

func (*PermissionRule) Validate added in v1.1.1

func (r *PermissionRule) Validate(ctx *ValidationContext) error

type PermissionRules added in v1.1.1

type PermissionRules struct {
	BaseRule
	// contains filtered or unexported fields
}

PermissionRules validates multiple fields with permission requirements

func (*PermissionRules) Validate added in v1.1.1

func (r *PermissionRules) Validate(ctx *ValidationContext) error

type PresenceSet added in v1.2.3

type PresenceSet interface {
	Has(field string) bool
	Fields() []string
}

PresenceSet reports which fields were included in the client request. It is populated from the raw args map, not from the decoded struct, so "field sent with zero value" and "field omitted" are distinguishable.

type PubSub added in v1.1.0

type PubSub interface {
	// Publish sends data to all subscribers of a topic.
	// The data will be JSON-marshaled automatically.
	//
	// Returns an error if:
	//   - JSON marshaling fails
	//   - Context is canceled
	//   - PubSub is closed
	Publish(ctx context.Context, topic string, data interface{}) error

	// Subscribe creates a new subscription to a topic.
	// Returns a channel that receives messages published to the topic.
	//
	// The subscription remains active until:
	//   - The context is canceled
	//   - Unsubscribe is called with the subscription ID
	//   - The PubSub is closed
	//
	// The returned channel will be closed when the subscription ends.
	Subscribe(ctx context.Context, topic string) <-chan *Message

	// Unsubscribe removes a subscription by its ID.
	// The subscription's message channel will be closed.
	Unsubscribe(ctx context.Context, subscriptionID string) error

	// Close shuts down the PubSub system and closes all active subscriptions.
	Close() error
}

PubSub defines the interface for publishing and subscribing to events. Implementations can use in-memory channels, Redis, Kafka, or other message brokers.

Example Usage:

pubsub := graph.NewInMemoryPubSub()
defer pubsub.Close()

// Subscribe to a topic
ctx := context.Background()
subscription := pubsub.Subscribe(ctx, "messages:channel1")

// Publish an event
pubsub.Publish(ctx, "messages:channel1", map[string]string{"text": "Hello"})

// Receive events
for msg := range subscription {
    fmt.Println("Received:", string(msg.Data))
}

type QueryASTCache added in v1.2.3

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

QueryASTCache caches parsed GraphQL ASTs keyed by the raw query string. It is safe for concurrent use. When the cache reaches its configured max size, the entire map is replaced — a coarse eviction that keeps the implementation lock-simple and predictable for steady-state traffic.

Rules walk the AST read-only; callers MUST NOT mutate cached ASTs.

func NewQueryASTCache added in v1.2.3

func NewQueryASTCache(maxEntries int) *QueryASTCache

NewQueryASTCache returns a cache bounded to maxEntries. A non-positive maxEntries is clamped to 1 to preserve cache semantics without growing unbounded.

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 RateLimitOption added in v1.1.1

type RateLimitOption func(*RateLimitRule)

RateLimitOption configures rate limiting behavior

func WithBudgetFunc added in v1.1.1

func WithBudgetFunc(fn func(userID string) (int, error)) RateLimitOption

WithBudgetFunc sets the function to get user's remaining budget

func WithBypassRoles added in v1.1.1

func WithBypassRoles(roles ...string) RateLimitOption

WithBypassRoles sets roles that bypass rate limiting (e.g., "admin", "service")

func WithCostPerUnit added in v1.1.1

func WithCostPerUnit(cost int) RateLimitOption

WithCostPerUnit sets the cost multiplier per complexity unit (default: 1)

type RateLimitRule added in v1.1.1

type RateLimitRule struct {
	BaseRule
	// contains filtered or unexported fields
}

RateLimitRule implements per-user rate limiting based on query complexity

func (*RateLimitRule) Validate added in v1.1.1

func (r *RateLimitRule) Validate(ctx *ValidationContext) error

type RequireAuthRule added in v1.1.1

type RequireAuthRule struct {
	BaseRule
	// contains filtered or unexported fields
}

RequireAuthRule requires authentication for specific operations or fields Simply checks if ctx.UserDetails != nil

func (*RequireAuthRule) Validate added in v1.1.1

func (r *RequireAuthRule) Validate(ctx *ValidationContext) error

type ResolveParams

type ResolveParams graphql.ResolveParams

type Result added in v1.2.3

type Result[T any] struct {
	Value   *T
	Created bool
}

Result is the return value for an Upsert resolver. Created distinguishes insert from update so the generated payload type can expose it to clients.

type RoleRule added in v1.1.1

type RoleRule struct {
	BaseRule
	// contains filtered or unexported fields
}

RoleRule validates a single field requires specific roles

func (*RoleRule) Validate added in v1.1.1

func (r *RoleRule) Validate(ctx *ValidationContext) error

type RoleRules added in v1.1.1

type RoleRules struct {
	BaseRule
	// contains filtered or unexported fields
}

RoleRules validates multiple fields with role requirements

func (*RoleRules) Validate added in v1.1.1

func (r *RoleRules) Validate(ctx *ValidationContext) error

type RootInfo added in v1.2.2

type RootInfo map[string]interface{}

RootInfo wraps map[string]interface{} to implement RootInfoGetter. This allows using p.Info.RootValue with type-safe generic functions.

Usage:

// Extract user details from root value
user := graph.GetRoot[UserDetails](graph.NewRootInfo(p), "details")

// With error handling
user, err := graph.GetRootE[UserDetails](graph.NewRootInfo(p), "details")

// With default value
token := graph.GetRootOr[string](graph.NewRootInfo(p), "token", "")

func NewRootInfo added in v1.2.2

func NewRootInfo(p ResolveParams) RootInfo

NewRootInfo extracts RootInfo from ResolveParams. Returns nil if RootValue is nil or not a map[string]interface{}.

Usage:

rootInfo := graph.NewRootInfo(p)
user := graph.GetRoot[UserDetails](rootInfo, "details")

func (RootInfo) GetRootValue added in v1.2.2

func (r RootInfo) GetRootValue(name string) (interface{}, bool)

GetRootValue implements RootInfoGetter interface

type RootInfoGetter added in v1.2.2

type RootInfoGetter interface {
	GetRootValue(name string) (interface{}, bool)
}

RootInfoGetter is an interface for types that can provide root value data by name. This allows GetRoot[T], GetRootE[T], GetRootOr[T], and MustGetRoot[T] to work with root value data extracted from 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"`

	// SubscriptionFields: List of subscription fields to include in the schema
	// Requires WebSocket support and PubSub configuration
	SubscriptionFields []SubscriptionField `group:"subscription_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 SubscriptionField added in v1.1.0

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

	// Name returns the subscription field name
	Name() string
}

SubscriptionField represents a GraphQL subscription field that can be added to a schema. It follows the same interface pattern as QueryField and MutationField.

Create subscription fields using NewSubscription:

sub := NewSubscription[MessageEvent]("messageAdded").
    WithArgs(...).
    WithResolver(...).
    BuildSubscription()

type SubscriptionFilterFn added in v1.1.0

type SubscriptionFilterFn[T any] func(ctx context.Context, data *T, p ResolveParams) bool

SubscriptionFilterFn filters events before sending them to clients. Return true to send the event, false to skip it.

This is useful for:

  • User-specific filtering based on permissions
  • Content-based filtering based on subscription arguments
  • Rate limiting or throttling

Example:

func(ctx context.Context, data *MessageEvent, p ResolveParams) bool {
    userID, _ := GetRootString(p, "userID")
    return canUserViewMessage(userID, data.ID)
}

type SubscriptionResolveFn added in v1.1.0

type SubscriptionResolveFn[T any] func(ctx context.Context, p ResolveParams) (<-chan *T, error)

SubscriptionResolveFn is the resolver function for subscriptions. It returns a channel that emits events of type T.

The resolver should:

  • Create a buffered channel to prevent blocking
  • Start a goroutine to handle event publishing
  • Close the channel when done or context is canceled
  • Handle errors by returning nil channel and an error

Example:

func(ctx context.Context, p ResolveParams) (<-chan *MessageEvent, error) {
    channelID, _ := GetArgString(p, "channelID")
    events := make(chan *MessageEvent, 10)

    subscription := pubsub.Subscribe(ctx, "messages:"+channelID)

    go func() {
        defer close(events)
        for msg := range subscription {
            var event MessageEvent
            if err := json.Unmarshal(msg.Data, &event); err == nil {
                events <- &event
            }
        }
    }()

    return events, nil
}

type SubscriptionResolver added in v1.1.0

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

SubscriptionResolver builds type-safe subscription fields with extensive customization capabilities. It provides a fluent API similar to UnifiedResolver for building subscriptions.

Type Parameters:

  • T: The Go struct type that will be sent to subscribers

The resolver function should return a channel that emits events of type T. When the channel is closed or the context is canceled, the subscription ends.

Basic Usage:

type MessageEvent struct {
    ID        string    `json:"id"`
    Content   string    `json:"content"`
    Timestamp time.Time `json:"timestamp"`
}

sub := NewSubscription[MessageEvent]("messageAdded").
    WithDescription("Subscribe to new messages").
    WithArgs(graphql.FieldConfigArgument{
        "channelID": &graphql.ArgumentConfig{
            Type: graphql.NewNonNull(graphql.String),
        },
    }).
    WithResolver(func(ctx context.Context, p ResolveParams) (<-chan *MessageEvent, error) {
        channelID, _ := GetArgString(p, "channelID")
        events := make(chan *MessageEvent)
        subscription := pubsub.Subscribe(ctx, "messages:"+channelID)

        go func() {
            defer close(events)
            for msg := range subscription {
                var event MessageEvent
                json.Unmarshal(msg.Data, &event)
                events <- &event
            }
        }()

        return events, nil
    }).
    WithFilter(func(ctx context.Context, data *MessageEvent, p ResolveParams) bool {
        // Optional: filter events before sending to client
        return true
    }).
    WithMiddleware(AuthMiddleware("user")).
    BuildSubscription()

Advanced Features:

// Middleware support
sub := NewSubscription[Event]("events").
    WithMiddleware(LoggingMiddleware).
    WithMiddleware(AuthMiddleware("admin")).
    // ... rest of configuration

// Field-level customization (like UnifiedResolver)
sub := NewSubscription[ComplexEvent]("complexEvent").
    WithFieldResolver("computedField", func(p ResolveParams) (interface{}, error) {
        event := p.Source.(ComplexEvent)
        return computeValue(event), nil
    }).
    // ... rest of configuration

func NewSubscription added in v1.1.0

func NewSubscription[T any](name string) *SubscriptionResolver[T]

NewSubscription creates a new subscription resolver with the specified name. The type parameter T determines the event type that will be sent to subscribers.

Example:

type UserStatusEvent struct {
    UserID string `json:"userID"`
    Status string `json:"status"`
    Timestamp time.Time `json:"timestamp"`
}

sub := NewSubscription[UserStatusEvent]("userStatusChanged").
    WithArgs(graphql.FieldConfigArgument{
        "userID": &graphql.ArgumentConfig{Type: graphql.String},
    }).
    WithResolver(func(ctx context.Context, p ResolveParams) (<-chan *UserStatusEvent, error) {
        // Implementation
    }).
    BuildSubscription()

func (*SubscriptionResolver[T]) BuildSubscription added in v1.1.0

func (s *SubscriptionResolver[T]) BuildSubscription() SubscriptionField

BuildSubscription builds and returns a SubscriptionField that can be added to the schema.

This method:

  • Auto-generates the GraphQL type from the Go struct T
  • Applies field-level customizations and middleware
  • Creates the subscription and resolve functions
  • Registers the type in the global type registry

Example:

sub := NewSubscription[MessageEvent]("messageAdded").
    WithArgs(...).
    WithResolver(...).
    BuildSubscription()

// Add to schema
schema := graph.NewSchemaBuilder(graph.SchemaBuilderParams{
    SubscriptionFields: []graph.SubscriptionField{sub},
})

func (*SubscriptionResolver[T]) WithArgs added in v1.1.0

WithArgs sets custom arguments for the subscription.

Example:

WithArgs(graphql.FieldConfigArgument{
    "channelID": &graphql.ArgumentConfig{
        Type:        graphql.NewNonNull(graphql.String),
        Description: "Channel to subscribe to",
    },
    "filter": &graphql.ArgumentConfig{
        Type:        graphql.String,
        Description: "Optional filter pattern",
    },
})

func (*SubscriptionResolver[T]) WithDescription added in v1.1.0

func (s *SubscriptionResolver[T]) WithDescription(desc string) *SubscriptionResolver[T]

WithDescription adds a description to the subscription field.

func (*SubscriptionResolver[T]) WithFieldMiddleware added in v1.1.0

func (s *SubscriptionResolver[T]) WithFieldMiddleware(fieldName string, middleware FieldMiddleware) *SubscriptionResolver[T]

WithFieldMiddleware adds middleware to a specific field in the event type.

Example:

WithFieldMiddleware("sensitiveData", AuthMiddleware("admin"))

func (*SubscriptionResolver[T]) WithFieldResolver added in v1.1.0

func (s *SubscriptionResolver[T]) WithFieldResolver(fieldName string, resolver graphql.FieldResolveFn) *SubscriptionResolver[T]

WithFieldResolver overrides the resolver for a specific field in the event type. This allows customizing how specific fields are resolved.

Example:

WithFieldResolver("author", func(p ResolveParams) (interface{}, error) {
    event := p.Source.(MessageEvent)
    return userService.GetByID(event.AuthorID), nil
})

func (*SubscriptionResolver[T]) WithFilter added in v1.1.0

WithFilter adds a filter function to filter events before sending to clients. Only events that pass the filter (return true) will be sent.

Example:

WithFilter(func(ctx context.Context, data *MessageEvent, p ResolveParams) bool {
    userID, _ := GetRootString(p, "userID")
    return data.AuthorID != userID // Don't send user's own messages
})

func (*SubscriptionResolver[T]) WithMiddleware added in v1.1.0

func (s *SubscriptionResolver[T]) WithMiddleware(middleware FieldMiddleware) *SubscriptionResolver[T]

WithMiddleware adds middleware to the subscription resolver. Middleware is executed in the order it's added.

Example:

WithMiddleware(LoggingMiddleware).
WithMiddleware(AuthMiddleware("user"))

func (*SubscriptionResolver[T]) WithResolver added in v1.1.0

func (s *SubscriptionResolver[T]) WithResolver(fn SubscriptionResolveFn[T]) *SubscriptionResolver[T]

WithResolver sets the subscription resolver function. The resolver should return a channel that emits events of type T.

Example:

WithResolver(func(ctx context.Context, p ResolveParams) (<-chan *MessageEvent, error) {
    channelID, _ := GetArgString(p, "channelID")

    events := make(chan *MessageEvent, 10)
    subscription := pubsub.Subscribe(ctx, "messages:"+channelID)

    go func() {
        defer close(events)
        for msg := range subscription {
            var event MessageEvent
            if err := json.Unmarshal(msg.Data, &event); err == nil {
                events <- &event
            }
        }
    }()

    return events, nil
})

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]) FieldInfo added in v1.2.4

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

FieldInfo returns a serializable snapshot of this resolver's metadata. Calling this is cheap — it reads already-stored fields and runs Serve() once for the return type. Safe to call multiple times.

func (*UnifiedResolver[T]) GetArgsDefinition added in v1.2.4

func (r *UnifiedResolver[T]) GetArgsDefinition() graphql.FieldConfigArgument

GetArgsDefinition returns the underlying graphql.FieldConfigArgument. Useful for tools that want the raw graphql-go types (e.g. to fabricate their own introspection query programmatically).

func (*UnifiedResolver[T]) GetMiddlewareCount added in v1.2.4

func (r *UnifiedResolver[T]) GetMiddlewareCount() int

GetMiddlewareCount returns the number of middlewares on the main resolver. For richer information use FieldInfo().Middlewares.

func (*UnifiedResolver[T]) GetMiddlewareInfos added in v1.2.4

func (r *UnifiedResolver[T]) GetMiddlewareInfos() []MiddlewareInfo

GetMiddlewareInfos returns the MiddlewareInfo slice in application order. Entries for middlewares added via plain WithMiddleware have Name == "anonymous".

func (*UnifiedResolver[T]) IsDeprecated added in v1.2.4

func (r *UnifiedResolver[T]) IsDeprecated() bool

IsDeprecated reports whether WithDeprecated was applied.

func (*UnifiedResolver[T]) IsList added in v1.2.4

func (r *UnifiedResolver[T]) IsList() bool

IsList reports whether AsList was used (or the return type is a slice).

func (*UnifiedResolver[T]) IsMutation added in v1.2.4

func (r *UnifiedResolver[T]) IsMutation() bool

IsMutation reports whether BuildMutation or AsMutation was used.

func (*UnifiedResolver[T]) IsPaginated added in v1.2.4

func (r *UnifiedResolver[T]) IsPaginated() bool

IsPaginated reports whether AsPaginated was used.

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]) WithArg added in v1.1.7

func (r *UnifiedResolver[T]) WithArg(name string, argType interface{}) *UnifiedResolver[T]

WithArg adds an argument to the resolver. Supports: - Go primitive types: string, int, int64, float64, bool (pass zero value or instance) - GraphQL types: graph.String, graph.Int, graph.Float, graph.Boolean, graph.ID - Struct types: any struct will be converted to GraphQL InputObject (supports deeply nested structs) - Slices: []Type will be converted to [Type] GraphQL list

Usage:

// With Go primitive types (recommended)
NewResolver[User]("user").
	WithArg("id", "").           // string
	WithArg("limit", 0).         // int
	WithArg("active", false).    // bool
	WithResolver(func(p ResolveParams) (*User, error) {
		id := Get[string](ArgsMap(p.Args), "id")
		return userService.GetByID(id)
	})

// With struct type (deeply nested supported)
type AddressInput struct {
	Street  string `json:"street"`
	City    string `json:"city"`
}
type UserInput struct {
	Name    string       `json:"name"`
	Address AddressInput `json:"address"`
}

NewResolver[User]("createUser").
	WithArg("input", UserInput{}).
	WithResolver(func(p ResolveParams) (*User, error) {
		input := Get[UserInput](ArgsMap(p.Args), "input")
		return userService.Create(input)
	})

func (*UnifiedResolver[T]) WithArgDefault added in v1.1.7

func (r *UnifiedResolver[T]) WithArgDefault(name string, argType interface{}, defaultValue interface{}) *UnifiedResolver[T]

WithArgDefault adds an argument with a default value

func (*UnifiedResolver[T]) WithArgRequired added in v1.1.7

func (r *UnifiedResolver[T]) WithArgRequired(name string, argType interface{}) *UnifiedResolver[T]

WithArgRequired adds a required argument to the resolver

func (*UnifiedResolver[T]) WithArgValidator added in v1.2.4

func (r *UnifiedResolver[T]) WithArgValidator(argName string, validators ...Validator) *UnifiedResolver[T]

WithArgValidator attaches one or more validators to a named argument. Validators run after graphql-go parses input but before the resolver fires; the first failing validator aborts with its error. Metadata surfaces on FieldInfo.Args[i].Validators for dashboards.

Use the built-in constructors (Required, StringLength, StringMatch, OneOf, IntRange, Custom) rather than populating Validator by hand.

r.WithArgValidator("title", graph.Required(), graph.StringLength(1, 100))
r.WithArgValidator("email", graph.StringMatch(`^\S+@\S+$`, "must be an email"))

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]) WithDeprecated added in v1.2.4

func (r *UnifiedResolver[T]) WithDeprecated(reason string) *UnifiedResolver[T]

WithDeprecated marks this field deprecated. The reason propagates to the generated *graphql.Field and to FieldInfo.DeprecationReason so both GraphQL clients (via introspection) and tools see it.

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]) WithNamedMiddleware added in v1.2.4

func (r *UnifiedResolver[T]) WithNamedMiddleware(name, description string, middleware FieldMiddleware) *UnifiedResolver[T]

WithNamedMiddleware adds a middleware with a display name and description. Prefer this over WithMiddleware when the middleware's identity matters to tools that render the resolver — dashboards, documentation generators, or a lint step that enforces "every mutation must have an 'auth' middleware".

The name shows up verbatim in FieldInfo.Middlewares[i].Name, so keep it kebab-case and stable ("auth", "rate-limit", "permission:admin").

r.WithNamedMiddleware("auth", "Bearer token validation", AuthMiddleware)
r.WithNamedMiddleware("permission:admin", "Admin role required",
    PermissionMiddleware([]string{"admin"}))

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(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.

WithResolver requires the function signature:

  • func(p ResolveParams) (*T, error)

Access arguments using type-safe getter functions with ArgsMap:

  • Get[T](ArgsMap(p.Args), "key") - returns zero value if not found
  • GetE[T](ArgsMap(p.Args), "key") - returns error if not found
  • GetOr[T](ArgsMap(p.Args), "key", defaultVal) - returns default if not found
  • MustGet[T](ArgsMap(p.Args), "key") - panics if not found

Example usage:

NewResolver[User]("user").
	WithArg("id", graph.String).
	WithResolver(func(p graph.ResolveParams) (*User, error) {
		id := graph.Get[string](graph.ArgsMap(p.Args), "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()

type UpdateBuilder added in v1.2.3

type UpdateBuilder[T any, In any] struct {
	// contains filtered or unexported fields
}

func (*UpdateBuilder[T, In]) Build added in v1.2.3

func (b *UpdateBuilder[T, In]) Build() MutationField

func (*UpdateBuilder[T, In]) WithResolver added in v1.2.3

func (b *UpdateBuilder[T, In]) WithResolver(
	fn func(ctx context.Context, p Patch[In]) (*T, error),
) *UpdateBuilder[T, In]

type UpsertBuilder added in v1.2.3

type UpsertBuilder[T any, In any] struct {
	// contains filtered or unexported fields
}

func (*UpsertBuilder[T, In]) Build added in v1.2.3

func (b *UpsertBuilder[T, In]) Build() MutationField

func (*UpsertBuilder[T, In]) WithResolver added in v1.2.3

func (b *UpsertBuilder[T, In]) WithResolver(
	fn func(ctx context.Context, p Patch[In]) (Result[T], error),
) *UpsertBuilder[T, In]

type ValidationContext added in v1.1.1

type ValidationContext struct {
	// GraphQL query components
	Query     string
	Document  *ast.Document
	Schema    *graphql.Schema
	Variables map[string]interface{}

	// Request context
	Request *http.Request

	// User details from UserDetailsFn (can be nil if not authenticated)
	// Validation rules can type-assert this to whatever structure they need
	UserDetails interface{}
}

ValidationContext provides all necessary information for validation

type ValidationError added in v1.1.1

type ValidationError struct {
	Rule     string
	Message  string
	Location *ast.Location
	Path     []string
}

ValidationError provides detailed error information

func (*ValidationError) Error added in v1.1.1

func (e *ValidationError) Error() string

type ValidationOptions added in v1.1.1

type ValidationOptions struct {
	// StopOnFirstError stops validation after first error
	StopOnFirstError bool

	// SkipInDebug skips validation when DEBUG=true
	SkipInDebug bool

	// QueryCache, when non-nil, caches parsed ASTs keyed by query string so
	// repeated requests with the same query skip the parser.
	QueryCache *QueryASTCache
}

ValidationOptions configures validation behavior

type ValidationRule added in v1.1.1

type ValidationRule interface {
	// Name returns a unique identifier for this rule
	Name() string

	// Validate executes the rule against the parsed query
	// Returns nil if valid, error if validation fails
	Validate(ctx *ValidationContext) error

	// Enabled checks if this rule should be executed
	Enabled() bool

	// Enable enables the rule
	Enable()

	// Disable disables the rule
	Disable()
}

ValidationRule represents a single validation rule that can be applied to GraphQL queries

func CombineRules added in v1.1.1

func CombineRules(ruleSets ...[]ValidationRule) []ValidationRule

CombineRules combines multiple rule sets into one

Example:

rules := CombineRules(
    SecurityRules,
    []ValidationRule{NewRequireAuthRule("mutation")},
)

func DefaultValidationRules added in v1.1.1

func DefaultValidationRules() []ValidationRule

DefaultValidationRules returns the default validation rules Equivalent to EnableValidation=true

func DevelopmentValidationRules added in v1.1.1

func DevelopmentValidationRules() []ValidationRule

DevelopmentValidationRules returns lenient development rules

func NewBlockedFieldsRule added in v1.1.1

func NewBlockedFieldsRule(fields ...string) ValidationRule

NewBlockedFieldsRule creates a new blocked fields rule

Example:

NewBlockedFieldsRule("internalUsers", "deprecatedField")

func NewMaxAliasesRule added in v1.1.1

func NewMaxAliasesRule(maxAliases int) ValidationRule

NewMaxAliasesRule creates a new max aliases validation rule

func NewMaxComplexityRule added in v1.1.1

func NewMaxComplexityRule(maxComplexity int) ValidationRule

NewMaxComplexityRule creates a new max complexity validation rule

func NewMaxDepthRule added in v1.1.1

func NewMaxDepthRule(maxDepth int) ValidationRule

NewMaxDepthRule creates a new max depth validation rule

func NewMaxTokensRule added in v1.1.1

func NewMaxTokensRule(maxTokens int) ValidationRule

NewMaxTokensRule creates a new max tokens validation rule

func NewNoIntrospectionRule added in v1.1.1

func NewNoIntrospectionRule() ValidationRule

NewNoIntrospectionRule creates a new no introspection validation rule

func NewPermissionRule added in v1.1.1

func NewPermissionRule(field string, permissions ...string) ValidationRule

NewPermissionRule creates a new permission validation rule for a single field

Example:

NewPermissionRule("sensitiveData", "read:sensitive")
NewPermissionRule("exportData", "export:data", "admin:all")

func NewPermissionRules added in v1.1.1

func NewPermissionRules(config map[string][]string) ValidationRule

NewPermissionRules creates a batch permission validation rule config maps field names to required permissions

Example:

NewPermissionRules(map[string][]string{
    "sensitiveData": {"read:sensitive"},
    "exportData":    {"export:data"},
    "adminPanel":    {"admin:access"},
})

func NewRateLimitRule added in v1.1.1

func NewRateLimitRule(opts ...RateLimitOption) ValidationRule

NewRateLimitRule creates a new rate limiting rule with optional configuration

Example:

NewRateLimitRule(
    WithBudgetFunc(getBudgetFromRedis),
    WithCostPerUnit(2),
    WithBypassRoles("admin", "service"),
)

func NewRequireAuthRule added in v1.1.1

func NewRequireAuthRule(targets ...string) ValidationRule

NewRequireAuthRule creates a new require authentication rule Targets can be operation types ("mutation", "subscription", "query") or field names

Example:

NewRequireAuthRule("mutation", "subscription")  // Require auth for all mutations and subscriptions
NewRequireAuthRule("deleteUser", "updateUser")  // Require auth for specific fields

func NewRoleRule added in v1.1.1

func NewRoleRule(field string, roles ...string) ValidationRule

NewRoleRule creates a new role validation rule for a single field

Example:

NewRoleRule("deleteUser", "admin")
NewRoleRule("viewAuditLog", "admin", "auditor")

func NewRoleRules added in v1.1.1

func NewRoleRules(config map[string][]string) ValidationRule

NewRoleRules creates a batch role validation rule config maps field names to required roles

Example:

NewRoleRules(map[string][]string{
    "deleteUser":    {"admin"},
    "viewAuditLog":  {"admin", "auditor"},
    "approveOrder":  {"admin", "manager"},
})

func ProductionValidationRules added in v1.1.1

func ProductionValidationRules() []ValidationRule

ProductionValidationRules returns recommended production rules

type Validator added in v1.2.4

type Validator struct {
	Info ValidatorInfo
	Fn   ArgValidator
}

Validator pairs a runtime function with its metadata. Use the helpers in validators.go (Required, StringLength, StringMatch, ...) to construct these rather than populating by hand.

func Custom added in v1.2.4

func Custom(name, message string, fn ArgValidator) Validator

Custom wraps an arbitrary validator function with a displayable name. The kind is always "custom"; use the message/details fields of ValidatorInfo for anything you want the dashboard to show.

r.WithArgValidator("slug", graph.Custom("kebab-case", "lowercase letters and hyphens only",
    func(v any) error { ... }))

func IntRange added in v1.2.4

func IntRange(min, max int) Validator

IntRange enforces min <= value <= max for Int args. Use math.MinInt / math.MaxInt to skip a bound.

func OneOf added in v1.2.4

func OneOf(allowed ...any) Validator

OneOf restricts the value to the given allowed set. Works for strings and ints (most GraphQL scalars that survive JSON decoding).

func Required added in v1.2.4

func Required() Validator

Required rejects nil / missing values. Useful for "Int" args where the GraphQL type is nullable but the business rule isn't.

func StringLength added in v1.2.4

func StringLength(min, max int) Validator

StringLength enforces min <= len(value) <= max for string args. Use -1 for min or max to skip that side of the check.

func StringMatch added in v1.2.4

func StringMatch(pattern string, message ...string) Validator

StringMatch returns a validator that accepts the string arg iff it matches the regex. The optional message replaces the default "invalid format" text. Panics at build time if the regex is invalid — that's a programmer bug.

type ValidatorInfo added in v1.2.4

type ValidatorInfo struct {
	Kind    string `json:"kind"`
	Message string `json:"message,omitempty"`
	Details any    `json:"details,omitempty"`
}

ValidatorInfo describes a per-argument validator for dashboard display. The Kind field identifies the validator family (so a UI can render a matching icon or tooltip); Details holds the parameters (e.g. {"min":1,"max":100} for StringLength).

type WSMessage added in v1.1.0

type WSMessage struct {
	ID      string                 `json:"id,omitempty"`
	Type    string                 `json:"type"`
	Payload map[string]interface{} `json:"payload,omitempty"`
}

WSMessage represents a GraphQL WebSocket Protocol message. This follows the graphql-ws protocol specification.

type WebSocketManager added in v1.1.0

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

WebSocketManager manages WebSocket connections for GraphQL subscriptions. It handles connection lifecycle, authentication, and message routing.

func (*WebSocketManager) CloseAllConnections added in v1.1.0

func (m *WebSocketManager) CloseAllConnections()

CloseAllConnections closes all active WebSocket connections. This is useful for graceful shutdown.

func (*WebSocketManager) HandleWebSocket added in v1.1.0

func (m *WebSocketManager) HandleWebSocket(w http.ResponseWriter, r *http.Request)

HandleWebSocket upgrades HTTP connections to WebSocket and manages the connection lifecycle.

type WebSocketParams added in v1.1.0

type WebSocketParams struct {
	// Schema: The GraphQL schema with subscription fields
	Schema *graphql.Schema

	// PubSub: The PubSub system for event distribution
	PubSub PubSub

	// CheckOrigin: Function to check WebSocket upgrade origin
	// If nil, all origins are allowed (development only!)
	CheckOrigin func(r *http.Request) bool

	// AuthFn: Authentication function to extract user details from request
	// Called during connection_init phase
	AuthFn func(r *http.Request) (interface{}, error)

	// RootObjectFn: Custom function to set up root object for each connection
	// Similar to HTTP handler's RootObjectFn
	RootObjectFn func(ctx context.Context, r *http.Request) map[string]interface{}

	// PingInterval: Interval for sending ping messages (default: 30 seconds)
	// Set to 0 to disable automatic pinging
	PingInterval time.Duration

	// ConnectionTimeout: Timeout for connection_init message (default: 10 seconds)
	ConnectionTimeout time.Duration
}

WebSocketParams configures the WebSocket handler for subscriptions.

Directories

Path Synopsis
subscription command

Jump to

Keyboard shortcuts

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