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

Convert existing REST /links/count endpoint to XRPC equivalent #8

closed opened by seoul.systems targeting main from seoul.systems/microcosm-rs: xrpc_backlinks_count

Add getCounts XRPC equivalent to REST /links/count

Simple conversion of the existing endpoint from REST to XRPC. Consequtively marked the pre-existing REST endpoint as deprecated.

In addition we now ignore rocks.test

Labels

None yet.

Participants 2
AT URI
at://did:plc:53wellrw53o7sw4zlpfenvuh/sh.tangled.repo.pull/3mcybnzocig22
+5 -708
Interdiff #0 โ†’ #1
.gitignore

This file has not been changed.

.prettierrc

This file has not been changed.

+1 -7
constellation/src/lib.rs
··· 31 } 32 } 33 34 - #[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] 35 pub struct RecordId { 36 pub did: Did, 37 pub collection: String, ··· 48 pub fn rkey(&self) -> String { 49 self.rkey.clone() 50 } 51 - } 52 - 53 - #[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)] 54 - pub struct RecordsBySubject { 55 - pub subject: String, 56 - pub records: Vec<RecordId>, 57 } 58 59 /// maybe the worst type in this repo, and there are some bad types
··· 31 } 32 } 33 34 + #[derive(Debug, PartialEq, Serialize, Deserialize)] 35 pub struct RecordId { 36 pub did: Did, 37 pub collection: String, ··· 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
+1 -103
constellation/src/server/mod.rs
··· 18 use tokio_util::sync::CancellationToken; 19 20 use crate::storage::{LinkReader, StorageStats}; 21 - use crate::{CountsByCount, Did, RecordId, RecordsBySubject}; 22 23 mod acceptable; 24 mod filters; ··· 97 .await 98 .map_err(to500)? 99 } 100 - }), 101 - ) 102 - .route( 103 - "/xrpc/blue.microcosm.links.getManyToMany", 104 - get({ 105 - let store = store.clone(); 106 - move |accept, query| async { 107 - spawn_blocking(|| get_many_to_many(accept, query, store)) 108 - .await 109 - .map_err(to500)? 110 - } 111 112 113 ··· 642 643 644 645 - } 646 - 647 - #[derive(Clone, Deserialize)] 648 - #[serde(rename_all = "camelCase")] 649 - struct GetManyToManyItemsQuery { 650 - subject: String, 651 - source: String, 652 - /// path to the secondary link in the linking record 653 - path_to_other: String, 654 - /// filter to linking records (join of the m2m) by these DIDs 655 - #[serde(default)] 656 - did: Vec<String>, 657 - /// filter to specific secondary records 658 - #[serde(default)] 659 - other_subject: Vec<String>, 660 - cursor: Option<OpaqueApiCursor>, 661 - #[serde(default = "get_default_cursor_limit")] 662 - limit: u64, 663 - } 664 - #[derive(Template, Serialize)] 665 - #[template(path = "get-many-to-many.html.j2")] 666 - struct GetManyToManyItemsResponse { 667 - linking_records: Vec<RecordsBySubject>, 668 - cursor: Option<OpaqueApiCursor>, 669 - #[serde(skip_serializing)] 670 - query: GetManyToManyItemsQuery, 671 - } 672 - fn get_many_to_many( 673 - accept: ExtractAccept, 674 - query: axum_extra::extract::Query<GetManyToManyItemsQuery>, // supports multiple param occurrences 675 - store: impl LinkReader, 676 - ) -> Result<impl IntoResponse, http::StatusCode> { 677 - let after = query 678 - .cursor 679 - .clone() 680 - .map(|oc| ApiKeyedCursor::try_from(oc).map_err(|_| http::StatusCode::BAD_REQUEST)) 681 - .transpose()? 682 - .map(|c| c.next); 683 - 684 - let limit = query.limit; 685 - if limit > DEFAULT_CURSOR_LIMIT_MAX { 686 - return Err(http::StatusCode::BAD_REQUEST); 687 - } 688 - 689 - let filter_dids: HashSet<Did> = HashSet::from_iter( 690 - query 691 - .did 692 - .iter() 693 - .map(|d| d.trim()) 694 - .filter(|d| !d.is_empty()) 695 - .map(|d| Did(d.to_string())), 696 - ); 697 - 698 - let filter_other_subjects: HashSet<String> = HashSet::from_iter( 699 - query 700 - .other_subject 701 - .iter() 702 - .map(|s| s.trim().to_string()) 703 - .filter(|s| !s.is_empty()), 704 - ); 705 - 706 - let Some((collection, path)) = query.source.split_once(':') else { 707 - return Err(http::StatusCode::BAD_REQUEST); 708 - }; 709 - let path = format!(".{path}"); 710 - 711 - let path_to_other = format!(".{}", query.path_to_other); 712 - 713 - let paged = store 714 - .get_many_to_many( 715 - &query.subject, 716 - collection, 717 - &path, 718 - &path_to_other, 719 - limit, 720 - after, 721 - &filter_dids, 722 - &filter_other_subjects, 723 - ) 724 - .map_err(|_| http::StatusCode::INTERNAL_SERVER_ERROR)?; 725 - 726 - let cursor = paged.next.map(|next| ApiKeyedCursor { next }.into()); 727 - 728 - Ok(acceptable( 729 - accept, 730 - GetManyToManyItemsResponse { 731 - linking_records: paged.items, 732 - cursor, 733 - query: (*query).clone(), 734 - }, 735 - )) 736 } 737 738 #[derive(Clone, Deserialize)]
··· 18 use tokio_util::sync::CancellationToken; 19 20 use crate::storage::{LinkReader, StorageStats}; 21 + use crate::{CountsByCount, Did, RecordId}; 22 23 mod acceptable; 24 mod filters; ··· 97 .await 98 .map_err(to500)? 99 } 100 101 102 ··· 631 632 633 634 } 635 636 #[derive(Clone, Deserialize)]
+1 -97
constellation/src/storage/mem_store.rs
··· 1 use super::{ 2 LinkReader, LinkStorage, PagedAppendingCollection, PagedOrderedCollection, StorageStats, 3 }; 4 - use crate::{ActionableEvent, CountsByCount, Did, RecordId, RecordsBySubject}; 5 use anyhow::Result; 6 use links::CollectedLink; 7 use std::collections::{HashMap, HashSet}; ··· 232 .map(|(did, _)| did) 233 .collect::<HashSet<_>>() 234 .len() as u64) 235 - } 236 - 237 - fn get_many_to_many( 238 - &self, 239 - target: &str, 240 - collection: &str, 241 - path: &str, 242 - path_to_other: &str, 243 - limit: u64, 244 - after: Option<String>, 245 - filter_dids: &HashSet<Did>, 246 - filter_to_targets: &HashSet<String>, 247 - ) -> Result<PagedOrderedCollection<RecordsBySubject, String>> { 248 - let empty_res = Ok(PagedOrderedCollection { 249 - items: Vec::new(), 250 - next: None, 251 - }); 252 - 253 - // struct MemStorageData { 254 - // dids: HashMap<Did, bool>, 255 - // targets: HashMap<Target, HashMap<Source, Linkers>>, 256 - // links: HashMap<Did, HashMap<RepoId, Vec<(RecordPath, Target)>>>, 257 - // } 258 - let data = self.0.lock().unwrap(); 259 - 260 - let Some(sources) = data.targets.get(&Target::new(target)) else { 261 - return empty_res; 262 - }; 263 - let Some(linkers) = sources.get(&Source::new(collection, path)) else { 264 - return empty_res; 265 - }; 266 - let path_to_other = RecordPath::new(path_to_other); 267 - 268 - // Convert filter_to_targets to Target objects for comparison 269 - let filter_to_target_objs: HashSet<Target> = 270 - HashSet::from_iter(filter_to_targets.iter().map(|s| Target::new(s))); 271 - 272 - let mut grouped_links: HashMap<Target, Vec<RecordId>> = HashMap::new(); 273 - for (did, rkey) in linkers.iter().flatten().cloned() { 274 - // Filter by DID if filter is provided 275 - if !filter_dids.is_empty() && !filter_dids.contains(&did) { 276 - continue; 277 - } 278 - if let Some(fwd_target) = data 279 - .links 280 - .get(&did) 281 - .unwrap_or(&HashMap::new()) 282 - .get(&RepoId { 283 - collection: collection.to_string(), 284 - rkey: rkey.clone(), 285 - }) 286 - .unwrap_or(&Vec::new()) 287 - .iter() 288 - .find_map(|(path, target)| { 289 - if *path == path_to_other 290 - && (filter_to_target_objs.is_empty() 291 - || filter_to_target_objs.contains(target)) 292 - { 293 - Some(target) 294 - } else { 295 - None 296 - } 297 - }) 298 - { 299 - let record_ids = grouped_links.entry(fwd_target.clone()).or_default(); 300 - record_ids.push(RecordId { 301 - did, 302 - collection: collection.to_string(), 303 - rkey: rkey.0, 304 - }); 305 - } 306 - } 307 - 308 - let mut items = grouped_links 309 - .into_iter() 310 - .map(|(t, r)| RecordsBySubject { 311 - subject: t.0, 312 - records: r, 313 - }) 314 - .collect::<Vec<_>>(); 315 - 316 - items.sort_by(|a, b| a.subject.cmp(&b.subject)); 317 - 318 - items = items 319 - .into_iter() 320 - .skip_while(|item| after.as_ref().map(|a| &item.subject <= a).unwrap_or(false)) 321 - .take(limit as usize) 322 - .collect(); 323 - 324 - let next = if items.len() as u64 >= limit { 325 - items.last().map(|item| item.subject.clone()) 326 - } else { 327 - None 328 - }; 329 - 330 - Ok(PagedOrderedCollection { items, next }) 331 } 332 333 fn get_links(
··· 1 use super::{ 2 LinkReader, LinkStorage, PagedAppendingCollection, PagedOrderedCollection, StorageStats, 3 }; 4 + use crate::{ActionableEvent, CountsByCount, Did, RecordId}; 5 use anyhow::Result; 6 use links::CollectedLink; 7 use std::collections::{HashMap, HashSet}; ··· 232 .map(|(did, _)| did) 233 .collect::<HashSet<_>>() 234 .len() as u64) 235 } 236 237 fn get_links(
+1 -245
constellation/src/storage/mod.rs
··· 1 - use crate::{ActionableEvent, CountsByCount, Did, RecordId, RecordsBySubject}; 2 use anyhow::Result; 3 use serde::{Deserialize, Serialize}; 4 use std::collections::{HashMap, HashSet}; ··· 103 104 fn get_all_record_counts(&self, _target: &str) 105 -> Result<HashMap<String, HashMap<String, u64>>>; 106 - 107 - fn get_many_to_many( 108 - &self, 109 - target: &str, 110 - collection: &str, 111 - path: &str, 112 - path_to_other: &str, 113 - limit: u64, 114 - after: Option<String>, 115 - filter_dids: &HashSet<Did>, 116 - filter_to_targets: &HashSet<String>, 117 - ) -> Result<PagedOrderedCollection<RecordsBySubject, String>>; 118 119 fn get_all_counts( 120 &self, ··· 1563 next: None, 1564 } 1565 ); 1566 - }); 1567 - 1568 - test_each_storage!(get_m2m_empty, |storage| { 1569 - assert_eq!( 1570 - storage.get_many_to_many( 1571 - "a.com", 1572 - "a.b.c", 1573 - ".d.e", 1574 - ".f.g", 1575 - 10, 1576 - None, 1577 - &HashSet::new(), 1578 - &HashSet::new(), 1579 - )?, 1580 - PagedOrderedCollection { 1581 - items: vec![], 1582 - next: None, 1583 - } 1584 - ); 1585 - }); 1586 - 1587 - test_each_storage!(get_m2m_single, |storage| { 1588 - storage.push( 1589 - &ActionableEvent::CreateLinks { 1590 - record_id: RecordId { 1591 - did: "did:plc:asdf".into(), 1592 - collection: "app.t.c".into(), 1593 - rkey: "asdf".into(), 1594 - }, 1595 - links: vec![ 1596 - CollectedLink { 1597 - target: Link::Uri("a.com".into()), 1598 - path: ".abc.uri".into(), 1599 - }, 1600 - CollectedLink { 1601 - target: Link::Uri("b.com".into()), 1602 - path: ".def.uri".into(), 1603 - }, 1604 - CollectedLink { 1605 - target: Link::Uri("b.com".into()), 1606 - path: ".ghi.uri".into(), 1607 - }, 1608 - ], 1609 - }, 1610 - 0, 1611 - )?; 1612 - assert_eq!( 1613 - storage.get_many_to_many( 1614 - "a.com", 1615 - "app.t.c", 1616 - ".abc.uri", 1617 - ".def.uri", 1618 - 10, 1619 - None, 1620 - &HashSet::new(), 1621 - &HashSet::new(), 1622 - )?, 1623 - PagedOrderedCollection { 1624 - items: vec![RecordsBySubject { 1625 - subject: "b.com".to_string(), 1626 - records: vec![RecordId { 1627 - did: "did:plc:asdf".into(), 1628 - collection: "app.t.c".into(), 1629 - rkey: "asdf".into(), 1630 - }] 1631 - }], 1632 - next: None, 1633 - } 1634 - ); 1635 - }); 1636 - 1637 - test_each_storage!(get_m2m_filters, |storage| { 1638 - storage.push( 1639 - &ActionableEvent::CreateLinks { 1640 - record_id: RecordId { 1641 - did: "did:plc:asdf".into(), 1642 - collection: "app.t.c".into(), 1643 - rkey: "asdf".into(), 1644 - }, 1645 - links: vec![ 1646 - CollectedLink { 1647 - target: Link::Uri("a.com".into()), 1648 - path: ".abc.uri".into(), 1649 - }, 1650 - CollectedLink { 1651 - target: Link::Uri("b.com".into()), 1652 - path: ".def.uri".into(), 1653 - }, 1654 - ], 1655 - }, 1656 - 0, 1657 - )?; 1658 - storage.push( 1659 - &ActionableEvent::CreateLinks { 1660 - record_id: RecordId { 1661 - did: "did:plc:asdf".into(), 1662 - collection: "app.t.c".into(), 1663 - rkey: "asdf2".into(), 1664 - }, 1665 - links: vec![ 1666 - CollectedLink { 1667 - target: Link::Uri("a.com".into()), 1668 - path: ".abc.uri".into(), 1669 - }, 1670 - CollectedLink { 1671 - target: Link::Uri("b.com".into()), 1672 - path: ".def.uri".into(), 1673 - }, 1674 - ], 1675 - }, 1676 - 1, 1677 - )?; 1678 - storage.push( 1679 - &ActionableEvent::CreateLinks { 1680 - record_id: RecordId { 1681 - did: "did:plc:fdsa".into(), 1682 - collection: "app.t.c".into(), 1683 - rkey: "fdsa".into(), 1684 - }, 1685 - links: vec![ 1686 - CollectedLink { 1687 - target: Link::Uri("a.com".into()), 1688 - path: ".abc.uri".into(), 1689 - }, 1690 - CollectedLink { 1691 - target: Link::Uri("c.com".into()), 1692 - path: ".def.uri".into(), 1693 - }, 1694 - ], 1695 - }, 1696 - 2, 1697 - )?; 1698 - storage.push( 1699 - &ActionableEvent::CreateLinks { 1700 - record_id: RecordId { 1701 - did: "did:plc:fdsa".into(), 1702 - collection: "app.t.c".into(), 1703 - rkey: "fdsa2".into(), 1704 - }, 1705 - links: vec![ 1706 - CollectedLink { 1707 - target: Link::Uri("a.com".into()), 1708 - path: ".abc.uri".into(), 1709 - }, 1710 - CollectedLink { 1711 - target: Link::Uri("c.com".into()), 1712 - path: ".def.uri".into(), 1713 - }, 1714 - ], 1715 - }, 1716 - 3, 1717 - )?; 1718 - 1719 - // Test without filters - should get all records grouped by secondary target 1720 - let result = storage.get_many_to_many( 1721 - "a.com", 1722 - "app.t.c", 1723 - ".abc.uri", 1724 - ".def.uri", 1725 - 10, 1726 - None, 1727 - &HashSet::new(), 1728 - &HashSet::new(), 1729 - )?; 1730 - assert_eq!(result.items.len(), 2); 1731 - assert_eq!(result.next, None); 1732 - // Find b.com group 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 1741 - .records 1742 - .iter() 1743 - .any(|r| r.did.0 == "did:plc:asdf" && r.rkey == "asdf")); 1744 - assert!(b_group 1745 - .records 1746 - .iter() 1747 - .any(|r| r.did.0 == "did:plc:asdf" && r.rkey == "asdf2")); 1748 - // Find c.com group 1749 - let c_group = result 1750 - .items 1751 - .iter() 1752 - .find(|group| group.subject == "c.com") 1753 - .unwrap(); 1754 - assert_eq!(c_group.subject, "c.com"); 1755 - assert_eq!(c_group.records.len(), 2); 1756 - assert!(c_group 1757 - .records 1758 - .iter() 1759 - .any(|r| r.did.0 == "did:plc:fdsa" && r.rkey == "fdsa")); 1760 - assert!(c_group 1761 - .records 1762 - .iter() 1763 - .any(|r| r.did.0 == "did:plc:fdsa" && r.rkey == "fdsa2")); 1764 - 1765 - // Test with DID filter - should only get records from did:plc:fdsa 1766 - let result = storage.get_many_to_many( 1767 - "a.com", 1768 - "app.t.c", 1769 - ".abc.uri", 1770 - ".def.uri", 1771 - 10, 1772 - None, 1773 - &HashSet::from_iter([Did("did:plc:fdsa".to_string())]), 1774 - &HashSet::new(), 1775 - )?; 1776 - assert_eq!(result.items.len(), 1); 1777 - let group = &result.items[0]; 1778 - assert_eq!(group.subject, "c.com"); 1779 - assert_eq!(group.records.len(), 2); 1780 - assert!(group.records.iter().all(|r| r.did.0 == "did:plc:fdsa")); 1781 - 1782 - // Test with target filter - should only get records linking to b.com 1783 - let result = storage.get_many_to_many( 1784 - "a.com", 1785 - "app.t.c", 1786 - ".abc.uri", 1787 - ".def.uri", 1788 - 10, 1789 - None, 1790 - &HashSet::new(), 1791 - &HashSet::from_iter(["b.com".to_string()]), 1792 - )?; 1793 - assert_eq!(result.items.len(), 1); 1794 - let group = &result.items[0]; 1795 - assert_eq!(group.subject, "b.com"); 1796 - assert_eq!(group.records.len(), 2); 1797 - assert!(group.records.iter().all(|r| r.did.0 == "did:plc:asdf")); 1798 }); 1799 }
··· 1 + use crate::{ActionableEvent, CountsByCount, Did, RecordId}; 2 use anyhow::Result; 3 use serde::{Deserialize, Serialize}; 4 use std::collections::{HashMap, HashSet}; ··· 103 104 fn get_all_record_counts(&self, _target: &str) 105 -> Result<HashMap<String, HashMap<String, u64>>>; 106 107 fn get_all_counts( 108 &self, ··· 1551 next: None, 1552 } 1553 ); 1554 }); 1555 }
+1 -147
constellation/src/storage/rocks_store.rs
··· 2 ActionableEvent, LinkReader, LinkStorage, PagedAppendingCollection, PagedOrderedCollection, 3 StorageStats, 4 }; 5 - use crate::{CountsByCount, Did, RecordId, RecordsBySubject}; 6 use anyhow::{bail, Result}; 7 use bincode::Options as BincodeOptions; 8 use links::CollectedLink; ··· 1120 } else { 1121 Ok(0) 1122 } 1123 - } 1124 - 1125 - fn get_many_to_many( 1126 - &self, 1127 - target: &str, 1128 - collection: &str, 1129 - path: &str, 1130 - path_to_other: &str, 1131 - limit: u64, 1132 - after: Option<String>, 1133 - filter_dids: &HashSet<Did>, 1134 - filter_to_targets: &HashSet<String>, 1135 - ) -> Result<PagedOrderedCollection<RecordsBySubject, String>> { 1136 - let collection = Collection(collection.to_string()); 1137 - let path = RPath(path.to_string()); 1138 - 1139 - let target_key = TargetKey(Target(target.to_string()), collection.clone(), path); 1140 - 1141 - let after = after.map(|s| s.parse::<u64>().map(TargetId)).transpose()?; 1142 - 1143 - let Some(target_id) = self.target_id_table.get_id_val(&self.db, &target_key)? else { 1144 - eprintln!("Target not found for {target_key:?}"); 1145 - return Ok(Default::default()); 1146 - }; 1147 - 1148 - let filter_did_ids: HashMap<DidId, bool> = filter_dids 1149 - .iter() 1150 - .filter_map(|did| self.did_id_table.get_id_val(&self.db, did).transpose()) 1151 - .collect::<Result<Vec<DidIdValue>>>()? 1152 - .into_iter() 1153 - .map(|DidIdValue(id, active)| (id, active)) 1154 - .collect(); 1155 - 1156 - let mut filter_to_target_ids: HashSet<TargetId> = HashSet::new(); 1157 - for t in filter_to_targets { 1158 - for (_, target_id) in self.iter_targets_for_target(&Target(t.to_string())) { 1159 - filter_to_target_ids.insert(target_id); 1160 - } 1161 - } 1162 - 1163 - let linkers = self.get_target_linkers(&target_id)?; 1164 - 1165 - // we want to provide many to many which effectively means that we want to show a specific 1166 - // list of reords that is linked to by a specific number of linkers 1167 - let mut grouped_links: BTreeMap<TargetId, Vec<RecordId>> = BTreeMap::new(); 1168 - for (did_id, rkey) in linkers.0 { 1169 - if did_id.is_empty() { 1170 - continue; 1171 - } 1172 - 1173 - if !filter_did_ids.is_empty() && filter_did_ids.get(&did_id) != Some(&true) { 1174 - continue; 1175 - } 1176 - 1177 - // Make sure the current did is active 1178 - let Some(did) = self.did_id_table.get_val_from_id(&self.db, did_id.0)? else { 1179 - eprintln!("failed to look up did from did_id {did_id:?}"); 1180 - continue; 1181 - }; 1182 - let Some(DidIdValue(_, active)) = self.did_id_table.get_id_val(&self.db, &did)? else { 1183 - eprintln!("failed to look up did_value from did_id {did_id:?}: {did:?}: data consistency bug?"); 1184 - continue; 1185 - }; 1186 - if !active { 1187 - continue; 1188 - } 1189 - 1190 - let record_link_key = RecordLinkKey(did_id, collection.clone(), rkey.clone()); 1191 - let Some(targets) = self.get_record_link_targets(&record_link_key)? else { 1192 - continue; 1193 - }; 1194 - 1195 - let Some(fwd_target) = targets 1196 - .0 1197 - .into_iter() 1198 - .filter_map(|RecordLinkTarget(rpath, target_id)| { 1199 - if rpath.0 == path_to_other 1200 - && (filter_to_target_ids.is_empty() 1201 - || filter_to_target_ids.contains(&target_id)) 1202 - { 1203 - Some(target_id) 1204 - } else { 1205 - None 1206 - } 1207 - }) 1208 - .take(1) 1209 - .next() 1210 - else { 1211 - eprintln!("no forward match found."); 1212 - continue; 1213 - }; 1214 - 1215 - // pagination logic mirrors what is currently done in get_many_to_many_counts 1216 - if after.as_ref().map(|a| fwd_target <= *a).unwrap_or(false) { 1217 - continue; 1218 - } 1219 - let page_is_full = grouped_links.len() as u64 >= limit; 1220 - if page_is_full { 1221 - let current_max = grouped_links.keys().next_back().unwrap(); 1222 - if fwd_target > *current_max { 1223 - continue; 1224 - } 1225 - } 1226 - 1227 - // pagination, continued 1228 - let mut should_evict = false; 1229 - let entry = grouped_links.entry(fwd_target.clone()).or_insert_with(|| { 1230 - should_evict = page_is_full; 1231 - Vec::default() 1232 - }); 1233 - entry.push(RecordId { 1234 - did, 1235 - collection: collection.0.clone(), 1236 - rkey: rkey.0, 1237 - }); 1238 - 1239 - if should_evict { 1240 - grouped_links.pop_last(); 1241 - } 1242 - } 1243 - 1244 - let mut items: Vec<RecordsBySubject> = Vec::with_capacity(grouped_links.len()); 1245 - for (fwd_target_id, records) in &grouped_links { 1246 - let Some(target_key) = self 1247 - .target_id_table 1248 - .get_val_from_id(&self.db, fwd_target_id.0)? 1249 - else { 1250 - eprintln!("failed to look up target from target_id {fwd_target_id:?}"); 1251 - continue; 1252 - }; 1253 - 1254 - let target_string = target_key.0 .0; 1255 - 1256 - items.push(RecordsBySubject { 1257 - subject: target_string, 1258 - records: records.clone(), 1259 - }); 1260 - } 1261 - 1262 - let next = if grouped_links.len() as u64 >= limit { 1263 - grouped_links.keys().next_back().map(|k| format!("{}", k.0)) 1264 - } else { 1265 - None 1266 - }; 1267 - 1268 - Ok(PagedOrderedCollection { items, next }) 1269 } 1270 1271 fn get_links(
··· 2 ActionableEvent, LinkReader, LinkStorage, PagedAppendingCollection, PagedOrderedCollection, 3 StorageStats, 4 }; 5 + use crate::{CountsByCount, Did, RecordId}; 6 use anyhow::{bail, Result}; 7 use bincode::Options as BincodeOptions; 8 use links::CollectedLink; ··· 1120 } else { 1121 Ok(0) 1122 } 1123 } 1124 1125 fn get_links(
constellation/templates/dids.html.j2

This file has not been changed.

constellation/templates/get-counts.html.j2

This file has not been changed.

-60
constellation/templates/get-many-to-many.html.j2
··· 1 - {% extends "base.html.j2" %} 2 - {% import "try-it-macros.html.j2" as try_it %} 3 - 4 - {% block title %}Many-to-Many Links{% endblock %} 5 - {% block description %}All {{ query.source }} records with many-to-many links to {{ query.subject }} joining through {{ query.path_to_other }}{% endblock %} 6 - 7 - {% block content %} 8 - 9 - {% call try_it::get_many_to_many(query.subject, query.source, query.path_to_other, query.did, query.other_subject, query.limit) %} 10 - 11 - <h2> 12 - Many-to-many links to <code>{{ query.subject }}</code> 13 - {% if let Some(browseable_uri) = query.subject|to_browseable %} 14 - <small style="font-weight: normal; font-size: 1rem"><a href="{{ browseable_uri }}">browse record</a></small> 15 - {% endif %} 16 - </h2> 17 - 18 - <p><strong>Many-to-many links</strong> from <code>{{ query.source }}</code> joining through <code>{{ query.path_to_other }}</code></p> 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 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 - <pre style="display: block; margin: 1em 2em" class="code"><strong>DID</strong>: {{ record.did().0 }} 30 - <strong>Collection</strong>: {{ record.collection }} 31 - <strong>RKey</strong>: {{ record.rkey }} 32 - -> <a href="https://pdsls.dev/at://{{ record.did().0 }}/{{ record.collection }}/{{ record.rkey }}">browse record</a></pre> 33 - {% endfor %} 34 - {% endfor %} 35 - 36 - {% if let Some(c) = cursor %} 37 - <form method="get" action="/xrpc/blue.microcosm.links.getManyToMany"> 38 - <input type="hidden" name="subject" value="{{ query.subject }}" /> 39 - <input type="hidden" name="source" value="{{ query.source }}" /> 40 - <input type="hidden" name="pathToOther" value="{{ query.path_to_other }}" /> 41 - {% for did in query.did %} 42 - <input type="hidden" name="did" value="{{ did }}" /> 43 - {% endfor %} 44 - {% for other in query.other_subject %} 45 - <input type="hidden" name="otherSubject" value="{{ other }}" /> 46 - {% endfor %} 47 - <input type="hidden" name="limit" value="{{ query.limit }}" /> 48 - <input type="hidden" name="cursor" value={{ c|json|safe }} /> 49 - <button type="submit">next page&hellip;</button> 50 - </form> 51 - {% else %} 52 - <button disabled><em>end of results</em></button> 53 - {% endif %} 54 - 55 - <details> 56 - <summary>Raw JSON response</summary> 57 - <pre class="code">{{ self|tojson }}</pre> 58 - </details> 59 - 60 - {% endblock %}
···
-19
constellation/templates/hello.html.j2
··· 81 ) %} 82 83 84 - <h3 class="route"><code>GET /xrpc/blue.microcosm.links.getManyToMany</code></h3> 85 - 86 - <p>A list of many-to-many join records linking to a target and a secondary target.</p> 87 - 88 - <h4>Query parameters:</h4> 89 - 90 - <ul> 91 - <li><p><code>subject</code>: required, must url-encode. Example: <code>at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r</code></p></li> 92 - <li><p><code>source</code>: required. Example: <code>app.bsky.feed.like:subject.uri</code></p></li> 93 - <li><p><code>pathToOther</code>: required. Path to the secondary link in the many-to-many record. Example: <code>otherThing.uri</code></p></li> 94 - <li><p><code>did</code>: optional, filter links to those from specific users. Include multiple times to filter by multiple users. Example: <code>did=did:plc:vc7f4oafdgxsihk4cry2xpze&did=did:plc:vc7f4oafdgxsihk4cry2xpze</code></p></li> 95 - <li><p><code>otherSubject</code>: optional, filter secondary links to specific subjects. Include multiple times to filter by multiple subjects. Example: <code>at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r</code></p></li> 96 - <li><p><code>limit</code>: optional. Default: <code>16</code>. Maximum: <code>100</code></p></li> 97 - </ul> 98 - 99 - <p style="margin-bottom: 0"><strong>Try it:</strong></p> 100 - {% 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) %} 101 - 102 - 103 <h3 class="route"><code>GET /links</code></h3> 104 105 <p>A list of records linking to a target.</p>
··· 81 ) %} 82 83 84 <h3 class="route"><code>GET /links</code></h3> 85 86 <p>A list of records linking to a target.</p>
-30
constellation/templates/try-it-macros.html.j2
··· 66 </script> 67 {% endmacro %} 68 69 - {% macro get_many_to_many(subject, source, pathToOther, dids, otherSubjects, limit) %} 70 - <form method="get" action="/xrpc/blue.microcosm.links.getManyToMany"> 71 - <pre class="code"><strong>GET</strong> /xrpc/blue.microcosm.links.getManyToMany 72 - ?subject= <input type="text" name="subject" value="{{ subject }}" placeholder="at-uri, did, uri..." /> 73 - &source= <input type="text" name="source" value="{{ source }}" placeholder="app.bsky.feed.like:subject" /> 74 - &pathToOther= <input type="text" name="pathToOther" value="{{ pathToOther }}" placeholder="otherThing" /> 75 - {%- for did in dids %}{% if !did.is_empty() %} 76 - &did= <input type="text" name="did" value="{{ did }}" placeholder="did:plc:..." />{% endif %}{% endfor %} 77 - <span id="m2m-did-placeholder"></span> <button id="m2m-add-did">+ did filter</button> 78 - {%- for otherSubject in otherSubjects %}{% if !otherSubject.is_empty() %} 79 - &otherSubject= <input type="text" name="otherSubject" value="{{ otherSubject }}" placeholder="at-uri, did, uri..." />{% endif %}{% endfor %} 80 - <span id="m2m-other-placeholder"></span> <button id="m2m-add-other">+ other subject filter</button> 81 - &limit= <input type="number" name="limit" value="{{ limit }}" max="100" placeholder="100" /> <button type="submit">get many-to-many links</button></pre> 82 - </form> 83 - <script> 84 - const m2mAddDidButton = document.getElementById('m2m-add-did'); 85 - const m2mDidPlaceholder = document.getElementById('m2m-did-placeholder'); 86 - m2mAddDidButton.addEventListener('click', e => { 87 - e.preventDefault(); 88 - const i = document.createElement('input'); 89 - i.placeholder = 'did:plc:...'; 90 - i.name = "did" 91 - const p = m2mAddDidButton.parentNode; 92 - p.insertBefore(document.createTextNode('&did= '), m2mDidPlaceholder); 93 - p.insertBefore(i, m2mDidPlaceholder); 94 - p.insertBefore(document.createTextNode('\n '), m2mDidPlaceholder); 95 - }); 96 - </script> 97 - {% endmacro %} 98 - 99 {% macro links(target, collection, path, dids, limit) %} 100 <form method="get" action="/links"> 101 <pre class="code"><strong>GET</strong> /links
··· 66 </script> 67 {% endmacro %} 68 69 {% macro links(target, collection, path, dids, limit) %} 70 <form method="get" action="/links"> 71 <pre class="code"><strong>GET</strong> /links
lexicons/blue.microcosm/links/getBacklinks.json

