A lexicon-driven AppView for ATProto.
0

Configure Feed

Select the types of activity you want to include in your feed.

happyview / src / record_refs.rs
3.2 kB 116 lines
1use crate::db::{DatabaseBackend, adapt_sql}; 2use serde_json::Value; 3use std::collections::HashSet; 4 5/// Recursively walk a JSON value and collect all string values starting with "at://". 6pub fn extract_at_uris(value: &Value) -> HashSet<String> { 7 let mut uris = HashSet::new(); 8 collect_at_uris(value, &mut uris); 9 uris 10} 11 12fn collect_at_uris(value: &Value, uris: &mut HashSet<String>) { 13 match value { 14 Value::String(s) if s.starts_with("at://") => { 15 uris.insert(s.clone()); 16 } 17 Value::Array(arr) => { 18 for item in arr { 19 collect_at_uris(item, uris); 20 } 21 } 22 Value::Object(obj) => { 23 for v in obj.values() { 24 collect_at_uris(v, uris); 25 } 26 } 27 _ => {} 28 } 29} 30 31/// Update record_refs for a given source record. 32/// Deletes old refs and inserts new ones. 33pub async fn sync_refs( 34 db: &sqlx::AnyPool, 35 source_uri: &str, 36 collection: &str, 37 record: &Value, 38 backend: DatabaseBackend, 39) -> Result<(), sqlx::Error> { 40 let uris = extract_at_uris(record); 41 42 // Delete existing refs for this source 43 let delete_sql = adapt_sql( 44 "DELETE FROM happyview_record_refs WHERE source_uri = ?", 45 backend, 46 ); 47 crate::db::query(&delete_sql) 48 .bind(source_uri) 49 .execute(db) 50 .await?; 51 52 // Insert new refs 53 let insert_sql = adapt_sql( 54 "INSERT INTO happyview_record_refs (source_uri, target_uri, collection) VALUES (?, ?, ?) ON CONFLICT DO NOTHING", 55 backend, 56 ); 57 for target_uri in &uris { 58 crate::db::query(&insert_sql) 59 .bind(source_uri) 60 .bind(target_uri) 61 .bind(collection) 62 .execute(db) 63 .await?; 64 } 65 66 Ok(()) 67} 68 69#[cfg(test)] 70mod tests { 71 use super::*; 72 use serde_json::json; 73 74 #[test] 75 fn extracts_top_level_uri() { 76 let val = json!({"subject": "at://did:plc:abc/com.example/123"}); 77 let uris = extract_at_uris(&val); 78 assert_eq!(uris.len(), 1); 79 assert!(uris.contains("at://did:plc:abc/com.example/123")); 80 } 81 82 #[test] 83 fn extracts_nested_uri() { 84 let val = json!({"outer": {"inner": "at://did:plc:abc/col/rkey"}}); 85 let uris = extract_at_uris(&val); 86 assert_eq!(uris.len(), 1); 87 assert!(uris.contains("at://did:plc:abc/col/rkey")); 88 } 89 90 #[test] 91 fn extracts_uris_from_arrays() { 92 let val = json!({"refs": ["at://did:plc:a/col/1", "at://did:plc:b/col/2"]}); 93 let uris = extract_at_uris(&val); 94 assert_eq!(uris.len(), 2); 95 } 96 97 #[test] 98 fn ignores_non_at_strings() { 99 let val = json!({"url": "https://example.com", "name": "test"}); 100 let uris = extract_at_uris(&val); 101 assert!(uris.is_empty()); 102 } 103 104 #[test] 105 fn empty_object_returns_empty() { 106 let uris = extract_at_uris(&json!({})); 107 assert!(uris.is_empty()); 108 } 109 110 #[test] 111 fn deduplicates_repeated_uris() { 112 let val = json!({"a": "at://did:plc:x/c/1", "b": "at://did:plc:x/c/1"}); 113 let uris = extract_at_uris(&val); 114 assert_eq!(uris.len(), 1); 115 } 116}