graph

package
v0.7.2 Latest Latest
Warning

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

Go to latest
Published: Apr 24, 2026 License: MIT Imports: 20 Imported by: 0

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/nexus/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)

This package is absorbed from github.com/paulmanoni/go-graph (the standalone module continues to exist; nexus carries its own copy so the nexus.AsQuery/AsMutation reflective constructors can bind directly).

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

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

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

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

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

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

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

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

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

MergeRoleConfigs combines multiple role configurations

Example:

allRoles := MergeRoleConfigs(AdminOnlyFields, ManagerFields, AuditorFields)

func MustGet

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

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

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

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

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

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

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

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

func (*ActionBuilder[T, In]) Build

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

func (*ActionBuilder[T, In]) WithResolver

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

type ArgInfo

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

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

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

Args provides type-safe access to GraphQL arguments

func NewArgs

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

NewArgs creates an Args instance from a map

func (Args) GetArg

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

GetArg implements ArgsGetter interface

func (Args) Has

func (a Args) Has(name string) bool

Has checks if an argument exists

func (Args) Raw

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

Raw returns the underlying map

type ArgsGetter

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

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

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

GetArg implements ArgsGetter interface

type BaseRule

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

func NewBaseRule(name string) BaseRule

NewBaseRule creates a new base rule with the given name

func (*BaseRule) Disable

func (r *BaseRule) Disable()

func (*BaseRule) Enable

func (r *BaseRule) Enable()

func (*BaseRule) Enabled

func (r *BaseRule) Enabled() bool

func (*BaseRule) Name

func (r *BaseRule) Name() string

func (*BaseRule) NewError

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

NewValidationError creates a validation error for this rule

func (*BaseRule) NewErrorf

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

NewErrorf creates a validation error with formatted message

func (*BaseRule) SetEnabled

func (r *BaseRule) SetEnabled(enabled bool)

SetEnabled sets the enabled state

type BlockedFieldsRule

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

BlockedFieldsRule blocks specific fields from being queried

func (*BlockedFieldsRule) BlockField

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

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

func (*BlockedFieldsRule) Validate

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

type Connection

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

Connection represents a single WebSocket connection.

type CreateBuilder

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

func (*CreateBuilder[T, In]) Build

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

func (*CreateBuilder[T, In]) WithResolver

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

type DeleteBuilder

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

func (*DeleteBuilder[T, In]) Build

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

func (*DeleteBuilder[T, In]) WithResolver

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

type EchoInput

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

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

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

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

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

type HasIDInterface interface {
	GetID() string
}

HasIDInterface - implement this on your user struct for rate limiting

type HasPermissionsInterface

type HasPermissionsInterface interface {
	HasPermission(permission string) bool
}

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

type HasRolesInterface

type HasRolesInterface interface {
	HasRole(role string) bool
}

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

type InMemoryPubSub

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

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

func (p *InMemoryPubSub) Close() error

Close shuts down the PubSub and closes all active subscriptions.

func (*InMemoryPubSub) Publish

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

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

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

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

InputAuthorizer runs before Validate. Failures surface as UNAUTHORIZED.

type InputNormalizer

type InputNormalizer interface{ Normalize() }

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

type InputObjectInfo

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

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

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

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

MaxAliasesRule validates number of aliases

func (*MaxAliasesRule) Validate

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

type MaxComplexityRule

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

MaxComplexityRule validates query complexity

func (*MaxComplexityRule) Validate

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

type MaxDepthRule

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

MaxDepthRule validates maximum query depth

func (*MaxDepthRule) Validate

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

type MaxTokensRule

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

MaxTokensRule limits query size by token count

func (*MaxTokensRule) Validate

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

type Message

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

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

type MultiValidationError struct {
	Errors []error
}

MultiValidationError combines multiple validation errors

func NewMultiValidationError

func NewMultiValidationError(errors []error) *MultiValidationError

func (*MultiValidationError) Error

func (e *MultiValidationError) Error() string

type MutationBuilder

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

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

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

func (*MutationBuilder[T, In]) Create

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

func (*MutationBuilder[T, In]) Delete

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

func (*MutationBuilder[T, In]) Update

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

func (*MutationBuilder[T, In]) Upsert

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

func (*MutationBuilder[T, In]) Use

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

func (*MutationBuilder[T, In]) WithDescription

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

func (*MutationBuilder[T, In]) WithInputName

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

type MutationError

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

func (*MutationError) Error

func (e *MutationError) Error() string

func (*MutationError) Extensions

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

func (*MutationError) Unwrap

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

type NoIntrospectionRule struct {
	BaseRule
}

NoIntrospectionRule blocks introspection queries

func (*NoIntrospectionRule) Validate

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

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

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

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

func (Patch[T]) Fields

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

func (Patch[T]) Get

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

func (Patch[T]) Has

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

func (Patch[T]) Presence

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

type PatchInputValidator

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

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

PermissionRule validates a single field requires specific permissions

func (*PermissionRule) Validate

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

type PermissionRules

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

