use super::{Executor, runners};
use crate::{
error::{FraiseQLError, Result},
graphql::FieldSelection,
security::SecurityContext,
};
impl Executor {
fn mutation_runner(&self) -> runners::mutation::MutationRunner {
runners::mutation::MutationRunner::new(std::sync::Arc::clone(&self.ctx))
}
pub async fn execute_mutation(
&self,
mutation_name: &str,
variables: Option<&serde_json::Value>,
selections: WriteSelections<'_>,
) -> Result<serde_json::Value> {
self.mutation_runner()
.execute_mutation(mutation_name, variables, selections)
.await
}
pub async fn execute_mutation_as(
&self,
mutation_name: &str,
variables: Option<&serde_json::Value>,
security_context: Option<&SecurityContext>,
selections: WriteSelections<'_>,
) -> Result<crate::runtime::MutationExecution> {
self.execute_mutation_detailed(
mutation_name,
mutation_name,
variables,
security_context,
selections,
&[],
)
.await
}
pub async fn execute_mutation_with_security(
&self,
mutation_name: &str,
arguments: &serde_json::Value,
security_context: Option<&SecurityContext>,
) -> crate::error::Result<serde_json::Value> {
let selections = mutation_return_selections(self.schema(), mutation_name);
self.execute_mutation_detailed(
mutation_name,
mutation_name,
Some(arguments),
security_context,
WriteSelections::new(&selections)?,
&[],
)
.await
.map(|execution| execution.data)
}
pub async fn execute_mutation_batch(
&self,
mutation_name: &str,
items: &[serde_json::Value],
security_context: Option<&SecurityContext>,
) -> crate::error::Result<crate::runtime::BulkResult> {
let mut entities = Vec::with_capacity(items.len());
for item in items {
let result = self
.execute_mutation_with_security(mutation_name, item, security_context)
.await?;
entities.push(result);
}
Ok(crate::runtime::BulkResult {
affected_rows: entities.len() as u64,
entities: Some(entities),
})
}
pub async fn execute_bulk_by_ids(
&self,
mutation_name: &str,
id_field: &str,
ids: &[serde_json::Value],
body: Option<&serde_json::Value>,
security_context: Option<&SecurityContext>,
) -> crate::error::Result<crate::runtime::BulkResult> {
let mut entities = Vec::with_capacity(ids.len());
for id in ids {
let mut args = body.and_then(|b| b.as_object().cloned()).unwrap_or_default();
args.insert(id_field.to_string(), id.clone());
let result = self
.execute_mutation_with_security(
mutation_name,
&serde_json::Value::Object(args),
security_context,
)
.await?;
entities.push(result);
}
Ok(crate::runtime::BulkResult {
affected_rows: u64::try_from(entities.len()).unwrap_or(u64::MAX),
entities: Some(entities),
})
}
}
impl Executor {
pub(super) async fn execute_mutation_query(
&self,
mutation_name: &str,
response_key: &str,
variables: Option<&serde_json::Value>,
security_context: Option<&SecurityContext>,
selections: &[FieldSelection],
inline_arguments: &[crate::graphql::GraphQLArgument],
) -> Result<serde_json::Value> {
if let Some(def) = self.schema().find_mutation(mutation_name) {
crate::graphql::validate_leaf_field_selections(
self.schema(),
&def.return_type,
selections,
)?;
}
self.execute_mutation_detailed(
mutation_name,
response_key,
variables,
security_context,
WriteSelections::new(selections)?,
inline_arguments,
)
.await
.map(|execution| execution.data)
}
pub(super) async fn execute_mutation_detailed(
&self,
mutation_name: &str,
response_key: &str,
variables: Option<&serde_json::Value>,
security_context: Option<&SecurityContext>,
selections: WriteSelections<'_>,
inline_arguments: &[crate::graphql::GraphQLArgument],
) -> Result<runners::mutation::MutationExecution> {
runners::mutation::execute_mutation_impl(
&self.ctx,
mutation_name,
response_key,
variables,
security_context,
selections,
inline_arguments,
)
.await
}
}
#[derive(Clone, Copy, Debug)]
pub struct WriteSelections<'a>(&'a [FieldSelection]);
impl<'a> WriteSelections<'a> {
pub fn new(selections: &'a [FieldSelection]) -> Result<Self> {
if selections.is_empty() {
return Err(FraiseQLError::Validation {
message: "A write needs a selection set: an empty one is read as \"no field \
filtering\", which returns the stored entity whole and skips \
field-level authorization."
.to_string(),
path: None,
});
}
Ok(Self(selections))
}
#[must_use]
pub const fn as_slice(self) -> &'a [FieldSelection] {
self.0
}
}
#[cfg(test)]
pub fn any_write_selections() -> WriteSelections<'static> {
static SET: std::sync::OnceLock<Vec<FieldSelection>> = std::sync::OnceLock::new();
let set = SET.get_or_init(|| {
vec![FieldSelection {
name: "__typename".to_string(),
alias: None,
arguments: vec![],
nested_fields: vec![],
directives: vec![],
}]
});
WriteSelections::new(set).expect("a one-field selection set is not empty")
}
#[must_use]
pub fn mutation_return_selections(
schema: &crate::schema::CompiledSchema,
mutation_name: &str,
) -> Vec<FieldSelection> {
fn named(name: &str) -> FieldSelection {
FieldSelection {
name: name.to_string(),
alias: None,
arguments: vec![],
nested_fields: vec![],
directives: vec![],
}
}
schema
.find_mutation(mutation_name)
.and_then(|m| schema.find_type(&m.return_type))
.map(|t| {
t.fields
.iter()
.filter(|f| f.field_type.is_scalar())
.map(|f| named(f.output_name()))
.collect::<Vec<_>>()
})
.filter(|s| !s.is_empty())
.unwrap_or_else(|| vec![named("status"), named("entity_id"), named("message")])
}