use graph_storage_sdk::plugin_api::GraphStoreError;
use sea_orm::sea_query::{Expr, ExprTrait};
use sea_orm::{ColumnTrait, Condition, EntityTrait, QueryFilter};
use std::collections::BTreeSet;
use toolkit_db::secure::{DBRunner, SecureDeleteExt, SecureEntityExt};
use crate::infra::projections::{
EndpointPair, NodeIdent, TypeMeta, endpoint_pair_columns, node_ident_columns, type_meta_columns,
};
use crate::infra::storage::entity::{edge, gts_type, node};
use crate::infra::store::map_scope_err;
use crate::infra::store::types::traits_from_json;
fn plain(attribute: &str) -> bool {
!attribute.is_empty()
&& attribute.len() <= 128
&& attribute
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-' || c == '.')
}
struct ScopedTypes {
managed_nodes: Vec<i32>,
static_edges: Vec<i32>,
}
async fn scoped_types(
scope: &toolkit_security::AccessScope,
tx: &impl DBRunner,
) -> Result<ScopedTypes, GraphStoreError> {
let rows = gts_type::Entity::find()
.secure()
.scope_with(scope)
.project_all(tx, |query| {
type_meta_columns(query).into_model::<TypeMeta>()
})
.await
.map_err(map_scope_err)?;
let mut managed_nodes = Vec::new();
let mut static_edges = Vec::new();
for row in rows {
let traits = traits_from_json(&row.effective_traits);
match row.kind.as_str() {
"node" if traits.scope_managed => managed_nodes.push(row.id),
"edge" if traits.family.as_deref() == Some("static") => static_edges.push(row.id),
_ => {}
}
}
Ok(ScopedTypes {
managed_nodes,
static_edges,
})
}
pub(crate) async fn remove_stale(
scope: &toolkit_security::AccessScope,
tx: &impl DBRunner,
attribute: &str,
value: &str,
written: &BTreeSet<String>,
declared_edges: &BTreeSet<String>,
) -> Result<(u64, u64), GraphStoreError> {
if !plain(attribute) {
return Err(GraphStoreError::InvalidQuery {
what: format!(
"scope attribute `{attribute}` is not a plain payload field name \
(`[A-Za-z0-9_.-]`, at most 128 characters)"
),
});
}
let types = scoped_types(scope, tx).await?;
let member =
|| Expr::cust(format!("(payload #>> '{{{attribute}}}')")).eq(Expr::val(value.to_owned()));
let stale_ids: Vec<i64> = if types.managed_nodes.is_empty() {
Vec::new()
} else {
let candidates: Vec<NodeIdent> = node::Entity::find()
.secure()
.scope_with(scope)
.filter(
Condition::all()
.add(node::Column::GtsNodeTypeId.is_in(types.managed_nodes))
.add(node::Column::DeletedAt.is_null())
.add(member()),
)
.project_all(tx, |query| {
node_ident_columns(query).into_model::<NodeIdent>()
})
.await
.map_err(map_scope_err)?;
let stale: Vec<&NodeIdent> = candidates
.iter()
.filter(|row| !written.contains(&row.node_key))
.collect();
stale.iter().map(|row| row.id).collect()
};
let removed_edges = if types.static_edges.is_empty() {
0
} else {
let abandoned = Condition::all()
.add(edge::Column::ScopeAttribute.eq(attribute.to_owned()))
.add(edge::Column::ScopeValue.eq(value.to_owned()))
.add(edge::Column::EdgeKey.is_not_in(declared_edges.iter().cloned()));
let mut leaves = Condition::any().add(abandoned);
if !stale_ids.is_empty() {
leaves = leaves.add(
Condition::any()
.add(edge::Column::SrcNodeId.is_in(stale_ids.clone()))
.add(edge::Column::DstNodeId.is_in(stale_ids.clone())),
);
}
edge::Entity::delete_many()
.filter(
Condition::all()
.add(edge::Column::GtsEdgeTypeId.is_in(types.static_edges))
.add(leaves),
)
.secure()
.scope_with(scope)
.exec(tx)
.await
.map_err(map_scope_err)?
.rows_affected
};
if stale_ids.is_empty() {
return Ok((0, removed_edges));
}
edge::Entity::delete_many()
.filter(
Condition::all()
.add(edge::Column::DeletedAt.is_not_null())
.add(
Condition::any()
.add(edge::Column::SrcNodeId.is_in(stale_ids.clone()))
.add(edge::Column::DstNodeId.is_in(stale_ids.clone())),
),
)
.secure()
.scope_with(scope)
.exec(tx)
.await
.map_err(map_scope_err)?;
let still_referenced: BTreeSet<i64> = edge::Entity::find()
.secure()
.scope_with(scope)
.filter(
Condition::any()
.add(edge::Column::SrcNodeId.is_in(stale_ids.clone()))
.add(edge::Column::DstNodeId.is_in(stale_ids.clone())),
)
.project_all(tx, |query| {
endpoint_pair_columns(query).into_model::<EndpointPair>()
})
.await
.map_err(map_scope_err)?
.into_iter()
.flat_map(|row| [row.src_node_id, row.dst_node_id])
.collect();
let removable: Vec<i64> = stale_ids
.into_iter()
.filter(|id| !still_referenced.contains(id))
.collect();
let removed_nodes = if removable.is_empty() {
0
} else {
node::Entity::delete_many()
.filter(
Condition::all()
.add(node::Column::Id.is_in(removable))
.add(node::Column::DeletedAt.is_null())
.add(member()),
)
.secure()
.scope_with(scope)
.exec(tx)
.await
.map_err(map_scope_err)?
.rows_affected
};
Ok((removed_nodes, removed_edges))
}