PermissionRules validates multiple fields with permission requirements

func (*PermissionRules) Validate

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

type PresenceSet

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

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

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

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

type RateLimitOption func(*RateLimitRule)

RateLimitOption configures rate limiting behavior

func WithBudgetFunc

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

WithBudgetFunc sets the function to get user's remaining budget

func WithBypassRoles

func WithBypassRoles(roles ...string) RateLimitOption

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

func WithCostPerUnit

func WithCostPerUnit(cost int) RateLimitOption

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

type RateLimitRule

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

RateLimitRule implements per-user rate limiting based on query complexity

func (*RateLimitRule) Validate

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

type RequireAuthRule

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

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

type ResolveParams

type ResolveParams graphql.ResolveParams

type Result

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

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

RoleRule validates a single field requires specific roles

func (*RoleRule) Validate

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

type RoleRules

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

RoleRules validates multiple fields with role requirements

func (*RoleRules) Validate

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

type RootInfo

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

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

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

GetRootValue implements RootInfoGetter interface

type RootInfoGetter

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

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

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

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

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

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

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

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

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

WithDescription adds a description to the subscription field.

func (*SubscriptionResolver[T]) WithFieldMiddleware

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

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

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

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

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 NewResolverFromType

func NewResolverFromType(name string, returnType reflect.Type) *UnifiedResolver[any]

NewResolverFromType is the non-generic sibling of NewResolver[T]. Use it when the return type is only known at runtime — typically driven by reflection on a handler function signature. The resulting resolver behaves identically to NewResolver[T]: same chainable API, same Serve path, same FieldInfo introspection.

The framework-facing use is nexus.AsQuery / AsMutation, which extract the return type from the handler function and call this to avoid forcing the caller to repeat `[T]` at the registration site.

returnType should be the concrete Go type the resolver returns. Pointer return types (*T) are dereferenced automatically. Slice return types ([]T) are detected and flagged isList.

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

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

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

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

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

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

IsDeprecated reports whether WithDeprecated was applied.

func (*UnifiedResolver[T]) IsList

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

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

func (*UnifiedResolver[T]) IsMutation

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

IsMutation reports whether BuildMutation or AsMutation was used.

func (*UnifiedResolver[T]) IsPaginated

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

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

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

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

WithArgRequired adds a required argument to the resolver

func (*UnifiedResolver[T]) WithArgValidator

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

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

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]) WithRawResolver

func (r *UnifiedResolver[T]) WithRawResolver(resolver func(ResolveParams) (any, error)) *UnifiedResolver[T]

WithRawResolver sets the resolver directly without the typed (*T, error) wrapper. Used by nexus.AsQuery / AsMutation when the handler is invoked via reflection and results are already `any`-typed.

Prefer WithResolver when you have a statically-typed handler — you lose nothing and the signature documents the return shape.

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

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

func (*UpdateBuilder[T, In]) Build

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

func (*UpdateBuilder[T, In]) WithResolver

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

type UpsertBuilder

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

func (*UpsertBuilder[T, In]) Build

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

func (*UpsertBuilder[T, In]) WithResolver

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

type ValidationContext

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

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

ValidationError provides detailed error information

func (*ValidationError) Error

func (e *ValidationError) Error() string

type ValidationOptions

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

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

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

CombineRules combines multiple rule sets into one

Example:

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

func DefaultValidationRules

func DefaultValidationRules() []ValidationRule

DefaultValidationRules returns the default validation rules Equivalent to EnableValidation=true

func DevelopmentValidationRules

func DevelopmentValidationRules() []ValidationRule

DevelopmentValidationRules returns lenient development rules

func NewBlockedFieldsRule

func NewBlockedFieldsRule(fields ...string) ValidationRule

NewBlockedFieldsRule creates a new blocked fields rule

Example:

NewBlockedFieldsRule("internalUsers", "deprecatedField")

func NewMaxAliasesRule

func NewMaxAliasesRule(maxAliases int) ValidationRule

NewMaxAliasesRule creates a new max aliases validation rule

func NewMaxComplexityRule

func NewMaxComplexityRule(maxComplexity int) ValidationRule

NewMaxComplexityRule creates a new max complexity validation rule

func NewMaxDepthRule

func NewMaxDepthRule(maxDepth int) ValidationRule

NewMaxDepthRule creates a new max depth validation rule

func NewMaxTokensRule

func NewMaxTokensRule(maxTokens int) ValidationRule

NewMaxTokensRule creates a new max tokens validation rule

func NewNoIntrospectionRule

func NewNoIntrospectionRule() ValidationRule

NewNoIntrospectionRule creates a new no introspection validation rule

func NewPermissionRule

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

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

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

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

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

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

func ProductionValidationRules() []ValidationRule

ProductionValidationRules returns recommended production rules

type Validator

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

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

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

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

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

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

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

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

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

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

func (m *WebSocketManager) CloseAllConnections()

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

func (*WebSocketManager) HandleWebSocket

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

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

type WebSocketParams

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.

Jump to

Keyboard shortcuts

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