0.6 rust重构01

This commit is contained in:
lix-2026
2026-04-14 13:22:29 +08:00
parent 71fb1aee7e
commit 84a8454fa9
401 changed files with 5841 additions and 1512 deletions
@@ -0,0 +1,18 @@
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BridgeContext {
pub deployment_id: Option<String>,
pub project_id: Option<String>,
pub workspace_id: Option<String>,
pub request_id: String,
pub trace_id: String,
pub actor_type: String,
pub actor_id: String,
pub session_id: Option<String>,
pub tenant_id: Option<String>,
pub auth_token: Option<String>,
pub source_channel: String,
pub source_client: String,
pub idempotency_key: Option<String>,
pub validate_only: bool,
pub dry_run: bool,
}
@@ -0,0 +1,266 @@
pub mod context;
pub mod mapping;
pub mod read_path;
pub mod types;
pub mod validation;
pub mod write_path;
pub use context::BridgeContext;
pub use mapping::{
map_command_name_to_convex, map_query_name_to_convex, ConvexMutationRequest, ConvexQueryRequest,
};
pub use read_path::{build_query_request, QueryReadResult};
pub use types::{BridgeError, BridgeErrorKind, BridgeResult};
pub use validation::{validate_command_envelope, validate_query_envelope};
pub use write_path::{
build_command_log_record, build_domain_event_record, build_write_pipeline, build_write_request,
WritePipelineResult,
};
#[cfg(test)]
mod tests {
use super::*;
use core_protocol::{
command::{CreatePage, CreateWorkspace, UpdatePageOptions, UpdatePageStats, UpdatePageTitle},
query::GetPage,
ActorPayload, CommandEnvelope, QueryEnvelope, SourcePayload, TargetRef,
};
fn demo_context() -> BridgeContext {
BridgeContext {
deployment_id: Some("dep_1".into()),
project_id: Some("proj_1".into()),
request_id: "req_1".into(),
trace_id: "trace_1".into(),
actor_type: "human".into(),
actor_id: "user_1".into(),
session_id: Some("session_1".into()),
workspace_id: Some("ws_1".into()),
tenant_id: Some("tenant_1".into()),
auth_token: Some("token_1".into()),
source_channel: "cli".into(),
source_client: "mnote-cli".into(),
idempotency_key: Some("idem_ctx".into()),
validate_only: false,
dry_run: false,
}
}
#[test]
fn build_write_request_keeps_workspace_scope() {
let command = CommandEnvelope {
name: "create_page".into(),
command_id: "cmd_1".into(),
idempotency_key: Some("idem_cmd".into()),
actor: ActorPayload {
actor_type: "human".into(),
actor_id: "user_1".into(),
session_id: Some("session_1".into()),
},
source: SourcePayload {
channel: "cli".into(),
client: "mnote-cli".into(),
},
target: Some(TargetRef {
workspace_id: Some("ws_1".into()),
page_id: None,
block_id: None,
}),
payload: CreatePage {
title: "第一页".into(),
parent_page_id: None,
position: None,
},
reason: Some("初始化页面".into()),
refs: vec!["spec:phase1-f".into()],
dry_run: false,
validate_only: false,
};
let request = build_write_request(&demo_context(), &command).expect("write request should build");
assert_eq!(request.function_name, "pages:create");
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
assert_eq!(request.deployment_id.as_deref(), Some("dep_1"));
assert_eq!(request.project_id.as_deref(), Some("proj_1"));
assert_eq!(request.idempotency_key.as_deref(), Some("idem_cmd"));
assert!(request.payload_json.contains("\"kind\":\"command\""));
assert!(request.payload_json.contains("\"name\":\"create_page\""));
}
#[test]
fn build_query_request_includes_trace_and_workspace() {
let query = QueryEnvelope {
name: "get_page".into(),
payload: GetPage {
page_id: "page_1".into(),
},
};
let request = build_query_request(&demo_context(), &query).expect("query request should build");
assert_eq!(request.function_name, "pages:get");
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
assert_eq!(request.request_id, "req_1");
assert_eq!(request.trace_id, "trace_1");
assert!(request.payload_json.contains("\"kind\":\"query\""));
assert!(request.payload_json.contains("\"name\":\"get_page\""));
}
#[test]
fn build_write_pipeline_sets_trace_fields() {
let command = CommandEnvelope {
name: "create_workspace".into(),
command_id: "cmd_2".into(),
idempotency_key: None,
actor: ActorPayload {
actor_type: "human".into(),
actor_id: "user_1".into(),
session_id: Some("session_1".into()),
},
source: SourcePayload {
channel: "cli".into(),
client: "mnote-cli".into(),
},
target: None,
payload: CreateWorkspace {
name: "demo".into(),
slug: Some("demo".into()),
},
reason: Some("初始化工作区".into()),
refs: vec![],
dry_run: false,
validate_only: false,
};
let pipeline = build_write_pipeline(&demo_context(), &command).expect("pipeline should build");
assert_eq!(pipeline.command_log.command_name, "create_workspace");
assert!(pipeline.command_log.payload_summary.contains("request_id=req_1"));
assert_eq!(pipeline.command_log.trace_id, "trace_1");
assert_eq!(pipeline.command_log.request_id, "req_1");
assert!(pipeline.domain_event.payload_json.contains("\"trace_id\":\"trace_1\""));
assert_eq!(pipeline.result.event_ids, vec!["evt_cmd_2".to_string()]);
}
#[test]
fn document_title_command_maps_to_documents_update_title() {
let command = CommandEnvelope {
name: "documents.title.update".into(),
command_id: "cmd_title_1".into(),
idempotency_key: Some("idem_title".into()),
actor: ActorPayload {
actor_type: "human".into(),
actor_id: "user_1".into(),
session_id: Some("session_1".into()),
},
source: SourcePayload {
channel: "next-route".into(),
client: "wolai-frontend".into(),
},
target: Some(TargetRef {
workspace_id: Some("ws_1".into()),
page_id: Some("page_1".into()),
block_id: None,
}),
payload: UpdatePageTitle {
page_id: "page_1".into(),
title: "新标题".into(),
},
reason: None,
refs: vec![],
dry_run: false,
validate_only: false,
};
let request = build_write_request(&demo_context(), &command).expect("write request should build");
assert_eq!(request.function_name, "documents:updateTitle");
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
assert!(request.payload_json.contains("\"name\":\"documents.title.update\""));
}
#[test]
fn document_stats_command_maps_to_documents_update_stats() {
let command = CommandEnvelope {
name: "documents.stats.update".into(),
command_id: "cmd_stats_1".into(),
idempotency_key: Some("idem_stats".into()),
actor: ActorPayload {
actor_type: "human".into(),
actor_id: "user_1".into(),
session_id: Some("session_1".into()),
},
source: SourcePayload {
channel: "next-route".into(),
client: "wolai-frontend".into(),
},
target: Some(TargetRef {
workspace_id: Some("ws_1".into()),
page_id: Some("page_1".into()),
block_id: None,
}),
payload: UpdatePageStats {
page_id: "page_1".into(),
word_count: 10,
character_count: 20,
block_count: 3,
todo_total: 4,
todo_done: 1,
},
reason: None,
refs: vec![],
dry_run: false,
validate_only: false,
};
let request = build_write_request(&demo_context(), &command).expect("write request should build");
assert_eq!(request.function_name, "documents:updateStats");
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
assert!(request.payload_json.contains("\"name\":\"documents.stats.update\""));
}
#[test]
fn document_options_command_maps_to_documents_update_options() {
let command = CommandEnvelope {
name: "documents.options.update".into(),
command_id: "cmd_options_1".into(),
idempotency_key: Some("idem_options".into()),
actor: ActorPayload {
actor_type: "human".into(),
actor_id: "user_1".into(),
session_id: Some("session_1".into()),
},
source: SourcePayload {
channel: "next-route".into(),
client: "wolai-frontend".into(),
},
target: Some(TargetRef {
workspace_id: Some("ws_1".into()),
page_id: Some("page_1".into()),
block_id: None,
}),
payload: UpdatePageOptions {
page_id: "page_1".into(),
wide_layout: None,
small_text: None,
show_heading_numbers: None,
show_toc: Some(true),
show_structure: None,
protect_editing: None,
show_word_count: None,
collapse_backlinks: None,
page_font: None,
layout_density: Some("compact".into()),
hide_child_pages: None,
show_block_ref_count: None,
embed_default_block_id: None,
},
reason: None,
refs: vec![],
dry_run: false,
validate_only: false,
};
let request = build_write_request(&demo_context(), &command).expect("write request should build");
assert_eq!(request.function_name, "documents:updateOptions");
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
assert!(request.payload_json.contains("\"name\":\"documents.options.update\""));
}
}
@@ -0,0 +1,93 @@
use crate::context::BridgeContext;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConvexMutationRequest {
pub function_name: String,
pub deployment_id: Option<String>,
pub project_id: Option<String>,
pub workspace_id: Option<String>,
pub request_id: String,
pub trace_id: String,
pub idempotency_key: Option<String>,
pub actor_id: String,
pub payload_json: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConvexQueryRequest {
pub function_name: String,
pub deployment_id: Option<String>,
pub project_id: Option<String>,
pub workspace_id: Option<String>,
pub request_id: String,
pub trace_id: String,
pub actor_id: String,
pub payload_json: String,
}
pub fn map_command_name_to_convex(command_name: &str) -> &'static str {
match command_name {
"create_workspace" => "workspaces:create",
"create_page" => "pages:create",
"documents.title.update" => "documents:updateTitle",
"documents.stats.update" => "documents:updateStats",
"documents.options.update" => "documents:updateOptions",
"insert_block" => "blocks:insert",
"update_block" => "blocks:update",
"move_block" => "blocks:move",
"delete_block" => "blocks:delete",
_ => "commands:unknown",
}
}
pub fn map_query_name_to_convex(query_name: &str) -> &'static str {
match query_name {
"get_page" => "pages:get",
"list_page_blocks" => "blocks:list_by_page",
"search_pages" => "search:pages",
"search_blocks" => "search:blocks",
_ => "queries:unknown",
}
}
fn json_opt(value: &Option<String>) -> String {
match value {
Some(v) => format!("\"{}\"", v),
None => "null".into(),
}
}
pub fn payload_json_for_command(context: &BridgeContext, command_name: &str) -> String {
format!(
"{{\"kind\":\"command\",\"name\":\"{}\",\"request_id\":\"{}\",\"trace_id\":\"{}\",\"deployment_id\":{},\"project_id\":{},\"workspace_id\":{},\"tenant_id\":{},\"idempotency_key\":{},\"actor\":{{\"type\":\"{}\",\"id\":\"{}\",\"session_id\":{}}},\"source\":{{\"channel\":\"{}\",\"client\":\"{}\"}}}}",
command_name,
context.request_id,
context.trace_id,
json_opt(&context.deployment_id),
json_opt(&context.project_id),
json_opt(&context.workspace_id),
json_opt(&context.tenant_id),
json_opt(&context.idempotency_key),
context.actor_type,
context.actor_id,
json_opt(&context.session_id),
context.source_channel,
context.source_client,
)
}
pub fn payload_json_for_query(context: &BridgeContext, query_name: &str) -> String {
format!(
"{{\"kind\":\"query\",\"name\":\"{}\",\"request_id\":\"{}\",\"trace_id\":\"{}\",\"deployment_id\":{},\"project_id\":{},\"workspace_id\":{},\"tenant_id\":{},\"actor_id\":\"{}\",\"source\":{{\"channel\":\"{}\",\"client\":\"{}\"}}}}",
query_name,
context.request_id,
context.trace_id,
json_opt(&context.deployment_id),
json_opt(&context.project_id),
json_opt(&context.workspace_id),
json_opt(&context.tenant_id),
context.actor_id,
context.source_channel,
context.source_client,
)
}
@@ -0,0 +1,29 @@
use crate::context::BridgeContext;
use crate::mapping::{map_query_name_to_convex, payload_json_for_query, ConvexQueryRequest};
use crate::types::BridgeResult;
use crate::validation::validate_query_envelope;
use core_protocol::QueryEnvelope;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct QueryReadResult {
pub workspace_id: Option<String>,
pub request_id: String,
pub trace_id: String,
}
pub fn build_query_request<T>(
context: &BridgeContext,
query: &QueryEnvelope<T>,
) -> BridgeResult<ConvexQueryRequest> {
validate_query_envelope(context, query)?;
Ok(ConvexQueryRequest {
function_name: map_query_name_to_convex(&query.name).to_string(),
deployment_id: context.deployment_id.clone(),
project_id: context.project_id.clone(),
workspace_id: context.workspace_id.clone(),
request_id: context.request_id.clone(),
trace_id: context.trace_id.clone(),
actor_id: context.actor_id.clone(),
payload_json: payload_json_for_query(context, &query.name),
})
}
@@ -0,0 +1,26 @@
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum BridgeErrorKind {
Validation,
Unauthorized,
Conflict,
NotFound,
Transport,
Rejected,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BridgeError {
pub kind: BridgeErrorKind,
pub message: String,
}
impl BridgeError {
pub fn validation(message: impl Into<String>) -> Self {
Self {
kind: BridgeErrorKind::Validation,
message: message.into(),
}
}
}
pub type BridgeResult<T> = Result<T, BridgeError>;
@@ -0,0 +1,50 @@
use crate::context::BridgeContext;
use crate::types::{BridgeError, BridgeResult};
use core_protocol::{CommandEnvelope, QueryEnvelope};
pub fn validate_command_envelope<T>(
context: &BridgeContext,
command: &CommandEnvelope<T>,
) -> BridgeResult<()> {
if command.name.trim().is_empty() {
return Err(BridgeError::validation("command name 不能为空"));
}
if command.command_id.trim().is_empty() {
return Err(BridgeError::validation("command_id 不能为空"));
}
if context.request_id.trim().is_empty() {
return Err(BridgeError::validation("request_id 不能为空"));
}
if context.trace_id.trim().is_empty() {
return Err(BridgeError::validation("trace_id 不能为空"));
}
if context.actor_id.trim().is_empty() {
return Err(BridgeError::validation("actor_id 不能为空"));
}
if context
.workspace_id
.as_deref()
.unwrap_or("")
.trim()
.is_empty()
{
return Err(BridgeError::validation("workspace_id 不能为空"));
}
Ok(())
}
pub fn validate_query_envelope<T>(
context: &BridgeContext,
query: &QueryEnvelope<T>,
) -> BridgeResult<()> {
if query.name.trim().is_empty() {
return Err(BridgeError::validation("query name 不能为空"));
}
if context.request_id.trim().is_empty() {
return Err(BridgeError::validation("request_id 不能为空"));
}
if context.trace_id.trim().is_empty() {
return Err(BridgeError::validation("trace_id 不能为空"));
}
Ok(())
}
@@ -0,0 +1,131 @@
use crate::context::BridgeContext;
use crate::mapping::{map_command_name_to_convex, payload_json_for_command, ConvexMutationRequest};
use crate::types::BridgeResult;
use crate::validation::validate_command_envelope;
use core_domain::{RefLink, Timestamp, WorkspaceId};
use core_protocol::{command::CommandResult, AffectedObject, CommandEnvelope};
use event_log::{CommandLogRecord, CommandLogStatus, DomainEventRecord, EventStatus};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WritePipelineResult {
pub mutation: ConvexMutationRequest,
pub command_log: CommandLogRecord,
pub domain_event: DomainEventRecord,
pub result: CommandResult,
}
pub fn build_write_request<T>(
context: &BridgeContext,
command: &CommandEnvelope<T>,
) -> BridgeResult<ConvexMutationRequest> {
validate_command_envelope(context, command)?;
let workspace_id = command
.target
.as_ref()
.and_then(|target| target.workspace_id.clone())
.or_else(|| context.workspace_id.clone());
Ok(ConvexMutationRequest {
function_name: map_command_name_to_convex(&command.name).to_string(),
deployment_id: context.deployment_id.clone(),
project_id: context.project_id.clone(),
workspace_id,
request_id: context.request_id.clone(),
trace_id: context.trace_id.clone(),
idempotency_key: command
.idempotency_key
.clone()
.or_else(|| context.idempotency_key.clone()),
actor_id: context.actor_id.clone(),
payload_json: payload_json_for_command(context, &command.name),
})
}
pub fn build_command_log_record(
command_id: &str,
command_name: &str,
context: &BridgeContext,
workspace_id: &str,
) -> CommandLogRecord {
CommandLogRecord {
command_log_id: format!("clog_{command_id}"),
command_name: command_name.into(),
actor_type: context.actor_type.clone(),
actor_id: context.actor_id.clone(),
source: context.source_channel.clone(),
workspace_id: WorkspaceId::new(workspace_id),
target_objects: vec![RefLink::External(format!("workspace:{workspace_id}"))],
payload_summary: format!(
"command={command_name};request_id={};trace_id={}",
context.request_id, context.trace_id
),
refs_json: format!(
"[\"request:{}\",\"trace:{}\"]",
context.request_id, context.trace_id
),
idempotency_key: context.idempotency_key.clone(),
status: CommandLogStatus::Pending,
created_at: Timestamp::new("2026-04-11T00:00:00Z"),
finished_at: None,
trace_id: context.trace_id.clone(),
request_id: context.request_id.clone(),
}
}
pub fn build_domain_event_record(
command_id: &str,
command_name: &str,
context: &BridgeContext,
workspace_id: &str,
) -> DomainEventRecord {
DomainEventRecord {
event_id: format!("evt_{command_id}"),
workspace_id: workspace_id.into(),
aggregate_type: "workspace".into(),
aggregate_id: workspace_id.into(),
event_type: format!("{command_name}_requested"),
event_version: 1,
payload_json: format!(
"{{\"request_id\":\"{}\",\"trace_id\":\"{}\",\"command_id\":\"{}\",\"command_name\":\"{}\"}}",
context.request_id, context.trace_id, command_id, command_name
),
command_log_id: format!("clog_{command_id}"),
actor_type: context.actor_type.clone(),
created_at: Timestamp::new("2026-04-11T00:00:00Z"),
status: EventStatus::Pending,
trace_id: context.trace_id.clone(),
request_id: context.request_id.clone(),
command_id: command_id.into(),
}
}
pub fn build_write_pipeline<T>(
context: &BridgeContext,
command: &CommandEnvelope<T>,
) -> BridgeResult<WritePipelineResult> {
let mutation = build_write_request(context, command)?;
let workspace_id = mutation
.workspace_id
.clone()
.unwrap_or_else(|| "workspace_unknown".into());
let command_log =
build_command_log_record(&command.command_id, &command.name, context, &workspace_id);
let domain_event =
build_domain_event_record(&command.command_id, &command.name, context, &workspace_id);
let result = CommandResult {
ok: true,
command_id: command.command_id.clone(),
event_ids: vec![domain_event.event_id.clone()],
affected_objects: vec![AffectedObject {
object_type: "workspace".into(),
object_id: workspace_id.clone(),
}],
revision: Some(1),
warnings: vec![],
};
Ok(WritePipelineResult {
mutation,
command_log,
domain_event,
result,
})
}