GraphQL interop#
Meta: this is the page that justifies the language. Lead with "schema-as-stdlib" — when you import Dagger, every type and root function in the schema becomes part of the language. Import wiring (dang.toml, import) lives in Modules and imports; type machinery in Types and nullability; record/object access in Objects (type).
Schema-as-stdlib#
- every imported schema's types are first-class Dang types
- root
QueryandMutationfields become callable functions - enum and input types map directly
- custom scalars become Dang scalars
Calling a field#
let users = users(limit: 10)
- named args: as in the schema
- positional args: in schema-declared order
- a no-arg field calls automatically:
serverInfo.platform(no parens)
Multi-field selection#
user.{{ name, email, posts.{{ title, createdAt }} }}
- multi-field selection uses double braces
.{{ }}, mirroring record literals{{ }}(both infer to the same anonymous structural type) - desugars to a single GraphQL query — the headline feature
- nested selections work to arbitrary depth
- arguments on nested fields:
user.{{ posts(first: 5).{{ title }} }}
- positional args work in nested selections too:
users.{{ posts(1).{{ ... }} }} - selection on a nullable receiver short-circuits: if
userisnull,user.{{ name }}isnull(not an error), and the result type is nullable - the result is a record (
{{ ... }}); access fields by name; see Objects (type) - after selection you only have the fields you selected: accessing an unselected field is a compile error (
field "lives" not found in Cat), even after acasenarrows the type .{{ }}selection is navigation (it reads fields), so it short-circuits on null; the single-brace dot-block.{ }is block application, so it passes the receiver — null included — into the block. Same.-brace surface, different jobs; see Blocks's Dot-block application (piping)
Inline fragments#
node(id: "x").{{
... on User { name, email }
... on Post { title }
}}
- type-conditional selection on unions and interfaces (Interfaces and unions)
- selects different field sets per concrete type; applies to a single value or to a list (maps over elements)
- type conditions resolve against the receiver's (GraphQL) schema, not a local type that shadows the name
- can nest:
... on Post { title, author.{{name}} }, and selections-of-selectionsedges.{{ node.{{ ... on User { ... } }} }} - the union-type result narrows in
case(see Interfaces and unions)
Lazy inline fragments#
- the lazy form
... on User(no field block) narrows the value to that type without selecting fields, returning a chainable lazy value / typed reference - works on a single value or a list (
pets.{{ ... on Cat, ... on Dog }}— comma- or newline-separated); multiple lazy fragments narrow a union - a narrowing that doesn't match returns
null:cat.{{... on Dog}} == null(e.g.node(...).{{... on Post}}when the node is a User) - the non-null form
... on User!asserts and unwraps; a mismatch is a runtime error:inline fragment type assertion failed: expected one of Cat, got Dog - useful for narrowing a GraphQL interface/union value before chaining (
node(id).{{... on User}}.name)
Lists of objects#
users.{{ name, email }}
- applies the selection elementwise; result is
[ {{ name, email }} ](a list of records) - index into the result to force it:
users.{{name}}[0].name; see Collections
Mutations#
- root
Mutationfields are functions like queries - the result is whatever the schema declares
- side effects happen when the call executes, which is when its value is forced
- the laziness/forcing rules below apply to mutations exactly as they do to queries
Laziness: GraphQL field access in Dang is lazy. A GraphQLValue accumulates a query chain (.field, .{{...}} selections, args); no request is sent until the value is forced — i.e. materialized at an expected-type boundary (assertion, print, assignment to a typed field, indexing into a result, etc.). Forcing runs the built-up selection as a single Execute against the endpoint. This is the desugaring that makes user.{{ name, posts.{{ title }} }} one round-trip. See Mutation and copy-on-write for how forcing interacts with side effects.
Errors from the server#
- non-null violations and GraphQL errors raise — catchable via
try/catch(see Errors:try,catch,raise)
Talking to multiple endpoints#
- one
[imports.Name]per endpoint; each becomes its own qualified namespace (config in Modules and imports) - types from different endpoints are distinct even if they share a name (e.g.
Test.User≠Other.User) - a bare unqualified name provided by two imports is ambiguous — must qualify (see Modules and imports)