infrahub 0.2.1

small graphql client for infrahub
Documentation
# 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"))?;

let fetch = |cursor: Option<String>| async {
    let vars = serde_json::json!({ "after": cursor });
    client.execute_raw("query($after: String) { Branch(after: $after) { edges { node { id name } cursor } } }", Some(vars), None).await
};

let extract = |response| {
    let data = response
        .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)