feat: complete deletion of items on request

This commit is contained in:
Per Stark
2025-01-30 16:38:32 +01:00
parent d38fba7365
commit de070080f2
8 changed files with 178 additions and 26 deletions

View File

@@ -2406,6 +2406,9 @@
} }
} }
} }
.z-4 {
z-index: 4;
}
.tab-content { .tab-content {
order { order {
} }
@@ -3503,6 +3506,9 @@
.h-5 { .h-5 {
height: calc(var(--spacing) * 5); height: calc(var(--spacing) * 5);
} }
.h-6 {
height: calc(var(--spacing) * 6);
}
.h-32 { .h-32 {
height: calc(var(--spacing) * 32); height: calc(var(--spacing) * 32);
} }
@@ -3512,6 +3518,9 @@
.min-h-screen { .min-h-screen {
min-height: 100vh; min-height: 100vh;
} }
.w-2 {
width: calc(var(--spacing) * 2);
}
.w-5 { .w-5 {
width: calc(var(--spacing) * 5); width: calc(var(--spacing) * 5);
} }

View File

@@ -66,7 +66,7 @@ impl LLMGraphAnalysisResult {
.await?; .await?;
// Process relationships // Process relationships
let relationships = self.process_relationships(Arc::clone(&mapper))?; let relationships = self.process_relationships(source_id, Arc::clone(&mapper))?;
Ok((entities, relationships)) Ok((entities, relationships))
} }
@@ -116,6 +116,7 @@ impl LLMGraphAnalysisResult {
fn process_relationships( fn process_relationships(
&self, &self,
source_id: &str,
mapper: Arc<Mutex<GraphMapper>>, mapper: Arc<Mutex<GraphMapper>>,
) -> Result<Vec<KnowledgeRelationship>, AppError> { ) -> Result<Vec<KnowledgeRelationship>, AppError> {
let mut mapper_guard = mapper let mut mapper_guard = mapper
@@ -130,6 +131,7 @@ impl LLMGraphAnalysisResult {
Ok(KnowledgeRelationship::new( Ok(KnowledgeRelationship::new(
source_db_id.to_string(), source_db_id.to_string(),
target_db_id.to_string(), target_db_id.to_string(),
source_id.to_string(),
rel.type_.clone(), rel.type_.clone(),
None, None,
)) ))

View File

@@ -16,8 +16,12 @@ use crate::{
AppState, AppState,
}, },
storage::{ storage::{
db::delete_item, db::{delete_item, get_all_stored_items, get_item},
types::{job::Job, text_content::TextContent, user::User}, types::{
file_info::FileInfo, job::Job, knowledge_entity::KnowledgeEntity,
knowledge_relationship::KnowledgeRelationship, text_chunk::TextChunk,
text_content::TextContent, user::User,
},
}, },
}; };
@@ -56,19 +60,15 @@ pub async fn index_handler(
false => vec![], false => vec![],
}; };
info!("{:?}", latest_text_contents); // let latest_knowledge_entities = match auth.current_user.is_some() {
// true => User::get_latest_knowledge_entities(
let latest_knowledge_entities = match auth.current_user.is_some() { // auth.current_user.clone().unwrap().id.as_str(),
true => User::get_latest_knowledge_entities( // &state.surreal_db_client,
auth.current_user.clone().unwrap().id.as_str(), // )
&state.surreal_db_client, // .await
) // .map_err(|e| HtmlError::new(e, state.templates.clone()))?,
.await // false => vec![],
.map_err(|e| HtmlError::new(e, state.templates.clone()))?, // };
false => vec![],
};
info!("{:?}", latest_knowledge_entities);
let output = render_template( let output = render_template(
IndexData::template_name(), IndexData::template_name(),
@@ -100,15 +100,65 @@ pub async fn delete_text_content(
None => return Ok(Redirect::to("/").into_response()), None => return Ok(Redirect::to("/").into_response()),
}; };
delete_item::<TextContent>(&state.surreal_db_client, &id) // Get TextContent from db
let text_content = match get_item::<TextContent>(&state.surreal_db_client, &id)
.await
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?
{
Some(text_content) => text_content,
None => {
return Err(HtmlError::new(
AppError::NotFound("No item found".to_string()),
state.templates,
))
}
};
// Validate that the user is the owner
if text_content.user_id != user.id {
return Err(HtmlError::new(
AppError::Auth("You are not the owner of that content".to_string()),
state.templates,
));
}
// If TextContent has file_info, delete it from db and file from disk.
if text_content.file_info.is_some() {
FileInfo::delete_by_id(
&text_content.file_info.unwrap().id,
&state.surreal_db_client,
)
.await
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
}
// Delete textcontent from db
delete_item::<TextContent>(&state.surreal_db_client, &text_content.id)
.await .await
.map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?; .map_err(|e| HtmlError::new(AppError::from(e), state.templates.clone()))?;
let latest_text_contents = User::get_latest_text_contents(&user.id, &state.surreal_db_client) // Delete TextChunks
TextChunk::delete_by_source_id(&text_content.id, &state.surreal_db_client)
.await .await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?; .map_err(|e| HtmlError::new(e, state.templates.clone()))?;
info!("{:?}", latest_text_contents); // Delete KnowledgeEntities
KnowledgeEntity::delete_by_source_id(&text_content.id, &state.surreal_db_client)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
// Delete KnowledgeRelationships
KnowledgeRelationship::delete_relationships_by_source_id(
&text_content.id,
&state.surreal_db_client,
)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
// Get latest text contents after updates
let latest_text_contents = User::get_latest_text_contents(&user.id, &state.surreal_db_client)
.await
.map_err(|e| HtmlError::new(e, state.templates.clone()))?;
let output = render_block( let output = render_block(
"index/signed_in/recent_content.html", "index/signed_in/recent_content.html",

View File

@@ -7,11 +7,12 @@ use std::{
}; };
use tempfile::NamedTempFile; use tempfile::NamedTempFile;
use thiserror::Error; use thiserror::Error;
use tokio::fs::remove_dir_all;
use tracing::info; use tracing::info;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
storage::db::{store_item, SurrealDbClient}, storage::db::{delete_item, get_item, store_item, SurrealDbClient},
stored_object, stored_object,
}; };
@@ -198,4 +199,50 @@ impl FileInfo {
.next() .next()
.ok_or(FileError::FileNotFound(sha256.to_string())) .ok_or(FileError::FileNotFound(sha256.to_string()))
} }
/// Removes FileInfo from database and file from disk
///
/// # Arguments
/// * `id` - Id of the FileInfo
/// * `db_client` - Reference to SurrealDbClient
///
/// # Returns
/// `Result<(), FileError>`
pub async fn delete_by_id(id: &str, db_client: &SurrealDbClient) -> Result<(), FileError> {
// Get the FileInfo from the database
let file_info = match get_item::<FileInfo>(db_client, id).await? {
Some(info) => info,
None => {
return Err(FileError::FileNotFound(format!(
"File with id {} was not found",
id
)))
}
};
// Remove the file and its parent directory
let file_path = Path::new(&file_info.path);
if file_path.exists() {
// Get the parent directory of the file
if let Some(parent_dir) = file_path.parent() {
// Remove the entire directory containing the file
remove_dir_all(parent_dir).await?;
info!("Removed directory {:?} and its contents", parent_dir);
} else {
return Err(FileError::FileNotFound(
"File has no parent directory".to_string(),
));
}
} else {
return Err(FileError::FileNotFound(format!(
"File at path {:?} was not found",
file_path
)));
}
// Delete the FileInfo from the database
delete_item::<FileInfo>(db_client, id).await?;
Ok(())
}
} }

