use crate::bridge::envelope::ErrorCode;
use crate::control::security::catalog::TransitionCheckDef;
pub fn check_transition_predicates(
collection: &str,
checks: &[TransitionCheckDef],
old_doc: &serde_json::Value,
new_doc: &serde_json::Value,
) -> Result<(), ErrorCode> {
let old_val = nodedb_types::Value::from(old_doc.clone());
let new_val = nodedb_types::Value::from(new_doc.clone());
for check in checks {
let result = check.predicate.eval_with_old(&new_val, &old_val);
let passed = match result {
nodedb_types::Value::Bool(b) => b,
nodedb_types::Value::Null => false, _ => true, };
if !passed {
return Err(ErrorCode::TransitionCheckViolation {
collection: collection.to_string(),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::bridge::expr_eval::{BinaryOp, SqlExpr};
fn make_check(name: &str, predicate: SqlExpr) -> TransitionCheckDef {
TransitionCheckDef {
name: name.to_string(),
predicate,
}
}
#[test]
fn simple_true_predicate() {
let check = make_check(
"always_pass",
SqlExpr::Literal(nodedb_types::Value::Bool(true)),
);
let old = serde_json::json!({"x": 1});
let new = serde_json::json!({"x": 2});
assert!(check_transition_predicates("coll", &[check], &old, &new).is_ok());
}
#[test]
fn simple_false_predicate() {
let check = make_check(
"always_fail",
SqlExpr::Literal(nodedb_types::Value::Bool(false)),
);
let old = serde_json::json!({"x": 1});
let new = serde_json::json!({"x": 2});
assert!(check_transition_predicates("coll", &[check], &old, &new).is_err());
}
#[test]
fn old_equals_new_column_check() {
let check = make_check(
"not_sealed",
SqlExpr::BinaryOp {
left: Box::new(SqlExpr::OldColumn("sealed".into())),
op: BinaryOp::Eq,
right: Box::new(SqlExpr::Literal(nodedb_types::Value::Bool(false))),
},
);
let old = serde_json::json!({"sealed": false, "amount": 100});
let new = serde_json::json!({"sealed": false, "amount": 200});
assert!(
check_transition_predicates("coll", std::slice::from_ref(&check), &old, &new).is_ok()
);
let old_sealed = serde_json::json!({"sealed": true, "amount": 100});
assert!(
check_transition_predicates("coll", std::slice::from_ref(&check), &old_sealed, &new)
.is_err()
);
}
#[test]
fn amount_cannot_decrease() {
let check = make_check(
"no_decrease",
SqlExpr::BinaryOp {
left: Box::new(SqlExpr::Column("amount".into())),
op: BinaryOp::GtEq,
right: Box::new(SqlExpr::OldColumn("amount".into())),
},
);
let old = serde_json::json!({"amount": 100});
let new = serde_json::json!({"amount": 200});
assert!(
check_transition_predicates("coll", std::slice::from_ref(&check), &old, &new).is_ok()
);
let new_less = serde_json::json!({"amount": 50});
assert!(check_transition_predicates("coll", &[check], &old, &new_less).is_err());
}
#[test]
fn multiple_checks_all_must_pass() {
let c1 = make_check("c1", SqlExpr::Literal(nodedb_types::Value::Bool(true)));
let c2 = make_check("c2", SqlExpr::Literal(nodedb_types::Value::Bool(false)));
let old = serde_json::json!({});
let new = serde_json::json!({});
assert!(check_transition_predicates("coll", &[c1, c2], &old, &new).is_err());
}
#[test]
fn empty_checks_passes() {
let old = serde_json::json!({"x": 1});
let new = serde_json::json!({"x": 2});
assert!(check_transition_predicates("coll", &[], &old, &new).is_ok());
}
#[test]
fn null_result_treated_as_false() {
let check = make_check("null_check", SqlExpr::OldColumn("nonexistent".into()));
let old = serde_json::json!({"x": 1});
let new = serde_json::json!({"x": 2});
assert!(check_transition_predicates("coll", &[check], &old, &new).is_err());
}
}