use std::collections::HashMap;
use fraiseql_db::TranslationPart;
use crate::{
error::{FraiseQLError, Result},
graphql::{FieldSelection, GraphQLArgument},
schema::{CompiledSchema, FieldType, LOCALIZED_STRING_TYPE},
};
pub const LOCALE_ARGUMENT: &str = "locale";
pub fn resolve_locale_arguments(
schema: &CompiledSchema,
type_name: &str,
selections: &mut [FieldSelection],
variables: &HashMap<String, serde_json::Value>,
relay: bool,
) -> Result<()> {
if !relay {
return resolve_at(schema, type_name, selections, variables);
}
for edges in selections.iter_mut().filter(|s| s.name == "edges") {
for node in edges.nested_fields.iter_mut().filter(|s| s.name == "node") {
resolve_at(schema, type_name, &mut node.nested_fields, variables)?;
}
}
Ok(())
}
#[must_use]
pub fn mentions_locale_argument(selections: &[FieldSelection]) -> bool {
selections.iter().any(|sel| {
sel.arguments.iter().any(|a| a.name == LOCALE_ARGUMENT)
|| mentions_locale_argument(&sel.nested_fields)
})
}
fn resolve_at(
schema: &CompiledSchema,
type_name: &str,
selections: &mut [FieldSelection],
variables: &HashMap<String, serde_json::Value>,
) -> Result<()> {
let Some(type_def) = schema.find_type(type_name) else {
return Ok(());
};
for sel in selections {
if let Some(condition) = sel.name.strip_prefix("...on ") {
resolve_at(schema, condition.trim(), &mut sel.nested_fields, variables)?;
continue;
}
let label = format!("{type_name}.{}", sel.name);
if type_def.translations_of(&sel.name).is_some() {
check_translations(&label, sel)?;
continue;
}
let Some(field) = type_def.find_field(&sel.name) else {
continue;
};
if let Some(at) = sel.arguments.iter().position(|a| a.name == LOCALE_ARGUMENT) {
if !field.localized {
return Err(refusal(format!(
"Unknown argument '{LOCALE_ARGUMENT}' on {label}: it is defined on localized \
fields only"
)));
}
match canonical_tag(schema, &label, &sel.arguments[at], variables)? {
Some(tag) => {
sel.arguments[at] = GraphQLArgument {
name: LOCALE_ARGUMENT.to_string(),
value_type: "string".to_string(),
value_json: serde_json::Value::String(tag).to_string(),
};
},
None => {
sel.arguments.remove(at);
},
}
}
let child = match &field.field_type {
FieldType::List { item: inner, .. } => inner.type_name(),
other => other.type_name(),
};
if let Some(child) = child.filter(|_| !sel.nested_fields.is_empty()) {
resolve_at(schema, child, &mut sel.nested_fields, variables)?;
}
}
Ok(())
}
fn check_translations(label: &str, sel: &FieldSelection) -> Result<()> {
if let Some(argument) = sel.arguments.first() {
return Err(refusal(format!("Unknown argument '{}' on {label}", argument.name)));
}
for sub in &sel.nested_fields {
let name = sub.name.strip_prefix("...on ").map_or(sub.name.as_str(), str::trim);
let known =
matches!(name, "locale" | "value" | LOCALIZED_STRING_TYPE) || name.starts_with("__");
if !known {
return Err(refusal(format!(
"Cannot query field '{name}' on type '{LOCALIZED_STRING_TYPE}'."
)));
}
if name == LOCALIZED_STRING_TYPE {
check_translations(label, sub)?;
}
}
Ok(())
}
fn canonical_tag(
schema: &CompiledSchema,
label: &str,
argument: &GraphQLArgument,
variables: &HashMap<String, serde_json::Value>,
) -> Result<Option<String>> {
let value = crate::runtime::QueryMatcher::resolve_inline_arg(argument, variables)?;
let requested = match value {
None | Some(serde_json::Value::Null) => return Ok(None),
Some(serde_json::Value::String(requested)) => requested,
Some(other) => {
return Err(refusal(format!(
"Argument '{LOCALE_ARGUMENT}' on {label} must be a String, got {other}"
)));
},
};
let Some(config) = schema.locale.as_ref() else {
return Ok(None);
};
config.match_tag(&requested).map(|tag| Some(tag.to_string())).ok_or_else(|| {
refusal(format!(
"Argument '{LOCALE_ARGUMENT}' on {label}: \"{requested}\" is not an allowed locale \
(allowed: {})",
config.allowed.join(", ")
))
})
}
const fn refusal(message: String) -> FraiseQLError {
FraiseQLError::Validation {
message,
path: None,
}
}
#[must_use]
pub fn selection_chain(schema: &CompiledSchema, selection: &FieldSelection) -> Option<Vec<String>> {
argued_chain(schema, selection).or_else(|| crate::runtime::localization_chain(schema))
}
#[must_use]
pub fn selection_chain_in(
schema: &CompiledSchema,
selection: &FieldSelection,
request_chain: Option<&[String]>,
) -> Option<Vec<String>> {
argued_chain(schema, selection).or_else(|| request_chain.map(<[String]>::to_vec))
}
fn argued_chain(schema: &CompiledSchema, selection: &FieldSelection) -> Option<Vec<String>> {
selection
.arguments
.iter()
.find(|a| a.name == LOCALE_ARGUMENT)
.and_then(|a| crate::graphql::value_json::decode(&a.value_json).ok())
.and_then(|value| value.as_str().map(str::to_string))
.and_then(|tag| schema.locale.as_ref()?.chain(&tag).map(<[String]>::to_vec))
}
#[must_use]
pub fn translations_read(
schema: &CompiledSchema,
selection: &FieldSelection,
) -> Option<fraiseql_db::LocalizedRead> {
Some(fraiseql_db::LocalizedRead::Translations {
allowed: schema.locale.as_ref()?.allowed.clone(),
keys: translation_keys(&selection.nested_fields),
})
}
fn translation_keys(selections: &[FieldSelection]) -> Vec<(String, TranslationPart)> {
let mut keys = Vec::new();
for sel in selections {
if sel.name.strip_prefix("...on ").is_some() {
keys.extend(translation_keys(&sel.nested_fields));
continue;
}
let part = match sel.name.as_str() {
"locale" => TranslationPart::Locale,
"value" => TranslationPart::Value,
"__typename" => TranslationPart::Typename,
_ => continue,
};
keys.push((sel.response_key().to_string(), part));
}
keys
}
#[must_use]
pub fn translations(
value: &serde_json::Value,
read: &fraiseql_db::LocalizedRead,
) -> serde_json::Value {
let fraiseql_db::LocalizedRead::Translations { allowed, keys } = read else {
return serde_json::Value::Array(Vec::new());
};
let map = value.as_object();
let elements = allowed
.iter()
.filter_map(|tag| {
let label = map?.get(tag)?.as_str()?;
let element = keys
.iter()
.map(|(key, part)| {
let v = match part {
TranslationPart::Locale => tag.as_str(),
TranslationPart::Value => label,
TranslationPart::Typename => LOCALIZED_STRING_TYPE,
};
(key.clone(), serde_json::Value::String(v.to_string()))
})
.collect::<serde_json::Map<_, _>>();
Some(serde_json::Value::Object(element))
})
.collect();
serde_json::Value::Array(elements)
}