View File

@@ -1,4 +1,4 @@
use crate::stored_object; use crate::{error::AppError, storage::db::SurrealDbClient, stored_object};
use uuid::Uuid; use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
@@ -58,4 +58,18 @@ impl KnowledgeEntity {
user_id, user_id,
} }
} }
pub async fn delete_by_source_id(
source_id: &str,
db_client: &SurrealDbClient,
) -> Result<(), AppError> {
let query = format!(
"DELETE {} WHERE source_id = '{}'",
Self::table_name(),
source_id
);
db_client.query(query).await?;
Ok(())
}
} }

View File

@@ -1,13 +1,14 @@
use crate::{error::AppError, stored_object}; use crate::{error::AppError, storage::db::SurrealDbClient, stored_object};
use surrealdb::{engine::any::Any, Surreal}; use surrealdb::{engine::any::Any, sql::Subquery, Surreal};
use tracing::debug; use tracing::debug;
use uuid::Uuid; use uuid::Uuid;
stored_object!(KnowledgeRelationship, "knowledge_relationship", { stored_object!(KnowledgeRelationship, "relates_to", {
#[serde(rename = "in")] #[serde(rename = "in")]
in_: String, in_: String,
out: String, out: String,
relationship_type: String, relationship_type: String,
source_id: String,
metadata: Option<serde_json::Value> metadata: Option<serde_json::Value>
}); });
@@ -15,6 +16,7 @@ impl KnowledgeRelationship {
pub fn new( pub fn new(
in_: String, in_: String,
out: String, out: String,
source_id: String,
relationship_type: String, relationship_type: String,
metadata: Option<serde_json::Value>, metadata: Option<serde_json::Value>,
) -> Self { ) -> Self {
@@ -25,6 +27,7 @@ impl KnowledgeRelationship {
updated_at: now, updated_at: now,
in_, in_,
out, out,
source_id,
relationship_type, relationship_type,
metadata, metadata,
} }
@@ -41,4 +44,18 @@ impl KnowledgeRelationship {
Ok(()) Ok(())
} }
pub async fn delete_relationships_by_source_id(
source_id: &str,
db_client: &SurrealDbClient,
) -> Result<(), AppError> {
let query = format!(
"DELETE knowledge_entity -> relates_to WHERE source_id = '{}'",
source_id
);
db_client.query(query).await?;
Ok(())
}
} }

View File

@@ -1,4 +1,4 @@
use crate::stored_object; use crate::{error::AppError, storage::db::SurrealDbClient, stored_object};
use uuid::Uuid; use uuid::Uuid;
stored_object!(TextChunk, "text_chunk", { stored_object!(TextChunk, "text_chunk", {
@@ -21,4 +21,18 @@ impl TextChunk {
user_id, user_id,
} }
} }
pub async fn delete_by_source_id(
source_id: &str,
db_client: &SurrealDbClient,
) -> Result<(), AppError> {
let query = format!(
"DELETE {} WHERE source_id = '{}'",
Self::table_name(),
source_id
);
db_client.query(query).await?;
Ok(())
}
} }

View File

@@ -7,7 +7,6 @@ use super::file_info::FileInfo;
stored_object!(TextContent, "text_content", { stored_object!(TextContent, "text_content", {
text: String, text: String,
file_info: Option<FileInfo>, file_info: Option<FileInfo>,
url: Option<String>, url: Option<String>,
instructions: String, instructions: String,
category: String, category: String,