Documentation
¶
Overview ¶
Package gqlx is a self-contained GraphQL engine used by apic-generated services. It implements the full request pipeline in the standard library: a lexer and parser producing an AST, a schema model with scalar/object/field descriptors and resolver functions, query validation, and an executor that runs queries and mutations. An http.Handler serves GraphQL over GET and POST (including JSON batch requests), and subscriptions are delivered over a RFC 6455 WebSocket transport whose framing layer is inlined here (subscription.go) and is independent of pkg/wsx. Security limits are first-class and fail-closed: bounded query size, depth, complexity, batch size, and alias counts; introspection (__schema/__type) is denied by default; and parsed, validated documents are cached in a bounded LRU sitting behind the size gate.
Index ¶
- Constants
- Variables
- func ContextWithRequest(ctx context.Context, r *http.Request) context.Context
- func RequestFromContext(ctx context.Context) (*http.Request, bool)
- func Validate(doc *Document, schema *Schema, opts ValidateOptions) error
- type ArgDef
- type Argument
- type AuthFunc
- type BoolValue
- type Directive
- type Document
- type EnumType
- type EnumValue
- type EnumValueDef
- type ExecuteOptions
- type Field
- type FieldDef
- type FloatValue
- type FragmentDef
- type FragmentSpread
- type GQLError
- type GQLType
- type Handler
- type HandlerOptions
- type InlineFragment
- type InputFieldDef
- type InputObjectType
- type IntValue
- type ListType
- type ListValue
- type Location
- type NonNullType
- type NullValue
- type ObjectField
- type ObjectType
- type ObjectValue
- type Operation
- type OperationType
- type ParseOptions
- type ResolverFunc
- type Response
- type ScalarType
- type Schema
- type Selection
- type StringValue
- type SubscribeFunc
- type SubscriptionHandler
- type SubscriptionOptions
- type Token
- type TokenKind
- type TypeRef
- type ValidateOptions
- type Value
- type VariableDef
- type VariableValue
Constants ¶
const DefaultMaxResolvedFields = 10000
DefaultMaxResolvedFields is the resolved-field budget applied when ExecuteOptions.MaxResolvedFields is zero. Generous: a legitimate query under the default validation limits (depth 10, complexity 100) resolves orders of magnitude fewer fields. PERF-0075.
Variables ¶
var ( ErrDepthExceeded = errors.New("gqlx: query depth exceeds configured limit") ErrComplexityExceeded = errors.New("gqlx: query complexity exceeds configured limit") ErrAliasLimitExceeded = errors.New("gqlx: alias count exceeds configured limit") ErrIntrospection = errors.New("gqlx: introspection disabled") ErrBatchLimitExceeded = errors.New("gqlx: batch size exceeds configured limit") ErrResolverTimeout = errors.New("gqlx: resolver execution timed out") ErrFragmentCycle = errors.New("gqlx: fragment cycle detected") ErrQueryTooLarge = errors.New("gqlx: query body exceeds configured size") ErrAuthRequired = errors.New("gqlx: authentication required") ErrRateLimited = errors.New("gqlx: rate limit exceeded") ErrInvalidConfig = errors.New("gqlx: invalid configuration") ErrFieldNotFound = errors.New("gqlx: field not found in schema") ErrInvalidArgument = errors.New("gqlx: invalid argument") ErrVariableNotDefined = errors.New("gqlx: variable referenced but not defined") ErrVariableUnused = errors.New("gqlx: variable defined but not used") // ErrResolvedFieldLimit trips when a single execution resolves more // fields than the configured budget permits — ExecuteOptions.MaxResolvedFields // directly, or HandlerOptions.MaxResolvedFields for handler-served // requests (PERF-0075). It is the executor's own defense against // fragment bombs reaching execution with the validator's complexity // limits disabled. The GQLError surfacing it wraps this sentinel, so // errors.Is(respErr, ErrResolvedFieldLimit) holds. ErrResolvedFieldLimit = errors.New("gqlx: resolved field count exceeds configured limit") // ErrMutationOverGET trips when a client submits a mutation or // subscription operation via HTTP GET (GAP-0022). GET requests are // idempotent/cacheable by convention and, in a cookie-authenticated // deployment, reachable via simple top-level cross-site navigation // even under SameSite=Lax — so a state-changing operation permitted // over GET is a CSRF primitive. The generator additionally hardcodes // AllowGET: false in front of RegisterGeneratedGQL (see // cmd/apic/templates/server_lib.go.tmpl) so generated servers never // route GET here at all, but Handler is itself an exported package // usable directly (NewHandler wired by hand), so the invariant is // enforced here too, at the library layer. ErrMutationOverGET = errors.New("gqlx: mutation and subscription operations are not allowed over GET") )
Sentinel errors for GraphQL runtime. Kept at package level per repo conventions. Messages use a lowercase, package-prefixed, diagnostic form so log output reads naturally; errors.Is callers should rely on pointer equality rather than substring matching of Error().
var ( TypeString = &ScalarType{Name: "String"} TypeInt = &ScalarType{Name: "Int"} TypeFloat = &ScalarType{Name: "Float"} TypeBoolean = &ScalarType{Name: "Boolean"} TypeID = &ScalarType{Name: "ID"} )
Built-in scalar types per the GraphQL specification.
var BuiltinScalarCoercers = map[string]*ScalarType{ "datetime": ScalarDateTime, "uuid": ScalarUUID, "json": ScalarJSON, }
BuiltinScalarCoercers exposes the built-in custom scalars by their short coercer-id so generated code can register them from config without reflecting on Go variable names.
var ErrWSFrameTooLarge = errors.New("gqlx: websocket frame exceeds configured size")
ErrWSFrameTooLarge is returned by the in-package WebSocket frame reader when an inbound frame exceeds [maxWSFrameBytes]. The reader is independent of pkg/wsx's framing, so the sentinel lives here to keep gqlx self-contained.
var ScalarDateTime = &ScalarType{ Name: "DateTime", Coerce: func(v any) (any, error) { switch x := v.(type) { case nil: return nil, nil case time.Time: return x, nil case *time.Time: if x == nil { return nil, nil } return *x, nil case string: if t, err := time.Parse(time.RFC3339Nano, x); err == nil { return t, nil } if t, err := time.Parse(time.RFC3339, x); err == nil { return t, nil } return nil, fmt.Errorf("gqlx: DateTime must be RFC3339, got %q", x) default: return nil, fmt.Errorf("gqlx: DateTime cannot accept %T", v) } }, }
ScalarDateTime coerces RFC3339 / RFC3339Nano strings (and *time.Time / time.Time values) to time.Time. It is intentionally strict: malformed strings produce an error so they are rejected at validation time rather than producing garbage downstream.
var ScalarJSON = &ScalarType{ Name: "JSON", Coerce: func(v any) (any, error) { return v, nil }, }
ScalarJSON accepts any JSON-shaped value and passes it through. Use for free-form structured payloads where the schema does not need to enforce shape (audit blobs, vendor extensions, etc.).
var ScalarUUID = &ScalarType{ Name: "UUID", Coerce: func(v any) (any, error) { s, ok := v.(string) if !ok { return nil, fmt.Errorf("gqlx: UUID must be a string, got %T", v) } if !isUUID(s) { return nil, fmt.Errorf("gqlx: UUID malformed: %q", s) } return s, nil }, }
ScalarUUID coerces RFC4122 UUID strings. The check is purely syntactic (length + hyphen layout + hex characters) so the package keeps zero runtime deps — callers that want canonicalization can still wrap this with their own scalar.
Functions ¶
func ContextWithRequest ¶
ContextWithRequest stores the active HTTP request in a context for resolver auth checks.
func RequestFromContext ¶
RequestFromContext retrieves the active HTTP request when present.
Types ¶
type AuthFunc ¶
AuthFunc extracts an auth credential from the context. Return a non-empty string if the credential is present and valid.
type Document ¶
type Document struct {
Operations []*Operation
Fragments []*FragmentDef
}
Document is the top-level AST node produced by the parser.
func Parse ¶
Parse lexes the input GraphQL document string and builds an AST using the historical defaults (introspection root fields rejected). Callers who need to flip the introspection knob should use ParseWithOptions.
func ParseWithOptions ¶
func ParseWithOptions(input string, opts ParseOptions) (*Document, error)
ParseWithOptions is the option-aware form of Parse.
type EnumType ¶
EnumType represents a GraphQL enum. Like ObjectType, enum values are stored in insertion order for deterministic schema emission while also supporting O(1) lookup by name.
func NewEnumType ¶
NewEnumType creates a new EnumType with the given name and initialized internal storage.
func (*EnumType) AddValue ¶
AddValue registers an enum variant under the given name. Re-adding a name replaces its definition while preserving insertion order.
func (*EnumType) Value ¶
func (e *EnumType) Value(name string) (EnumValueDef, bool)
Value looks up an enum variant by name. Returns the definition and true if found, or a zero EnumValueDef and false otherwise.
func (*EnumType) Values ¶
func (e *EnumType) Values() []EnumValueDef
Values returns variant definitions in insertion order. The returned slice is a copy and may be mutated by the caller.
type EnumValueDef ¶
EnumValueDef is one variant within an EnumType.
type ExecuteOptions ¶
type ExecuteOptions struct {
Validate ValidateOptions
AllowIntrospection bool
// MaxResolvedFields bounds the TOTAL number of fields the executor
// resolves for one operation, counting every field reached through
// fragment spreads, inline fragments, nested selections, and list
// elements. It is the executor's own defense-in-depth against
// fragment bombs: the validator already memoizes fragment cost
// (PERF-0064), but a caller that disables the complexity limits can
// still hand the executor a document whose expansion is exponential
// in document size (the executor re-walks fragment selections per
// spread site — memoizing results would be wrong, the result differs
// per spread site). When the budget is exhausted execution halts and
// the response carries a single ErrResolvedFieldLimit error.
// PERF-0075.
//
// Zero applies DefaultMaxResolvedFields; a negative value disables
// the budget.
MaxResolvedFields int
}
ExecuteOptions tunes Execute's behavior beyond the historical ValidateOptions surface. The zero value preserves Execute's original contract; in particular, AllowIntrospection defaults to false so a rebuild does not unintentionally expose a protected schema.
type Field ¶
type Field struct {
Alias string
Name string
Arguments []*Argument
Directives []*Directive
Selections []Selection
}
Field is a single field selection, optionally aliased and nested.
type FieldDef ¶
type FieldDef struct {
Type GQLType
Args []ArgDef
Description string
Auth string
RateLimit int
Resolve ResolverFunc
Subscribe SubscribeFunc
// contains filtered or unexported fields
}
FieldDef describes a single field on an ObjectType, including its return type, arguments, metadata, and resolver function.
type FragmentDef ¶
type FragmentDef struct {
Name string
TypeCondition string
Directives []*Directive
Selections []Selection
}
FragmentDef is a named fragment definition.
type FragmentSpread ¶
FragmentSpread references a named fragment definition.
type GQLError ¶
type GQLError struct {
Message string `json:"message"`
Locations []Location `json:"locations,omitempty"`
Path []any `json:"path,omitempty"`
Extensions map[string]any `json:"extensions,omitempty"`
// contains filtered or unexported fields
}
GQLError represents a single error in a GraphQL response per the spec.
type GQLType ¶
type GQLType interface {
TypeName() string
// contains filtered or unexported methods
}
GQLType is the interface implemented by all GraphQL type descriptors. The unexported marker method prevents external packages from implementing the interface.
type Handler ¶
type Handler struct {
// contains filtered or unexported fields
}
Handler serves GraphQL queries over HTTP GET and POST. It implements http.Handler and supports single requests as well as batch requests (JSON arrays).
func NewHandler ¶
func NewHandler(schema *Schema, opts HandlerOptions) *Handler
NewHandler creates a Handler for the given schema, applying sensible defaults for any zero-valued option fields.
type HandlerOptions ¶
type HandlerOptions struct {
Timeout time.Duration
MaxQueryBytes int
MaxBatchSize int
MaxDepth int
MaxComplexity int
MaxAliases int
// AllowIntrospection, when true, lets the handler accept root-level
// __schema and __type selections and resolves them against the
// loaded Schema. Defaults to false so an accidental rebuild does not
// expose a previously-protected surface. Pair with an admin-only
// auth check on the route.
AllowIntrospection bool
// MaxResolvedFields bounds the TOTAL number of fields the executor
// may resolve for a single handler-served request, mirroring
// ExecuteOptions.MaxResolvedFields (PERF-0075). Zero applies
// DefaultMaxResolvedFields; a negative value disables the budget.
MaxResolvedFields int
// DocCacheSize bounds the per-Handler LRU cache of parsed+validated
// documents keyed by raw query string (PERF-0058). The zero value
// selects the default of 256 entries; a negative value disables the
// cache so every request re-parses and re-validates. The MaxQueryBytes
// gate runs in front of the cache, so cache keys are bounded and an
// attacker streaming unique oversized queries cannot grow the cache.
DocCacheSize int
// MaxBatchConcurrency bounds the number of batch elements handleBatch
// executes concurrently (N-10), mirroring pkg/mcpx's
// batchMaxConcurrency semaphore for its own parallel tool-batch
// dispatch. MaxBatchSize alone only bounds the TOTAL element count
// per request, not how many run at once — every element previously
// got its own unbounded goroutine, so an operator raising
// MaxBatchSize (e.g. for a legitimately large trusted-client batch)
// also raised the in-flight goroutine/resolver-concurrency ceiling by
// the same amount with no separate control. A value <= 0 selects the
// default of 8 (matching mcpx).
MaxBatchConcurrency int
}
HandlerOptions configures the GraphQL HTTP handler's security and operational limits.
type InlineFragment ¶
InlineFragment is an anonymous fragment with an optional type condition.
type InputFieldDef ¶
InputFieldDef describes one field on an InputObjectType.
type InputObjectType ¶
type InputObjectType struct {
Name string
Description string
// contains filtered or unexported fields
}
InputObjectType represents a GraphQL input object. Field types must be scalar, enum, list-of-input, or another input object — i.e. they may not point at output ObjectTypes. The type enforcement is not done at construction time; the validator and executor surface mismatches.
func NewInputObjectType ¶
func NewInputObjectType(name string) *InputObjectType
NewInputObjectType creates an InputObjectType with the given name and initialized internal storage.
func (*InputObjectType) AddField ¶
func (o *InputObjectType) AddField(name string, def InputFieldDef)
AddField registers an input field under the given name. Re-adding a name replaces its definition but preserves insertion order.
func (*InputObjectType) Field ¶
func (o *InputObjectType) Field(name string) (InputFieldDef, bool)
Field looks up an input field by name.
func (*InputObjectType) Fields ¶
func (o *InputObjectType) Fields() []namedInputField
Fields returns input field definitions in insertion order.
func (*InputObjectType) TypeName ¶
func (o *InputObjectType) TypeName() string
TypeName returns the input object's name as written in the schema.
type ListType ¶
type ListType struct {
Elem GQLType
}
ListType wraps another GQLType to represent a GraphQL list ([T]).
type NonNullType ¶
type NonNullType struct {
Elem GQLType
}
NonNullType wraps another GQLType to represent a non-null GraphQL type (T!).
func (*NonNullType) TypeName ¶
func (n *NonNullType) TypeName() string
type ObjectField ¶
ObjectField is a single field inside an ObjectValue.
type ObjectType ¶
type ObjectType struct {
// contains filtered or unexported fields
}
ObjectType represents a GraphQL object type with named fields. Fields maintain insertion order for deterministic output while also supporting O(1) lookup by name.
func NewObjectType ¶
func NewObjectType(name string) *ObjectType
NewObjectType creates a new ObjectType with the given name and initialized internal storage.
func (*ObjectType) AddField ¶
func (o *ObjectType) AddField(name string, f FieldDef)
AddField registers a field definition under the given name. If a field with the same name already exists, it is replaced but the insertion position is preserved. The field's argument index is precomputed here (once per registration) so resolver-time coercion avoids rebuilding it on every request (PERF-0061).
func (*ObjectType) Field ¶
func (o *ObjectType) Field(name string) (FieldDef, bool)
Field looks up a field by name. Returns the definition and true if found, or a zero FieldDef and false otherwise.
func (*ObjectType) Fields ¶
func (o *ObjectType) Fields() []string
Fields returns field names in insertion order.
func (*ObjectType) TypeName ¶
func (o *ObjectType) TypeName() string
type ObjectValue ¶
type ObjectValue struct{ Fields []*ObjectField }
ObjectValue holds a GraphQL input object literal.
type Operation ¶
type Operation struct {
Type OperationType
Name string
Variables []*VariableDef
Directives []*Directive
Selections []Selection
}
Operation represents a single query, mutation, or subscription.
type OperationType ¶
type OperationType int
OperationType enumerates the three root operation kinds in GraphQL.
const ( OperationQuery OperationType = iota OperationMutation OperationSubscription )
type ParseOptions ¶
type ParseOptions struct {
// AllowIntrospection, when true, lets __schema and __type appear as
// root-level selections. Even with this disabled __typename remains
// allowed in nested selections so client tooling that depends on
// type-tagging continues to work.
AllowIntrospection bool
// MaxDepth caps the structural nesting depth (selection sets + list/object
// argument values) the recursive-descent parser will descend before
// aborting with ErrDepthExceeded. This is a DEFENSE against stack-overflow
// DoS: the validator's MaxDepth runs only AFTER a full parse, so without
// this a deeply-nested document crashes the process during parsing (a Go
// stack overflow is a fatal throw that recover() cannot catch). 0 selects
// defaultParseMaxDepth. Counts both selection and value nesting, so it is
// intentionally larger than the selection-only validation MaxDepth. R2-1.
MaxDepth int
}
ParseOptions tunes the parser's behavior. The zero value preserves the historical Parse() contract — introspection root fields (__schema, __type) are rejected so a misconfigured server cannot leak its schema.
type ResolverFunc ¶
ResolverFunc is the signature for field resolver functions. It receives a context and a map of parsed arguments, returning the resolved value or an error.
type Response ¶
Response is the top-level JSON envelope for GraphQL responses.
func Execute ¶
func Execute(ctx context.Context, schema *Schema, query string, variables map[string]any, operationName string, timeout time.Duration, vopts ...ValidateOptions) *Response
Execute parses, validates, and executes a GraphQL query against the given schema. It never returns a Go error; all errors are wrapped in the Response.Errors slice. Variables supplies runtime variable values, operationName selects a specific operation when the document contains more than one, and timeout limits individual resolver execution time. An optional ValidateOptions may be provided; when absent sensible defaults are used.
func ExecuteWithOptions ¶
func ExecuteWithOptions(ctx context.Context, schema *Schema, query string, variables map[string]any, operationName string, timeout time.Duration, eopts ExecuteOptions) *Response
ExecuteWithOptions is the option-aware form of Execute. It is wired from HandlerOptions so callers can opt in to introspection without changing the historical Execute signature.
type ScalarType ¶
ScalarType represents a built-in or custom GraphQL scalar.
Coerce is optional. When set, it is invoked by the executor whenever a value at this scalar position is observed (resolver argument coming from a variable, literal during validation). A nil Coerce keeps the historical pass-through behavior so the five built-in scalars (String/Int/Float/Boolean/ID) remain wire-compatible without changes.
func (*ScalarType) TypeName ¶
func (s *ScalarType) TypeName() string
type Schema ¶
type Schema struct {
Query *ObjectType
Mutation *ObjectType
Subscription *ObjectType
// AuthFunc is called to enforce field-level auth. When a FieldDef has
// a non-empty Auth string the executor calls AuthFunc; if it returns
// "" the field is rejected with ErrAuthRequired.
AuthFunc AuthFunc
}
Schema holds the three root operation types for a GraphQL service.
type Selection ¶
type Selection interface {
// contains filtered or unexported methods
}
Selection is implemented by Field, FragmentSpread, and InlineFragment.
type SubscribeFunc ¶
SubscribeFunc is the signature for subscription resolver functions. It returns a channel that emits values for as long as the subscription is active. Closing the channel ends the subscription.
type SubscriptionHandler ¶
type SubscriptionHandler struct {
// contains filtered or unexported fields
}
SubscriptionHandler manages subscription lifecycle over WebSocket using the graphql-transport-ws protocol (https://github.com/enisdenjo/graphql-ws).
N-09: one handler per connection. HandleConn is meant to be called exactly once per *SubscriptionHandler — the generated code follows this by constructing a fresh handler for every upgraded connection (cmd/apic/templates/gql_handlers.go.tmpl). Calling HandleConn again on an instance that already served a connection (sequentially or concurrently) is defended against (see HandleConn) but is not the intended usage: a dedicated handler per connection keeps the subs/conn/bw state trivially scoped and avoids relying on that defense at all.
func NewSubscriptionHandler ¶
func NewSubscriptionHandler(schema *Schema, opts SubscriptionOptions) *SubscriptionHandler
NewSubscriptionHandler creates a subscription handler.
func (*SubscriptionHandler) HandleConn ¶
func (h *SubscriptionHandler) HandleConn(c net.Conn, req *http.Request)
HandleConn runs the graphql-transport-ws protocol over a raw net.Conn. The caller is responsible for completing the HTTP-to-WebSocket upgrade before passing the connection. HandleConn blocks until the connection is closed or an unrecoverable error occurs.
N-09: intended to be called once per handler (see the one-handler-per- connection contract on SubscriptionHandler). As defense in depth against a caller reusing an instance, any subscriptions still tracked from a PRIOR HandleConn call are cancelled here before the registry is reset — otherwise h.subs = make(...) below would silently orphan them: their stream goroutines (streamSubscription) delete from whatever h.subs currently points to, which after this reassignment is a different, freshly-empty map, so their contexts/goroutines would never be cancelled and would leak until (if ever) their channel closes on its own.
type SubscriptionOptions ¶
type SubscriptionOptions struct {
MaxSubscriptions int
ValidateOptions ValidateOptions
// IdleTimeout bounds how long the read loop will block waiting for the
// next client frame. Without it a connected-but-idle (or slow-trickle)
// client holds the read goroutine and socket forever — a slow-loris DoS,
// since the GraphQL-WS path reads the raw net.Conn directly. 0 selects
// defaultSubscriptionIdleTimeout. R2-2.
IdleTimeout time.Duration
}
SubscriptionOptions configures the subscription handler.
type Token ¶
Token represents a single lexical token from a GraphQL document.
type TokenKind ¶
type TokenKind int
TokenKind classifies each token produced by the lexer.
const ( TokEOF TokenKind = iota // end of input TokName // identifier: [_A-Za-z][_0-9A-Za-z]* TokInt // integer literal TokFloat // float literal TokString // string or block string literal TokLBrace // { TokRBrace // } TokLParen // ( TokRParen // ) TokLBracket // [ TokRBracket // ] TokColon // : TokBang // ! TokDollar // $ TokAt // @ TokEquals // = TokSpread // ... TokPipe // | )
type ValidateOptions ¶
ValidateOptions controls the security limits enforced during validation.
type Value ¶
type Value interface {
// contains filtered or unexported methods
}
Value is implemented by all GraphQL value literal types.
type VariableDef ¶
VariableDef captures a variable declaration inside an operation.
type VariableValue ¶
type VariableValue struct{ Name string }
VariableValue references a variable by name.