Initial firehose connections

This commit is contained in:
Lewis
2025-12-10 19:24:10 +02:00
parent 8cfea3b480
commit c09f0e9982
27 changed files with 1339 additions and 1438 deletions
+87 -263
View File
@@ -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(&current_root_cid).await {
let tracking_store = TrackingBlockStore::new(state.block_store.clone());
let commit_bytes = match tracking_store.get(&current_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
View File
@@ -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(&current_root_cid).await {
let commit_bytes = match tracking_store.get(&current_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()
}
+3 -1
View File
@@ -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,
};
+125
View File
@@ -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
View File
@@ -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(&current_root_cid).await {
let commit_bytes = match tracking_store.get(&current_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(&current_root_cid).await {
let commit_bytes = match tracking_store.get(&current_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()
}