This file has not been changed.

lexicons/blue.microcosm/links/getCounts.json

This file has not been changed.

lexicons/blue.microcosm/links/getManyToMany.json

This file has not been changed.

lexicons/blue.microcosm/links/getManyToManyCounts.json

This file has not been changed.

spacedust/src/error.rs

This file has not been changed.

spacedust/src/server.rs

This file has not been changed.

spacedust/src/subscriber.rs

This file has not been changed.

History

8 rounds 13 comments
sign up or login to add to the discussion
8 commits
expand
Add getCounts XRPC equivalent to REST /links/count
Modify backlinks counting XRPC endpoint name
Mark /links/count REST endpoint as deprecated
Remove .uri suffix
Reformat existing lexicons
Remove wrongly commited getManyToMany lexicon
Fix Git whitespace error in "hello" template
Format .prettierrc and fix Git whitespace error
expand 3 comments

I'm giving up on the whitespace issue...

max@max-mbpro ~/dev/microcosm-rs (xrpc_backlinks_count) $ git diff --check upstream/main
max@max-mbpro ~/dev/microcosm-rs (xrpc_backlinks_count) $

git-diff --checked doesn't indicate any errors either.

As far as I can tell this seems to be a confirmed Tangled issue

happy to do merging locally as needed!

merged. thanks!

