Add idempotency key for create
This commit is contained in:
parent
a63903734d
commit
ae590e6fc8
35 changed files with 624 additions and 143 deletions
|
|
@ -325,7 +325,8 @@ impl Database {
|
|||
is_deleted,
|
||||
user_id,
|
||||
device_id,
|
||||
has_been_merged
|
||||
has_been_merged,
|
||||
idempotency_key
|
||||
from latest_document_versions
|
||||
where relative_path = ? and is_deleted = false
|
||||
order by vault_update_id desc -- `latest_document_versions` only contains a single latest version of each document, however,
|
||||
|
|
@ -365,7 +366,8 @@ impl Database {
|
|||
is_deleted,
|
||||
user_id,
|
||||
device_id,
|
||||
has_been_merged
|
||||
has_been_merged,
|
||||
idempotency_key
|
||||
from latest_document_versions
|
||||
where document_id = ?
|
||||
"#,
|
||||
|
|
@ -400,7 +402,8 @@ impl Database {
|
|||
is_deleted,
|
||||
user_id,
|
||||
device_id,
|
||||
has_been_merged
|
||||
has_been_merged,
|
||||
idempotency_key
|
||||
from documents
|
||||
where vault_update_id = ?"#,
|
||||
vault_update_id
|
||||
|
|
@ -434,9 +437,10 @@ impl Database {
|
|||
content,
|
||||
is_deleted,
|
||||
user_id,
|
||||
device_id
|
||||
device_id,
|
||||
idempotency_key
|
||||
)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
values (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"#,
|
||||
version.vault_update_id,
|
||||
document_id,
|
||||
|
|
@ -445,7 +449,8 @@ impl Database {
|
|||
version.content,
|
||||
version.is_deleted,
|
||||
version.user_id,
|
||||
version.device_id
|
||||
version.device_id,
|
||||
version.idempotency_key
|
||||
);
|
||||
|
||||
if let Some(mut transaction) = transaction {
|
||||
|
|
@ -481,6 +486,44 @@ impl Database {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_document_by_idempotency_key(
|
||||
&self,
|
||||
vault: &VaultId,
|
||||
idempotency_key: &str,
|
||||
transaction: Option<&mut Transaction<'_>>,
|
||||
) -> Result<Option<StoredDocumentVersion>> {
|
||||
let query = sqlx::query_as!(
|
||||
StoredDocumentVersion,
|
||||
r#"
|
||||
select
|
||||
d.vault_update_id,
|
||||
d.document_id as "document_id: Hyphenated",
|
||||
d.relative_path,
|
||||
d.updated_date as "updated_date: chrono::DateTime<Utc>",
|
||||
d.content,
|
||||
d.is_deleted,
|
||||
d.user_id,
|
||||
d.device_id,
|
||||
d.has_been_merged,
|
||||
d.idempotency_key
|
||||
from latest_document_versions d
|
||||
inner join documents d2 on d.document_id = d2.document_id
|
||||
where d2.idempotency_key = ?
|
||||
limit 1
|
||||
"#,
|
||||
idempotency_key
|
||||
);
|
||||
|
||||
if let Some(transaction) = transaction {
|
||||
query.fetch_optional(&mut **transaction).await
|
||||
} else {
|
||||
query
|
||||
.fetch_optional(&self.get_connection_pool(vault).await?)
|
||||
.await
|
||||
}
|
||||
.context("Cannot fetch document by idempotency key")
|
||||
}
|
||||
|
||||
/// Cleanup idle connection pools that haven't been accessed in more than 5 minutes
|
||||
async fn cleanup_idle_pools(&self) {
|
||||
let mut pools = self.connection_pools.lock().await;
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE documents ADD COLUMN idempotency_key TEXT;
|
||||
|
|
@ -22,6 +22,7 @@ pub struct StoredDocumentVersion {
|
|||
pub device_id: DeviceId,
|
||||
#[allow(dead_code)] // This is for manual analysis
|
||||
pub has_been_merged: bool,
|
||||
pub idempotency_key: Option<String>,
|
||||
}
|
||||
|
||||
impl PartialEq<Self> for StoredDocumentVersion {
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ mod fetch_latest_documents;
|
|||
mod index;
|
||||
mod ping;
|
||||
mod requests;
|
||||
mod resolve_keys;
|
||||
mod responses;
|
||||
mod update_document;
|
||||
mod websocket;
|
||||
|
|
@ -108,6 +109,10 @@ fn get_authed_routes(app_state: AppState) -> Router<AppState> {
|
|||
"/vaults/:vault_id/documents",
|
||||
post(create_document::create_document),
|
||||
)
|
||||
.route(
|
||||
"/vaults/:vault_id/documents/resolve-keys",
|
||||
post(resolve_keys::resolve_keys),
|
||||
)
|
||||
.route(
|
||||
"/vaults/:vault_id/documents/:document_id",
|
||||
get(fetch_latest_document_version::fetch_latest_document_version),
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
use anyhow::Context;
|
||||
use axum::{
|
||||
Extension, Json,
|
||||
extract::{Path, State},
|
||||
|
|
@ -47,6 +48,25 @@ pub async fn create_document(
|
|||
.await
|
||||
.map_err(server_error)?;
|
||||
|
||||
if let Some(ref idempotency_key) = request.idempotency_key {
|
||||
let existing = state
|
||||
.database
|
||||
.get_document_by_idempotency_key(&vault_id, idempotency_key, Some(&mut transaction))
|
||||
.await
|
||||
.map_err(server_error)?;
|
||||
if let Some(existing) = existing {
|
||||
info!("Found existing document with idempotency key `{idempotency_key}`, returning existing document");
|
||||
transaction
|
||||
.rollback()
|
||||
.await
|
||||
.context("Failed to roll back transaction")
|
||||
.map_err(server_error)?;
|
||||
return Ok(Json(DocumentUpdateResponse::FastForwardUpdate(
|
||||
existing.into(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let sanitized_relative_path = sanitize_path(&request.relative_path);
|
||||
|
||||
let latest_version = state
|
||||
|
|
@ -74,6 +94,7 @@ pub async fn create_document(
|
|||
&sanitized_relative_path,
|
||||
request.content.contents.to_vec(),
|
||||
transaction,
|
||||
request.idempotency_key,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
|
@ -111,6 +132,7 @@ pub async fn create_document(
|
|||
user_id: user.name,
|
||||
device_id: device_id.0,
|
||||
has_been_merged: false,
|
||||
idempotency_key: request.idempotency_key,
|
||||
};
|
||||
|
||||
state
|
||||
|
|
|
|||
|
|
@ -84,6 +84,7 @@ pub async fn delete_document(
|
|||
user_id: user.name,
|
||||
device_id: device_id.0,
|
||||
has_been_merged: false,
|
||||
idempotency_key: None,
|
||||
};
|
||||
|
||||
state
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ pub struct CreateDocumentVersion {
|
|||
#[ts(as = "Vec<u8>")]
|
||||
#[form_data(limit = "unlimited")]
|
||||
pub content: FieldData<Bytes>,
|
||||
|
||||
pub idempotency_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, TryFromMultipart)]
|
||||
|
|
|
|||
63
sync-server/src/server/resolve_keys.rs
Normal file
63
sync-server/src/server/resolve_keys.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use axum::{
|
||||
Json,
|
||||
extract::{Path, State},
|
||||
};
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
app_state::{AppState, database::models::VaultId},
|
||||
errors::{SyncServerError, server_error},
|
||||
utils::normalize::normalize,
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ResolveKeysPathParams {
|
||||
#[serde(deserialize_with = "normalize")]
|
||||
vault_id: VaultId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResolveKeysRequest {
|
||||
pub idempotency_keys: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResolveKeysResponse {
|
||||
/// Maps `idempotency_key` -> `document_id` for keys that were found
|
||||
pub resolved: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
pub async fn resolve_keys(
|
||||
Path(ResolveKeysPathParams { vault_id }): Path<ResolveKeysPathParams>,
|
||||
State(state): State<AppState>,
|
||||
Json(request): Json<ResolveKeysRequest>,
|
||||
) -> Result<Json<ResolveKeysResponse>, SyncServerError> {
|
||||
debug!(
|
||||
"Resolving {} idempotency keys in vault `{vault_id}`",
|
||||
request.idempotency_keys.len()
|
||||
);
|
||||
|
||||
let mut resolved = HashMap::new();
|
||||
|
||||
for key in &request.idempotency_keys {
|
||||
let document = state
|
||||
.database
|
||||
.get_document_by_idempotency_key(&vault_id, key, None)
|
||||
.await
|
||||
.map_err(server_error)?;
|
||||
|
||||
if let Some(doc) = document {
|
||||
resolved.insert(key.clone(), doc.document_id.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
debug!("Resolved {}/{} idempotency keys", resolved.len(), request.idempotency_keys.len());
|
||||
|
||||
Ok(Json(ResolveKeysResponse { resolved }))
|
||||
}
|
||||
|
|
@ -182,6 +182,7 @@ async fn update_document(
|
|||
&sanitized_relative_path,
|
||||
content,
|
||||
transaction,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
|
@ -198,6 +199,7 @@ pub async fn merge_with_stored_version(
|
|||
sanitized_relative_path: &str,
|
||||
content: Vec<u8>,
|
||||
mut transaction: Transaction<'_>,
|
||||
idempotency_key: Option<String>,
|
||||
) -> Result<Json<DocumentUpdateResponse>, SyncServerError> {
|
||||
// Return the latest version if the content and path are the same as the latest
|
||||
// version
|
||||
|
|
@ -290,6 +292,7 @@ pub async fn merge_with_stored_version(
|
|||
user_id: user.name,
|
||||
device_id: device_id.0,
|
||||
has_been_merged: are_all_participants_mergable && is_different_from_request_content,
|
||||
idempotency_key,
|
||||
};
|
||||
|
||||
state
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue