graphql

package module
v0.0.0-...-8018d94 Latest Latest
Warning

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

Go to latest
Published: Oct 2, 2026 License: MIT Imports: 35 Imported by: 0

README

graphql-go

CI Go Reference Go Report Card

A schema-first GraphQL runtime for Go with a code-first, type-safe binding API.

graphql-go is built for large schemas: hundreds of types, thousands of fields, generated or hand-written. It keeps the build fast because bindings are ordinary function values assembled at start-up, and it keeps requests fast because every operation is compiled once into an immutable plan and streamed straight into a pooled JSON buffer.

Why

  • Small generated surface. Bindings are plain calls to generic constructors (Object, Field, Resolve, Input, Enum, Scalar). Nothing is emitted per field beyond a one-line closure, so a 200-entity schema generates in under a second using 44 MB, where gqlgen needs 31 s and 4.6 GB. See docs/benchmarks.md for what this does and does not buy at compile time.
  • Typed output path. Go types are matched to SDL types once in NewSchema. Resolvers, scalar writers and explicit InputField setters are then ordinary function values. Args[T]() / Input[T](name) may reflect once per input value; that is decode only.
  • Specification conformance. Null bubbling, list element errors, fragments, @skip/@include, variable and argument coercion, @oneOf input objects from literals and variables alike, interfaces and unions, introspection (including specifiedByURL, isOneOf, deprecated arguments and directive deprecation) and the GraphQL over HTTP protocol. The gaps that remain are all in the parser rather than the engine; each is tracked with a reproduction and an upstream patch in docs/spec-conformance.md.
  • Production behaviour by default. Saturating query cost, response size and error-count limits, panic recovery (DataLoader batches and subscription events included), error masking, gRPC-style interceptors, schema directives, CSRF prevention, DataLoader batching, and GitHub/Shopify-style complexity, depth and query-cost limits.

Install

go get github.com/syssam/graphql-go

Go 1.27 or newer.

The API reference is godoc -- go doc github.com/syssam/graphql-go, and doc.go is the overview worth reading before writing any binding. docs/README.md maps everything else: which example teaches what, which document answers whether to trust this, and what is not written yet.

Quick start

package main

import (
	"context"
	"log"
	"net/http"
	"time"

	"github.com/syssam/graphql-go"
	"github.com/syssam/graphql-go/transport/gqlhttp"
)

type User struct {
	ID   graphql.ID
	Name string
}

type userArgs struct{ ID graphql.ID }

func main() {
	users := map[graphql.ID]*User{"1": {ID: "1", Name: "Ada"}}

	s, err := graphql.NewSchema(graphql.SDL(`
		type User { id: ID! name: String! }
		type Query { user(id: ID!): User }
	`),
		graphql.Args[userArgs](),
		graphql.Object[User]("User",
			graphql.Field("id", func(u *User) graphql.ID { return u.ID }),
			graphql.Field("name", func(u *User) string { return u.Name }),
		),
		graphql.Object[graphql.Root]("Query",
			graphql.ResolveArgs("user", func(ctx context.Context, _ graphql.Root, a userArgs) (*User, error) {
				return users[a.ID], nil
			}),
		),
	)
	if err != nil {
		log.Fatal(err)
	}

	// Depth, cost and timeout limits are off by default because the right
	// numbers depend on the schema; set them before serving anything
	// untrusted. docs/operations.md has how to choose them.
	exec := graphql.NewExecutor(s,
		graphql.WithMaxDepth(10),
		graphql.WithQueryCost(graphql.QueryCost{Max: 10_000, DefaultListSize: 20}),
		graphql.WithOperationTimeout(5*time.Second),
	)
	http.Handle("/graphql", gqlhttp.New(exec))
	log.Fatal(http.ListenAndServe(":8080", nil))
}
curl -s localhost:8080/graphql -H 'content-type: application/json' \
  -d '{"query":"{ user(id: \"1\") { id name } }"}'
# {"data":{"user":{"id":"1","name":"Ada"}}}

CSRF prevention is on by default, so a plain GET such as curl 'localhost:8080/graphql?query={...}' is refused with 403: a browser can send a GET cross-site without a preflight. Send GraphQL-Require-Preflight: 1, Apollo-Require-Preflight: 1 (what Apollo Client sends) or X-Requested-With, or a Content-Type a form cannot send (the POST above uses application/json), to show the request is not forgeable. A browser EventSource cannot set headers, so subscribe over SSE with fetch and a POST instead.

Subscriptions

Subscribe binds a subscription root field to a channel. Each value received becomes one response, with the field's sub-selection executed against it:

graphql.Subscription(
	graphql.Subscribe("postCreated", func(ctx context.Context) (<-chan *Post, error) {
		return blog.PostsCreated(ctx), nil
	}),
)

Serve them over either streaming transport; both also serve queries and mutations, so a client needs only one endpoint:

mux.Handle("/graphql", gqlhttp.New(exec))
mux.Handle("/graphql/stream", gqlsse.New(exec))   // Server-Sent Events
mux.Handle("/graphql/ws", gqlws.New(exec))        // graphql-transport-ws
curl -N localhost:8080/graphql/stream -H 'content-type: application/json'   -H 'accept: text/event-stream'   -d '{"query":"subscription { postCreated { title } }"}'
# event: next
# data: {"data":{"postCreated":{"title":"Live"}}}

Closing the connection unsubscribes: the source channel's context is cancelled, so a broker can drop the subscriber and stop producing.

On shutdown, the server's own Shutdown drains ordinary requests but not these: it neither closes nor waits for WebSockets, and an SSE stream never ends by itself, so Shutdown waits out its whole timeout. A drain.Drain handed to both streaming handlers winds them down — subscriptions end (WebSocket close 1001, or the SSE response ends without complete, so clients reconnect), queries and mutations already running over a WebSocket finish, and new WebSocket connections and new SSE subscriptions get 503:

d := drain.New()
mux.Handle("/graphql/stream", gqlsse.New(exec, gqlsse.WithDrain(d)))
mux.Handle("/graphql/ws", gqlws.New(exec, gqlws.WithDrain(d)))

// on SIGTERM: both together, under one deadline
var wg sync.WaitGroup
wg.Go(func() { _ = d.Shutdown(ctx) })
_ = srv.Shutdown(ctx)
wg.Wait()

gqlecho.SSE/WS take the same options, and gqlfiber.WithDrain does the same for Fiber. Past the deadline d.Shutdown cuts what is left and returns.

Behind a load balancer, gqlws.WithMaxConnectionAge(age, grace) retires WebSocket connections after age (±10%, spread out so they do not all reconnect at once) the same way, so clients reconnect to wherever the balancer now sends them; WithMaxConnectionIdle closes one with nothing in flight, and gqlsse.WithMaxStreamAge ends SSE subscription streams. gqlfiber has the same options; all are off by default.

A fuller example with interfaces, unions, enums, custom scalars, input objects, Omittable PATCH semantics, a schema directive, a DataLoader for Post.author and a subscription fed by the createPost mutation lives in examples/blog. examples/quickstart is a much smaller note board — hand-written bindings, no codegen. examples/storefront is the one to read before deploying anything: an order API where four principals see different parts of the same graph, every position declares what it requires, and the server carries the limits, persisted queries, tracing and shutdown drain the other examples leave out. examples/README.md compares them.

The development loop

If you know gRPC you already know the shape: write the contract, generate from it, implement the interface it hands you, wire it to a transport.

gRPC graphql-go
write service.proto write schema.graphql
protoc --go_out=. go tool gqlc -schema schema.graphql -out graph
implement XxxServer implement the generated Resolver
grpc.NewServer() graph.NewSchema(r), then a transport

docs/workflow.md walks it through, including what gqlc does not do -- it never loads Go packages, which is why a 200-entity schema generates in under a second -- and the DataLoader step that turns a resolver field from an N+1 into one query per wave.

Concepts

Constructor Purpose
Object[E](name, fields...) Bind a GraphQL object type to Go struct E. Field functions receive *E.
Field / FieldArgs Pure data access; runs inline, never on its own goroutine.
Resolve / ResolveArgs May perform I/O; scheduled concurrently, one goroutine each. Bound the work of a request with the cost, complexity and response-size limits.
Args[A]() / Input[T](name) Decoder from struct fields (graphql / json tags, or AuthorID → authorId). Explicit InputField stays the zero-reflect path.
InputField, OmittableField Hand-written setters when a name or type needs an override.
Enum, Scalar Leaf types; several Go types may back one GraphQL type.
Time(name) time.Time as RFC 3339, gqlgen's Time wire format; the one custom scalar almost every schema has.
EnumMarshaler / ScalarMarshaler Types with gqlgen's MarshalGQL/UnmarshalGQL, bound as they are (ent and velox order fields and cursors).
Interface, Union, TypeResolver Abstract types resolved from the dynamic Go type or an explicit function.
Directive / DirectiveArgs[A] Schema-directive middleware on FIELD_DEFINITION and OBJECT.
Query / Mutation / Subscription Bind the schema's root types without repeating their names.
Subscribe / SubscribeArgs Bind a subscription root field to a <-chan R source. Executor.Subscribe yields one response per event.
fed.Subgraph Apollo Federation subgraph in graphql-go/fed: _service, _entities, the federation directives and an _Entity union derived from the @key types. No engine change; the author's SDL is never rewritten.
relay.Node / relay.Bind[T] Relay global object identification and cursor connections in graphql-go/relay. ToGlobalID/FromGlobalID, IDField, Pagination, FromSlice/FromPage. The SDL still declares the types.
loader.New / loader.NewMapped Per-request batch+cache (Facebook DataLoader) in graphql-go/loader. Load coalesces concurrent Resolve fields in one execution wave, driven by graphql.WaveCoordinator. NewMapped reports failure per key, so one bad id does not null its siblings.

Resolvers can read their context with graphql.FieldFrom, graphql.PathFrom and graphql.SelectionFrom; graphql.OperationFrom exposes the operation, variables, complexity, depth and cost. SetExtension writes response-level metadata (tracing ids, rate-limit windows).

Executor options follow the gRPC style: WithMaxConcurrency, WithPlanCache, WithPlanCacheBytes (bound the cache by query text, 16 MiB by default), WithErrorPresenter, WithRecover, typed interceptors, WithFieldObserver (watch every field, pure ones included, without the type-erased path a field interceptor forces), plus production limits WithMaxResponseBytes (null data and one RESPONSE_TOO_LARGE error once a response's data passes the limit, 64 MiB by default; execution stops early rather than writing the rest), WithMaxErrors (at most 1000 errors a response by default, then one ERROR_LIMIT_EXCEEDED notice), WithOperationTimeout (a deadline per query, mutation or subscription event, reported as OPERATION_TIMEOUT rather than the caller's REQUEST_CANCELLED; off by default), WithMaxComplexity, WithMaxDepth and WithQueryCost (Shopify-style first/last multipliers, Connections to price a Relay connection by its requested page size, and optional extensions.cost; Actual adds actualQueryCost, summed from the fields really resolved rather than from assumed list sizes). Schema options: DisableIntrospection.

Empty Args[T]() / Input[T](name) derive every field (graphql tag, else json tag, else AuthorID → authorId). Passing any InputField switches that struct to fully explicit setters; the two modes do not mix.

Status

Phases 1-4 of the design are complete.

Engine. Schema-first with a code-first binding API: SDL is the contract and Go bindings are plain function values, checked against it at NewSchema rather than at request time. A document is parsed and validated once and cached in an LRU bounded by entry count and by query text; concurrent misses for one query share a single parse. Each (operation, @skip/@include variant) compiles to an immutable plan with fragments flattened, directives constant-folded, arguments pre-decoded and response keys pre-serialized, and abstract types expanded through a memo so the plan stays a DAG rather than a tree. The request path writes JSON straight into a pooled buffer, with no reflection on it at all.

Code generation. cmd/gqlc emits models, args structs, a Resolver interface and bindings from SDL alone -- it never loads Go packages -- splitting into per-group packages when more than one group is present. codegen.Config.Manifest binds GraphQL types and fields to Go types outright instead of inferring them, which is the mode an external generator such as an ORM wants, and AutoBind discovers the same bindings from named packages, loading only their export data rather than a whole module. examples/blog uses the generated package.

