Documentation
¶
Overview ¶
Package graphql is a dependency-free, struct-first GraphQL execution engine for the lagodev framework. You describe a schema in plain Go — objects, fields, arguments and resolver functions — and the package parses incoming GraphQL query documents, executes the requested selection set against your resolvers, and produces the standard {"data": ..., "errors": [...]} JSON response. It ships with a hand-written lexer + parser and an http.Handler, and pulls in nothing outside the standard library.
What this is (and is not) ¶
This is a pragmatic core, not a full GraphQL-spec implementation. It covers the parts day-to-day APIs actually use; the unsupported edges are documented honestly below so you are never surprised in production.
Supported:
- Operations: query and mutation, anonymous or named.
- Selection sets: nested fields, aliases (alias: field), arguments.
- Arguments: Int, Float, String, Boolean, ID, enum values, lists, input objects, null, and variable references ($var).
- Variables: declared in the operation, supplied via the request, coerced to the declared type (Int/Float/String/Boolean/ID/enum/list/input).
- Fragments: inline fragments (... on Type { ... }) and named fragment definitions referenced via spreads (...Name).
- Types: Object, Scalar (Int, Float, String, Boolean, ID), Enum, InputObject, List (NewList) and NonNull (NewNonNull) wrappers.
- Execution: depth-first resolution, per-field error collection with a response path, default field resolution from struct fields / maps.
- Introspection-lite: the __typename meta field on every object.
- HTTP: Schema.Handler() speaking the standard POST JSON protocol ({query, variables, operationName}).
Not supported (deferred — will error or be ignored, never silently wrong):
- Directives (@skip, @include, custom). Parsed-away is not attempted; a directive in the document is a parse error.
- Subscriptions.
- Full __schema / __type introspection (only __typename is provided).
- Custom scalar coercion hooks beyond the five built-ins.
- Schema-side input validation beyond type coercion (e.g. no @constraint).
Defining a schema ¶
var queryType = &graphql.Object{
Name: "Query",
Fields: graphql.Fields{
"user": &graphql.Field{
Type: userType,
Args: graphql.Args{"id": {Type: graphql.NewNonNull(graphql.ID)}},
Resolve: func(ctx context.Context, p any, a graphql.ArgValues) (any, error) {
return findUser(a.String("id")), nil
},
},
},
}
schema, err := graphql.NewSchema(graphql.Schema{Query: queryType})
Executing ¶
res := schema.Execute(ctx, graphql.Request{Query: `{ user(id:"1"){ name } }`})
out, _ := json.Marshal(res) // {"data":{"user":{"name":"Ann"}}}
See the package examples for a complete users->posts schema, a mutation, and an HTTP round-trip.
Example ¶
Example shows the everyday path: build a schema, then Execute a query with a nested selection, a literal argument and a variable, and marshal the result.
package main
import (
"context"
"encoding/json"
"fmt"
"sort"
"github.com/devituz/lagodev/graphql"
)
// user is the in-memory record a real resolver would load from a store.
type user struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
}
// userStore is fixed, in-memory data so the example is deterministic.
var userStore = map[string]*user{
"1": {ID: "1", Name: "Ann", Role: "admin"},
"2": {ID: "2", Name: "Bob", Role: "member"},
"3": {ID: "3", Name: "Cleo", Role: "member"},
}
// postStore maps a user ID to their posts.
var postStore = map[string][]map[string]any{
"1": {{"id": "10", "title": "Hello"}, {"id": "11", "title": "World"}},
"2": {{"id": "20", "title": "Notes"}},
"3": {},
}
// buildBlogSchema wires a small Query -> User -> [Post] schema over the
// in-memory stores above. It is shared by the examples below.
func buildBlogSchema() *graphql.CompiledSchema {
postType := &graphql.Object{
Name: "Post",
Fields: graphql.Fields{
"id": &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
"title": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
},
}
userType := &graphql.Object{
Name: "User",
Fields: graphql.Fields{
"id": &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
"name": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
"role": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
"posts": &graphql.Field{
Type: graphql.NewList(graphql.NewNonNull(postType)),
Resolve: func(_ context.Context, parent any, _ graphql.ArgValues) (any, error) {
u, _ := parent.(*user)
if u == nil {
return nil, nil
}
out := make([]any, 0, len(postStore[u.ID]))
for _, p := range postStore[u.ID] {
out = append(out, p)
}
return out, nil
},
},
},
}
queryType := &graphql.Object{
Name: "Query",
Fields: graphql.Fields{
"user": &graphql.Field{
Type: userType,
Args: graphql.Args{"id": {Type: graphql.NewNonNull(graphql.ID)}},
Resolve: func(_ context.Context, _ any, args graphql.ArgValues) (any, error) {
return userStore[args.String("id")], nil
},
},
"users": &graphql.Field{
Type: graphql.NewList(graphql.NewNonNull(userType)),
Args: graphql.Args{"role": {Type: graphql.String}},
Resolve: func(_ context.Context, _ any, args graphql.ArgValues) (any, error) {
want := args.String("role")
ids := make([]string, 0, len(userStore))
for id := range userStore {
ids = append(ids, id)
}
sort.Strings(ids)
out := make([]any, 0, len(ids))
for _, id := range ids {
u := userStore[id]
if want == "" || u.Role == want {
out = append(out, u)
}
}
return out, nil
},
},
},
}
cs, err := graphql.NewSchema(graphql.Schema{Query: queryType})
if err != nil {
panic(err)
}
return cs
}
func main() {
cs := buildBlogSchema()
// Nested selection (user -> posts), an argument on the inner list is not
// needed here; the variable $role drives the top-level users(role:) field.
query := `
query Feed($role: String) {
admins: users(role: $role) {
name
role
posts { id title }
}
one: user(id: "2") {
name
}
}`
res := graphql.Execute(context.Background(), cs, graphql.Request{
Query: query,
Variables: map[string]any{"role": "admin"},
})
out, _ := json.Marshal(res)
fmt.Println(string(out))
}
Output: {"data":{"admins":[{"name":"Ann","posts":[{"id":"10","title":"Hello"},{"id":"11","title":"World"}],"role":"admin"}],"one":{"name":"Bob"}}}
Index ¶
- Variables
- func Handler(cs *CompiledSchema, opts ...Option) http.Handler
- type Arg
- type ArgValues
- func (a ArgValues) Bool(name string) bool
- func (a ArgValues) Float(name string) float64
- func (a ArgValues) Get(name string) (any, bool)
- func (a ArgValues) Input(name string) map[string]any
- func (a ArgValues) Int(name string) int64
- func (a ArgValues) List(name string) []any
- func (a ArgValues) String(name string) string
- type Args
- type CompiledSchema
- type Enum
- type EnumValue
- type Field
- type Fields
- type GqlError
- type InputObject
- type Limits
- type List
- type NonNull
- type Object
- type Option
- type Request
- type ResolveFunc
- type Response
- type Scalar
- type Schema
- type Type
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( Int = &Scalar{Name: "Int", Description: "Signed 32/64-bit integer."} Float = &Scalar{Name: "Float", Description: "Signed double-precision float."} String = &Scalar{Name: "String", Description: "UTF-8 character sequence."} Boolean = &Scalar{Name: "Boolean", Description: "true or false."} ID = &Scalar{Name: "ID", Description: "Unique identifier, serialised as a String."} )
Built-in scalar types.
Functions ¶
func Handler ¶
func Handler(cs *CompiledSchema, opts ...Option) http.Handler
Handler returns an http.Handler that executes GraphQL requests against cs and writes the standard {"data":...,"errors":[...]} JSON envelope.
It speaks the conventional GraphQL-over-HTTP protocol:
- POST with Content-Type application/json and a body of {"query":..., "operationName":..., "variables":{...}}.
- GET with the query in the ?query= parameter (and optional ?operationName= and ?variables= as a JSON-encoded object), suitable for simple read-only requests.
The request context is threaded into Execute, so resolver cancellation and deadlines follow the connection. Transport-level problems (wrong method, unreadable body, malformed JSON) yield a JSON error envelope; resolver and validation errors surface through the normal response "errors" array with a 200 status, per GraphQL convention.
Example ¶
ExampleHandler serves the schema over HTTP and performs a POST round-trip with httptest, printing the raw JSON response body the client receives.
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"sort"
"strings"
"github.com/devituz/lagodev/graphql"
)
// user is the in-memory record a real resolver would load from a store.
type user struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
}
// userStore is fixed, in-memory data so the example is deterministic.
var userStore = map[string]*user{
"1": {ID: "1", Name: "Ann", Role: "admin"},
"2": {ID: "2", Name: "Bob", Role: "member"},
"3": {ID: "3", Name: "Cleo", Role: "member"},
}
// postStore maps a user ID to their posts.
var postStore = map[string][]map[string]any{
"1": {{"id": "10", "title": "Hello"}, {"id": "11", "title": "World"}},
"2": {{"id": "20", "title": "Notes"}},
"3": {},
}
// buildBlogSchema wires a small Query -> User -> [Post] schema over the
// in-memory stores above. It is shared by the examples below.
func buildBlogSchema() *graphql.CompiledSchema {
postType := &graphql.Object{
Name: "Post",
Fields: graphql.Fields{
"id": &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
"title": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
},
}
userType := &graphql.Object{
Name: "User",
Fields: graphql.Fields{
"id": &graphql.Field{Type: graphql.NewNonNull(graphql.ID)},
"name": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
"role": &graphql.Field{Type: graphql.NewNonNull(graphql.String)},
"posts": &graphql.Field{
Type: graphql.NewList(graphql.NewNonNull(postType)),
Resolve: func(_ context.Context, parent any, _ graphql.ArgValues) (any, error) {
u, _ := parent.(*user)
if u == nil {
return nil, nil
}
out := make([]any, 0, len(postStore[u.ID]))
for _, p := range postStore[u.ID] {
out = append(out, p)
}
return out, nil
},
},
},
}
queryType := &graphql.Object{
Name: "Query",
Fields: graphql.Fields{
"user": &graphql.Field{
Type: userType,
Args: graphql.Args{"id": {Type: graphql.NewNonNull(graphql.ID)}},
Resolve: func(_ context.Context, _ any, args graphql.ArgValues) (any, error) {
return userStore[args.String("id")], nil
},
},
"users": &graphql.Field{
Type: graphql.NewList(graphql.NewNonNull(userType)),
Args: graphql.Args{"role": {Type: graphql.String}},
Resolve: func(_ context.Context, _ any, args graphql.ArgValues) (any, error) {
want := args.String("role")
ids := make([]string, 0, len(userStore))
for id := range userStore {
ids = append(ids, id)
}
sort.Strings(ids)
out := make([]any, 0, len(ids))
for _, id := range ids {
u := userStore[id]
if want == "" || u.Role == want {
out = append(out, u)
}
}
return out, nil
},
},
},
}
cs, err := graphql.NewSchema(graphql.Schema{Query: queryType})
if err != nil {
panic(err)
}
return cs
}
func main() {
cs := buildBlogSchema()
srv := httptest.NewServer(graphql.Handler(cs))
defer srv.Close()
body := `{"query":"{ user(id:\"1\"){ name role posts { title } } }"}`
resp, err := http.Post(srv.URL, "application/json", strings.NewReader(body))
if err != nil {
panic(err)
}
defer resp.Body.Close()
var buf strings.Builder
dec := json.NewDecoder(resp.Body)
var v any
if err := dec.Decode(&v); err != nil {
panic(err)
}
enc := json.NewEncoder(&buf)
if err := enc.Encode(v); err != nil {
panic(err)
}
fmt.Print(buf.String())
}
Output: {"data":{"user":{"name":"Ann","posts":[{"title":"Hello"},{"title":"World"}],"role":"admin"}}}
Types ¶
type Arg ¶
Arg describes a single field/input argument: its Type and an optional Default applied when the caller omits it.
type ArgValues ¶
ArgValues is the resolved, coerced argument map handed to a resolver. Keys are argument names; values are Go values already coerced to the argument's declared type (int64 for Int, float64 for Float, string for String/ID/enum, bool for Boolean, []any for lists, map[string]any for input objects).
type CompiledSchema ¶
type CompiledSchema struct {
// contains filtered or unexported fields
}
CompiledSchema is a validated, executable schema returned by NewSchema. It is safe for concurrent use.
func NewSchema ¶
func NewSchema(s Schema) (*CompiledSchema, error)
NewSchema validates s and returns an executable *CompiledSchema. It errors when Query is nil or a reachable type is malformed (nil field type, etc.).
type EnumValue ¶
EnumValue is one member of an Enum. Value is the Go value the resolver sees for input and is compared against for output; when nil the Name is used.
type Field ¶
type Field struct {
Type Type
Args Args
Description string
Resolve ResolveFunc
}
Field describes one field of an Object: its result Type, optional Args, an optional human Description and the Resolve function. When Resolve is nil the executor falls back to default resolution (struct field or map key matching the field name, case-sensitive then case-insensitive).
type GqlError ¶
GqlError is a single GraphQL error entry: a human message and, for field errors, the response Path at which it occurred (field names and list indices, root-to-leaf).
type InputObject ¶
InputObject is a structured argument type: a named set of input Fields, each with a Type and optional Default. Input objects may nest other input objects and lists.
type Limits ¶ added in v0.26.0
type Limits struct {
// MaxDocumentBytes caps the length, in bytes, of the query document string.
// Guards the lexer against very long inputs before tokenisation begins.
MaxDocumentBytes int
// MaxTokens caps the number of lexical tokens the parser will accept. Guards
// against token floods that pass the byte limit yet still cost CPU to parse.
MaxTokens int
// MaxDepth caps the nesting depth of the (fragment-expanded) selection tree.
// Defends against query-depth bombs that exhaust the resolution stack.
MaxDepth int
// MaxSelectionNodes caps the total number of selected fields after fragment
// expansion. This is the primary defence against fragment/alias width
// amplification: a fragment referenced many times, or a field aliased many
// times, multiplies the node count and trips this budget.
MaxSelectionNodes int
// MaxAliases caps the number of aliased fields in the document (counted on
// the raw AST, before expansion). Aliases are the cheap multiplier in an
// amplification attack; this stops thousands of distinct response keys.
MaxAliases int
// DisableIntrospection rejects any document that selects an introspection
// meta-field (__typename, __schema, __type) when true. Use in production to
// keep the schema shape private.
DisableIntrospection bool
}
Limits bounds the cost of an incoming GraphQL document so a public, untrusted-input endpoint cannot be driven into stack/CPU/memory exhaustion by a hostile query. Every field is a hard ceiling; a document exceeding any one of them is rejected with a bounded error before resolvers run.
The zero value disables every check. Use DefaultLimits for production-safe ceilings that pass normal queries; override individual fields as needed. A value <= 0 in any field means "no limit" for that dimension, so callers can relax a single dimension without losing the others.
func DefaultLimits ¶ added in v0.26.0
func DefaultLimits() Limits
DefaultLimits returns production-safe limits: generous enough that ordinary application queries pass unchanged, tight enough that the threat-model attacks (depth bombs, alias/fragment amplification, token floods, oversized documents) are rejected with a bounded error. Introspection stays enabled to preserve current behaviour; set DisableIntrospection explicitly to lock it.
type NonNull ¶
type NonNull struct{ OfType Type }
NonNull is the T! wrapper. Use NewNonNull to construct.
type Option ¶ added in v0.26.0
type Option func(*handlerConfig)
Option configures a Handler. Options compose; later options override earlier ones. The zero set of options yields DefaultLimits and the default body cap, preserving the handler's historical behaviour for normal traffic.
func WithLimits ¶ added in v0.26.0
WithLimits sets the execution Limits enforced on every request's document. Use DefaultLimits as a base and override individual fields, or pass a custom Limits. The zero Limits value disables all structural checks.
func WithMaxBodyBytes ¶ added in v0.26.0
WithMaxBodyBytes overrides the maximum request-body size (in bytes) the handler will read before decoding. Values <= 0 leave the default in place. It should be set in concert with Limits.MaxDocumentBytes.
func WithoutIntrospection ¶ added in v0.26.0
func WithoutIntrospection() Option
WithoutIntrospection disables introspection meta-fields (__typename, __schema, __type) for the handler, a common production hardening step.
type Request ¶
Request is one incoming GraphQL execution request. Query is the document source; OperationName selects which operation to run when the document defines more than one (it may be empty for a single-operation document); Variables supplies values for the operation's declared variables; Root, when non-nil, is the parent value passed to top-level resolvers.
type ResolveFunc ¶
ResolveFunc computes the value of a field. parent is the value returned by the enclosing field's resolver (or the root value for top-level fields); args are the coerced arguments. Returning an error surfaces it in the response "errors" array with the field's path, and the field's value becomes null without aborting sibling fields.
type Response ¶
type Response struct {
Data any `json:"data,omitempty"`
Errors []GqlError `json:"errors,omitempty"`
}
Response is the standard GraphQL result envelope. Data holds the resolved selection tree (nil when execution could not begin, e.g. a parse error); Errors collects request-level and per-field errors. It marshals to the {"data":...,"errors":[...]} JSON shape, omitting an empty errors array.
func Execute ¶
func Execute(ctx context.Context, cs *CompiledSchema, req Request) Response
Execute parses Req.Query, selects the requested operation, and resolves its selection set against cs. Argument and variable values are coerced to their declared types; resolver errors are collected into the response with their path while sibling fields continue. A nil schema or a parse/validation failure yields a Response with Data nil and a single error.
func ExecuteWithLimits ¶ added in v0.26.0
func ExecuteWithLimits(ctx context.Context, cs *CompiledSchema, req Request, limits Limits) Response
ExecuteWithLimits behaves like Execute but enforces the supplied Limits on the incoming document (size, token count, depth, selection-node count, aliases, introspection) before any resolver runs. A document that violates a limit yields a Response with Data nil and a single bounded error. The zero Limits value disables every check; DefaultLimits returns production-safe ceilings.
type Scalar ¶
Scalar is a leaf type that serialises to a single JSON value. The five built-in scalars (Int, Float, String, Boolean, ID) are package-level variables; coercion of input/variable values into Go values is driven by the Name via coerceScalar.