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