+12
-3
constellation/src/server/mod.rs
+12
-3
constellation/src/server/mod.rs
···
239
239
/// Set the max number of links to return per page of results
240
240
#[serde(default = "get_default_cursor_limit")]
241
241
limit: u64,
242
+
/// Allow returning links in reverse order (default: false)
243
+
#[serde(default)]
244
+
reverse: bool,
242
245
}
243
246
#[derive(Serialize)]
244
247
struct OtherSubjectCount {
···
301
304
collection,
302
305
&path,
303
306
&path_to_other,
307
+
query.reverse,
304
308
limit,
305
309
cursor_key,
306
310
&filter_dids,
···
409
413
/// Set the max number of links to return per page of results
410
414
#[serde(default = "get_default_cursor_limit")]
411
415
limit: u64,
412
-
// TODO: allow reverse (er, forward) order as well
416
+
/// Allow returning links in reverse order (default: false)
417
+
#[serde(default)]
418
+
reverse: bool,
413
419
}
414
420
#[derive(Template, Serialize)]
415
421
#[template(path = "get-backlinks.html.j2")]
···
460
466
&query.subject,
461
467
collection,
462
468
&path,
469
+
query.reverse,
463
470
limit,
464
471
until,
465
472
&filter_dids,
···
508
515
from_dids: Option<String>, // comma separated: gross
509
516
#[serde(default = "get_default_cursor_limit")]
510
517
limit: u64,
511
-
// TODO: allow reverse (er, forward) order as well
518
+
/// Allow returning links in reverse order (default: false)
519
+
#[serde(default)]
520
+
reverse: bool,
512
521
}
513
522
#[derive(Template, Serialize)]
514
523
#[template(path = "links.html.j2")]
···
562
571
&query.target,
563
572
&query.collection,
564
573
&query.path,
574
+
query.reverse,
565
575
limit,
566
576
until,
567
577
&filter_dids,
···
594
604
path: String,
595
605
cursor: Option<OpaqueApiCursor>,
596
606
limit: Option<u64>,
597
-
// TODO: allow reverse (er, forward) order as well
598
607
}
599
608
#[derive(Template, Serialize)]
600
609
#[template(path = "dids.html.j2")]
+22
-6
constellation/src/storage/mem_store.rs
+22
-6
constellation/src/storage/mem_store.rs
···
140
140
collection: &str,
141
141
path: &str,
142
142
path_to_other: &str,
143
+
reverse: bool,
143
144
limit: u64,
144
145
after: Option<String>,
145
146
filter_dids: &HashSet<Did>,
···
157
158
let filter_to_targets: HashSet<Target> =
158
159
HashSet::from_iter(filter_to_targets.iter().map(|s| Target::new(s)));
159
160
160
-
let mut grouped_counts: HashMap<Target, (u64, HashSet<Did>)> = HashMap::new();
161
-
for (did, rkey) in linkers.iter().flatten().cloned() {
161
+
// the last type field here acts as an index to allow keeping track of the order in which
162
+
// we encountred single elements
163
+
let mut grouped_counts: HashMap<Target, (u64, HashSet<Did>, usize)> = HashMap::new();
164
+
for (idx, (did, rkey)) in linkers.iter().flatten().cloned().enumerate() {
162
165
if !filter_dids.is_empty() && !filter_dids.contains(&did) {
163
166
continue;
164
167
}
···
184
187
.take(1)
185
188
.next()
186
189
{
187
-
let e = grouped_counts.entry(fwd_target.clone()).or_default();
190
+
let e =
191
+
grouped_counts
192
+
.entry(fwd_target.clone())
193
+
.or_insert((0, HashSet::new(), idx));
188
194
e.0 += 1;
189
195
e.1.insert(did.clone());
190
196
}
191
197
}
192
198
let mut items: Vec<(String, u64, u64)> = grouped_counts
193
199
.iter()
194
-
.map(|(k, (n, u))| (k.0.clone(), *n, u.len() as u64))
200
+
.map(|(k, (n, u, _))| (k.0.clone(), *n, u.len() as u64))
195
201
.collect();
196
-
items.sort();
202
+
// sort in reverse order to show entries from oldest to newest
203
+
if reverse {
204
+
items.sort_by(|a, b| b.cmp(a));
205
+
} else {
206
+
items.sort();
207
+
}
197
208
items = items
198
209
.into_iter()
199
210
.skip_while(|(t, _, _)| after.as_ref().map(|a| t <= a).unwrap_or(false))
···
239
250
target: &str,
240
251
collection: &str,
241
252
path: &str,
253
+
reverse: bool,
242
254
limit: u64,
243
255
until: Option<u64>,
244
256
filter_dids: &HashSet<Did>,
···
261
273
});
262
274
};
263
275
264
-
let did_rkeys: Vec<_> = if !filter_dids.is_empty() {
276
+
let mut did_rkeys: Vec<_> = if !filter_dids.is_empty() {
265
277
did_rkeys
266
278
.iter()
267
279
.filter(|m| {
···
284
296
285
297
let alive = did_rkeys.iter().flatten().count();
286
298
let gone = total - alive;
299
+
300
+
if reverse {
301
+
did_rkeys.reverse();
302
+
}
287
303
288
304
let items: Vec<_> = did_rkeys[begin..end]
289
305
.iter()
+2
constellation/src/storage/mod.rs
+2
constellation/src/storage/mod.rs
···
72
72
collection: &str,
73
73
path: &str,
74
74
path_to_other: &str,
75
+
reverse: bool,
75
76
limit: u64,
76
77
after: Option<String>,
77
78
filter_dids: &HashSet<Did>,
···
87
88
target: &str,
88
89
collection: &str,
89
90
path: &str,
91
+
reverse: bool,
90
92
limit: u64,
91
93
until: Option<u64>,
92
94
filter_dids: &HashSet<Did>,
+15
-1
constellation/src/storage/rocks_store.rs
+15
-1
constellation/src/storage/rocks_store.rs
···
941
941
collection: &str,
942
942
path: &str,
943
943
path_to_other: &str,
944
+
reverse: bool,
944
945
limit: u64,
945
946
after: Option<String>,
946
947
filter_dids: &HashSet<Did>,
···
1071
1072
}
1072
1073
1073
1074
let mut items: Vec<(String, u64, u64)> = Vec::with_capacity(grouped_counts.len());
1075
+
1074
1076
for (target_id, (n, dids)) in &grouped_counts {
1075
1077
let Some(target) = self
1076
1078
.target_id_table
···
1080
1082
continue;
1081
1083
};
1082
1084
items.push((target.0 .0, *n, dids.len() as u64));
1085
+
}
1086
+
1087
+
// Sort in desired direction
1088
+
if reverse {
1089
+
items.sort_by(|a, b| b.cmp(a)); // descending
1090
+
} else {
1091
+
items.sort(); // ascending
1083
1092
}
1084
1093
1085
1094
let next = if grouped_counts.len() as u64 >= limit {
···
1127
1136
target: &str,
1128
1137
collection: &str,
1129
1138
path: &str,
1139
+
reverse: bool,
1130
1140
limit: u64,
1131
1141
until: Option<u64>,
1132
1142
filter_dids: &HashSet<Did>,
···
1171
1181
let begin = end.saturating_sub(limit as usize);
1172
1182
let next = if begin == 0 { None } else { Some(begin as u64) };
1173
1183
1174
-
let did_id_rkeys = linkers.0[begin..end].iter().rev().collect::<Vec<_>>();
1184
+
let mut did_id_rkeys = linkers.0[begin..end].iter().rev().collect::<Vec<_>>();
1185
+
1186
+
if reverse {
1187
+
did_id_rkeys.reverse();
1188
+
}
1175
1189
1176
1190
let mut items = Vec::with_capacity(did_id_rkeys.len());
1177
1191
// TODO: use get-many (or multi-get or whatever it's called)
+4
constellation/templates/base.html.j2
+4
constellation/templates/base.html.j2
+2
-1
constellation/templates/get-backlinks.html.j2
+2
-1
constellation/templates/get-backlinks.html.j2
···
6
6
7
7
{% block content %}
8
8
9
-
{% call try_it::get_backlinks(query.subject, query.source, query.did, query.limit) %}
9
+
{% call try_it::get_backlinks(query.subject, query.source, query.did, query.limit, query.reverse) %}
10
10
11
11
<h2>
12
12
Links to <code>{{ query.subject }}</code>
···
40
40
<input type="hidden" name="did" value="{{ did }}" />
41
41
{% endfor %}
42
42
<input type="hidden" name="cursor" value={{ c|json|safe }} />
43
+
<input type="hidden" name="reverse" value="{{ query.reverse }}">
43
44
<button type="submit">next page…</button>
44
45
</form>
45
46
{% else %}
+2
constellation/templates/get-many-to-many-counts.html.j2
+2
constellation/templates/get-many-to-many-counts.html.j2
···
13
13
query.did,
14
14
query.other_subject,
15
15
query.limit,
16
+
query.reverse,
16
17
) %}
17
18
18
19
<h2>
···
53
54
{% endfor %}
54
55
<input type="hidden" name="limit" value="{{ query.limit }}" />
55
56
<input type="hidden" name="cursor" value={{ c|json|safe }} />
57
+
<input type="hidden" name="reverse" value="{{ query.reverse }}">
56
58
<button type="submit">next page…</button>
57
59
</form>
58
60
{% else %}
+7
-2
constellation/templates/hello.html.j2
+7
-2
constellation/templates/hello.html.j2
···
49
49
<li><p><code>source</code>: required. Example: <code>app.bsky.feed.like:subject.uri</code></p></li>
50
50
<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>
51
51
<li><p><code>limit</code>: optional. Default: <code>16</code>. Maximum: <code>100</code></p></li>
52
+
<li><p><code>reverse</code>: optional, return links in reverse order. Default: <code>false</code></p></li>
52
53
</ul>
53
54
54
55
<p style="margin-bottom: 0"><strong>Try it:</strong></p>
55
-
{% call try_it::get_backlinks("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like:subject.uri", [""], 16) %}
56
+
{% call
57
+
try_it::get_backlinks("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like:subject.uri", [""], 16, false) %}
56
58
57
59
58
60
<h3 class="route"><code>GET /xrpc/blue.microcosm.links.getManyToManyCounts</code></h3>
···
68
70
<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>
69
71
<li><p><code>otherSubject</code>: optional, filter secondary links to specific subjects. Include multiple times to filter by multiple users. Example: <code>at://did:plc:vc7f4oafdgxsihk4cry2xpze/app.bsky.feed.post/3lgwdn7vd722r</code></p></li>
70
72
<li><p><code>limit</code>: optional. Default: <code>16</code>. Maximum: <code>100</code></p></li>
73
+
<li><p><code>reverse</code>: optional, return links in reverse order. Default: <code>false</code></p></li>
71
74
</ul>
72
75
73
76
<p style="margin-bottom: 0"><strong>Try it:</strong></p>
···
78
81
[""],
79
82
[""],
80
83
25,
84
+
false,
81
85
) %}
82
86
83
87
···
96
100
<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>
97
101
<li><p><code>from_dids</code> [deprecated]: optional. Use <code>did</code> instead. Example: <code>from_dids=did:plc:vc7f4oafdgxsihk4cry2xpze,did:plc:vc7f4oafdgxsihk4cry2xpze</code></p></li>
98
102
<li><p><code>limit</code>: optional. Default: <code>16</code>. Maximum: <code>100</code></p></li>
103
+
<li><p><code>reverse</code>: optional, return links in reverse order. Default: <code>false</code></p></li>
99
104
</ul>
100
105
101
106
<p style="margin-bottom: 0"><strong>Try it:</strong></p>
102
-
{% call try_it::links("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like", ".subject.uri", [""], 16) %}
107
+
{% call try_it::links("at://did:plc:a4pqq234yw7fqbddawjo7y35/app.bsky.feed.post/3m237ilwc372e", "app.bsky.feed.like", ".subject.uri", [""], 16, false) %}
103
108
104
109
105
110
<h3 class="route"><code>GET /links/distinct-dids</code></h3>
+2
-1
constellation/templates/links.html.j2
+2
-1
constellation/templates/links.html.j2
···
6
6
7
7
{% block content %}
8
8
9
-
{% call try_it::links(query.target, query.collection, query.path, query.did, query.limit) %}
9
+
{% call try_it::links(query.target, query.collection, query.path, query.did, query.limit, query.reverse) %}
10
10
11
11
<h2>
12
12
Links to <code>{{ query.target }}</code>
···
37
37
<input type="hidden" name="collection" value="{{ query.collection }}" />
38
38
<input type="hidden" name="path" value="{{ query.path }}" />
39
39
<input type="hidden" name="cursor" value={{ c|json|safe }} />
40
+
<input type="hidden" name="reverse" value="{{ query.reverse }}">
40
41
<button type="submit">next page…</button>
41
42
</form>
42
43
{% else %}
+10
-6
constellation/templates/try-it-macros.html.j2
+10
-6
constellation/templates/try-it-macros.html.j2
···
1
-
{% macro get_backlinks(subject, source, dids, limit) %}
1
+
{% macro get_backlinks(subject, source, dids, limit, reverse) %}
2
2
<form method="get" action="/xrpc/blue.microcosm.links.getBacklinks">
3
3
<pre class="code"><strong>GET</strong> /xrpc/blue.microcosm.links.getBacklinks
4
4
?subject= <input type="text" name="subject" value="{{ subject }}" placeholder="at-uri, did, uri..." />
···
6
6
{%- for did in dids %}{% if !did.is_empty() %}
7
7
&did= <input type="text" name="did" value="{{ did }}" placeholder="did:plc:..." />{% endif %}{% endfor %}
8
8
<span id="did-placeholder"></span> <button id="add-did">+ did filter</button>
9
-
&limit= <input type="number" name="limit" value="{{ limit }}" max="100" placeholder="100" /> <button type="submit">get links</button></pre>
9
+
&limit= <input type="number" name="limit" value="{{ limit }}" max="100" placeholder="100" />
10
+
&reverse= <input type="checkbox" name="reverse" value="true" checked="false"><button type="submit">get links</button></pre>
10
11
</form>
11
12
<script>
12
13
const addDidButton = document.getElementById('add-did');
···
24
25
</script>
25
26
{% endmacro %}
26
27
27
-
{% macro get_many_to_many_counts(subject, source, pathToOther, dids, otherSubjects, limit) %}
28
+
{% macro get_many_to_many_counts(subject, source, pathToOther, dids, otherSubjects, limit, reverse) %}
28
29
<form method="get" action="/xrpc/blue.microcosm.links.getManyToManyCounts">
29
30
<pre class="code"><strong>GET</strong> /xrpc/blue.microcosm.links.getManyToManyCounts
30
31
?subject= <input type="text" name="subject" value="{{ subject }}" placeholder="at-uri, did, uri..." />
···
36
37
{%- for otherSubject in otherSubjects %}{% if !otherSubject.is_empty() %}
37
38
&otherSubject= <input type="text" name="did" value="{{ otherSubject }}" placeholder="at-uri, did, uri..." />{% endif %}{% endfor %}
38
39
<span id="m2m-did-placeholder"></span> <button id="m2m-add-did">+ did filter</button>
39
-
&limit= <input type="number" name="limit" value="{{ limit }}" max="100" placeholder="100" /> <button type="submit">get links</button></pre>
40
+
&limit= <input type="number" name="limit" value="{{ limit }}" max="100" placeholder="100" />
41
+
&reverse= <input type="checkbox" name="reverse" value="true" checked="false"><button type="submit">get links</button></pre>
40
42
</form>
41
43
<script>
42
44
const m2mAddDidButton = document.getElementById('m2m-add-did');
···
66
68
</script>
67
69
{% endmacro %}
68
70
69
-
{% macro links(target, collection, path, dids, limit) %}
71
+
{% macro links(target, collection, path, dids, limit, reverse) %}
70
72
<form method="get" action="/links">
71
73
<pre class="code"><strong>GET</strong> /links
72
74
?target= <input type="text" name="target" value="{{ target }}" placeholder="target" />
···
75
77
{%- for did in dids %}{% if !did.is_empty() %}
76
78
&did= <input type="text" name="did" value="{{ did }}" placeholder="did:plc:..." />{% endif %}{% endfor %}
77
79
<span id="did-placeholder"></span> <button id="add-did">+ did filter</button>
78
-
&limit= <input type="number" name="limit" value="{{ limit }}" max="100" placeholder="100" /> <button type="submit">get links</button></pre>
80
+
&limit= <input type="number" name="limit" value="{{ limit }}" max="100" placeholder="100" />
81
+
&reverse= <input type="checkbox" name="reverse" value="true" checked="false">
82
+
<button type="submit">get links</button></pre>
79
83
</form>
80
84
<script>
81
85
const addDidButton = document.getElementById('add-did');