this repo has no description
1use crate::api::error::ApiError;
2use crate::api::repo::record::utils::{CommitParams, RecordOp, commit_and_log};
3use crate::api::repo::record::write::{CommitInfo, prepare_repo_write};
4use crate::delegation::{self, DelegationActionType};
5use crate::repo::tracking::TrackingBlockStore;
6use crate::state::AppState;
7use crate::types::{AtIdentifier, Nsid, Rkey};
8use axum::{
9 Json,
10 extract::State,
11 http::{HeaderMap, StatusCode},
12 response::{IntoResponse, Response},
13};
14use cid::Cid;
15use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
16use serde::{Deserialize, Serialize};
17use serde_json::json;
18use std::str::FromStr;
19use std::sync::Arc;
20use tracing::error;
21
22#[derive(Deserialize)]
23pub struct DeleteRecordInput {
24 pub repo: AtIdentifier,
25 pub collection: Nsid,
26 pub rkey: Rkey,
27 #[serde(rename = "swapRecord")]
28 pub swap_record: Option<String>,
29 #[serde(rename = "swapCommit")]
30 pub swap_commit: Option<String>,
31}
32
33#[derive(Serialize)]
34#[serde(rename_all = "camelCase")]
35pub struct DeleteRecordOutput {
36 #[serde(skip_serializing_if = "Option::is_none")]
37 pub commit: Option<CommitInfo>,
38}
39
40pub async fn delete_record(
41 State(state): State<AppState>,
42 headers: HeaderMap,
43 axum::extract::OriginalUri(uri): axum::extract::OriginalUri,
44 Json(input): Json<DeleteRecordInput>,
45) -> Response {
46 let auth = match prepare_repo_write(
47 &state,
48 &headers,
49 &input.repo,
50 "POST",
51 &crate::util::build_full_url(&uri.to_string()),
52 )
53 .await
54 {
55 Ok(res) => res,
56 Err(err_res) => return err_res,
57 };
58
59 if let Err(e) = crate::auth::scope_check::check_repo_scope(
60 auth.is_oauth,
61 auth.scope.as_deref(),
62 crate::oauth::RepoAction::Delete,
63 &input.collection,
64 ) {
65 return e;
66 }
67
68 if crate::util::is_account_migrated(&state.db, &auth.did)
69 .await
70 .unwrap_or(false)
71 {
72 return ApiError::AccountMigrated.into_response();
73 }
74
75 let did = auth.did;
76 let user_id = auth.user_id;
77 let current_root_cid = auth.current_root_cid;
78 let controller_did = auth.controller_did;
79
80 if let Some(swap_commit) = &input.swap_commit
81 && Cid::from_str(swap_commit).ok() != Some(current_root_cid)
82 {
83 return ApiError::InvalidSwap(Some("Repo has been modified".into())).into_response();
84 }
85 let tracking_store = TrackingBlockStore::new(state.block_store.clone());
86 let commit_bytes = match tracking_store.get(¤t_root_cid).await {
87 Ok(Some(b)) => b,
88 _ => return ApiError::InternalError(Some("Commit block not found".into())).into_response(),
89 };
90 let commit = match Commit::from_cbor(&commit_bytes) {
91 Ok(c) => c,
92 _ => return ApiError::InternalError(Some("Failed to parse commit".into())).into_response(),
93 };
94 let mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
95 let key = format!("{}/{}", input.collection, input.rkey);
96 if let Some(swap_record_str) = &input.swap_record {
97 let expected_cid = Cid::from_str(swap_record_str).ok();
98 let actual_cid = mst.get(&key).await.ok().flatten();
99 if expected_cid != actual_cid {
100 return ApiError::InvalidSwap(Some("Record has been modified or does not exist".into()))
101 .into_response();
102 }
103 }
104 let prev_record_cid = mst.get(&key).await.ok().flatten();
105 if prev_record_cid.is_none() {
106 return (StatusCode::OK, Json(DeleteRecordOutput { commit: None })).into_response();
107 }
108 let new_mst = match mst.delete(&key).await {
109 Ok(m) => m,
110 Err(e) => {
111 error!("Failed to delete from MST: {:?}", e);
112 return ApiError::InternalError(Some(format!("Failed to delete from MST: {:?}", e)))
113 .into_response();
114 }
115 };
116 let new_mst_root = match new_mst.persist().await {
117 Ok(c) => c,
118 Err(e) => {
119 error!("Failed to persist MST: {:?}", e);
120 return ApiError::InternalError(Some("Failed to persist MST".into())).into_response();
121 }
122 };
123 let collection_for_audit = input.collection.to_string();
124 let rkey_for_audit = input.rkey.to_string();
125 let op = RecordOp::Delete {
126 collection: input.collection.to_string(),
127 rkey: rkey_for_audit.clone(),
128 prev: prev_record_cid,
129 };
130 let mut relevant_blocks = std::collections::BTreeMap::new();
131 if new_mst
132 .blocks_for_path(&key, &mut relevant_blocks)
133 .await
134 .is_err()
135 {
136 return ApiError::InternalError(Some("Failed to get new MST blocks for path".into()))
137 .into_response();
138 }
139 if mst
140 .blocks_for_path(&key, &mut relevant_blocks)
141 .await
142 .is_err()
143 {
144 return ApiError::InternalError(Some("Failed to get old MST blocks for path".into()))
145 .into_response();
146 }
147 let mut written_cids = tracking_store.get_all_relevant_cids();
148 for cid in relevant_blocks.keys() {
149 if !written_cids.contains(cid) {
150 written_cids.push(*cid);
151 }
152 }
153 let written_cids_str = written_cids
154 .iter()
155 .map(|c| c.to_string())
156 .collect::<Vec<_>>();
157 let commit_result = match commit_and_log(
158 &state,
159 CommitParams {
160 did: &did,
161 user_id,
162 current_root_cid: Some(current_root_cid),
163 prev_data_cid: Some(commit.data),
164 new_mst_root,
165 ops: vec![op],
166 blocks_cids: &written_cids_str,
167 blobs: &[],
168 },
169 )
170 .await
171 {
172 Ok(res) => res,
173 Err(e) => return ApiError::InternalError(Some(e)).into_response(),
174 };
175
176 if let Some(ref controller) = controller_did {
177 let _ = delegation::log_delegation_action(
178 &state.db,
179 &did,
180 controller,
181 Some(controller),
182 DelegationActionType::RepoWrite,
183 Some(json!({
184 "action": "delete",
185 "collection": collection_for_audit,
186 "rkey": rkey_for_audit
187 })),
188 None,
189 None,
190 )
191 .await;
192 }
193
194 (
195 StatusCode::OK,
196 Json(DeleteRecordOutput {
197 commit: Some(CommitInfo {
198 cid: commit_result.commit_cid.to_string(),
199 rev: commit_result.rev,
200 }),
201 }),
202 )
203 .into_response()
204}