2 Commits
Author SHA1 Message Date
zeekay 5331b2e456 One GraphQL engine: a resolver is handed the whole selection
A graph served for something else (MountGraphFor) now gets what a
second engine had been written for:

- The request is gathered before anything resolves: fragments spread
  once per selection set, cycles and unknown fragments refused at
  parse, @skip and @include decided (on inline fragments too), fields
  under one key merged, variables put in place. A Resolver receives
  the field as a Selection with its whole subtree, so it can fetch
  only what is selected or refuse a request too deep to answer.
- An Object answer is asked for each selected field with that field's
  own arguments, so nesting can be traversal. Selection.Bind decodes
  arguments into a struct, refusing unknown ones.
- The schema is rendered per request with the caller's context, and a
  schema that fails is a 500 that says so.
- Bounds on the gathered tree refuse whole: 64 root fields, 10000
  fields in all, depth 64.
- A query's root fields resolve at once; a mutation's in order. A
  panic fails its field.
- Keys go out in the order selected. A given variable the operation
  does not declare is ignored, and a declared one given nothing leaves
  its argument out, both as the specification says.
- MountGraphFor's routes are declared, so a host routes to them and a
  document describes them; Headers(ctx) relays a request whole.
2026-10-18 20:08:59 -07:00
zeekayandHanzo Dev 41925ad262 Serve the graph projection: GET the schema, POST a query
GraphQLSDL published what the ops describe; nothing ran it. This adds the
executor and mounts both halves on one path.

The resolver is op.direct, so a field runs the contract the REST route runs:
validate, authorize, handler. Two properties are enforced rather than merely
published, and each has a test that fails when the enforcement is removed:

  - A query operation resolves names against GET ops only. A mutating op is
    absent from the index a query searches, so it cannot be invoked by one.
  - A `header:` field is ambient. The schema does not publish it as an
    argument, an argument naming it is refused, and its value is bound from
    the request after the arguments — what the gateway sets, a client cannot
    supply.

The projection carries the Go type alongside the decoded value, which is what
separates a field absent because it was empty (null) from a field absent from
the type (an error). Without it every misspelling silently answers null.

The reader is hand-written: a dependency here is a dependency in every service
that links zip, to read a grammar whose executable half is six token kinds and
five productions. It takes operations, variables, aliases, fragments, inline
fragments and @skip/@include. Subscriptions and introspection are refused by
name — no op is a stream, and the schema is published whole at GET.

Also corrects the schema walk to use wireFields, so an embedded struct's
promoted fields appear, and renders time.Time as String rather than by fields
that never cross the wire.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-21 22:10:11 -07:00