mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-26 12:14:15 +00:00
Initial firehose connections
This commit is contained in:
+87
-263
@@ -1,17 +1,15 @@
|
||||
use crate::api::repo::record::utils::{commit_and_log, RecordOp};
|
||||
use crate::repo::tracking::TrackingBlockStore;
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use cid::Cid;
|
||||
use jacquard::types::{
|
||||
did::Did,
|
||||
integer::LimitedU32,
|
||||
string::{Nsid, Tid},
|
||||
};
|
||||
use jacquard::types::string::Nsid;
|
||||
use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
@@ -98,10 +96,7 @@ pub async fn apply_writes(
|
||||
.unwrap_or(None);
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Some(row) => (
|
||||
row.did,
|
||||
row.key_bytes,
|
||||
),
|
||||
Some(row) => (row.did, row.key_bytes),
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
@@ -143,12 +138,11 @@ pub async fn apply_writes(
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
|
||||
let user_id: uuid::Uuid = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let user_id: uuid::Uuid = match user_query {
|
||||
Ok(Some(row)) => row.id,
|
||||
.await
|
||||
{
|
||||
Ok(Some(id)) => id,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
@@ -158,45 +152,34 @@ pub async fn apply_writes(
|
||||
}
|
||||
};
|
||||
|
||||
let repo_root_query = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let current_root_cid = match repo_root_query {
|
||||
Ok(Some(row)) => {
|
||||
let cid_str: String = row.repo_root_cid;
|
||||
match Cid::from_str(&cid_str) {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Invalid repo root CID"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let root_cid_str: String =
|
||||
match sqlx::query_scalar!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
{
|
||||
Ok(Some(cid_str)) => cid_str,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Repo root not found"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
};
|
||||
|
||||
let current_root_cid = match Cid::from_str(&root_cid_str) {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Repo root not found"})),
|
||||
Json(json!({"error": "InternalError", "message": "Invalid repo root CID"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(swap_commit) = &input.swap_commit {
|
||||
let swap_cid = match Cid::from_str(swap_commit) {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidSwap", "message": "Invalid swapCommit CID"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
if swap_cid != current_root_cid {
|
||||
if Cid::from_str(swap_commit).ok() != Some(current_root_cid) {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
Json(json!({"error": "InvalidSwap", "message": "Repo has been modified"})),
|
||||
@@ -205,43 +188,34 @@ pub async fn apply_writes(
|
||||
}
|
||||
}
|
||||
|
||||
let commit_bytes = match state.block_store.get(¤t_root_cid).await {
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
|
||||
let commit_bytes = match tracking_store.get(¤t_root_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
Ok(None) => {
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Commit block not found"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to load commit block: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
let commit = match Commit::from_cbor(&commit_bytes) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to parse commit: {:?}", e);
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
Json(json!({"error": "InternalError", "message": "Failed to parse commit"})),
|
||||
)
|
||||
.into_response();
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
let mst_root = commit.data;
|
||||
let store = Arc::new(state.block_store.clone());
|
||||
let mut mst = Mst::load(store.clone(), mst_root, None);
|
||||
let mut mst = Mst::load(Arc::new(tracking_store.clone()), commit.data, None);
|
||||
|
||||
let mut results: Vec<WriteResult> = Vec::new();
|
||||
let mut record_ops: Vec<(String, String, Option<String>)> = Vec::new();
|
||||
let mut ops: Vec<RecordOp> = Vec::new();
|
||||
|
||||
for write in &input.writes {
|
||||
match write {
|
||||
@@ -250,248 +224,98 @@ pub async fn apply_writes(
|
||||
rkey,
|
||||
value,
|
||||
} => {
|
||||
let collection_nsid = match collection.parse::<Nsid>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidCollection"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let rkey = rkey
|
||||
.clone()
|
||||
.unwrap_or_else(|| Utc::now().format("%Y%m%d%H%M%S%f").to_string());
|
||||
|
||||
let mut record_bytes = Vec::new();
|
||||
if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, value) {
|
||||
error!("Error serializing record: {:?}", e);
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
serde_ipld_dagcbor::to_writer(&mut record_bytes, value).unwrap();
|
||||
let record_cid = tracking_store.put(&record_bytes).await.unwrap();
|
||||
|
||||
let record_cid = match state.block_store.put(&record_bytes).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to save record block: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let key = format!("{}/{}", collection_nsid, rkey);
|
||||
mst = match mst.add(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to add to MST: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let key = format!("{}/{}", collection.parse::<Nsid>().unwrap(), rkey);
|
||||
mst = mst.add(&key, record_cid).await.unwrap();
|
||||
|
||||
let uri = format!("at://{}/{}/{}", did, collection, rkey);
|
||||
results.push(WriteResult::CreateResult {
|
||||
uri: uri.clone(),
|
||||
uri,
|
||||
cid: record_cid.to_string(),
|
||||
});
|
||||
record_ops.push((collection.clone(), rkey, Some(record_cid.to_string())));
|
||||
ops.push(RecordOp::Create {
|
||||
collection: collection.clone(),
|
||||
rkey,
|
||||
cid: record_cid,
|
||||
});
|
||||
}
|
||||
WriteOp::Update {
|
||||
collection,
|
||||
rkey,
|
||||
value,
|
||||
} => {
|
||||
let collection_nsid = match collection.parse::<Nsid>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidCollection"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let mut record_bytes = Vec::new();
|
||||
if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, value) {
|
||||
error!("Error serializing record: {:?}", e);
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
serde_ipld_dagcbor::to_writer(&mut record_bytes, value).unwrap();
|
||||
let record_cid = tracking_store.put(&record_bytes).await.unwrap();
|
||||
|
||||
let record_cid = match state.block_store.put(&record_bytes).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to save record block: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let key = format!("{}/{}", collection_nsid, rkey);
|
||||
mst = match mst.update(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to update MST: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let key = format!("{}/{}", collection.parse::<Nsid>().unwrap(), rkey);
|
||||
mst = mst.update(&key, record_cid).await.unwrap();
|
||||
|
||||
let uri = format!("at://{}/{}/{}", did, collection, rkey);
|
||||
results.push(WriteResult::UpdateResult {
|
||||
uri: uri.clone(),
|
||||
uri,
|
||||
cid: record_cid.to_string(),
|
||||
});
|
||||
record_ops.push((collection.clone(), rkey.clone(), Some(record_cid.to_string())));
|
||||
ops.push(RecordOp::Update {
|
||||
collection: collection.clone(),
|
||||
rkey: rkey.clone(),
|
||||
cid: record_cid,
|
||||
});
|
||||
}
|
||||
WriteOp::Delete { collection, rkey } => {
|
||||
let collection_nsid = match collection.parse::<Nsid>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidCollection"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let key = format!("{}/{}", collection_nsid, rkey);
|
||||
mst = match mst.delete(&key).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to delete from MST: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
let key = format!("{}/{}", collection.parse::<Nsid>().unwrap(), rkey);
|
||||
mst = mst.delete(&key).await.unwrap();
|
||||
|
||||
results.push(WriteResult::DeleteResult {});
|
||||
record_ops.push((collection.clone(), rkey.clone(), None));
|
||||
ops.push(RecordOp::Delete {
|
||||
collection: collection.clone(),
|
||||
rkey: rkey.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let new_mst_root = match mst.persist().await {
|
||||
Ok(c) => c,
|
||||
let new_mst_root = mst.persist().await.unwrap();
|
||||
let written_cids = tracking_store.get_written_cids();
|
||||
let written_cids_str = written_cids
|
||||
.iter()
|
||||
.map(|c| c.to_string())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let commit_res = match commit_and_log(
|
||||
&state,
|
||||
&did,
|
||||
user_id,
|
||||
Some(current_root_cid),
|
||||
new_mst_root,
|
||||
ops,
|
||||
&written_cids_str,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(res) => res,
|
||||
Err(e) => {
|
||||
error!("Failed to persist MST: {:?}", e);
|
||||
error!("Commit failed: {}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
Json(json!({"error": "InternalError", "message": "Failed to commit changes"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let did_obj = match Did::new(&did) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Invalid DID"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
let new_commit = Commit::new_unsigned(did_obj, new_mst_root, rev.clone(), Some(current_root_cid));
|
||||
|
||||
let new_commit_bytes = match new_commit.to_cbor() {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
error!("Failed to serialize new commit: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let new_root_cid = match state.block_store.put(&new_commit_bytes).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to save new commit: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let update_repo = sqlx::query!("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2", new_root_cid.to_string(), user_id)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = update_repo {
|
||||
error!("Failed to update repo root in DB: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
for (collection, rkey, record_cid) in record_ops {
|
||||
match record_cid {
|
||||
Some(cid) => {
|
||||
let _ = sqlx::query!(
|
||||
"INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()",
|
||||
user_id,
|
||||
collection,
|
||||
rkey,
|
||||
cid
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
}
|
||||
None => {
|
||||
let _ = sqlx::query!(
|
||||
"DELETE FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3",
|
||||
user_id,
|
||||
collection,
|
||||
rkey
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(ApplyWritesOutput {
|
||||
commit: CommitInfo {
|
||||
cid: new_root_cid.to_string(),
|
||||
rev: rev.to_string(),
|
||||
cid: commit_res.commit_cid.to_string(),
|
||||
rev: commit_res.rev,
|
||||
},
|
||||
results,
|
||||
}),
|
||||
|
||||
+44
-165
@@ -1,16 +1,15 @@
|
||||
use crate::api::repo::record::utils::{commit_and_log, RecordOp};
|
||||
use crate::api::repo::record::write::prepare_repo_write;
|
||||
use crate::repo::tracking::TrackingBlockStore;
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use cid::Cid;
|
||||
use jacquard::types::{
|
||||
did::Did,
|
||||
integer::LimitedU32,
|
||||
string::{Nsid, Tid},
|
||||
};
|
||||
use jacquard::types::string::Nsid;
|
||||
use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
@@ -31,122 +30,58 @@ pub struct DeleteRecordInput {
|
||||
|
||||
pub async fn delete_record(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
headers: HeaderMap,
|
||||
Json(input): Json<DeleteRecordInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
let (did, user_id, current_root_cid) =
|
||||
match prepare_repo_write(&state, &headers, &input.repo).await {
|
||||
Ok(res) => res,
|
||||
Err(err_res) => return err_res,
|
||||
};
|
||||
|
||||
let session = sqlx::query!(
|
||||
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1",
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Some(row) => (
|
||||
row.did,
|
||||
row.key_bytes,
|
||||
),
|
||||
None => {
|
||||
if let Some(swap_commit) = &input.swap_commit {
|
||||
if Cid::from_str(swap_commit).ok() != Some(current_root_cid) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
StatusCode::CONFLICT,
|
||||
Json(json!({"error": "InvalidSwap", "message": "Repo has been modified"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if input.repo != did {
|
||||
return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response();
|
||||
}
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
|
||||
let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let user_id: uuid::Uuid = match user_query {
|
||||
Ok(Some(row)) => row.id,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "User not found"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let repo_root_query = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let current_root_cid = match repo_root_query {
|
||||
Ok(Some(row)) => {
|
||||
let cid_str: String = row.repo_root_cid;
|
||||
Cid::from_str(&cid_str).ok()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if current_root_cid.is_none() {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Repo root not found"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let current_root_cid = current_root_cid.unwrap();
|
||||
|
||||
let commit_bytes = match state.block_store.get(¤t_root_cid).await {
|
||||
let commit_bytes = match tracking_store.get(¤t_root_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
Ok(None) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response(),
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to load commit block: {:?}", e)}))).into_response(),
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response(),
|
||||
};
|
||||
|
||||
let commit = match Commit::from_cbor(&commit_bytes) {
|
||||
Ok(c) => c,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to parse commit: {:?}", e)}))).into_response(),
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to parse commit"}))).into_response(),
|
||||
};
|
||||
|
||||
let mst_root = commit.data;
|
||||
let store = Arc::new(state.block_store.clone());
|
||||
let mst = Mst::load(store.clone(), mst_root, None);
|
||||
|
||||
let mst = Mst::load(
|
||||
Arc::new(tracking_store.clone()),
|
||||
commit.data,
|
||||
None,
|
||||
);
|
||||
let collection_nsid = match input.collection.parse::<Nsid>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidCollection"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(),
|
||||
};
|
||||
|
||||
let key = format!("{}/{}", collection_nsid, input.rkey);
|
||||
|
||||
// TODO: Check swapRecord if provided? Skipping for brevity/robustness
|
||||
if let Some(swap_record_str) = &input.swap_record {
|
||||
let expected_cid = Cid::from_str(swap_record_str).ok();
|
||||
let actual_cid = mst.get(&key).await.ok().flatten();
|
||||
if expected_cid != actual_cid {
|
||||
return (StatusCode::CONFLICT, Json(json!({"error": "InvalidSwap", "message": "Record has been modified or does not exist"}))).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
if mst.get(&key).await.ok().flatten().is_none() {
|
||||
return (StatusCode::OK, Json(json!({}))).into_response();
|
||||
}
|
||||
|
||||
let new_mst = match mst.delete(&key).await {
|
||||
Ok(m) => m,
|
||||
@@ -160,73 +95,17 @@ pub async fn delete_record(
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to persist MST: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Failed to persist MST"})),
|
||||
)
|
||||
.into_response();
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to persist MST"}))).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let did_obj = match Did::new(&did) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Invalid DID"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let op = RecordOp::Delete { collection: input.collection, rkey: input.rkey };
|
||||
let written_cids = tracking_store.get_written_cids();
|
||||
let written_cids_str = written_cids.iter().map(|c| c.to_string()).collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = commit_and_log(&state, &did, user_id, Some(current_root_cid), new_mst_root, vec![op], &written_cids_str).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": e}))).into_response();
|
||||
};
|
||||
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
|
||||
let new_commit = Commit::new_unsigned(did_obj, new_mst_root, rev, Some(current_root_cid));
|
||||
|
||||
let new_commit_bytes =
|
||||
match new_commit.to_cbor() {
|
||||
Ok(b) => b,
|
||||
Err(_e) => return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(
|
||||
json!({"error": "InternalError", "message": "Failed to serialize new commit"}),
|
||||
),
|
||||
)
|
||||
.into_response(),
|
||||
};
|
||||
|
||||
let new_root_cid = match state.block_store.put(&new_commit_bytes).await {
|
||||
Ok(c) => c,
|
||||
Err(_e) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Failed to save new commit"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let update_repo = sqlx::query!("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2", new_root_cid.to_string(), user_id)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = update_repo {
|
||||
error!("Failed to update repo root in DB: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Failed to update repo root in DB"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let record_delete =
|
||||
sqlx::query!("DELETE FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3", user_id, input.collection, input.rkey)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = record_delete {
|
||||
error!("Error deleting record index: {:?}", e);
|
||||
}
|
||||
|
||||
(StatusCode::OK, Json(json!({}))).into_response()
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
pub mod batch;
|
||||
pub mod delete;
|
||||
pub mod read;
|
||||
pub mod utils;
|
||||
pub mod write;
|
||||
|
||||
pub use batch::apply_writes;
|
||||
pub use delete::{DeleteRecordInput, delete_record};
|
||||
pub use read::{GetRecordInput, ListRecordsInput, ListRecordsOutput, get_record, list_records};
|
||||
pub use utils::*;
|
||||
pub use write::{
|
||||
CreateRecordInput, CreateRecordOutput, PutRecordInput, PutRecordOutput, create_record,
|
||||
put_record,
|
||||
put_record, prepare_repo_write,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
use crate::state::AppState;
|
||||
use cid::Cid;
|
||||
use jacquard::types::{did::Did, integer::LimitedU32, string::Tid};
|
||||
use jacquard_repo::commit::Commit;
|
||||
use jacquard_repo::storage::BlockStore;
|
||||
use serde_json::json;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub enum RecordOp {
|
||||
Create { collection: String, rkey: String, cid: Cid },
|
||||
Update { collection: String, rkey: String, cid: Cid },
|
||||
Delete { collection: String, rkey: String },
|
||||
}
|
||||
|
||||
pub struct CommitResult {
|
||||
pub commit_cid: Cid,
|
||||
pub rev: String,
|
||||
}
|
||||
|
||||
pub async fn commit_and_log(
|
||||
state: &AppState,
|
||||
did: &str,
|
||||
user_id: Uuid,
|
||||
current_root_cid: Option<Cid>,
|
||||
new_mst_root: Cid,
|
||||
ops: Vec<RecordOp>,
|
||||
blocks_cids: &Vec<String>,
|
||||
) -> Result<CommitResult, String> {
|
||||
let did_obj = Did::new(did).map_err(|e| format!("Invalid DID: {}", e))?;
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
|
||||
let new_commit = Commit::new_unsigned(did_obj, new_mst_root, rev.clone(), current_root_cid);
|
||||
|
||||
let new_commit_bytes = new_commit.to_cbor().map_err(|e| format!("Failed to serialize commit: {:?}", e))?;
|
||||
|
||||
let new_root_cid = state.block_store.put(&new_commit_bytes).await
|
||||
.map_err(|e| format!("Failed to save commit block: {:?}", e))?;
|
||||
|
||||
sqlx::query!("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2", new_root_cid.to_string(), user_id)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (repos): {}", e))?;
|
||||
|
||||
for op in &ops {
|
||||
match op {
|
||||
RecordOp::Create { collection, rkey, cid } | RecordOp::Update { collection, rkey, cid } => {
|
||||
sqlx::query!(
|
||||
"INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()",
|
||||
user_id,
|
||||
collection,
|
||||
rkey,
|
||||
cid.to_string()
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (records): {}", e))?;
|
||||
}
|
||||
RecordOp::Delete { collection, rkey } => {
|
||||
sqlx::query!(
|
||||
"DELETE FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3",
|
||||
user_id,
|
||||
collection,
|
||||
rkey
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (records): {}", e))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let ops_json = ops.iter().map(|op| {
|
||||
match op {
|
||||
RecordOp::Create { collection, rkey, cid } => json!({
|
||||
"action": "create",
|
||||
"path": format!("{}/{}", collection, rkey),
|
||||
"cid": cid.to_string()
|
||||
}),
|
||||
RecordOp::Update { collection, rkey, cid } => json!({
|
||||
"action": "update",
|
||||
"path": format!("{}/{}", collection, rkey),
|
||||
"cid": cid.to_string()
|
||||
}),
|
||||
RecordOp::Delete { collection, rkey } => json!({
|
||||
"action": "delete",
|
||||
"path": format!("{}/{}", collection, rkey),
|
||||
"cid": null
|
||||
}),
|
||||
}
|
||||
}).collect::<Vec<_>>();
|
||||
|
||||
let event_type = "commit";
|
||||
let prev_cid_str = current_root_cid.map(|c| c.to_string());
|
||||
|
||||
let seq_row = sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO repo_seq (did, event_type, commit_cid, prev_cid, ops, blobs, blocks_cids)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING seq
|
||||
"#,
|
||||
did,
|
||||
event_type,
|
||||
new_root_cid.to_string(),
|
||||
prev_cid_str,
|
||||
json!(ops_json),
|
||||
&[] as &[String],
|
||||
blocks_cids,
|
||||
)
|
||||
.fetch_one(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (repo_seq): {}", e))?;
|
||||
|
||||
sqlx::query(
|
||||
&format!("NOTIFY repo_updates, '{}'", seq_row.seq)
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await
|
||||
.map_err(|e| format!("DB Error (notify): {}", e))?;
|
||||
|
||||
Ok(CommitResult {
|
||||
commit_cid: new_root_cid,
|
||||
rev: rev.to_string(),
|
||||
})
|
||||
}
|
||||
+190
-523
@@ -1,23 +1,115 @@
|
||||
use crate::api::repo::record::utils::{commit_and_log, RecordOp};
|
||||
use crate::repo::tracking::TrackingBlockStore;
|
||||
use crate::state::AppState;
|
||||
use axum::{
|
||||
Json,
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
http::{HeaderMap, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use chrono::Utc;
|
||||
use cid::Cid;
|
||||
use jacquard::types::{
|
||||
did::Did,
|
||||
integer::LimitedU32,
|
||||
string::{Nsid, Tid},
|
||||
};
|
||||
use jacquard::types::string::Nsid;
|
||||
use jacquard_repo::{commit::Commit, mst::Mst, storage::BlockStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use tracing::error;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub async fn prepare_repo_write(
|
||||
state: &AppState,
|
||||
headers: &HeaderMap,
|
||||
repo_did: &str,
|
||||
) -> Result<(String, Uuid, Cid), Response> {
|
||||
let auth_header = headers.get("Authorization").ok_or_else(|| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
let token = auth_header
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
|
||||
let session = sqlx::query!(
|
||||
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1",
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching session: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response()
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
crate::auth::verify_token(&token, &session.key_bytes).map_err(|_| {
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
if repo_did != session.did {
|
||||
return Err((
|
||||
StatusCode::FORBIDDEN,
|
||||
Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"})),
|
||||
)
|
||||
.into_response());
|
||||
}
|
||||
|
||||
let user_id = sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", session.did)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching user: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response()
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "User not found"})),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
let root_cid_str: String =
|
||||
sqlx::query_scalar!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("DB error fetching repo root: {}", e);
|
||||
(StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError"}))).into_response()
|
||||
})?
|
||||
.ok_or_else(|| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Repo root not found"})),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
let current_root_cid = Cid::from_str(&root_cid_str).map_err(|_| {
|
||||
(
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Invalid repo root CID"})),
|
||||
)
|
||||
.into_response()
|
||||
})?;
|
||||
|
||||
Ok((session.did, user_id, current_root_cid))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
@@ -40,145 +132,47 @@ pub struct CreateRecordOutput {
|
||||
|
||||
pub async fn create_record(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
headers: HeaderMap,
|
||||
Json(input): Json<CreateRecordInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
let (did, user_id, current_root_cid) =
|
||||
match prepare_repo_write(&state, &headers, &input.repo).await {
|
||||
Ok(res) => res,
|
||||
Err(err_res) => return err_res,
|
||||
};
|
||||
|
||||
let session = sqlx::query!(
|
||||
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1",
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Some(row) => (
|
||||
row.did,
|
||||
row.key_bytes,
|
||||
),
|
||||
None => {
|
||||
if let Some(swap_commit) = &input.swap_commit {
|
||||
if Cid::from_str(swap_commit).ok() != Some(current_root_cid) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
StatusCode::CONFLICT,
|
||||
Json(json!({"error": "InvalidSwap", "message": "Repo has been modified"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if input.repo != did {
|
||||
return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response();
|
||||
}
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
|
||||
let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let user_id: uuid::Uuid = match user_query {
|
||||
Ok(Some(row)) => row.id,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "User not found"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let repo_root_query = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let current_root_cid = match repo_root_query {
|
||||
Ok(Some(row)) => {
|
||||
let cid_str: String = row.repo_root_cid;
|
||||
Cid::from_str(&cid_str).ok()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if current_root_cid.is_none() {
|
||||
error!("Repo root not found for user {}", did);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Repo root not found"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let current_root_cid = current_root_cid.unwrap();
|
||||
|
||||
let commit_bytes = match state.block_store.get(¤t_root_cid).await {
|
||||
let commit_bytes = match tracking_store.get(¤t_root_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
Ok(None) => {
|
||||
error!("Commit block not found: {}", current_root_cid);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to load commit block: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response(),
|
||||
};
|
||||
|
||||
let commit = match Commit::from_cbor(&commit_bytes) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to parse commit: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to parse commit"}))).into_response(),
|
||||
};
|
||||
|
||||
let mst_root = commit.data;
|
||||
let store = Arc::new(state.block_store.clone());
|
||||
let mst = Mst::load(store.clone(), mst_root, None);
|
||||
let mst = Mst::load(
|
||||
Arc::new(tracking_store.clone()),
|
||||
commit.data,
|
||||
None,
|
||||
);
|
||||
|
||||
let collection_nsid = match input.collection.parse::<Nsid>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidCollection"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(),
|
||||
};
|
||||
|
||||
let rkey = input
|
||||
.rkey
|
||||
.unwrap_or_else(|| Utc::now().format("%Y%m%d%H%M%S%f").to_string());
|
||||
|
||||
if input.validate.unwrap_or(true) {
|
||||
if input.collection == "app.bsky.feed.post" {
|
||||
if input.record.get("text").is_none() || input.record.get("createdAt").is_none() {
|
||||
@@ -191,130 +185,39 @@ pub async fn create_record(
|
||||
}
|
||||
}
|
||||
|
||||
let mut record_bytes = Vec::new();
|
||||
if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record) {
|
||||
error!("Error serializing record: {:?}", e);
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let rkey = input.rkey.unwrap_or_else(|| Utc::now().format("%Y%m%d%H%M%S%f").to_string());
|
||||
|
||||
let record_cid = match state.block_store.put(&record_bytes).await {
|
||||
let mut record_bytes = Vec::new();
|
||||
if serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record).is_err() {
|
||||
return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"}))).into_response();
|
||||
}
|
||||
let record_cid = match tracking_store.put(&record_bytes).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to save record block: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to save record block"}))).into_response(),
|
||||
};
|
||||
|
||||
let key = format!("{}/{}", collection_nsid, rkey);
|
||||
let new_mst = match mst.add(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to add to MST: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to add to MST"}))).into_response(),
|
||||
};
|
||||
|
||||
let new_mst_root = match new_mst.persist().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to persist MST: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to persist MST"}))).into_response(),
|
||||
};
|
||||
|
||||
let did_obj = match Did::new(&did) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Invalid DID"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let op = RecordOp::Create { collection: input.collection.clone(), rkey: rkey.clone(), cid: record_cid };
|
||||
let written_cids = tracking_store.get_written_cids();
|
||||
let written_cids_str = written_cids.iter().map(|c| c.to_string()).collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = commit_and_log(&state, &did, user_id, Some(current_root_cid), new_mst_root, vec![op], &written_cids_str).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": e}))).into_response();
|
||||
};
|
||||
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
|
||||
let new_commit = Commit::new_unsigned(did_obj, new_mst_root, rev, Some(current_root_cid));
|
||||
|
||||
let new_commit_bytes = match new_commit.to_cbor() {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
error!("Failed to serialize new commit: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let new_root_cid = match state.block_store.put(&new_commit_bytes).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to save new commit: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let update_repo = sqlx::query!("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2", new_root_cid.to_string(), user_id)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = update_repo {
|
||||
error!("Failed to update repo root in DB: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let record_insert = sqlx::query!(
|
||||
"INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()",
|
||||
user_id,
|
||||
input.collection,
|
||||
rkey,
|
||||
record_cid.to_string()
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = record_insert {
|
||||
error!("Error inserting record index: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Failed to index record"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let output = CreateRecordOutput {
|
||||
uri: format!("at://{}/{}/{}", input.repo, input.collection, rkey),
|
||||
(StatusCode::OK, Json(CreateRecordOutput {
|
||||
uri: format!("at://{}/{}/{}", did, input.collection, rkey),
|
||||
cid: record_cid.to_string(),
|
||||
};
|
||||
(StatusCode::OK, Json(output)).into_response()
|
||||
})).into_response()
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -340,142 +243,42 @@ pub struct PutRecordOutput {
|
||||
|
||||
pub async fn put_record(
|
||||
State(state): State<AppState>,
|
||||
headers: axum::http::HeaderMap,
|
||||
headers: HeaderMap,
|
||||
Json(input): Json<PutRecordInput>,
|
||||
) -> Response {
|
||||
let auth_header = headers.get("Authorization");
|
||||
if auth_header.is_none() {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationRequired"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let token = auth_header
|
||||
.unwrap()
|
||||
.to_str()
|
||||
.unwrap_or("")
|
||||
.replace("Bearer ", "");
|
||||
let (did, user_id, current_root_cid) =
|
||||
match prepare_repo_write(&state, &headers, &input.repo).await {
|
||||
Ok(res) => res,
|
||||
Err(err_res) => return err_res,
|
||||
};
|
||||
|
||||
let session = sqlx::query!(
|
||||
"SELECT s.did, k.key_bytes FROM sessions s JOIN users u ON s.did = u.did JOIN user_keys k ON u.id = k.user_id WHERE s.access_jwt = $1",
|
||||
token
|
||||
)
|
||||
.fetch_optional(&state.db)
|
||||
.await
|
||||
.unwrap_or(None);
|
||||
|
||||
let (did, key_bytes) = match session {
|
||||
Some(row) => (
|
||||
row.did,
|
||||
row.key_bytes,
|
||||
),
|
||||
None => {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed"})),
|
||||
)
|
||||
.into_response();
|
||||
if let Some(swap_commit) = &input.swap_commit {
|
||||
if Cid::from_str(swap_commit).ok() != Some(current_root_cid) {
|
||||
return (StatusCode::CONFLICT, Json(json!({"error": "InvalidSwap", "message": "Repo has been modified"}))).into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
|
||||
return (
|
||||
StatusCode::UNAUTHORIZED,
|
||||
Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
if input.repo != did {
|
||||
return (StatusCode::FORBIDDEN, Json(json!({"error": "InvalidRepo", "message": "Repo does not match authenticated user"}))).into_response();
|
||||
}
|
||||
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
|
||||
|
||||
let user_query = sqlx::query!("SELECT id FROM users WHERE did = $1", did)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let user_id: uuid::Uuid = match user_query {
|
||||
Ok(Some(row)) => row.id,
|
||||
_ => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "User not found"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let repo_root_query = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
|
||||
.fetch_optional(&state.db)
|
||||
.await;
|
||||
|
||||
let current_root_cid = match repo_root_query {
|
||||
Ok(Some(row)) => {
|
||||
let cid_str: String = row.repo_root_cid;
|
||||
Cid::from_str(&cid_str).ok()
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if current_root_cid.is_none() {
|
||||
error!("Repo root not found for user {}", did);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Repo root not found"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let current_root_cid = current_root_cid.unwrap();
|
||||
|
||||
let commit_bytes = match state.block_store.get(¤t_root_cid).await {
|
||||
let commit_bytes = match tracking_store.get(¤t_root_cid).await {
|
||||
Ok(Some(b)) => b,
|
||||
Ok(None) => {
|
||||
error!("Commit block not found: {}", current_root_cid);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Commit block not found"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(e) => {
|
||||
error!("Failed to load commit block: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Failed to load commit block"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Commit block not found"}))).into_response(),
|
||||
};
|
||||
|
||||
let commit = match Commit::from_cbor(&commit_bytes) {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to parse commit: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Failed to parse commit"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to parse commit"}))).into_response(),
|
||||
};
|
||||
|
||||
let mst_root = commit.data;
|
||||
let store = Arc::new(state.block_store.clone());
|
||||
let mst = Mst::load(store.clone(), mst_root, None);
|
||||
|
||||
let mst = Mst::load(
|
||||
Arc::new(tracking_store.clone()),
|
||||
commit.data,
|
||||
None,
|
||||
);
|
||||
let collection_nsid = match input.collection.parse::<Nsid>() {
|
||||
Ok(n) => n,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidCollection"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
Err(_) => return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidCollection"}))).into_response(),
|
||||
};
|
||||
|
||||
let rkey = input.rkey.clone();
|
||||
let key = format!("{}/{}", collection_nsid, input.rkey);
|
||||
|
||||
if input.validate.unwrap_or(true) {
|
||||
if input.collection == "app.bsky.feed.post" {
|
||||
@@ -489,183 +292,47 @@ pub async fn put_record(
|
||||
}
|
||||
}
|
||||
|
||||
let mut record_bytes = Vec::new();
|
||||
if let Err(e) = serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record) {
|
||||
error!("Error serializing record: {:?}", e);
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let record_cid = match state.block_store.put(&record_bytes).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to save record block: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Failed to save record block"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let key = format!("{}/{}", collection_nsid, rkey);
|
||||
|
||||
let existing = match mst.get(&key).await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Failed to check MST key: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(
|
||||
json!({"error": "InternalError", "message": "Failed to check existing record"}),
|
||||
),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(swap_record_str) = &input.swap_record {
|
||||
let swap_record_cid = match Cid::from_str(swap_record_str) {
|
||||
Ok(c) => c,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(
|
||||
json!({"error": "InvalidSwapRecord", "message": "Invalid swapRecord CID"}),
|
||||
),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
match &existing {
|
||||
Some(current_cid) if *current_cid != swap_record_cid => {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
Json(json!({"error": "InvalidSwap", "message": "Record has been modified"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
None => {
|
||||
return (
|
||||
StatusCode::CONFLICT,
|
||||
Json(json!({"error": "InvalidSwap", "message": "Record does not exist"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
_ => {}
|
||||
let expected_cid = Cid::from_str(swap_record_str).ok();
|
||||
let actual_cid = mst.get(&key).await.ok().flatten();
|
||||
if expected_cid != actual_cid {
|
||||
return (StatusCode::CONFLICT, Json(json!({"error": "InvalidSwap", "message": "Record has been modified or does not exist"}))).into_response();
|
||||
}
|
||||
}
|
||||
|
||||
let new_mst = if existing.is_some() {
|
||||
match mst.update(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to update MST: {:?}", e);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to update MST: {:?}", e)}))).into_response();
|
||||
}
|
||||
}
|
||||
let existing_cid = mst.get(&key).await.ok().flatten();
|
||||
|
||||
let mut record_bytes = Vec::new();
|
||||
if serde_ipld_dagcbor::to_writer(&mut record_bytes, &input.record).is_err() {
|
||||
return (StatusCode::BAD_REQUEST, Json(json!({"error": "InvalidRecord", "message": "Failed to serialize record"}))).into_response();
|
||||
}
|
||||
let record_cid = match tracking_store.put(&record_bytes).await {
|
||||
Ok(c) => c,
|
||||
_ => return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": "Failed to save record block"}))).into_response(),
|
||||
};
|
||||
|
||||
let new_mst = if existing_cid.is_some() {
|
||||
mst.update(&key, record_cid).await.unwrap()
|
||||
} else {
|
||||
match mst.add(&key, record_cid).await {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
error!("Failed to add to MST: {:?}", e);
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": format!("Failed to add to MST: {:?}", e)}))).into_response();
|
||||
}
|
||||
}
|
||||
mst.add(&key, record_cid).await.unwrap()
|
||||
};
|
||||
let new_mst_root = new_mst.persist().await.unwrap();
|
||||
|
||||
let op = if existing_cid.is_some() {
|
||||
RecordOp::Update { collection: input.collection.clone(), rkey: input.rkey.clone(), cid: record_cid }
|
||||
} else {
|
||||
RecordOp::Create { collection: input.collection.clone(), rkey: input.rkey.clone(), cid: record_cid }
|
||||
};
|
||||
|
||||
let new_mst_root = match new_mst.persist().await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to persist MST: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Failed to persist MST"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
let written_cids = tracking_store.get_written_cids();
|
||||
let written_cids_str = written_cids.iter().map(|c| c.to_string()).collect::<Vec<_>>();
|
||||
|
||||
if let Err(e) = commit_and_log(&state, &did, user_id, Some(current_root_cid), new_mst_root, vec![op], &written_cids_str).await {
|
||||
return (StatusCode::INTERNAL_SERVER_ERROR, Json(json!({"error": "InternalError", "message": e}))).into_response();
|
||||
};
|
||||
|
||||
let did_obj = match Did::new(&did) {
|
||||
Ok(d) => d,
|
||||
Err(_) => {
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Invalid DID"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let rev = Tid::now(LimitedU32::MIN);
|
||||
|
||||
let new_commit = Commit::new_unsigned(did_obj, new_mst_root, rev, Some(current_root_cid));
|
||||
|
||||
let new_commit_bytes = match new_commit.to_cbor() {
|
||||
Ok(b) => b,
|
||||
Err(e) => {
|
||||
error!("Failed to serialize new commit: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(
|
||||
json!({"error": "InternalError", "message": "Failed to serialize new commit"}),
|
||||
),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let new_root_cid = match state.block_store.put(&new_commit_bytes).await {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
error!("Failed to save new commit: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Failed to save new commit"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
};
|
||||
|
||||
let update_repo = sqlx::query!("UPDATE repos SET repo_root_cid = $1 WHERE user_id = $2", new_root_cid.to_string(), user_id)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = update_repo {
|
||||
error!("Failed to update repo root in DB: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Failed to update repo root in DB"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let record_insert = sqlx::query!(
|
||||
"INSERT INTO records (repo_id, collection, rkey, record_cid) VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (repo_id, collection, rkey) DO UPDATE SET record_cid = $4, created_at = NOW()",
|
||||
user_id,
|
||||
input.collection,
|
||||
rkey,
|
||||
record_cid.to_string()
|
||||
)
|
||||
.execute(&state.db)
|
||||
.await;
|
||||
|
||||
if let Err(e) = record_insert {
|
||||
error!("Error inserting record index: {:?}", e);
|
||||
return (
|
||||
StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Json(json!({"error": "InternalError", "message": "Failed to index record"})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
let output = PutRecordOutput {
|
||||
uri: format!("at://{}/{}/{}", input.repo, input.collection, rkey),
|
||||
(StatusCode::OK, Json(PutRecordOutput {
|
||||
uri: format!("at://{}/{}/{}", did, input.collection, input.rkey),
|
||||
cid: record_cid.to_string(),
|
||||
};
|
||||
(StatusCode::OK, Json(output)).into_response()
|
||||
})).into_response()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user