Files
mnote/rust/crates/mnote-web/src/routes/stream_support.rs
T

171 lines
4.9 KiB
Rust
Raw Normal View History

use crate::app::AppConfig;
use crate::context::RequestContext;
use crate::error::WebError;
use crate::routes::query_support::{
execute_runtime_query_via_convex, resolve_effective_workspace_id,
};
use crate::routes::snapshot_support::{
execute_kernel_query, load_projection_snapshot, load_sidebar_dataset, subtree_query,
ProjectionSnapshotSpec,
};
use bridge_runtime::RuntimeQueryEnvelopeWire;
use core_protocol::KernelProjectionKind;
use serde::Deserialize;
use serde_json::{json, Value};
#[derive(Debug, Clone, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct StreamSnapshotQuery {
pub workspace_id: Option<String>,
pub root_node_id: Option<String>,
pub depth: Option<u32>,
pub cursor: Option<String>,
pub limit: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StreamSnapshotScope {
Workspace,
Subtree,
}
impl StreamSnapshotScope {
pub fn as_str(self) -> &'static str {
match self {
Self::Workspace => "workspace",
Self::Subtree => "subtree",
}
}
}
pub fn resolve_stream_scope(query: &StreamSnapshotQuery) -> StreamSnapshotScope {
if query
.root_node_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.is_some()
{
StreamSnapshotScope::Subtree
} else {
StreamSnapshotScope::Workspace
}
}
fn workspace_overview_query(
workspace_id: &str,
query: &StreamSnapshotQuery,
) -> RuntimeQueryEnvelopeWire {
RuntimeQueryEnvelopeWire {
name: "bridge.workspace.overview".into(),
payload: json!({
"workspaceId": workspace_id,
"cursor": query.cursor,
"limit": query.limit.unwrap_or(20),
"commandStatus": Value::Null,
"eventStatus": Value::Null,
"targetPageId": Value::Null,
"targetBlockId": Value::Null,
"aggregateType": Value::Null,
"aggregateId": query.root_node_id,
}),
}
}
pub async fn load_stream_snapshot(
config: &AppConfig,
context: &RequestContext,
query: &StreamSnapshotQuery,
) -> Result<Value, WebError> {
let effective_workspace_id =
resolve_effective_workspace_id(context, query.workspace_id.as_deref(), true)?
.expect("workspace_required 已确保存在");
let scope = resolve_stream_scope(query);
let snapshot = match scope {
StreamSnapshotScope::Workspace => {
let loaded = load_projection_snapshot(
config,
context,
&ProjectionSnapshotSpec {
workspace_id: &effective_workspace_id,
root_node_id: None,
depth: query.depth,
projection: KernelProjectionKind::SidebarTree,
},
)
.await?;
json!({
"dataset": loaded.dataset,
"tree": loaded.projection,
})
}
StreamSnapshotScope::Subtree => {
let root_node_id = query
.root_node_id
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.expect("subtree scope 已确保 rootNodeId 存在");
let dataset = load_sidebar_dataset(config, context, &effective_workspace_id).await?;
let tree = execute_kernel_query(
context,
&effective_workspace_id,
subtree_query(&effective_workspace_id, root_node_id, query.depth),
dataset.clone(),
)?;
json!({
"dataset": dataset,
"tree": tree,
})
}
};
let overview = execute_runtime_query_via_convex(
config,
context,
Some(&effective_workspace_id),
workspace_overview_query(&effective_workspace_id, query),
)
.await
.ok();
Ok(json!({
"kind": "snapshot",
"scope": scope.as_str(),
"requestId": context.trace.request_id,
"traceId": context.trace.trace_id,
"workspaceId": effective_workspace_id,
"rootNodeId": query.root_node_id,
"depth": query.depth,
"snapshot": snapshot,
"overview": overview,
}))
}
#[cfg(test)]
mod tests {
use super::{resolve_stream_scope, StreamSnapshotQuery, StreamSnapshotScope};
#[test]
fn stream_scope_defaults_to_workspace() {
assert_eq!(
resolve_stream_scope(&StreamSnapshotQuery::default()),
StreamSnapshotScope::Workspace
);
}
#[test]
fn stream_scope_switches_to_subtree_when_root_exists() {
assert_eq!(
resolve_stream_scope(&StreamSnapshotQuery {
root_node_id: Some("page_root".into()),
..StreamSnapshotQuery::default()
}),
StreamSnapshotScope::Subtree
);
}
}