From d4b66508ee6b12b170929c6e3981c3eb6f8ae509 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 8 Dec 2024 14:23:07 +0000 Subject: [PATCH] Add endpoint handlers --- backend/sync_server/src/server.rs | 67 +++++++++++ .../sync_server/src/server/delete_document.rs | 66 +++++++++++ .../server/fetch_latest_document_version.rs | 32 +++++ .../src/server/fetch_latest_documents.rs | 22 ++++ .../sync_server/src/server/update_document.rs | 110 ++++++++++++++++++ 5 files changed, 297 insertions(+) create mode 100644 backend/sync_server/src/server.rs create mode 100644 backend/sync_server/src/server/delete_document.rs create mode 100644 backend/sync_server/src/server/fetch_latest_document_version.rs create mode 100644 backend/sync_server/src/server/fetch_latest_documents.rs create mode 100644 backend/sync_server/src/server/update_document.rs diff --git a/backend/sync_server/src/server.rs b/backend/sync_server/src/server.rs new file mode 100644 index 00000000..1b3350b4 --- /dev/null +++ b/backend/sync_server/src/server.rs @@ -0,0 +1,67 @@ +use crate::app_state::AppState; +use anyhow::Context; +use anyhow::Result; +use axum::extract::DefaultBodyLimit; +use axum::extract::WebSocketUpgrade; +use axum::response::Response; +use axum::routing::delete; +use axum::{routing::get, routing::put, Router}; +use log::info; + +mod delete_document; +mod fetch_latest_document_version; +mod fetch_latest_documents; +mod requests; +mod update_document; + +pub async fn create_server(app_state: AppState) -> Result<()> { + let address = format!( + "{}:{}", + &app_state.config.server.host, &app_state.config.server.port + ); + + let app = Router::new() + .route( + "/vaults/:vault_id/documents/latest", + get(fetch_latest_documents::fetch_latest_documents), + ) + .route( + "/vaults/:vault_id/documents/:document_id/versions/:parent_version_id", + put(update_document::update_document), + ) + .route( + "/vaults/:vault_id/documents/:document_id", + delete(delete_document::delete_document), + ) + .route( + "/vaults/:vault_id/documents/:document_id/versions/latest", + get(fetch_latest_document_version::fetch_latest_document_version), + ) + .route("/ws", get(handler)) + .layer(DefaultBodyLimit::max( + app_state.config.server.max_body_size_mb * 1024 * 1024, + )) + .with_state(app_state); + + let listener = tokio::net::TcpListener::bind(address.clone()) + .await + .with_context(|| format!("Failed to bind to address: {}", address))?; + + info!( + "Listening on {}", + listener + .local_addr() + .context("Failed to get local address")? + ); + + axum::serve(listener, app) + .await + .context("Failed to start server") +} + +async fn handler(ws: WebSocketUpgrade) -> Response { + ws.protocols(["graphql-ws", "graphql-transport-ws"]) + .on_upgrade(|socket| async { + // ... + }) +} diff --git a/backend/sync_server/src/server/delete_document.rs b/backend/sync_server/src/server/delete_document.rs new file mode 100644 index 00000000..d3d2a9ac --- /dev/null +++ b/backend/sync_server/src/server/delete_document.rs @@ -0,0 +1,66 @@ +use crate::app_state::AppState; +use crate::database::models::DocumentId; +use crate::database::models::StoredDocumentVersion; +use crate::database::models::VaultId; +use crate::errors::not_found_error; +use crate::errors::server_error; +use crate::errors::SyncServerError; +use anyhow::anyhow; +use anyhow::Context; +use axum::extract::Path; +use axum::extract::State; +use axum::Json; + +use super::requests::DeleteDocumentVersion; + +#[axum::debug_handler] +pub async fn delete_document( + Path((vault_id, document_id)): Path<(VaultId, DocumentId)>, + State(state): State, + Json(request): Json, +) -> Result<(), SyncServerError> { + let mut transaction = state + .database + .create_transaction() + .await + .map_err(server_error)?; + + let latest_version = state + .database + .get_latest_document_version(&vault_id, &document_id, Some(&mut transaction)) + .await + .map_err(server_error)? + .map(Ok) + .unwrap_or_else(|| { + Err(not_found_error(anyhow!( + "Latest document version of document `{}` not found", + document_id + ))) + })?; + + let new_version = StoredDocumentVersion { + vault_id, + document_id, + version_id: latest_version.version_id + 1, + content: vec![], + created_date: request.created_date, + updated_date: chrono::Utc::now(), + relative_path: latest_version.relative_path, + is_binary: latest_version.is_binary, + is_deleted: true, + }; + + state + .database + .insert_document_version(&new_version, Some(&mut transaction)) + .await + .map_err(server_error)?; + + transaction + .commit() + .await + .context("Failed to commit successful transaction") + .map_err(server_error)?; + + Ok(()) +} diff --git a/backend/sync_server/src/server/fetch_latest_document_version.rs b/backend/sync_server/src/server/fetch_latest_document_version.rs new file mode 100644 index 00000000..f1a515ce --- /dev/null +++ b/backend/sync_server/src/server/fetch_latest_document_version.rs @@ -0,0 +1,32 @@ +use crate::app_state::AppState; +use crate::database::models::DocumentId; +use crate::database::models::DocumentVersion; +use crate::database::models::VaultId; +use crate::errors::not_found_error; +use crate::errors::server_error; +use crate::errors::SyncServerError; +use anyhow::anyhow; +use axum::extract::Path; +use axum::extract::State; +use axum::Json; + +#[axum::debug_handler] +pub async fn fetch_latest_document_version( + Path((vault_id, document_id)): Path<(VaultId, DocumentId)>, + State(state): State, +) -> Result, SyncServerError> { + let latest_version = state + .database + .get_latest_document_version(&vault_id, &document_id, None) + .await + .map_err(server_error)? + .map(Ok) + .unwrap_or_else(|| { + Err(not_found_error(anyhow!( + "Latest document version of document `{}` not found", + document_id + ))) + })?; + + Ok(Json(latest_version.into())) +} diff --git a/backend/sync_server/src/server/fetch_latest_documents.rs b/backend/sync_server/src/server/fetch_latest_documents.rs new file mode 100644 index 00000000..d0bb23ac --- /dev/null +++ b/backend/sync_server/src/server/fetch_latest_documents.rs @@ -0,0 +1,22 @@ +use crate::app_state::AppState; +use crate::database::models::DocumentVersionWithoutContent; +use crate::database::models::VaultId; +use crate::errors::server_error; +use crate::errors::SyncServerError; +use axum::extract::Path; +use axum::extract::State; +use axum::Json; + +#[axum::debug_handler] +pub async fn fetch_latest_documents( + Path(vault_id): Path, + State(state): State, +) -> Result>, SyncServerError> { + let latest_version = state + .database + .get_latest_documents(&vault_id, None) + .await + .map_err(server_error)?; + + Ok(Json(latest_version)) +} diff --git a/backend/sync_server/src/server/update_document.rs b/backend/sync_server/src/server/update_document.rs new file mode 100644 index 00000000..030a496e --- /dev/null +++ b/backend/sync_server/src/server/update_document.rs @@ -0,0 +1,110 @@ +use crate::app_state::AppState; +use crate::database::models::DocumentId; +use crate::database::models::DocumentVersionId; +use crate::database::models::DocumentVersionWithoutContent; +use crate::database::models::StoredDocumentVersion; +use crate::database::models::VaultId; +use crate::errors::client_error; +use crate::errors::not_found_error; +use crate::errors::server_error; +use crate::errors::SyncServerError; +use anyhow::anyhow; +use anyhow::Context; +use axum::extract::Path; +use axum::extract::State; +use axum::Json; +use sync_lib::base64_to_bytes; +use sync_lib::base64_to_string; + +use super::requests::CreateDocumentVersion; + +#[axum::debug_handler] +pub async fn update_document( + Path((vault_id, document_id, parent_version_id)): Path<( + VaultId, + DocumentId, + DocumentVersionId, + )>, + State(state): State, + Json(request): Json, +) -> Result, SyncServerError> { + let parent = state + .database + .get_document_version(&vault_id, &document_id, &parent_version_id, None) + .await + .map_err(server_error)? + .map(Ok) + .unwrap_or_else(|| { + Err(not_found_error(anyhow!( + "Parent version with id `{}` not found", + parent_version_id + ))) + })?; + + let mut transaction = state + .database + .create_transaction() + .await + .map_err(server_error)?; + + let latest_version = state + .database + .get_latest_document_version(&vault_id, &document_id, Some(&mut transaction)) + .await + .map_err(server_error)? + .map(Ok) + .unwrap_or_else(|| { + Err(not_found_error(anyhow!( + "Latest document version of document `{}` not found", + document_id + ))) + })?; + + if latest_version.is_deleted { + return Err(client_error(anyhow!( + "Document `{}` is deleted", + document_id + ))); + } + + let merged_content = if request.is_binary { + base64_to_bytes(&request.content_base64) + .context("Failed to decode base64 content in request") + .map_err(client_error)? + } else { + reconcile::reconcile( + &parent.content_as_string(), + &latest_version.content_as_string(), + &base64_to_string(&request.content_base64) + .context("Failed to decode base64 content in request") + .map_err(client_error)?, + ) + .into_bytes() + }; + + let new_version = StoredDocumentVersion { + vault_id, + document_id, + version_id: latest_version.version_id + 1, + content: merged_content, + created_date: request.created_date, + relative_path: request.relative_path, + updated_date: chrono::Utc::now(), + is_binary: request.is_binary, + is_deleted: false, + }; + + state + .database + .insert_document_version(&new_version, Some(&mut transaction)) + .await + .map_err(server_error)?; + + transaction + .commit() + .await + .context("Failed to commit successful transaction") + .map_err(server_error)?; + + Ok(Json(new_version.into())) +}