feat(plugin): P1 跨插件数据引用系统 — 后端 Phase 1-3
实现跨插件实体引用的基础后端能力:
Phase 1 — Manifest 扩展 + Entity Registry 数据层:
- PluginField 新增 ref_plugin/ref_fallback_label 支持跨插件引用声明
- PluginRelation 新增 name/relation_type/display_field(CRM 已在用的字段)
- PluginEntity 新增 is_public 标记可被其他插件引用的实体
- 数据库迁移:plugin_entities 新增 manifest_id + is_public 列 + 索引
- SeaORM Entity 和 install 流程同步更新
Phase 2 — 后端跨插件引用解析 + 校验:
- data_service: 新增 resolve_cross_plugin_entity/is_plugin_active 函数
- validate_ref_entities: 支持 ref_plugin 字段,目标插件未安装时跳过校验(软警告)
- host.rs: HostState 新增 cross_plugin_entities 映射,db_query 支持点分记号
- engine.rs: execute_wasm 自动构建跨插件实体映射
Phase 3 — API 端点:
- POST /plugins/{id}/{entity}/resolve-labels 批量标签解析
- GET /plugin-registry/entities 公开实体注册表查询
This commit is contained in:
@@ -47,6 +47,8 @@ pub struct PluginEntity {
|
||||
pub relations: Vec<PluginRelation>,
|
||||
#[serde(default)]
|
||||
pub data_scope: Option<bool>, // 是否启用行级数据权限
|
||||
#[serde(default)]
|
||||
pub is_public: Option<bool>, // 是否可被其他插件引用
|
||||
}
|
||||
|
||||
/// 字段校验规则
|
||||
@@ -87,6 +89,8 @@ pub struct PluginField {
|
||||
pub no_cycle: Option<bool>, // 禁止循环引用
|
||||
#[serde(default)]
|
||||
pub scope_role: Option<String>, // 标记为数据权限的"所有者"字段
|
||||
pub ref_plugin: Option<String>, // 跨插件引用的目标插件 manifest ID(如 "erp-crm")
|
||||
pub ref_fallback_label: Option<String>, // 目标插件未安装时的降级显示文本
|
||||
}
|
||||
|
||||
/// 字段类型
|
||||
@@ -158,6 +162,8 @@ impl PluginField {
|
||||
validation: None,
|
||||
no_cycle: None,
|
||||
scope_role: None,
|
||||
ref_plugin: None,
|
||||
ref_fallback_label: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,6 +192,12 @@ pub struct PluginRelation {
|
||||
pub entity: String,
|
||||
pub foreign_key: String,
|
||||
pub on_delete: OnDeleteStrategy,
|
||||
#[serde(default)]
|
||||
pub name: Option<String>, // 关联名称(UI 显示用)
|
||||
#[serde(default, alias = "type")]
|
||||
pub relation_type: Option<String>, // "one_to_many" | "many_to_one" | "many_to_many"
|
||||
#[serde(default)]
|
||||
pub display_field: Option<String>, // 关联记录的显示字段
|
||||
}
|
||||
|
||||
/// 事件订阅配置
|
||||
@@ -916,6 +928,98 @@ cascade_filter = "customer_id"
|
||||
assert_eq!(contact_field.cascade_filter.as_deref(), Some("customer_id"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_field_with_cross_plugin_ref() {
|
||||
let toml = r#"
|
||||
[metadata]
|
||||
id = "erp-inventory"
|
||||
name = "进销存"
|
||||
version = "0.2.0"
|
||||
dependencies = ["erp-crm"]
|
||||
|
||||
[schema]
|
||||
[[schema.entities]]
|
||||
name = "sales_order"
|
||||
display_name = "销售订单"
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "customer_id"
|
||||
field_type = "uuid"
|
||||
display_name = "客户"
|
||||
ui_widget = "entity_select"
|
||||
ref_plugin = "erp-crm"
|
||||
ref_entity = "customer"
|
||||
ref_label_field = "name"
|
||||
ref_search_fields = ["name", "code"]
|
||||
ref_fallback_label = "CRM 客户"
|
||||
"#;
|
||||
let manifest = parse_manifest(toml).unwrap();
|
||||
let field = &manifest.schema.unwrap().entities[0].fields[0];
|
||||
assert_eq!(field.ref_plugin.as_deref(), Some("erp-crm"));
|
||||
assert_eq!(field.ref_entity.as_deref(), Some("customer"));
|
||||
assert_eq!(field.ref_label_field.as_deref(), Some("name"));
|
||||
assert_eq!(field.ref_fallback_label.as_deref(), Some("CRM 客户"));
|
||||
assert_eq!(manifest.metadata.dependencies, vec!["erp-crm"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_entity_with_is_public() {
|
||||
let toml = r#"
|
||||
[metadata]
|
||||
id = "erp-crm"
|
||||
name = "CRM"
|
||||
version = "0.1.0"
|
||||
|
||||
[schema]
|
||||
[[schema.entities]]
|
||||
name = "customer"
|
||||
display_name = "客户"
|
||||
is_public = true
|
||||
|
||||
[[schema.entities]]
|
||||
name = "internal_config"
|
||||
display_name = "内部配置"
|
||||
"#;
|
||||
let manifest = parse_manifest(toml).unwrap();
|
||||
let entities = &manifest.schema.unwrap().entities;
|
||||
assert_eq!(entities[0].is_public, Some(true));
|
||||
assert_eq!(entities[1].is_public, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_relation_with_name_and_type() {
|
||||
let toml = r#"
|
||||
[metadata]
|
||||
id = "test"
|
||||
name = "Test"
|
||||
version = "0.1.0"
|
||||
|
||||
[schema]
|
||||
[[schema.entities]]
|
||||
name = "customer"
|
||||
display_name = "客户"
|
||||
|
||||
[[schema.entities.fields]]
|
||||
name = "code"
|
||||
field_type = "string"
|
||||
display_name = "编码"
|
||||
|
||||
[[schema.entities.relations]]
|
||||
entity = "contact"
|
||||
foreign_key = "customer_id"
|
||||
on_delete = "cascade"
|
||||
name = "contacts"
|
||||
type = "one_to_many"
|
||||
display_field = "name"
|
||||
"#;
|
||||
let manifest = parse_manifest(toml).unwrap();
|
||||
let relation = &manifest.schema.unwrap().entities[0].relations[0];
|
||||
assert_eq!(relation.entity, "contact");
|
||||
assert_eq!(relation.name.as_deref(), Some("contacts"));
|
||||
assert_eq!(relation.relation_type.as_deref(), Some("one_to_many"));
|
||||
assert_eq!(relation.display_field.as_deref(), Some("name"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_kanban_page() {
|
||||
let toml = r#"
|
||||
|
||||
Reference in New Issue
Block a user