feat: cut over rust web main shell
This commit is contained in:
@@ -6,15 +6,15 @@ use crate::routes::query_support::{
|
||||
execute_runtime_query_via_convex, fetch_documents_meta_via_convex,
|
||||
resolve_effective_workspace_id,
|
||||
};
|
||||
use axum::Json;
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::StatusCode;
|
||||
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||
use axum::Json;
|
||||
use bridge_runtime::RuntimeQueryEnvelopeWire;
|
||||
use bridge_runtime::{
|
||||
RuntimeActorWire, RuntimeCommandEnvelopeWire, RuntimeSourceWire, RuntimeTargetWire,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{Value, json};
|
||||
use serde_json::{json, Value};
|
||||
use std::fs;
|
||||
use std::time::Duration;
|
||||
|
||||
@@ -47,19 +47,34 @@ pub struct DocumentSaveRequest {
|
||||
}
|
||||
|
||||
const NEXT_DOCUMENTS_BASE_URL_ENV: &str = "MNOTE_NEXT_BASE_URL";
|
||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||
const HEADER_DOCUMENTS_TRANSPORT: &str = "x-mnote-documents-transport";
|
||||
|
||||
fn ok_response(context: &RequestContext, result: Value) -> (StatusCode, Json<Value>) {
|
||||
fn ok_response(context: &RequestContext, result: Value) -> (StatusCode, HeaderMap, Json<Value>) {
|
||||
let mut headers = HeaderMap::new();
|
||||
stamp_documents_headers(&mut headers);
|
||||
(
|
||||
StatusCode::OK,
|
||||
headers,
|
||||
Json(json!({
|
||||
"ok": true,
|
||||
"requestId": context.trace.request_id,
|
||||
"traceId": context.trace.trace_id,
|
||||
"owner": "mnote-web",
|
||||
"result": result,
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
fn stamp_documents_headers(headers: &mut HeaderMap) {
|
||||
if let Ok(name) = HeaderName::from_lowercase(HEADER_MNOTE_WEB_OWNER.as_bytes()) {
|
||||
headers.insert(name, HeaderValue::from_static("mnote-web"));
|
||||
}
|
||||
if let Ok(name) = HeaderName::from_lowercase(HEADER_DOCUMENTS_TRANSPORT.as_bytes()) {
|
||||
headers.insert(name, HeaderValue::from_static("documents-api"));
|
||||
}
|
||||
}
|
||||
|
||||
fn read_env_or_dotenv(key: &str) -> Option<String> {
|
||||
if let Ok(value) = std::env::var(key) {
|
||||
let trimmed = value.trim().trim_matches('"').to_string();
|
||||
@@ -99,13 +114,8 @@ fn next_documents_base_url() -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn should_proxy_via_next(context: &RequestContext) -> bool {
|
||||
context
|
||||
.auth
|
||||
.cookie_header
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.is_some_and(|value| !value.is_empty())
|
||||
fn should_proxy_via_next(_context: &RequestContext) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn build_next_proxy_headers(
|
||||
@@ -368,33 +378,80 @@ async fn proxy_next_documents_save(
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn load_document_meta_result(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
query: DocumentMetaQuery,
|
||||
) -> Result<Value, WebError> {
|
||||
let document_id_owned = query.document_id.trim().to_string();
|
||||
if document_id_owned.is_empty() {
|
||||
return Err(
|
||||
WebError::bad_request_code("document_id_required", "缺少有效 documentId")
|
||||
.with_context(context),
|
||||
);
|
||||
}
|
||||
let effective_workspace_id =
|
||||
resolve_effective_workspace_id(context, query.workspace_id.as_deref(), false)?;
|
||||
if should_proxy_via_next(context) {
|
||||
return proxy_next_documents_meta(
|
||||
context,
|
||||
effective_workspace_id.as_deref(),
|
||||
&document_id_owned,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
fetch_documents_meta_via_convex(
|
||||
state.config(),
|
||||
context,
|
||||
effective_workspace_id.as_deref(),
|
||||
&document_id_owned,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn load_document_content_result(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
query: DocumentContentQuery,
|
||||
) -> Result<Value, WebError> {
|
||||
let document_id_owned = query.document_id.trim().to_string();
|
||||
if document_id_owned.is_empty() {
|
||||
return Err(
|
||||
WebError::bad_request_code("document_id_required", "缺少有效 documentId")
|
||||
.with_context(context),
|
||||
);
|
||||
}
|
||||
let effective_workspace_id =
|
||||
resolve_effective_workspace_id(context, query.workspace_id.as_deref(), false)?;
|
||||
if should_proxy_via_next(context) {
|
||||
return proxy_next_documents_content(
|
||||
context,
|
||||
effective_workspace_id.as_deref(),
|
||||
&document_id_owned,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
execute_runtime_query_via_convex(
|
||||
state.config(),
|
||||
context,
|
||||
effective_workspace_id.as_deref(),
|
||||
RuntimeQueryEnvelopeWire {
|
||||
name: "documents.content.get".into(),
|
||||
payload: json!({
|
||||
"documentId": document_id_owned,
|
||||
"workspaceId": effective_workspace_id,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn meta(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<DocumentMetaQuery>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let document_id = query.document_id.trim();
|
||||
if document_id.is_empty() {
|
||||
return Err(
|
||||
WebError::bad_request_code("document_id_required", "缺少有效 documentId")
|
||||
.with_context(&context),
|
||||
);
|
||||
}
|
||||
let effective_workspace_id =
|
||||
resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), false)?;
|
||||
if should_proxy_via_next(&context) {
|
||||
let result =
|
||||
proxy_next_documents_meta(&context, effective_workspace_id.as_deref(), document_id)
|
||||
.await?;
|
||||
return Ok(ok_response(&context, result));
|
||||
}
|
||||
let result = fetch_documents_meta_via_convex(
|
||||
state.config(),
|
||||
&context,
|
||||
effective_workspace_id.as_deref(),
|
||||
document_id,
|
||||
)
|
||||
.await?;
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
let result = load_document_meta_result(&state, &context, query).await?;
|
||||
Ok(ok_response(&context, result))
|
||||
}
|
||||
|
||||
@@ -402,35 +459,8 @@ pub async fn content(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<DocumentContentQuery>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
let document_id = query.document_id.trim();
|
||||
if document_id.is_empty() {
|
||||
return Err(
|
||||
WebError::bad_request_code("document_id_required", "缺少有效 documentId")
|
||||
.with_context(&context),
|
||||
);
|
||||
}
|
||||
let effective_workspace_id =
|
||||
resolve_effective_workspace_id(&context, query.workspace_id.as_deref(), false)?;
|
||||
if should_proxy_via_next(&context) {
|
||||
let result =
|
||||
proxy_next_documents_content(&context, effective_workspace_id.as_deref(), document_id)
|
||||
.await?;
|
||||
return Ok(ok_response(&context, result));
|
||||
}
|
||||
let result = execute_runtime_query_via_convex(
|
||||
state.config(),
|
||||
&context,
|
||||
effective_workspace_id.as_deref(),
|
||||
RuntimeQueryEnvelopeWire {
|
||||
name: "documents.content.get".into(),
|
||||
payload: json!({
|
||||
"documentId": document_id,
|
||||
"workspaceId": effective_workspace_id,
|
||||
}),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
let result = load_document_content_result(&state, &context, query).await?;
|
||||
Ok(ok_response(&context, result))
|
||||
}
|
||||
|
||||
@@ -438,7 +468,7 @@ pub async fn save(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Json(body): Json<DocumentSaveRequest>,
|
||||
) -> Result<(StatusCode, Json<Value>), WebError> {
|
||||
) -> Result<(StatusCode, HeaderMap, Json<Value>), WebError> {
|
||||
let document_id = body.document_id.trim();
|
||||
if document_id.is_empty() {
|
||||
return Err(
|
||||
@@ -500,9 +530,9 @@ pub async fn save(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::app::{AppConfig, AppState, build_app};
|
||||
use axum::body::{Body, to_bytes};
|
||||
use axum::http::{Request, StatusCode};
|
||||
use crate::app::{build_app, AppConfig, AppState};
|
||||
use axum::body::{to_bytes, Body};
|
||||
use axum::http::{HeaderMap, HeaderValue, Method, Request, StatusCode, Uri};
|
||||
use serde_json::Value;
|
||||
use tower::util::ServiceExt;
|
||||
|
||||
@@ -511,6 +541,9 @@ mod tests {
|
||||
service_name: "mnote-web".into(),
|
||||
service_version: "0.1.0".into(),
|
||||
bind_addr: "127.0.0.1:0".into(),
|
||||
public_bind_addr: "127.0.0.1:3000".into(),
|
||||
legacy_next_base_url: Some("http://127.0.0.1:3100".into()),
|
||||
enable_legacy_next_compat: true,
|
||||
enable_debug_shell_routes: false,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
@@ -579,8 +612,24 @@ mod tests {
|
||||
}))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn convex_auth_cookie_does_not_force_next_proxy() {
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
"cookie",
|
||||
HeaderValue::from_static("__convexAuthJWT=jwt-demo; foo=bar"),
|
||||
);
|
||||
let context = crate::context::RequestContext::from_http_parts(
|
||||
&Method::GET,
|
||||
&"/api/documents/meta".parse::<Uri>().expect("uri"),
|
||||
&headers,
|
||||
);
|
||||
|
||||
assert!(!super::should_proxy_via_next(&context));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn documents_meta_route_returns_document_metadata() {
|
||||
async fn documents_api_meta_route_returns_document_metadata() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -592,6 +641,20 @@ mod tests {
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-mnote-web-owner")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("mnote-web")
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-mnote-documents-transport")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("documents-api")
|
||||
);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
@@ -603,7 +666,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn documents_content_route_returns_page_subtree() {
|
||||
async fn documents_api_content_route_returns_page_subtree() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
|
||||
Reference in New Issue
Block a user