# grafilter
A Rust library for building GraphQL proxies with field-level interception. Sits between your client and an upstream GraphQL server, letting you intercept, transform, or fully resolve individual fields — all through a single unified API.
## Why?
Existing GraphQL middleware libraries either wrap local resolvers (not proxies) or operate at the schema level (rename types, filter fields). None of them give you a single closure where you decide *at runtime* whether to call upstream, resolve locally, or transform the response.
grafilter fills that gap. One `intercept` call, one closure, full control:
- **Resolve locally** — return a value without ever hitting upstream
- **Forward and transform** — call upstream, then modify the result
- **Conditional forwarding** — check a cache first, only call upstream on miss
- **Access parent fields** — read sibling fields from the parent object
- **Per-request state** — inject request-scoped data (tenant ID, auth, etc.) accessible in every interceptor
## Quick start
```rust
use grafilter::{Grafilter, Executor, Error};
use serde_json::{json, Value};
use std::sync::Arc;
// You provide the executor — any async function that sends a query
// string to your upstream GraphQL server and returns the JSON response.
// your HTTP client call here
todo!()
})
});
let proxy = Grafilter::builder(executor)
.intercept(("user", "email"), |ctx| async move {
// Mask the email before returning to the client
let original = ctx.forward().await?;
let masked = original.as_str().unwrap_or("").replace("@", " [at] ");
Ok(json!(masked))
})
.build();
let result = proxy
.execute("{ user { name email } }", &json!({}), ())
.await?;
```
## Schema-aware type matching
Without a schema, interceptors match by parent field name: `("user", "email")` intercepts `email` under the `user` field. This works but breaks if the same type appears under different field names (e.g., `author` is also a `User`).
With a schema, interceptors match by actual GraphQL type name:
```rust
// Fetch the introspection result from your upstream server
let schema_json = my_client
.post(url, grafilter::introspection_query())
.await?;
let proxy = Grafilter::builder(executor)
.with_schema(&schema_json)?
// Matches email on ANY User, whether it's under "user", "author",
// "assignee", etc.
.intercept(("User", "email"), |_ctx| async move {
Ok(json!("***@***.com"))
})
.build();
```
## Per-request state
Similar to Axum's `.with_state()`, you can pass request-scoped state that every interceptor can access:
```rust
#[derive(Clone)]
struct RequestCtx {
tenant_id: String,
user_role: String,
}
let proxy = Grafilter::<RequestCtx>::builder(executor)
.intercept(("User", "email"), |ctx| async move {
if ctx.state().user_role == "admin" {
// Admins see the real email
ctx.forward().await
} else {
Ok(json!("hidden"))
}
})
.build();
// State is passed per-request
let ctx = RequestCtx {
tenant_id: "acme".into(),
user_role: "viewer".into(),
};
let result = proxy
.execute("{ user { name email } }", &json!({}), ctx)
.await?;
```
## Interceptor patterns
### Resolve locally (skip upstream entirely)
```rust
```rust
"profile:{}:{}",
ctx.state().tenant_id,
ctx.parent().get("id").unwrap()
);
if let Some(cached) = my_cache.get(&cache_key).await {
return Ok(cached);
}
let value = ctx.forward().await?;
my_cache.set(&cache_key, &value).await;
Ok(value)
})
```
### Use parent fields
```rust
let role = ctx.parent().get("role").and_then(|v| v.as_str()).unwrap_or("user");
Ok(json!(format!("{} ({})", name, role)))
})
```
## InterceptContext API
Inside an interceptor closure, `ctx` provides:
| `ctx.forward()` | Call upstream for this field and return the result |
| `ctx.parent()` | The parent object's fields (from upstream response) |
| `ctx.state()` | Per-request state passed to `execute()` |
| `ctx.field_name()` | Name of the intercepted field |
| `ctx.args()` | Arguments passed to this field in the query |
## Roadmap
- **Batched forwarding** — collect all `forward()` calls and batch into a single upstream request instead of individual calls per field
- **Wildcard selectors** — `("*", "email")` to intercept a field on any type
- **`forward_with`** — modify the sub-selection before forwarding to upstream
## License
MIT