closed without merging
7 commits
expand
Add getCounts XRPC equivalent to REST /links/count
Modify backlinks counting XRPC endpoint name
Mark /links/count REST endpoint as deprecated
Remove .uri suffix
Reformat existing lexicons
Remove wrongly commited getManyToMany lexicon
Fix Git whitespace error in "hello" template
expand 0 comments
6 commits
expand
Add getCounts XRPC equivalent to REST /links/count
Modify backlinks counting XRPC endpoint name
Mark /links/count REST endpoint as deprecated
Remove .uri suffix
Reformat existing lexicons
Remove wrongly commited getManyToMany lexicon
expand 0 comments
3 commits
expand
Add getCounts XRPC equivalent to REST /links/count
Modify backlinks counting XRPC endpoint name
Mark /links/count REST endpoint as deprecated
expand 4 comments

Tangled somehow complains about merge conflicts here, but I couldn't find any after rebasing on upstream/main again?!

very weird!

one tiny thing left: the source for blocks is app.bsky.graph.block:subject (no .uri suffix on the path) -- it's in the hello.html template.

i think some many-to-many order stuff ended up on this branch but i'm too tired for git rn.

if you don't get to it first i'm happy to fix the source and git stuff and merge when i can get to it :)

Sorry about the the m2m stuff that I didn't catch before opening the PR. I did clean this up again, and nothing belonging there should be remaining here; Addresses your above comment regarding the .uri suffix as well.

