Constellation, Spacedust, Slingshot, UFOs: atproto crates and services for microcosm

ManyToManyItem and set default "try-it": listitems

authored by bad-example.com and committed by tangled.org 679b32fb bb9f4d21

+107 -66
+14
constellation/src/lib.rs
··· 48 pub fn rkey(&self) -> String { 49 self.rkey.clone() 50 } 51 } 52 53 /// maybe the worst type in this repo, and there are some bad types
··· 48 pub fn rkey(&self) -> String { 49 self.rkey.clone() 50 } 51 + pub fn uri(&self) -> String { 52 + let RecordId { 53 + did: Did(did), 54 + collection, 55 + rkey, 56 + } = self; 57 + format!("at://{did}/{collection}/{rkey}") 58 + } 59 + } 60 + 61 + #[derive(Debug, Serialize, PartialEq)] 62 + pub struct ManyToManyItem { 63 + link_record: RecordId, 64 + other_subject: String, 65 } 66 67 /// maybe the worst type in this repo, and there are some bad types
+2 -16
constellation/src/server/mod.rs
··· 18 use tokio_util::sync::CancellationToken; 19 20 use crate::storage::{LinkReader, Order, StorageStats}; 21 - use crate::{CountsByCount, Did, RecordId}; 22 23 mod acceptable; 24 mod filters; ··· 714 #[serde(default = "get_default_cursor_limit")] 715 limit: u64, 716 } 717 - #[derive(Debug, Serialize)] 718 - struct ManyToManyItem { 719 - link: RecordId, 720 - subject: String, 721 - } 722 #[derive(Template, Serialize)] 723 #[template(path = "get-many-to-many.html.j2")] 724 struct GetManyToManyItemsResponse { ··· 782 783 let cursor = paged.next.map(|next| ApiKeyedCursor { next }.into()); 784 785 - let items: Vec<ManyToManyItem> = paged 786 - .items 787 - .into_iter() 788 - .map(|(record_id, subject)| ManyToManyItem { 789 - link: record_id, 790 - subject, 791 - }) 792 - .collect(); 793 - 794 Ok(acceptable( 795 accept, 796 GetManyToManyItemsResponse { 797 - items, 798 cursor, 799 query: (*query).clone(), 800 },
··· 18 use tokio_util::sync::CancellationToken; 19 20 use crate::storage::{LinkReader, Order, StorageStats}; 21 + use crate::{CountsByCount, Did, ManyToManyItem, RecordId}; 22 23 mod acceptable; 24 mod filters; ··· 714 #[serde(default = "get_default_cursor_limit")] 715 limit: u64, 716 } 717 #[derive(Template, Serialize)] 718 #[template(path = "get-many-to-many.html.j2")] 719 struct GetManyToManyItemsResponse { ··· 777 778 let cursor = paged.next.map(|next| ApiKeyedCursor { next }.into()); 779 780 Ok(acceptable( 781 accept, 782 GetManyToManyItemsResponse { 783 + items: paged.items, 784 cursor, 785 query: (*query).clone(), 786 },
+10 -11
constellation/src/storage/mem_store.rs
··· 2 LinkReader, LinkStorage, ManyToManyCursor, Order, PagedAppendingCollection, 3 PagedOrderedCollection, StorageStats, 4 }; 5 - use crate::{ActionableEvent, CountsByCount, Did, RecordId}; 6 7 use anyhow::{anyhow, Result}; 8 use links::CollectedLink; ··· 248 after: Option<String>, 249 filter_dids: &HashSet<Did>, 250 filter_targets: &HashSet<String>, 251 - ) -> Result<PagedOrderedCollection<(RecordId, String), String>> { 252 // setup variables that we need later 253 let path_to_other = RecordPath(path_to_other.to_string()); 254 let filter_targets: HashSet<Target> = ··· 280 return Ok(PagedOrderedCollection::empty()); 281 }; 282 283 - let mut items: Vec<(usize, usize, RecordId, String)> = Vec::new(); 284 285 // iterate backwards (who linked to the target?) 286 for (linker_idx, (did, rkey)) in linkers ··· 313 }) 314 .take(limit as usize + 1 - items.len()) 315 { 316 - items.push(( 317 - linker_idx, 318 - link_idx, 319 - RecordId { 320 did: did.clone(), 321 collection: collection.to_string(), 322 rkey: rkey.0.clone(), 323 }, 324 - fwd_target.0.clone(), 325 - )); 326 } 327 328 // page full - eject ··· 332 } 333 334 let next = (items.len() > limit as usize).then(|| { 335 - let (l, f, _, _) = items[limit as usize - 1]; 336 format!("{l},{f}") 337 }); 338 339 let items = items 340 .into_iter() 341 .take(limit as usize) 342 - .map(|(_, _, rid, t)| (rid, t)) 343 .collect(); 344 345 Ok(PagedOrderedCollection { items, next })
··· 2 LinkReader, LinkStorage, ManyToManyCursor, Order, PagedAppendingCollection, 3 PagedOrderedCollection, StorageStats, 4 }; 5 + use crate::{ActionableEvent, CountsByCount, Did, ManyToManyItem, RecordId}; 6 7 use anyhow::{anyhow, Result}; 8 use links::CollectedLink; ··· 248 after: Option<String>, 249 filter_dids: &HashSet<Did>, 250 filter_targets: &HashSet<String>, 251 + ) -> Result<PagedOrderedCollection<ManyToManyItem, String>> { 252 // setup variables that we need later 253 let path_to_other = RecordPath(path_to_other.to_string()); 254 let filter_targets: HashSet<Target> = ··· 280 return Ok(PagedOrderedCollection::empty()); 281 }; 282 283 + let mut items: Vec<(usize, usize, ManyToManyItem)> = Vec::new(); 284 285 // iterate backwards (who linked to the target?) 286 for (linker_idx, (did, rkey)) in linkers ··· 313 }) 314 .take(limit as usize + 1 - items.len()) 315 { 316 + let item = ManyToManyItem { 317 + link_record: RecordId { 318 did: did.clone(), 319 collection: collection.to_string(), 320 rkey: rkey.0.clone(), 321 }, 322 + other_subject: fwd_target.0.clone(), 323 + }; 324 + items.push((linker_idx, link_idx, item)); 325 } 326 327 // page full - eject ··· 331 } 332 333 let next = (items.len() > limit as usize).then(|| { 334 + let (l, f, _) = items[limit as usize - 1]; 335 format!("{l},{f}") 336 }); 337 338 let items = items 339 .into_iter() 340 .take(limit as usize) 341 + .map(|(_, _, item)| item) 342 .collect(); 343 344 Ok(PagedOrderedCollection { items, next })
+50 -25
constellation/src/storage/mod.rs
··· 1 - use crate::{ActionableEvent, CountsByCount, Did, RecordId}; 2 use anyhow::Result; 3 use serde::{Deserialize, Serialize}; 4 use std::collections::{HashMap, HashSet}; ··· 152 after: Option<String>, 153 filter_dids: &HashSet<Did>, 154 filter_to_targets: &HashSet<String>, 155 - ) -> Result<PagedOrderedCollection<(RecordId, String), String>>; 156 157 fn get_all_counts( 158 &self, ··· 1817 2, 1818 "both forward links at path_to_other should be emitted" 1819 ); 1820 - let mut targets: Vec<_> = result.items.iter().map(|(_, t)| t.as_str()).collect(); 1821 targets.sort(); 1822 assert_eq!(targets, vec!["b.com", "c.com"]); 1823 assert!(result 1824 .items 1825 .iter() 1826 - .all(|(r, _)| r.did.0 == "did:plc:asdf" && r.rkey == "asdf")); 1827 assert_eq!(result.next, None); 1828 }); 1829 ··· 1926 let b_items: Vec<_> = result 1927 .items 1928 .iter() 1929 - .filter(|(_, subject)| subject == "b.com") 1930 .collect(); 1931 assert_eq!(b_items.len(), 2); 1932 - assert!(b_items 1933 - .iter() 1934 - .any(|(r, _)| r.did.0 == "did:plc:asdf" && r.rkey == "asdf")); 1935 - assert!(b_items 1936 - .iter() 1937 - .any(|(r, _)| r.did.0 == "did:plc:asdf" && r.rkey == "asdf2")); 1938 // Check c.com items 1939 let c_items: Vec<_> = result 1940 .items 1941 .iter() 1942 - .filter(|(_, subject)| subject == "c.com") 1943 .collect(); 1944 assert_eq!(c_items.len(), 2); 1945 - assert!(c_items 1946 - .iter() 1947 - .any(|(r, _)| r.did.0 == "did:plc:fdsa" && r.rkey == "fdsa")); 1948 - assert!(c_items 1949 - .iter() 1950 - .any(|(r, _)| r.did.0 == "did:plc:fdsa" && r.rkey == "fdsa2")); 1951 1952 // Test with DID filter - should only get records from did:plc:fdsa 1953 let result = storage.get_many_to_many( ··· 1961 &HashSet::new(), 1962 )?; 1963 assert_eq!(result.items.len(), 2); 1964 - assert!(result.items.iter().all(|(_, subject)| subject == "c.com")); 1965 - assert!(result.items.iter().all(|(r, _)| r.did.0 == "did:plc:fdsa")); 1966 1967 // Test with target filter - should only get records linking to b.com 1968 let result = storage.get_many_to_many( ··· 1976 &HashSet::from_iter(["b.com".to_string()]), 1977 )?; 1978 assert_eq!(result.items.len(), 2); 1979 - assert!(result.items.iter().all(|(_, subject)| subject == "b.com")); 1980 - assert!(result.items.iter().all(|(r, _)| r.did.0 == "did:plc:asdf")); 1981 1982 // Pagination edge cases: we have 4 flat items 1983 ··· 2051 assert_eq!(result2.next, None, "next should be None on final page"); 2052 2053 // Verify we got all 4 unique items across both pages (no duplicates, no gaps) 2054 - let mut all_rkeys: Vec<_> = result.items.iter().map(|(r, _)| r.rkey.clone()).collect(); 2055 - all_rkeys.extend(result2.items.iter().map(|(r, _)| r.rkey.clone())); 2056 all_rkeys.sort(); 2057 assert_eq!( 2058 all_rkeys, ··· 2130 .items 2131 .iter() 2132 .chain(page2.items.iter()) 2133 - .map(|(_, t)| t.clone()) 2134 .collect(); 2135 all_targets.sort(); 2136 assert_eq!(
··· 1 + use crate::{ActionableEvent, CountsByCount, Did, ManyToManyItem, RecordId}; 2 use anyhow::Result; 3 use serde::{Deserialize, Serialize}; 4 use std::collections::{HashMap, HashSet}; ··· 152 after: Option<String>, 153 filter_dids: &HashSet<Did>, 154 filter_to_targets: &HashSet<String>, 155 + ) -> Result<PagedOrderedCollection<ManyToManyItem, String>>; 156 157 fn get_all_counts( 158 &self, ··· 1817 2, 1818 "both forward links at path_to_other should be emitted" 1819 ); 1820 + let mut targets: Vec<_> = result 1821 + .items 1822 + .iter() 1823 + .map(|item| item.other_subject.as_str()) 1824 + .collect(); 1825 targets.sort(); 1826 assert_eq!(targets, vec!["b.com", "c.com"]); 1827 assert!(result 1828 .items 1829 .iter() 1830 + .all(|item| item.link_record.uri() == "at://did:plc:asdf/app.t.c/asdf")); 1831 assert_eq!(result.next, None); 1832 }); 1833 ··· 1930 let b_items: Vec<_> = result 1931 .items 1932 .iter() 1933 + .filter(|item| item.other_subject == "b.com") 1934 .collect(); 1935 assert_eq!(b_items.len(), 2); 1936 + assert!(b_items.iter().any( 1937 + |item| item.link_record.did.0 == "did:plc:asdf" && item.link_record.rkey == "asdf" 1938 + )); 1939 + assert!(b_items.iter().any( 1940 + |item| item.link_record.did.0 == "did:plc:asdf" && item.link_record.rkey == "asdf2" 1941 + )); 1942 // Check c.com items 1943 let c_items: Vec<_> = result 1944 .items 1945 .iter() 1946 + .filter(|item| item.other_subject == "c.com") 1947 .collect(); 1948 assert_eq!(c_items.len(), 2); 1949 + assert!(c_items.iter().any( 1950 + |item| item.link_record.did.0 == "did:plc:fdsa" && item.link_record.rkey == "fdsa" 1951 + )); 1952 + assert!(c_items.iter().any( 1953 + |item| item.link_record.did.0 == "did:plc:fdsa" && item.link_record.rkey == "fdsa2" 1954 + )); 1955 1956 // Test with DID filter - should only get records from did:plc:fdsa 1957 let result = storage.get_many_to_many( ··· 1965 &HashSet::new(), 1966 )?; 1967 assert_eq!(result.items.len(), 2); 1968 + assert!(result 1969 + .items 1970 + .iter() 1971 + .all(|item| item.other_subject == "c.com")); 1972 + assert!(result 1973 + .items 1974 + .iter() 1975 + .all(|item| item.link_record.did.0 == "did:plc:fdsa")); 1976 1977 // Test with target filter - should only get records linking to b.com 1978 let result = storage.get_many_to_many( ··· 1986 &HashSet::from_iter(["b.com".to_string()]), 1987 )?; 1988 assert_eq!(result.items.len(), 2); 1989 + assert!(result 1990 + .items 1991 + .iter() 1992 + .all(|item| item.other_subject == "b.com")); 1993 + assert!(result 1994 + .items 1995 + .iter() 1996 + .all(|item| item.link_record.did.0 == "did:plc:asdf")); 1997 1998 // Pagination edge cases: we have 4 flat items 1999 ··· 2067 assert_eq!(result2.next, None, "next should be None on final page"); 2068 2069 // Verify we got all 4 unique items across both pages (no duplicates, no gaps) 2070 + let mut all_rkeys: Vec<_> = result 2071 + .items 2072 + .iter() 2073 + .map(|item| item.link_record.rkey.clone()) 2074 + .collect(); 2075 + all_rkeys.extend( 2076 + result2 2077 + .items 2078 + .iter() 2079 + .map(|item| item.link_record.rkey.clone()), 2080 + ); 2081 all_rkeys.sort(); 2082 assert_eq!( 2083 all_rkeys, ··· 2155 .items 2156 .iter() 2157 .chain(page2.items.iter()) 2158 + .map(|item| item.other_subject.clone()) 2159 .collect(); 2160 all_targets.sort(); 2161 assert_eq!(
+12 -8
constellation/src/storage/rocks_store.rs
··· 2 ActionableEvent, LinkReader, LinkStorage, ManyToManyCursor, Order, PagedAppendingCollection, 3 PagedOrderedCollection, StorageStats, 4 }; 5 - use crate::{CountsByCount, Did, RecordId}; 6 7 use anyhow::{anyhow, bail, Result}; 8 use bincode::Options as BincodeOptions; ··· 1134 after: Option<String>, 1135 filter_link_dids: &HashSet<Did>, 1136 filter_to_targets: &HashSet<String>, 1137 - ) -> Result<PagedOrderedCollection<(RecordId, String), String>> { 1138 // helper to resolve dids 1139 let resolve_active_did = |did_id: &DidId| -> Result<Option<Did>> { 1140 let Some(did) = self.did_id_table.get_val_from_id(&self.db, did_id.0)? else { ··· 1192 }; 1193 let linkers = self.get_target_linkers(&target_id)?; 1194 1195 - let mut items: Vec<(usize, usize, RecordId, String)> = Vec::new(); 1196 1197 - // iterate backwards (who linked to the target?) 1198 for (linker_idx, (did_id, rkey)) in 1199 linkers.0.iter().enumerate().skip_while(|(linker_idx, _)| { 1200 cursor.is_some_and(|c| *linker_idx < c.backlink as usize) ··· 1215 continue; 1216 }; 1217 1218 - // iterate forward (which of these links point to the __other__ target?) 1219 for (link_idx, RecordLinkTarget(_, fwd_target_id)) in links 1220 .0 1221 .into_iter() ··· 1250 collection: collection.0.clone(), 1251 rkey: rkey.0.clone(), 1252 }; 1253 - items.push((linker_idx, link_idx, record_id, fwd_target_key.0 .0)); 1254 } 1255 1256 // page full - eject ··· 1269 // forward_link_idx are skipped. This correctly resumes mid-record when 1270 // a single backlinker has multiple forward links at path_to_other. 1271 let next = (items.len() > limit as usize).then(|| { 1272 - let (l, f, _, _) = items[limit as usize - 1]; 1273 format!("{l},{f}") 1274 }); 1275 1276 let items = items 1277 .into_iter() 1278 .take(limit as usize) 1279 - .map(|(_, _, rid, t)| (rid, t)) 1280 .collect(); 1281 1282 Ok(PagedOrderedCollection { items, next })
··· 2 ActionableEvent, LinkReader, LinkStorage, ManyToManyCursor, Order, PagedAppendingCollection, 3 PagedOrderedCollection, StorageStats, 4 }; 5 + use crate::{CountsByCount, Did, ManyToManyItem, RecordId}; 6 7 use anyhow::{anyhow, bail, Result}; 8 use bincode::Options as BincodeOptions; ··· 1134 after: Option<String>, 1135 filter_link_dids: &HashSet<Did>, 1136 filter_to_targets: &HashSet<String>, 1137 + ) -> Result<PagedOrderedCollection<ManyToManyItem, String>> { 1138 // helper to resolve dids 1139 let resolve_active_did = |did_id: &DidId| -> Result<Option<Did>> { 1140 let Some(did) = self.did_id_table.get_val_from_id(&self.db, did_id.0)? else { ··· 1192 }; 1193 let linkers = self.get_target_linkers(&target_id)?; 1194 1195 + let mut items: Vec<(usize, usize, ManyToManyItem)> = Vec::new(); 1196 1197 + // iterate backlinks (who linked to the target?) 1198 for (linker_idx, (did_id, rkey)) in 1199 linkers.0.iter().enumerate().skip_while(|(linker_idx, _)| { 1200 cursor.is_some_and(|c| *linker_idx < c.backlink as usize) ··· 1215 continue; 1216 }; 1217 1218 + // iterate fwd links (which of these links point to the __other__ target?) 1219 for (link_idx, RecordLinkTarget(_, fwd_target_id)) in links 1220 .0 1221 .into_iter() ··· 1250 collection: collection.0.clone(), 1251 rkey: rkey.0.clone(), 1252 }; 1253 + let item = ManyToManyItem { 1254 + link_record: record_id, 1255 + other_subject: fwd_target_key.0 .0, 1256 + }; 1257 + items.push((linker_idx, link_idx, item)); 1258 } 1259 1260 // page full - eject ··· 1273 // forward_link_idx are skipped. This correctly resumes mid-record when 1274 // a single backlinker has multiple forward links at path_to_other. 1275 let next = (items.len() > limit as usize).then(|| { 1276 + let (l, f, _) = items[limit as usize - 1]; 1277 format!("{l},{f}") 1278 }); 1279 1280 let items = items 1281 .into_iter() 1282 .take(limit as usize) 1283 + .map(|(_, _, item)| item) 1284 .collect(); 1285 1286 Ok(PagedOrderedCollection { items, next })
+11 -5
constellation/templates/get-many-to-many.html.j2
··· 19 20 <ul> 21 <li>See all links to this target at <code>/links/all</code>: <a href="/links/all?target={{ query.subject|urlencode }}">/links/all?target={{ query.subject }}</a></li> 22 </ul> 23 24 <h3>Many-to-many links, most recent first:</h3> 25 26 {% for item in items %} 27 - <pre style="display: block; margin: 1em 2em" class="code"><strong>Subject</strong>: <a href="/links/all?target={{ item.subject|urlencode }}">{{ item.subject }}</a> 28 - <strong>DID</strong>: {{ item.link.did().0 }} 29 - <strong>Collection</strong>: {{ item.link.collection }} 30 - <strong>RKey</strong>: {{ item.link.rkey }} 31 - -> <a href="https://pdsls.dev/at://{{ item.link.did().0 }}/{{ item.link.collection }}/{{ item.link.rkey }}">browse record</a></pre> 32 {% endfor %} 33 34 {% if let Some(c) = cursor %}
··· 19 20 <ul> 21 <li>See all links to this target at <code>/links/all</code>: <a href="/links/all?target={{ query.subject|urlencode }}">/links/all?target={{ query.subject }}</a></li> 22 + {# todo: add link to see many-to-many counts #} 23 </ul> 24 25 <h3>Many-to-many links, most recent first:</h3> 26 27 {% for item in items %} 28 + <pre style="display: block; margin: 1em 2em" class="code"><strong>Linking record</strong>: 29 + {%- if let Some(uri) = item.link_record.uri().as_str()|to_browseable %} <a href="{{ uri }}">browse link record</a>{% endif %} 30 + DID: {{ item.link_record.did().0 }} 31 + Collection: {{ item.link_record.collection() }} 32 + RKey: {{ item.link_record.rkey() }} 33 + <strong>Other subject</strong>: {{ item.other_subject }} 34 + {%- if let Some(uri) = item.other_subject.as_str()|to_browseable %} 35 + -> <a href="{{ uri }}">browse subject</a> 36 + {%- endif %} 37 + </pre> 38 {% endfor %} 39 40 {% if let Some(c) = cursor %}
+8 -1
constellation/templates/hello.html.j2
··· 98 </ul> 99 100 <p style="margin-bottom: 0"><strong>Try it:</strong></p> 101 - {% call try_it::get_many_to_many("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like:subject.uri", "reply.parent.uri", [""], [""], 16) %} 102 103 <h3 class="route"><code>GET /xrpc/blue.microcosm.links.getDistinct</code></h3> 104
··· 98 </ul> 99 100 <p style="margin-bottom: 0"><strong>Try it:</strong></p> 101 + {% call try_it::get_many_to_many( 102 + "at://did:plc:uyauirpjzk6le4ygqzatcwnq/app.bsky.graph.list/3lzhg33t5bf2h", 103 + "app.bsky.graph.listitem:list", 104 + "subject", 105 + [""], 106 + [""], 107 + 16, 108 + ) %} 109 110 <h3 class="route"><code>GET /xrpc/blue.microcosm.links.getDistinct</code></h3> 111