+6
constellation/src/lib.rs
+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
+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
+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
+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
+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
+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
+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
+
}