use cognee_database::IngestDb;
use cognee_delete::{DeleteMode, DeleteRequest, DeleteResult, DeleteScope, DeleteService};
use uuid::Uuid;
use super::error::ApiError;
#[derive(Debug, Clone)]
pub enum DatasetRef {
Name(String),
Id(Uuid),
}
impl DatasetRef {
pub async fn to_name(
&self,
owner_id: Uuid,
db: Option<&dyn IngestDb>,
) -> Result<String, ApiError> {
match self {
DatasetRef::Name(name) => Ok(name.clone()),
DatasetRef::Id(id) => {
let db = db.ok_or_else(|| {
ApiError::InvalidArgument(
"db connection required to resolve dataset UUID".to_string(),
)
})?;
let dataset = db.get_dataset(*id).await.map_err(|e| {
ApiError::InvalidArgument(format!("Dataset {id} lookup failed: {e}"))
})?;
let dataset = dataset
.ok_or_else(|| ApiError::InvalidArgument(format!("Dataset {id} not found")))?;
if dataset.owner_id != owner_id {
return Err(ApiError::InvalidArgument(format!(
"Dataset {id} not owned by the requesting user"
)));
}
Ok(dataset.name)
}
}
}
}
#[derive(Debug, Clone)]
pub enum ForgetTarget {
Item { data_id: Uuid, dataset: DatasetRef },
Dataset { dataset: DatasetRef },
All,
DatasetMemoryOnly { dataset: DatasetRef },
DataItemMemoryOnly { data_id: Uuid, dataset: DatasetRef },
}
#[derive(Debug, Clone)]
pub struct ForgetResult {
pub target: String,
pub delete_result: DeleteResult,
}
pub async fn forget(
target: ForgetTarget,
owner_id: Uuid,
delete_service: &DeleteService,
db: Option<&dyn IngestDb>,
) -> Result<ForgetResult, ApiError> {
#[cfg(feature = "telemetry")]
{
let (target_label, dataset_dbg, data_id_dbg) = match &target {
ForgetTarget::Item { data_id, dataset } => {
("data_item", format!("{dataset:?}"), data_id.to_string())
}
ForgetTarget::Dataset { dataset } => ("dataset", format!("{dataset:?}"), String::new()),
ForgetTarget::All => ("everything", String::new(), String::new()),
ForgetTarget::DatasetMemoryOnly { dataset } => {
("dataset_memory_only", format!("{dataset:?}"), String::new())
}
ForgetTarget::DataItemMemoryOnly { data_id, dataset } => (
"data_item_memory_only",
format!("{dataset:?}"),
data_id.to_string(),
),
};
cognee_telemetry::send_telemetry(
"cognee.forget",
owner_id,
Some(serde_json::json!({
"target": target_label,
"dataset": dataset_dbg,
"data_id": data_id_dbg,
"cognee_version": env!("CARGO_PKG_VERSION"),
})),
);
}
let (scope, memory_only, label) = match target {
ForgetTarget::Item { data_id, dataset } => {
let dataset_name = dataset.to_name(owner_id, db).await?;
let scope = DeleteScope::Data {
owner_id,
data_id,
dataset_name: Some(dataset_name),
delete_dataset_if_empty: false,
};
(scope, false, format!("item:{data_id}"))
}
ForgetTarget::Dataset { dataset } => {
let dataset_name = dataset.to_name(owner_id, db).await?;
if let Some(db) = db {
let _dataset = db
.get_dataset_by_name(&dataset_name, owner_id, None)
.await
.map_err(|e| {
ApiError::InvalidArgument(format!(
"Dataset '{dataset_name}' not found: {e}"
))
})?;
}
let scope = DeleteScope::Dataset {
owner_id,
dataset_name: dataset_name.clone(),
};
(scope, false, format!("dataset:{dataset_name}"))
}
ForgetTarget::All => {
let scope = DeleteScope::User { owner_id };
(scope, false, "all".to_string())
}
ForgetTarget::DatasetMemoryOnly { dataset } => {
let dataset_name = dataset.to_name(owner_id, db).await?;
let scope = DeleteScope::Dataset {
owner_id,
dataset_name: dataset_name.clone(),
};
(scope, true, format!("dataset_memory_only:{dataset_name}"))
}
ForgetTarget::DataItemMemoryOnly { data_id, dataset } => {
let dataset_name = dataset.to_name(owner_id, db).await?;
let scope = DeleteScope::Data {
owner_id,
data_id,
dataset_name: Some(dataset_name),
delete_dataset_if_empty: false,
};
(scope, true, format!("data_item_memory_only:{data_id}"))
}
};
let request = build_delete_request(scope, memory_only);
let delete_result = delete_service.execute(&request).await?;
Ok(ForgetResult {
target: label,
delete_result,
})
}
fn build_delete_request(scope: DeleteScope, memory_only: bool) -> DeleteRequest {
DeleteRequest {
scope,
mode: DeleteMode::Soft,
memory_only,
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
reason = "test code — panics are acceptable failures"
)]
mod tests {
use super::*;
#[test]
fn forget_target_debug_format() {
let target = ForgetTarget::All;
let debug_str = format!("{target:?}");
assert!(debug_str.contains("All"));
}
#[test]
fn forget_target_item_holds_fields() {
let id = Uuid::new_v4();
let target = ForgetTarget::Item {
data_id: id,
dataset: DatasetRef::Name("test_ds".to_string()),
};
match target {
ForgetTarget::Item { data_id, dataset } => {
assert_eq!(data_id, id);
match dataset {
DatasetRef::Name(name) => assert_eq!(name, "test_ds"),
_ => panic!("expected Name variant"),
}
}
_ => panic!("expected Item variant"),
}
}
#[tokio::test]
async fn dataset_ref_name_passthrough() {
let owner_id = Uuid::new_v4();
let dref = DatasetRef::Name("my_ds".to_string());
let resolved = dref.to_name(owner_id, None).await.expect("passthrough ok");
assert_eq!(resolved, "my_ds");
}
#[tokio::test]
async fn dataset_ref_id_requires_db() {
let owner_id = Uuid::new_v4();
let dref = DatasetRef::Id(Uuid::new_v4());
let result = dref.to_name(owner_id, None).await;
match result {
Err(ApiError::InvalidArgument(msg)) => {
assert!(
msg.contains("db connection required"),
"unexpected msg: {msg}"
);
}
other => panic!("expected InvalidArgument, got {other:?}"),
}
}
#[test]
fn forget_target_dataset_uuid_variant_debug() {
let id = Uuid::new_v4();
let target = ForgetTarget::Dataset {
dataset: DatasetRef::Id(id),
};
let dbg = format!("{target:?}");
assert!(dbg.contains("Dataset"), "debug: {dbg}");
assert!(dbg.contains("Id"), "debug: {dbg}");
}
}