vyrn

std/graphql

12 exports, from std/graphql.vyrn.

The contract as a GraphQL SDL document, built on generator imports. sdl(contract) returns a module exporting sdlText() -> String, baked at generation time from the reflected type sources and procedure signatures.

import { sdl } from "std/graphql"
import { sdlText } from sdl("./contract")

Mapping rules:

  • a record becomes a type and an input twin (Book, BookInput); a non-Option field is non-null (!).
  • Int64 and the sized ints => Int, Float64/Float32 => Float, String => String, Bool => Boolean.
  • a validated scalar (BookId = Int64 where value >= 0) becomes a named scalar whose description carries its base and constraint.
  • Map<String, V> becomes scalar JSON; a named map alias its own scalar.
  • a payload enum (including Result<A, B>) becomes an object with one nullable field per variant (nullary => Boolean, one payload => its type, more => JSON); a nullary-only enum is a GraphQL enum.
  • a mut fn is a Mutation field, any other procedure a Query field (FnInfo.mutates). A procedure with a parameter takes (input: <Req>Input). An empty Mutation is omitted.
  • an object with no fields gets _placeholder: Boolean, since GraphQL requires one field; the executor answers it too.
  • a name the document would define twice (Query, Mutation, JSON, or Foo beside FooInput), or one starting with __, is reported, not renamed.
  • /// docs on types become descriptions. Reflection carries no procedure docs, so operations have none.

graphqlServer emits graphqlHandle(req) -> Option<Response> answering POST /graphql. It walks the same iface.functions and bakes its type graph from the same gqlMembers, so schema and executor agree by construction. The graph keeps each field's ! markers, which decide how far a null propagates.

Dependencies

The import graph shows the whole library.

Imported by
gen fn sdl(contract: String) -> String

Emits a module exporting sdlText() -> String, the contract's SDL as a constant.

type GqlSel = {
    key: String,
    name: String,
    args: Array<JsonField>,
    subs: Array<GqlSel>,
}

One selected field: the response key (the alias, else the field name), the field it names, its arguments as a JSON tree, and its subselections.

type GqlQuery = { root: String, sels: Array<GqlSel>, err: String }

A parsed operation: its root ("query" or "mutation"), its fields, and err != "" when it did not parse.

fn gqlParseQuery(src: String) -> GqlQuery

One executable operation: an optional query or mutation keyword and name, then a selection set. A second operation is refused.

type GqlErr = { message: String, path: Array<Json> }

One GraphQL error: its message and the response path to the fault, response keys and list indices as JStr and JNum in one array.

type GqlOut = { value: Json, errs: Array<GqlErr>, failed: Bool }

A completed position: its value, every error at or below it, and whether its null is still climbing (failed), as a fault under a non-null position does.

fn gqlProject(
    v: Json,
    sels: Array<GqlSel>,
    tref: String,
    path: Array<Json>,
    schema: fn(String, String) -> String,
) -> GqlOut

Completes one field position: projects sels out of v at path, against tref, the field's type reference with its wrappers. An empty sels takes the value whole.

This is a projection over a value the procedure already computed: a leaf is omitted, not avoided. A lazy field (.lazy) would be the one case where omission must also skip the work.

A fault becomes a null at its position and climbs through non-null (!) positions. This is the one place a climb stops: at a nullable position.

fn gqlQueryText(body: String) -> String

The query text of a request body: the query member of a JSON envelope, or the body itself (application/graphql). A GraphQL document is not JSON, so the two cannot be confused.

fn gqlErrorBody(message: String) -> String

{"errors":[{"message":"..."}]}: a fault before execution (an unparseable document, a second operation, a validation fault), with no data. A 200, because a GraphQL client reads errors, not the status line.

fn gqlAnswer(
    body: String,
    resolve: fn(String, String, Array<JsonField>) -> Result<Json, String>,
    schema: fn(String, String) -> String,
) -> Response

Answers one GraphQL request body with the two baked tables: resolve (root, field, args -> the encoded value or why not) and schema, the type graph. The whole tree is validated before any procedure runs, so a typo'd subfield of a mut fn never performs the mutation. Each root field then completes on its own: a fault puts its message in errors at its path and leaves the siblings in data. Argument decode faults surface per field.

gen fn graphqlServer(contract: String) -> String

Emits a module exposing graphqlHandle(req: Request) -> Option<Response>, answering POST /graphql for the contract sdl(contract) documents. It mounts beside rpcHandle and connectHandle. The generator and its module link into one flat namespace, so the generator is not named graphqlHandle.

Tests

31 test blocks in std/graphql.vyrn. CI runs all of them.

Test names
  • gqlPrim maps the honest scalar table
  • gqlSplitDecl splits name and rhs
  • gqlBetweenAngles and gqlStripWhere
  • gqlSplitTop respects angle and paren nesting
  • gqlSplitTop is string-literal aware — a predicate string never splits
  • gqlSplitTop clamps bracket depth at zero
  • gqlEscTripleQuote escapes only triple quotes
  • gqlDescBlock keeps a quote-free body single-line, pads a quote-ending body
  • gqlDescBlock keeps a trailing backslash away from the closing delimiter
  • gqlParseQuery reads a nested selection set
  • gqlParseQuery takes an operation type and treats commas as whitespace
  • gqlParseQuery reads an alias as the response key, not as the field
  • gqlParseQuery reads arguments as the JSON tree fromJson decodes
  • a query nested past gqlMaxDepth is an err, not a trap
  • gqlParseQuery refuses what it cannot mean rather than dropping it
  • gqlProject omits what was not selected
  • gqlProject answers under the alias the query wrote
  • gqlProject takes a leaf whole and answers null for a DECLARED member the value lacks
  • gqlProject reports a member the schema does not declare — M1's first hole
  • gqlProject refuses a selection on a scalar — M1's second hole
  • a null stops at the nearest NULLABLE ancestor and the siblings survive
  • gqlProject refuses an argument written where the schema declares none
  • gqlArgOf takes the declared argument and names any other
  • gqlQueryText reads an envelope or a bare document
  • gqlErrorBody is the shape a client reads
  • a typo'd subfield of a mut fn is refused BEFORE the procedure runs
  • validation descends: a typo two levels down refuses before any resolver runs
  • a valid operation still reaches its resolvers
  • a response key may be selected once
  • an argument may be written once
  • { _placeholder } on an empty query root is answered, not refused

Examples

2 programs in examples/ import this module. Each runs as WebAssembly and as a native binary.