moongql — a code-first GraphQL library for MoonBit (← strawberry-graphql): define object types and fields in code, emit the schema SDL.
Dependencies
let s = @moongql.Schema::new()
let q = s.object("Query")
q.field("hello", @moongql.NonNull(@moongql.Scalar("String")))
q.field("user", @moongql.Named("User"))
let u = s.object("User")
u.field("id", @moongql.NonNull(@moongql.Scalar("ID")))
u.field("name", @moongql.NonNull(@moongql.Scalar("String")))
u.field("tags", @moongql.ListOf(@moongql.NonNull(@moongql.Scalar("String"))))
let sdl = s.to_sdl()schema {
query: Query
}
type Query {
hello: String!
user: User
}
type User {
id: ID!
name: String!
tags: [String!]
}let doc = @moongql.parse(
"query ($id: ID!) { user(id: $id) { name ...fields @skip(if: false) } }",
)
// doc.definitions[0] is an OperationDefinition; doc.to_query() prints it back.let s = @moongql.Schema::new()
let q = s.object("Query")
q.field_args("user", [("id", @moongql.NonNull(@moongql.Scalar("ID")))],
@moongql.Named("User"))
let u = s.object("User")
u.field("id", @moongql.NonNull(@moongql.Scalar("ID")))
u.field("name", @moongql.NonNull(@moongql.Scalar("String")))
let r = @moongql.Resolvers::new()
r.field("Query", "user", fn(info) {
match info.arg("id") {
String("1") => { "id": "1", "name": "Alice" }
_ => Json::null()
}
})
// User.id / User.name fall back to the default resolver (read off the parent).
let vars : Map[String, Json] = { "uid": "1" }
let res = @moongql.execute(s, r,
"query ($uid: ID!) { hero: user(id: $uid) { id name } }", variables=vars)
// res.stringify() == {"data":{"hero":{"id":"1","name":"Alice"}}}let s = @moongql.Schema::new()
let node = s.interface("Node")
node.field("id", @moongql.NonNull(@moongql.Scalar("ID")))
let q = s.object("Query")
q.field_args(
"user",
[("id", @moongql.NonNull(@moongql.Scalar("ID")))],
@moongql.Named("User"),
)
let u = s.object("User")
u.implements("Node")
u.field("id", @moongql.NonNull(@moongql.Scalar("ID")))
let inp = s.input("UserFilter")
inp.field("name", @moongql.Scalar("String"))
s.enum_("Role", ["ADMIN", "USER"])s.union("SearchResult", ["Book", "Author"])
// { search { __typename ... on Book { title } ... on Author { name } } }s.scalar("DateTime",
serialize=fn(j) { j }, // value -> output JSON
parse_value=fn(j) { j }) // input JSON -> valuelet subs = @moongql.Subscribers::new()
subs.field("Subscription", "messageAdded", fn(info) {
// one Json payload per event, in delivery order
[msg1, msg2, msg3]
})
let payloads = @moongql.execute_subscription(schema, resolvers, subs,
"subscription { messageAdded(channel: \"general\") { id body } }")
// payloads[0].stringify() == {"data":{"messageAdded":{"id":"1","body":"hello"}}}let loader : @moongql.DataLoader[Int, String] = @moongql.DataLoader::new(
fn(ids) { load_authors(ids) }, // called once per pass
fn(id) { id.to_string() }, // cache-key function
)
loader.load(1); loader.load(2); loader.load(1) // three requests, two keys
loader.dispatch() // one batch call for {1, 2}
loader.get(1) // Some("author#1")let handler = @moongql.graphql_handler(schema, resolvers) // path defaults to /graphql
let app = @moongql.graphql_app(schema, resolvers) // the same, lifted onto an AsgiApplet client = @moonasgi.TestClient::new(@moongql.graphql_handler(schema, resolvers))
let body = @utf8.encode(
"{\"query\":\"{ user(id: \\\"1\\\") { name } }\"}",
)
let resp = client.post("/graphql", body~)
// resp.status == 200, resp.text() == {"data":{"user":{"name":"Alice"}}}let fed = @moongql.Federation::new()
fed.entity(name="User", key="id", resolve=(rep, _ctx) => {
let id = match rep {
Object(m) => match m.get("id") { Some(String(s)) => s; _ => "" }
_ => ""
}
{ "id": id, "name": "User#" + id }.to_json() // materialise from the key
})
fed.apply(schema, resolvers)
// { _service { sdl } } -> the subgraph SDL with `type User @key(fields: "id")`
// _entities(representations: [{ __typename: "User", id: "7" }]) -> the User objectlet fed = @moongql.Federation::new(v2=true)
fed.entity(name="Product", key="upc", extends=true, external=["weight"],
resolve=(rep, _ctx) => {
// @requires(fields: "weight"): the gateway ships the external weight in the
// representation, so the resolver can read it to compute the estimate.
let weight = read_int(rep, "weight")
{ "upc": read_str(rep, "upc"), "shippingEstimate": weight * 2 }.to_json()
})
fed.shareable("Product", "name")
fed.override_("Product", "price", from="legacy")
fed.requires("Product", "shippingEstimate", fields="weight")
fed.inaccessible("User", "ssn") // present in the subgraph, hidden from the composed schema
fed.apply(schema, resolvers)s.directive("prefix",
locations=["FIELD"],
args=[("text", @moongql.NonNull(@moongql.Scalar("String")))],
on_field=Some((value, args) => match (value, args.get("text")) {
(String(x), Some(String(p))) => (p + x).to_json()
_ => value
}))
// { greeting @prefix(text: "Hello, ") } -> "Hello, world"| Rule | Spec |
|---|---|
| Operation name uniqueness | §5.2.1.1 |
| Lone anonymous operation | §5.2.2.1 |
| Argument uniqueness | §5.4.2 |
| Fragment name uniqueness | §5.5.1.1 |
| Fragments must be used | §5.5.1.4 |
| Fragment spreads must not form cycles | §5.5.2.2 |
| Directives are defined | §5.7.1 |
| Directives are in valid locations | §5.7.2 |
| Directive repeatability | §5.7.3 |
| Variable uniqueness | §5.8.1 |
| All variables used | §5.8.4 |
pub suberror GqlSyntaxError {
GqlSyntaxError(String, Int, Int)
}impl Show for GqlSyntaxErrorpub(all) suberror ResolverError {
ResolverError(String)
}fn[K, V] DataLoader::new(batch_load : (Array[K]) -> Array[V], key : (K) -> String) -> DataLoader[K, V]pub(all) struct EntityDef {
name : String
key : String
extends : Bool
external : Array[String]
resolve : (Json, Json) -> Json raise ResolverError
}pub struct Federation {
entities : Array[EntityDef]
v2 : Bool
field_dirs : Array[(String, String, AppliedDirective)]
}fn Federation::entity(self : Federation, name~ : String, key~ : String, resolve~ : (Json, Json) -> Json raise ResolverError, extends? : Bool, external? : Array[String]) -> Unitfn Federation::override_(self : Federation, type_name : String, field_name : String, from~ : String) -> Unitfn Federation::provides(self : Federation, type_name : String, field_name : String, fields~ : String) -> Unitfn Federation::requires(self : Federation, type_name : String, field_name : String, fields~ : String) -> Unitfn ObjectType::field_args(self : ObjectType, name : String, args : Array[(String, GqlType)], typ : GqlType) -> Unitpub(all) struct OperationDefinition {
operation : OperationType
name : String?
variable_definitions : Array[VariableDefinition]
directives : Array[Directive]
selection_set : Array[Selection]
}fn Resolvers::field(self : Resolvers, type_name : String, field_name : String, resolver : (ResolveInfo) -> Json raise ResolverError) -> Unitpub struct Schema {
types : Array[ObjectType]
enums : Array[EnumType]
unions : Array[UnionType]
scalars : Array[ScalarType]
query : String
mutation : String?
subscription : String?
field_directives : Map[String, Array[AppliedDirective]]
type_directives : Map[String, Array[AppliedDirective]]
schema_directives : Array[AppliedDirective]
directive_defs : Array[DirectiveDef]
}fn Schema::apply_field_directive(self : Schema, type_name : String, field_name : String, directive : AppliedDirective) -> Unitfn Schema::apply_type_directive(self : Schema, type_name : String, directive : AppliedDirective) -> Unitfn Schema::field_applied_directives(self : Schema, type_name : String, field_name : String) -> Array[AppliedDirective]fn Subscribers::field(self : Subscribers, type_name : String, field_name : String, source : (ResolveInfo) -> Array[Json] raise ResolverError) -> Unitpub(all) enum TokenKind {
Name
IntVal
FloatVal
StringVal
BlockStringVal
Bang
Dollar
Amp
ParenL
ParenR
Spread
Colon
Equals
At
BracketL
BracketR
BraceL
BraceR
Pipe
Eof
} derive(Eq)fn graphiql_html(endpoint? : String) -> Stringmoongql — a code-first GraphQL library for MoonBit (← strawberry-graphql): define object types and fields in code, emit the schema SDL.
Dependencies