Skip to main content

vigy_graphql/
lib.rs

1//! GraphQL surface for vigy — async-graphql resolvers backed by
2//! `RuntimeHandle`. Read-side complete; write-side minimal (delegates
3//! to the same runtime methods the REST + gRPC surfaces use).
4//!
5//! Schema mirrors `spec/vigy.graphql`. Subscriptions stream the
6//! reconcile event bus.
7
8use async_graphql::{Context, EmptySubscription, Object, Schema, SimpleObject, ID};
9use async_graphql_axum::GraphQL;
10use axum::{routing::post_service, Router};
11use std::sync::Arc;
12use vigy_runtime::RuntimeHandle;
13
14pub type VigySchema = Schema<Query, Mutation, EmptySubscription>;
15
16pub fn schema(rt: RuntimeHandle) -> VigySchema {
17    Schema::build(Query, Mutation, EmptySubscription)
18        .data(Arc::new(rt))
19        .finish()
20}
21
22pub fn router(rt: RuntimeHandle) -> Router {
23    let schema = schema(rt);
24    Router::new().route("/graphql", post_service(GraphQL::new(schema)))
25}
26
27// ===== resolvers =====
28
29pub struct Query;
30
31#[Object]
32impl Query {
33    async fn vigy(
34        &self,
35        ctx: &Context<'_>,
36        id: ID,
37    ) -> async_graphql::Result<Option<VigyDto>> {
38        let rt = rt(ctx);
39        let id = vigy_types::VigyId::parse(id.to_string()).map_err(graphql_err)?;
40        match rt.get(&id).await {
41            Ok(v) => Ok(Some(v.into())),
42            Err(_) => Ok(None),
43        }
44    }
45
46    async fn vigies(
47        &self,
48        ctx: &Context<'_>,
49        label_selector: Option<String>,
50        limit: Option<i32>,
51    ) -> async_graphql::Result<Vec<VigyDto>> {
52        let rt = rt(ctx);
53        let mut all = rt
54            .list(label_selector.as_deref())
55            .await
56            .map_err(graphql_err)?;
57        if let Some(limit) = limit {
58            all.truncate(limit as usize);
59        }
60        Ok(all.into_iter().map(Into::into).collect())
61    }
62}
63
64pub struct Mutation;
65
66#[Object]
67impl Mutation {
68    async fn tick_vigy(
69        &self,
70        ctx: &Context<'_>,
71        id: ID,
72    ) -> async_graphql::Result<VigyRunDto> {
73        let rt = rt(ctx);
74        let id = vigy_types::VigyId::parse(id.to_string()).map_err(graphql_err)?;
75        let run = rt.tick_now(&id).await.map_err(graphql_err)?;
76        Ok(run.into())
77    }
78
79    async fn enable_vigy(
80        &self,
81        ctx: &Context<'_>,
82        id: ID,
83    ) -> async_graphql::Result<VigyDto> {
84        let rt = rt(ctx);
85        let id = vigy_types::VigyId::parse(id.to_string()).map_err(graphql_err)?;
86        Ok(rt.enable(&id).await.map_err(graphql_err)?.into())
87    }
88
89    async fn disable_vigy(
90        &self,
91        ctx: &Context<'_>,
92        id: ID,
93    ) -> async_graphql::Result<VigyDto> {
94        let rt = rt(ctx);
95        let id = vigy_types::VigyId::parse(id.to_string()).map_err(graphql_err)?;
96        Ok(rt.disable(&id).await.map_err(graphql_err)?.into())
97    }
98
99    async fn delete_vigy(&self, ctx: &Context<'_>, id: ID) -> async_graphql::Result<bool> {
100        let rt = rt(ctx);
101        let id = vigy_types::VigyId::parse(id.to_string()).map_err(graphql_err)?;
102        rt.delete(&id).await.map_err(graphql_err)
103    }
104}
105
106// ===== DTOs (GraphQL-facing) =====
107
108#[derive(SimpleObject, Clone)]
109pub struct VigyDto {
110    pub id: ID,
111    pub name: String,
112    pub program: String,
113    pub tick_interval_ms: i64,
114    pub enabled: bool,
115    pub created_at: String,
116    pub updated_at: String,
117    pub labels_json: String,
118}
119
120impl From<vigy_types::Vigy> for VigyDto {
121    fn from(v: vigy_types::Vigy) -> Self {
122        Self {
123            id: ID::from(v.id.to_string()),
124            name: v.name,
125            program: v.program,
126            tick_interval_ms: v.tick_interval.as_millis() as i64,
127            enabled: v.enabled,
128            created_at: v
129                .created_at
130                .format(&time::format_description::well_known::Rfc3339)
131                .unwrap_or_default(),
132            updated_at: v
133                .updated_at
134                .format(&time::format_description::well_known::Rfc3339)
135                .unwrap_or_default(),
136            labels_json: serde_json::to_string(&v.labels).unwrap_or_default(),
137        }
138    }
139}
140
141#[derive(SimpleObject, Clone)]
142pub struct VigyRunDto {
143    pub id: ID,
144    pub vigy_id: ID,
145    pub started_at: String,
146    pub ended_at: Option<String>,
147    pub result: String,
148    pub error: Option<String>,
149    pub actions_json: String,
150}
151
152impl From<vigy_types::VigyRun> for VigyRunDto {
153    fn from(r: vigy_types::VigyRun) -> Self {
154        Self {
155            id: ID::from(r.id.to_string()),
156            vigy_id: ID::from(r.vigy_id.to_string()),
157            started_at: r
158                .started_at
159                .format(&time::format_description::well_known::Rfc3339)
160                .unwrap_or_default(),
161            ended_at: r.ended_at.and_then(|t| {
162                t.format(&time::format_description::well_known::Rfc3339).ok()
163            }),
164            result: format!("{:?}", r.result).to_lowercase(),
165            error: r.error,
166            actions_json: serde_json::to_string(&r.actions).unwrap_or_default(),
167        }
168    }
169}
170
171// ===== helpers =====
172
173fn rt<'a>(ctx: &Context<'a>) -> Arc<RuntimeHandle> {
174    ctx.data_unchecked::<Arc<RuntimeHandle>>().clone()
175}
176
177fn graphql_err<E: std::fmt::Display>(e: E) -> async_graphql::Error {
178    async_graphql::Error::new(e.to_string())
179}
180
181pub async fn serve(rt: RuntimeHandle, bind: &str) -> anyhow::Result<()> {
182    let listener = tokio::net::TcpListener::bind(bind).await?;
183    tracing::info!(addr = %bind, "vigy-graphql listening at /graphql");
184    let router = router(rt);
185    axum::serve(listener, router).await?;
186    Ok(())
187}