feat(plugin): 循环引用检测 — no_cycle 字段支持
新增 check_no_cycle 异步函数,通过沿 parent 链上溯检测 是否存在循环引用。在 update 方法中集成,对声明 no_cycle 的字段执行检测,最多遍历 100 层防止无限循环。
This commit is contained in:
@@ -209,6 +209,13 @@ impl PluginDataService {
|
|||||||
validate_data(&data, &fields)?;
|
validate_data(&data, &fields)?;
|
||||||
validate_ref_entities(&data, &fields, entity_name, plugin_id, tenant_id, db, false, Some(id)).await?;
|
validate_ref_entities(&data, &fields, entity_name, plugin_id, tenant_id, db, false, Some(id)).await?;
|
||||||
|
|
||||||
|
// 循环引用检测
|
||||||
|
for field in &fields {
|
||||||
|
if field.no_cycle == Some(true) && data.get(&field.name).is_some() {
|
||||||
|
check_no_cycle(id, field, &data, &info.table_name, tenant_id, db).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (sql, values) = DynamicTableManager::build_update_sql(
|
let (sql, values) = DynamicTableManager::build_update_sql(
|
||||||
&info.table_name,
|
&info.table_name,
|
||||||
id,
|
id,
|
||||||
@@ -576,6 +583,62 @@ async fn validate_ref_entities(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 循环引用检测 — 用于 no_cycle 字段
|
||||||
|
async fn check_no_cycle(
|
||||||
|
record_id: Uuid,
|
||||||
|
field: &PluginField,
|
||||||
|
data: &serde_json::Value,
|
||||||
|
table_name: &str,
|
||||||
|
tenant_id: Uuid,
|
||||||
|
db: &sea_orm::DatabaseConnection,
|
||||||
|
) -> AppResult<()> {
|
||||||
|
let Some(val) = data.get(&field.name) else { return Ok(()) };
|
||||||
|
let new_parent = val.as_str().unwrap_or("").trim().to_string();
|
||||||
|
if new_parent.is_empty() { return Ok(()); }
|
||||||
|
|
||||||
|
let new_parent_id = Uuid::parse_str(&new_parent).map_err(|_| {
|
||||||
|
AppError::Validation("parent_id 不是有效的 UUID".to_string())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let field_name = sanitize_identifier(&field.name);
|
||||||
|
let mut visited = vec![record_id];
|
||||||
|
let mut current_id = new_parent_id;
|
||||||
|
|
||||||
|
for _ in 0..100 {
|
||||||
|
if visited.contains(¤t_id) {
|
||||||
|
let label = field.display_name.as_deref().unwrap_or(&field.name);
|
||||||
|
return Err(AppError::Validation(format!(
|
||||||
|
"字段 '{}' 形成循环引用", label
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
visited.push(current_id);
|
||||||
|
|
||||||
|
let query_sql = format!(
|
||||||
|
"SELECT data->>'{}' as parent FROM \"{}\" WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL",
|
||||||
|
field_name, table_name
|
||||||
|
);
|
||||||
|
#[derive(FromQueryResult)]
|
||||||
|
struct ParentRow { parent: Option<String> }
|
||||||
|
let row = ParentRow::find_by_statement(Statement::from_sql_and_values(
|
||||||
|
sea_orm::DatabaseBackend::Postgres,
|
||||||
|
query_sql,
|
||||||
|
[current_id.into(), tenant_id.into()],
|
||||||
|
)).one(db).await?;
|
||||||
|
|
||||||
|
match row {
|
||||||
|
Some(r) => {
|
||||||
|
let parent = r.parent.unwrap_or_default().trim().to_string();
|
||||||
|
if parent.is_empty() { break; }
|
||||||
|
current_id = Uuid::parse_str(&parent).map_err(|_| {
|
||||||
|
AppError::Internal("parent_id 不是有效的 UUID".to_string())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
None => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod validate_tests {
|
mod validate_tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
Reference in New Issue
Block a user