2026-04-17 00:25:28 +08:00
|
|
|
use crate::app::AppConfig;
|
|
|
|
|
use crate::context::RequestContext;
|
|
|
|
|
use crate::error::WebError;
|
|
|
|
|
use crate::transport::convex::execute_convex_query_plan;
|
|
|
|
|
use bridge_runtime::{
|
|
|
|
|
execute_runtime_input, execute_runtime_query, RuntimeActorWire, RuntimeBridgeContextWire,
|
|
|
|
|
RuntimeExecutionPlan, RuntimeInput, RuntimeQueryEnvelopeWire, RuntimeQueryExecutionPlan,
|
|
|
|
|
RuntimeSourceWire,
|
|
|
|
|
};
|
2026-04-19 21:03:25 +08:00
|
|
|
use core_protocol::{GetPageMeta, QueryEnvelope};
|
2026-04-17 00:25:28 +08:00
|
|
|
use serde_json::Value;
|
2026-04-19 21:03:25 +08:00
|
|
|
use storage_convex_bridge::{build_query_request, BridgeContext};
|
2026-04-17 00:25:28 +08:00
|
|
|
|
|
|
|
|
pub fn resolve_effective_workspace_id(
|
|
|
|
|
context: &RequestContext,
|
|
|
|
|
query_workspace_id: Option<&str>,
|
|
|
|
|
require_workspace: bool,
|
|
|
|
|
) -> Result<Option<String>, WebError> {
|
|
|
|
|
let query_workspace_id = query_workspace_id
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|value| !value.is_empty());
|
|
|
|
|
let header_workspace_id = context
|
|
|
|
|
.workspace
|
|
|
|
|
.workspace_id
|
|
|
|
|
.as_deref()
|
|
|
|
|
.map(str::trim)
|
|
|
|
|
.filter(|value| !value.is_empty());
|
|
|
|
|
|
|
|
|
|
if let (Some(query_id), Some(header_id)) = (query_workspace_id, header_workspace_id) {
|
|
|
|
|
if query_id != header_id {
|
|
|
|
|
return Err(WebError::bad_request_code(
|
|
|
|
|
"workspace_context_conflict",
|
|
|
|
|
format!("workspaceId 参数与请求头中的 workspace 不一致: {query_id} != {header_id}"),
|
|
|
|
|
)
|
|
|
|
|
.with_context(context)
|
|
|
|
|
.with_header("x-error-phase", "workspace_resolve"));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let effective_workspace_id = query_workspace_id.or(header_workspace_id);
|
|
|
|
|
if require_workspace && effective_workspace_id.is_none() {
|
|
|
|
|
return Err(WebError::bad_request_code(
|
|
|
|
|
"workspace_required",
|
|
|
|
|
"缺少 workspaceId,请在 query 或请求头中提供有效工作区",
|
|
|
|
|
)
|
|
|
|
|
.with_context(context)
|
|
|
|
|
.with_header("x-error-phase", "workspace_resolve"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(effective_workspace_id.map(ToOwned::to_owned))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn runtime_context(
|
|
|
|
|
context: &RequestContext,
|
|
|
|
|
effective_workspace_id: Option<&str>,
|
|
|
|
|
) -> RuntimeBridgeContextWire {
|
|
|
|
|
RuntimeBridgeContextWire {
|
|
|
|
|
deployment_id: context.workspace.deployment_id.clone(),
|
|
|
|
|
project_id: context.workspace.project_id.clone(),
|
|
|
|
|
workspace_id: effective_workspace_id
|
|
|
|
|
.map(ToOwned::to_owned)
|
|
|
|
|
.or_else(|| context.workspace.workspace_id.clone()),
|
|
|
|
|
request_id: context.trace.request_id.clone(),
|
|
|
|
|
trace_id: context.trace.trace_id.clone(),
|
|
|
|
|
actor: RuntimeActorWire {
|
|
|
|
|
actor_type: context.auth.actor_type.clone(),
|
|
|
|
|
actor_id: context.auth.actor_id.clone(),
|
|
|
|
|
session_id: context.auth.session_id.clone(),
|
|
|
|
|
},
|
|
|
|
|
source: RuntimeSourceWire {
|
|
|
|
|
channel: context.source.channel.clone(),
|
|
|
|
|
client: context.source.client.clone(),
|
|
|
|
|
},
|
|
|
|
|
tenant_id: context.workspace.tenant_id.clone(),
|
|
|
|
|
auth_token: context.auth.authorization.clone(),
|
|
|
|
|
idempotency_key: context.source.idempotency_key.clone(),
|
|
|
|
|
validate_only: false,
|
|
|
|
|
dry_run: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 21:03:25 +08:00
|
|
|
fn storage_context(
|
|
|
|
|
context: &RequestContext,
|
|
|
|
|
effective_workspace_id: Option<&str>,
|
|
|
|
|
) -> BridgeContext {
|
|
|
|
|
BridgeContext {
|
|
|
|
|
deployment_id: context.workspace.deployment_id.clone(),
|
|
|
|
|
project_id: context.workspace.project_id.clone(),
|
|
|
|
|
request_id: context.trace.request_id.clone(),
|
|
|
|
|
trace_id: context.trace.trace_id.clone(),
|
|
|
|
|
actor_type: context.auth.actor_type.clone(),
|
|
|
|
|
actor_id: context.auth.actor_id.clone(),
|
|
|
|
|
session_id: context.auth.session_id.clone(),
|
|
|
|
|
workspace_id: effective_workspace_id
|
|
|
|
|
.map(ToOwned::to_owned)
|
|
|
|
|
.or_else(|| context.workspace.workspace_id.clone()),
|
|
|
|
|
tenant_id: context.workspace.tenant_id.clone(),
|
|
|
|
|
auth_token: context.auth.authorization.clone(),
|
|
|
|
|
source_channel: context.source.channel.clone(),
|
|
|
|
|
source_client: context.source.client.clone(),
|
|
|
|
|
idempotency_key: context.source.idempotency_key.clone(),
|
|
|
|
|
validate_only: false,
|
|
|
|
|
dry_run: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:25:28 +08:00
|
|
|
pub fn build_runtime_query_plan(
|
|
|
|
|
context: &RequestContext,
|
|
|
|
|
effective_workspace_id: Option<&str>,
|
|
|
|
|
query: RuntimeQueryEnvelopeWire,
|
|
|
|
|
) -> Result<RuntimeQueryExecutionPlan, WebError> {
|
|
|
|
|
let runtime_input = RuntimeInput::Query {
|
|
|
|
|
context: runtime_context(context, effective_workspace_id),
|
|
|
|
|
query,
|
|
|
|
|
data: None,
|
|
|
|
|
};
|
|
|
|
|
let RuntimeExecutionPlan::Query(plan) = execute_runtime_input(runtime_input)
|
|
|
|
|
.map_err(|error| WebError::bad_request(error.message).with_context(context))?
|
|
|
|
|
else {
|
|
|
|
|
return Err(WebError::internal("runtime query 未返回 query plan").with_context(context));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Ok(plan)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-19 21:03:25 +08:00
|
|
|
pub fn build_documents_meta_query_plan(
|
|
|
|
|
context: &RequestContext,
|
|
|
|
|
effective_workspace_id: Option<&str>,
|
|
|
|
|
document_id: &str,
|
|
|
|
|
) -> Result<RuntimeQueryExecutionPlan, WebError> {
|
|
|
|
|
let query = QueryEnvelope {
|
|
|
|
|
name: "documents.meta.get".into(),
|
|
|
|
|
payload: GetPageMeta {
|
|
|
|
|
page_id: document_id.to_string(),
|
|
|
|
|
workspace_id: effective_workspace_id.map(ToOwned::to_owned),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
let request = build_query_request(&storage_context(context, effective_workspace_id), &query)
|
|
|
|
|
.map_err(|error| WebError::bad_request(error.message).with_context(context))?;
|
|
|
|
|
Ok(RuntimeQueryExecutionPlan {
|
|
|
|
|
query_name: query.name,
|
|
|
|
|
function_name: request.function_name,
|
|
|
|
|
workspace_id: request.workspace_id,
|
|
|
|
|
request_id: request.request_id,
|
|
|
|
|
trace_id: request.trace_id,
|
|
|
|
|
actor_id: request.actor_id,
|
|
|
|
|
payload_json: request.payload_json,
|
|
|
|
|
args_json: serde_json::json!({
|
|
|
|
|
"id": document_id,
|
|
|
|
|
}),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:36:24 +08:00
|
|
|
pub async fn fetch_query_data_via_convex(
|
2026-04-17 00:25:28 +08:00
|
|
|
config: &AppConfig,
|
|
|
|
|
context: &RequestContext,
|
|
|
|
|
effective_workspace_id: Option<&str>,
|
|
|
|
|
query: RuntimeQueryEnvelopeWire,
|
|
|
|
|
) -> Result<Value, WebError> {
|
|
|
|
|
let plan = build_runtime_query_plan(context, effective_workspace_id, query)?;
|
2026-04-17 23:36:24 +08:00
|
|
|
execute_convex_query_plan(config, context, &plan).await
|
2026-04-17 00:25:28 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-19 21:03:25 +08:00
|
|
|
pub async fn fetch_documents_meta_via_convex(
|
|
|
|
|
config: &AppConfig,
|
|
|
|
|
context: &RequestContext,
|
|
|
|
|
effective_workspace_id: Option<&str>,
|
|
|
|
|
document_id: &str,
|
|
|
|
|
) -> Result<Value, WebError> {
|
|
|
|
|
let plan = build_documents_meta_query_plan(context, effective_workspace_id, document_id)?;
|
|
|
|
|
execute_convex_query_plan(config, context, &plan).await
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 00:25:28 +08:00
|
|
|
pub fn execute_runtime_query_against_data(
|
|
|
|
|
context: &RequestContext,
|
|
|
|
|
effective_workspace_id: Option<&str>,
|
|
|
|
|
query: RuntimeQueryEnvelopeWire,
|
|
|
|
|
data: Value,
|
|
|
|
|
) -> Result<Value, WebError> {
|
|
|
|
|
execute_runtime_query(RuntimeInput::Query {
|
|
|
|
|
context: runtime_context(context, effective_workspace_id),
|
|
|
|
|
query,
|
|
|
|
|
data: Some(data),
|
|
|
|
|
})
|
|
|
|
|
.map_err(|error| WebError::bad_request(error.message).with_context(context))
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-17 23:36:24 +08:00
|
|
|
pub async fn execute_runtime_query_via_convex(
|
2026-04-17 00:25:28 +08:00
|
|
|
config: &AppConfig,
|
|
|
|
|
context: &RequestContext,
|
|
|
|
|
effective_workspace_id: Option<&str>,
|
|
|
|
|
query: RuntimeQueryEnvelopeWire,
|
|
|
|
|
) -> Result<Value, WebError> {
|
2026-04-18 09:38:16 +08:00
|
|
|
let data =
|
|
|
|
|
fetch_query_data_via_convex(config, context, effective_workspace_id, query.clone()).await?;
|
2026-04-17 00:25:28 +08:00
|
|
|
execute_runtime_query_against_data(context, effective_workspace_id, query, data)
|
|
|
|
|
}
|