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

Replace tuple with RecordsBySubject struct

Replace (String, Vec<RecordId>) tuples with a proper
RecordsBySubject struct throughout the many-to-many query
implementation as Lexicons per ATProto spec do not offer a tuple type.

Add lexicon definition for the getManyToMany XRPC endpoint.

Changed files
+176 -39
constellation
lexicons
blue.microcosm
+6
constellation/src/lib.rs
··· 50 50 } 51 51 } 52 52 53 + #[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)] 54 + pub struct RecordsBySubject { 55 + pub subject: String, 56 + pub records: Vec<RecordId>, 57 + } 58 + 53 59 /// maybe the worst type in this repo, and there are some bad types 54 60 #[derive(Debug, Serialize, PartialEq)] 55 61 pub struct CountsByCount {
+2 -2
constellation/src/server/mod.rs
··· 18 18 use tokio_util::sync::CancellationToken; 19 19 20 20 use crate::storage::{LinkReader, StorageStats}; 21 - use crate::{CountsByCount, Did, RecordId}; 21 + use crate::{CountsByCount, Did, RecordId, RecordsBySubject}; 22 22 23 23 mod acceptable; 24 24 mod filters; ··· 618 618 #[derive(Template, Serialize)] 619 619 #[template(path = "get-many-to-many.html.j2")] 620 620 struct GetManyToManyItemsResponse { 621 - linking_records: Vec<(String, Vec<RecordId>)>, 621 + linking_records: Vec<RecordsBySubject>, 622 622 cursor: Option<OpaqueApiCursor>, 623 623 #[serde(skip_serializing)] 624 624 query: GetManyToManyItemsQuery,
+9 -6
constellation/src/storage/mem_store.rs
··· 1 1 use super::{ 2 2 LinkReader, LinkStorage, PagedAppendingCollection, PagedOrderedCollection, StorageStats, 3 3 }; 4 - use crate::{ActionableEvent, CountsByCount, Did, RecordId}; 4 + use crate::{ActionableEvent, CountsByCount, Did, RecordId, RecordsBySubject}; 5 5 use anyhow::Result; 6 6 use links::CollectedLink; 7 7 use std::collections::{HashMap, HashSet}; ··· 244 244 after: Option<String>, 245 245 filter_dids: &HashSet<Did>, 246 246 filter_to_targets: &HashSet<String>, 247 - ) -> Result<PagedOrderedCollection<(String, Vec<RecordId>), String>> { 247 + ) -> Result<PagedOrderedCollection<RecordsBySubject, String>> { 248 248 let empty_res = Ok(PagedOrderedCollection { 249 249 items: Vec::new(), 250 250 next: None, ··· 307 307 308 308 let mut items = grouped_links 309 309 .into_iter() 310 - .map(|(t, r)| (t.0, r)) 310 + .map(|(t, r)| RecordsBySubject { 311 + subject: t.0, 312 + records: r, 313 + }) 311 314 .collect::<Vec<_>>(); 312 315 313 - items.sort_by(|(a, _), (b, _)| a.cmp(b)); 316 + items.sort_by(|a, b| a.subject.cmp(&b.subject)); 314 317 315 318 items = items 316 319 .into_iter() 317 - .skip_while(|(t, _)| after.as_ref().map(|a| t <= a).unwrap_or(false)) 320 + .skip_while(|item| after.as_ref().map(|a| &item.subject <= a).unwrap_or(false)) 318 321 .take(limit as usize) 319 322 .collect(); 320 323 321 324 let next = if items.len() as u64 >= limit { 322 - items.last().map(|(t, _)| t.clone()) 325 + items.last().map(|item| item.subject.clone()) 323 326 } else { 324 327 None 325 328 };
+40 -24
constellation/src/storage/mod.rs
··· 1 - use crate::{ActionableEvent, CountsByCount, Did, RecordId}; 1 + use crate::{ActionableEvent, CountsByCount, Did, RecordId, RecordsBySubject}; 2 2 use anyhow::Result; 3 3 use serde::{Deserialize, Serialize}; 4 4 use std::collections::{HashMap, HashSet}; ··· 114 114 after: Option<String>, 115 115 filter_dids: &HashSet<Did>, 116 116 filter_to_targets: &HashSet<String>, 117 - ) -> Result<PagedOrderedCollection<(String, Vec<RecordId>), String>>; 117 + ) -> Result<PagedOrderedCollection<RecordsBySubject, String>>; 118 118 119 119 fn get_all_counts( 120 120 &self, ··· 1621 1621 &HashSet::new(), 1622 1622 )?, 1623 1623 PagedOrderedCollection { 1624 - items: vec![( 1625 - "b.com".to_string(), 1626 - vec![RecordId { 1624 + items: vec![RecordsBySubject { 1625 + subject: "b.com".to_string(), 1626 + records: vec![RecordId { 1627 1627 did: "did:plc:asdf".into(), 1628 1628 collection: "app.t.c".into(), 1629 1629 rkey: "asdf".into(), 1630 1630 }] 1631 - )], 1631 + }], 1632 1632 next: None, 1633 1633 } 1634 1634 ); ··· 1730 1730 assert_eq!(result.items.len(), 2); 1731 1731 assert_eq!(result.next, None); 1732 1732 // Find b.com group 1733 - let (b_target, b_records) = result.items.iter().find(|(target, _)| target == "b.com").unwrap(); 1734 - assert_eq!(b_target, "b.com"); 1735 - assert_eq!(b_records.len(), 2); 1736 - assert!(b_records.iter().any(|r| r.did.0 == "did:plc:asdf" && r.rkey == "asdf")); 1737 - assert!(b_records.iter().any(|r| r.did.0 == "did:plc:asdf" && r.rkey == "asdf2")); 1733 + let b_group = result 1734 + .items 1735 + .iter() 1736 + .find(|group| group.subject == "b.com") 1737 + .unwrap(); 1738 + assert_eq!(b_group.subject, "b.com"); 1739 + assert_eq!(b_group.records.len(), 2); 1740 + assert!(b_group.records 1741 + .iter() 1742 + .any(|r| r.did.0 == "did:plc:asdf" && r.rkey == "asdf")); 1743 + assert!(b_group.records 1744 + .iter() 1745 + .any(|r| r.did.0 == "did:plc:asdf" && r.rkey == "asdf2")); 1738 1746 // Find c.com group 1739 - let (c_target, c_records) = result.items.iter().find(|(target, _)| target == "c.com").unwrap(); 1740 - assert_eq!(c_target, "c.com"); 1741 - assert_eq!(c_records.len(), 2); 1742 - assert!(c_records.iter().any(|r| r.did.0 == "did:plc:fdsa" && r.rkey == "fdsa")); 1743 - assert!(c_records.iter().any(|r| r.did.0 == "did:plc:fdsa" && r.rkey == "fdsa2")); 1747 + let c_group = result 1748 + .items 1749 + .iter() 1750 + .find(|group| group.subject == "c.com") 1751 + .unwrap(); 1752 + assert_eq!(c_group.subject, "c.com"); 1753 + assert_eq!(c_group.records.len(), 2); 1754 + assert!(c_group.records 1755 + .iter() 1756 + .any(|r| r.did.0 == "did:plc:fdsa" && r.rkey == "fdsa")); 1757 + assert!(c_group.records 1758 + .iter() 1759 + .any(|r| r.did.0 == "did:plc:fdsa" && r.rkey == "fdsa2")); 1744 1760 1745 1761 // Test with DID filter - should only get records from did:plc:fdsa 1746 1762 let result = storage.get_many_to_many( ··· 1754 1770 &HashSet::new(), 1755 1771 )?; 1756 1772 assert_eq!(result.items.len(), 1); 1757 - let (target, records) = &result.items[0]; 1758 - assert_eq!(target, "c.com"); 1759 - assert_eq!(records.len(), 2); 1760 - assert!(records.iter().all(|r| r.did.0 == "did:plc:fdsa")); 1773 + let group = &result.items[0]; 1774 + assert_eq!(group.subject, "c.com"); 1775 + assert_eq!(group.records.len(), 2); 1776 + assert!(group.records.iter().all(|r| r.did.0 == "did:plc:fdsa")); 1761 1777 1762 1778 // Test with target filter - should only get records linking to b.com 1763 1779 let result = storage.get_many_to_many( ··· 1771 1787 &HashSet::from_iter(["b.com".to_string()]), 1772 1788 )?; 1773 1789 assert_eq!(result.items.len(), 1); 1774 - let (target, records) = &result.items[0]; 1775 - assert_eq!(target, "b.com"); 1776 - assert_eq!(records.len(), 2); 1777 - assert!(records.iter().all(|r| r.did.0 == "did:plc:asdf")); 1790 + let group = &result.items[0]; 1791 + assert_eq!(group.subject, "b.com"); 1792 + assert_eq!(group.records.len(), 2); 1793 + assert!(group.records.iter().all(|r| r.did.0 == "did:plc:asdf")); 1778 1794 }); 1779 1795 }
+7 -4
constellation/src/storage/rocks_store.rs
··· 2 2 ActionableEvent, LinkReader, LinkStorage, PagedAppendingCollection, PagedOrderedCollection, 3 3 StorageStats, 4 4 }; 5 - use crate::{CountsByCount, Did, RecordId}; 5 + use crate::{CountsByCount, Did, RecordId, RecordsBySubject}; 6 6 use anyhow::{bail, Result}; 7 7 use bincode::Options as BincodeOptions; 8 8 use links::CollectedLink; ··· 1132 1132 after: Option<String>, 1133 1133 filter_dids: &HashSet<Did>, 1134 1134 filter_to_targets: &HashSet<String>, 1135 - ) -> Result<PagedOrderedCollection<(String, Vec<RecordId>), String>> { 1135 + ) -> Result<PagedOrderedCollection<RecordsBySubject, String>> { 1136 1136 let collection = Collection(collection.to_string()); 1137 1137 let path = RPath(path.to_string()); 1138 1138 ··· 1241 1241 } 1242 1242 } 1243 1243 1244 - let mut items: Vec<(String, Vec<RecordId>)> = Vec::with_capacity(grouped_links.len()); 1244 + let mut items: Vec<RecordsBySubject> = Vec::with_capacity(grouped_links.len()); 1245 1245 for (fwd_target_id, records) in &grouped_links { 1246 1246 let Some(target_key) = self 1247 1247 .target_id_table ··· 1253 1253 1254 1254 let target_string = target_key.0 .0; 1255 1255 1256 - items.push((target_string, records.clone())); 1256 + items.push(RecordsBySubject { 1257 + subject: target_string, 1258 + records: records.clone(), 1259 + }); 1257 1260 } 1258 1261 1259 1262 let next = if grouped_links.len() as u64 >= limit {
+3 -3
constellation/templates/get-many-to-many.html.j2
··· 23 23 24 24 <h3>Many-to-many links, most recent first:</h3> 25 25 26 - {% for (target, records) in linking_records %} 27 - <h4>Target: <code>{{ target }}</code> <small>(<a href="/links/all?target={{ target|urlencode }}">view all links</a>)</small></h4> 28 - {% for record in records %} 26 + {% for group in linking_records %} 27 + <h4>Target: <code>{{ group.subject }}</code> <small>(<a href="/links/all?target={{ group.subject|urlencode }}">view all links</a>)</small></h4> 28 + {% for record in group.records %} 29 29 <pre style="display: block; margin: 1em 2em" class="code"><strong>DID</strong>: {{ record.did().0 }} 30 30 <strong>Collection</strong>: {{ record.collection }} 31 31 <strong>RKey</strong>: {{ record.rkey }}
+109
lexicons/blue.microcosm/links/getManyToMany.json
··· 1 + { 2 + "lexicon": 1, 3 + "id": "blue.microcosm.links.getManyToMany", 4 + "defs": { 5 + "main": { 6 + "type": "query", 7 + "description": "Get records that link to a primary subject, grouped by the secondary subjects they also reference", 8 + "parameters": { 9 + "type": "params", 10 + "required": ["subject", "source", "pathToOther"], 11 + "properties": { 12 + "subject": { 13 + "type": "string", 14 + "format": "uri", 15 + "description": "the primary target being linked to (at-uri, did, or uri)" 16 + }, 17 + "source": { 18 + "type": "string", 19 + "description": "collection and path specification for the primary link (e.g., 'app.bsky.feed.like:subject.uri')" 20 + }, 21 + "pathToOther": { 22 + "type": "string", 23 + "description": "path to the secondary link in the many-to-many record (e.g., 'otherThing.uri')" 24 + }, 25 + "did": { 26 + "type": "array", 27 + "description": "filter links to those from specific users", 28 + "items": { 29 + "type": "string", 30 + "format": "did" 31 + } 32 + }, 33 + "otherSubject": { 34 + "type": "array", 35 + "description": "filter secondary links to specific subjects", 36 + "items": { 37 + "type": "string" 38 + } 39 + }, 40 + "limit": { 41 + "type": "integer", 42 + "minimum": 1, 43 + "maximum": 100, 44 + "default": 16, 45 + "description": "number of results to return" 46 + } 47 + } 48 + }, 49 + "output": { 50 + "encoding": "application/json", 51 + "schema": { 52 + "type": "object", 53 + "required": ["linking_records"], 54 + "properties": { 55 + "linking_records": { 56 + "type": "array", 57 + "items": { 58 + "type": "ref", 59 + "ref": "#recordsBySubject" 60 + } 61 + }, 62 + "cursor": { 63 + "type": "string", 64 + "description": "pagination cursor" 65 + } 66 + } 67 + } 68 + } 69 + }, 70 + "recordsBySubject": { 71 + "type": "object", 72 + "required": ["subject", "records"], 73 + "properties": { 74 + "subject": { 75 + "type": "string", 76 + "description": "the secondary subject that these records link to" 77 + }, 78 + "records": { 79 + "type": "array", 80 + "items": { 81 + "type": "ref", 82 + "ref": "#linkRecord" 83 + } 84 + } 85 + } 86 + }, 87 + "linkRecord": { 88 + "type": "object", 89 + "required": ["did", "collection", "rkey"], 90 + "description": "A record identifier consisting of a DID, collection, and record key", 91 + "properties": { 92 + "did": { 93 + "type": "string", 94 + "format": "did", 95 + "description": "the DID of the linking record's repository" 96 + }, 97 + "collection": { 98 + "type": "string", 99 + "format": "nsid", 100 + "description": "the collection of the linking record" 101 + }, 102 + "rkey": { 103 + "type": "string", 104 + "format": "record-key" 105 + } 106 + } 107 + } 108 + } 109 + }