3 commits
expand
Add getCounts XRPC equivalent to REST /links/count
Modify backlinks counting XRPC endpoint name
Mark /links/count REST endpoint as deprecated
expand 0 comments
1 commit
expand
Add getCounts XRPC equivalent to REST /links/count
expand 6 comments

Had to do some rebasing and cleanup, but good to go now I think.

sweet!

(btw the local/ gitignore is there for doing local/rocks.test, but i'm fine adding it to the top-level gitignore too!)

i think links.getCounts is a little too broad -- what do you think of links.getBacklinksCount?

we also throw a deprecated warning under /links/count on the main page template, matching the one for /links.

it's weird but the whitespace in the forms in try-it-macros is significant (one of my moments of knowingly doing things the wrong way because i was bored, sorry!)

the new form for this endpoint needs little tweaking to match the others.

Ah got it. The rocks tests ended up cluttering the top-level directory so I just added them to .gitignore. I often keep a .local file in .gitignores to ignore whatever's supposed to stay in one's local copy and somehow assumed this had the same intent.

Changed the function and endpoint/method name as requested to get_backlinks_count/getBacklinksCount

The endpoint in the try-it-macro should now match the other existing ones down to how they're formatted with whitespace

3 commits
expand
Make metrics collection opt-in
Increase constellation response limits
Add getCounts XRPC equivalent to REST /links/count
expand 0 comments
7 commits
expand
Make metrics collection opt-in
Increase constellation response limits
wip: m2m
Add tests for new get_many_to_many query handler
Fix get_m2m_empty test
Replace tuple with RecordsBySubject struct
Add getCounts XRPC equivalent to REST /links/count
expand 0 comments