summaryrefslogtreecommitdiffstats
path: root/atuin-server
diff options
context:
space:
mode:
authorEllie Huxtable <ellie@elliehuxtable.com>2023-03-20 09:26:54 +0000
committerGitHub <noreply@github.com>2023-03-20 09:26:54 +0000
commitdcd77749dd1fdf6b0c8183bfbdf4f97bf238ebe4 (patch)
tree97c623911eeb52da65c2b3fd80092f2c86f3dd18 /atuin-server
parentedcd477153d00944c5dae16ec3ba69e339e1450c (diff)
Add history deletion (#791)
* Drop events. I'd still like to do them, but differently * Start adding delete api stuff * Set mailmap * Delete delete delete * Fix tests * Make clippy happy
Diffstat (limited to 'atuin-server')
-rw-r--r--atuin-server/migrations/20230315220537_drop-events.sql2
-rw-r--r--atuin-server/migrations/20230315224203_create-deleted.sql5
-rw-r--r--atuin-server/src/database.rs46
-rw-r--r--atuin-server/src/handlers/history.rs22
-rw-r--r--atuin-server/src/handlers/mod.rs1
-rw-r--r--atuin-server/src/handlers/status.rs29
-rw-r--r--atuin-server/src/router.rs4
7 files changed, 108 insertions, 1 deletions
diff --git a/atuin-server/migrations/20230315220537_drop-events.sql b/atuin-server/migrations/20230315220537_drop-events.sql
new file mode 100644
index 000000000..fe3cae170
--- /dev/null
+++ b/atuin-server/migrations/20230315220537_drop-events.sql
@@ -0,0 +1,2 @@
+-- Add migration script here
+drop table events;
diff --git a/atuin-server/migrations/20230315224203_create-deleted.sql b/atuin-server/migrations/20230315224203_create-deleted.sql
new file mode 100644
index 000000000..9a9e6263f
--- /dev/null
+++ b/atuin-server/migrations/20230315224203_create-deleted.sql
@@ -0,0 +1,5 @@
+-- Add migration script here
+alter table history add column if not exists deleted_at timestamp;
+
+-- queries will all be selecting the ids of history for a user, that has been deleted
+create index if not exists history_deleted_index on history(client_id, user_id, deleted_at);
diff --git a/atuin-server/src/database.rs b/atuin-server/src/database.rs
index ef6c6d859..7f3e5dac4 100644
--- a/atuin-server/src/database.rs
+++ b/atuin-server/src/database.rs
@@ -4,6 +4,9 @@ use async_trait::async_trait;
use chrono::{Datelike, TimeZone};
use chronoutil::RelativeDuration;
use sqlx::{postgres::PgPoolOptions, Result};
+
+use sqlx::Row;
+
use tracing::{debug, instrument, warn};
use super::{
@@ -28,6 +31,9 @@ pub trait Database {
async fn count_history(&self, user: &User) -> Result<i64>;
async fn count_history_cached(&self, user: &User) -> Result<i64>;
+ async fn delete_history(&self, user: &User, id: String) -> Result<()>;
+ async fn deleted_history(&self, user: &User) -> Result<Vec<String>>;
+
async fn count_history_range(
&self,
user: &User,
@@ -141,6 +147,46 @@ impl Database for Postgres {
Ok(res.0 as i64)
}
+ async fn delete_history(&self, user: &User, id: String) -> Result<()> {
+ sqlx::query(
+ "update history
+ set deleted_at = $3
+ where user_id = $1
+ and client_id = $2
+ and deleted_at is null", // don't just keep setting it
+ )
+ .bind(user.id)
+ .bind(id)
+ .bind(chrono::Utc::now().naive_utc())
+ .fetch_all(&self.pool)
+ .await?;
+
+ Ok(())
+ }
+
+ #[instrument(skip_all)]
+ async fn deleted_history(&self, user: &User) -> Result<Vec<String>> {
+ // The cache is new, and the user might not yet have a cache value.
+ // They will have one as soon as they post up some new history, but handle that
+ // edge case.
+
+ let res = sqlx::query(
+ "select client_id from history
+ where user_id = $1
+ and deleted_at is not null",
+ )
+ .bind(user.id)
+ .fetch_all(&self.pool)
+ .await?;
+
+ let res = res
+ .iter()
+ .map(|row| row.get::<String, _>("client_id"))
+ .collect();
+
+ Ok(res)
+ }
+
#[instrument(skip_all)]
async fn count_history_range(
&self,
diff --git a/atuin-server/src/handlers/history.rs b/atuin-server/src/handlers/history.rs
index 7cf18323d..9a7cb2458 100644
--- a/atuin-server/src/handlers/history.rs
+++ b/atuin-server/src/handlers/history.rs
@@ -75,6 +75,28 @@ pub async fn list<DB: Database>(
}
#[instrument(skip_all, fields(user.id = user.id))]
+pub async fn delete<DB: Database>(
+ user: User,
+ state: State<AppState<DB>>,
+ Json(req): Json<DeleteHistoryRequest>,
+) -> Result<Json<MessageResponse>, ErrorResponseStatus<'static>> {
+ let db = &state.0.database;
+
+ // user_id is the ID of the history, as set by the user (the server has its own ID)
+ let deleted = db.delete_history(&user, req.client_id).await;
+
+ if let Err(e) = deleted {
+ error!("failed to delete history: {}", e);
+ return Err(ErrorResponse::reply("failed to delete history")
+ .with_status(StatusCode::INTERNAL_SERVER_ERROR));
+ }
+
+ Ok(Json(MessageResponse {
+ message: String::from("deleted OK"),
+ }))
+}
+
+#[instrument(skip_all, fields(user.id = user.id))]
pub async fn add<DB: Database>(
user: User,
state: State<AppState<DB>>,
diff --git a/atuin-server/src/handlers/mod.rs b/atuin-server/src/handlers/mod.rs
index 082ae4711..35d32f6f6 100644
--- a/atuin-server/src/handlers/mod.rs
+++ b/atuin-server/src/handlers/mod.rs
@@ -2,6 +2,7 @@ use atuin_common::api::{ErrorResponse, IndexResponse};
use axum::{response::IntoResponse, Json};
pub mod history;
+pub mod status;
pub mod user;
const VERSION: &str = env!("CARGO_PKG_VERSION");
diff --git a/atuin-server/src/handlers/status.rs b/atuin-server/src/handlers/status.rs
new file mode 100644
index 000000000..9c7ef7792
--- /dev/null
+++ b/atuin-server/src/handlers/status.rs
@@ -0,0 +1,29 @@
+use axum::{extract::State, Json};
+use http::StatusCode;
+use tracing::instrument;
+
+use super::{ErrorResponse, ErrorResponseStatus, RespExt};
+use crate::{database::Database, models::User, router::AppState};
+
+use atuin_common::api::*;
+
+#[instrument(skip_all, fields(user.id = user.id))]
+pub async fn status<DB: Database>(
+ user: User,
+ state: State<AppState<DB>>,
+) -> Result<Json<StatusResponse>, ErrorResponseStatus<'static>> {
+ let db = &state.0.database;
+
+ let history_count = db.count_history_cached(&user).await;
+ let deleted = db.deleted_history(&user).await;
+
+ if history_count.is_err() || deleted.is_err() {
+ return Err(ErrorResponse::reply("failed to query history count")
+ .with_status(StatusCode::INTERNAL_SERVER_ERROR));
+ }
+
+ Ok(Json(StatusResponse {
+ count: history_count.unwrap(),
+ deleted: deleted.unwrap(),
+ }))
+}
diff --git a/atuin-server/src/router.rs b/atuin-server/src/router.rs
index c4f7d3099..58aac3bd4 100644
--- a/atuin-server/src/router.rs
+++ b/atuin-server/src/router.rs
@@ -2,7 +2,7 @@ use async_trait::async_trait;
use axum::{
extract::FromRequestParts,
response::IntoResponse,
- routing::{get, post},
+ routing::{delete, get, post},
Router,
};
use eyre::Result;
@@ -68,7 +68,9 @@ pub fn router<DB: Database + Clone + Send + Sync + 'static>(
.route("/sync/count", get(handlers::history::count))
.route("/sync/history", get(handlers::history::list))
.route("/sync/calendar/:focus", get(handlers::history::calendar))
+ .route("/sync/status", get(handlers::status::status))
.route("/history", post(handlers::history::add))
+ .route("/history", delete(handlers::history::delete))
.route("/user/:username", get(handlers::user::get))
.route("/register", post(handlers::user::register))
.route("/login", post(handlers::user::login));