moongql
A code-first GraphQL library for MoonBit — define object types and fields in code and emit the schema SDL, the way strawberry does for Python. Pure logic, no runtime dependencies.
moon add Lfan-ke/moongql✶ The contract at a glance
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.
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.
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) -> Unit
Add a field with no arguments to this type.
fn ObjectType::field_args( self : ObjectType, name : String, args : Array[(String, GqlType)], typ : GqlType) -> 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.
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 and an ordered list of value names.
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 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.
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]) -> Unit
Declare a union type over the named member object types. Renders as union Name = A | B.
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) -> 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) -> 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) -> 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]) -> Unit
Declare an enum type with an ordered set of value names. Renders as enum Name { A B } with one value per line.
fn Schema::to_sdl(self : Schema) -> String
Emit the schema as GraphQL SDL: a schema { query: ... } block, then one block per composite type (type / input / interface, with implements and field arguments), then one block per enum type.
§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 GqlSyntaxErrorA syntax error raised by the lexer or parser, carrying a message and the 1-based line/column where the offending token starts.
fn GqlSyntaxError::to_string(self : GqlSyntaxError) -> String
Render a syntax error as Syntax error at L:C: message.
impl Show for GqlSyntaxError with fn output(self, logger)
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 GqlSyntaxError
Tokenize an entire source string into a token array ending with Eof. Primarily for tests and tooling; the parser pulls tokens on demand.
§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 Document
A parsed GraphQL document: an ordered list of executable definitions.
enum Definition
A top-level executable definition: an operation or a named fragment.
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 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.
§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 GqlSyntaxError
Parse a GraphQL executable document from source text. This is the front half of the executor: parse(query).definitions yields the operations and fragments to validate and execute.
§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, doc : Document) -> Array[GqlError]
Validate a whole document against schema, returning all errors found (an empty array means the document is valid and ready to execute).
§Validation Rules (spec §5)
The document-level slice of the GraphQL validation table: 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 resolved field, the moongql equivalent of a strawberry SchemaDirective with a resolver-side effect.
struct 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).
fn Schema::directive( self : Schema, name : String, locations~ : Array[String], args? : Array[(String, GqlType)] = [], is_repeatable? : Bool = false, on_field? : FieldDirectiveHook? = None) -> Unit
Register a directive definition on the schema. locations lists where it may be used ("FIELD", "FRAGMENT_SPREAD", "INLINE_FRAGMENT", "OBJECT", "FIELD_DEFINITION", ...); args declares its arguments; on_field — when given and the directive targets fields — transforms the resolved value of any field the directive is applied to. The directive then appears in introspection (__schema { directives }) and is accepted by the validator.
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.
§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 ResolverError to surface a field error.
fn ResolveInfo::arg(self : ResolveInfo, name : String) -> Json
Read a named argument as JSON, or Json::null() when it was not supplied.
suberror ResolverErrorAn 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).
fn Resolvers::new() -> Resolvers
Create an empty resolver registry.
fn Resolvers::field( self : Resolvers, type_name : String, field_name : String, resolver : (ResolveInfo) -> Json raise ResolverError) -> Unit
Register resolver for field field_name on type type_name.
struct GqlError
A GraphQL error entry: a message, an optional response path (string field keys and integer list indices), and optional source locations.
fn execute( schema : Schema, resolvers : Resolvers, query : String, variables? : Map[String, Json] = Map([]), operation_name? : String? = None, root_value? : Json = Json::null(), context? : Json = Json::null()) -> 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, and context is threaded to every resolver. The function never raises: parse and validation failures yield an errors-only response, and field errors are collected alongside partial data.
§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 ResolverError) -> Unit
Register a source stream for the root subscription field field_name on type_name. The source is called once per operation and yields the ordered events to deliver.
fn execute_subscription( schema : Schema, resolvers : Resolvers, subscribers : Subscribers, query : String, variables? : Map[String, Json] = Map([]), operation_name? : String? = None, root_value? : Json = Json::null(), context? : Json = Json::null()) -> Array[Json]
Run a subscription operation and collect its ordered response payloads. Parses and validates query, requires a single-root-field subscription operation, invokes the registered source for that field, then maps every event to a { data, errors } response — returning them in stream order. A parse/validation failure or a missing source yields a single error response. variables, operation_name, root_value and context mirror execute.
§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 hits the backend (strawberry/Facebook DataLoader's prime).
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.
§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 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.
fn graphql_handler( schema : Schema, resolvers : Resolvers, path? : String = "/graphql", graphiql? : Bool = true) -> @moonasgi.Handler
Build the moonasgi request handler for a schema and its resolvers. A GET on path serves the GraphiQL IDE (when graphiql is true); a POST on path executes a GraphQL request and returns { data, errors }. Any other path is a 404 and any other method a 405, both as JSON errors. Lift it onto an AsgiApp with graphql_app, or drive it directly with moonasgi's TestClient.
fn graphql_app( schema : Schema, resolvers : Resolvers, path? : String = "/graphql", graphiql? : Bool = true) -> @moonasgi.AsgiApp
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.
§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 on fields, rendered into the subgraph SDL.
type EntityResolver = (Json, Json) -> Json raise ResolverError
The reference resolver for an entity type: given a representation (a JSON object carrying __typename and the entity's key fields) and the request context, return the fully resolved entity as JSON. This is the subgraph's answer to "you have a key, give me the object" — Apollo's __resolveReference.
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), 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.
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::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::sdl(self : Federation, schema : Schema) -> String
Render the subgraph SDL: the schema as the developer wrote it, annotated with @key on entity types (prefixed extend when the type is an extension), @external on external fields, and with the federation-internal additions (_Service, _Entity, _Any, and the _service / _entities root fields) left out. This is what _service { sdl } returns.
fn Federation::apply( self : Federation, schema : Schema, resolvers : Resolvers) -> Unit
Install the federation machinery onto schema and resolvers: declare the _Any scalar, the _Service type with its sdl field, and the _Entity union over every registered entity type; add the _service and _entities fields to the query root; and register the two resolvers. After this the schema answers a federated query — { _service { sdl } } and _entities(representations:) — through the normal executor.