Server overhaul: sync push delete step in its own function; clap env feature dropped

process<K> (158 lines) hands step 7 against a stored row to delete_stored<K>; same SQL, audit and push. kvitt-server
never reads a clap argument from the environment (Config reads it), so clap's env feature goes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
This commit is contained in:
2026-10-18 22:36:32 +02:00
parent c942d73c66
commit 21407456f1
2 changed files with 42 additions and 29 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ axum = "0.8.9"
base64 = "0.23.1"
chrono = { version = "0.4.45", default-features = false, features = ["clock", "std"] }
chrono-tz = "0.10.4"
clap = { version = "4.6.7", features = ["derive", "env"] }
clap = { version = "4.6.7", features = ["derive"] }
getrandom = "0.4.3"
image = { version = "0.25.10", default-features = false, features = ["jpeg", "png", "webp"] }
jsonwebtoken = { version = "11.1.0", default-features = false, features = ["aws_lc_rs", "use_pem"] }
+41 -28
View File
@@ -462,6 +462,46 @@ async fn audit<K: Kind>(
Ok(())
}
/// Step 7 of SPEC §14.4.4 against a live stored row: the delete always wins. `updated_at` becomes the later of the
/// stored and the pushed one; audited as `delete` with the pushed `deleted_at` as `client_at`.
async fn delete_stored<K: Kind>(
conn: &mut SqliteConnection,
ctx: &Ctx<'_>,
env: &Envelope,
stored: &K::Wire,
deleted_at: &str,
updated_at: DateTime<Utc>,
) -> Result<Processed, AppError> {
let stored_updated = parse_ts(K::stored_updated_at(stored))
.ok_or_else(|| AppError::internal("stored updated_at does not parse"))?;
let new_updated_at = format_ts(stored_updated.max(updated_at));
let version = db::next_server_version(conn).await?;
sqlx::query(K::DELETE_SQL)
.bind(deleted_at)
.bind(&new_updated_at)
.bind(ctx.caller.user_id.to_string())
.bind(ctx.caller.device_id.to_string())
.bind(version)
.bind(env.entity_id.to_string())
.execute(&mut *conn)
.await?;
let after = reload::<K>(conn, env.entity_id).await?;
audit::<K>(
conn,
ctx,
env,
AuditAction::Delete,
deleted_at,
Some(stored),
&after,
)
.await?;
Ok(Processed {
result: accepted(env, K::entity(after)),
push: Some(push_of(K::DELETED, env, ctx)),
})
}
/// Steps 3 to 11 of SPEC §14.4.4 for one mutation, on the mutation's savepoint.
async fn process<K: Kind>(
conn: &mut SqliteConnection,
@@ -529,34 +569,7 @@ async fn process<K: Kind>(
// 7. Pushed entity is a delete: always wins.
if let Some(deleted_at) = K::deleted_at(&pushed).map(str::to_owned) {
if let Some(stored) = stored {
let stored_updated = parse_ts(K::stored_updated_at(&stored))
.ok_or_else(|| AppError::internal("stored updated_at does not parse"))?;
let new_updated_at = format_ts(stored_updated.max(updated_at));
let version = db::next_server_version(conn).await?;
sqlx::query(K::DELETE_SQL)
.bind(&deleted_at)
.bind(&new_updated_at)
.bind(ctx.caller.user_id.to_string())
.bind(ctx.caller.device_id.to_string())
.bind(version)
.bind(env.entity_id.to_string())
.execute(&mut *conn)
.await?;
let after = reload::<K>(conn, env.entity_id).await?;
audit::<K>(
conn,
ctx,
env,
AuditAction::Delete,
&deleted_at,
Some(&stored),
&after,
)
.await?;
return Ok(Processed {
result: accepted(env, K::entity(after)),
push: Some(push_of(K::DELETED, env, ctx)),
});
return delete_stored::<K>(conn, ctx, env, &stored, &deleted_at, updated_at).await;
}
// Created and deleted before it ever synced: store the tombstone, no audit, no push. It is validated like a
// create so the row satisfies every rule of the schema (SPEC §14.4.4 step 7 asks for the references).