Transports. gqlhttp, gqlsse (Server-Sent Events), gqlws (graphql-transport-ws), and gqlecho and gqlfiber for Echo v5 and Fiber v3. All five serve every operation kind, so a client needs one endpoint, and they share one set of CSRF, body-limit and content-negotiation rules that transport/equivalence_test.go proves by driving real requests through all six HTTP-carrying handlers. transport/drain winds down what http.Server.Shutdown cannot: an SSE stream is a request that never ends and a WebSocket is hijacked, which Shutdown neither closes nor waits for. Connection age and idle limits mirror grpc's keepalive.ServerParameters, all off by default.

Authorization. Declared in the SDL and compiled into the plan, not wrapped around it: a field that declares nothing costs one integer compare and no allocation. @requiresScopes is built in, RequirementDirective and MarkerDirective add a schema's own spelling (Apollo's @policy and @authenticated are one call each), @authorizeInput makes an argument's contents a site of its own, and @authorizeObject decides individual rows during execution, batched per list. An Authorizer receives what the operation touches and records an outcome per site -- allow, deny, null, zero, redact or drop -- and RequireAuthCoverage fails the build for a field that declares neither a requirement nor @public. examples/storefront wires all of it.

Extensions. ext/apq for automatic persisted queries, opt-in on every HTTP transport and on gqlws and gqlfiber; ext/trusted turns the same wiring into a safelist that runs only documents a build step registered and refuses query text whatever hash accompanies it, which is what Relay's --persist-output and Apollo's manifest are for; ext/otel for traces and metrics, including DataLoader batch spans (otel.Batch("user", loadUsers)) and runtime instruments registered after the executor and drain exist (otel.ObserveExecutor, otel.ObserveDrain, each once per meter); and ext/throttle for cost-based rate limiting, a bucket of points per caller quoted before the query runs and charged what it really cost, reported as extensions.cost.throttleStatus the way Shopify's Admin API does.

Limits, all off by default except the last two: query depth, complexity and cost (QueryCost.Actual reports what really resolved alongside what was assumed from list sizes, which is how you tell whether DefaultListSize is set near reality), an operation timeout that bounds work rather than latency, a response size cap (64 MiB), and an error-list cap (1000).

Also: relay/ for global ids, Node and cursor connections, shown end to end in examples/relaynode; fed/ for Apollo Federation subgraphs, with examples/federation showing two of them and the fetch a router performs across them; and lint/, an analyzer for a bug class -race cannot find -- a check-then-act pair on OperationContext that silently degrades DataLoader batching to N+1.

Not built: ext/authz, and what is left of it is smaller than a package -- @requiresScopes is core, @policy is one RequirementDirective call and @authenticated one MarkerDirective, leaving only a batched Guard. @defer/@stream is not merely unbuilt: the prelude's @defer is stripped so that the validator and introspection agree the server says no.

API stability

Nothing is tagged yet. docs/api-stability.md records what the promise will cover, which parts are still expected to move, and the one coupling that decides this library's major version for it: gqlparser/v2's AST is part of the public API, so a gqlparser v3 means a major release here. TestPublicAPISurface pins all 448 exported declarations against docs/public-api.txt, so an API change is a failing test rather than a code review someone has to catch.

Running it in production

docs/operations.md is the deployment page: which limits to turn on before the engine faces anything untrusted (four are off by default), how to shut down streams the HTTP server cannot, what sizes a replica, which metric to alarm on, and the limits to design around -- including the ones this engine will not do, and the figures nobody has measured yet.

Development

sh scripts/gate.sh                 # vet and test every module, not just the root
go vet ./... && go test -race -count=1 ./...
go test -run xxx -bench . -benchmem .
go test -run xxx -fuzz FuzzExecute -fuzztime 30s .
cd benchmarks && go test -run '^$' -bench . -benchmem -count=5

Measured performance and specification conformance are summarised in docs/performance.md, which links the detail: query execution and memory in compare/, build cost in docs/benchmarks.md, and the official GraphQL over HTTP audit in docs/graphql-http-audit.md — 0 errors, all 13 MUST requirements met. Bindings can be generated from SDL with cmd/gqlc.

See CONTRIBUTING.md before opening a pull request, and SECURITY.md to report a vulnerability. Licensed under the MIT License.

Documentation

Overview

Package graphql is a schema-first GraphQL runtime for Go with a code-first, type-safe binding API.

A Schema is built from SDL plus bindings that connect GraphQL types to Go types: Object, Field, Resolve, Input, Args, Enum, Scalar, Interface, Union and Directive. Generated code and hand-written code use the same constructors, so code generation is a convenience rather than a requirement. Bindings are validated against the SDL once, at NewSchema.

After start-up, output fields, explicit InputField setters and scalar writers are typed function values. Args[T]() / Input[T](name) may use reflection once per input value to fill struct fields; that is decode only and is not on the JSON write path. Composite lists deeper than one level also use a reflective traverser recorded at start-up. A composite list field may return iter.Seq[E] or iter.Seq[*E] wherever it may return []E or []*E.

An Executor compiles each operation into an immutable, cached plan and writes the response directly into a pooled JSON buffer. Fields bound with Field are treated as pure data access and run inline; fields bound with Resolve may perform I/O and are scheduled concurrently under a bounded semaphore. The loader package batches I/O across those resolvers in one request through WaveCoordinator; WithMaxComplexity, WithMaxDepth and WithQueryCost reject expensive operations before they run.

Subscriptions are bound with Subscribe and SubscribeArgs, which return the source channel for a subscription root field. Executor.Subscribe runs one such operation and yields a Response per event, executing the field's sub-selection against the event exactly as a query would be executed against a resolver's result.

Authorization is a compiled pass over the plan, not a wrapper around resolvers, and keeps three concerns separate: what an operation touches, what a principal may do with it, and how that decision is enforced. A schema author marks a field with @requiresScopes(scopes: [[String!]!]!), an OR of ANDs read into a Requirement; NewSchema rejects a malformed scopes value. That spelling is the default rather than the only one: RequirementDirective(name, arg, shape) declares another SDL directive read into a Requirement, so a schema that already spells its requirements @auth(requires: [String!]) is enforced without renaming every site. The shape is declared rather than inferred, because @auth(requires: ["a","b"]) is the same text whether the author meant AND or OR and guessing wrongly widens access. MarkerDirective(name, scope) covers a directive with no argument, whose presence alone is the requirement, which is the shape of Apollo's @authenticated. @requiresScopes and @authorizeInput only describe positions in the schema; nothing is enforced against them unless an Authorizer is configured with WithAuthorizer. At plan compile, buildAuthShape walks the plan once and records every such field, and every field that selects an @authorizeInput argument, as a Site in an AuthShape, cached with the plan itself -- the shape does not depend on who is asking, so the plan cache is not multiplied by policy, and a field that declares nothing and selects no @authorizeInput argument carries authIdx -1 and costs one integer compare on the request path. An Authorizer turns a shape into a Decision once per query or mutation -- and, for a subscription, once when the stream opens plus once more per event. At open, exactly three things refuse the subscription before its source is opened: an Authorize error, a Deny recorded for the subscription root field, or a Deny recorded for one of that field's argument sites; any other outcome on the root, and every outcome below it, is left to the per-event pass. That pass gates each event's Response, so a scope revoked mid-stream takes effect on the next event without tearing the subscription down; a refused event is an error response, not a closed stream. Decision.Set assigns each Site an Outcome: Allow (the zero value), Deny (an AIP-211-worded error that reveals neither the value nor whether the resource exists), Null, Zero (a non-null field's zero value, for withholding without null-bubbling its parent), or Redact (resolves the field and rewrites the value). Drop is valid only as an ObjectAuthorizer's reply for an instance site at a list element; Decision.Set rejects it everywhere else. An outcome Set rejects is recorded as a denial, so a policy that ignores the error still fails closed. Deny, Null and Zero write their response without calling the resolver, so a FieldInterceptor never observes those fields; Redact does resolve, so a FieldInterceptor sees the pre-redaction value.

WithSubscriptionInterceptor wraps the opening of a subscription's stream, the way OperationInterceptor wraps a query or mutation -- gRPC's Unary/Stream split. It exists because an OperationInterceptor only sees a subscription's individual events, by which point the source is already open and refusing the subscription outright is no longer possible. Relatedly, NewSchema rejects a bound directive (one that wraps a field executor) on a subscription root field: the per-event writer substitutes that field's executor outright to yield the event, so a directive bound there would compose cleanly and then never run.

RequireAuthCoverage is an opt-in, build-time default deny: NewSchema fails for any bound field that declares neither @requiresScopes nor @public. A field's effective requirement is the AND of its own @requiresScopes, its object type's, and each interface it implements -- both that interface's type-level requirement and its same-named field's. resolveAuthRequirements computes it once, at NewSchema, and stores the result on the field's fieldDef and the object's objectType; the shape builder and RequireAuthCoverage both read those stored values, so what a request enforces and what coverage accepts cannot diverge. __typename is guarded the same way, through a SiteObject built from its object's effective type-level requirement; Decision.Set admits only Allow and Deny there, since __typename is String! and has no value to null, zero or redact. That guard is for consistency, not confidentiality: authorization at the field or instance level does not hide how many objects of a guarded type exist or that they exist, and an object whose selection folds to empty is written without consulting any site. __schema and __type are fields of the query root, so an @requiresScopes on the Query type guards introspection as well; that fails closed on purpose. @requiresScopes anywhere the engine does not enforce it -- a union, enum, enum value, scalar, input object, input field, an argument, the schema definition itself, or a directive definition's own argument -- is a build error, not a silent no-op. RequireAuthCoverage counts a field as covered exactly when its effective requirement is non-zero, or when the field or its own object type carries @public; @public on an interface does not exempt its implementers. An argument marked @authorizeInput(kind: FILTER | WRITE) gets a SiteFilterArg or SiteInputWrite site on every field that selects it, whether or not the client supplied a value for it. Decision.Input reports what the client actually supplied there: input-object key paths, enum values and explicit nulls, but never a scalar value, since a policy decides on schema identifiers and scalars are user data; a default an operation variable itself carries counts, because the operation's author chose it, while a default the SDL supplies for the argument or an input field does not, because the client never chose it. Only Allow and Deny apply to an argument site; Deny refuses the field before its resolver runs, whatever outcome is recorded for the field's own output site. ScopeAuthorizer leaves every argument site at Allow, since its Requirement is zero and mapping a key or enum value to the field it restricts is the consumer's naming convention rather than the engine's; RequireAuthCoverage does not require an argument to declare @authorizeInput, for the same reason. Individual values are decided separately. An object type marked @authorizeObject has every value of it offered to the ObjectAuthorizer registered with WithObjectAuthorizer before it is written; without one the marker describes positions and nothing is enforced. These decisions cannot live in a Decision, which is built before any resolver runs and so cannot name values that do not exist yet. Checks are batched per list -- a list of guarded values is drained and decided in one call before any element is written, split only by WithObjectAuthBatch (50 by default) -- while a guarded object reached through a field that is not a list is one call carrying one check. Allow, Deny, Null and Drop apply; Null only where the position is nullable, and Drop only to a list element, where it omits the value and renumbers the indices that follow, so a later denial is reported at the index the client actually sees. A batch that fails -- an error, a mismatched length, an outcome the site cannot represent, a panic -- fails every check still outstanding rather than allowing the ones a later batch would have covered. The marker is rejected at NewSchema anywhere the executor would not consult an ObjectAuthorizer, a root operation type included. An instance site appears in AuthShape.Sites so an Authorizer can see that instance checks will happen, but its outcome comes from the ObjectAuthorizer: Decision.Set refuses it, and an operation whose only sites are instance sites does not consult the Authorizer at all.

An Authorizer failure that is not a *Error -- typically a policy backend's own transport failure -- is never shown to a client: it is presented as a generic internal error, with the original logged and kept behind an unexported cause for a presenter that wants to test it.

Index

Constants

View Source
const (
	CodeInternal            = "INTERNAL_SERVER_ERROR"
	CodeParseFailed         = "GRAPHQL_PARSE_FAILED"
	CodeValidationFailed    = "GRAPHQL_VALIDATION_FAILED"
	CodeBadUserInput        = "BAD_USER_INPUT"
	CodeOperationResolution = "OPERATION_RESOLUTION_FAILURE"
	CodeRequestCancelled    = "REQUEST_CANCELLED"
	// CodeTooComplex is returned when static field count or query cost
	// exceeds the executor limit (GitHub / Shopify style).
	CodeTooComplex = "COMPLEXITY_LIMIT_EXCEEDED"
	// CodeMaxDepth is returned when the selection nesting exceeds WithMaxDepth.
	CodeMaxDepth = "MAX_DEPTH_EXCEEDED"
	// CodeForbidden is returned when an Authorizer denies a site.
	CodeForbidden = "FORBIDDEN"
	// CodeResponseTooLarge is returned when a response's data exceeds
	// WithMaxResponseBytes.
	CodeResponseTooLarge = "RESPONSE_TOO_LARGE"
	// CodeOperationTimeout is returned for work cut short by
	// WithOperationTimeout. When a deadline the caller set ends execution,
	// the executor reports CodeRequestCancelled instead.
	CodeOperationTimeout = "OPERATION_TIMEOUT"
	// CodeErrorLimitExceeded marks the one error that stands in for those
	// omitted past WithMaxErrors.
	CodeErrorLimitExceeded = "ERROR_LIMIT_EXCEEDED"
)

Error codes reported in the "code" entry of an error's extensions. They follow the conventions used by the wider GraphQL ecosystem so that generic clients can act on them.

Variables

This section is empty.

Functions

func PrintSDL

func PrintSDL(s *Schema) string

PrintSDL renders the schema as SDL, excluding built-in types and directives.

Every applied directive is printed, including ones the schema uses internally such as authorization requirements; a schema published from this output publishes those too. fed.Subgraph serves the author's own SDL instead.

The output reloads as the same schema, descriptions and string values included, which gqlparser's formatter does not guarantee; see sdlprint.

func Validate

func Validate(src Source, opts ...SchemaOption) error

Validate builds the schema and discards it. It exists for tests that want to assert bindings are complete and shape-compatible without executing anything.

Types

type AbstractOption

type AbstractOption interface {
	// contains filtered or unexported methods
}

AbstractOption tunes an Interface or Union binding.

func TypeResolver

func TypeResolver[T any](fn func(T) string) AbstractOption

TypeResolver overrides dynamic Go type lookup with an explicit function returning the concrete GraphQL object type name for a value.

type AuthShape

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

AuthShape is what an operation touches, independent of who is asking. It is computed once per compiled plan and cached with it, which is why the plan cache is not multiplied by the number of distinct policies.

func (*AuthShape) IsEmpty

func (s *AuthShape) IsEmpty() bool

IsEmpty reports whether the operation touches nothing an Authorizer can decide: it declares no requirement, selects no field with an @authorizeInput argument, and -- since an instance site is decided by the ObjectAuthorizer rather than here -- an operation whose only sites are instance sites is empty too. An Authorizer is not consulted for such an operation. Sites still reports the instance sites, so an Authorizer that is consulted for some other reason can see that instance checks will happen.

func (*AuthShape) Scopes

func (s *AuthShape) Scopes() []string

Scopes returns every scope named anywhere in the operation, sorted and deduplicated, so an Authorizer can load them in one round trip. Like Sites, this is a copy of the plan's own slice, for the same reason: the shape is cached with the plan and shared by every later request.

func (*AuthShape) Sites

func (s *AuthShape) Sites() []AuthSite

Sites returns the positions needing a decision, indexed by site index. planField.authIdx indexes into this slice.

