use ciborium::Value as CborValue;
use vantage_core::{Result, error};
use vantage_table::column::core::Column;
use vantage_table::sorting::{OrderBy, SortDirection as TableSortDirection};
use vantage_table::traits::table_source::TableSource as _;
use vantage_vista::{
Capability, Condition, FilterOp, SortDirection, condition_column_error, operand_text,
unsupported,
};
use crate::graphql::impls::get_by_id::{id_for_column, is_integer_column};
use crate::graphql::impls::search::contains_pattern;
use crate::graphql::operation::GraphqlOperation;
use crate::graphql::types::AnyGraphqlType;
use crate::graphql::vista::source::GraphqlApiTableShell;
impl GraphqlApiTableShell {
pub(crate) fn apply_condition(&mut self, condition: &Condition) -> Result<()> {
let field = condition.column.as_str();
if field.contains('.')
&& self.table.data_source().can_filter()
&& !self.table.columns().contains_key(field)
{
return Err(condition_column_error(&self.metadata, field));
}
let op = condition.op;
if op != FilterOp::Eq && !self.capabilities.has(Capability::FilterOps) {
return unsupported(Capability::FilterOps);
}
let column = Column::<AnyGraphqlType>::new(&condition.column);
let scalar = || self.operand(field, &condition.value);
let expression = match op {
FilterOp::Eq => column.eq(scalar()?),
FilterOp::Ne => column.ne(scalar()?),
FilterOp::Gt => column.gt(scalar()?),
FilterOp::Gte => column.gte(scalar()?),
FilterOp::Lt => column.lt(scalar()?),
FilterOp::Lte => column.lte(scalar()?),
FilterOp::InSet | FilterOp::NotInSet => {
let items = match &condition.value {
CborValue::Array(items) => items
.iter()
.map(|item| self.operand(field, item))
.collect::<Result<Vec<_>>>()?,
single => vec![self.operand(field, single)?],
};
if op == FilterOp::InSet {
column.in_(items)
} else {
column.not_in(items)
}
}
FilterOp::Like => column.ilike(contains_pattern(&operand_text(&condition.value))),
_ => return unsupported(Capability::FilterOps),
};
self.table.add_condition(expression);
Ok(())
}
fn operand(&self, field: &str, value: &CborValue) -> Result<AnyGraphqlType> {
let column = self.table.columns().get(field);
match value {
CborValue::Text(text) if is_integer_column(column) => {
Ok(AnyGraphqlType::untyped(id_for_column(column, text, field)?))
}
other => Ok(AnyGraphqlType::from(other.clone())),
}
}
pub(crate) fn apply_search(&mut self, text: Option<&str>) -> Result<()> {
let condition = text.map(|text| {
self.table
.data_source()
.search_table_condition(&self.table, text)
});
self.search_handle = self
.table
.replace_temp_condition(self.search_handle, condition)?;
Ok(())
}
pub(crate) fn apply_order(&mut self, order: Option<(&str, SortDirection)>) -> Result<()> {
let Some((field, dir)) = order else {
self.table.clear_orders();
return Ok(());
};
if !self.table.columns().contains_key(field) {
return Err(error!("Unknown column for order", field = field));
}
if field.contains('.') {
return Err(
error!("GraphQL cannot order by a dotted column", field = field).mark_unsupported(),
);
}
self.table.clear_orders();
let direction = match dir {
SortDirection::Ascending => TableSortDirection::Ascending,
SortDirection::Descending => TableSortDirection::Descending,
};
self.table.add_order(OrderBy {
expression: Column::<AnyGraphqlType>::new(field).eq(field),
direction,
});
Ok(())
}
}
#[cfg(test)]
mod tests {
use vantage_table::table::Table;
use vantage_types::EmptyEntity;
use vantage_vista::{TableShell, VistaCapabilities, VistaMetadata};
use super::*;
use crate::graphql::api::GraphqlApi;
fn shell(capabilities: VistaCapabilities) -> GraphqlApiTableShell {
shell_over(GraphqlApi::new("https://api.test/graphql"), capabilities)
}
fn shell_over(api: GraphqlApi, capabilities: VistaCapabilities) -> GraphqlApiTableShell {
let table = Table::<GraphqlApi, EmptyEntity>::new("runs", api)
.with_id_column("id")
.with_column_of::<String>("run.id");
GraphqlApiTableShell::new(table, capabilities, VistaMetadata::new())
}
#[test]
fn an_undeclared_dotted_path_is_refused_only_for_push_down() {
let probe = Condition::new("id.part", FilterOp::Eq, "x");
let mut pushed = shell(VistaCapabilities::new());
let err = pushed.add_condition(&probe).unwrap_err();
assert!(err.is_unimplemented(), "{err:?}");
assert_eq!(pushed.table.conditions().count(), 0);
let declared = VistaMetadata::new().with_column(vantage_vista::Column::new("id", "String"));
let mut known = GraphqlApiTableShell::new(
Table::<GraphqlApi, EmptyEntity>::new(
"runs",
GraphqlApi::new("https://api.test/graphql"),
),
VistaCapabilities::new(),
declared,
);
let unknown = Condition::new("colour.hue", FilterOp::Eq, "x");
let err = known.add_condition(&unknown).unwrap_err();
assert!(!err.is_unimplemented() && !err.is_unsupported(), "{err:?}");
let local_api = GraphqlApi::builder("https://api.test/graphql")
.supports(crate::graphql::api::Supports::default().with_filter(false))
.build();
let mut local = shell_over(local_api, VistaCapabilities::new());
local.add_condition(&probe).unwrap();
assert_eq!(local.table.conditions().count(), 1);
}
#[test]
fn shell_refuses_ordering_a_dotted_column() {
let mut shell = shell(VistaCapabilities::new().with(Capability::Order));
let err = shell
.set_order(Some(("run.id", SortDirection::Ascending)))
.unwrap_err();
assert!(err.is_unsupported(), "{err}");
assert!(shell.table.orders().next().is_none());
}
#[test]
fn shell_refuses_operators_without_filter_ops() {
let mut shell = shell(VistaCapabilities::new());
let gt = Condition::new("id", FilterOp::Gt, "5");
assert!(shell.add_condition(>).unwrap_err().is_unsupported());
assert_eq!(shell.table.conditions().count(), 0);
}
}