GraphQL plugin
A real GraphQL API derived from your models - relations, mutations, subscriptions, and a schema that never lies about what you can see.
umbral-graphql derives a GraphQL schema from the model registry: queries, relation traversal, mutations, and subscriptions, all from models you already declared. Where umbral-rest returns the shape you designed, GraphQL returns the shape the caller designed — so the plugin is built around saying yes deliberately, one capability at a time.
use umbral::prelude::*;use umbral_graphql::GraphqlPlugin; App::builder() .plugin( GraphqlPlugin::new() .expose("post") .expose("auth_user") .hide("auth_user", "email") // exposing a model exposes EVERY column of it .mutable("post"), // now createPost / updatePost / deletePost exist ) .build(){ post(id: "1") { title author { username } comments { body } } }Install
cargo add umbral-graphqlNothing is exposed until you say so
Three separate opt-ins, because the blast radius of each is different:
| Call | Grants |
|---|---|
.expose("post") | read post, and every column on it |
.mutable("post") | createPost, updatePost, deletePost |
.subscribable("post") | live postChanged subscription |
expose does not imply mutable. A read you got wrong leaks data; a write you got wrong destroys it, so you say so again.
Exposing a model exposes all of its columns. Reach for .hide("auth_user", "email") — or #[umbral(private)] on the field — for anything that shouldn't leave the database.
One honest schema
A #[umbral(private)] column can be unlocked per-caller with .allow_private_if(..), the same unlock REST has. The schema stays a single document: the field is always present and always nullable — even when the column is NOT NULL — because a caller without the unlock receives nothing, and "nothing" has to be a legal value.
That is the honest encoding. Introspection is one shared document; a schema that showed a field to some callers and hid it from others would be lying to one of them.
Writes are governed separately: private hides a column from responses, it does not stop it being set. A private column stays in the mutation's input type — so a caller can set cost and still read back null for it. To make a column unsettable from an untrusted body, mark it #[umbral(privileged)].
Relations don't N+1
Relation fields resolve through a per-request DataLoader, so posts { author { .. } } batches the author lookup instead of firing one query per row. The loaders are per-request by construction — a shared cache would serve one caller's unlocked columns to another.
See also
- REST plugin — the same models, a fixed response shape.
- Design rationale:
arch.mdanddocs/specs/in the repository.