use serde_json::{Map, Value as JsonValue};
use crate::{
db::types::JsonbValue,
error::{FraiseQLError, Result},
graphql::FieldSelection,
schema::{CompiledSchema, FieldDefinition},
utils::casing::{to_camel_case, to_snake_case},
};
#[derive(Debug, Clone)]
pub struct FieldMapping {
pub source: String,
pub output: String,
pub source_fallback: Option<String>,
pub nested_typename: Option<String>,
pub nested_fields: Option<Vec<FieldMapping>>,
}
impl FieldMapping {
#[must_use]
pub fn simple(name: impl Into<String>) -> Self {
let name = name.into();
Self {
source: name.clone(),
output: name,
source_fallback: None,
nested_typename: None,
nested_fields: None,
}
}
#[must_use]
pub fn aliased(source: impl Into<String>, alias: impl Into<String>) -> Self {
Self {
source: source.into(),
output: alias.into(),
source_fallback: None,
nested_typename: None,
nested_fields: None,
}
}
#[must_use]
pub fn nested_object(
name: impl Into<String>,
typename: impl Into<String>,
fields: Vec<FieldMapping>,
) -> Self {
let name = name.into();
Self {
source: name.clone(),
output: name,
source_fallback: None,
nested_typename: Some(typename.into()),
nested_fields: Some(fields),
}
}
#[must_use]
pub fn nested_object_aliased(
source: impl Into<String>,
alias: impl Into<String>,
typename: impl Into<String>,
fields: Vec<FieldMapping>,
) -> Self {
Self {
source: source.into(),
output: alias.into(),
source_fallback: None,
nested_typename: Some(typename.into()),
nested_fields: Some(fields),
}
}
#[must_use]
pub fn with_nested_typename(mut self, typename: impl Into<String>) -> Self {
self.nested_typename = Some(typename.into());
self
}
#[must_use]
pub fn with_nested_fields(mut self, fields: Vec<FieldMapping>) -> Self {
self.nested_fields = Some(fields);
self
}
}
#[derive(Debug, Clone)]
pub struct ProjectionMapper {
pub fields: Vec<FieldMapping>,
pub typename: Option<String>,
pub federation_mode: bool,
}
impl ProjectionMapper {
#[must_use]
pub fn new(fields: Vec<String>) -> Self {
Self {
fields: fields.into_iter().map(FieldMapping::simple).collect(),
typename: None,
federation_mode: false,
}
}
#[must_use]
pub const fn with_mappings(fields: Vec<FieldMapping>) -> Self {
Self {
fields,
typename: None,
federation_mode: false,
}
}
#[must_use]
pub fn with_typename(mut self, typename: impl Into<String>) -> Self {
self.typename = Some(typename.into());
self
}
#[must_use]
pub const fn with_federation_mode(mut self, enabled: bool) -> Self {
self.federation_mode = enabled;
self
}
pub fn project(&self, jsonb: &JsonbValue) -> Result<JsonValue> {
let value = jsonb.as_value();
match value {
JsonValue::Object(map) => self.project_json_object(map),
JsonValue::Array(arr) => self.project_json_array(arr),
v => Ok(v.clone()),
}
}
pub fn project_json_object(
&self,
map: &serde_json::Map<String, JsonValue>,
) -> Result<JsonValue> {
let mut result = Map::new();
if let Some(ref typename) = self.typename {
result.insert("__typename".to_string(), JsonValue::String(typename.clone()));
}
for field in &self.fields {
let value = map
.get(&field.source)
.or_else(|| field.source_fallback.as_ref().and_then(|fb| map.get(fb)));
if let Some(value) = value {
let projected_value = self.project_nested_value(value, field)?;
result.insert(field.output.clone(), projected_value);
}
}
Ok(JsonValue::Object(result))
}
#[allow(clippy::self_only_used_in_recursion)] fn project_nested_value(&self, value: &JsonValue, field: &FieldMapping) -> Result<JsonValue> {
match value {
JsonValue::Object(obj) => {
if let Some(ref typename) = field.nested_typename {
let mut result = Map::new();
result.insert("__typename".to_string(), JsonValue::String(typename.clone()));
if let Some(ref nested_fields) = field.nested_fields {
for nested_field in nested_fields {
if let Some(nested_value) = obj.get(&nested_field.source) {
let projected =
self.project_nested_value(nested_value, nested_field)?;
result.insert(nested_field.output.clone(), projected);
}
}
} else {
for (k, v) in obj {
result.insert(k.clone(), v.clone());
}
}
Ok(JsonValue::Object(result))
} else {
Ok(value.clone())
}
},
JsonValue::Array(arr) => {
if field.nested_typename.is_some() {
let projected: Result<Vec<JsonValue>> =
arr.iter().map(|item| self.project_nested_value(item, field)).collect();
Ok(JsonValue::Array(projected?))
} else {
Ok(value.clone())
}
},
_ => {
if let JsonValue::String(ref s) = *value {
if let Ok(parsed @ (JsonValue::Object(_) | JsonValue::Array(_))) =
serde_json::from_str::<JsonValue>(s)
{
return self.project_nested_value(&parsed, field);
}
}
Ok(value.clone())
},
}
}
fn project_json_array(&self, arr: &[JsonValue]) -> Result<JsonValue> {
let projected: Vec<JsonValue> = arr
.iter()
.filter_map(|item| {
if let JsonValue::Object(obj) = item {
self.project_json_object(obj).ok()
} else {
Some(item.clone())
}
})
.collect();
Ok(JsonValue::Array(projected))
}
}
pub struct ResultProjector {
mapper: ProjectionMapper,
}
impl ResultProjector {
#[must_use]
pub fn new(fields: Vec<String>) -> Self {
Self {
mapper: ProjectionMapper::new(fields),
}
}
#[must_use]
pub const fn with_mappings(fields: Vec<FieldMapping>) -> Self {
Self {
mapper: ProjectionMapper::with_mappings(fields),
}
}
#[must_use]
pub fn with_typename(mut self, typename: impl Into<String>) -> Self {
self.mapper = self.mapper.with_typename(typename);
self
}
#[must_use]
pub fn configure_typename_from_selections(
self,
selections: &[FieldSelection],
entity_type: &str,
) -> Self {
let wants_typename = selections
.first()
.is_some_and(|root| root.nested_fields.iter().any(|f| f.name == "__typename"));
if wants_typename {
self.with_typename(entity_type)
} else {
self
}
}
#[must_use]
pub fn with_federation_mode(mut self, enabled: bool) -> Self {
self.mapper = self.mapper.with_federation_mode(enabled);
self
}
pub fn project_results(&self, results: &[JsonbValue], is_list: bool) -> Result<JsonValue> {
if is_list {
let projected: Result<Vec<JsonValue>> =
results.iter().map(|r| self.mapper.project(r)).collect();
Ok(JsonValue::Array(projected?))
} else {
if let Some(first) = results.first() {
self.mapper.project(first)
} else {
Ok(JsonValue::Null)
}
}
}
#[must_use]
pub fn wrap_in_data_envelope(result: JsonValue, query_name: &str) -> JsonValue {
let mut data = Map::new();
data.insert(query_name.to_string(), result);
let mut response = Map::new();
response.insert("data".to_string(), JsonValue::Object(data));
JsonValue::Object(response)
}
pub fn add_typename_only(
&self,
projected_data: &JsonbValue,
typename: &str,
) -> Result<JsonValue> {
let value = projected_data.as_value();
match value {
JsonValue::Object(map) => {
let mut result = map.clone();
result.insert("__typename".to_string(), JsonValue::String(typename.to_string()));
Ok(JsonValue::Object(result))
},
JsonValue::Array(arr) => {
let updated: Result<Vec<JsonValue>> = arr
.iter()
.map(|item| {
if let JsonValue::Object(obj) = item {
let mut result = obj.clone();
result.insert(
"__typename".to_string(),
JsonValue::String(typename.to_string()),
);
Ok(JsonValue::Object(result))
} else {
Ok(item.clone())
}
})
.collect();
Ok(JsonValue::Array(updated?))
},
v => Ok(v.clone()),
}
}
#[must_use]
pub fn wrap_error(error: &FraiseQLError) -> JsonValue {
let mut error_obj = Map::new();
error_obj.insert("message".to_string(), JsonValue::String(error.to_string()));
let mut response = Map::new();
response.insert("errors".to_string(), JsonValue::Array(vec![JsonValue::Object(error_obj)]));
JsonValue::Object(response)
}
}
const MAX_ENTITY_PROJECTION_DEPTH: usize = 4;
#[must_use]
pub fn project_entity(
entity: &JsonValue,
type_name: &str,
selections: &[FieldSelection],
schema: &CompiledSchema,
) -> JsonValue {
if selections.is_empty() {
return entity.clone();
}
project_entity_at(entity, type_name, selections, schema, 0)
}
fn project_entity_at(
entity: &JsonValue,
type_name: &str,
selections: &[FieldSelection],
schema: &CompiledSchema,
depth: usize,
) -> JsonValue {
let JsonValue::Object(obj) = entity else {
return entity.clone();
};
let type_def = schema.find_type(type_name);
let mut out = Map::new();
for sel in effective_selections(selections, type_name, schema) {
if sel.name == "__typename" {
out.insert(sel.response_key().to_string(), JsonValue::String(type_name.to_string()));
continue;
}
let field_def =
type_def.and_then(|td| td.fields.iter().find(|f| f.name.as_str() == sel.name));
let Some(value) = lookup_source(obj, &sel.name) else {
continue;
};
let projected = project_field_value(value, field_def, &sel.nested_fields, schema, depth);
out.insert(sel.response_key().to_string(), projected);
}
JsonValue::Object(out)
}
fn project_field_value(
value: &JsonValue,
field_def: Option<&FieldDefinition>,
nested: &[FieldSelection],
schema: &CompiledSchema,
depth: usize,
) -> JsonValue {
if depth < MAX_ENTITY_PROJECTION_DEPTH && !nested.is_empty() {
if let Some(fd) = field_def {
if !fd.field_type.is_scalar() && !fd.field_type.is_list() {
if let Some(child_type) = fd.field_type.type_name() {
match value {
JsonValue::Object(_) => {
return project_entity_at(value, child_type, nested, schema, depth + 1);
},
JsonValue::String(s) => {
if let Ok(parsed @ JsonValue::Object(_)) =
serde_json::from_str::<JsonValue>(s)
{
return project_entity_at(
&parsed,
child_type,
nested,
schema,
depth + 1,
);
}
},
_ => {},
}
}
}
}
}
value.clone()
}
fn lookup_source<'a>(obj: &'a Map<String, JsonValue>, field_name: &str) -> Option<&'a JsonValue> {
let snake = to_snake_case(field_name);
if let Some(v) = obj.get(&snake) {
return Some(v);
}
let camel = to_camel_case(field_name);
if camel != snake {
obj.get(&camel)
} else {
None
}
}
fn effective_selections<'a>(
selections: &'a [FieldSelection],
type_name: &str,
schema: &CompiledSchema,
) -> Vec<&'a FieldSelection> {
let mut out = Vec::new();
for sel in selections {
if let Some(frag_type) = sel.name.strip_prefix("...on ") {
let frag_type = frag_type.trim();
let applies = frag_type == type_name
|| schema
.find_type(type_name)
.is_some_and(|td| td.implements.iter().any(|i| i == frag_type));
if applies {
out.extend(effective_selections(&sel.nested_fields, type_name, schema));
}
} else {
out.push(sel);
}
}
out
}