use crate::{
error::{FraiseQLError, Result},
graphql::FieldSelection,
schema::{CompiledSchema, QueryDefinition},
};
pub(super) struct AbstractRead<'s> {
query: &'s QueryDefinition,
possible: Vec<&'s str>,
view_type: Option<&'s str>,
}
impl<'s> AbstractRead<'s> {
pub(super) fn of(schema: &'s CompiledSchema, query: &'s QueryDefinition) -> Option<Self> {
let name = query.return_type.as_str();
let possible: Vec<&str> = if schema.find_interface(name).is_some() {
schema
.types
.iter()
.filter(|t| t.implements.iter().any(|i| i == name))
.map(|t| t.name.as_str())
.collect()
} else {
schema.find_union(name)?.member_types.iter().map(String::as_str).collect()
};
let mut reading_the_view = possible.iter().copied().filter(|t| {
schema.find_type(t).is_some_and(|td| {
query.sql_source.as_deref().is_some_and(|view| td.sql_source.as_str() == view)
})
});
let view_type = match (reading_the_view.next(), reading_the_view.next()) {
(Some(only), None) => Some(only),
_ => None,
};
Some(Self {
query,
possible,
view_type,
})
}
pub(super) fn refuse_gated_fields(
&self,
schema: &CompiledSchema,
root_fields: &[FieldSelection],
) -> Result<()> {
let Some(gated) = self.possible.iter().find(|t| {
crate::security::field_authorizer::selection_set_selects_gated_field(
schema,
t,
root_fields,
)
}) else {
return Ok(());
};
Err(FraiseQLError::Authorization {
message: format!(
"'{}' returns the abstract type '{}', and the selection reaches a policy-gated \
field of '{gated}', which field-level authorization does not support through \
an abstract type; query '{gated}' directly",
self.query.name, self.query.return_type
),
action: Some("read".to_string()),
resource: Some((*gated).to_string()),
})
}
pub(super) fn classify(
&self,
schema: &CompiledSchema,
root_fields: &[FieldSelection],
security_context: Option<&crate::security::SecurityContext>,
authz: Option<super::query_nested::LevelAuthz<'_>>,
) -> Result<super::query_nested::SelectionAccess> {
let roots: Vec<(&str, &[FieldSelection])> =
self.possible.iter().map(|t| (*t, root_fields)).collect();
super::query_nested::SelectionAccess::classify_roots(
schema,
&roots,
security_context,
authz,
)
}
fn type_of(&self, row: &serde_json::Value) -> Result<&'s str> {
match row.get("__typename").and_then(serde_json::Value::as_str) {
Some(named) => self.possible.iter().copied().find(|t| *t == named).ok_or_else(|| {
self.refusal(&format!(
"a row's `__typename` is '{named}', which is not one of its types ({})",
self.possible.join(", ")
))
}),
None => self.view_type.ok_or_else(|| {
self.refusal(&format!(
"its view holds rows of several of its types ({}) and a row names none; \
store `__typename` in each row's data, or read a view of one type",
self.possible.join(", ")
))
}),
}
}
fn refusal(&self, why: &str) -> FraiseQLError {
FraiseQLError::Validation {
message: format!(
"Query '{}' returns the abstract type '{}' and {why}",
self.query.name, self.query.return_type
),
path: None,
}
}
pub(super) fn project(
&self,
documents: &[crate::backend::JsonbValue],
root_fields: &[FieldSelection],
schema: &CompiledSchema,
access: &super::query_nested::SelectionAccess,
) -> Result<serde_json::Value> {
let project = |document: &crate::backend::JsonbValue| -> Result<serde_json::Value> {
let type_name = self.type_of(&document.data)?;
let mut row =
crate::runtime::project_entity(&document.data, type_name, root_fields, schema);
access.null_masked(&mut row, type_name, root_fields, schema);
Ok(row)
};
if self.query.returns_list {
documents
.iter()
.map(project)
.collect::<Result<Vec<_>>>()
.map(serde_json::Value::Array)
} else {
documents.first().map_or(Ok(serde_json::Value::Null), project)
}
}
}