mirror of
https://tangled.org/tranquil.farm/tranquil-pds
synced 2026-09-19 08:44:13 +00:00
feat(repo): missing $type? invent one
Lewis: May this revision serve well! <lu5a@proton.me>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
use super::validation::validate_record_with_status;
|
||||
use super::validation_mode::{ValidationMode, deserialize_validation_mode};
|
||||
use crate::repo::record::write::CommitInfo;
|
||||
use crate::repo::record::write::{CommitInfo, ensure_record_type};
|
||||
use axum::{Json, extract::State};
|
||||
use jacquard_repo::{mst::Mst, storage::BlockStore};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -54,6 +54,8 @@ async fn process_single_write(
|
||||
rkey,
|
||||
value,
|
||||
} => {
|
||||
let value = ensure_record_type(value, collection);
|
||||
let value = &*value;
|
||||
let validation_status = if validate.should_skip() {
|
||||
None
|
||||
} else {
|
||||
@@ -117,6 +119,8 @@ async fn process_single_write(
|
||||
rkey,
|
||||
value,
|
||||
} => {
|
||||
let value = ensure_record_type(value, collection);
|
||||
let value = &*value;
|
||||
let validation_status = if validate.should_skip() {
|
||||
None
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,7 @@ use cid::Cid;
|
||||
use jacquard_repo::storage::BlockStore;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::borrow::Cow;
|
||||
use std::str::FromStr;
|
||||
use tracing::error;
|
||||
use tranquil_pds::api::error::{ApiError, DbResultExt};
|
||||
@@ -62,6 +63,29 @@ pub async fn prepare_repo_write<A: RepoScopeAction>(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_record_type<'a>(
|
||||
record: &'a serde_json::Value,
|
||||
collection: &Nsid,
|
||||
) -> Cow<'a, serde_json::Value> {
|
||||
let serde_json::Value::Object(map) = record else {
|
||||
return Cow::Borrowed(record);
|
||||
};
|
||||
let needs_fill = match map.get("$type") {
|
||||
None | Some(serde_json::Value::Null) => true,
|
||||
Some(serde_json::Value::String(existing)) => existing.is_empty(),
|
||||
Some(_) => false,
|
||||
};
|
||||
if !needs_fill {
|
||||
return Cow::Borrowed(record);
|
||||
}
|
||||
let mut map = map.clone();
|
||||
map.insert(
|
||||
"$type".to_string(),
|
||||
serde_json::Value::String(collection.to_string()),
|
||||
);
|
||||
Cow::Owned(serde_json::Value::Object(map))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct CreateRecordInput {
|
||||
@@ -95,8 +119,11 @@ pub struct CreateRecordOutput {
|
||||
pub async fn create_record(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<CreateRecordInput>,
|
||||
Json(mut input): Json<CreateRecordInput>,
|
||||
) -> Result<Json<CreateRecordOutput>, ApiError> {
|
||||
if let Cow::Owned(record) = ensure_record_type(&input.record, &input.collection) {
|
||||
input.record = record;
|
||||
}
|
||||
let scope_proof = auth.verify_repo_create(&input.collection)?;
|
||||
let repo_auth = prepare_repo_write(&state, &scope_proof, &input.repo).await?;
|
||||
let did = repo_auth.did;
|
||||
@@ -278,8 +305,11 @@ pub struct PutRecordOutput {
|
||||
pub async fn put_record(
|
||||
State(state): State<AppState>,
|
||||
auth: Auth<Active>,
|
||||
Json(input): Json<PutRecordInput>,
|
||||
Json(mut input): Json<PutRecordInput>,
|
||||
) -> Result<Json<PutRecordOutput>, ApiError> {
|
||||
if let Cow::Owned(record) = ensure_record_type(&input.record, &input.collection) {
|
||||
input.record = record;
|
||||
}
|
||||
let upsert_proof = auth.verify_repo_upsert(&input.collection)?;
|
||||
let repo_auth = prepare_repo_write(&state, &upsert_proof, &input.repo).await?;
|
||||
let did = repo_auth.did;
|
||||
|
||||
@@ -773,3 +773,166 @@ async fn test_list_records_comprehensive() {
|
||||
.expect("Failed with nonexistent repo");
|
||||
assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_missing_type_is_filled_from_collection() {
|
||||
let client = client();
|
||||
let (did, jwt) = setup_new_user("missing-type").await;
|
||||
let now = Utc::now().to_rfc3339();
|
||||
|
||||
let create_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.repo.createRecord",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.json(&json!({
|
||||
"repo": did,
|
||||
"collection": "app.bsky.feed.post",
|
||||
"record": { "text": "no type set", "createdAt": now }
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to create record without $type");
|
||||
assert_eq!(
|
||||
create_res.status(),
|
||||
StatusCode::OK,
|
||||
"createRecord should fill missing $type from collection"
|
||||
);
|
||||
let create_body: Value = create_res.json().await.unwrap();
|
||||
let create_rkey = create_body["uri"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.rsplit('/')
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let get_created = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.repo.getRecord",
|
||||
base_url().await
|
||||
))
|
||||
.query(&[
|
||||
("repo", did.as_str()),
|
||||
("collection", "app.bsky.feed.post"),
|
||||
("rkey", &create_rkey),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to get created record");
|
||||
let created_body: Value = get_created.json().await.unwrap();
|
||||
assert_eq!(created_body["value"]["$type"], "app.bsky.feed.post");
|
||||
|
||||
let put_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.repo.putRecord",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.json(&json!({
|
||||
"repo": did,
|
||||
"collection": "app.bsky.actor.profile",
|
||||
"rkey": "self",
|
||||
"record": { "displayName": "No Type" }
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to put record without $type");
|
||||
assert_eq!(
|
||||
put_res.status(),
|
||||
StatusCode::OK,
|
||||
"putRecord should fill missing $type from collection"
|
||||
);
|
||||
let get_put = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.repo.getRecord",
|
||||
base_url().await
|
||||
))
|
||||
.query(&[
|
||||
("repo", did.as_str()),
|
||||
("collection", "app.bsky.actor.profile"),
|
||||
("rkey", "self"),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to get put record");
|
||||
let put_body: Value = get_put.json().await.unwrap();
|
||||
assert_eq!(put_body["value"]["$type"], "app.bsky.actor.profile");
|
||||
|
||||
let apply_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.repo.applyWrites",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.json(&json!({
|
||||
"repo": did,
|
||||
"writes": [
|
||||
{ "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.feed.post", "rkey": "batch-no-type", "value": { "text": "batch no type", "createdAt": now } }
|
||||
]
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to apply writes without $type");
|
||||
assert_eq!(
|
||||
apply_res.status(),
|
||||
StatusCode::OK,
|
||||
"applyWrites should fill missing $type from collection"
|
||||
);
|
||||
let get_batch = client
|
||||
.get(format!(
|
||||
"{}/xrpc/com.atproto.repo.getRecord",
|
||||
base_url().await
|
||||
))
|
||||
.query(&[
|
||||
("repo", did.as_str()),
|
||||
("collection", "app.bsky.feed.post"),
|
||||
("rkey", "batch-no-type"),
|
||||
])
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to get batch record");
|
||||
let batch_body: Value = get_batch.json().await.unwrap();
|
||||
assert_eq!(batch_body["value"]["$type"], "app.bsky.feed.post");
|
||||
|
||||
let mismatch_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.repo.createRecord",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.json(&json!({
|
||||
"repo": did,
|
||||
"collection": "app.bsky.feed.post",
|
||||
"record": { "$type": "app.bsky.feed.like", "text": "wrong type", "createdAt": now }
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send mismatch request");
|
||||
assert_eq!(
|
||||
mismatch_res.status(),
|
||||
StatusCode::BAD_REQUEST,
|
||||
"explicit mismatched $type should still be rejected"
|
||||
);
|
||||
|
||||
let non_string_type_res = client
|
||||
.post(format!(
|
||||
"{}/xrpc/com.atproto.repo.createRecord",
|
||||
base_url().await
|
||||
))
|
||||
.bearer_auth(&jwt)
|
||||
.json(&json!({
|
||||
"repo": did,
|
||||
"collection": "app.bsky.feed.post",
|
||||
"record": { "$type": 123, "text": "non-string type", "createdAt": now }
|
||||
}))
|
||||
.send()
|
||||
.await
|
||||
.expect("Failed to send non-string type request");
|
||||
assert_eq!(
|
||||
non_string_type_res.status(),
|
||||
StatusCode::BAD_REQUEST,
|
||||
"present non-string $type should be rejected, not overwritten"
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user