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) 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 List
- type NonNull
- type Object
- 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) 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 NonNull ¶
type NonNull struct{ OfType Type }
NonNull is the T! wrapper. Use NewNonNull to construct.
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.
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.