leptos_query_rs/mutation/
mod.rs

1//! Mutation Hooks and Options
2//!
3//! The main user-facing API for data mutations with optimistic updates.
4
5use leptos::prelude::*;
6use leptos::task::spawn_local;
7use std::future::Future;
8use serde::{Serialize, de::DeserializeOwned};
9
10use crate::client::QueryClient;
11use crate::retry::RetryConfig;
12use crate::types::QueryKeyPattern;
13
14/// Options for configuring a mutation
15#[derive(Clone)]
16pub struct MutationOptions {
17    /// Whether the mutation should run
18    pub enabled: bool,
19    /// Retry configuration
20    pub retry: RetryConfig,
21    /// Whether to invalidate queries on success
22    pub invalidate_queries: Option<Vec<QueryKeyPattern>>,
23}
24
25impl Default for MutationOptions {
26    fn default() -> Self {
27        Self {
28            enabled: true,
29            retry: RetryConfig::default(),
30            invalidate_queries: None,
31        }
32    }
33}
34
35impl MutationOptions {
36    /// Create options with custom retry configuration
37    pub fn with_retry(mut self, retry: RetryConfig) -> Self {
38        self.retry = retry;
39        self
40    }
41    
42    /// Set queries to invalidate on success
43    pub fn invalidate_queries(mut self, patterns: Vec<QueryKeyPattern>) -> Self {
44        self.invalidate_queries = Some(patterns);
45        self
46    }
47    
48    /// Disable the mutation by default
49    pub fn disabled(mut self) -> Self {
50        self.enabled = false;
51        self
52    }
53}
54
55/// Result of a mutation hook
56#[derive(Clone)]
57pub struct MutationResult<TData: 'static + Send + Sync, TError: 'static + Send + Sync, TVariables: 'static> {
58    /// The mutation data
59    pub data: Signal<Option<TData>>,
60    /// Error if any
61    pub error: Signal<Option<TError>>,
62    /// Whether the mutation is loading
63    pub is_loading: Signal<bool>,
64    /// Whether the mutation succeeded
65    pub is_success: Signal<bool>,
66    /// Whether the mutation failed
67    pub is_error: Signal<bool>,
68    
69    // Actions
70    /// Execute the mutation
71    pub mutate: Callback<TVariables>,
72}
73
74/// Main mutation hook
75pub fn use_mutation<TData, TError, TVariables, F, Fut>(
76    mutation_fn: F,
77    options: MutationOptions,
78) -> MutationResult<TData, TError, TVariables>
79where
80    TData: Clone + Send + Sync + Serialize + DeserializeOwned + 'static,
81    TError: Clone + Send + Sync + 'static,
82    TVariables: Clone + Send + Sync + 'static,
83    F: Fn(TVariables) -> Fut + Send + Sync + 'static + Clone,
84    Fut: Future<Output = Result<TData, TError>> + 'static,
85{
86    // Create reactive state
87    let (data, set_data) = signal(None::<TData>);
88    let (error, set_error) = signal(None::<TError>);
89    let (is_loading, set_loading) = signal(false);
90
91    // Get query client from context
92    let client = use_context::<QueryClient>().expect("QueryClient not found in context");
93    
94    // Create mutation function
95    let execute_mutation = {
96        let client = client.clone();
97        let mutation_fn = mutation_fn.clone();
98        let options = options.clone();
99        
100        move |vars: TVariables| {
101            let _client = client.clone();
102            let mutation_fn = mutation_fn.clone();
103            let options = options.clone();
104            
105            spawn_local(async move {
106                set_loading.set(true);
107                set_error.set(None);
108                
109                // Execute the mutation directly without retry for now
110                let result = mutation_fn(vars.clone()).await;
111                
112                match result {
113                    Ok(result_data) => {
114                        set_data.set(Some(result_data));
115                        
116                        // Invalidate queries if specified
117                        if let Some(patterns) = &options.invalidate_queries {
118                            for _pattern in patterns {
119                                // TODO: Implement invalidate_queries method
120                                // client.invalidate_queries(pattern);
121                            }
122                        }
123                    }
124                    Err(err) => {
125                        set_error.set(Some(err));
126                    }
127                }
128                
129                set_loading.set(false);
130            });
131        }
132    };
133    
134    // Create computed signals
135    let is_success = Memo::new(move |_| data.get().is_some() && error.get().is_none());
136    let is_error = Memo::new(move |_| error.get().is_some());
137    
138    // Create result
139    MutationResult {
140        data: data.into(),
141        error: error.into(),
142        is_loading: is_loading.into(),
143        is_success: is_success.into(),
144        is_error: is_error.into(),
145        mutate: Callback::new(execute_mutation),
146    }
147}
148
149/// Hook for optimistic updates
150pub fn use_optimistic_mutation<TData, TError, TVariables, F, Fut>(
151    mutation_fn: F,
152    options: MutationOptions,
153    _on_success: impl Fn(&TData) + Send + Sync + 'static,
154    _on_error: impl Fn(&TError) + Send + Sync + 'static,
155) -> MutationResult<TData, TError, TVariables>
156where
157    TData: Clone + Send + Sync + Serialize + DeserializeOwned + 'static,
158    TError: Clone + Send + Sync + 'static,
159    TVariables: Clone + Send + Sync + 'static,
160    F: Fn(TVariables) -> Fut + Send + Sync + 'static + Clone,
161    Fut: Future<Output = Result<TData, TError>> + 'static,
162{
163    // For now, just return the regular mutation
164    // TODO: Implement optimistic updates
165    use_mutation(mutation_fn, options)
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171    use std::time::Duration;
172    use crate::types::QueryKey;
173    
174    #[test]
175    fn test_mutation_options_builder() {
176        let options = MutationOptions::default()
177            .with_retry(RetryConfig::new(5, Duration::from_secs(2)))
178            .invalidate_queries(vec![QueryKeyPattern::Exact(QueryKey::from("users"))]);
179        
180        assert_eq!(options.retry.max_retries, 5);
181        assert!(options.invalidate_queries.is_some());
182    }
183}