use std::{collections::HashMap, sync::Arc};
use fraiseql_core::{
db::traits::DatabaseAdapter,
runtime::{Executor, QueryMatch},
schema::{Cardinality, CompiledSchema, Relationship, RestConfig},
security::SecurityContext,
};
use crate::routes::rest::handler::RestError;
pub(super) struct EmbedCtx<'a, A: DatabaseAdapter> {
pub executor: &'a Arc<Executor<A>>,
pub schema: &'a CompiledSchema,
pub config: &'a RestConfig,
pub security_context: Option<&'a SecurityContext>,
}
pub(super) async fn embed_into_rows<A: DatabaseAdapter>(
ctx: &EmbedCtx<'_, A>,
rel: &Relationship,
output_name: &str,
sub_field_names: &[String],
embedded_filter: Option<&serde_json::Value>,
rows: &mut [serde_json::Value],
) -> Result<(), RestError> {
for row in rows.iter_mut() {
embed_into_single(ctx, rel, output_name, sub_field_names, embedded_filter, row).await?;
}
Ok(())
}
pub(super) async fn embed_into_single<A: DatabaseAdapter>(
ctx: &EmbedCtx<'_, A>,
rel: &Relationship,
output_name: &str,
sub_field_names: &[String],
embedded_filter: Option<&serde_json::Value>,
row: &mut serde_json::Value,
) -> Result<(), RestError> {
let parent_key_value = extract_join_key(row, rel);
let Some(parent_key_value) = parent_key_value else {
set_empty_embedding(row, output_name, rel.cardinality);
return Ok(());
};
let mut where_obj = serde_json::Map::new();
let filter_field = match rel.cardinality {
Cardinality::ManyToOne | Cardinality::OneToOne => &rel.referenced_key,
_ => &rel.foreign_key,
};
where_obj.insert(filter_field.clone(), serde_json::json!({ "eq": parent_key_value }));
if let Some(filter) = embedded_filter {
if let Some(filter_map) = filter.as_object() {
for (k, v) in filter_map {
where_obj.insert(k.clone(), v.clone());
}
}
}
let where_clause = serde_json::Value::Object(where_obj);
let target_query = find_list_query_for_type(ctx.schema, &rel.target_type);
let Some(target_query) = target_query else {
set_empty_embedding(row, output_name, rel.cardinality);
return Ok(());
};
let target_type_def = ctx.schema.find_type(&rel.target_type);
let mut arguments: HashMap<String, serde_json::Value> = HashMap::new();
arguments.insert("where".to_string(), where_clause);
arguments.insert("limit".to_string(), serde_json::json!(ctx.config.max_page_size));
let query_match = QueryMatch::from_operation(
target_query.clone(),
sub_field_names.to_vec(),
arguments,
target_type_def,
)
.map_err(|e| RestError::internal(format!("Failed to build embedded query: {e}")))?;
let variables = serde_json::json!({});
let vars_ref = Some(&variables);
let result = ctx
.executor
.execute_query_direct(&query_match, vars_ref, ctx.security_context)
.await
.map_err(RestError::from)?;
let embedded_data = extract_query_data(&result, &target_query.name);
if let Some(obj) = row.as_object_mut() {
match rel.cardinality {
Cardinality::OneToMany => {
let arr = match embedded_data {
Some(serde_json::Value::Array(a)) => serde_json::Value::Array(a),
Some(other) => serde_json::json!([other]),
None => serde_json::json!([]),
};
obj.insert(output_name.to_string(), arr);
},
Cardinality::ManyToOne | Cardinality::OneToOne => {
let val = match embedded_data {
Some(serde_json::Value::Array(mut a)) if !a.is_empty() => a.remove(0),
Some(other) => other,
None => serde_json::Value::Null,
};
obj.insert(output_name.to_string(), val);
},
_ => {
obj.insert(
output_name.to_string(),
embedded_data.unwrap_or(serde_json::Value::Null),
);
},
}
}
Ok(())
}
pub(super) fn extract_join_key(
row: &serde_json::Value,
rel: &Relationship,
) -> Option<serde_json::Value> {
let key_field = match rel.cardinality {
Cardinality::ManyToOne | Cardinality::OneToOne => &rel.foreign_key,
_ => &rel.referenced_key,
};
row.get(key_field.as_str()).cloned().filter(|v| !v.is_null())
}
pub(super) fn set_empty_embedding(
row: &mut serde_json::Value,
output_name: &str,
cardinality: Cardinality,
) {
if let Some(obj) = row.as_object_mut() {
match cardinality {
Cardinality::OneToMany => {
obj.insert(output_name.to_string(), serde_json::json!([]));
},
_ => {
obj.insert(output_name.to_string(), serde_json::Value::Null);
},
}
}
}
pub(super) fn find_list_query_for_type<'a>(
schema: &'a CompiledSchema,
type_name: &str,
) -> Option<&'a fraiseql_core::schema::QueryDefinition> {
schema.queries.iter().find(|q| q.return_type == type_name && q.returns_list)
}
pub(super) fn extract_query_data(
parsed: &serde_json::Value,
query_name: &str,
) -> Option<serde_json::Value> {
parsed.get("data").and_then(|d| d.get(query_name)).cloned()
}
pub(super) async fn count_related<A: DatabaseAdapter>(
executor: &Arc<Executor<A>>,
schema: &CompiledSchema,
rel: &Relationship,
row: &serde_json::Value,
security_context: Option<&SecurityContext>,
) -> Result<u64, RestError> {
let parent_key_value = extract_join_key(row, rel);
let Some(parent_key_value) = parent_key_value else {
return Ok(0);
};
let filter_field = match rel.cardinality {
Cardinality::ManyToOne | Cardinality::OneToOne => &rel.referenced_key,
_ => &rel.foreign_key,
};
let mut where_obj = serde_json::Map::new();
where_obj.insert(filter_field.clone(), serde_json::json!({ "eq": parent_key_value }));
let where_clause = serde_json::Value::Object(where_obj);
let target_query = find_list_query_for_type(schema, &rel.target_type);
let Some(target_query) = target_query else {
return Ok(0);
};
let target_type_def = schema.find_type(&rel.target_type);
let mut arguments: HashMap<String, serde_json::Value> = HashMap::new();
arguments.insert("where".to_string(), where_clause);
let query_match =
QueryMatch::from_operation(target_query.clone(), Vec::new(), arguments, target_type_def)
.map_err(|e| RestError::internal(format!("Failed to build count query: {e}")))?;
let variables = serde_json::json!({});
let vars_ref = Some(&variables);
let count = executor
.count_rows(&query_match, vars_ref, security_context)
.await
.map_err(RestError::from)?;
Ok(count)
}