1use 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
27pub 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#[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
171fn 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}