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 super::{Executor, runners};
15use crate::{
16 db::traits::{DatabaseAdapter, SupportsMutations},
17 error::{FraiseQLError, Result},
18 graphql::FieldSelection,
19 security::SecurityContext,
20};
21
22/// Compile-time enforcement: `SqliteAdapter` must NOT implement `SupportsMutations`.
23///
24/// Calling `execute_mutation` on an `Executor<SqliteAdapter>` must not compile
25/// because `SqliteAdapter` does not implement the `SupportsMutations` marker trait.
26///
27/// ```compile_fail
28/// use fraiseql_core::runtime::Executor;
29/// use fraiseql_core::db::sqlite::SqliteAdapter;
30/// use fraiseql_core::schema::CompiledSchema;
31/// use std::sync::Arc;
32/// async fn _wont_compile() {
33/// let adapter = Arc::new(SqliteAdapter::new_in_memory().await.unwrap());
34/// let executor = Executor::new(CompiledSchema::new(), adapter);
35/// executor.execute_mutation("createUser", None, &[]).await.unwrap();
36/// }
37/// ```
38impl<A: DatabaseAdapter + SupportsMutations> Executor<A> {
39 /// Construct a mutation runner on demand.
40 ///
41 /// Zero-cost: `Arc::clone` is one atomic increment, no allocation.
42 fn mutation_runner(&self) -> runners::mutation::MutationRunner<A> {
43 runners::mutation::MutationRunner::new(std::sync::Arc::clone(&self.ctx))
44 }
45
46 /// Execute a GraphQL mutation directly, with compile-time capability enforcement.
47 ///
48 /// Unlike `execute()` (which accepts raw GraphQL strings and performs a runtime
49 /// `supports_mutations()` check), this method is only available on adapters that
50 /// implement [`SupportsMutations`]. The capability is enforced at **compile time**:
51 /// attempting to call this method with `SqliteAdapter` results in a compiler error.
52 ///
53 /// # Arguments
54 ///
55 /// * `mutation_name` - The GraphQL mutation field name (e.g. `"createUser"`)
56 /// * `variables` - Optional JSON object of GraphQL variable values
57 /// * `selections` - The result selection set (inline fragments intact) used to project the
58 /// response; pass `&[]` for no field filtering.
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 selections: &[FieldSelection],
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, 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 /// // Returns {"data":{"createUser":{"id":"...", "name":"Alice"}}}
112 /// // or {"data":{"createUser":{"__typename":"UserAlreadyExistsError", "email":"..."}}}
113 /// let result = executor.execute_mutation("createUser", Some(&vars), &[]).await?;
114 /// # Ok(())
115 /// # }
116 /// ```
117 pub(super) async fn execute_mutation_query(
118 &self,
119 mutation_name: &str,
120 variables: Option<&serde_json::Value>,
121 security_context: Option<&SecurityContext>,
122 selections: &[FieldSelection],
123 inline_arguments: &[crate::graphql::GraphQLArgument],
124 ) -> Result<serde_json::Value> {
125 // Runtime guard: verify this adapter supports mutations.
126 // Note: this is a runtime check, not compile-time enforcement.
127 // The common execute() entry point accepts raw GraphQL strings and
128 // determines the operation type at runtime, which precludes compile-time
129 // mutation gating. The direct execute_mutation() API provides compile-time
130 // enforcement via the SupportsMutations bound on MutationRunner.
131 if !self.ctx.adapter.supports_mutations() {
132 return Err(FraiseQLError::Validation {
133 message: format!(
134 "Mutation '{mutation_name}' cannot be executed: the configured database \
135 adapter does not support mutations. Use PostgresAdapter, MySqlAdapter, \
136 or SqlServerAdapter for mutation operations."
137 ),
138 path: None,
139 });
140 }
141 runners::mutation::execute_mutation_impl(
142 &self.ctx,
143 mutation_name,
144 variables,
145 security_context,
146 selections,
147 inline_arguments,
148 )
149 .await
150 }
151
152 /// Execute a mutation with security context for REST transport.
153 ///
154 /// Delegates to the standard mutation execution path with RLS enforcement.
155 ///
156 /// # Errors
157 ///
158 /// Returns `FraiseQLError::Database` if the adapter returns an error.
159 /// Returns `FraiseQLError::Validation` if inject params require a missing security context.
160 pub async fn execute_mutation_with_security(
161 &self,
162 mutation_name: &str,
163 arguments: &serde_json::Value,
164 security_context: Option<&SecurityContext>,
165 ) -> crate::error::Result<serde_json::Value> {
166 // Build a synthetic GraphQL mutation query and delegate to execute()
167 let args_str = if let Some(obj) = arguments.as_object() {
168 obj.iter().map(|(k, v)| format!("{k}: {v}")).collect::<Vec<_>>().join(", ")
169 } else {
170 String::new()
171 };
172 let query = if args_str.is_empty() {
173 format!("mutation {{ {mutation_name} {{ status entity_id message }} }}")
174 } else {
175 format!("mutation {{ {mutation_name}({args_str}) {{ status entity_id message }} }}")
176 };
177
178 if let Some(ctx) = security_context {
179 self.execute_with_security(&query, None, ctx).await
180 } else {
181 self.execute(&query, None).await
182 }
183 }
184
185 /// Execute a batch of mutations (for REST bulk insert).
186 ///
187 /// Executes each mutation individually and collects results into a `BulkResult`.
188 ///
189 /// # Errors
190 ///
191 /// Returns the first error encountered during batch execution.
192 pub async fn execute_mutation_batch(
193 &self,
194 mutation_name: &str,
195 items: &[serde_json::Value],
196 security_context: Option<&SecurityContext>,
197 ) -> crate::error::Result<crate::runtime::BulkResult> {
198 let mut entities = Vec::with_capacity(items.len());
199 for item in items {
200 let result = self
201 .execute_mutation_with_security(mutation_name, item, security_context)
202 .await?;
203 entities.push(result);
204 }
205 Ok(crate::runtime::BulkResult {
206 affected_rows: entities.len() as u64,
207 entities: Some(entities),
208 })
209 }
210
211 /// Execute a bulk operation (collection-level PATCH/DELETE) by filter.
212 ///
213 /// # Errors
214 ///
215 /// Returns `FraiseQLError::Database` if the adapter returns an error.
216 pub async fn execute_bulk_by_filter(
217 &self,
218 query_match: &crate::runtime::matcher::QueryMatch,
219 mutation_name: &str,
220 body: Option<&serde_json::Value>,
221 _id_field: &str,
222 _max_affected: u64,
223 security_context: Option<&SecurityContext>,
224 ) -> crate::error::Result<crate::runtime::BulkResult> {
225 // Execute the filter query to find matching rows.
226 let filter_result = self
227 .query_runner()
228 .execute_query_direct(query_match, None, security_context)
229 .await?;
230
231 let args = body.cloned().unwrap_or(serde_json::json!({}));
232 let result = self
233 .execute_mutation_with_security(mutation_name, &args, security_context)
234 .await?;
235
236 let count = filter_result
237 .get("data")
238 .and_then(|d| d.as_object())
239 .and_then(|o| o.values().next())
240 .and_then(|v| v.as_array())
241 .map_or(1, |a| a.len() as u64);
242
243 Ok(crate::runtime::BulkResult {
244 affected_rows: count,
245 entities: Some(vec![result]),
246 })
247 }
248}