moongql

    moongql — a code-first GraphQL library for MoonBit (← strawberry-graphql): define object types and fields in code, emit the schema SDL.

    graphql
    strawberry
    schema
    sdl
    code-first
    moonbit
    Download zip
    Version
    0.8.1
    License
    Apache-2.0
    Last updated
    2 hours ago
    Downloads
    3

    Dependencies

    #moongql

    A code-first GraphQL library for MoonBit — ← strawberry-graphql.

    Check and Test License mooncakes

    Moved on mooncakes from Lfan-ke/moongql to moonbitstack/moongql.

    moongql defines a GraphQL schema in code — object types and fields — the way strawberry-graphql does for Python, and emits the schema SDL. v0 ships the code-first schema and SDL printer; it's pure logic with no runtime dependencies, so it runs on every backend.

    #Quickstart

    let s = @moongql.Schema::new()

    let q = s.object("Query")
    q.field("hello", @moongql.NonNull(@moongql.Scalar("String")))
    q.field("user", @moongql.Named("User"))

    let u = s.object("User")
    u.field("id", @moongql.NonNull(@moongql.Scalar("ID")))
    u.field("name", @moongql.NonNull(@moongql.Scalar("String")))
    u.field("tags", @moongql.ListOf(@moongql.NonNull(@moongql.Scalar("String"))))

    let sdl = s.to_sdl()

    produces:

    schema { query: Query } type Query { hello: String! user: User } type User { id: ID! name: String! tags: [String!] }

    Verified across all backends (wasm, wasm-gc, js, native) in CI, 0 warnings under --deny-warn.

    #Roadmap (transliterating strawberry)

    The code-first schema and SDL are here, with field arguments (field_args), input object types (Schema::input), enum types (Schema::enum_), and interfaces (Schema::interface + object implements). The query parser now lands too — a hand-written lexer and a recursive-descent parser that turn a query string into a Document AST:

    let doc = @moongql.parse(
    "query ($id: ID!) { user(id: $id) { name ...fields @skip(if: false) } }",
    )
    // doc.definitions[0] is an OperationDefinition; doc.to_query() prints it back.

    It covers operations (query/mutation/subscription and the { ... } shorthand), selection sets, fields with aliases/arguments/directives, variable definitions with default values, named and inline fragments, @skip/@include directives, and every input value (variables, ints, floats, strings, block strings, booleans, null, enums, lists, objects) — enough to parse the full GraphQL introspection query.

    #Executing a query

    moongql now runs queries. execute parses, validates against the schema, then walks the document with a resolver map(ResolveInfo) -> Json functions keyed by "Type.field" — returning the { data, errors } response as JSON. It supports variables (with defaults), aliases, named and inline fragments, @skip/@include, nested selection, list and object results, non-null error propagation, and introspection (__schema / __type / __typename), including the introspection types themselves and argument defaults.

    let s = @moongql.Schema::new()
    let q = s.object("Query")
    q.field_args("user", [("id", @moongql.NonNull(@moongql.Scalar("ID")))],
    @moongql.Named("User"))
    let u = s.object("User")
    u.field("id", @moongql.NonNull(@moongql.Scalar("ID")))
    u.field("name", @moongql.NonNull(@moongql.Scalar("String")))

    let r = @moongql.Resolvers::new()
    r.field("Query", "user", fn(info) {
    match info.arg("id") {
    String("1") => { "id": "1", "name": "Alice" }
    _ => Json::null()
    }
    })
    // User.id / User.name fall back to the default resolver (read off the parent).

    let vars : Map[String, Json] = { "uid": "1" }
    let res = @moongql.execute(s, r,
    "query ($uid: ID!) { hero: user(id: $uid) { id name } }", variables=vars)
    // res.stringify() == {"data":{"hero":{"id":"1","name":"Alice"}}}

    Every error that can be traced to a point in the document carries its locations — the line and column of the field, argument, directive or variable it is about, not just a message:

    let res = @moongql.execute(s, r, "query Hero {\n user(id: \"1\") {\n nope\n }\n}")
    // {"errors":[{"message":"Cannot query field 'nope' on type 'User'",
    // "locations":[{"line":3,"column":5}]}]}

    A server's own extensions entry (spec §7.1) rides on the response — a trace id, a timing, whatever the deployment wants to say alongside the result — and is left out when empty:

    let res = @moongql.execute(s, r, "{ user(id: \"1\") { name } }",
    extensions={ "traceId": "qwq-233" })
    // {"data":{...},"extensions":{"traceId":"qwq-233"}}

    #Design: faithful equivalences to strawberry

    MoonBit has no runtime reflection, so where strawberry discovers resolvers and schema from Python classes, moongql uses explicit values: resolvers are functions registered by "Type.field" (like Rust/Go GraphQL servers), and a field's declared return type drives leaf-vs-composite completion exactly as graphql-core's complete_value does. Fields with no registered resolver read their value off the parent JSON object — the equivalent of graphql-core's default_field_resolver.

    Union types, custom scalars, subscriptions, a DataLoader, a GraphiQL HTTP endpoint, and Apollo Federation all land too (below).

    let s = @moongql.Schema::new()

    let node = s.interface("Node")
    node.field("id", @moongql.NonNull(@moongql.Scalar("ID")))

    let q = s.object("Query")
    q.field_args(
    "user",
    [("id", @moongql.NonNull(@moongql.Scalar("ID")))],
    @moongql.Named("User"),
    )

    let u = s.object("User")
    u.implements("Node")
    u.field("id", @moongql.NonNull(@moongql.Scalar("ID")))

    let inp = s.input("UserFilter")
    inp.field("name", @moongql.Scalar("String"))

    s.enum_("Role", ["ADMIN", "USER"])

    emits user(id: ID!): User, type User implements Node { .. }, input UserFilter { .. }, and enum Role { ADMIN USER }.

    #Unions and custom scalars

    Declare a union with Schema::union and select into it through inline fragments; each value is discriminated by its __typename:

    s.union("SearchResult", ["Book", "Author"])
    // { search { __typename ... on Book { title } ... on Author { name } } }

    A custom scalar carries its own serialize (output) and parse_value (input) hooks, the same two coercions strawberry's Scalar defines. Input arguments and variables run through parse_value before a resolver sees them; resolver results run through serialize:

    s.scalar("DateTime",
    serialize=fn(j) { j }, // value -> output JSON
    parse_value=fn(j) { j }) // input JSON -> value

    Both show up in SDL (union SearchResult = Book | Author, scalar DateTime) and in introspection (kind: UNION with possibleTypes, kind: SCALAR).

    #Subscriptions

    A subscription has one root field backed by a source stream. execute_subscription returns the ordered payloads a client would receive, one { data, errors } per event. The stream is a pull source — a resolver returning Array[Json] — so the whole pipeline runs on every backend; the async native variant over a WebSocket wraps the same per-event core.

    let subs = @moongql.Subscribers::new()
    subs.field("Subscription", "messageAdded", fn(info) {
    // one Json payload per event, in delivery order
    [msg1, msg2, msg3]
    })

    let payloads = @moongql.execute_subscription(schema, resolvers, subs,
    "subscription { messageAdded(channel: \"general\") { id body } }")
    // payloads[0].stringify() == {"data":{"messageAdded":{"id":"1","body":"hello"}}}

    The single-root-field rule (spec §5.2.3.1) is enforced, and an introspection field can't be a subscription root.

    #DataLoader

    DataLoader is the batch-and-cache tool for the N+1 problem. Keys requested during a resolution pass are queued and deduped; one dispatch runs the batch function once over the distinct keys:

    let loader : @moongql.DataLoader[Int, String] = @moongql.DataLoader::new(
    fn(ids) { load_authors(ids) }, // called once per pass
    fn(id) { id.to_string() }, // cache-key function
    )
    loader.load(1); loader.load(2); loader.load(1) // three requests, two keys
    loader.dispatch() // one batch call for {1, 2}
    loader.get(1) // Some("author#1")

    prime, clear, clear_all, load_many and load_now round out the API. Where Facebook's DataLoader coalesces within an event-loop tick, this drains the queue explicitly so it stays testable without a runtime.

    #HTTP endpoint

    graphql_handler wires a schema and its resolvers onto the moonasgi seam, so moongql serves over mooncat (or any server that binds the ASGI callable), following the GraphQL-over-HTTP specification:

    let handler = @moongql.graphql_handler(schema, resolvers) // path defaults to /graphql
    let app = @moongql.graphql_app(schema, resolvers) // the same, lifted onto an AsgiApp

    A POST reads an application/json body — { query, variables, operationName }, or an array of those for a batch, answered by an array of results. A POST with no Content-Type is a 415: the server will not guess what the bytes are. A GET carrying ?query= runs it (a query only — a mutation over GET is a 405, since a GET must stay safe), taking variables as a JSON-encoded parameter; a GET without one serves the GraphiQL IDE.

    Accept picks the response media type. Ask for application/graphql-response+json and the status carries meaning — 400 for a request error, where the document never ran, and 200 for an operation that ran whatever its field errors say. Ask for application/json, or send no Accept at all, and every GraphQL response is a 200 with the errors in the body, which is what pre-specification clients expect. A client that accepts neither gets a 406.

    graphql_app is what a server mounts. The request/response logic is shared with graphql_handler, which moonasgi's TestClient drives without a socket — so the endpoint is tested on every backend:

    let client = @moonasgi.TestClient::new(@moongql.graphql_handler(schema, resolvers))
    let body = @utf8.encode(
    "{\"query\":\"{ user(id: \\\"1\\\") { name } }\"}",
    )
    let resp = client.post("/graphql", body~, headers=[
    ("content-type", "application/json"),
    ("accept", "application/graphql-response+json"),
    ])
    // resp.status == 200, resp.text() == {"data":{"user":{"name":"Alice"}}}

    #graphql-transport-ws

    GqlWs is one WebSocket connection's protocol state, and recv maps a client frame to the frames sent back: connection_initconnection_ack, subscribe → a run of next then complete, pingpong. The connection_init payload becomes the connection's parameters and reaches every resolver's context under connectionParams, so a client hands over a token once instead of on every operation. An operation id is unique among running operations — reusing one that has completed is fine, reusing one still running closes with 4409.

    The protocol's two timers are state, not threads. This package has no runtime and no clock, so a server drives them by calling tick with a millisecond reading of a monotonic clock and sending whatever comes back: the 4408 close when connection_init never arrived, or a keep-alive ping.

    let ws = @moongql.GqlWs::new(schema, resolvers, subs, init_timeout=3000, ping_interval=12000)
    let handler = ws.handler() // a moonasgi WebSocketHandler
    ws.tick(now_ms) // the server's clock, as often as it likes

    graphql_ws_handler builds a handler whose connection state nothing can reach, for a server with no clock to spare.

    #Apollo Federation

    A subgraph exposes _service { sdl } (its SDL, annotated with the federation directives), the _entities(representations:) resolver that turns a { __typename, <key> } reference back into a full object, and @key / @extends / @external markers. Register the entity types and their reference resolvers, then apply installs the machinery — the _Service type, the _Any scalar, the _Entity union, and the two root fields — so the ordinary executor answers a federated query:

    let fed = @moongql.Federation::new()
    fed.entity(name="User", key="id", resolve=(rep, _ctx) => {
    let id = match rep {
    Object(m) => match m.get("id") { Some(String(s)) => s; _ => "" }
    _ => ""
    }
    { "id": id, "name": "User#" + id }.to_json() // materialise from the key
    })
    fed.apply(schema, resolvers)

    // { _service { sdl } } -> the subgraph SDL with `type User @key(fields: "id")`
    // _entities(representations: [{ __typename: "User", id: "7" }]) -> the User object

    An extended type declares extends=true and lists its external fields, which render as extend type ... @key(...) with @external on the borrowed fields — the shape a gateway composes.

    #Federation v2

    Pass v2=true for a Federation v2 subgraph. Its SDL opens with extend schema @link(...) onto the federation spec, and the v2 field directives — @shareable, @inaccessible, @override(from:), @requires(fields:), @provides(fields:) — are declared on fields and rendered into the subgraph SDL:

    let fed = @moongql.Federation::new(v2=true)
    fed.entity(name="Product", key="upc", extends=true, external=["weight"],
    resolve=(rep, _ctx) => {
    // @requires(fields: "weight"): the gateway ships the external weight in the
    // representation, so the resolver can read it to compute the estimate.
    let weight = read_int(rep, "weight")
    { "upc": read_str(rep, "upc"), "shippingEstimate": weight * 2 }.to_json()
    })
    fed.shareable("Product", "name")
    fed.override_("Product", "price", from="legacy")
    fed.requires("Product", "shippingEstimate", fields="weight")
    fed.inaccessible("User", "ssn") // present in the subgraph, hidden from the composed schema
    fed.apply(schema, resolvers)

    @inaccessible is the one v2 directive with a runtime effect beyond SDL: the field stays resolvable inside the subgraph but is hidden from introspection, so a gateway composing the public schema never sees it.

    #Custom directives

    Beyond @skip / @include, a schema can register its own directives. A directive definition carries its valid locations, arguments, repeatability, and an optional on_field hook that transforms a resolved field value during execution:

    s.directive("prefix",
    locations=["FIELD"],
    args=[("text", @moongql.NonNull(@moongql.Scalar("String")))],
    on_field=Some((value, args) => match (value, args.get("text")) {
    (String(x), Some(String(p))) => (p + x).to_json()
    _ => value
    }))
    // { greeting @prefix(text: "Hello, ") } -> "Hello, world"

    Registered directives show up in __schema { directives } beside the built-ins, and the validator accepts them only where they are declared.

    #Spec validation

    The validator implements a broad slice of the GraphQL spec's §5 validation table. Beyond the per-selection rules (fields and arguments exist, leaf-vs-composite selections, fragment type conditions, and variables defined by the operation — checked through the fragments it spreads, not only its own selections), it enforces the document-level rules:

    RuleSpec
    Operation name uniqueness§5.2.1.1
    Lone anonymous operation§5.2.2.1
    Argument uniqueness§5.4.2
    Fragment name uniqueness§5.5.1.1
    Fragments must be used§5.5.1.4
    Fragment spreads must not form cycles§5.5.2.2
    Directives are defined§5.7.1
    Directives are in valid locations§5.7.2
    Directive repeatability§5.7.3
    Variable uniqueness§5.8.1
    All variables used§5.8.4

    #License

    Apache-2.0.

    EntityResolver

    type EntityResolver = (Json, Json) -> Json raise ResolverError

    The reference resolver for an entity type: given a representation (a JSON object carrying __typename and the entity's key fields) and the request context, return the fully resolved entity as JSON. This is the subgraph's answer to "you have a key, give me the object" — Apollo's __resolveReference.

    FieldDirectiveHook

    type FieldDirectiveHook = (Json, Map[String, Json]) -> Json

    The run-time hook a user directive may carry: given the resolved field value and the directive's coerced arguments, return the transformed value. This is how a custom executable directive (@upper, @default(value:), ...) alters a resolved field, the moongql equivalent of a strawberry SchemaDirective with a resolver-side effect.

    GqlSyntaxError

    pub suberror GqlSyntaxError {
    GqlSyntaxError(String, Int, Int)
    }

    A syntax error raised by the lexer or parser, carrying a message and the 1-based line/column where the offending token starts.

    GqlSyntaxError::to_string

    fn GqlSyntaxError::to_string(self : GqlSyntaxError) -> String

    Render a syntax error as Syntax error at L:C: message.

    ResolverError

    pub(all) suberror ResolverError {
    ResolverError(String)
    ResolverErrorExt(String, Map[String, Json])
    }

    An error raised by a field resolver; its message is reported in the response errors list with the field's response path.

    AppliedDirective

    pub(all) struct AppliedDirective {
    name : String
    args : Array[(String, Json)]
    }

    A directive applied to a schema element: its name and its constant arguments as ordered (name, value) pairs. Build one with AppliedDirective::new.

    AppliedDirective::new

    fn AppliedDirective::new(name : String, args? : Array[(String, Json)]) -> AppliedDirective

    An applied directive with no arguments, or with the given constant arguments.

    Argument

    pub(all) struct Argument {
    name : String
    value : Value
    pos : Pos
    }

    A name: value argument on a field or directive.

    DataLoader

    pub struct DataLoader[K, V] {
    batch_load : (Array[K]) -> Array[V]
    key : (K) -> String
    cache : Map[String, V]
    queue : Array[K]
    queued : Map[String, Bool]
    }

    A batching, caching loader over key type K and value type V. batch_load receives the distinct, un-cached keys and must return their values positionally (result i is the value for key i). key derives a stable cache/identity string for a key.

    DataLoader::clear

    fn[K, V] DataLoader::clear(self : DataLoader[K, V], k : K) -> Unit

    Drop the cached value for k, so the next load+dispatch reloads it.

    DataLoader::clear_all

    fn[K, V] DataLoader::clear_all(self : DataLoader[K, V]) -> Unit

    Drop the whole cache.

    DataLoader::dispatch

    fn[K, V] DataLoader::dispatch(self : DataLoader[K, V]) -> Unit

    Run the pending batch: call batch_load once with the queued distinct keys, store each returned value in the cache under its key, and clear the queue. A no-op when nothing is queued, so it is safe to call after every pass.

    DataLoader::get

    fn[K, V] DataLoader::get(self : DataLoader[K, V], k : K) -> V?

    The cached value for k, or None if it has not been loaded and dispatched.

    DataLoader::load

    fn[K, V] DataLoader::load(self : DataLoader[K, V], k : K) -> Unit

    Queue k for the next dispatch. A key already cached or already queued in this pass is dropped, so the batch that dispatch runs sees each distinct key exactly once — this dedupe is what collapses N+1 into one call.

    DataLoader::load_many

    fn[K, V] DataLoader::load_many(self : DataLoader[K, V], ks : Array[K]) -> Unit

    Queue several keys for the next dispatch.

    DataLoader::load_now

    fn[K, V] DataLoader::load_now(self : DataLoader[K, V], k : K) -> V?

    Load one key and dispatch immediately, returning its value. Convenience for a single lookup; batching still applies to anything already queued.

    DataLoader::new

    fn[K, V] DataLoader::new(batch_load : (Array[K]) -> Array[V], key : (K) -> String) -> DataLoader[K, V]

    Create a loader from its batch-load function and a key-identity function.

    DataLoader::prime

    fn[K, V] DataLoader::prime(self : DataLoader[K, V], k : K, v : V) -> Unit

    Seed the cache with a known value so a later load for k never hits the backend (strawberry/Facebook DataLoader's prime).

    Definition

    pub(all) enum Definition {
    OperationDef(OperationDefinition)
    FragmentDef(FragmentDefinition)
    }

    A top-level executable definition: an operation or a named fragment.

    Directive

    pub(all) struct Directive {
    name : String
    arguments : Array[Argument]
    pos : Pos
    }

    A directive application: @name(args).

    DirectiveDef

    pub(all) struct DirectiveDef {
    name : String
    locations : Array[String]
    args : Array[(String, GqlType)]
    is_repeatable : Bool
    on_field : (Json, Map[String, Json]) -> Json?
    }

    A directive definition: its name, the locations it may appear at (the __DirectiveLocation enum values, e.g. "FIELD", "FIELD_DEFINITION"), its declared args, whether it is_repeatable, and an optional executable on_field hook. A definition with locations that include executable ones and an on_field hook is applied during execution; one with only type-system locations is a pure schema directive (SDL + introspection only).

    Document

    pub(all) struct Document {
    definitions : Array[Definition]
    }

    A parsed GraphQL document: an ordered list of executable definitions.

    Document::to_query

    fn Document::to_query(self : Document) -> String

    Print a whole document back to a canonical GraphQL string: each definition, separated by a blank line. Parsing this output yields an equivalent AST.

    EntityDef

    pub(all) struct EntityDef {
    name : String
    key : String
    extends : Bool
    external : Array[String]
    resolve : (Json, Json) -> Json raise ResolverError
    }

    One federated entity: its type name, the key field set (@key(fields:)), whether the subgraph only extends a type it does not own, the names of its external fields (@external, owned by another subgraph), and the reference resolver that materialises it from a representation.

    EnumType

    pub(all) struct EnumType {
    name : String
    values : Array[String]
    deprecations : Map[String, String]
    description : String?
    value_descriptions : Map[String, String]
    }

    A GraphQL enum type: a name, an ordered list of value names, and each value's @deprecated reason keyed by value name (mirroring Field.arg_defaults).

    Federation

    pub struct Federation {
    entities : Array[EntityDef]
    v2 : Bool
    field_dirs : Array[(String, String, AppliedDirective)]
    }

    The federation configuration for a subgraph: the set of entity types it contributes, whether it is a Federation v2 subgraph (which opts in with an @link to the federation spec and unlocks @shareable / @inaccessible / @override), and the field-level federation directives it declares. Build it with new, register entities with entity, mark fields with shareable / inaccessible / override_ / requires / provides, then apply it to a schema and resolver map.

    Federation::apply

    fn Federation::apply(self : Federation, schema : Schema, resolvers : Resolvers) -> Unit

    Install the federation machinery onto schema and resolvers: declare the _Any scalar, the _Service type with its sdl field, and the _Entity union over every registered entity type; add the _service and _entities fields to the query root; and register the two resolvers. After this the schema answers a federated query — { _service { sdl } } and _entities(representations:) — through the normal executor.

    Federation::entity

    fn Federation::entity(self : Federation, name~ : String, key~ : String, resolve~ : (Json, Json) -> Json raise ResolverError, extends? : Bool, external? : Array[String]) -> Unit

    Register an entity type. name is the object type (which must also be declared on the schema), key is its @key field set ("id", or space- separated "upc sku" for a compound key), resolve turns a representation back into the object. Set extends when this subgraph extends a type owned by another, and list external fields that other subgraphs own.

    Federation::inaccessible

    fn Federation::inaccessible(self : Federation, type_name : String, field_name : String) -> Unit

    Mark type_name.field_name @inaccessible (v2) — present in this subgraph but omitted from the composed public schema, and hidden from introspection.

    Federation::new

    fn Federation::new(v2? : Bool) -> Federation

    An empty federation config. Pass v2=true for a Federation v2 subgraph, whose SDL opens with extend schema @link(...) importing the federation spec and which may use the v2-only directives (@shareable, @inaccessible, @override).

    Federation::override_

    fn Federation::override_(self : Federation, type_name : String, field_name : String, from~ : String) -> Unit

    Declare that type_name.field_name is @override-taken from the subgraph from (v2) — this subgraph now resolves the field the other used to own.

    Federation::provides

    fn Federation::provides(self : Federation, type_name : String, field_name : String, fields~ : String) -> Unit

    Declare that resolving type_name.field_name @provides the named fields of the returned entity, so the gateway can skip a round trip for them.

    Federation::requires

    fn Federation::requires(self : Federation, type_name : String, field_name : String, fields~ : String) -> Unit

    Declare that resolving type_name.field_name @requires the named external key fields ("weight size"), which the gateway then includes in the entity representation the _entities resolver receives.

    Federation::sdl

    fn Federation::sdl(self : Federation, schema : Schema) -> String

    Render the subgraph SDL: the schema as the developer wrote it, annotated with @key on entity types (prefixed extend when the type is an extension), @external on external fields, and with the federation-internal additions (_Service, _Entity, _Any, and the _service / _entities root fields) left out. This is what _service { sdl } returns.

    Federation::shareable

    fn Federation::shareable(self : Federation, type_name : String, field_name : String) -> Unit

    Mark type_name.field_name @shareable — resolvable by more than one subgraph (v2). Without it, a non-key field must be owned by exactly one subgraph.

    Field

    pub(all) struct Field {
    name : String
    args : Array[(String, GqlType)]
    typ : GqlType
    arg_defaults : Map[String, Json]
    deprecation_reason : String?
    description : String?
    }

    A field on an object type: a name, an ordered list of arguments (each a (name, type) pair), and the field's return type. A field with no arguments prints as name: Type; with arguments it prints as name(a: A, b: B): Type.

    FragmentDefinition

    pub(all) struct FragmentDefinition {
    name : String
    type_condition : String
    directives : Array[Directive]
    selection_set : Array[Selection]
    pos : Pos
    }

    A named fragment definition: fragment Name on Type @dir { selection }.

    GqlError

    pub(all) struct GqlError {
    message : String
    path : Array[Json]
    locations : Array[Pos]
    extensions : Map[String, Json]
    }

    A GraphQL error entry: a message, an optional response path (string field keys and integer list indices), and optional source locations.

    GqlType

    pub(all) enum GqlType {
    Scalar(String)
    Named(String)
    NonNull(GqlType)
    ListOf(GqlType)
    } derive(Eq)

    A GraphQL type reference: a scalar (String, Int, Boolean, Float, ID), a named object type, or a non-null / list wrapper around another type.

    GqlType::named_base

    fn GqlType::named_base(self : GqlType) -> String

    The base named type a possibly-wrapped type refers to (unwrapping !/[]).

    GqlWs

    pub struct GqlWs {
    schema : Schema
    resolvers : Resolvers
    subscribers : Subscribers
    root_value : Json
    context : Json
    init_timeout : Int64
    ping_interval : Int64
    initialized : Bool
    params : Json
    opened : Int64?
    pinged : Int64
    active : Map[String, Unit]
    }

    One graphql-transport-ws connection: the schema it serves plus the protocol state a connection carries — whether connection_init has arrived, the parameters it brought, the operation ids currently running, and the two timers the protocol defines.

    This package has no async runtime and no clock of its own, so the timers are state plus a transition and nothing more: a server drives them by calling tick with a millisecond reading of a monotonic clock, as often as it likes (once a second is plenty), and sending whatever frames come back. Nothing else starts the connection-init countdown or emits a keep-alive ping.

    GqlWs::claim

    fn GqlWs::claim(self : GqlWs, id : String) -> Bool

    Claim operation id for an operation that is starting, answering false when the id is already running — the caller must then close the socket with 4409.

    recv claims and releases an id itself, because a pull source is drained before it returns. A server streaming a live source claims the id when it starts the task and calls release when the task sends its last frame; that is what keeps 4409 meaning "still running" rather than "used once".

    GqlWs::handler

    The connection as a moonasgi WebSocketHandler, negotiating the graphql-transport-ws subprotocol when the client offers it.

    GqlWs::new

    fn GqlWs::new(schema : Schema, resolvers : Resolvers, subscribers : Subscribers, root_value? : Json, context? : Json, init_timeout? : Int64, ping_interval? : Int64) -> GqlWs

    Open a connection's protocol state for schema / resolvers / subscribers. root_value and context are threaded to every operation the way execute threads them.

    init_timeout is how long a client may take to send connection_init before tick closes the socket with 4408, and ping_interval how long the server waits between keep-alive ping frames; both are milliseconds, and either is switched off by passing zero.

    GqlWs::recv

    Handle one inbound message, returning the frames to send back.

    GqlWs::release

    fn GqlWs::release(self : GqlWs, id : String) -> Unit

    Release operation id, freeing it for reuse. Called after the operation's terminating complete or error has been sent.

    GqlWs::tick

    fn GqlWs::tick(self : GqlWs, now : Int64) -> Array[
    WsSend
    ]

    Drive the connection's timers from a monotonic clock reading in milliseconds, returning the frames the elapsed time calls for: the 4408 close when connection_init did not arrive in time, or a keep-alive ping.

    The first call starts the clock, so a server should tick as soon as it has accepted the socket and keep ticking for as long as it holds it.

    ObjectType

    pub(all) struct ObjectType {
    name : String
    fields : Array[Field]
    kind : TypeKind
    interfaces : Array[String]
    description : String?
    }

    A GraphQL composite type with an ordered set of fields. The same shape backs output objects, input objects, and interfaces; kind selects the SDL keyword and interfaces lists the interfaces an object implements.

    ObjectType::field

    fn ObjectType::field(self : ObjectType, name : String, typ : GqlType, deprecation_reason? : String, description? : String) -> Unit

    Add a field with no arguments to this type.

    ObjectType::field_args

    fn ObjectType::field_args(self : ObjectType, name : String, args : Array[(String, GqlType)], typ : GqlType, defaults? : Array[(String, Json)], deprecation_reason? : String, description? : String) -> Unit

    Add a field carrying arguments to this type. Each argument is a (name, type) pair and renders as name(a: A, b: B): RetType. defaults supplies default values for arguments a query may omit (GraphQL field-argument defaults): an absent argument takes its default, an explicitly-null one does not.

    ObjectType::field_by_name

    fn ObjectType::field_by_name(self : ObjectType, name : String) -> Field?

    Look up a field on this type by its name.

    ObjectType::implements

    fn ObjectType::implements(self : ObjectType, name : String) -> Unit

    Declare that this object type implements a named interface. Implemented interfaces render as type Name implements A & B { ... }.

    OperationDefinition

    pub(all) struct OperationDefinition {
    operation : OperationType
    name : String?
    variable_definitions : Array[VariableDefinition]
    directives : Array[Directive]
    selection_set : Array[Selection]
    pos : Pos
    }

    An operation: its type, an optional name, variable definitions, directives and a selection set. The anonymous { ... } query shorthand parses to an OperationDefinition with operation = Query and name = None.

    OperationType

    pub(all) enum OperationType {
    Query
    Mutation
    Subscription
    } derive(Eq)

    Which kind of operation a definition describes.

    Pos

    pub(all) struct Pos {
    line : Int
    col : Int
    } derive(Eq)

    The 1-based line and column of a node's first token. Every node an error can point back at carries one, so a validation or field error can answer with the locations entry the spec requires (§7.1.2).

    QueryField

    pub(all) struct QueryField {
    alias_ : String?
    name : String
    arguments : Array[Argument]
    directives : Array[Directive]
    selection_set : Array[Selection]
    pos : Pos
    }

    A queried field: an optional alias, the field name, arguments, directives and an optional nested selection set. Prints as alias: name(args) @dir { ... }.

    ResolveInfo

    pub(all) struct ResolveInfo {
    parent : Json
    args : Map[String, Json]
    ctx : Json
    field_name : String
    }

    The information a field resolver receives: the resolved parent object (as JSON), the coerced arguments, the shared context value, and the field's name. A resolver returns the field's value as JSON (an object for composite types, whose sub-fields are then resolved against it) and may raise ResolverError to surface a field error.

    ResolveInfo::arg

    fn ResolveInfo::arg(self : ResolveInfo, name : String) -> Json

    Read a named argument as JSON, or Json::null() when it was not supplied.

    Resolvers

    pub struct Resolvers {
    map : Map[String, (ResolveInfo) -> Json raise ResolverError]
    }

    A registry of field resolvers, keyed by "TypeName.fieldName". Fields with no registered resolver fall back to the default resolver, which reads the field's name off the parent JSON object (matching graphql-core's default_field_resolver).

    Resolvers::field

    fn Resolvers::field(self : Resolvers, type_name : String, field_name : String, resolver : (ResolveInfo) -> Json raise ResolverError) -> Unit

    Register resolver for field field_name on type type_name.

    Resolvers::new

    fn Resolvers::new() -> Resolvers

    Create an empty resolver registry.

    ScalarType

    pub(all) struct ScalarType {
    name : String
    serialize : (Json) -> Json
    parse_value : (Json) -> Json
    spec_url : String?
    }

    A custom scalar type with the two coercion hooks strawberry's Scalar carries: serialize maps a resolved value to its output JSON, and parse_value maps an input JSON value (an argument or variable) to the value a resolver sees. Both default to identity when a scalar only renames an existing representation.

    Schema

    pub struct Schema {
    types : Array[ObjectType]
    enums : Array[EnumType]
    unions : Array[UnionType]
    scalars : Array[ScalarType]
    query : String
    mutation : String?
    subscription : String?
    field_directives : Map[String, Array[AppliedDirective]]
    type_directives : Map[String, Array[AppliedDirective]]
    schema_directives : Array[AppliedDirective]
    directive_defs : Array[DirectiveDef]
    }

    A code-first GraphQL schema: composite types (objects, inputs, interfaces), enum types, plus the names of the root operation types. query is required; mutation and subscription are optional (a schema without them cannot run operations of that kind).

    Schema::apply_field_directive

    fn Schema::apply_field_directive(self : Schema, type_name : String, field_name : String, directive : AppliedDirective) -> Unit

    Record an applied directive on the field type_name.field_name.

    Schema::apply_schema_directive

    fn Schema::apply_schema_directive(self : Schema, directive : AppliedDirective) -> Unit

    Record an applied directive on the schema itself (e.g. federation's @link).

    Schema::apply_type_directive

    fn Schema::apply_type_directive(self : Schema, type_name : String, directive : AppliedDirective) -> Unit

    Record an applied directive on the type type_name.

    Schema::directive

    fn Schema::directive(self : Schema, name : String, locations~ : Array[String], args? : Array[(String, GqlType)], is_repeatable? : Bool, on_field? : (Json, Map[String, Json]) -> Json?) -> Unit

    Register a directive definition on the schema. locations lists where it may be used ("FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT", "OBJECT", "FIELD_DEFINITION", ...); args declares its arguments; on_field — when given and the directive targets fields — transforms the resolved value of any field the directive is applied to. The directive then appears in introspection (__schema { directives }) and is accepted by the validator.

    Schema::directive_def_by_name

    fn Schema::directive_def_by_name(self : Schema, name : String) -> DirectiveDef?

    The registered directive definition named name, if any.

    Schema::enum_

    fn Schema::enum_(self : Schema, name : String, values : Array[String], deprecated? : Array[(String, String)], description? : String, descriptions? : Array[(String, String)]) -> Unit

    Declare an enum type with an ordered set of value names. Renders as enum Name { A B } with one value per line. deprecated marks individual values @deprecated, each a (value, reason) pair (as field_args takes argument defaults), surfacing in introspection's isDeprecated.

    Schema::enum_by_name

    fn Schema::enum_by_name(self : Schema, name : String) -> EnumType?

    Look up a declared enum type by name.

    Schema::field_applied_directives

    fn Schema::field_applied_directives(self : Schema, type_name : String, field_name : String) -> Array[AppliedDirective]

    The directives applied to the field type_name.field_name.

    Schema::field_has_directive

    fn Schema::field_has_directive(self : Schema, type_name : String, field_name : String, name : String) -> Bool

    Whether the field type_name.field_name carries the directive name.

    Schema::input

    fn Schema::input(self : Schema, name : String, description? : String) -> ObjectType

    Declare an input object type and return it so fields can be added. Renders as input Name { ... }.

    Schema::interface

    fn Schema::interface(self : Schema, name : String, description? : String) -> ObjectType

    Declare an interface type and return it so fields can be added. Renders as interface Name { ... }; objects declare conformance via implements.

    Schema::new

    fn Schema::new(query? : String) -> Schema

    Create an empty schema whose root query type is query (default Query), with no mutation or subscription root until set_mutation / set_subscription name them.

    Schema::object

    fn Schema::object(self : Schema, name : String, description? : String) -> ObjectType

    Declare an output object type and return it so fields can be added. The type is registered by reference, so later .field(...) calls are seen.

    Schema::scalar

    fn Schema::scalar(self : Schema, name : String, serialize? : (Json) -> Json, parse_value? : (Json) -> Json, spec_url? : String) -> Unit

    Register a custom scalar with its serialize / parse_value hooks. Both default to identity, which is enough for a scalar that only renames JSON it already carries (a DateTime stored as an ISO string, say).

    Schema::scalar_by_name

    fn Schema::scalar_by_name(self : Schema, name : String) -> ScalarType?

    Look up a registered custom scalar by name.

    Schema::set_mutation

    fn Schema::set_mutation(self : Schema, name : String) -> Unit

    Name the root mutation type; it must also be declared with object.

    Schema::set_subscription

    fn Schema::set_subscription(self : Schema, name : String) -> Unit

    Name the root subscription type; it must also be declared with object.

    Schema::to_sdl

    fn Schema::to_sdl(self : Schema) -> String

    Emit the schema as GraphQL SDL: a schema { query: ... } block, then one block per composite type (type / input / interface, with implements and field arguments), then one block per enum type.

    Schema::type_applied_directives

    fn Schema::type_applied_directives(self : Schema, type_name : String) -> Array[AppliedDirective]

    The directives applied to the type type_name.

    Schema::type_by_name

    fn Schema::type_by_name(self : Schema, name : String) -> ObjectType?

    Look up a declared composite type (object / input / interface) by name.

    Schema::type_has_directive

    fn Schema::type_has_directive(self : Schema, type_name : String, name : String) -> Bool

    Whether the type type_name carries the directive name.

    Schema::union

    fn Schema::union(self : Schema, name : String, members : Array[String]) -> Unit

    Declare a union type over the named member object types. Renders as union Name = A | B.

    Schema::union_by_name

    fn Schema::union_by_name(self : Schema, name : String) -> UnionType?

    Look up a declared union type by name.

    Selection

    pub(all) enum Selection {
    FieldSel(QueryField)
    FragmentSpreadSel(String, Array[Directive], Pos)
    InlineFragmentSel(String?, Array[Directive], Array[Selection], Pos)
    }

    One entry in a selection set: a field, a ...Name fragment spread, or an inline ... on Type { ... } fragment.

    Subscribers

    pub struct Subscribers {
    map : Map[String, (ResolveInfo) -> Array[Json] raise ResolverError]
    }

    A registry of subscription source resolvers, keyed by "TypeName.fieldName" on the subscription root type. A source returns the ordered stream of event payloads; each payload is the resolved value of the root field for one event, and its sub-selection is resolved against it like any object value.

    Subscribers::field

    fn Subscribers::field(self : Subscribers, type_name : String, field_name : String, source : (ResolveInfo) -> Array[Json] raise ResolverError) -> Unit

    Register a source stream for the root subscription field field_name on type_name. The source is called once per operation and yields the ordered events to deliver.

    Subscribers::new

    Create an empty subscription source registry.

    SubscriptionResult

    pub(all) enum SubscriptionResult {
    RequestError(Array[GqlError])
    EventStream(Array[Json])
    }

    The outcome of establishing a subscription (GraphQL spec §6.2.3 CreateSourceEventStream + MapSourceToResponseEvent): either the request failed before any event could be produced — a parse, validation, or source-resolution error the client should receive instead of a stream — or the source yielded its ordered { data, errors } response payloads. Keeping the two apart lets a transport (the graphql-transport-ws handler) send a single error message for the former and a run of next messages for the latter, which the flat "one error response in the payload list" shape cannot distinguish.

    Token

    pub(all) struct Token {
    kind : TokenKind
    value : String
    line : Int
    col : Int
    }

    A lexical token: its kind, its text (for value-bearing kinds; unescaped for strings), and the 1-based line/column of its first character.

    TokenKind

    pub(all) enum TokenKind {
    Name
    IntVal
    FloatVal
    StringVal
    BlockStringVal
    Bang
    Dollar
    Amp
    ParenL
    ParenR
    Spread
    Colon
    Equals
    At
    BracketL
    BracketR
    BraceL
    BraceR
    Pipe
    Eof
    } derive(Eq)

    The lexical category of a token. Value-bearing kinds (Name, IntVal, FloatVal, StringVal, BlockStringVal) carry their text in Token::value.

    TypeKind

    pub(all) enum TypeKind {
    Object
    Input
    Interface
    } derive(Eq)

    Which kind of composite type an ObjectType describes: an output object (type), an input object, or an interface.

    TypeRef

    pub(all) enum TypeRef {
    NamedType(String)
    ListType(TypeRef)
    NonNullType(TypeRef)
    } derive(Eq)

    A type reference as it appears in the query grammar: a named type, a list wrapper [T], or a non-null wrapper T!. Distinct from the schema builder's GqlType (which carries a Scalar sugar the parser never sees).

    TypeRef::to_query

    fn TypeRef::to_query(self : TypeRef) -> String

    Print a type reference back to GraphQL notation ([Int!]!).

    UnionType

    pub(all) struct UnionType {
    name : String
    members : Array[String]
    }

    A GraphQL union type: a name and the ordered names of its member object types. A value at a union position is resolved to one member via its __typename, and only inline/named fragments on member types (plus __typename) may select into it.

    Value

    pub(all) enum Value {
    Variable(String)
    IntValue(String)
    FloatValue(String)
    StringValue(String, Bool)
    BooleanValue(Bool)
    NullValue
    EnumValue(String)
    ListValue(Array[Value])
    ObjectValue(Array[(String, Value)])
    }

    A GraphQL input value. Numeric and string literals keep their source text (matching graphql-core, whose IntValueNode.value etc. are strings), so no lossy numeric round-trip is baked into the AST. StringValue's second field is the block-string flag.

    Value::to_query

    fn Value::to_query(self : Value) -> String

    Print a value back to GraphQL literal syntax.

    VariableDefinition

    pub(all) struct VariableDefinition {
    variable : String
    typ : TypeRef
    default_value : Value?
    directives : Array[Directive]
    pos : Pos
    }

    A variable declaration in an operation's (...) list: $name: Type = default with optional default value and directives.

    create_source_event_stream

    fn create_source_event_stream(schema : Schema, resolvers : Resolvers, subscribers : Subscribers, query : String, variables? : Map[String, Json], operation_name? : String?, root_value? : Json, context? : Json) -> SubscriptionResult

    Establish a subscription and materialise its event payloads. Parses and validates query, requires a single-root-field subscription operation, invokes the registered source, and maps every event through execute_subscription_event. Any failure before the first event — parse/validation error, a non-subscription operation, more than one root field, an introspection root, a missing source, or a source ResolverError is a RequestError; success is an EventStream of ordered response payloads.

    execute

    fn execute(schema : Schema, resolvers : Resolvers, query : String, variables? : Map[String, Json], operation_name? : String?, root_value? : Json, context? : Json, extensions? : Map[String, Json]) -> Json

    Execute a GraphQL request end to end: parse query, validate it against schema, select the operation, coerce variables, then walk the selection set with resolvers — returning the { data, errors } response as JSON.

    variables supplies operation variable values, operation_name picks an operation when the document has several, root_value seeds the root object, context is threaded to every resolver, and extensions is the server's own extensions entry on the response (§7.1), emitted when non-empty. The function never raises: parse and validation failures yield an errors-only response, and field errors are collected alongside partial data.

    execute_subscription

    fn execute_subscription(schema : Schema, resolvers : Resolvers, subscribers : Subscribers, query : String, variables? : Map[String, Json], operation_name? : String?, root_value? : Json, context? : Json) -> Array[Json]

    Run a subscription operation and collect its ordered response payloads.

    A thin wrapper over create_source_event_stream: a RequestError collapses to a single { errors } response (the flat shape callers without a streaming transport expect), and an EventStream is returned as-is. variables, operation_name, root_value and context mirror execute.

    graphiql_html

    fn graphiql_html(endpoint? : String) -> String

    The GraphiQL IDE page, wired to fetch against endpoint. This is the same standalone GraphiQL build strawberry serves: React and GraphiQL load from a CDN via an import map, and a fetcher points at the GraphQL endpoint. Splitting the template around the endpoint keeps it a plain string with no interpolation machinery.

    graphql_app

    fn graphql_app(schema : Schema, resolvers : Resolvers, path? : String, graphiql? : Bool) -> (async (
    Scope
    , async () ->
    Event
    , async (
    Event
    ) -> Unit) -> Unit)

    Lift the GraphQL handler onto the load-bearing AsgiApp a server binds to. This is what mooncat (or any moonasgi server) mounts to serve the schema over real HTTP; the request→response logic is shared with graphql_handler, which TestClient exercises without a socket.

    graphql_handler

    fn graphql_handler(schema : Schema, resolvers : Resolvers, path? : String, graphiql? : Bool) -> ((
    Request
    ) ->
    Response
    )

    Build the moonasgi request handler for a schema and its resolvers, following the GraphQL-over-HTTP specification.

    A POST on path executes an application/json body — one request object, or an array of them for a batch. A GET with a ?query= executes a query operation (only a query: a mutation there is a 405); a GET without one serves the GraphiQL IDE when graphiql is true. The response media type is negotiated from Accept: a client asking for application/graphql-response+json gets it, and with it the status codes that separate a request error (400) from a field error (200); anything else gets application/json and a 200. Lift it onto an AsgiApp with graphql_app, or drive it directly with moonasgi's TestClient.

    graphql_ws_handler

    fn graphql_ws_handler(schema : Schema, resolvers : Resolvers, subscribers : Subscribers, root_value? : Json, context? : Json) ->
    WebSocketHandler

    Build a graphql-transport-ws WebSocket handler for schema / resolvers / subscribers. root_value and context are threaded to every operation the way execute threads them. Negotiates the graphql-transport-ws subprotocol when the client offers it.

    The handler owns its connection state, so nothing can drive the protocol's timers: build a GqlWs and take its handler when the server can tick.

    parse

    fn parse(source : String) -> Document raise GqlSyntaxError

    Parse a GraphQL executable document from source text. This is the front half of the executor: parse(query).definitions yields the operations and fragments to validate and execute.

    tokenize

    fn tokenize(src : String) -> Array[Token] raise GqlSyntaxError

    Tokenize an entire source string into a token array ending with Eof. Primarily for tests and tooling; the parser pulls tokens on demand.

    validate

    fn validate(schema : Schema, doc : Document) -> Array[GqlError]

    Validate a whole document against schema, returning all errors found (an empty array means the document is valid and ready to execute).