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:
@@ -21,7 +21,7 @@ axum = "0.8.9"
|
|||||||
base64 = "0.23.1"
|
base64 = "0.23.1"
|
||||||
chrono = { version = "0.4.45", default-features = false, features = ["clock", "std"] }
|
chrono = { version = "0.4.45", default-features = false, features = ["clock", "std"] }
|
||||||
chrono-tz = "0.10.4"
|
chrono-tz = "0.10.4"
|
||||||
clap = { version = "4.6.7", features = ["derive", "env"] }
|
clap = { version = "4.6.7", features = ["derive"] }
|
||||||
getrandom = "0.4.3"
|
getrandom = "0.4.3"
|
||||||
image = { version = "0.25.10", default-features = false, features = ["jpeg", "png", "webp"] }
|
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"] }
|
jsonwebtoken = { version = "11.1.0", default-features = false, features = ["aws_lc_rs", "use_pem"] }
|
||||||
|
|||||||
@@ -462,6 +462,46 @@ async fn audit<K: Kind>(
|
|||||||
Ok(())
|
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.
|
/// Steps 3 to 11 of SPEC §14.4.4 for one mutation, on the mutation's savepoint.
|
||||||
async fn process<K: Kind>(
|
async fn process<K: Kind>(
|
||||||
conn: &mut SqliteConnection,
|
conn: &mut SqliteConnection,
|
||||||
@@ -529,34 +569,7 @@ async fn process<K: Kind>(
|
|||||||
// 7. Pushed entity is a delete: always wins.
|
// 7. Pushed entity is a delete: always wins.
|
||||||
if let Some(deleted_at) = K::deleted_at(&pushed).map(str::to_owned) {
|
if let Some(deleted_at) = K::deleted_at(&pushed).map(str::to_owned) {
|
||||||
if let Some(stored) = stored {
|
if let Some(stored) = stored {
|
||||||
let stored_updated = parse_ts(K::stored_updated_at(&stored))
|
return delete_stored::<K>(conn, ctx, env, &stored, &deleted_at, updated_at).await;
|
||||||
.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)),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
// Created and deleted before it ever synced: store the tombstone, no audit, no push. It is validated like a
|
// 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).
|
// create so the row satisfies every rule of the schema (SPEC §14.4.4 step 7 asks for the references).
|
||||||
|
|||||||
Reference in New Issue
Block a user