Sign in

    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.10.0
    License
    Apache-2.0
    Last updated
    3 days ago
    Downloads
    46

    #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", NonNull(Scalar("String")))
    q.field("user", Named("User"))

    let u = s.object("User")
    u.field("id", NonNull(Scalar("ID")))
    u.field("name", NonNull(Scalar("String")))
    u.field("tags", ListOf(NonNull(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.

    #Packages

    Each stage of a request is its own subpackage, so a program links the stages it uses and no others. The root re-exports all of them, so @moongql.Schema and @moongql.execute work as they always did.

    PackageWhat it holdsDepends on
    moongql/langthe document syntax tree, tokenize, parse, GqlErrornothing
    moongql/schematypes, directives, the built-in scalars and meta-fields, SDLlang
    moongql/validvalidatelang, schema
    moongql/execexecute, subscriptions, introspectionlang, schema, valid
    moongql/federationApollo Federation v1 and v2schema, exec
    moongql/serverthe HTTP endpoint and graphql-transport-wsexec and below, moonasgi
    moongql/loaderDataLoadernothing

    Running a query in-process needs nothing from server, so a test or a command line tool never links moonasgi.

    Constructors are written without the package. pub using re-exports types but not their constructors, so @moongql.NonNull(...) does not resolve. Wherever the expected type is known — a field's type, a match on a value — the bare name does, and reads better: NonNull(Scalar("ID")). The one place it is not known is an error, because raise and catch work on any error; there the subpackage is named, @lang.GqlSyntaxError(...) or @exec.ResolverError(...).

    #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", NonNull(Scalar("ID")))],
    Named("User"))
    let u = s.object("User")
    u.field("id", NonNull(Scalar("ID")))
    u.field("name", NonNull(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", NonNull(Scalar("ID")))

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

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

    let inp = s.input("UserFilter")
    inp.field("name", 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_init → connection_ack, subscribe → a run of next then complete, ping → pong. 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", NonNull(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.

    AppliedDirective

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

    Argument

    A name: value argument on a field or directive.

    DataLoader

    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.

    Definition

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

    Directive

    A directive application: @name(args).

    DirectiveDef

    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

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

    EntityDef

    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.

    EntityResolver

    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.

    EnumType

    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

    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.

    Field

    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.

    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.

    FragmentDefinition

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

    GqlError

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

    GqlSyntaxError

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

    GqlType

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

    GqlWs

    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.

    ObjectType

    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.

    OperationDefinition

    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

    Which kind of operation a definition describes.

    Pos

    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

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

    ResolveInfo

    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.

    ResolverError

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

    Resolvers

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

    ScalarType

    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

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

    Selection

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

    Subscribers

    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.

    SubscriptionResult

    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

    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

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

    TypeKind

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

    TypeRef

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

    UnionType

    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

    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.

    VariableDefinition

    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

    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

    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

    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

    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

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

    validate

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

    Source Files