supabase_client_graphql/
query.rs1use serde::de::DeserializeOwned;
2use serde_json::Value;
3
4use crate::client::GraphqlClient;
5use crate::error::GraphqlError;
6use crate::filter::GqlFilter;
7use crate::order::{OrderByDirection, OrderByEntry};
8use crate::render;
9use crate::types::Connection;
10
11#[derive(Debug)]
26pub struct QueryBuilder {
27 client: GraphqlClient,
28 collection: String,
29 select_fields: Vec<String>,
30 filter: Option<GqlFilter>,
31 order_by: Vec<OrderByEntry>,
32 first: Option<i64>,
33 last: Option<i64>,
34 after: Option<String>,
35 before: Option<String>,
36 offset: Option<i64>,
37 include_total_count: bool,
38}
39
40impl QueryBuilder {
41 pub(crate) fn new(client: GraphqlClient, collection: String) -> Self {
42 Self {
43 client,
44 collection,
45 select_fields: Vec::new(),
46 filter: None,
47 order_by: Vec::new(),
48 first: None,
49 last: None,
50 after: None,
51 before: None,
52 offset: None,
53 include_total_count: false,
54 }
55 }
56
57 pub fn select(mut self, fields: &[&str]) -> Self {
59 self.select_fields = fields.iter().map(|s| s.to_string()).collect();
60 self
61 }
62
63 pub fn filter(mut self, filter: GqlFilter) -> Self {
65 self.filter = Some(filter);
66 self
67 }
68
69 pub fn order_by(mut self, column: &str, direction: OrderByDirection) -> Self {
71 self.order_by.push(OrderByEntry {
72 column: column.to_string(),
73 direction,
74 });
75 self
76 }
77
78 pub fn first(mut self, n: i64) -> Self {
80 self.first = Some(n);
81 self
82 }
83
84 pub fn last(mut self, n: i64) -> Self {
86 self.last = Some(n);
87 self
88 }
89
90 pub fn after(mut self, cursor: &str) -> Self {
92 self.after = Some(cursor.to_string());
93 self
94 }
95
96 pub fn before(mut self, cursor: &str) -> Self {
98 self.before = Some(cursor.to_string());
99 self
100 }
101
102 pub fn offset(mut self, n: i64) -> Self {
104 self.offset = Some(n);
105 self
106 }
107
108 pub fn total_count(mut self) -> Self {
110 self.include_total_count = true;
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_collection_query(
120 &self.collection,
121 &self.select_fields,
122 filter_value.as_ref(),
123 &self.order_by,
124 self.first,
125 self.last,
126 self.after.as_deref(),
127 self.before.as_deref(),
128 self.offset,
129 self.include_total_count,
130 )
131 }
132
133 pub async fn execute<T: DeserializeOwned>(self) -> Result<Connection<T>, GraphqlError> {
138 let (query, variables) = self.build();
139 let collection_name = self.collection.clone();
140
141 let response = self
142 .client
143 .execute::<Value>(&query, Some(variables), None)
144 .await?;
145
146 let data = response.data.ok_or_else(|| {
147 GraphqlError::InvalidConfig("No data in GraphQL response".to_string())
148 })?;
149
150 let collection_data = data.get(&collection_name).ok_or_else(|| {
151 GraphqlError::InvalidConfig(format!(
152 "Collection '{}' not found in response data",
153 collection_name
154 ))
155 })?;
156
157 let connection: Connection<T> = serde_json::from_value(collection_data.clone())?;
158 Ok(connection)
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use serde_json::json;
166
167 fn test_client() -> GraphqlClient {
168 GraphqlClient::new("https://example.supabase.co", "test-key").unwrap()
169 }
170
171 #[test]
172 fn build_simple_query() {
173 let builder = QueryBuilder::new(test_client(), "blogCollection".into())
174 .select(&["id", "title"])
175 .first(10);
176
177 let (query, vars) = builder.build();
178 assert!(query.contains("blogCollection"));
179 assert!(query.contains("node { id title }"));
180 assert!(query.contains("$first: Int"));
181 assert_eq!(vars["first"], 10);
182 }
183
184 #[test]
185 fn build_query_with_filter() {
186 let builder = QueryBuilder::new(test_client(), "blogCollection".into())
187 .select(&["id"])
188 .filter(GqlFilter::eq("status", json!("published")));
189
190 let (query, _) = builder.build();
191 assert!(query.contains("filter: {status: {eq: \"published\"}}"));
192 }
193
194 #[test]
195 fn build_query_with_order() {
196 let builder = QueryBuilder::new(test_client(), "blogCollection".into())
197 .select(&["id"])
198 .order_by("createdAt", OrderByDirection::DescNullsLast);
199
200 let (query, _) = builder.build();
201 assert!(query.contains("orderBy: [{createdAt: DescNullsLast}]"));
202 }
203
204 #[test]
205 fn build_query_with_total_count() {
206 let builder = QueryBuilder::new(test_client(), "blogCollection".into())
207 .select(&["id"])
208 .total_count();
209
210 let (query, _) = builder.build();
211 assert!(query.contains("totalCount"));
212 }
213
214 #[test]
215 fn build_query_with_cursors() {
216 let builder = QueryBuilder::new(test_client(), "blogCollection".into())
217 .select(&["id"])
218 .first(5)
219 .after("abc123");
220
221 let (query, vars) = builder.build();
222 assert!(query.contains("$after: Cursor"));
223 assert!(query.contains("after: $after"));
224 assert_eq!(vars["after"], "abc123");
225 }
226
227 #[test]
228 fn build_query_backward_pagination() {
229 let builder = QueryBuilder::new(test_client(), "blogCollection".into())
230 .select(&["id"])
231 .last(5)
232 .before("xyz789");
233
234 let (query, vars) = builder.build();
235 assert!(query.contains("$last: Int"));
236 assert!(query.contains("$before: Cursor"));
237 assert_eq!(vars["last"], 5);
238 assert_eq!(vars["before"], "xyz789");
239 }
240
241 #[test]
242 fn build_full_query() {
243 let builder = QueryBuilder::new(test_client(), "blogCollection".into())
244 .select(&["id", "title", "createdAt"])
245 .filter(GqlFilter::and(vec![
246 GqlFilter::eq("status", json!("published")),
247 GqlFilter::gte("views", json!(100)),
248 ]))
249 .order_by("createdAt", OrderByDirection::DescNullsLast)
250 .first(10)
251 .after("cursor123")
252 .total_count();
253
254 let (query, vars) = builder.build();
255 assert!(query.contains("blogCollection"));
256 assert!(query.contains("node { id title createdAt }"));
257 assert!(query.contains("filter:"));
258 assert!(query.contains("orderBy:"));
259 assert!(query.contains("totalCount"));
260 assert_eq!(vars["first"], 10);
261 assert_eq!(vars["after"], "cursor123");
262 }
263}