1use async_trait::async_trait;
25use std::collections::HashMap;
26use std::marker::PhantomData;
27use std::sync::Arc;
28
29use crate::service::ServiceResult;
30
31#[async_trait]
37pub trait GraphQLCapableService<E, C, U>: Send + Sync + 'static
38where
39 E: Clone + Send + Sync + 'static,
40 C: Send + Sync + 'static,
41 U: Send + Sync + 'static,
42{
43 async fn list(&self, page: u32, limit: u32, filters: HashMap<String, String>) -> ServiceResult<(Vec<E>, u64)>;
44 async fn create(&self, dto: C) -> ServiceResult<E>;
45 async fn get_by_id(&self, id: &str) -> ServiceResult<Option<E>>;
46 async fn update(&self, id: &str, dto: U) -> ServiceResult<Option<E>>;
47 async fn soft_delete(&self, id: &str) -> ServiceResult<bool>;
48 async fn restore(&self, id: &str) -> ServiceResult<Option<E>>;
49 async fn list_deleted(&self, page: u32, limit: u32) -> ServiceResult<(Vec<E>, u64)>;
50 async fn empty_trash(&self) -> ServiceResult<u64>;
51}
52
53pub struct GenericGraphQLResolver<E, C, U, S>
87where
88 E: Clone + Send + Sync + 'static,
89 C: Send + Sync + 'static,
90 U: Send + Sync + 'static,
91 S: GraphQLCapableService<E, C, U>,
92{
93 service: Arc<S>,
94 _phantom: PhantomData<(E, C, U)>,
95}
96
97impl<E, C, U, S> GenericGraphQLResolver<E, C, U, S>
98where
99 E: Clone + Send + Sync + 'static,
100 C: Send + Sync + 'static,
101 U: Send + Sync + 'static,
102 S: GraphQLCapableService<E, C, U>,
103{
104 pub fn new(service: Arc<S>) -> Self {
105 Self {
106 service,
107 _phantom: PhantomData,
108 }
109 }
110
111 pub fn service(&self) -> &Arc<S> {
112 &self.service
113 }
114
115 pub async fn query_get_by_id(&self, id: &str) -> ServiceResult<Option<E>> {
119 self.service.get_by_id(id).await
120 }
121
122 pub async fn query_list(
124 &self,
125 page: u32,
126 limit: u32,
127 filters: HashMap<String, String>,
128 ) -> ServiceResult<GraphQLListResult<E>> {
129 let (items, total) = self.service.list(page, limit, filters).await?;
130 Ok(GraphQLListResult { items, total, page, limit })
131 }
132
133 pub async fn query_list_deleted(
135 &self,
136 page: u32,
137 limit: u32,
138 ) -> ServiceResult<GraphQLListResult<E>> {
139 let (items, total) = self.service.list_deleted(page, limit).await?;
140 Ok(GraphQLListResult { items, total, page, limit })
141 }
142
143 pub async fn mutation_create(&self, dto: C) -> ServiceResult<E> {
147 self.service.create(dto).await
148 }
149
150 pub async fn mutation_update(&self, id: &str, dto: U) -> ServiceResult<Option<E>> {
152 self.service.update(id, dto).await
153 }
154
155 pub async fn mutation_delete(&self, id: &str) -> ServiceResult<bool> {
157 self.service.soft_delete(id).await
158 }
159
160 pub async fn mutation_restore(&self, id: &str) -> ServiceResult<Option<E>> {
162 self.service.restore(id).await
163 }
164
165 pub async fn mutation_empty_trash(&self) -> ServiceResult<u64> {
167 self.service.empty_trash().await
168 }
169}
170
171impl<E, C, U, S> Clone for GenericGraphQLResolver<E, C, U, S>
173where
174 E: Clone + Send + Sync + 'static,
175 C: Send + Sync + 'static,
176 U: Send + Sync + 'static,
177 S: GraphQLCapableService<E, C, U>,
178{
179 fn clone(&self) -> Self {
180 Self {
181 service: self.service.clone(),
182 _phantom: PhantomData,
183 }
184 }
185}
186
187#[derive(Debug, Clone)]
194pub struct GraphQLListResult<E> {
195 pub items: Vec<E>,
196 pub total: u64,
197 pub page: u32,
198 pub limit: u32,
199}
200
201impl<E> GraphQLListResult<E> {
202 pub fn total_pages(&self) -> u32 {
203 if self.limit == 0 {
204 return 0;
205 }
206 ((self.total as f64) / (self.limit as f64)).ceil() as u32
207 }
208}
209
210#[derive(Debug, Clone)]
214pub struct GraphQLPaginationInput {
215 pub page: Option<u32>,
216 pub limit: Option<u32>,
217}
218
219impl GraphQLPaginationInput {
220 pub fn page(&self) -> u32 {
221 self.page.unwrap_or(1)
222 }
223
224 pub fn limit(&self) -> u32 {
225 self.limit.unwrap_or(20)
226 }
227}
228
229impl Default for GraphQLPaginationInput {
230 fn default() -> Self {
231 Self {
232 page: Some(1),
233 limit: Some(20),
234 }
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use std::sync::Mutex;
242
243 #[derive(Debug, Clone)]
244 struct Item {
245 id: String,
246 name: String,
247 }
248
249 struct CreateItemInput { name: String }
250 struct UpdateItemInput { name: String }
251
252 struct FakeItemService {
253 store: Mutex<Vec<Item>>,
254 }
255
256 impl FakeItemService {
257 fn new() -> Self {
258 Self {
259 store: Mutex::new(Vec::new()),
260 }
261 }
262 }
263
264 #[async_trait]
265 impl GraphQLCapableService<Item, CreateItemInput, UpdateItemInput> for FakeItemService {
266 async fn list(&self, _p: u32, _l: u32, _f: HashMap<String, String>) -> ServiceResult<(Vec<Item>, u64)> {
267 let store = self.store.lock().unwrap();
268 let items: Vec<_> = store.iter().filter(|i| !i.id.starts_with("del-")).cloned().collect();
269 let total = items.len() as u64;
270 Ok((items, total))
271 }
272 async fn create(&self, dto: CreateItemInput) -> ServiceResult<Item> {
273 let item = Item { id: uuid::Uuid::new_v4().to_string(), name: dto.name };
274 self.store.lock().unwrap().push(item.clone());
275 Ok(item)
276 }
277 async fn get_by_id(&self, id: &str) -> ServiceResult<Option<Item>> {
278 Ok(self.store.lock().unwrap().iter().find(|i| i.id == id).cloned())
279 }
280 async fn update(&self, id: &str, dto: UpdateItemInput) -> ServiceResult<Option<Item>> {
281 let mut store = self.store.lock().unwrap();
282 if let Some(item) = store.iter_mut().find(|i| i.id == id) {
283 item.name = dto.name;
284 return Ok(Some(item.clone()));
285 }
286 Ok(None)
287 }
288 async fn soft_delete(&self, id: &str) -> ServiceResult<bool> {
289 let mut store = self.store.lock().unwrap();
290 if let Some(item) = store.iter_mut().find(|i| i.id == id) {
291 item.id = format!("del-{}", item.id);
292 return Ok(true);
293 }
294 Ok(false)
295 }
296 async fn restore(&self, _id: &str) -> ServiceResult<Option<Item>> { Ok(None) }
297 async fn list_deleted(&self, _p: u32, _l: u32) -> ServiceResult<(Vec<Item>, u64)> { Ok((vec![], 0)) }
298 async fn empty_trash(&self) -> ServiceResult<u64> { Ok(0) }
299 }
300
301 #[tokio::test]
302 async fn create_and_query_roundtrip() {
303 let service = Arc::new(FakeItemService::new());
304 let resolver = GenericGraphQLResolver::new(service);
305
306 resolver.mutation_create(CreateItemInput { name: "widget".into() }).await.unwrap();
307
308 let result = resolver.query_list(1, 20, Default::default()).await.unwrap();
309 assert_eq!(result.items.len(), 1);
310 assert_eq!(result.items[0].name, "widget");
311 assert_eq!(result.total, 1);
312 }
313
314 #[tokio::test]
315 async fn list_result_total_pages() {
316 let result: GraphQLListResult<i32> = GraphQLListResult {
317 items: vec![],
318 total: 50,
319 page: 1,
320 limit: 20,
321 };
322 assert_eq!(result.total_pages(), 3);
323 }
324}