supabase_client_graphql/
mutation.rs1use serde::de::DeserializeOwned;
2use serde_json::Value;
3
4use crate::client::GraphqlClient;
5use crate::error::GraphqlError;
6use crate::filter::GqlFilter;
7use crate::render;
8use crate::types::MutationResult;
9
10#[derive(Debug, Clone, Copy)]
12pub enum MutationKind {
13 Insert,
14 Update,
15 Delete,
16}
17
18impl From<MutationKind> for render::MutationKind {
19 fn from(kind: MutationKind) -> Self {
20 match kind {
21 MutationKind::Insert => render::MutationKind::Insert,
22 MutationKind::Update => render::MutationKind::Update,
23 MutationKind::Delete => render::MutationKind::Delete,
24 }
25 }
26}
27
28#[derive(Debug)]
55pub struct MutationBuilder {
56 client: GraphqlClient,
57 collection: String,
58 kind: MutationKind,
59 returning_fields: Vec<String>,
60 filter: Option<GqlFilter>,
61 set: Option<Value>,
62 objects: Option<Value>,
63 at_most: Option<i64>,
64}
65
66impl MutationBuilder {
67 pub(crate) fn new(client: GraphqlClient, collection: String, kind: MutationKind) -> Self {
68 Self {
69 client,
70 collection,
71 kind,
72 returning_fields: Vec::new(),
73 filter: None,
74 set: None,
75 objects: None,
76 at_most: None,
77 }
78 }
79
80 pub fn returning(mut self, fields: &[&str]) -> Self {
82 self.returning_fields = fields.iter().map(|s| s.to_string()).collect();
83 self
84 }
85
86 pub fn filter(mut self, filter: GqlFilter) -> Self {
88 self.filter = Some(filter);
89 self
90 }
91
92 pub fn set(mut self, values: Value) -> Self {
96 self.set = Some(values);
97 self
98 }
99
100 pub fn objects(mut self, objects: Vec<Value>) -> Self {
104 self.objects = Some(Value::Array(objects));
105 self
106 }
107
108 pub fn at_most(mut self, n: i64) -> Self {
110 self.at_most = Some(n);
111 self
112 }
113
114 pub fn build(&self) -> (String, Value) {
118 let filter_value = self.filter.as_ref().map(|f| f.to_value());
119 render::render_mutation(
120 &self.collection,
121 self.kind.into(),
122 &self.returning_fields,
123 filter_value.as_ref(),
124 self.set.as_ref(),
125 self.objects.as_ref(),
126 self.at_most,
127 )
128 }
129
130 pub async fn execute<T: DeserializeOwned>(self) -> Result<MutationResult<T>, GraphqlError> {
135 let (query, variables) = self.build();
136
137 let mutation_field = match self.kind {
139 MutationKind::Insert => format!(
140 "insertInto{}{}",
141 self.collection[..1].to_uppercase(),
142 &self.collection[1..]
143 ),
144 MutationKind::Update => format!(
145 "update{}{}",
146 self.collection[..1].to_uppercase(),
147 &self.collection[1..]
148 ),
149 MutationKind::Delete => format!(
150 "deleteFrom{}{}",
151 self.collection[..1].to_uppercase(),
152 &self.collection[1..]
153 ),
154 };
155
156 let response = self
157 .client
158 .execute::<Value>(&query, Some(variables), None)
159 .await?;
160
161 let data = response.data.ok_or_else(|| {
162 GraphqlError::InvalidConfig("No data in GraphQL response".to_string())
163 })?;
164
165 let mutation_data = data.get(&mutation_field).ok_or_else(|| {
166 GraphqlError::InvalidConfig(format!(
167 "Mutation field '{}' not found in response data",
168 mutation_field
169 ))
170 })?;
171
172 let result: MutationResult<T> = serde_json::from_value(mutation_data.clone())?;
173 Ok(result)
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use serde_json::json;
181
182 fn test_client() -> GraphqlClient {
183 GraphqlClient::new("https://example.supabase.co", "test-key").unwrap()
184 }
185
186 #[test]
187 fn build_insert_mutation() {
188 let builder = MutationBuilder::new(
189 test_client(),
190 "blogCollection".into(),
191 MutationKind::Insert,
192 )
193 .objects(vec![json!({"title": "New", "body": "Content"})])
194 .returning(&["id", "title"]);
195
196 let (query, _) = builder.build();
197 assert!(query.contains("insertIntoBlogCollection"));
198 assert!(query.contains("objects:"));
199 assert!(query.contains("records { id title }"));
200 assert!(query.contains("affectedCount"));
201 }
202
203 #[test]
204 fn build_update_mutation() {
205 let builder = MutationBuilder::new(
206 test_client(),
207 "blogCollection".into(),
208 MutationKind::Update,
209 )
210 .set(json!({"title": "Updated"}))
211 .filter(GqlFilter::eq("id", json!(1)))
212 .at_most(1)
213 .returning(&["id", "title"]);
214
215 let (query, _) = builder.build();
216 assert!(query.contains("updateBlogCollection"));
217 assert!(query.contains("set: {title: \"Updated\"}"));
218 assert!(query.contains("filter: {id: {eq: 1}}"));
219 assert!(query.contains("atMost: 1"));
220 }
221
222 #[test]
223 fn build_delete_mutation() {
224 let builder = MutationBuilder::new(
225 test_client(),
226 "blogCollection".into(),
227 MutationKind::Delete,
228 )
229 .filter(GqlFilter::eq("id", json!(1)))
230 .at_most(1)
231 .returning(&["id"]);
232
233 let (query, _) = builder.build();
234 assert!(query.contains("deleteFromBlogCollection"));
235 assert!(query.contains("filter: {id: {eq: 1}}"));
236 assert!(query.contains("atMost: 1"));
237 assert!(query.contains("records { id }"));
238 }
239
240 #[test]
241 fn build_mutation_no_returning_uses_typename() {
242 let builder = MutationBuilder::new(
243 test_client(),
244 "blogCollection".into(),
245 MutationKind::Delete,
246 )
247 .filter(GqlFilter::eq("id", json!(1)));
248
249 let (query, _) = builder.build();
250 assert!(query.contains("records { __typename }"));
251 }
252
253 #[test]
254 fn build_insert_multiple_objects() {
255 let builder = MutationBuilder::new(
256 test_client(),
257 "blogCollection".into(),
258 MutationKind::Insert,
259 )
260 .objects(vec![
261 json!({"title": "Post 1"}),
262 json!({"title": "Post 2"}),
263 ])
264 .returning(&["id"]);
265
266 let (query, _) = builder.build();
267 assert!(query.contains("insertIntoBlogCollection"));
268 assert!(query.contains("objects: ["));
269 }
270}