The shape is cached with its plan and shared by every request that reuses it, and Authorize hands it to caller-supplied code, so this returns an independent copy rather than the plan's own slice: a caller that mutates a returned AuthSite's Coord, Requires or Grants must not corrupt authorization for a later request. Field and Object remain shared *ast.FieldDefinition/*ast.Definition pointers into the parsed schema — cloning gqlparser's AST per call would be its own, larger cost, and nothing in this package ever mutates that AST after NewSchema returns, so a caller would have to go out of its way to reach for a mutation this package itself never performs.

type AuthSite

type AuthSite struct {
	Coord string

	// Field and Object describe the position. Neither names a field for the
	// two kinds that are about a type rather than a field position:
	// SiteObject leaves Field nil, and SiteInstance leaves both nil, since an
	// instance is decided by its type and the value alone and the same
	// resolved object can reach several field positions. Coord carries the
	// type name for both.
	Field  *ast.FieldDefinition
	Object *ast.Definition

	Kind     SiteKind
	Requires Requirement
	Grants   []string

	// Arg names the argument for SiteFilterArg and SiteInputWrite sites. For
	// those kinds, Coord is Type.field(arg:) and Decision.Input reports what
	// the client supplied there.
	Arg string

	// ListElement reports that the value at this position is an element of a
	// list. It exists for SiteInstance, whose Field is nil: an
	// ObjectAuthorizer has to choose between Drop and Deny for a row it
	// withholds, Drop is valid only at a list element position, and without
	// this the only way to tell was to know the schema by heart. Every other
	// kind can read the same fact from Field.Type.
	ListElement bool

	// NonNull reports that the position a SiteInstance value occupies is
	// non-null: the field's type with every list wrapper stripped, so
	// Customer! and [Customer!]! say true and [Customer]! says false. It is
	// the other half of what ListElement is for. Null is the outcome that
	// gives nothing away -- a withheld row answers exactly as a missing one
	// does -- and it is valid only where the position can hold null, which a
	// policy otherwise had to know the schema to tell. Every other kind can
	// read the same fact from Field.Type.
	NonNull bool
	// contains filtered or unexported fields
}

AuthSite is one position in a plan that may need a decision.

type Authorizer

type Authorizer interface {
	Authorize(ctx context.Context, shape *AuthShape, d *Decision) error
}

Authorizer turns an operation's shape into a decision for one principal. It runs once per query or mutation, before any field resolves, and is not called at all when the operation touches nothing that declares a requirement and selects no field with an @authorizeInput argument. A subscription calls it once when the stream opens and again for every event, so a scope revoked mid-stream applies to the next event.

A returned error rejects the whole operation, or for a subscription the opening of the stream or the one event being evaluated. The interface deliberately holds no policy of its own: it is the adapter to whatever decides, be that OPA, Cedar, OpenFGA, Casbin or a hand-written checker.

func ScopeAuthorizer

func ScopeAuthorizer(held func(context.Context) map[string]bool) Authorizer

ScopeAuthorizer is the default policy: a site is allowed when held covers its Requirement, and denied with the AIP-211 wording otherwise. It exists so the common case needs no Authorizer of its own.

type AuthorizerFunc

type AuthorizerFunc func(ctx context.Context, shape *AuthShape, d *Decision) error

AuthorizerFunc adapts a function to Authorizer.

func (AuthorizerFunc) Authorize

func (f AuthorizerFunc) Authorize(ctx context.Context, shape *AuthShape, d *Decision) error

Authorize implements Authorizer.

type Decision

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

Decision records the outcome for each site in a shape. It is passed to the Authorizer rather than returned by it so the framework owns the allocation and sizes it from the shape. The zero value of every entry allows.

func (*Decision) Input

func (d *Decision) Input(site int) []InputKey

Input returns what the client supplied for an argument site: key paths, enum values and explicit nulls, never scalar values. It is empty for any other site and for an argument the client did not supply.

func (*Decision) Outcome

func (d *Decision) Outcome(site int) Outcome

Outcome returns the recorded outcome for a site.

func (*Decision) Set

func (d *Decision) Set(site int, o Outcome) error

Set records the outcome for one site. It reports an error for an outcome the site cannot represent, so a policy mistake surfaces once per request with the coordinate attached rather than as a null-bubbled parent, and records a denial in its place so the mistake never widens access.

type Error

type Error struct {
	Message    string
	Locations  []Location
	Path       Path
	Extensions map[string]any
	Err        error
	// contains filtered or unexported fields
}

Error is a GraphQL error as it appears in the "errors" array of a response. Err carries the underlying cause for logging and is never serialized.

func DefaultErrorPresenter

func DefaultErrorPresenter(_ context.Context, err error) *Error

DefaultErrorPresenter returns the first *Error or *gqlerror.Error found in err's chain (copied, so annotating it is safe), or wraps err verbatim. Extensions contributed by an ExtensionsProvider in the chain are merged in.

func Errorf

func Errorf(format string, args ...any) *Error

Errorf formats a new Error.

func (*Error) Error

func (e *Error) Error() string

Error implements the error interface.

func (*Error) MarshalJSON

func (e *Error) MarshalJSON() ([]byte, error)

MarshalJSON renders the error in the specification's format, emitting message, locations, path and extensions in that order and omitting empty members.

func (*Error) Unwrap

func (e *Error) Unwrap() error

Unwrap returns the underlying cause, if any.

func (*Error) WithCode

func (e *Error) WithCode(code string) *Error

WithCode sets the "code" extension and returns e.

func (*Error) WithExtension

func (e *Error) WithExtension(key string, v any) *Error

WithExtension sets one extension entry and returns e.

v must marshal with encoding/json. It is not checked here, and the failure surfaces late and badly: Response.WriteTo and MarshalJSON return an error for the whole envelope, and a transport has already written its status header by then, so the client receives an empty body with no explanation while the server logs a warning. One decorative extension can therefore cost the error it was decorating. Marshal it yourself if the value is anything but a string, number, bool, slice or map of those.

func (*Error) WithPath

func (e *Error) WithPath(p Path) *Error

WithPath sets the response path and returns e.

type ErrorPresenter

type ErrorPresenter func(ctx context.Context, err error) *Error

ErrorPresenter converts any error raised during a request into the Error that is sent to the client. Implementations typically mask internal messages and attach codes. It may return the same *Error for many calls: the executor copies it before setting the path and locations. Returning nil drops a field error; a request error, which the response cannot omit, is replaced by a generic one carrying the original's code.

type Executor

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

Executor runs operations against a Schema. It owns the plan cache and the concurrency budget and is safe for concurrent use.

func NewExecutor

func NewExecutor(s *Schema, opts ...ExecutorOption) *Executor

NewExecutor creates an executor for s.

func (*Executor) Execute

func (e *Executor) Execute(ctx context.Context, req *Request) *Response

Execute runs a query or mutation and returns its response. Call Response.Release once the response has been serialized.

func (*Executor) OperationKind

func (e *Executor) OperationKind(query, operationName string) (ast.Operation, error)

OperationKind reports the kind of the operation a request would execute, using the document cache. Transports use it to reject mutations over GET.

func (*Executor) Schema

func (e *Executor) Schema() *Schema

Schema returns the executor's schema.

func (*Executor) Stats

func (e *Executor) Stats() ExecutorStats

Stats reports the executor's current shared state.

func (*Executor) Subscribe

func (e *Executor) Subscribe(ctx context.Context, req *Request) (<-chan *Response, error)

Subscribe runs a subscription operation. It parses, validates and plans the document, opens the field's source stream, and returns a channel carrying one response per event. The channel is closed when the source closes or ctx is cancelled; cancelling ctx is how a caller unsubscribes.

Responses are sent unbuffered, so a slow consumer applies backpressure to the source rather than accumulating events. Call Response.Release on each one after serializing it.

On a request error nothing is started: the channel is nil and the error is a *SubscribeError carrying the response to send.

type ExecutorOption

type ExecutorOption func(*Executor)

ExecutorOption configures an Executor.

func DisableSuggestions

func DisableSuggestions() ExecutorOption

DisableSuggestions withholds the "Did you mean" clause from validation errors. DisableIntrospection alone does not close that channel: the suggestions name neighbouring fields, types, arguments and input fields, so a schema stays enumerable one typo at a time. The errors still report what was wrong, only without naming what would have been right.

func WithAuthorizer

func WithAuthorizer(a Authorizer) ExecutorOption

WithAuthorizer sets the authorizer consulted once per operation.

func WithErrorPresenter

func WithErrorPresenter(p ErrorPresenter) ExecutorOption

WithErrorPresenter replaces DefaultErrorPresenter.

func WithFieldInterceptor

func WithFieldInterceptor(is ...FieldInterceptor) ExecutorOption

WithFieldInterceptor registers field interceptors. The first is the outermost. Field interceptors force every field through the type-erased path; to observe fields without that cost, use WithFieldObserver.

func WithFieldObserver

func WithFieldObserver(o ...FieldObserver) ExecutorOption

WithFieldObserver registers field observers. The first is outermost, as with the interceptor options.

func WithMaxComplexity

func WithMaxComplexity(n int) ExecutorOption

WithMaxComplexity rejects operations whose static field count (aliases included) exceeds n. Zero, the default, means unlimited. __schema and __type count as one field each: see WithMaxDepth.

func WithMaxConcurrency

func WithMaxConcurrency(n int) ExecutorOption

WithMaxConcurrency sets the slot budget shared by every request's concurrently scheduled fields, which Stats reports as ConcurrencyInUse and ConcurrencyLimit. It does not cap goroutines: each concurrently scheduled field or list element runs on its own goroutine and takes a slot only if one is free, because a field waiting for a slot would hold back the DataLoader wave its siblings are parked in. What bounds the work of one request is its size, so bound that with WithMaxComplexity, WithQueryCost and WithMaxResponseBytes. Zero disables concurrent resolution: every field runs inline, in order. The default is 4 × GOMAXPROCS.

func WithMaxDepth

func WithMaxDepth(n int) ExecutorOption

WithMaxDepth rejects operations whose selection nesting exceeds n. Zero, the default, means unlimited.

__schema and __type count as one level, whatever they select, here and in WithMaxComplexity and QueryCost. These limits are set against an API's own queries, and the introspection query IDEs and client generators send nests its type references seven levels deep, so charging it would lock the tools out of any API that sets them. Introspection is bounded instead by graphql-js's MaxIntrospectionDepth rule: a path may pass through at most two of fields, interfaces, possibleTypes and inputFields. A public API should still DisableIntrospection.

func WithMaxErrors

func WithMaxErrors(n int) ExecutorOption

WithMaxErrors bounds how many errors one response carries. Past n, field errors are dropped, and a single ERROR_LIMIT_EXCEEDED error is appended in their place; the data is unaffected. A dropped error is not presented or given a path, except that concurrent fields racing the limit may be presented before they are dropped, so a presenter that logs can run a few more than n times. Errors that explain why a request stopped (cancellation, timeout, an oversized response) are always kept. Request errors from parsing and validation are cut the same way. Which field errors are kept is the order they were recorded in, which for concurrent fields is not deterministic. Zero means unlimited. The default is 1000: generous enough that a list with a failed field on each of hundreds of rows keeps every error, bounded enough that a million failing elements do not become a million errors in memory.

func WithMaxNesting

func WithMaxNesting(n int) ExecutorOption

WithMaxNesting caps how deeply a document's selection sets, or its input value literals, may nest as written: { a { b } } and [[1]] are both 2. A deeper document is refused with GRAPHQL_PARSE_FAILED before validation, whose ValuesOfCorrectType check does work at every level proportional to what is below it. It is not WithMaxDepth: that counts an operation's depth after fragments are expanded, this counts the text. Zero means unlimited. The default is 100.

func WithMaxResponseBytes

func WithMaxResponseBytes(n int64) ExecutorOption

WithMaxResponseBytes bounds the size of each response's data: one query or mutation, or one subscription event. A response over the limit has null data and a single RESPONSE_TOO_LARGE error in place of any other errors, and execution stops resolving fields once it sees the limit passed.

A response is rejected when its finished data is over the limit, or when the limit was passed at any point while executing: execution was cut short from then on, so the result is rejected even if null bubbling later discarded the bytes that passed it. While executing, a field other than __typename is checked before it is written, so each buffer being written concurrently can pass the limit by about one field's output before execution notices. The errors list is not counted. Zero means unlimited. The default is 64 MiB, which measured no cost distinguishable from no limit.

func WithMaxTokens

func WithMaxTokens(n int) ExecutorOption

WithMaxTokens caps the tokens a document may have: parsing stops at the limit and the request is refused with GRAPHQL_PARSE_FAILED. Parsing and validation run before WithMaxDepth, WithMaxComplexity and WithQueryCost can, on every document, valid or not, and gqlparser's default rules are not all linear in it, so this and WithMaxNesting are what bound that work. Zero means unlimited, which leaves gqlparser's recursive parser bounded only by the request body: about a million levels of [ overflows the goroutine stack, which no recover catches, and the transports' 1 MiB default body keeps a document under that. The default is 15 000, gqlgen's and Apollo Router's.

func WithObjectAuthBatch

func WithObjectAuthBatch(n int) ExecutorOption

WithObjectAuthBatch bounds how many checks one AuthorizeObjects call carries; larger waves are split into sequential calls. The default of 50 matches OpenFGA's BatchCheck default, which is the shape most remote policy backends are tuned for.

Sequential is the word to read twice: the batches are issued one after another, so a remote policy's latency is paid once per batch and not once per list. A thousand rows at the default of 50 is twenty round trips -- 100ms of added latency at 5ms each, measured, and halving the batch doubles it (TestObjectAuthBatchesAreSequential). Size this against what one call costs in latency, not against what the backend can accept in one request.

func WithObjectAuthorizer

func WithObjectAuthorizer(a ObjectAuthorizer) ExecutorOption

WithObjectAuthorizer enables instance-level authorization. Without it, @authorizeObject describes positions and nothing is enforced.

func WithOperationInterceptor

func WithOperationInterceptor(is ...OperationInterceptor) ExecutorOption

WithOperationInterceptor registers operation interceptors. The first is the outermost.

func WithOperationTimeout

func WithOperationTimeout(d time.Duration) ExecutorOption

WithOperationTimeout bounds how long one operation may run: a query or mutation from the moment Execute is called, parsing and every interceptor included, or one subscription event, never a stream as a whole. Opening a subscription is not bounded either — its interceptors, authorization and source opener share the context the stream lives on, which a deadline would end. Each entry of an HTTP batch is its own operation, so a batch of n may take n times the timeout.

At the deadline the operation's context is done, so fields not yet started are skipped and resolvers that honour their context return; the response keeps the data already written, with OPERATION_TIMEOUT errors where work was cut. A resolver's own error is kept in that error's chain, with its extensions. It does not force a response out on time: the executor waits for every resolver it started, so one that ignores its context still holds the response until it returns.

Only this executor's own deadline is OPERATION_TIMEOUT. When a deadline the caller set ends execution, the executor reports REQUEST_CANCELLED, and a resolver's error for it is passed through unchanged. Zero, the default, means no timeout: the right bound depends on the workload, and a wrong default would cut short legitimate slow operations.

func WithPlanCache

func WithPlanCache(size int) ExecutorOption

WithPlanCache sets the number of parsed documents kept in the LRU cache. Zero disables caching. The default is 1024.

func WithPlanCacheBytes

func WithPlanCacheBytes(n int64) ExecutorOption

WithPlanCacheBytes bounds the query text the plan cache holds, evicting the least recently used documents to stay within it. Zero means no byte limit; the entry count from WithPlanCache still applies. The default is 16 MiB.

The budget is on query text because that is what can be counted cheaply, but what it actually bounds is memory: a parsed and validated document retained roughly 26 times its query text when measured on an alias-heavy query (other shapes will differ), so without this limit 1024 distinct valid 1 MiB queries of that shape hold tens of gigabytes. Ordinary queries are a few kilobytes, so the default entry count binds long before this does and only unusually large queries ever reach it. A query larger than the whole budget is not cached but still executes.

func WithQueryCost

func WithQueryCost(c QueryCost) ExecutorOption

WithQueryCost enables Shopify-style query cost. When c.Max > 0, operations above the cap are rejected before execution.

func WithRecover

func WithRecover(enabled bool) ExecutorOption

WithRecover controls whether resolver panics are converted into INTERNAL_SERVER_ERROR field errors, and Authorizer panics into the same error for the whole operation or event. A panic anywhere else in Execute or in opening a subscription -- an interceptor, the presenter, a scalar's unmarshal, an input setter, a TypeResolver, a lazy list's body -- costs the whole operation that same error and no data, and one in a subscription event costs that event. It is enabled by default; disable it only in tests that want panics to surface.

func WithRequestInterceptor

func WithRequestInterceptor(is ...RequestInterceptor) ExecutorOption

WithRequestInterceptor registers request interceptors. The first is the outermost, matching grpc.ChainUnaryInterceptor.

func WithSubscriptionInterceptor

func WithSubscriptionInterceptor(is ...SubscriptionInterceptor) ExecutorOption

WithSubscriptionInterceptor registers subscription interceptors. The first is the outermost.

type ExecutorStats

type ExecutorStats struct {
	// PlanCacheEntries and PlanCacheBytes are the documents the plan cache
	// holds and their total query text, the quantity WithPlanCacheBytes bounds.
	PlanCacheEntries int
	PlanCacheBytes   int64

	// PlanCacheHits and PlanCacheMisses count operations that were planned: a
	// hit found its compiled plan cached, a miss compiled one. A request
	// rejected by parsing, validation or a depth or complexity limit is
	// neither, and so is OperationKind, which transports call before Execute;
	// one refused by the query cost limit has already been planned and counts.
	// A document with too many conditional variables to cache its plans
	// (OperationStats.PlanUncacheable) is a miss on every request. A
	// subscription counts once, when it opens: its events reuse that plan.
	// Per operation this is the judgement OperationStats.CacheHit reports,
	// though CacheHit is repeated on every subscription event.
	PlanCacheHits   int64
	PlanCacheMisses int64

	// ConcurrencyInUse and ConcurrencyLimit are the resolver slots held right
	// now and the WithMaxConcurrency budget; both are zero when concurrency is
	// disabled.
	ConcurrencyInUse int
	ConcurrencyLimit int
}

ExecutorStats is a point-in-time view of an Executor's shared state, for exporting as metrics (ext/otel.ObserveExecutor does). Taking one is cheap and safe from any goroutine.

type ExtensionsProvider

type ExtensionsProvider interface {
	GraphQLExtensions() map[string]any
}

ExtensionsProvider may be implemented by error types to contribute entries to the "extensions" member of the presented GraphQL error.

type FieldContext

type FieldContext struct {
	Field  *ast.FieldDefinition
	Object *ast.Definition
	Args   any
	Parent any
	// contains filtered or unexported fields
}

FieldContext describes the field a resolver is executing. It is attached to the context passed to Resolve and ResolveArgs functions only: a Field or FieldArgs accessor takes no context, so nothing is attached for one, and a FieldInterceptor must use the FieldContext it is handed as a parameter rather than FieldFrom.

Args is the decoded argument struct, a pointer to the type the field was bound with. A FieldInterceptor may replace it with a pointer to a value of that same type, and the field then runs with the replacement; that is how an interceptor supplies an argument the client did not send, such as a default page size. Copy before changing: literal arguments are decoded once per plan and shared by every request with the same query text, so writing through the original would alter all of them.

func FieldFrom

func FieldFrom(ctx context.Context) *FieldContext

FieldFrom returns the FieldContext, or nil when ctx does not belong to a resolver invocation.

func (*FieldContext) ArgumentMap

func (fc *FieldContext) ArgumentMap(vars map[string]any) (map[string]any, error)

ArgumentMap returns this field's argument values as the request gave them, before binding; see SelectedField.ArgumentMap for what that means for variables, defaults and absent arguments. It is for a FieldInterceptor that enforces a rule over what the client sent, such as refusing a write to a read-only key or a filter on a hidden column: the decoded Args struct cannot say whether a key was omitted or sent as null, and the interceptor cannot name it. vars is OperationFrom(ctx).Variables. A nil or hand-built FieldContext has no arguments and returns nil.

func (*FieldContext) Path

func (fc *FieldContext) Path() Path

Path returns the response path of the field. The node is built here rather than when the FieldContext is, because most fields are never asked for it.

func (*FieldContext) Selection

func (fc *FieldContext) Selection() Selection

Selection returns the sub-selection requested beneath the field.

type FieldFunc

type FieldFunc func(ctx context.Context, parent any, args any) (any, error)

FieldFunc is the type-erased field executor seen by directives and field interceptors. args is the decoded argument struct pointer (*A) or nil for fields without a bound argument type.

type FieldHandler

type FieldHandler func(ctx context.Context) (any, error)

FieldHandler resolves a field.

type FieldInfo

type FieldInfo struct {
	Object string
	Field  string
	Alias  string
	// Resolver reports whether the field is bound with Resolve or
	// ResolveArgs, which may do I/O, rather than Field, which reads data it
	// was handed. A tracer that wants to see where time and queries go
	// spans these and skips the rest.
	Resolver bool
	// contains filtered or unexported fields
}

FieldInfo identifies a field to an observer. It is a value, and its path is materialized only if asked for, so the engine allocates nothing to observe a field until the observer itself spends something. It is not free in time: each observed field still pays two interface calls and a defer.

It deliberately carries neither the arguments nor the parent value: those are what a FieldContext is for, and wanting them means wanting a FieldInterceptor.

func (FieldInfo) Path

func (f FieldInfo) Path() Path

Path returns the response path of the field.

type FieldInterceptor

type FieldInterceptor interface {
	InterceptField(ctx context.Context, fc *FieldContext, next FieldHandler) (any, error)
}

FieldInterceptor wraps every field executor in the plan. It routes field results through any, so use it only to change or replace a result; to watch fields without doing that, use WithFieldObserver, which keeps them typed.

Read the field from the fc parameter. The FieldContext is attached to the context only for Resolve and ResolveArgs fields, so inside a pure field FieldFrom, PathFrom and SelectionFrom find nothing.

func ChainFieldInterceptors

func ChainFieldInterceptors(is ...FieldInterceptor) FieldInterceptor

ChainFieldInterceptors composes interceptors so that the first is outermost. A nil or empty list is a no-op interceptor.

type FieldInterceptorFunc

type FieldInterceptorFunc func(ctx context.Context, fc *FieldContext, next FieldHandler) (any, error)

FieldInterceptorFunc adapts a function to FieldInterceptor.

func (FieldInterceptorFunc) InterceptField

func (f FieldInterceptorFunc) InterceptField(ctx context.Context, fc *FieldContext, next FieldHandler) (any, error)

InterceptField implements FieldInterceptor.

type FieldObserver

type FieldObserver interface {
	// BeginField runs before the field. The context it returns is the one the
	// field's own resolver runs under, so a span the resolver starts nests
	// beneath one the observer started. It is not the context of the field's
	// sub-selection: children run under the context the field itself
	// received.
	BeginField(ctx context.Context, f FieldInfo) context.Context

	// EndField runs when the field's resolver returns, with the context this
	// observer's BeginField returned. For a composite field that is before its
	// sub-selection is written, so a span ended here covers the resolver and
	// not the children.
	EndField(ctx context.Context, f FieldInfo, err error)
}

FieldObserver watches every field, pure ones included, without seeing its value. Not seeing the value is what lets the engine leave each field on its typed write path: a FieldInterceptor, which may replace a result, cannot.

A field that never runs is never observed: __typename, a field whose arguments fail to decode, a field skipped because the request context was already cancelled, and a field an Authorizer's decision denied, nulled or zeroed without resolving it. A redacted field does run -- Redact rewrites the resolved value -- and is observed like any other.

Sibling resolver fields are scheduled concurrently, so both methods may be called from several goroutines at once; an observer that holds state of its own must synchronise it.

A panic inside BeginField or EndField is not recovered by WithRecover: BeginField runs before the field's recovery is in place, and EndField after it has finished, so that EndField can see a recovered panic as err.

type FieldOption

type FieldOption interface {
	// contains filtered or unexported methods
}

FieldOption declares one field of an Object binding.

func Field

func Field[P, R any](name string, fn func(P) R, opts ...FieldSchedule) FieldOption

Field binds a pure field: fn reads data from the parent without I/O. Pure fields are executed inline and never scheduled on their own goroutine. A composite list result may be spelled iter.Seq[E] or iter.Seq[*E] wherever it may be spelled []E or []*E.

func FieldArgs

func FieldArgs[P, A, R any](name string, fn func(P, A) R, opts ...FieldSchedule) FieldOption

FieldArgs binds a pure field that takes arguments decoded into A.

func Resolve

func Resolve[P, R any](name string, fn func(context.Context, P) (R, error), opts ...FieldSchedule) FieldOption

Resolve binds a resolver field that may perform I/O. Resolver fields are eligible for concurrent scheduling. A composite list result may be spelled iter.Seq[E] or iter.Seq[*E] wherever it may be spelled []E or []*E.

func ResolveArgs

func ResolveArgs[P, A, R any](name string, fn func(context.Context, P, A) (R, error), opts ...FieldSchedule) FieldOption

ResolveArgs binds a resolver field that takes arguments decoded into A. An Args[A] registration must be present in the same schema.

func Subscribe

func Subscribe[R any](name string, fn func(context.Context) (<-chan R, error), opts ...FieldSchedule) FieldOption

Subscribe binds a subscription root field to the function that opens its source event stream. Each value received becomes one response, with the value taking the place of the field's result, so the field's sub-selection is executed against it exactly as a query's would be.

fn is called once per operation. It should return promptly and do its work in a goroutine feeding the channel; closing the channel ends the subscription. An error returned here prevents the subscription from starting. A channel carries no error of its own, so a source that can fail mid-stream should carry the failure in its event type.

func SubscribeArgs

func SubscribeArgs[A, R any](name string, fn func(context.Context, A) (<-chan R, error), opts ...FieldSchedule) FieldOption

SubscribeArgs binds a subscription root field that takes arguments decoded into A. An Args[A] registration must be present in the same schema.

type FieldSchedule

type FieldSchedule func(*fieldSpec)

FieldSchedule tunes whether a field runs inline or concurrently. The name follows the gRPC CallOption style: it only affects scheduling.

func Concurrent

func Concurrent() FieldSchedule

Concurrent allows a Field, which is normally executed inline, to be scheduled concurrently. Use it for pure fields that are CPU-heavy.

func Inline

func Inline() FieldSchedule

Inline forces a Resolve field to run synchronously in the parent's goroutine even when siblings are scheduled concurrently.

type ID

type ID string

ID is the default Go representation of the GraphQL ID scalar.

type InputFieldOption

type InputFieldOption interface {
	// contains filtered or unexported methods
}

InputFieldOption declares one field of an Input or Args binding.

func InputField

func InputField[T, V any](name string, set func(*T, V)) InputFieldOption

InputField binds the input field or argument name to a setter on *T. V must be registered for the field's GraphQL type through a scalar, enum or input binding.

func OmittableField

func OmittableField[T, V any](name string, set func(*T, Omittable[V])) InputFieldOption

OmittableField is like InputField but the setter receives an Omittable that is set whenever the field was present or a default applied, even if its value is null.

func ZeroForNull

func ZeroForNull() InputFieldOption

ZeroForNull lets a Go type that cannot represent null back a nullable input position on this binding: absent, explicit null and the zero value all decode to the zero value.

The rule it relaxes is there for a reason -- a bool field cannot tell an omitted flag from a false one, and for a PATCH-style input those are different requests, which is what Omittable exists for. It is opt-in per binding so that the author states where the two genuinely mean the same thing, rather than the engine assuming it everywhere.

The case it was added for is an ORM filter. entgql emits VariantIDIsNil bool under variantIdIsNil: Boolean and reads false as "apply no predicate", so absent and false really are one value; one real schema has 11 035 such fields, in SDL its author does not hand-write.

It applies to the fields this binding derives from the struct. A field declared explicitly with InputField carries its own setter, so its Go type is already the author's choice.

type InputKey

type InputKey struct {
	Path []string
	Enum string
	Null bool
}

InputKey is one position a client supplied inside an argument that an Authorizer is asked about: the input-object keys from the argument down, list indices omitted. Enum holds the value when that position is an enum, because ordering and grouping name a column by enum value rather than by key. Null is set for an explicit null, which is a write where an absent key is not. Scalar values are never carried: a policy decides on schema identifiers, and the values are user data.

type Location

type Location struct {
	Line   int `json:"line"`
	Column int `json:"column"`
}

Location is a line/column position in the request document.

type Marshaler

type Marshaler[T any] interface {
	*T
	MarshalGQL(w io.Writer)
	UnmarshalGQL(v any) error
}

Marshaler is the encoding contract gqlgen defines and that ent-, velox- and gqlgen-generated leaf types implement: MarshalGQL writes the value's JSON encoding, UnmarshalGQL decodes an input value into the receiver. PT is *T; MarshalGQL may have either receiver.

type ObjectAuthorizer

type ObjectAuthorizer interface {
	AuthorizeObjects(ctx context.Context, checks []ObjectCheck) ([]Outcome, error)
}

ObjectAuthorizer decides whether a principal may see individual objects.

Batching is per list: a list of guarded values is drained and decided in one call before any element is written, split into several only by WithObjectAuthBatch. That is what keeps a policy backed by a remote decision point to one call per list rather than one per row. It is not per wave -- a guarded object reached through a field that is not a list is one call carrying one check, so a selection that reaches guarded objects through nested single-object fields issues one call per such field. Where that matters, model the position as a list or cache inside the implementation.

The returned slice is positional over checks and must have the same length; any other length is an error, because a short slice would silently allow the rows it does not cover. checks is only valid for the duration of the call: do not retain it, and do not reorder it in place, since the outcomes are mapped back by position in the order it was given.

type ObjectCheck

type ObjectCheck struct {
	Site   AuthSite
	Type   string
	Object any
}

ObjectCheck is one resolved object an ObjectAuthorizer decides on. Type is the concrete object type's name, which differs from Site.Coord when the field's declared type is an interface or a union.

type Omittable

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

Omittable distinguishes an input field that was absent from one that was explicitly set to null. Use it on PATCH-style input structs; Auto bindings and OmittableField both understand it.

func OmittableOf

func OmittableOf[T any](v T) Omittable[T]

OmittableOf returns an Omittable that is set to v.

func (Omittable[T]) IsSet

func (o Omittable[T]) IsSet() bool

IsSet reports whether the field was present in the input, even if its value was null.

func (Omittable[T]) MarshalJSON

func (o Omittable[T]) MarshalJSON() ([]byte, error)

MarshalJSON encodes the value, or null when unset.

func (Omittable[T]) Or

func (o Omittable[T]) Or(def T) T

Or returns the set value, or def when the field was absent.

func (*Omittable[T]) UnmarshalJSON

func (o *Omittable[T]) UnmarshalJSON(b []byte) error

UnmarshalJSON decodes into the value and marks it set. A field absent from the JSON never reaches this method, so it stays unset; an explicit null does, and is set with the zero value. That is the distinction Omittable exists for, and it lets an input struct be filled from JSON (a stored definition, a test fixture) as well as from a GraphQL request.

func (Omittable[T]) Value

func (o Omittable[T]) Value() T

Value returns the decoded value, or the zero value when unset.

func (Omittable[T]) ValueOK

func (o Omittable[T]) ValueOK() (T, bool)

ValueOK returns the decoded value together with whether it was set.

type OperationContext

type OperationContext struct {
	Operation     *ast.OperationDefinition
	Doc           *ast.QueryDocument
	RawQuery      string
	OperationName string
	Variables     map[string]any
	Stats         OperationStats
	// contains filtered or unexported fields
}

OperationContext describes the operation being executed. It is available from any context derived from the request through OperationFrom.

func OperationFrom

func OperationFrom(ctx context.Context) *OperationContext

OperationFrom returns the OperationContext, or nil outside a request.

func (*OperationContext) ActualCost

func (oc *OperationContext) ActualCost() (int, bool)

ActualCost returns the cost summed from the fields this operation really resolved, and whether it was measured. It is measured only when the executor was built with a QueryCost whose Actual is set, and only after execution.

func (*OperationContext) AuthShape

func (oc *OperationContext) AuthShape() *AuthShape

AuthShape returns what this operation touches, or nil when it touches nothing that declares an authorization requirement.

func (*OperationContext) Complexity

func (oc *OperationContext) Complexity() int

Complexity returns the static field count computed by operationMetrics over the document, copied onto the plan at compile time.

func (*OperationContext) Cost

func (oc *OperationContext) Cost() int

Cost returns the Shopify-style query cost. Without WithQueryCost it uses a default list size of 1 so callers can still observe a number.

func (*OperationContext) Depth

func (oc *OperationContext) Depth() int

Depth returns the maximum selection nesting computed by operationMetrics over the document, copied onto the plan at compile time.

func (*OperationContext) Get

func (oc *OperationContext) Get(key any) (any, bool)

Get retrieves a value stored with Set.

func (*OperationContext) GetOrSet

func (oc *OperationContext) GetOrSet(key, value any) (actual any, loaded bool)

GetOrSet returns the existing value for key, or stores and returns value when the key is absent; loaded reports which happened. It is the atomic form of Get followed by Set.

Request-scoped extensions must use it rather than Get-then-Set: concurrent resolvers reach their first Load at the same time, and a check-then-act pair lets every one of them install its own state, so only the last write survives while the others keep using orphaned copies.

func (*OperationContext) Set

func (oc *OperationContext) Set(key, value any)

Set stores an arbitrary value scoped to the operation, for use by interceptors and extensions.

Use OperationContext.GetOrSet to install request-scoped state. Get followed by Set is a check-then-act pair, and sibling resolvers running concurrently reach their first call together: each one then sees no value, installs its own, and state meant to be shared is silently duplicated. For a DataLoader that means the pending queue and the cache split and batching degrades to N+1, with no error and no race report -- the repository ships an analyzer (lint/cmd/gqlvet) for that pair because -race does not find it. Set is for a value the caller knows it is the only writer of.

func (*OperationContext) SetExtension

func (oc *OperationContext) SetExtension(key string, value any)

SetExtension records a response-level extension (Netflix / Apollo tracing, GitHub rate-limit metadata). Values are copied onto the Response after the operation completes.

func (*OperationContext) Waves

func (oc *OperationContext) Waves() *WaveCoordinator

Waves returns the coordinator for this operation. It is nil when the operation is not running under an Executor, in which case extensions fall back to their own scheduling.

type OperationHandler

type OperationHandler func(ctx context.Context, oc *OperationContext) *Response

OperationHandler executes a compiled operation.

type OperationInterceptor

type OperationInterceptor interface {
	InterceptOperation(ctx context.Context, oc *OperationContext, next OperationHandler) *Response
}

OperationInterceptor wraps execution of a parsed and planned operation. Use it for complexity limits, tracing spans and metrics.

func ChainOperationInterceptors

func ChainOperationInterceptors(is ...OperationInterceptor) OperationInterceptor

ChainOperationInterceptors composes interceptors so that the first is outermost.

type OperationInterceptorFunc

type OperationInterceptorFunc func(ctx context.Context, oc *OperationContext, next OperationHandler) *Response

OperationInterceptorFunc adapts a function to OperationInterceptor.

func (OperationInterceptorFunc) InterceptOperation

func (f OperationInterceptorFunc) InterceptOperation(ctx context.Context, oc *OperationContext, next OperationHandler) *Response

InterceptOperation implements OperationInterceptor.

type OperationStats

type OperationStats struct {
	Start    time.Time
	CacheHit bool
	// PlanUncacheable reports that this document references more Boolean
	// @skip/@include variables than the variant cache holds, so its plan is
	// compiled on every request and CacheHit can never become true. It is
	// reported separately because a false CacheHit alone is indistinguishable
	// from the first request for a perfectly cacheable document.
	PlanUncacheable bool
}

OperationStats records timing and cache information for observability.

type Outcome

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

Outcome is what the executor does with a site. The zero Outcome allows, so a Decision an Authorizer leaves untouched changes nothing.

func Allow

func Allow() Outcome

Allow resolves the field normally.

func Deny

func Deny(permission, resource string) Outcome

Deny refuses the field. The message follows AIP-211: it reveals neither the value nor whether the resource exists, because choosing between PERMISSION_DENIED and NOT_FOUND is itself an existence oracle.

That holds for the wording, not for the error being there. For a row looked up by id, a Deny is an error a missing id does not get, and that difference is the oracle; where existence must not show, an ObjectAuthorizer answers Null at a nullable position (AuthSite.NonNull says which) and Drop in a list.

func Drop

func Drop() Outcome

Drop omits the value from its enclosing list. It is valid only for an instance site at a list element position; dropping renumbers the indices that follow, so a later denial is reported at the index the client actually sees.

func Null

func Null() Outcome

Null writes null without resolving the field.

func Redact

func Redact(fn func(any) any) Outcome

Redact resolves the field and rewrites the result.

func RedactRow

func RedactRow(fn func(ctx context.Context, parent, value any) any) Outcome

RedactRow is Redact decided per row: fn receives the field's context, the object the field belongs to (as its Object binding holds it, a *T) and the resolved value, and returns what is written. It is what "a customer sees their own email, support sees everyone's masked" needs, which a Decision cannot say by itself because it is made before any row exists.

It is valid wherever Redact is -- a leaf field -- and runs once per row the field resolves on, so it must be cheap and must not fail open: return the masked value when the row cannot be judged.

func Zero

func Zero() Outcome

Zero writes the zero value of the field's type without resolving it. It is how a non-null field is withheld without null-bubbling its parent.

type Path

type Path []PathElem

Path locates a value in the response data, as defined by the GraphQL specification's error format.

func PathFrom

func PathFrom(ctx context.Context) Path

PathFrom returns the response path of the executing resolver, or nil.

func (Path) MarshalJSON

func (p Path) MarshalJSON() ([]byte, error)

MarshalJSON renders the path as a JSON array of strings and integers.

func (Path) String

func (p Path) String() string

String renders the path in dotted notation, for example user.posts[2].title.

type PathElem

type PathElem struct {
	Key     string
	Index   int
	IsIndex bool
}

PathElem is one segment of a response path: either a field key or a list index.

type QueryCost

type QueryCost struct {
	// Max is the maximum allowed cost. Zero means unlimited.
	Max int
	// DefaultListSize is used when a list field has no first/last.
	// Zero is treated as 10.
	DefaultListSize int
	// ListArguments names the arguments that bound a connection or list.
	// Empty means first, last.
	ListArguments []string
	// FieldWeight overrides the default weight of 1 for a schema
	// coordinate such as "Query.search".
	FieldWeight map[string]int
	// Report writes extensions.cost on every response, including operations
	// rejected by Max. Operations refused by WithMaxDepth/WithMaxComplexity
	// are rejected before a plan exists and so carry no cost.
	Report bool
	// Connections prices a Relay connection by the page size it asked for.
	// first/last sit on the connection field, one level above the edges list
	// they bound, so without this the arguments are invisible to the cost and
	// every connection is charged DefaultListSize whether it asked for one
	// element or a thousand.
	//
	// It is off by default because turning it on changes the number an
	// existing deployment has set its Max against.
	Connections bool
	// Actual also reports actualQueryCost, summed from the fields that were
	// really resolved rather than from assumed list sizes. Requested cost has
	// to guess how long a list will be; actual cost counts it, so the two
	// together say whether DefaultListSize is set anywhere near reality.
	//
	// It costs one integer add per resolved field on the write path, which is
	// why it is separate from Report rather than implied by it.
	Actual bool
}

QueryCost is a Shopify / GitHub-style cost model computed from the compiled plan and variable values, before resolvers run.

A field costs FieldWeight[Type.field] (default 1) plus the cost of its sub-selection multiplied by the list size. List size is the largest positive ListArguments value sent (default "first", "last"), or DefaultListSize when the field returns a list and neither argument is present. __typename is free, and so are __schema and __type with all they select: see WithMaxDepth.

type Request

type Request struct {
	Query         string          `json:"query"`
	OperationName string          `json:"operationName,omitempty"`
	Variables     json.RawMessage `json:"variables,omitempty"`
	Extensions    map[string]any  `json:"extensions,omitempty"`
}

Request is a GraphQL request as delivered by a transport.

type RequestHandler

type RequestHandler func(ctx context.Context, req *Request) *Response

RequestHandler executes a request; interceptors call it to continue.

type RequestInterceptor

type RequestInterceptor interface {
	InterceptRequest(ctx context.Context, req *Request, next RequestHandler) *Response
}

RequestInterceptor wraps the whole request, before parsing. Use it for authentication, logging, persisted query lookups and response extensions. The shape follows gRPC's UnaryServerInterceptor: observe, call next, observe.

func ChainRequestInterceptors

func ChainRequestInterceptors(is ...RequestInterceptor) RequestInterceptor

ChainRequestInterceptors composes interceptors so that the first is outermost. A nil or empty list is a no-op interceptor.

type RequestInterceptorFunc

type RequestInterceptorFunc func(ctx context.Context, req *Request, next RequestHandler) *Response

RequestInterceptorFunc adapts a function to RequestInterceptor.

func (RequestInterceptorFunc) InterceptRequest

func (f RequestInterceptorFunc) InterceptRequest(ctx context.Context, req *Request, next RequestHandler) *Response

InterceptRequest implements RequestInterceptor.

type Requirement

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

Requirement is an OR of ANDs: satisfied when every scope in any one group is held. The zero Requirement is satisfied by everyone, so a field that declares nothing is not accidentally locked.

The shape mirrors Apollo's @requiresScopes(scopes: [[String!]!]!), which is the vocabulary most tooling already reads.

func NewRequirement

func NewRequirement(anyOf ...[]string) Requirement

NewRequirement builds a Requirement from its groups. Each group is cloned to prevent external mutations from changing the requirement after construction, which is important for Requirement's use on the concurrent request path and its immutability contract everywhere else.

func (Requirement) And

And returns the requirement satisfied only when both r and o are. It is the cross product of their groups, each group sorted and deduplicated. The zero Requirement admits everyone, so it is the identity.

func (Requirement) IsZero

func (r Requirement) IsZero() bool

IsZero reports whether the requirement admits everyone.

func (Requirement) Satisfied

func (r Requirement) Satisfied(held map[string]bool) bool

Satisfied reports whether held covers any one of the groups.

func (Requirement) Scopes

func (r Requirement) Scopes() []string

Scopes returns every scope the requirement names, sorted and deduplicated, so a caller can load them from a policy source in one round trip.

type Response

type Response struct {
	Data       []byte
	Errors     []*Error
	Extensions map[string]any
	// contains filtered or unexported fields
}

Response is the result of executing a Request.

Data holds the already-encoded "data" member, or nil when the request failed before execution began. Call Release once the response has been written to return its buffer to the pool.

func (*Response) HasRequestErrors

func (r *Response) HasRequestErrors() bool

HasRequestErrors reports whether the request failed before execution, in which case the response carries errors and no data.

func (*Response) MarshalJSON

func (r *Response) MarshalJSON() ([]byte, error)

MarshalJSON renders the response envelope as a single byte slice.

func (*Response) Release

func (r *Response) Release()

Release returns the response buffer to the pool. Data must not be used afterwards. Calling Release more than once is a no-op.

func (*Response) WriteTo

func (r *Response) WriteTo(w io.Writer) (int64, error)

WriteTo writes the JSON response envelope to w with errors, data and extensions in that order, omitting members that are empty. Data is written straight from the execution buffer without an intermediate copy.

It returns an error, and writes nothing, when an extension value on the response or on any error does not marshal. Nothing is written rather than a truncated envelope, so a caller that checks the error can still send something of its own -- but the HTTP transports discover it after the status header is on the wire and can only log, which leaves the client an empty body. See Error.WithExtension.

type Root

type Root struct{}

Root is the parent value passed to fields of the Query, Mutation and Subscription root types.

type Schema

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

Schema is a validated GraphQL schema with all bindings resolved. It is immutable and safe for concurrent use.

func NewSchema

func NewSchema(src Source, opts ...SchemaOption) (*Schema, error)

NewSchema parses the SDL, applies every binding option and validates that the bindings cover the schema with compatible Go shapes. All problems are reported together in the returned error.

func (*Schema) AST

func (s *Schema) AST() *ast.Schema

AST returns the parsed schema definition.

func (*Schema) IntrospectionEnabled

func (s *Schema) IntrospectionEnabled() bool

IntrospectionEnabled reports whether __schema and __type may be queried.

type SchemaOption

type SchemaOption interface {
	// contains filtered or unexported methods
}

SchemaOption configures a Schema. All binding constructors return one.

func Args

func Args[A any](fields ...InputFieldOption) SchemaOption

Args declares how a field's arguments are decoded into the Go struct A. The same A may be shared by several fields with identical arguments.

func Directive

func Directive(name string, fn func(next FieldFunc) FieldFunc) SchemaOption

Directive binds a schema directive that has no arguments. For every field (or every field of an object) carrying @name, fn wraps the field executor. Directives with arguments use DirectiveArgs.

func DirectiveArgs

func DirectiveArgs[A any](name string, fn func(next FieldFunc, args A) FieldFunc) SchemaOption

DirectiveArgs binds a schema directive whose arguments decode into A. Register Args[A] when A is not struct{}. Locations FIELD_DEFINITION and OBJECT are applied; an OBJECT directive wraps every field of that type. An interface's field has no executor to wrap, so a bound directive written there must also be written on each implementing object's field, and the build fails when it is not.

func DisableIntrospection

func DisableIntrospection() SchemaOption

DisableIntrospection rejects operations that select __schema or __type. __typename remains available.

func Enum

func Enum[T comparable](name string, values map[T]string) SchemaOption

Enum binds the GraphQL enum name to the Go type T using an explicit mapping from Go values to SDL enum value names. Every SDL value must be mapped exactly once.

func EnumMarshaler

func EnumMarshaler[T any, PT Marshaler[T]](name string) SchemaOption

EnumMarshaler binds the GraphQL enum name to a Go type that encodes itself.

It is for enum types Enum cannot take: an ORM's order field is a struct holding a cursor function, not comparable, whose values are unexported and constructed only by UnmarshalGQL. Output is checked against the enum's declared values, so a MarshalGQL naming an undeclared one is a field error, as it is for Enum.

func Input

func Input[T any](name string, fields ...InputFieldOption) SchemaOption

Input binds the GraphQL input object type name to the Go struct T.

func Interface

func Interface[T any](name string, opts ...AbstractOption) SchemaOption

Interface binds the GraphQL interface name to the Go type T, typically a Go interface. Binding is optional: unbound abstract types resolve their concrete type from the dynamic Go type of the value.

func IntrospectDeprecatedInputValues

func IntrospectDeprecatedInputValues() SchemaOption

IntrospectDeprecatedInputValues makes introspection list deprecated input fields and deprecated arguments when the client does not say whether it wants them.

The specification hides them unless the query passes includeDeprecated: true, and graphql-js's getIntrospectionQuery passes nothing for inputFields and args. gqlgen returned them whatever was asked, so a client generated against it (a codegen that introspects the live server) lists every deprecated input field it still sends; against a server that follows the specification those fields disappear from the generated types without an error. This option changes the default the introspection schema declares, not the rule: a client that passes includeDeprecated: false still gets them hidden.

func MarkerDirective

func MarkerDirective(name, scope string) SchemaOption

MarkerDirective declares a no-argument SDL directive whose presence alone is a requirement for scope, which is the shape of Apollo's @authenticated: the field is guarded, and what the guard means is whatever the Authorizer takes that scope to be.

It composes like any other spelling. A coordinate carrying both a marker and a scope directive must satisfy both, which is how @authenticated and @requiresScopes sit together.

func Mutation

func Mutation(fields ...FieldOption) SchemaOption

Mutation binds fields of the schema's mutation root.

func Object

func Object[T any](name string, fields ...FieldOption) SchemaOption

Object binds the GraphQL object type name to the Go type T. Field functions receive *T (or T, which costs a copy). T must not itself be a pointer type. Several Object calls for the same name are merged.

func Options

func Options(opts ...SchemaOption) SchemaOption

Options combines schema options into one. Generated Bindings use it so a group can return a single SchemaOption.

func Query

func Query(fields ...FieldOption) SchemaOption

Query binds fields of the schema's query root, which is named Query unless the schema declaration says otherwise.

func RequireAuthCoverage

func RequireAuthCoverage() SchemaOption

RequireAuthCoverage fails NewSchema for any field that declares neither an authorization requirement nor @public. It is opt-in because it breaks every schema that has not adopted it.

The failure it prevents is not a field someone forgot to guard but a type nobody noticed arriving: a generated aggregate surface keyed by a resource no role grants and no role restricts is unguarded in both directions.

func RequirementDirective

func RequirementDirective(name, arg string, shape ScopeShape) SchemaOption

RequirementDirective declares an additional SDL directive read into a Requirement, so a schema that spells its requirements @auth(requires:) is enforced without renaming every site. @requiresScopes stays active, so one schema can carry a legacy spelling for existing coordinates and the Apollo one for new work; a coordinate carrying both must satisfy both.

func Scalar

func Scalar[T any](name string, marshal func(*Writer, T) error, unmarshal func(any) (T, error)) SchemaOption

Scalar binds the GraphQL scalar name to the Go type T. marshal writes the output representation; unmarshal coerces an input value, which is one of string, bool, json.Number, int64, float64, []any or map[string]any. Registering a Go type for a built-in scalar (for example ID as uuid.UUID) adds to the built-in mappings rather than replacing them.

unmarshal receives the same representation whether the value arrived as a literal in the query or as a variable -- a number is a json.Number carrying its original text either way, so a decimal or an id keeps the digits the client sent.

**Write it pure and cheap.** It runs twice for a value supplied as a variable: once from coerceVariables, which decodes to find out whether the value is acceptable and discards the result so a variable error is reported before execution starts, and once when the argument is decoded for real. A literal runs it once.

func ScalarMarshaler

func ScalarMarshaler[T any, PT Marshaler[T]](name string) SchemaOption

ScalarMarshaler binds the custom scalar name to a Go type that encodes itself -- a Relay cursor, a decimal, anything a generator emitted with MarshalGQL and UnmarshalGQL. It is Scalar with the two functions taken from the type.

func Subscription

func Subscription(fields ...FieldOption) SchemaOption

Subscription binds fields of the schema's subscription root.

func SubscriptionRootDirective

func SubscriptionRootDirective(names ...string) SchemaOption

SubscriptionRootDirective declares that the named directives, bound with Directive or DirectiveArgs for ordinary fields, are enforced on subscription root fields by a SubscriptionInterceptor the application installs.

A bound directive cannot wrap a subscription root field (the field is served by its stream, not its resolver), so a schema that carries one there is refused rather than built with a check that never runs. An application that enforces the directive itself when a subscription opens says so here, and the refusal is lifted for exactly those names. Naming a directive that is not bound is an error: a typo must not switch the refusal off for nothing.

func Time

func Time(name string) SchemaOption

Time binds the scalar name to time.Time as an RFC 3339 string, the wire format of gqlgen's built-in Time, so a service moving over answers with the same bytes: the value's own offset is kept, and the fraction is written only as far as it is non-zero (time.RFC3339Nano). *time.Time binds with it, and nil is null.

Two of gqlgen's choices are not taken. The zero time is written as "0001-01-01T00:00:00Z", not null, because null at a Time! position is an error the value did not cause; a column that can be empty is a *time.Time. And input must be RFC 3339: gqlgen also reads "" as the zero time, which hides a missing value, and "2006-01-02 15:04:05" as UTC, which is a wall-clock time each client means in its own zone.

func Union

func Union[T any](name string, opts ...AbstractOption) SchemaOption

Union binds the GraphQL union name to the Go type T.

type ScopeShape

type ScopeShape uint8

ScopeShape says how a requirement directive's argument composes into a Requirement. There is no valid zero value: reading a flat [String!] as AND when the author meant OR silently widens access, and the SDL cannot tell the two apart -- @auth(requires: ["a","b"]) is the same text either way.

const (
	// ScopesNested reads [[String!]!] as an OR of ANDs, which is Apollo's
	// @requiresScopes.
	ScopesNested ScopeShape = iota + 1
	// ScopesAllOf reads [String!] as one AND group: every scope is required.
	ScopesAllOf
	// ScopesAnyOf reads [String!] as one group per scope: any one suffices.
	ScopesAnyOf
)

type SelectedField

type SelectedField struct {
	Name  string
	Alias string
	// Args holds the decoded argument struct pointer when the arguments are
	// literal, or nil when they depend on variables or the field has none.
	// ArgumentMap answers both cases.
	Args any
	// contains filtered or unexported fields
}

SelectedField describes one requested field.

func (SelectedField) ArgumentMap

func (f SelectedField) ArgumentMap(vars map[string]any) (map[string]any, error)

ArgumentMap returns the field's argument values as the request gave them, before binding: variables substituted from vars, defaults applied, and an absent argument without a default left out. A number is a json.Number whether it was written in the query or sent as a variable, because the executor decodes variables with precision kept. It is nil for a field with no arguments.

It is for code that cannot know the field's argument struct, such as an ORM planning which rows the selection will read. vars is the operation's variables, OperationFrom(ctx).Variables; the plan is shared by every request with the same query text, so it holds none.

func (SelectedField) Selection

func (f SelectedField) Selection() Selection

Selection returns the sub-selection requested beneath the field, empty for a leaf. Two aliases of one field are two SelectedFields, each with its own sub-selection, where Sub returns only the first.

func (SelectedField) Withheld

func (f SelectedField) Withheld(ctx context.Context) bool

Withheld reports whether authorization withholds the field in this request: the Authorizer's Outcome for it is Deny, Null or Zero, so it is never resolved and nothing beneath it is read. Code that loads data for a selection ahead of the resolvers -- an ORM eager-loading edges -- skips such a field, so a subtree the caller may not see costs no query and never enters memory. ctx is the executing resolver's or interceptor's.

A Redact outcome resolves the field and is not withheld. Instance-level decisions (ObjectAuthorizer) are made per object as values arrive, after loading is planned, and are not reflected here.

type Selection

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

Selection is a read-only view of the fields requested beneath a field. It lets resolvers fetch only what the client asked for.

func SelectionFrom

func SelectionFrom(ctx context.Context) Selection

SelectionFrom returns the sub-selection of the executing resolver. Outside a resolver it returns an empty Selection.

func (Selection) Collect

func (s Selection) Collect[T any](fn func(SelectedField) T) []T

Collect builds a slice from the selected fields. It is a generic method (Go 1.27) so callers write sel.Collect(func(f SelectedField) string { ... }).

func (Selection) Fields

func (s Selection) Fields() iter.Seq[SelectedField]

Fields iterates over the selected fields. For abstract parents, fields from all possible types are visited once per response key.

func (Selection) ForType

func (s Selection) ForType(typeName string) Selection

ForType returns what is selected when the value is of the named object type: for an abstract parent, the fields that apply to that type, fragments on it and on the interfaces it implements included; empty when typeName is not one of the parent's possible types. A concrete parent's selection already applies to its one type and is returned as it is.

func (Selection) Has

func (s Selection) Has(name string) bool

Has reports whether a field with the given name is selected, on any possible type when the parent is abstract.

func (Selection) IsEmpty

func (s Selection) IsEmpty() bool

IsEmpty reports whether nothing is selected.

func (Selection) Sub

func (s Selection) Sub(name string) (Selection, bool)

Sub returns the selection beneath the named field. The second result is false when the field is not selected or is a leaf.

type SiteKind

type SiteKind uint8

SiteKind says what kind of position needs an authorization decision.

const (
	// SiteOutput is a selected output field. A SiteOutput site can carry a
	// zero Requires: buildAuthShape adds one for a field that has argument
	// sites but no requirement of its own, purely to route the field through
	// enforceAuth, so an Authorizer iterating output sites will see it.
	SiteOutput SiteKind = iota
	// SiteObject is a whole object type, checked once per instance rather
	// than once per field of it.
	SiteObject
	// SiteFilterArg is an argument marked @authorizeInput(kind: FILTER) on
	// an object field. A restricted field must not be filterable either, or
	// its value leaks by bisection without ever being selected. Only Allow
	// and Deny are valid outcomes for it.
	SiteFilterArg
	// SiteInputWrite is an argument marked @authorizeInput(kind: WRITE) on
	// an object field. Only Allow and Deny are valid outcomes for it.
	SiteInputWrite
	// SiteInstance is one resolved object of an @authorizeObject type, about
	// to be written at this field position. Unlike every other site it is
	// decided during execution by an ObjectAuthorizer, because the values do
	// not exist when a Decision is built.
	SiteInstance
)

type Source

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

Source supplies SDL to NewSchema.

func SDL

func SDL(s string) Source

SDL returns a Source holding one SDL document.

func SDLBytes

func SDLBytes(b []byte) Source

SDLBytes returns a Source holding one SDL document.

func SDLFS

func SDLFS(fsys fs.FS, patterns ...string) Source

SDLFS returns a Source that reads every file in fsys matching one of the glob patterns. Files are read when NewSchema runs and keep their names for error positions.

func Sources

func Sources(srcs ...Source) Source

Sources concatenates several sources. Each fs.FS is read independently, so SDLFS from different file systems can be combined.

type SubscribeError

type SubscribeError struct {
	Response *Response
}

SubscribeError reports that a subscription could not be started: the document failed to parse or validate, variables were invalid, or the binding's stream opener returned an error. Response holds the errors in the form a transport sends them, which for the streaming protocols is a protocol-level error rather than a payload.

func (*SubscribeError) Error

func (e *SubscribeError) Error() string

func (*SubscribeError) Unwrap

func (e *SubscribeError) Unwrap() []error

Unwrap exposes the response's errors to errors.Is and errors.As, so a caller reaches a *Error or a sentinel the ordinary way instead of having to know to walk Response.Errors by hand. It returns several because a subscription can be refused for more than one reason at once.

type SubscriptionHandler

type SubscriptionHandler func(ctx context.Context, oc *OperationContext) (<-chan *Response, error)

SubscriptionHandler opens a subscription's stream of responses.

type SubscriptionInterceptor

type SubscriptionInterceptor interface {
	InterceptSubscription(ctx context.Context, oc *OperationContext, next SubscriptionHandler) (<-chan *Response, error)
}

SubscriptionInterceptor wraps the opening of a subscription stream. It is the stream-shaped counterpart of OperationInterceptor, following gRPC's split between UnaryInterceptor and StreamInterceptor.

Operation interceptors see each event of a live subscription, which is too late to refuse one: by then the source is open. Refusing a subscription is this interceptor's job.

func ChainSubscriptionInterceptors

func ChainSubscriptionInterceptors(is ...SubscriptionInterceptor) SubscriptionInterceptor

ChainSubscriptionInterceptors composes interceptors so that the first is outermost. A nil or empty list is a no-op interceptor.

type SubscriptionInterceptorFunc

type SubscriptionInterceptorFunc func(ctx context.Context, oc *OperationContext, next SubscriptionHandler) (<-chan *Response, error)

SubscriptionInterceptorFunc adapts a function to SubscriptionInterceptor.

func (SubscriptionInterceptorFunc) InterceptSubscription

func (f SubscriptionInterceptorFunc) InterceptSubscription(ctx context.Context, oc *OperationContext, next SubscriptionHandler) (<-chan *Response, error)

InterceptSubscription implements SubscriptionInterceptor.

type WaveCoordinator

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

WaveCoordinator lets request-scoped batching extensions take part in the executor's dispatch of concurrent waves. A wave is announced before sibling tasks are launched, so a resolver that blocks on batched work cannot dispatch before its siblings have queued theirs — the GraphQL equivalent of DataLoader.dispatch at the end of a tick.

The protocol for an extension is:

w := graphql.OperationFrom(ctx).Waves()
w.OnReady(flush)  // once per operation
w.Park()          // before blocking on batched work
defer w.Unpark()

When nothing in the operation is left running -- every task begun, and each either parked or waiting for tasks of its own -- the registered callbacks run so the keys queued by all of them dispatch together. The loader package is built on this; most applications use that instead.

A nil *WaveCoordinator is valid and does nothing, which is what Waves returns outside an Execute call.

func (*WaveCoordinator) OnReady

func (w *WaveCoordinator) OnReady(fn func())

OnReady registers a callback to run when a wave is ready to dispatch. Callbacks are kept for the life of the operation and must be safe to call when there is nothing queued.

func (*WaveCoordinator) Park

func (w *WaveCoordinator) Park()

Park reports that the caller is about to block on batched work. When that leaves nothing running and no announced task still to begin, every key the operation can queue before it blocks has been queued, so the ready callbacks run.

func (*WaveCoordinator) Unpark

func (w *WaveCoordinator) Unpark()

Unpark reports that the caller is no longer blocked.

type Writer

type Writer jsonw.Writer

Writer is handed to scalar marshalers to emit exactly one JSON value. Conversions to and from the internal writer are free, so custom scalars add no allocation on the hot path.

func (*Writer) Bool

func (w *Writer) Bool(v bool)

Bool writes true or false.

func (*Writer) Float64

func (w *Writer) Float64(v float64) error

Float64 writes a JSON number. It returns an error for NaN and infinities.

func (*Writer) Int64

func (w *Writer) Int64(v int64)

Int64 writes a JSON number.

func (*Writer) Null

func (w *Writer) Null()

Null writes null.

func (*Writer) Raw

func (w *Writer) Raw(b []byte)

Raw writes an already-encoded JSON value. The caller is responsible for its validity.

func (*Writer) String

func (w *Writer) String(s string)

String writes a JSON string.

func (*Writer) Uint64

func (w *Writer) Uint64(v uint64)

Uint64 writes a JSON number.

Directories

Path Synopsis
cmd
gqlc command
Command gqlc generates GraphQL bindings from SDL.
Command gqlc generates GraphQL bindings from SDL.
Package codegen generates models, argument structs, a Resolver interface and bindings from SDL.
Package codegen generates models, argument structs, a Resolver interface and bindings from SDL.
examples
blog
Package blog is the composition root for the layered example.
Package blog is the composition root for the layered example.
blog/cmd/server command
Command server serves the example blog schema on every transport:
Command server serves the example blog schema on every transport:
blog/graph
Package graph wires the generated GraphQL schema and its bindings.
Package graph wires the generated GraphQL schema and its bindings.
blog/graph/model
Package model holds the types generated from the GraphQL schema.
Package model holds the types generated from the GraphQL schema.
blog/internal/app
Package app holds the use cases: the work that is neither storage nor protocol.
Package app holds the use cases: the work that is neither storage nor protocol.
blog/internal/domain
Package domain holds the blog's business entities.
Package domain holds the blog's business entities.
blog/internal/repository
Package repository is the in-memory store standing in for a database.
Package repository is the in-memory store standing in for a database.
blog/internal/transport/graphql
Package graphql adapts the domain to the generated GraphQL bindings.
Package graphql adapts the domain to the generated GraphQL bindings.
echo command
Command echo serves the examples/blog schema through Echo v5, on every transport:
Command echo serves the examples/blog schema through Echo v5, on every transport:
federation
Package federation is two Apollo Federation subgraphs and the fetch a router performs across them.
Package federation is two Apollo Federation subgraphs and the fetch a router performs across them.
federation/cmd/subgraphs command
Command subgraphs serves both federation subgraphs so a real router can be pointed at them:
Command subgraphs serves both federation subgraphs so a real router can be pointed at them:
fiber command
Command fiber serves the examples/blog schema through Fiber v3, on every transport:
Command fiber serves the examples/blog schema through Fiber v3, on every transport:
quickstart command
Command quickstart is the smallest thing in this repository that runs: one SDL file, hand-written bindings, no code generation and no layers.
Command quickstart is the smallest thing in this repository that runs: one SDL file, hand-written bindings, no code generation and no layers.
relaynode
Package relaynode is the Relay server contract end to end: global object identification and cursor connections, with nothing hand-rolled.
Package relaynode is the Relay server contract end to end: global object identification and cursor connections, with nothing hand-rolled.
relaynode/cmd/server command
Command server serves the Relay example on /graphql, so the global-id round trip can be run against a real endpoint:
Command server serves the Relay example on /graphql, so the global-id round trip can be run against a real endpoint:
storefront
Package storefront is a back-office API written the way a deployment has to write one: every position says what it requires, and nothing is visible because nobody thought to ask for it.
Package storefront is a back-office API written the way a deployment has to write one: every position says what it requires, and nothing is visible because nobody thought to ask for it.
storefront/cmd/server command
Command server serves the storefront schema with the wiring a deployment needs and the other examples leave out: request limits, a cost budget per caller, persisted queries, OpenTelemetry, and a shutdown that winds down streams the HTTP server cannot.
Command server serves the storefront schema with the wiring a deployment needs and the other examples leave out: request limits, a cost budget per caller, persisted queries, OpenTelemetry, and a shutdown that winds down streams the HTTP server cannot.
ext
apq
Package apq implements automatic persisted queries.
Package apq implements automatic persisted queries.
otel
Package otel instruments an Executor with OpenTelemetry traces and metrics.
Package otel instruments an Executor with OpenTelemetry traces and metrics.
throttle
Package throttle rate limits by query cost, the way Shopify's Admin API does: every caller holds a bucket of points that refills at a fixed rate, a query is quoted before it runs, and what it really cost is charged afterwards.
Package throttle rate limits by query cost, the way Shopify's Admin API does: every caller holds a bucket of points that refills at a fixed rate, a query is quoted before it runs, and what it really cost is charged afterwards.
trusted
Package trusted turns persisted queries into a safelist: the server executes only documents a build step registered, and refuses query text whatever hash accompanies it.
Package trusted turns persisted queries into a safelist: the server executes only documents a build step registered, and refuses query text whatever hash accompanies it.
Package fed makes a schema an Apollo Federation subgraph: the _service and _entities root fields, the federation directives, and the _Entity union.
Package fed makes a schema an Apollo Federation subgraph: the _service and _entities root fields, the federation directives, and the _Entity union.
internal
bindhook
Package bindhook lets this module's own packages ask the schema builder a question the public API has no word for.
Package bindhook lets this module's own packages ask the schema builder a question the public API has no word for.
gqlwsproto
Package gqlwsproto implements the graphql-transport-ws protocol — connection_init/connection_ack, ping/pong, and subscribe/next/error/complete per operation — independently of any WebSocket library.
Package gqlwsproto implements the graphql-transport-ws protocol — connection_init/connection_ack, ping/pong, and subscribe/next/error/complete per operation — independently of any WebSocket library.
httpreq
Package httpreq holds the request handling the HTTP transports share: query-parameter and JSON-body decoding, the body limit and the CSRF check.
Package httpreq holds the request handling the HTTP transports share: query-parameter and JSON-body decoding, the body limit and the CSRF check.
jitter
Package jitter spreads durations so that connections opened together do not all expire together and reconnect in one storm.
Package jitter spreads durations so that connections opened together do not all expire together and reconnect in one storm.
jsonw
Package jsonw implements a streaming JSON writer that appends directly to a byte buffer, tracks value separators per nesting level, and supports rewinding to an earlier mark so that partially written values can be replaced with null without re-serialization.
Package jsonw implements a streaming JSON writer that appends directly to a byte buffer, tracks value separators per nesting level, and supports rewinding to an earlier mark so that partially written values can be replaced with null without re-serialization.
sdlprint
Package sdlprint renders a schema as SDL.
Package sdlprint renders a schema as SDL.
Package loader provides a Facebook-style DataLoader for graphql-go: a per-request cache plus a batch function that coalesces the keys requested by concurrent resolvers into one call.
Package loader provides a Facebook-style DataLoader for graphql-go: a per-request cache plus a batch function that coalesces the keys requested by concurrent resolvers into one call.
Package relay implements the Relay server contract for graphql-go: global object identification and cursor connections.
Package relay implements the Relay server contract for graphql-go: global object identification and cursor connections.
transport
drain
Package drain winds down long-lived connections when a server shuts down.
Package drain winds down long-lived connections when a server shuts down.
gqlecho
Package gqlecho serves GraphQL through Echo v5.
Package gqlecho serves GraphQL through Echo v5.
gqlfiber
Package gqlfiber serves GraphQL through Fiber v3.
Package gqlfiber serves GraphQL through Fiber v3.
gqlhttp
Package gqlhttp serves GraphQL over HTTP following the GraphQL over HTTP specification: GET and POST requests, content negotiation between application/graphql-response+json and application/json, and CSRF prevention for requests browsers can send without a preflight.
Package gqlhttp serves GraphQL over HTTP following the GraphQL over HTTP specification: GET and POST requests, content negotiation between application/graphql-response+json and application/json, and CSRF prevention for requests browsers can send without a preflight.
gqlsse
Package gqlsse serves GraphQL over Server-Sent Events in the distinct connections mode of the graphql-sse protocol: one HTTP request per operation, results streamed back as "next" events and terminated by a "complete" event.
Package gqlsse serves GraphQL over Server-Sent Events in the distinct connections mode of the graphql-sse protocol: one HTTP request per operation, results streamed back as "next" events and terminated by a "complete" event.
gqlws
Package gqlws serves GraphQL over WebSocket using the graphql-transport-ws protocol: connection_init/connection_ack, ping/pong, and subscribe/next/ error/complete per operation.
Package gqlws serves GraphQL over WebSocket using the graphql-transport-ws protocol: connection_init/connection_ack, ping/pong, and subscribe/next/ error/complete per operation.

Jump to

Keyboard shortcuts

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