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