# client library
this crate provides a small, typed graphql client for infrahub.
## features
- graphql execution with raw and typed helpers
- file upload via the graphql multipart request spec (`CoreFileObject` mutations)
- file download via REST endpoints (`/api/files/`)
- schema fetch helper
- edges/node pagination helper
- structured errors with status and graphql details
- configurable http transport (prebuilt client or builder callback)
## surface
- `Client` - graphql client with auth and branch routing
- `ClientConfig` - base url, token, timeouts, headers, and http transport customization
- `FileUpload` - file upload payload for multipart mutations
- `Operation` - generated operation trait
- `Paginator` - edge/connection pagination helper
## how to use this crate
this crate is the base graphql client. you can use it directly with ad‑hoc
queries, or pair it with a generated, schema‑specific crate produced by
`infrahub-codegen`.
use cases:
- ad‑hoc graphql: use `execute_raw` / `execute` with your own query strings
- schema‑specific ergonomics: generate a crate and call its api from this client
## quick start (ad‑hoc)
```rust
use infrahub::{Client, ClientConfig};
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(ClientConfig::new("http://localhost:8000", "token"))?;
let response = client
.execute_raw("{ Branch { id name } }", None, None)
.await?;
println!("{:?}", response.data);
# Ok(())
# }
```
## install
```toml
[dependencies]
infrahub = "0.2.1"
tokio = { version = "1", features = ["full"] }
```
## config
```rust,no_run
use std::time::Duration;
use infrahub::ClientConfig;
use reqwest::header::{HeaderName, HeaderValue};
let config = ClientConfig::new("http://localhost:8000", "token")
.with_default_branch("main")
.with_timeout(Duration::from_secs(60))
.with_ssl_verification(false)
.with_header(
HeaderName::from_static("x-client"),
HeaderValue::from_static("infrahub-rs"),
);
```
## http transport customization
two escape hatches are available when the default reqwest client is not enough
(proxies, custom tls roots, tracing middleware, etc.).
### builder callback
receives the pre-configured builder (auth header, extra headers, user agent,
timeout, and ssl settings already applied) and returns a modified builder. use
this when you only need to add transport settings on top of the defaults — proxy
config, additional tls roots, connection pool tuning, etc.
```rust,no_run
use infrahub::{Client, ClientConfig};
let config = ClientConfig::new("http://localhost:8000", "token")
.with_http_client_builder(|builder| {
builder.proxy(reqwest::Proxy::all("http://proxy.example.com:3128").unwrap())
});
let client = Client::new(config)?;
# Ok::<_, Box<dyn std::error::Error>>(())
```
### prebuilt client
inject a fully configured `reqwest::Client`. **all** transport configuration —
auth headers, tls, timeouts, ssl verification, user agent — is taken from the
prebuilt client. the `timeout`, `verify_ssl`, `user_agent`, and `extra_headers`
fields on `ClientConfig` are ignored. use this for full control: shared clients
across multiple services, custom connectors, or tracing middleware.
when using a prebuilt client, the api token is not used for auth, so an empty
string is accepted by `ClientConfig::new`.
```rust,no_run
use infrahub::{Client, ClientConfig};
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
let mut headers = HeaderMap::new();
headers.insert(
HeaderName::from_static("x-infrahub-key"),
HeaderValue::from_static("my-token"),
);
let prebuilt = reqwest::Client::builder()
.default_headers(headers)
.timeout(std::time::Duration::from_secs(60))
.build()?;
// token is unused for auth here; pass "" or any placeholder
let config = ClientConfig::new("http://localhost:8000", "")
.with_http_client(prebuilt);
let client = Client::new(config)?;
# Ok::<_, Box<dyn std::error::Error>>(())
```
`with_http_client` takes precedence over `with_http_client_builder` if both are set.
## typed queries
```rust,no_run
use infrahub::{Client, ClientConfig};
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(ClientConfig::new("http://localhost:8000", "token"))?;
let response = client
.execute::<serde_json::Value>(
"query($name: String!) { Branch(name__value: $name) { edges { node { id name } } } }",
Some(serde_json::json!({ "name": "main" })),
None,
)
.await?;
println!("{:?}", response.data);
# Ok(())
# }
```
## generated client
use `infrahub-codegen` to generate a schema-specific crate, then call into it
from your base client.
the generated crate provides:
- `generated()` for full surface graphql methods
- `api()` for ergonomic, topic-grouped helpers (`list`, `get_by_id`, `paginate`, plus mutation helpers when available in your schema snapshot)
## branches
branches are routed by url: `POST {base}/graphql/{branch}` and `GET {base}/schema.graphql?branch=foo`.
```rust,no_run
use infrahub::{Client, ClientConfig};
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(ClientConfig::new("http://localhost:8000", "token"))?;
let response = client
.execute_raw("{ Branch { id name } }", None, Some("feature-a"))
.await?;
println!("{:?}", response.data);
# Ok(())
# }
```
## file upload
upload files to `CoreFileObject` mutations using the graphql multipart request spec:
```rust,no_run
use infrahub::{Client, ClientConfig, FileUpload};
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(ClientConfig::new("http://localhost:8000", "token"))?;
let file = FileUpload::new("report.pdf", "application/pdf", std::fs::read("report.pdf")?);
let response = client
.execute_multipart::<serde_json::Value>(
"mutation($data: MyFileCreateInput!, $file: Upload!) { MyFileCreate(data: $data, file: $file) { object { id } } }",
Some(serde_json::json!({ "data": { "name": { "value": "Q1 Report" } } })),
vec![("file", file)],
None,
)
.await?;
println!("{:?}", response.data);
# Ok(())
# }
```
## file download
download files by node id, human-friendly id, or storage id:
```rust,no_run
use infrahub::{Client, ClientConfig};
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(ClientConfig::new("http://localhost:8000", "token"))?;
let bytes = client.download_file("abc-123", None).await?;
let bytes = client.download_file_by_hfid("MyFile", &["value1"], None).await?;
let bytes = client.download_file_by_storage_id("store-456", None).await?;
# Ok(())
# }
```
## schema fetch
```rust,no_run
use infrahub::{Client, ClientConfig};
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(ClientConfig::new("http://localhost:8000", "token"))?;
let schema = client.fetch_schema(None).await?;
println!("schema len: {}", schema.len());
# Ok(())
# }
```
## pagination helper
`Paginator` is generic and does not assume a pageInfo shape. pass a fetch function and an extract function.
```rust,no_run
use infrahub::{Client, ClientConfig, EdgePage, Paginator, Result};
# async fn example() -> Result<()> {
let client = Client::new(ClientConfig::new("http://localhost:8000", "token"))?;
client.execute_raw("query($after: String) { Branch(after: $after) { edges { node { id name } cursor } } }", Some(vars), None).await
};
.data
.ok_or_else(|| infrahub::Error::Config("missing data".to_string()))?;
let edges = data["Branch"]["edges"].as_array().cloned().unwrap_or_default();
let mut nodes = Vec::new();
let mut next = None;
for edge in edges {
if let Some(node) = edge.get("node") {
nodes.push(node.clone());
}
next = edge.get("cursor").and_then(|c| c.as_str()).map(|s| s.to_string());
}
Ok(EdgePage { nodes, next_cursor: next })
};
let mut paginator = Paginator::new(fetch, extract);
let _all = paginator.collect_all().await?;
Ok(())
# }
```
## codegen
generate a full typed client from a schema snapshot:
```bash
cargo run --bin infrahub-codegen -- --schema /path/to/schema.graphql --out /tmp/infrahub-generated
```
then add it as a path dependency:
```toml
[dependencies]
infrahub = "0.2.1"
infrahub-generated = { path = "/tmp/infrahub-generated" }
```
usage example:
```rust,no_run
use infrahub::{Client, ClientConfig};
use infrahub_generated::ApiClient;
# async fn example() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(ClientConfig::new("http://localhost:8000", "token"))?;
let repository_api = client.api().core().repository();
let repositories = repository_api.list(None, None).await?;
println!("repositories: {}", repositories.len());
if let Some(first) = repositories.first() {
let fetched = repository_api.get_by_id(first.id.clone(), None).await?;
println!("fetched: {}", fetched.is_some());
}
let mut paginator = repository_api.paginate(None, None);
let first_page = paginator.next_page().await?;
println!("first page present: {}", first_page.is_some());
# Ok(())
# }
```
see also: [`examples/generated_api.md`](../examples/generated_api.md)