use fraiseql_core::{db::types::ReadRouting, schema::InputStyle};
use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use super::types::IntermediateDeprecation;
pub(super) mod inject_params_serde {
use indexmap::IndexMap;
use serde::{
Deserialize, Deserializer, Serializer,
de::Error as _,
ser::{Error as _, SerializeMap as _},
};
#[derive(Deserialize)]
#[serde(untagged)]
enum Spec {
Flat(String),
Nested { source: String, claim: String },
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<IndexMap<String, String>, D::Error>
where
D: Deserializer<'de>,
{
IndexMap::<String, Spec>::deserialize(deserializer)?
.into_iter()
.map(|(param, spec)| match spec {
Spec::Flat(source) if source.contains(':') => Ok((param, source)),
Spec::Flat(source) => Err(D::Error::custom(format!(
"inject_params['{param}'] must be \"<source>:<claim>\" (for example \
\"jwt:org_id\") or {{\"source\": \"jwt\", \"claim\": \"org_id\"}}, got \
\"{source}\""
))),
Spec::Nested { source, claim } => Ok((param, format!("{source}:{claim}"))),
})
.collect()
}
pub fn serialize<S>(map: &IndexMap<String, String>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut out = serializer.serialize_map(Some(map.len()))?;
for (param, spec) in map {
let (source, claim) = spec.split_once(':').ok_or_else(|| {
S::Error::custom(format!(
"inject_params['{param}'] is not in \"<source>:<claim>\" form: \"{spec}\""
))
})?;
out.serialize_entry(param, &serde_json::json!({"source": source, "claim": claim}))?;
}
out.end()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IntermediateRest {
pub path: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IntermediateInjectDefaults {
#[serde(
default,
with = "inject_params_serde",
skip_serializing_if = "IndexMap::is_empty"
)]
pub base: IndexMap<String, String>,
#[serde(
default,
with = "inject_params_serde",
skip_serializing_if = "IndexMap::is_empty"
)]
pub queries: IndexMap<String, String>,
#[serde(
default,
with = "inject_params_serde",
skip_serializing_if = "IndexMap::is_empty"
)]
pub mutations: IndexMap<String, String>,
}
impl IntermediateInjectDefaults {
pub fn for_queries(&self) -> IndexMap<String, String> {
Self::layer(&self.base, &self.queries)
}
pub fn for_mutations(&self) -> IndexMap<String, String> {
Self::layer(&self.base, &self.mutations)
}
fn layer(
base: &IndexMap<String, String>,
specific: &IndexMap<String, String>,
) -> IndexMap<String, String> {
let mut merged = base.clone();
for (key, source) in specific {
merged.insert(key.clone(), source.clone());
}
merged
}
pub fn apply_to(
defaults: &IndexMap<String, String>,
inject: &mut IndexMap<String, String>,
excluded: &[String],
operation: &str,
) -> anyhow::Result<Vec<String>> {
for key in excluded {
if inject.contains_key(key) {
anyhow::bail!(
"Operation '{operation}' both declares inject_params['{key}'] and lists \
'{key}' in exclude_inject_defaults. An exclusion drops a project default; \
a parameter the operation declares itself is not a default. Keep one."
);
}
if !defaults.contains_key(key) {
anyhow::bail!(
"Operation '{operation}' lists '{key}' in exclude_inject_defaults, but no \
[inject_defaults] entry supplies '{key}' to it. Remove the exclusion, or fix \
its spelling — an exclusion that matches nothing leaves every default applied."
);
}
}
let mut added = Vec::new();
for (key, source) in defaults {
let declared = inject.contains_key(key) || inject.values().any(|own| own == source);
if declared || excluded.contains(key) {
continue;
}
inject.insert(key.clone(), source.clone());
added.push(key.clone());
}
Ok(added)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IntermediateArgument {
pub name: String,
#[serde(rename = "type")]
pub arg_type: String,
pub nullable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<IntermediateDeprecation>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub localized: bool,
}
impl From<&crate::config::toml_schema::ArgumentDefinition> for IntermediateArgument {
fn from(arg: &crate::config::toml_schema::ArgumentDefinition) -> Self {
Self {
name: arg.name.clone(),
arg_type: arg.arg_type.clone(),
nullable: !arg.required,
default: arg.default.clone(),
description: arg.description.clone(),
deprecated: None,
localized: false,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IntermediateQuery {
pub name: String,
pub return_type: String,
#[serde(default)]
pub returns_list: bool,
#[serde(default)]
pub nullable: bool,
#[serde(default)]
pub arguments: Vec<IntermediateArgument>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sql_source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub function: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub auto_params: Option<IntermediateAutoParams>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<IntermediateDeprecation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub jsonb_column: Option<String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub relay: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub count: bool,
#[serde(
default,
rename = "inject_params",
with = "inject_params_serde",
skip_serializing_if = "IndexMap::is_empty"
)]
pub inject: IndexMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exclude_inject_defaults: Vec<String>,
#[serde(default, skip_serializing_if = "ReadRouting::is_default")]
pub read_routing: ReadRouting,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cache_ttl_seconds: Option<u64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub additional_views: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requires_role: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_actor: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relay_cursor_type: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rest: Option<IntermediateRest>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub rest_stream: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub pagination_order: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IntermediateMutation {
pub name: String,
pub return_type: String,
#[serde(default)]
pub returns_list: bool,
#[serde(default)]
pub nullable: bool,
#[serde(default)]
pub arguments: Vec<IntermediateArgument>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sql_source: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub operation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<IntermediateDeprecation>,
#[serde(
default,
rename = "inject_params",
with = "inject_params_serde",
skip_serializing_if = "IndexMap::is_empty"
)]
pub inject: IndexMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub exclude_inject_defaults: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requires_role: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub requires_actor: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub invalidates_fact_tables: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub invalidates_views: Vec<String>,
#[serde(default = "default_changelog")]
pub changelog: bool,
#[serde(default, skip_serializing_if = "InputStyle::is_flatten")]
pub input_style: InputStyle,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub changelog_pre_image: bool,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub cascade: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub success_fields: Vec<super::IntermediateField>,
#[serde(
default,
skip_serializing_if = "fraiseql_core::schema::CascadeSource::is_function"
)]
pub cascade_source: fraiseql_core::schema::CascadeSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rest: Option<IntermediateRest>,
}
impl Default for IntermediateMutation {
fn default() -> Self {
Self {
name: String::new(),
return_type: String::new(),
returns_list: false,
nullable: false,
arguments: Vec::new(),
description: None,
sql_source: None,
operation: None,
deprecated: None,
inject: IndexMap::new(),
exclude_inject_defaults: Vec::new(),
requires_actor: Vec::new(),
requires_role: None,
invalidates_fact_tables: Vec::new(),
invalidates_views: Vec::new(),
changelog: default_changelog(),
input_style: InputStyle::Flatten,
changelog_pre_image: false,
cascade: false,
success_fields: Vec::new(),
cascade_source: fraiseql_core::schema::CascadeSource::Function,
rest: None,
}
}
}
const fn default_changelog() -> bool {
true
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IntermediateAutoParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub offset: Option<bool>,
#[serde(rename = "where", default, skip_serializing_if = "Option::is_none")]
pub where_clause: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub order_by: Option<bool>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IntermediateQueryDefaults {
pub where_clause: bool,
pub order_by: bool,
pub limit: bool,
pub offset: bool,
pub pagination_order: crate::config::toml_schema::PaginationPosture,
}
impl Default for IntermediateQueryDefaults {
fn default() -> Self {
Self {
where_clause: true,
order_by: true,
limit: true,
offset: true,
pagination_order: crate::config::toml_schema::PaginationPosture::Identity,
}
}
}