Skip to main content

fraiseql_core/runtime/executor/
mutation.rs

1//! Mutation execution — thin wrappers on `Executor<A>`.
2//!
3//! The core mutation logic lives in
4//! [`runners::mutation::execute_mutation_impl`](super::runners::mutation::execute_mutation_impl).
5//! This module contains:
6//!
7//! - The compile-time-enforced public API ([`Executor::execute_mutation`], bounded on
8//!   [`SupportsMutations`]).
9//! - The runtime-guarded internal dispatch entry point (`Executor::execute_mutation_query`, bounded
10//!   only on [`DatabaseAdapter`]).
11//! - Convenience wrappers used by the REST transport ([`execute_mutation_with_security`],
12//!   [`execute_mutation_batch`], [`execute_bulk_by_filter`]).
13
14use std::collections::HashMap;
15
16use super::{Executor, runners};
17use crate::{
18    db::traits::{DatabaseAdapter, SupportsMutations},
19    error::{FraiseQLError, Result},
20    security::SecurityContext,
21};
22
23/// Compile-time enforcement: `SqliteAdapter` must NOT implement `SupportsMutations`.
24///
25/// Calling `execute_mutation` on an `Executor<SqliteAdapter>` must not compile
26/// because `SqliteAdapter` does not implement the `SupportsMutations` marker trait.
27///
28/// ```compile_fail
29/// use fraiseql_core::runtime::Executor;
30/// use fraiseql_core::db::sqlite::SqliteAdapter;
31/// use fraiseql_core::schema::CompiledSchema;
32/// use std::sync::Arc;
33/// async fn _wont_compile() {
34///     let adapter = Arc::new(SqliteAdapter::new_in_memory().await.unwrap());
35///     let executor = Executor::new(CompiledSchema::new(), adapter);
36///     executor.execute_mutation("createUser", None).await.unwrap();
37/// }
38/// ```
39impl<A: DatabaseAdapter + SupportsMutations> Executor<A> {
40    /// Construct a mutation runner on demand.
41    ///
42    /// Zero-cost: `Arc::clone` is one atomic increment, no allocation.
43    fn mutation_runner(&self) -> runners::mutation::MutationRunner<A> {
44        runners::mutation::MutationRunner::new(std::sync::Arc::clone(&self.ctx))
45    }
46
47    /// Execute a GraphQL mutation directly, with compile-time capability enforcement.
48    ///
49    /// Unlike `execute()` (which accepts raw GraphQL strings and performs a runtime
50    /// `supports_mutations()` check), this method is only available on adapters that
51    /// implement [`SupportsMutations`].  The capability is enforced at **compile time**:
52    /// attempting to call this method with `SqliteAdapter` results in a compiler error.
53    ///
54    /// # Arguments
55    ///
56    /// * `mutation_name` - The GraphQL mutation field name (e.g. `"createUser"`)
57    /// * `variables` - Optional JSON object of GraphQL variable values
58    /// * `type_selections` - Per-type field selections for projection
59    ///
60    /// # Returns
61    ///
62    /// A JSON-encoded GraphQL response value on success.
63    ///
64    /// # Errors
65    ///
66    /// Same as `execute_mutation_query`, minus the adapter
67    /// capability check.
68    pub async fn execute_mutation(
69        &self,
70        mutation_name: &str,
71        variables: Option<&serde_json::Value>,
72        type_selections: &HashMap<String, Vec<String>>,
73    ) -> Result<serde_json::Value> {
74        // No runtime supports_mutations() check: the SupportsMutations bound
75        // guarantees at compile time that this adapter supports mutations.
76        self.mutation_runner()
77            .execute_mutation(mutation_name, variables, type_selections)
78            .await
79    }
80}
81
82impl<A: DatabaseAdapter> Executor<A> {
83    /// Execute a GraphQL mutation by calling the configured database function.
84    ///
85    /// This is the **runtime-guarded** entry point called from [`execute_internal`] when the
86    /// query is classified as a mutation. It checks `adapter.supports_mutations()` at runtime
87    /// (because `execute_internal` is bounded only on `DatabaseAdapter`) and delegates to the
88    /// shared [`execute_mutation_impl`](runners::mutation::execute_mutation_impl) function.
89    ///
90    /// # Errors
91    ///
92    /// * [`FraiseQLError::Validation`] — the adapter does not support mutations, mutation name not
93    ///   found in the compiled schema, no `sql_source` configured, or the database function
94    ///   returned no rows.
95    /// * [`FraiseQLError::Database`] — the adapter's `execute_function_call` returned an error.
96    ///
97    /// # Example
98    ///
99    /// ```no_run
100    /// // Requires: live database adapter with SupportsMutations implementation.
101    /// // See: tests/integration/ for runnable examples.
102    /// # use fraiseql_core::db::postgres::PostgresAdapter;
103    /// # use fraiseql_core::schema::CompiledSchema;
104    /// # use fraiseql_core::runtime::Executor;
105    /// # use std::sync::Arc;
106    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
107    /// # let schema: CompiledSchema = panic!("example");
108    /// # let adapter = PostgresAdapter::new("postgresql://localhost/mydb").await?;
109    /// # let executor = Executor::new(schema, Arc::new(adapter));
110    /// let vars = serde_json::json!({ "name": "Alice", "email": "alice@example.com" });
111    /// let selections = std::collections::HashMap::new(); // no filtering
112    /// // Returns {"data":{"createUser":{"id":"...", "name":"Alice"}}}
113    /// // or      {"data":{"createUser":{"__typename":"UserAlreadyExistsError", "email":"..."}}}
114    /// let result = executor.execute_mutation("createUser", Some(&vars), &selections).await?;
115    /// # Ok(())
116    /// # }
117    /// ```
118    pub(super) async fn execute_mutation_query(
119        &self,
120        mutation_name: &str,
121        variables: Option<&serde_json::Value>,
122        type_selections: &HashMap<String, Vec<String>>,
123    ) -> Result<serde_json::Value> {
124        // Runtime guard: verify this adapter supports mutations.
125        // Note: this is a runtime check, not compile-time enforcement.
126        // The common execute() entry point accepts raw GraphQL strings and
127        // determines the operation type at runtime, which precludes compile-time
128        // mutation gating. The direct execute_mutation() API provides compile-time
129        // enforcement via the SupportsMutations bound on MutationRunner.
130        if !self.ctx.adapter.supports_mutations() {
131            return Err(FraiseQLError::Validation {
132                message: format!(
133                    "Mutation '{mutation_name}' cannot be executed: the configured database \
134                     adapter does not support mutations. Use PostgresAdapter, MySqlAdapter, \
135                     or SqlServerAdapter for mutation operations."
136                ),
137                path:    None,
138            });
139        }
140        runners::mutation::execute_mutation_impl(
141            &self.ctx,
142            mutation_name,
143            variables,
144            None,
145            type_selections,
146        )
147        .await
148    }
149
150    /// Execute a mutation with security context for REST transport.
151    ///
152    /// Delegates to the standard mutation execution path with RLS enforcement.
153    ///
154    /// # Errors
155    ///
156    /// Returns `FraiseQLError::Database` if the adapter returns an error.
157    /// Returns `FraiseQLError::Validation` if inject params require a missing security context.
158    pub async fn execute_mutation_with_security(
159        &self,
160        mutation_name: &str,
161        arguments: &serde_json::Value,
162        security_context: Option<&SecurityContext>,
163    ) -> crate::error::Result<serde_json::Value> {
164        // Build a synthetic GraphQL mutation query and delegate to execute()
165        let args_str = if let Some(obj) = arguments.as_object() {
166            obj.iter().map(|(k, v)| format!("{k}: {v}")).collect::<Vec<_>>().join(", ")
167        } else {
168            String::new()
169        };
170        let query = if args_str.is_empty() {
171            format!("mutation {{ {mutation_name} {{ status entity_id message }} }}")
172        } else {
173            format!("mutation {{ {mutation_name}({args_str}) {{ status entity_id message }} }}")
174        };
175
176        if let Some(ctx) = security_context {
177            self.execute_with_security(&query, None, ctx).await
178        } else {
179            self.execute(&query, None).await
180        }
181    }
182
183    /// Execute a batch of mutations (for REST bulk insert).
184    ///
185    /// Executes each mutation individually and collects results into a `BulkResult`.
186    ///
187    /// # Errors
188    ///
189    /// Returns the first error encountered during batch execution.
190    pub async fn execute_mutation_batch(
191        &self,
192        mutation_name: &str,
193        items: &[serde_json::Value],
194        security_context: Option<&SecurityContext>,
195    ) -> crate::error::Result<crate::runtime::BulkResult> {
196        let mut entities = Vec::with_capacity(items.len());
197        for item in items {
198            let result = self
199                .execute_mutation_with_security(mutation_name, item, security_context)
200                .await?;
201            entities.push(result);
202        }
203        Ok(crate::runtime::BulkResult {
204            affected_rows: entities.len() as u64,
205            entities:      Some(entities),
206        })
207    }
208
209    /// Execute a bulk operation (collection-level PATCH/DELETE) by filter.
210    ///
211    /// # Errors
212    ///
213    /// Returns `FraiseQLError::Database` if the adapter returns an error.
214    pub async fn execute_bulk_by_filter(
215        &self,
216        query_match: &crate::runtime::matcher::QueryMatch,
217        mutation_name: &str,
218        body: Option<&serde_json::Value>,
219        _id_field: &str,
220        _max_affected: u64,
221        security_context: Option<&SecurityContext>,
222    ) -> crate::error::Result<crate::runtime::BulkResult> {
223        // Execute the filter query to find matching rows.
224        let filter_result = self
225            .query_runner()
226            .execute_query_direct(query_match, None, security_context)
227            .await?;
228
229        let args = body.cloned().unwrap_or(serde_json::json!({}));
230        let result = self
231            .execute_mutation_with_security(mutation_name, &args, security_context)
232            .await?;
233
234        let count = filter_result
235            .get("data")
236            .and_then(|d| d.as_object())
237            .and_then(|o| o.values().next())
238            .and_then(|v| v.as_array())
239            .map_or(1, |a| a.len() as u64);
240
241        Ok(crate::runtime::BulkResult {
242            affected_rows: count,
243            entities:      Some(vec![result]),
244        })
245    }
246}