mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-26 20:24:15 +00:00
265 lines
8.7 KiB
Rust
265 lines
8.7 KiB
Rust
use crate::state::AppState;
|
|
use axum::{
|
|
Json,
|
|
extract::{Query, State},
|
|
http::StatusCode,
|
|
response::{IntoResponse, Response},
|
|
};
|
|
use cid::Cid;
|
|
use jacquard_repo::storage::BlockStore;
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::json;
|
|
use std::collections::HashMap;
|
|
use std::str::FromStr;
|
|
use tracing::error;
|
|
#[derive(Deserialize)]
|
|
pub struct GetRecordInput {
|
|
pub repo: String,
|
|
pub collection: String,
|
|
pub rkey: String,
|
|
pub cid: Option<String>,
|
|
}
|
|
pub async fn get_record(
|
|
State(state): State<AppState>,
|
|
Query(input): Query<GetRecordInput>,
|
|
) -> Response {
|
|
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
|
let user_id_opt = if input.repo.starts_with("did:") {
|
|
sqlx::query!("SELECT id FROM users WHERE did = $1", input.repo)
|
|
.fetch_optional(&state.db)
|
|
.await
|
|
.map(|opt| opt.map(|r| r.id))
|
|
} else {
|
|
let suffix = format!(".{}", hostname);
|
|
let short_handle = if input.repo.ends_with(&suffix) {
|
|
input.repo.strip_suffix(&suffix).unwrap_or(&input.repo)
|
|
} else {
|
|
&input.repo
|
|
};
|
|
sqlx::query!("SELECT id FROM users WHERE handle = $1", short_handle)
|
|
.fetch_optional(&state.db)
|
|
.await
|
|
.map(|opt| opt.map(|r| r.id))
|
|
};
|
|
let user_id: uuid::Uuid = match user_id_opt {
|
|
Ok(Some(id)) => id,
|
|
_ => {
|
|
return (
|
|
StatusCode::NOT_FOUND,
|
|
Json(json!({"error": "NotFound", "message": "Repo not found"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
let record_row = sqlx::query!(
|
|
"SELECT record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3",
|
|
user_id,
|
|
input.collection,
|
|
input.rkey
|
|
)
|
|
.fetch_optional(&state.db)
|
|
.await;
|
|
let record_cid_str: String = match record_row {
|
|
Ok(Some(row)) => row.record_cid,
|
|
_ => {
|
|
return (
|
|
StatusCode::NOT_FOUND,
|
|
Json(json!({"error": "NotFound", "message": "Record not found"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
if let Some(expected_cid) = &input.cid {
|
|
if &record_cid_str != expected_cid {
|
|
return (
|
|
StatusCode::NOT_FOUND,
|
|
Json(json!({"error": "NotFound", "message": "Record CID mismatch"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
}
|
|
let cid = match Cid::from_str(&record_cid_str) {
|
|
Ok(c) => c,
|
|
Err(_) => {
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(json!({"error": "InternalError", "message": "Invalid CID in DB"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
let block = match state.block_store.get(&cid).await {
|
|
Ok(Some(b)) => b,
|
|
_ => {
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(json!({"error": "InternalError", "message": "Record block not found"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
let value: serde_json::Value = match serde_ipld_dagcbor::from_slice(&block) {
|
|
Ok(v) => v,
|
|
Err(e) => {
|
|
error!("Failed to deserialize record: {:?}", e);
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(json!({"error": "InternalError"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
Json(json!({
|
|
"uri": format!("at://{}/{}/{}", input.repo, input.collection, input.rkey),
|
|
"cid": record_cid_str,
|
|
"value": value
|
|
}))
|
|
.into_response()
|
|
}
|
|
#[derive(Deserialize)]
|
|
pub struct ListRecordsInput {
|
|
pub repo: String,
|
|
pub collection: String,
|
|
pub limit: Option<i32>,
|
|
pub cursor: Option<String>,
|
|
#[serde(rename = "rkeyStart")]
|
|
pub rkey_start: Option<String>,
|
|
#[serde(rename = "rkeyEnd")]
|
|
pub rkey_end: Option<String>,
|
|
pub reverse: Option<bool>,
|
|
}
|
|
#[derive(Serialize)]
|
|
pub struct ListRecordsOutput {
|
|
pub cursor: Option<String>,
|
|
pub records: Vec<serde_json::Value>,
|
|
}
|
|
pub async fn list_records(
|
|
State(state): State<AppState>,
|
|
Query(input): Query<ListRecordsInput>,
|
|
) -> Response {
|
|
let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
|
|
let user_id_opt = if input.repo.starts_with("did:") {
|
|
sqlx::query!("SELECT id FROM users WHERE did = $1", input.repo)
|
|
.fetch_optional(&state.db)
|
|
.await
|
|
.map(|opt| opt.map(|r| r.id))
|
|
} else {
|
|
let suffix = format!(".{}", hostname);
|
|
let short_handle = if input.repo.ends_with(&suffix) {
|
|
input.repo.strip_suffix(&suffix).unwrap_or(&input.repo)
|
|
} else {
|
|
&input.repo
|
|
};
|
|
sqlx::query!("SELECT id FROM users WHERE handle = $1", short_handle)
|
|
.fetch_optional(&state.db)
|
|
.await
|
|
.map(|opt| opt.map(|r| r.id))
|
|
};
|
|
let user_id: uuid::Uuid = match user_id_opt {
|
|
Ok(Some(id)) => id,
|
|
_ => {
|
|
return (
|
|
StatusCode::NOT_FOUND,
|
|
Json(json!({"error": "NotFound", "message": "Repo not found"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
let limit = input.limit.unwrap_or(50).clamp(1, 100);
|
|
let reverse = input.reverse.unwrap_or(false);
|
|
let limit_i64 = limit as i64;
|
|
let order = if reverse { "ASC" } else { "DESC" };
|
|
let rows_res: Result<Vec<(String, String)>, sqlx::Error> = if let Some(cursor) = &input.cursor {
|
|
let comparator = if reverse { ">" } else { "<" };
|
|
let query = format!(
|
|
"SELECT rkey, record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey {} $3 ORDER BY rkey {} LIMIT $4",
|
|
comparator, order
|
|
);
|
|
sqlx::query_as(&query)
|
|
.bind(user_id)
|
|
.bind(&input.collection)
|
|
.bind(cursor)
|
|
.bind(limit_i64)
|
|
.fetch_all(&state.db)
|
|
.await
|
|
} else {
|
|
let mut conditions = vec!["repo_id = $1", "collection = $2"];
|
|
let mut param_idx = 3;
|
|
if input.rkey_start.is_some() {
|
|
conditions.push("rkey > $3");
|
|
param_idx += 1;
|
|
}
|
|
if input.rkey_end.is_some() {
|
|
conditions.push(if param_idx == 3 { "rkey < $3" } else { "rkey < $4" });
|
|
param_idx += 1;
|
|
}
|
|
let limit_idx = param_idx;
|
|
let query = format!(
|
|
"SELECT rkey, record_cid FROM records WHERE {} ORDER BY rkey {} LIMIT ${}",
|
|
conditions.join(" AND "),
|
|
order,
|
|
limit_idx
|
|
);
|
|
let mut query_builder = sqlx::query_as::<_, (String, String)>(&query)
|
|
.bind(user_id)
|
|
.bind(&input.collection);
|
|
if let Some(start) = &input.rkey_start {
|
|
query_builder = query_builder.bind(start);
|
|
}
|
|
if let Some(end) = &input.rkey_end {
|
|
query_builder = query_builder.bind(end);
|
|
}
|
|
query_builder.bind(limit_i64).fetch_all(&state.db).await
|
|
};
|
|
let rows = match rows_res {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
error!("Error listing records: {:?}", e);
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(json!({"error": "InternalError"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
let last_rkey = rows.last().map(|(rkey, _)| rkey.clone());
|
|
let mut cid_to_rkey: HashMap<Cid, (String, String)> = HashMap::new();
|
|
let mut cids: Vec<Cid> = Vec::with_capacity(rows.len());
|
|
for (rkey, cid_str) in &rows {
|
|
if let Ok(cid) = Cid::from_str(cid_str) {
|
|
cid_to_rkey.insert(cid, (rkey.clone(), cid_str.clone()));
|
|
cids.push(cid);
|
|
}
|
|
}
|
|
let blocks = match state.block_store.get_many(&cids).await {
|
|
Ok(b) => b,
|
|
Err(e) => {
|
|
error!("Error fetching blocks: {:?}", e);
|
|
return (
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
Json(json!({"error": "InternalError"})),
|
|
)
|
|
.into_response();
|
|
}
|
|
};
|
|
let mut records = Vec::new();
|
|
for (cid, block_opt) in cids.iter().zip(blocks.into_iter()) {
|
|
if let Some(block) = block_opt {
|
|
if let Some((rkey, cid_str)) = cid_to_rkey.get(cid) {
|
|
if let Ok(value) = serde_ipld_dagcbor::from_slice::<serde_json::Value>(&block) {
|
|
records.push(json!({
|
|
"uri": format!("at://{}/{}/{}", input.repo, input.collection, rkey),
|
|
"cid": cid_str,
|
|
"value": value
|
|
}));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Json(ListRecordsOutput {
|
|
cursor: last_rkey,
|
|
records,
|
|
})
|
|
.into_response()
|
|
}
|