moongql
A code-first GraphQL library for MoonBit: define object types and fields in code and print the schema SDL, then read SDL back into the same schema.
moon add moonbitstack/moongql✶ A schema in four lines
let s = Schema::new() let q = s.object("Query") q.field("hello", NonNull(Scalar("String"))) let sdl = s.to_sdl() // "schema { query: Query } type Query { hello: String! }"
§Schema & SDL
Code-first GraphQL: build object types and fields, then to_sdl emits the schema SDL. GqlType covers scalars, named refs, NonNull and List wrappers.
enum 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.
fn type_sdl(t : GqlType) -> String
Render a type reference to GraphQL SDL notation (String!, [User!], ...).
struct 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.
fn field_sdl(schema : Schema, f : Field) -> String
Render a single field to SDL, including its argument list when non-empty, each argument's default value (= v), and its @deprecated(reason:). The description and the applied directives live on the schema, so they are the printer's job (see description_sdl and Schema::to_sdl), not this one's. The schema is what a default value is read against.
fn description_sdl(description : String?, indent~ : Int32 = 0) -> String
Render a description as the SDL string literal that precedes the element it describes, followed by a newline — or the empty string when there is none (an empty description is the same thing as no description, which is how introspection reports it too). indent is the element's own indentation, in levels of two spaces.
enum TypeKind
Which kind of composite type an ObjectType describes: an output object (type), an input object, or an interface.
struct 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.
fn ObjectType::field( self : ObjectType, name : String, typ : GqlType, default? : Json, deprecation_reason? : String, description? : String) -> Unit
Add a field with no arguments to this type.
fn ObjectType::field_args( self : ObjectType, name : String, args : Array[(String, GqlType)], typ : GqlType, defaults~ : Array[(String, Json)] = [], default? : 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.
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 { ... }.
struct 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).
struct 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.
struct ScalarType
A custom scalar type and its two coercion hooks: serialize maps a resolved value to its output JSON, and parse_value maps an input JSON value (an argument or a variable) to the value a resolver sees. Both default to identity when a scalar only renames an existing representation.
struct 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).
fn Schema::new(query~ : String = "Query") -> 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.
fn Schema::set_mutation(self : Schema, name : String) -> Unit
Name the root mutation type; it must also be declared with object.
fn Schema::set_subscription(self : Schema, name : String) -> Unit
Name the root subscription type; it must also be declared with object.
fn Schema::type_by_name(self : Schema, name : String) -> ObjectType?
Look up a declared composite type (object / input / interface) by name.
fn Schema::enum_by_name(self : Schema, name : String) -> EnumType?
Look up a declared enum type by name.
fn Schema::union( self : Schema, name : String, members : Array[String], description? : String) -> Unit
Declare a union type over the named member object types. Renders as union Name = A | B, preceded by description when one is given.
fn Schema::union_by_name(self : Schema, name : String) -> UnionType?
Look up a declared union type by name.
fn Schema::scalar( self : Schema, name : String, serialize~ : (Json) -> Json = fn(x)
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).
fn Schema::scalar_by_name(self : Schema, name : String) -> ScalarType?
Look up a registered custom scalar by name.
fn ObjectType::field_by_name(self : ObjectType, name : String) -> Field?
Look up a field on this type by its name.
fn GqlType::named_base(self : GqlType) -> String
The base named type a possibly-wrapped type refers to (unwrapping !/[]).
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.
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 { ... }.
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.
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.
fn kind_keyword(k : TypeKind) -> String
The SDL keyword introducing a composite type of the given kind.
fn directive_def_sdl(schema : Schema, d : DirectiveDef) -> String
Render a directive definition to SDL: directive @name(a: A) repeatable on LOCATION | LOCATION. A definition is what a Schema::directive call registers; the places a directive is *used* are AppliedDirectives, which applied_directive_sdl renders.
fn Schema::to_sdl(self : Schema) -> String
Emit the schema as GraphQL SDL: a schema { query: ... } block, then one block per directive definition, then one block per composite type (type / input / interface, with implements and field arguments), then one block per enum, union and scalar type. Every element carries its description, its @deprecated(reason:), its arguments' default values and its applied directives, so that Schema::of_sdl reading this text back yields the same schema (§3).
extend GqlType with Eq
extend TypeKind with Eq
§Query Lexer
Hand-written GraphQL lexer: turns a query string into names, punctuators, int/float/string tokens (block strings dedented per spec), ignoring whitespace, commas and comments. Pure logic, no core/lexbuf dependency.
suberror RefusedA syntax error raised by the lexer or parser, carrying the fault it found and the 1-based line/column where the offending token starts. It is pub(all) so that any stage reading GraphQL text — the SDL reader included — raises the same error, with the same shape, as the parser. The variant is the kind of fault, not the wording: a caller that wants to say something about it in its own words matches the kind, while message keeps the sentence the parser wrote, which is what a response carries.
fn Refused::at(self : Refused) -> Pos
Where the fault is: the position of the token that could not be used.
fn Refused::message(self : Refused) -> String
What went wrong, in the parser's words.
fn Refused::to_string(self : Refused) -> String
Render a syntax error as Syntax error at L:C: message.
impl Show for Refused with fn output(self, logger)
Prints the line and column the parse gave up at, which is the whole point of hand-writing the parser.
enum TokenKind
The lexical category of a token. Value-bearing kinds (Name, IntVal, FloatVal, StringVal, BlockStringVal) carry their text in Token::value.
struct 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.
fn tokenize(src : String) -> Array[Token] raise Refused
Tokenize an entire source string into a token array ending with Eof. Primarily for tests and tooling; the parser pulls tokens on demand.
extend Refused with Show
extend TokenKind with Eq
§Document AST
The executable-document AST (operations, selection sets, fields, arguments, variables, fragments, directives, input values), with to_query printers that render each node back to canonical GraphQL.
struct 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).
struct Document
A parsed GraphQL document: an ordered list of executable definitions.
enum Definition
A top-level executable definition: an operation or a named fragment. A document may also carry type-system definitions, which are read so that the document can be told they are not executable (§5.1.1) rather than failing to parse; TypeSystemDef is that third case.
struct TypeSystemDefinition
A type-system definition standing in a document: type, interface, union, enum, input, scalar, schema, directive, or extend of any of them. Only its header is kept — the keyword, the name it defines, and where it starts — because a document that holds one is not executable and nothing downstream resolves against it. keyword is the word as written (extend included), name the type or directive it defines ("" for a schema block), and kind the kind word after extend ("" otherwise).
enum OperationType
Which kind of operation a definition describes.
struct 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.
struct VariableDefinition
A variable declaration in an operation's (...) list: $name: Type = default with optional default value and directives.
enum 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).
struct FragmentDefinition
A named fragment definition: fragment Name on Type @dir { selection }.
enum Selection
One entry in a selection set: a field, a ...Name fragment spread, or an inline ... on Type { ... } fragment.
struct QueryField
A queried field: an optional alias, the field name, arguments, directives and an optional nested selection set. Prints as alias: name(args) @dir { ... }.
struct Argument
A name: value argument on a field or directive.
struct Directive
A directive application: @name(args).
enum 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.
fn escape_string(s : String) -> String
Escape a string's contents for printing inside GraphQL double quotes.
fn Value::to_query(self : Value) -> String
Print a value back to GraphQL literal syntax.
fn TypeRef::to_query(self : TypeRef) -> String
Print a type reference back to GraphQL notation ([Int!]!).
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.
extend OperationType with Eq
extend Pos with Eq
extend TypeRef with Eq
§Query Parser
Recursive-descent parser: parse(source) builds a Document from operations (query/mutation/subscription and the { ... } shorthand), fields with aliases/arguments/directives, variable definitions, and named/inline fragments — the front half of the executor.
fn parse(source : String) -> Document raise Refused
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.
§Errors
GqlError, the error shape the specification's response section defines: a message, the path into the result and the source locations. Validation and execution both report in it, which is why it sits with the language rather than with either stage.
struct GqlError
A GraphQL error entry: a message, an optional response path (string field keys and integer list indices), and optional source locations.
fn GqlError::msg(message : String) -> GqlError
Build an error carrying only a message, for the request-level failures that cannot be pinned to a point in the document (an empty document, an unknown operation name).
fn GqlError::at(message : String, pos : Pos) -> GqlError
Build an error carrying a message and the position of the node it is about. §7.1.2 requires locations on every error that can name one.
fn GqlError::to_json(self : GqlError) -> Json
Serialise an error to the GraphQL JSON error shape, omitting an empty path or locations list.
fn refusal(e : Error) -> GqlError
Any fault a stage raised, as the GqlError a response carries. A refusal keeps the wording the parser wrote and the position it gave up at; anything else is reported by its text, with no position to point at.
fn refused(msg : String, at : Pos) -> Refused
The fault msg at the position at, for a stage that reads text with a scanner of its own and has no token kind to name it by.
§Fragments
collect_fragments gathers a document's fragment definitions by name; validation and execution both resolve spreads against it.
fn collect_fragments(doc : Document) -> Map[String, FragmentDefinition]
Build the document's fragment table by name.
§Built-in Types
What the specification fixes for every schema: the five built-in scalars, the meta-fields __typename, __schema and __type, and the introspection object types those meta-fields return.
fn is_builtin_scalar(name : String) -> Bool
The five built-in scalar type names, which are leaves with no declaration.
fn is_meta_field(name : String) -> Bool
Whether name is an introspection meta-field (__-prefixed), which the spec forbids as a subscription root field.
fn introspection_type_def(name : String) -> ObjectType?
The synthetic definition of an introspection type by name, or None. These drive leaf-vs-composite completion when executing an introspection query.
§Validator
Validate a Document against the Schema before execution: fields and arguments exist, leaf vs composite selection sets, fragment spreads and type conditions resolve, and referenced variables are defined. Errors are collected, not raised.
fn validate( schema : @schema.Schema, doc : @lang.Document) -> Array[@lang.GqlError]
Validate a whole document against schema, returning all errors found (an empty array means the document is valid and ready to execute).
§Validation Rules
The document-level slice of the GraphQL validation table (spec §5): operation-name uniqueness and lone anonymous operation, fragment-name uniqueness, no-unused-fragments, fragment-spread cycle detection, and the variable-use collector behind the all-variables-used rule.
§Directives
The directive extension points: AppliedDirective records a directive use on a schema element (federation's @key / @shareable / @inaccessible / @override live here); DirectiveDef declares a custom directive with valid locations, args, repeatability and an optional on_field execution hook that transforms a resolved field value.
struct AppliedDirective
A directive applied to a schema element: its name and its constant arguments as ordered (name, value) pairs. Build one with AppliedDirective::new.
fn AppliedDirective::new( name : String, args~ : Array[(String, Json)] = []) -> AppliedDirective
An applied directive with no arguments, or with the given constant arguments.
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 field after its resolver has run.
struct DirectiveDef
A directive definition: its name, its description, the locations it may appear at (the __DirectiveLocation enum values, e.g. "FIELD", "FIELD_DEFINITION"), its declared args with their default values, 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).
fn Schema::directive( self : Schema, name : String, locations~ : Array[String], args~ : Array[(String, GqlType)] = [], defaults~ : Array[(String, Json)] = [], is_repeatable~ : Bool = false, on_field? : FieldDirectiveHook, description? : String) -> 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. defaults supplies the default value of the arguments a use of the directive may leave out. The directive then appears in introspection (__schema { directives }) and is accepted by the validator.
fn Schema::directive_def_by_name( self : Schema, name : String) -> DirectiveDef?
The registered directive definition named name, if any.
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.
fn Schema::apply_type_directive( self : Schema, type_name : String, directive : AppliedDirective) -> Unit
Record an applied directive on the type type_name.
fn Schema::apply_schema_directive( self : Schema, directive : AppliedDirective) -> Unit
Record an applied directive on the schema itself (e.g. federation's @link).
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.
fn Schema::type_applied_directives( self : Schema, type_name : String) -> Array[AppliedDirective]
The directives applied to the type type_name.
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.
fn Schema::type_has_directive( self : Schema, type_name : String, name : String) -> Bool
Whether the type type_name carries the directive name.
fn json_to_gql_literal(j : Json) -> String
Render a JSON value as a GraphQL literal, for printing an applied directive's arguments into SDL (@override(from: "accounts"), @key(fields: "id name")).
fn applied_directive_sdl(d : AppliedDirective) -> String
Render one applied directive to SDL: @name or @name(a: v, b: w).
fn applied_directives_sdl(ds : Array[AppliedDirective]) -> String
Render a whole applied-directive list, each prefixed with a space (empty for an empty list), for appending after a field or type in SDL.
fn builtin_exec_directive(name : String) -> DirectiveDef?
The definition of a built-in executable directive (@skip / @include), or None for a name that is not a built-in. The validator reads its locations and its if: Boolean! argument; the executor evaluates the directive itself, so no on_field hook is attached.
§Executor
execute() runs a query end to end: parse, validate, then walk the document with a resolver map ((ResolveInfo) -> Json keyed by "Type.field"), supporting variables, aliases, fragments, @skip/@include, non-null error propagation and list/object results — returning { data, errors } as JSON.
struct 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 FieldError to surface a field error.
fn ResolveInfo::reads(self : ResolveInfo, name : String) -> Bool
Whether the document asks for the response key name under this field. A resolver that builds a query from the request — the ORM projection, a Relay node — asks this rather than fetching everything.
fn ResolveInfo::arg(self : ResolveInfo, name : String) -> Json
Read a named argument as JSON, or Json::null() when it was not supplied.
suberror FieldErrorAn error raised by a field resolver; its message is reported in the response errors list with the field's response path.
struct 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). It also holds the two hooks that answer "which object type is this value?" at an interface or union position, the equivalents of graphql-core's resolve_type and is_type_of.
fn Resolvers::new() -> Resolvers
Create an empty resolver registry.
fn Resolvers::field( self : Resolvers, type_name : String, field_name : String, resolver : (ResolveInfo) -> Json raise FieldError) -> Unit
Register resolver for field field_name on type type_name.
fn Resolvers::field_async( self : Resolvers, type_name : String, field_name : String, resolver : async (ResolveInfo) -> Json) -> Unit
Register resolver for TypeName.fieldName as one that waits. The asynchronous engine prefers it over a synchronous registration of the same field; the synchronous engine cannot run it and uses the synchronous one.
fn Resolvers::permission( self : Resolvers, type_name : String, field_name : String, permission : (ResolveInfo) -> Bool) -> Unit
Ask permission before the field is resolved. A field that answers false is refused with a not permitted field error, at the field's own path, and its resolver is never called.
fn Resolvers::hook(self : Resolvers, extension : &Extension) -> Unit
Register a hook (Extension): it sees every field before it is resolved, and the value after it is completed. Tracing, metrics and a field-level cache are extensions rather than branches inside the engine. extend itself is a keyword, so the registration is named after what it adds.
fn Resolvers::resolve_type( self : Resolvers, type_name : String, hook : (Json, Json) -> String?) -> Unit
Answer which object type a value at the interface or union type_name is. The hook receives the value and the request context and returns the object type's name, or None when it is not one this hook knows. It is asked before the value's own __typename, so a resolver that classifies values itself does not depend on the executor's convention.
fn Resolvers::is_type_of( self : Resolvers, type_name : String, hook : (Json, Json) -> Bool) -> Unit
Answer whether a value belongs to the object type type_name — the counterpart of graphql-core's is_type_of, asked of each possible type in turn when neither a resolve_type hook nor a __typename decided it.
fn build_response( data : Json?, errors : Array[@lang.GqlError], extensions~ : Map[String, Json] = Map([])) -> Json
Assemble the final { errors?, data?, extensions? } response object. data is omitted for request-level failures (no execution began) and included (possibly null) once execution starts; errors is omitted when empty, and so is the server's extensions map (§7.1, the entry reserved for whatever a server wants to say alongside the result — timings, a trace id, a cache hint).
fn execute( schema : @schema.Schema, resolvers : Resolvers, query : String, variables~ : Map[String, Json] = Map([]), operation_name? : String, root_value~ : Json = Json::null(), context~ : Json = Json::null(), extensions~ : Map[String, Json] = Map([])) -> 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.
fn operation_type( schema : @schema.Schema, doc : @lang.Document, operation_name : String?) -> Result[@lang.OperationType, @lang.GqlError]
Which kind of operation a request would run, chosen the way execute chooses it. The server needs this before running anything: a subscription goes over a socket, and GraphQL over HTTP refuses a mutation sent by GET.
§Asynchronous Execution
The same document resolved by resolvers that wait: sibling fields run concurrently, mutation roots serially, and @defer / @stream postpone work into later payloads.
async fn execute_incremental( schema : @schema.Schema, resolvers : Resolvers, query : String, variables~ : Map[String, Json] = Map([]), operation_name? : String, root_value~ : Json = Json::null(), context~ : Json = Json::null(), extensions~ : Map[String, Json] = Map([]), limits~ : Limits = limits) -> Array[Json]
Execute a GraphQL request with resolvers that wait, delivering the response in pieces when the document defers or streams work. The payloads are, in order: the initial { data, pending, hasNext } listing what was postponed, then one per round of settled work. A round's payload carries the incremental entries that came back (with the id of the pending entry they fulfil), the pending entries the round opened, and a completed entry per id that is now finished; the last payload answers hasNext: false. A document that defers nothing produces exactly one payload, with no hasNext. A deferred fragment runs in the round that announced it, and so does a deferral a deferral finds; a streamed remainder runs in the round after, since the payload that announces it has nothing of it to send.
async fn execute_async( schema : @schema.Schema, resolvers : Resolvers, query : String, variables~ : Map[String, Json] = Map([]), operation_name? : String, root_value~ : Json = Json::null(), context~ : Json = Json::null(), extensions~ : Map[String, Json] = Map([]), limits~ : Limits = limits) -> Json
Execute a GraphQL request with resolvers that wait, answering one response. This is execute_incremental with incremental delivery switched off: @defer and @stream collapse to their in-line meanings, so the response is the whole result in one payload, shaped exactly like execute's.
§Extensions and Limits
A hook into every field — before it resolves and on its value — plus the limits that bound how much work one request may start.
pub(open) trait ExtensionA hook into field resolution, applied to every field: on_field before the resolver runs (it may refuse, and the refusal is a field error at that field's path), on_result after the value is completed (it may replace it). Tracing, metrics, a per-field cache and Relay's global-id rewriting are all extensions, so the engine stays free of whichever ones a program happens to want.
struct Limits
A limit on how much work one request may start. A deferral past max_defer or a stream past max_stream is a field error rather than an unbounded run.
let limits : Limits
The preset: siblings resolved concurrently but not without bound, and enough room for the deferrals and streamed chunks a real document asks for.
§Event Streams
A live source of subscription events: a pull that waits, a way to stop it, and a channel-backed pairing for producers.
struct Stream
A live source of subscription events: something to pull from, and something to stop. next answers None when the source has ended, which is the signal a transport turns into the terminating complete. The pull is asynchronous because the events are not there yet — they arrive from a queue, a socket, or a database's change feed — and a subscription must not look like a list that was computed up front.
fn Stream::of( pull~ : async () -> Json?, stop~ : () -> Unit = () => ()) -> Stream
A stream over any pull, with a way to stop it. stop is called once, when the subscriber goes away or the stream ends.
async fn Stream::next(self : Stream) -> Json?
The next event, or None when the source has ended. An error the source raises is the subscriber's problem to report: it is not a field error, because the payload it would belong to does not exist.
fn Stream::close(self : Stream) -> Unit
Stop the source. Called on the way out of a subscription: the client sent complete, the socket closed, or the last event had already arrived.
struct Sender
What a producer holds while it feeds a stream. Dropping every sender ends the stream; close ends it at once.
fn Stream::channel() -> (Stream, Sender)
A stream paired with the sender that feeds it. The queue is unbounded, so a producer never blocks on a slow subscriber and never loses an event.
fn Sender::send(self : Sender, event : Json) -> Unit raise
Put an event into the stream. A closed stream refuses it, which is how a producer learns that nobody is listening any more.
fn Sender::close(self : Sender) -> Unit
End the stream: a pending or later next answers None.
§Conversions
ToGql and FromGql carry a MoonBit value into a result and back out of an argument or a parent value; ResolveInfo::read reads an argument as any FromGql type, and field_error turns any error a resolver raised into a field error. The #gql generator writes both traits for each annotated type.
pub(open) trait ToGqlA MoonBit value as a GraphQL result: async-graphql's OutputType. Implemented for the built-in scalars, Int64 (written as a string, since GraphQL's Int is 32-bit), Json, T? and Array[T]; the #gql generator writes it for each annotated type.
pub(open) trait FromGqlA MoonBit value read from an argument or a parent value: async-graphql's InputType. A refusal is a FieldError saying what was expected.
fn FieldError::message(self : FieldError) -> String
The sentence a field error carries, whichever variant it is.
fn FieldError::expected(want : String, got : Json) -> FieldError
The refusal of a value that is not a want, as every [FromGql] here writes it: expected Int, got "x".
impl ToGql for Json with fn to_gql(self)
impl FromGql for Json with fn from_gql(v)
impl ToGql for String with fn to_gql(self)
impl FromGql for String with fn from_gql(v)
impl ToGql for Bool with fn to_gql(self)
impl FromGql for Bool with fn from_gql(v)
impl ToGql for Int32 with fn to_gql(self)
impl FromGql for Int32 with fn from_gql(v)
Refuses a fraction or a number outside 32 bits, as the spec's Int does.
impl ToGql for Double with fn to_gql(self)
impl FromGql for Double with fn from_gql(v)
impl ToGql for Int64 with fn to_gql(self)
impl FromGql for Int64 with fn from_gql(v)
Reads the string Int64 is written as, or a whole number.
impl[T : ToGql] ToGql for T? with fn to_gql(self)
impl[T : FromGql] FromGql for T? with fn from_gql(v)
impl[T : ToGql] ToGql for Array[T] with fn to_gql(self)
impl[T : FromGql] FromGql for Array[T] with fn from_gql(v)
fn[T : FromGql] ResolveInfo::read( self : ResolveInfo, name : String) -> T raise FieldError
The argument name as any [FromGql] type; a missing argument reads as null, so it is None for a T?. A refusal names the argument.
fn[T : FromGql] read(value : Json, key : String) -> T raise FieldError
The member key of an object value as any [FromGql] type, the way a generated input type reads its fields; a missing member reads as null. A refusal names the member.
fn not_permitted() -> FieldError
Any error a resolver raised, as the field error it becomes; a FieldError passes through unchanged.
fn field_error(e : Error) -> FieldError
A resolver's error as a field error: a FieldError as it is, anything else by its text.
§Introspection
Answer __schema / __type / __typename from the schema: the introspection type system materialised as JSON that the executor walks like any object, with ofType wrapper chains, fields, interfaces, possibleTypes and enum values.
§Subscriptions
execute_subscription runs a single-root-field subscription against a source stream (a pull source returning Array[Json]) and returns the ordered { data, errors } payloads a client receives — the sync core the async WebSocket variant wraps.
struct 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.
fn Subscribers::new() -> Subscribers
Create an empty subscription source registry.
fn Subscribers::field( self : Subscribers, type_name : String, field_name : String, source : (ResolveInfo) -> Array[Json] raise FieldError) -> 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.
enum 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.
fn create_source_event_stream( schema : @schema.Schema, resolvers : Resolvers, subscribers : Subscribers, query : String, variables~ : Map[String, Json] = Map([]), operation_name? : String, root_value~ : Json = Json::null(), context~ : Json = Json::null()) -> 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 FieldError — is a RequestError; success is an EventStream of ordered response payloads.
fn execute_subscription( schema : @schema.Schema, resolvers : Resolvers, subscribers : Subscribers, query : String, variables~ : Map[String, Json] = Map([]), operation_name? : String, root_value~ : Json = Json::null(), context~ : Json = Json::null()) -> 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.
fn Subscribers::stream( self : Subscribers, type_name : String, field_name : String, source : (ResolveInfo) -> Stream raise FieldError) -> Unit
Register a *live* source for the root subscription field field_name on type_name: one that hands back a Stream whose events arrive while the subscription is open. A source registered this way is preferred over a materialised one, so a schema can offer both — a list of events for a test, a real feed in production.
struct Live
An established subscription over a live source. next answers the next response payload, or None when the source ended; close stops the source. Establishing one parses, validates and coerce exactly as execute_subscription does, so a request that could never work is refused with its errors before any stream is opened — which is what lets a transport answer a single error message instead of an empty next run.
async fn Live::next(self : Live) -> Json?
The next payload of the subscription, or None when the source has ended.
fn Live::close(self : Live) -> Unit
Stop the subscription: the source is closed and a pending next answers None.
fn subscribe_live( schema : @schema.Schema, resolvers : Resolvers, subscribers : Subscribers, query : String, variables~ : Map[String, Json] = Map([]), operation_name? : String, root_value~ : Json = Json::null(), context~ : Json = Json::null(), limits~ : Limits = limits) -> Result[Live, Array[@lang.GqlError]]
Establish a subscription over a live source, ready to be pumped event by event. The left side is the request failure a client should receive instead of a stream; the right side is the stream of payloads.
§Live WebSocket Endpoint
graphql-transport-ws over a real socket: a task per running subscription, each event written as it arrives.
fn graphql_ws_app( schema : @schema.Schema, resolvers : @exec.Resolvers, subscribers : @exec.Subscribers, root_value~ : Json = Json::null(), context~ : Json = Json::null(), path~ : String = "/graphql", limits~ : @exec.Limits = @exec.limits) -> @moonasgi.AsgiApp
The application that serves graphql-transport-ws on path for schema, resolvers and subscribers. A request that is not a WebSocket upgrade on path is answered with a 404, so one application can be mounted beside a plain HTTP endpoint.
§Asynchronous Endpoint
The ASGI application that runs the async engine, and answers in multipart when the client asks for incremental delivery.
fn incremental_content_type(boundary : String) -> String
The media type of an incremental response: one multipart document, the boundary the parts are separated by, and the version of the format, so a client knows how to read the parts.
fn multipart_part(boundary : String, payload : Json) -> String
One payload as a multipart part: a boundary line, the part's headers, and the payload itself. Parts end with the closing boundary rather than each carrying it, so close_multipart writes the last one.
fn multipart_close(boundary : String) -> String
The closing boundary of a multipart document.
fn graphql_app( schema : @schema.Schema, resolvers : @exec.Resolvers, path~ : String = "/graphql", ide~ : Ide = site, limits~ : @exec.Limits = @exec.limits) -> @moonasgi.AsgiApp
Lift the GraphQL handler onto the load-bearing AsgiApp a server binds to. A POST on path runs the request with the asynchronous engine: one JSON response, or — when the client's Accept asks for multipart/mixed — the initial payload followed by one part per deferred fragment or streamed list item. A GET serves the IDE, or runs a query operation with ?query=. Mount it in mooncat (or any ASGI server) where the schema should be reachable; graphql_handler is the same endpoint as a plain request-to-response function.
§Client
A GraphQL client over HTTP: the request body, the answer read back into errors, and a document checked before it is sent.
suberror RefusedWhy a call did not produce a result: the document is not a GraphQL document, the request did not complete, the answer is not a GraphQL response, or the server ran the operation and refused it.
fn Refused::to_string(self : Refused) -> String
A refusal in words: what failed, and what the server said about it.
struct Client
A GraphQL endpoint and what every request to it carries.
fn Client::new( url~ : String, headers~ : Array[(String, String)] = [], timeout? : Int32) -> Client
A client for url, sending headers with every request — an authorization, an accept, a trace id.
fn request_body( query : String, variables~ : Map[String, Json] = Map([]), operation_name? : String) -> Json
The body of a request: the document, its variables and the operation to run when the document holds several.
fn errors_of(answer : Json) -> Array[@lang.GqlError]?
The errors an answer carries, or None when it carries none. An answer with errors and data is a partial result, not a failure, so the two are read apart.
fn check(query : String) -> Unit raise Refused
Check that query is a GraphQL document before spending a round trip on it. Only the document's own syntax is checked — a client has no schema to check fields against — and the refusal names the position.
§Database
A pool of connections behind a resolver: one checked out per statement and given back on every path, a statement counter, and a session for a unit of work that spans several statements.
struct Database[D]
A database behind a connection pool: what a resolver asks for a row, rather than the connection it would otherwise open itself. A pool is the first half of the cost of a query — opening a connection is a handshake — and a checked-out connection is the unit of isolation: each query takes one, uses it, and gives it back, so two fields resolving at the same time never share a session, and neither sees the other's uncommitted work. The second half is the statement itself: [Database::select] builds the column list out of the document's own selection, because a resolver is the only place that knows what the client asked for, and [Database::batch] answers a whole list's worth of lookups in one statement instead of one per item.
fn[D] Database::new( open : () -> D raise @moondb.DbError, max_size~ : Int32 = 8, pre_ping~ : Bool = true, dialect~ : @query.Dialect = @query.Sqlite) -> Database[D] raise @moondb.DbError
Open a pooled database. open is called only when the pool has no idle connection and is still below max_size, so it may open a file, perform a handshake, or return a mock in a test. max_size is the ceiling on connections open at once, which is also the ceiling on queries in flight: a resolver waits on the pool rather than on the database. pre_ping probes an idle connection before handing it out, so a connection the server dropped is replaced instead of failing a field.
fn[D] Database::statements(self : Database[D]) -> Int32
The statements this database has issued, over its whole life.
fn[D] Database::reset_statements(self : Database[D]) -> Unit
Forget the statements so far, so a caller can measure one request.
fn[D] Database::connections(self : Database[D]) -> (Int32, Int32)
How many connections are open, and how many of them are checked out right now. A field that forgot to give its connection back shows up in in_use.
fn[D : @moondb.Driver, R] Database::session( self : Database[D], f : (@moonorm.Session) -> R raise) -> R raise @exec.FieldError
Run one unit of work on a connection of its own, and give the connection back whichever way it ends: a resolver that raises still returns what it borrowed. The unit of work is a Session, so it can read rows, write them, and hold a transaction across several statements — the connection is the boundary, not the statement. A database failure becomes a FieldError, which is the error a field reports at its own path.
fn[D : @moondb.Driver, R] Database::with_connection( self : Database[D], f : (D) -> R raise) -> R raise @exec.FieldError
Run f with one connection checked out, for what only the driver can do — creating a schema whole, a prepared statement, a statement a Session does not model. The connection goes back whichever way f ends.
fn[D : @moondb.Driver] Database::rows( self : Database[D], sql : String, params~ : Array[@moondb.Value] = []) -> Array[@moondb.Row] raise @exec.FieldError
Run sql on a connection of its own and answer its rows.
fn[D : @moondb.Driver] Database::execute( self : Database[D], sql : String, params~ : Array[@moondb.Value] = []) -> @moondb.ExecResult raise @exec.FieldError
Run sql on a connection of its own and answer what it changed.
§Select and Projection
SQL built from the document's selection: only the columns asked for are fetched, plus the keys a child field needs; rows become JSON objects the default resolver can read.
fn row_json(row : @moondb.Row) -> Json
A row as a JSON object named by its columns, which is the shape the default field resolver reads a parent object from.
fn[D : @moondb.Driver] Database::select( self : Database[D], info : @exec.ResolveInfo, table : String, columns : Array[String], predicate~ : String = "", params~ : Array[@moondb.Value] = [], order_by~ : String = "", limit~ : Int32? = None, keep~ : Array[String] = []) -> Array[Json] raise @exec.FieldError
Read one table for the field whose selection info carries, answering rows as JSON objects. Only the columns the document asks for are fetched. ``moonbit nocheck r.field_async("Query", "posts", fn(info) { Json(db.select(info, "posts", ["id", "title", "author_id", "body"])) }) ` WHERE takes ? for each of params, in order; limit` is a number written into the statement because a LIMIT is not a value a driver binds portably.
fn[D : @moondb.Driver] Database::select_one( self : Database[D], info : @exec.ResolveInfo, table : String, columns : Array[String], predicate~ : String, params~ : Array[@moondb.Value] = [], keep~ : Array[String] = []) -> Json? raise @exec.FieldError
One row as a JSON object, or None when the predicate matched nothing.
fn[D : @moondb.Driver] Database::rows_by_key( self : Database[D], table : String, columns : Array[String], key_column : String, keys : Array[String]) -> Map[String, Json] raise @exec.FieldError
The rows whose key_column is one of keys, in one statement, keyed by that column — the load half of [Batch]. The key column is always fetched, even when the document does not ask for it, because it is how a row finds the item it belongs to.
§Attaching a List's Children
One statement for a whole list's children: the field that produced the list fetches what its children need, so the child field costs nothing and the list does not cost N statements.
fn[D : @moondb.Driver] Database::attach( self : Database[D], parents : Array[Json], field : String, table : String, columns : Array[String], parent_key~ : String, child_key~ : String = "id") -> Array[Json] raise @exec.FieldError
Fetch and attach a field's rows for a whole list at once. N+1 is a shape, not a mistake: { posts { author { name } } } runs author once per post. A single Post.author resolver cannot see the other posts — it is handed one parent and has to answer — so the only place that knows the list is the field that produced it. Here that is Query.posts, and it attaches the authors in one statement: ``moonbit nocheck r.field("Query", "posts", fn(info) { let rows = db.select(info, "posts", ["id", "title", "author_id"], keep=["author_id"]) Json(db.attach(rows, "author", "authors", ["id", "name"], parent_key="author_id")) }) ` Post.author then needs no resolver at all: the default resolver reads the attached object off the parent, so the field costs nothing and the list costs one statement however long it is. The same call answers a single parent — one IN` with one value. Keys are deduplicated before the statement, so ten posts by one author fetch that author once.
§SQLite
The ORM layer on a real SQLite file: a pooled driver, a script runner for the schema, and the native-only half of the integration.
fn open( path : String, max_size~ : Int32 = 1) -> @orm.Database[@moonsqlite.SqliteDriver] raise @exec.FieldError
A pooled database on a SQLite file, or on :memory: for a test. max_size is one by default, and that default is not timidity: every connection to :memory: is a database of its own, so a pool larger than one would give a resolver a table it cannot see. Raise it only for a file, and only where several connections to the same file are what you want.
fn script( db : @orm.Database[@moonsqlite.SqliteDriver], sql : String) -> Unit raise @exec.FieldError
Run a script — DDL, seed data — as one unit of work, so a schema is created whole or not at all.
§DataLoader
Batch-and-cache loader for the N+1 problem: keys requested in a resolution pass are queued and deduped, then one dispatch runs the batch function over the distinct keys and fills a per-loader cache. Includes prime, clear and load_many/load_now.
struct DataLoader[K, V]
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.
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.
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.
fn[K, V] DataLoader::load_many( self : DataLoader[K, V], ks : Array[K]) -> Unit
Queue several keys for the next 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.
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.
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.
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 reaches the batch function.
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.
fn[K, V] DataLoader::clear_all(self : DataLoader[K, V]) -> Unit
Drop the whole cache.
§Schema Reference
The documentation page an endpoint serves when the Ide is `reference`: the root types first, then every other type grouped by kind, each field with its arguments, defaults, description and deprecation, and every type name a link. Rendered from the schema the handler already holds, so the page fetches nothing and runs no operation.
fn reference_html( schema : @schema.Schema, endpoint~ : String = "/graphql") -> String
The documentation page for schema, served at endpoint.
§Model Context Protocol
The same schema offered to an agent: every root field becomes a tool whose input schema is built from the field's arguments, plus a graphql_query tool for documents the agent writes, the SDL as a resource, and an Exposure record that decides how much of the schema is visible.
struct Exposure
How much of a schema to offer. The defaults are the cautious end: no mutations (a tool call is not a confirmation), two levels of selection, and at most a dozen fields per level, which keeps an answer small enough to read.
let exposure : Exposure
The preset: no mutations, two levels deep, twelve fields wide.
struct Mcp
A schema and the resolvers behind it, offered over MCP.
fn Mcp::of( schema~ : @schema.Schema, resolvers~ : @exec.Resolvers, exposure~ : Exposure = exposure) -> Mcp
Offer schema, answered by resolvers, with exposure deciding how much of it is visible.
fn Mcp::server( self : Mcp, name~ : String, version~ : String = "1.0.0") -> @moonmcp.Server
An MCP server with this schema attached, ready to be mounted: pass it to Server::to_asgi for Streamable HTTP, or to run_stdio for a client that starts it as a subprocess. The instructions the server reports are the SDL, so an agent that reads them can write documents for graphql_query without another round trip.
fn Mcp::attach(self : Mcp, server : @moonmcp.Server) -> Unit
Add this schema's tools, and the SDL as a resource, to a server the caller already holds. Registering onto a server of one's own is how a schema joins the tools a program offers for other reasons.
§HTTP Endpoint
graphql_handler / graphql_app wire a schema onto the moonasgi seam: GET serves the GraphiQL IDE, POST reads a { query, variables, operationName } body and returns { data, errors }. Plugs into mooncat and runs on every backend through moonasgi's TestClient.
fn graphiql_html(endpoint~ : String = "/graphql") -> String
The GraphiQL IDE page, wired to fetch against endpoint. This is what site (the default Ide) serves; the pages are built in ide.mbt.
fn graphql_handler( schema : @schema.Schema, resolvers : @exec.Resolvers, path~ : String = "/graphql", ide~ : Ide = site) -> @moonasgi.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 IDE ide names (the MoonQL page by default, or off for a 405). 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.
§Apollo Federation
Federation v1 and v2 subgraph support: register @key entities and their reference resolvers, then apply installs _service { sdl }, the _Any scalar, the _Entity union and the _entities(representations) resolver. v2 opts in with @link and adds @shareable / @inaccessible / @override / @requires / @provides / @tag on fields and types, rendered into the subgraph SDL. check reads every @key / @requires / @provides field set back against the schema.
type EntityResolver = (Json, Json) -> Json raise @exec.FieldError
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.
struct 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.
struct 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 / @tag), and the type- and field-level federation directives it declares. Build it with new, register entities with entity, mark fields with shareable / inaccessible / override_ / requires / provides / tag, then apply it to a schema and resolver map.
fn Federation::new(v2~ : Bool = false) -> 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).
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.
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.
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.
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.
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.
fn Federation::tag( self : Federation, type_name : String, name~ : String) -> Unit
Mark the type type_name @tag(name: tag_name) (v2) — a label the router can filter or route by. @tag is repeatable, so a type may carry several.
fn Federation::tag_field( self : Federation, type_name : String, field_name : String, name~ : String) -> Unit
Mark type_name.field_name @tag(name: tag_name) (v2), repeatable like the type-level form.
fn Federation::entity( self : Federation, name~ : String, key~ : String, resolve~ : EntityResolver, extends~ : Bool = false, 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.
fn Federation::check( self : Federation, schema : @schema.Schema) -> Array[String]
Everything a subgraph has to get right before a gateway can compose it: an @requires or @provides field set whose paths exist, the entity types the subgraph registers, and the directives it declares. Answers one line per problem, empty when there is none. @requires needs nothing at run time beyond what the executor already does — the gateway sends the named fields in the representation, and _entities keeps them on the value the entity resolver returned, so the resolver of the requiring field can read them. @provides needs nothing either: the fields are resolved here like any others, and this check is what lets the gateway rely on that.
fn Federation::sdl(self : Federation, schema : @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.
fn Federation::apply( self : Federation, schema : @schema.Schema, resolvers : @exec.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.
§Built-in IDE
The IDE the endpoint serves on a GET without a query: GraphiQL, Apollo Sandbox or Altair, chosen with one value. Each page is built here with its assets pinned to an exact version, and the subscription socket is derived from the page's own location unless you name one.
enum IdeKind
Which IDE the endpoint serves.
struct Ide
The IDE a GET without a ?query= is answered with. subscription_endpoint is where the IDE opens its WebSocket: left as None, each page derives one from the page's own location (the same host, wss: over HTTPS), which is right for an endpoint served next to the IDE.
let site : Ide
The MoonQL page: the library's own IDE, and the default.
let graphiql : Ide
GraphiQL, the reference IDE.
let apollo_sandbox : Ide
Apollo Sandbox, embedded as an iframe.
let altair : Ide
Altair, the IDE with environments and saved collections.
let reference : Ide
The schema reference: a documentation page built from the schema, for readers rather than for running operations.
let off : Ide
No IDE at all: a GET without a ?query= is a 405.
fn Ide::is_off(self : Ide) -> Bool
Whether this configuration serves no IDE.
fn Ide::title(self : Ide) -> String
The page title the IDE's HTML carries.
fn Ide::html( self : Ide, endpoint~ : String = "/graphql", schema? : @schema.Schema) -> String
The full HTML page mounting this IDE against endpoint. Off has no page, and answers the empty string. schema is what the reference page is rendered from; the four IDEs do not read it, since they introspect the endpoint the way any client does. A reference page asked for without a schema says so rather than pretending to be one.
extend IdeKind with Eq
§Type System Checks
Schema::check asks the type system's own rules (spec §3) of a schema rather than of a document: names, field and argument sides, input-object cycles, interface implementation consistency, union members, enum values, directive definitions and the root operation types. One message per problem, empty when the schema holds.
fn Schema::check(self : Schema) -> Array[@lang.GqlError]
Every problem the type system has with this schema, in the order the specification states the rules: names, then each shape of type, then the interfaces over them, then the roots that tie them together. An empty array means the schema is one the specification allows. The rules are §3's, not §5's: this asks whether the schema itself holds together, where validate asks whether a document is right for it.
§Reading SDL
The other direction: build a Schema from SDL text. print-then-parse is a round trip, so a schema written as SDL and the same schema built in code print identically; a definition that stops mid-way reports the line and column where it stopped.
fn Schema::of_sdl(text : String) -> Schema raise @lang.Refused
Build a schema from a type-system document (SDL), the inverse of to_sdl. A document that is not a well-formed type-system definition raises Refused carrying the line and column of the token that stopped it. The hooks SDL cannot carry come back as the identity: a custom scalar's serialize / parse_value, and a directive definition's on_field. ``mbt nocheck let s = Schema::of_sdl( ( #|schema { query: Query } #| #|"The root of everything." #|type Query { #| "Greets you." #| hello(name: String = "world"): String! #|} ), ) // s.type_by_name("Query") carries the description and the default value. ``
fn Schema::add_sdl( self : Schema, text : String) -> Unit raise @lang.Refused
Add the definitions in text to this schema, beside what it already has, so a schema can be part SDL and part built in code.
§graphql-transport-ws
The WebSocket subprotocol a subscription client speaks: connection_init and its ack, ping/pong, subscribe with single-use operation ids, and the next / error / complete messages a running operation produces.
struct 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.
fn GqlWs::new( schema : @schema.Schema, resolvers : @exec.Resolvers, subscribers : @exec.Subscribers, root_value~ : Json = Json::null(), context~ : Json = Json::null(), init_timeout~ : Int64 = 3000, ping_interval~ : Int64 = 12000) -> 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.
fn GqlWs::recv( self : GqlWs, msg : @moonasgi.WsMessage) -> Array[@moonasgi.WsSend]
Handle one inbound message, returning the frames to send back.
fn GqlWs::tick(self : GqlWs, now : Int64) -> Array[@moonasgi.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.
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".
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.
fn GqlWs::handler(self : GqlWs) -> @moonasgi.WebSocketHandler
The connection as a moonasgi WebSocketHandler, negotiating the graphql-transport-ws subprotocol when the client offers it.
fn graphql_ws_handler( schema : @schema.Schema, resolvers : @exec.Resolvers, subscribers : @exec.Subscribers, root_value~ : Json = Json::null(), context~ : Json = Json::null()) -> @moonasgi.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.