graphql

package
v0.24.0 Latest Latest
Warning

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

Go to latest
Published: Jun 24, 2026 License: MIT Imports: 10 Imported by: 0

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

Examples

Constants

This section is empty.

Variables

View Source
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

type Arg struct {
	Type    Type
	Default any
}

Arg describes a single field/input argument: its Type and an optional Default applied when the caller omits it.

type ArgValues

type ArgValues map[string]any

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).

func (ArgValues) Bool

func (a ArgValues) Bool(name string) bool

Bool returns the argument as a bool, or false.

func (ArgValues) Float

func (a ArgValues) Float(name string) float64

Float returns the argument as a float64, or 0.

func (ArgValues) Get

func (a ArgValues) Get(name string) (any, bool)

Get returns the raw value for name and whether it was present.

func (ArgValues) Input

func (a ArgValues) Input(name string) map[string]any

Input returns the argument as a map[string]any (input object), or nil.

func (ArgValues) Int

func (a ArgValues) Int(name string) int64

Int returns the argument as an int64, coercing float64 if needed, or 0.

func (ArgValues) List

func (a ArgValues) List(name string) []any

List returns the argument as a []any, or nil.

func (ArgValues) String

func (a ArgValues) String(name string) string

String returns the argument as a string, or "" if absent / not a string.

type Args

type Args map[string]*Arg

Args maps argument names to their definitions.

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 Enum

type Enum struct {
	Name        string
	Description string
	Values      map[string]*EnumValue
}

Enum is a scalar-like type restricted to a fixed set of named values.

func (*Enum) TypeName

func (e *Enum) TypeName() string

TypeName implements Type.

type EnumValue

type EnumValue struct {
	Value       any
	Description string
}

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 Fields

type Fields map[string]*Field

Fields maps field names to their definitions within an Object.

type GqlError

type GqlError struct {
	Message string `json:"message"`
	Path    []any  `json:"path,omitempty"`
}

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).

func (GqlError) Error

func (e GqlError) Error() string

Error implements the error interface so a GqlError can be returned directly.

type InputObject

type InputObject struct {
	Name        string
	Description string
	Fields      Args
}

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.

func (*InputObject) TypeName

func (i *InputObject) TypeName() string

TypeName implements Type.

type List

type List struct{ OfType Type }

List is the [T] wrapper. Use NewList to construct.

func NewList

func NewList(t Type) *List

NewList wraps t in a List ([t]).

func (*List) TypeName

func (l *List) TypeName() string

TypeName implements Type.

type NonNull

type NonNull struct{ OfType Type }

NonNull is the T! wrapper. Use NewNonNull to construct.

func NewNonNull

func NewNonNull(t Type) *NonNull

NewNonNull wraps t in a NonNull (t!).

func (*NonNull) TypeName

func (n *NonNull) TypeName() string

TypeName implements Type.

type Object

type Object struct {
	Name        string
	Description string
	Fields      Fields
}

Object is a GraphQL output object type: a named set of resolvable Fields.

func (*Object) TypeName

func (o *Object) TypeName() string

TypeName implements Type.

type Request

type Request struct {
	Query         string
	OperationName string
	Variables     map[string]any
	Root          any
}

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

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

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

type Scalar struct {
	Name        string
	Description string
}

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.

func (*Scalar) TypeName

func (s *Scalar) TypeName() string

TypeName implements Type.

type Schema

type Schema struct {
	Query    *Object
	Mutation *Object
}

Schema is the entry point you build: a root Query object and an optional Mutation object. Pass it to NewSchema to validate and obtain an executable *CompiledSchema.

type Type

type Type interface {
	// TypeName returns the printable type name, e.g. "Int", "[User!]!".
	TypeName() string
	// contains filtered or unexported methods
}

Type is the interface implemented by every GraphQL type (scalars, objects, enums, input objects, and the List / NonNull wrappers).

Jump to

Keyboard shortcuts

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