+31
-4
constellation/src/server/mod.rs
+31
-4
constellation/src/server/mod.rs
···
17
17
use tokio::task::spawn_blocking;
18
18
use tokio_util::sync::CancellationToken;
19
19
20
-
use crate::storage::{LinkReader, StorageStats};
20
+
use crate::storage::{LinkReader, Order, StorageStats};
21
21
use crate::{CountsByCount, Did, RecordId};
22
22
23
23
mod acceptable;
···
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 {
···
295
298
296
299
let path_to_other = format!(".{}", query.path_to_other);
297
300
301
+
let order = if query.reverse {
302
+
Order::OldestToNewest
303
+
} else {
304
+
Order::NewestToOldest
305
+
};
306
+
298
307
let paged = store
299
308
.get_many_to_many_counts(
300
309
&query.subject,
301
310
collection,
302
311
&path,
303
312
&path_to_other,
313
+
order,
304
314
limit,
305
315
cursor_key,
306
316
&filter_dids,
···
409
419
/// Set the max number of links to return per page of results
410
420
#[serde(default = "get_default_cursor_limit")]
411
421
limit: u64,
412
-
// TODO: allow reverse (er, forward) order as well
422
+
/// Allow returning links in reverse order (default: false)
423
+
#[serde(default)]
424
+
reverse: bool,
413
425
}
414
426
#[derive(Template, Serialize)]
415
427
#[template(path = "get-backlinks.html.j2")]
···
455
467
};
456
468
let path = format!(".{path}");
457
469
470
+
let order = if query.reverse {
471
+
Order::OldestToNewest
472
+
} else {
473
+
Order::NewestToOldest
474
+
};
475
+
458
476
let paged = store
459
477
.get_links(
460
478
&query.subject,
461
479
collection,
462
480
&path,
481
+
order,
463
482
limit,
464
483
until,
465
484
&filter_dids,
···
508
527
from_dids: Option<String>, // comma separated: gross
509
528
#[serde(default = "get_default_cursor_limit")]
510
529
limit: u64,
511
-
// TODO: allow reverse (er, forward) order as well
530
+
/// Allow returning links in reverse order (default: false)
531
+
#[serde(default)]
532
+
reverse: bool,
512
533
}
513
534
#[derive(Template, Serialize)]
514
535
#[template(path = "links.html.j2")]
···
557
578
}
558
579
}
559
580
581
+
let order = if query.reverse {
582
+
Order::OldestToNewest
583
+
} else {
584
+
Order::NewestToOldest
585
+
};
586
+
560
587
let paged = store
561
588
.get_links(
562
589
&query.target,
563
590
&query.collection,
564
591
&query.path,
592
+
order,
565
593
limit,
566
594
until,
567
595
&filter_dids,
···
594
622
path: String,
595
623
cursor: Option<OpaqueApiCursor>,
596
624
limit: Option<u64>,
597
-
// TODO: allow reverse (er, forward) order as well
598
625
}
599
626
#[derive(Template, Serialize)]
600
627
#[template(path = "dids.html.j2")]
+50
-12
constellation/src/storage/mem_store.rs
+50
-12
constellation/src/storage/mem_store.rs
···
1
1
use super::{
2
-
LinkReader, LinkStorage, PagedAppendingCollection, PagedOrderedCollection, StorageStats,
2
+
LinkReader, LinkStorage, Order, PagedAppendingCollection, PagedOrderedCollection,
3
+
StorageStats,
3
4
};
4
5
use crate::{ActionableEvent, CountsByCount, Did, RecordId};
5
6
use anyhow::Result;
···
140
141
collection: &str,
141
142
path: &str,
142
143
path_to_other: &str,
144
+
order: Order,
143
145
limit: u64,
144
146
after: Option<String>,
145
147
filter_dids: &HashSet<Did>,
···
157
159
let filter_to_targets: HashSet<Target> =
158
160
HashSet::from_iter(filter_to_targets.iter().map(|s| Target::new(s)));
159
161
160
-
let mut grouped_counts: HashMap<Target, (u64, HashSet<Did>)> = HashMap::new();
161
-
for (did, rkey) in linkers.iter().flatten().cloned() {
162
+
// the last type field here acts as an index to allow keeping track of the order in which
163
+
// we encountred single elements
164
+
let mut grouped_counts: HashMap<Target, (u64, HashSet<Did>, usize)> = HashMap::new();
165
+
for (idx, (did, rkey)) in linkers.iter().flatten().cloned().enumerate() {
162
166
if !filter_dids.is_empty() && !filter_dids.contains(&did) {
163
167
continue;
164
168
}
···
184
188
.take(1)
185
189
.next()
186
190
{
187
-
let e = grouped_counts.entry(fwd_target.clone()).or_default();
191
+
let e =
192
+
grouped_counts
193
+
.entry(fwd_target.clone())
194
+
.or_insert((0, HashSet::new(), idx));
188
195
e.0 += 1;
189
196
e.1.insert(did.clone());
190
197
}
191
198
}
192
199
let mut items: Vec<(String, u64, u64)> = grouped_counts
193
200
.iter()
194
-
.map(|(k, (n, u))| (k.0.clone(), *n, u.len() as u64))
201
+
.map(|(k, (n, u, _))| (k.0.clone(), *n, u.len() as u64))
195
202
.collect();
196
-
items.sort();
203
+
// Sort based on order: OldestToNewest uses descending order, NewestToOldest uses ascending
204
+
match order {
205
+
Order::OldestToNewest => items.sort_by(|a, b| b.cmp(a)),
206
+
Order::NewestToOldest => 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
+
order: Order,
242
254
limit: u64,
243
255
until: Option<u64>,
244
256
filter_dids: &HashSet<Did>,
···
276
288
};
277
289
278
290
let total = did_rkeys.len();
279
-
let end = until
280
-
.map(|u| std::cmp::min(u as usize, total))
281
-
.unwrap_or(total);
282
-
let begin = end.saturating_sub(limit as usize);
283
-
let next = if begin == 0 { None } else { Some(begin as u64) };
291
+
292
+
let begin: usize;
293
+
let end: usize;
294
+
let next: Option<u64>;
295
+
296
+
match order {
297
+
// OldestToNewest: start from the beginning, paginate forward
298
+
Order::OldestToNewest => {
299
+
begin = until.map(|u| (u) as usize).unwrap_or(0);
300
+
end = std::cmp::min(begin + limit as usize, total);
301
+
302
+
next = if end < total {
303
+
Some(end as u64 + 1)
304
+
} else {
305
+
None
306
+
};
307
+
}
308
+
// NewestToOldest: start from the end, paginate backward
309
+
Order::NewestToOldest => {
310
+
end = until
311
+
.map(|u| std::cmp::min(u as usize, total))
312
+
.unwrap_or(total);
313
+
begin = end.saturating_sub(limit as usize);
314
+
next = if begin == 0 { None } else { Some(begin as u64) };
315
+
}
316
+
}
284
317
285
318
let alive = did_rkeys.iter().flatten().count();
286
319
let gone = total - alive;
287
320
288
-
let items: Vec<_> = did_rkeys[begin..end]
321
+
let mut items: Vec<_> = did_rkeys[begin..end]
289
322
.iter()
290
323
.rev()
291
324
.flatten()
···
296
329
collection: collection.to_string(),
297
330
})
298
331
.collect();
332
+
333
+
// For OldestToNewest, reverse the items to maintain forward chronological order
334
+
if order == Order::OldestToNewest {
335
+
items.reverse();
336
+
}
299
337
300
338
Ok(PagedAppendingCollection {
301
339
version: (total as u64, gone as u64),
+256
-10
constellation/src/storage/mod.rs
+256
-10
constellation/src/storage/mod.rs
···
11
11
#[cfg(feature = "rocks")]
12
12
pub use rocks_store::RocksStorage;
13
13
14
+
/// Ordering for paginated link queries
15
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16
+
pub enum Order {
17
+
/// Newest links first (default)
18
+
NewestToOldest,
19
+
/// Oldest links first
20
+
OldestToNewest,
21
+
}
22
+
14
23
#[derive(Debug, PartialEq)]
15
24
pub struct PagedAppendingCollection<T> {
16
25
pub version: (u64, u64), // (collection length, deleted item count) // TODO: change to (total, active)? since dedups isn't "deleted"
···
72
81
collection: &str,
73
82
path: &str,
74
83
path_to_other: &str,
84
+
order: Order,
75
85
limit: u64,
76
86
after: Option<String>,
77
87
filter_dids: &HashSet<Did>,
···
87
97
target: &str,
88
98
collection: &str,
89
99
path: &str,
100
+
order: Order,
90
101
limit: u64,
91
102
until: Option<u64>,
92
103
filter_dids: &HashSet<Did>,
···
180
191
"a.com",
181
192
"app.t.c",
182
193
".abc.uri",
194
+
Order::NewestToOldest,
183
195
100,
184
196
None,
185
197
&HashSet::default()
···
683
695
"a.com",
684
696
"app.t.c",
685
697
".abc.uri",
698
+
Order::NewestToOldest,
686
699
100,
687
700
None,
688
701
&HashSet::default()
···
727
740
0,
728
741
)?;
729
742
}
730
-
let links =
731
-
storage.get_links("a.com", "app.t.c", ".abc.uri", 2, None, &HashSet::default())?;
743
+
let links = storage.get_links(
744
+
"a.com",
745
+
"app.t.c",
746
+
".abc.uri",
747
+
Order::NewestToOldest,
748
+
2,
749
+
None,
750
+
&HashSet::default(),
751
+
)?;
732
752
let dids = storage.get_distinct_dids("a.com", "app.t.c", ".abc.uri", 2, None)?;
733
753
assert_eq!(
734
754
links,
···
763
783
"a.com",
764
784
"app.t.c",
765
785
".abc.uri",
786
+
Order::NewestToOldest,
766
787
2,
767
788
links.next,
768
789
&HashSet::default(),
···
801
822
"a.com",
802
823
"app.t.c",
803
824
".abc.uri",
825
+
Order::NewestToOldest,
804
826
2,
805
827
links.next,
806
828
&HashSet::default(),
···
831
853
assert_stats(storage.get_stats()?, 5..=5, 1..=1, 5..=5);
832
854
});
833
855
856
+
test_each_storage!(get_links_reverse_order, |storage| {
857
+
for i in 1..=5 {
858
+
storage.push(
859
+
&ActionableEvent::CreateLinks {
860
+
record_id: RecordId {
861
+
did: format!("did:plc:asdf-{i}").into(),
862
+
collection: "app.t.c".into(),
863
+
rkey: "asdf".into(),
864
+
},
865
+
links: vec![CollectedLink {
866
+
target: Link::Uri("a.com".into()),
867
+
path: ".abc.uri".into(),
868
+
}],
869
+
},
870
+
0,
871
+
)?;
872
+
}
873
+
874
+
// Test OldestToNewest order (oldest first)
875
+
let links = storage.get_links(
876
+
"a.com",
877
+
"app.t.c",
878
+
".abc.uri",
879
+
Order::OldestToNewest,
880
+
2,
881
+
None,
882
+
&HashSet::default(),
883
+
)?;
884
+
assert_eq!(
885
+
links,
886
+
PagedAppendingCollection {
887
+
version: (5, 0),
888
+
items: vec![
889
+
RecordId {
890
+
did: "did:plc:asdf-1".into(),
891
+
collection: "app.t.c".into(),
892
+
rkey: "asdf".into(),
893
+
},
894
+
RecordId {
895
+
did: "did:plc:asdf-2".into(),
896
+
collection: "app.t.c".into(),
897
+
rkey: "asdf".into(),
898
+
},
899
+
],
900
+
next: Some(3),
901
+
total: 5,
902
+
}
903
+
);
904
+
// Test NewestToOldest order (newest first)
905
+
let links = storage.get_links(
906
+
"a.com",
907
+
"app.t.c",
908
+
".abc.uri",
909
+
Order::NewestToOldest,
910
+
2,
911
+
None,
912
+
&HashSet::default(),
913
+
)?;
914
+
assert_eq!(
915
+
links,
916
+
PagedAppendingCollection {
917
+
version: (5, 0),
918
+
items: vec![
919
+
RecordId {
920
+
did: "did:plc:asdf-5".into(),
921
+
collection: "app.t.c".into(),
922
+
rkey: "asdf".into(),
923
+
},
924
+
RecordId {
925
+
did: "did:plc:asdf-4".into(),
926
+
collection: "app.t.c".into(),
927
+
rkey: "asdf".into(),
928
+
},
929
+
],
930
+
next: Some(3),
931
+
total: 5,
932
+
}
933
+
);
934
+
assert_stats(storage.get_stats()?, 5..=5, 1..=1, 5..=5);
935
+
});
936
+
834
937
test_each_storage!(get_filtered_links, |storage| {
835
938
let links = storage.get_links(
836
939
"a.com",
837
940
"app.t.c",
838
941
".abc.uri",
942
+
Order::NewestToOldest,
839
943
2,
840
944
None,
841
945
&HashSet::from([Did("did:plc:linker".to_string())]),
···
869
973
"a.com",
870
974
"app.t.c",
871
975
".abc.uri",
976
+
Order::NewestToOldest,
872
977
2,
873
978
None,
874
979
&HashSet::from([Did("did:plc:linker".to_string())]),
···
891
996
"a.com",
892
997
"app.t.c",
893
998
".abc.uri",
999
+
Order::NewestToOldest,
894
1000
2,
895
1001
None,
896
1002
&HashSet::from([Did("did:plc:someone-else".to_string())]),
···
938
1044
"a.com",
939
1045
"app.t.c",
940
1046
".abc.uri",
1047
+
Order::NewestToOldest,
941
1048
2,
942
1049
None,
943
1050
&HashSet::from([Did("did:plc:linker".to_string())]),
···
967
1074
"a.com",
968
1075
"app.t.c",
969
1076
".abc.uri",
1077
+
Order::NewestToOldest,
970
1078
2,
971
1079
None,
972
1080
&HashSet::from([
···
999
1107
"a.com",
1000
1108
"app.t.c",
1001
1109
".abc.uri",
1110
+
Order::NewestToOldest,
1002
1111
2,
1003
1112
None,
1004
1113
&HashSet::from([Did("did:plc:someone-unknown".to_string())]),
···
1031
1140
0,
1032
1141
)?;
1033
1142
}
1034
-
let links =
1035
-
storage.get_links("a.com", "app.t.c", ".abc.uri", 2, None, &HashSet::default())?;
1143
+
let links = storage.get_links(
1144
+
"a.com",
1145
+
"app.t.c",
1146
+
".abc.uri",
1147
+
Order::NewestToOldest,
1148
+
2,
1149
+
None,
1150
+
&HashSet::default(),
1151
+
)?;
1036
1152
assert_eq!(
1037
1153
links,
1038
1154
PagedAppendingCollection {
···
1057
1173
"a.com",
1058
1174
"app.t.c",
1059
1175
".abc.uri",
1176
+
Order::NewestToOldest,
1060
1177
2,
1061
1178
links.next,
1062
1179
&HashSet::default(),
···
1101
1218
0,
1102
1219
)?;
1103
1220
}
1104
-
let links =
1105
-
storage.get_links("a.com", "app.t.c", ".abc.uri", 2, None, &HashSet::default())?;
1221
+
let links = storage.get_links(
1222
+
"a.com",
1223
+
"app.t.c",
1224
+
".abc.uri",
1225
+
Order::NewestToOldest,
1226
+
2,
1227
+
None,
1228
+
&HashSet::default(),
1229
+
)?;
1106
1230
assert_eq!(
1107
1231
links,
1108
1232
PagedAppendingCollection {
···
1141
1265
"a.com",
1142
1266
"app.t.c",
1143
1267
".abc.uri",
1268
+
Order::NewestToOldest,
1144
1269
2,
1145
1270
links.next,
1146
1271
&HashSet::default(),
···
1185
1310
0,
1186
1311
)?;
1187
1312
}
1188
-
let links =
1189
-
storage.get_links("a.com", "app.t.c", ".abc.uri", 2, None, &HashSet::default())?;
1313
+
let links = storage.get_links(
1314
+
"a.com",
1315
+
"app.t.c",
1316
+
".abc.uri",
1317
+
Order::NewestToOldest,
1318
+
2,
1319
+
None,
1320
+
&HashSet::default(),
1321
+
)?;
1190
1322
assert_eq!(
1191
1323
links,
1192
1324
PagedAppendingCollection {
···
1219
1351
"a.com",
1220
1352
"app.t.c",
1221
1353
".abc.uri",
1354
+
Order::NewestToOldest,
1222
1355
2,
1223
1356
links.next,
1224
1357
&HashSet::default(),
···
1256
1389
0,
1257
1390
)?;
1258
1391
}
1259
-
let links =
1260
-
storage.get_links("a.com", "app.t.c", ".abc.uri", 2, None, &HashSet::default())?;
1392
+
let links = storage.get_links(
1393
+
"a.com",
1394
+
"app.t.c",
1395
+
".abc.uri",
1396
+
Order::NewestToOldest,
1397
+
2,
1398
+
None,
1399
+
&HashSet::default(),
1400
+
)?;
1261
1401
assert_eq!(
1262
1402
links,
1263
1403
PagedAppendingCollection {
···
1286
1426
"a.com",
1287
1427
"app.t.c",
1288
1428
".abc.uri",
1429
+
Order::NewestToOldest,
1289
1430
2,
1290
1431
links.next,
1291
1432
&HashSet::default(),
···
1367
1508
"a.b.c",
1368
1509
".d.e",
1369
1510
".f.g",
1511
+
Order::NewestToOldest,
1370
1512
10,
1371
1513
None,
1372
1514
&HashSet::new(),
···
1410
1552
"app.t.c",
1411
1553
".abc.uri",
1412
1554
".def.uri",
1555
+
Order::NewestToOldest,
1413
1556
10,
1414
1557
None,
1415
1558
&HashSet::new(),
···
1509
1652
"app.t.c",
1510
1653
".abc.uri",
1511
1654
".def.uri",
1655
+
Order::NewestToOldest,
1512
1656
10,
1513
1657
None,
1514
1658
&HashSet::new(),
···
1525
1669
"app.t.c",
1526
1670
".abc.uri",
1527
1671
".def.uri",
1672
+
Order::NewestToOldest,
1528
1673
10,
1529
1674
None,
1530
1675
&HashSet::from_iter([Did("did:plc:fdsa".to_string())]),
···
1541
1686
"app.t.c",
1542
1687
".abc.uri",
1543
1688
".def.uri",
1689
+
Order::NewestToOldest,
1544
1690
10,
1545
1691
None,
1546
1692
&HashSet::new(),
···
1551
1697
next: None,
1552
1698
}
1553
1699
);
1700
+
});
1701
+
1702
+
test_each_storage!(get_m2m_counts_reverse_order, |storage| {
1703
+
// Create links from different DIDs to different targets
1704
+
storage.push(
1705
+
&ActionableEvent::CreateLinks {
1706
+
record_id: RecordId {
1707
+
did: "did:plc:user1".into(),
1708
+
collection: "app.t.c".into(),
1709
+
rkey: "post1".into(),
1710
+
},
1711
+
links: vec![
1712
+
CollectedLink {
1713
+
target: Link::Uri("a.com".into()),
1714
+
path: ".abc.uri".into(),
1715
+
},
1716
+
CollectedLink {
1717
+
target: Link::Uri("b.com".into()),
1718
+
path: ".def.uri".into(),
1719
+
},
1720
+
],
1721
+
},
1722
+
0,
1723
+
)?;
1724
+
storage.push(
1725
+
&ActionableEvent::CreateLinks {
1726
+
record_id: RecordId {
1727
+
did: "did:plc:user2".into(),
1728
+
collection: "app.t.c".into(),
1729
+
rkey: "post1".into(),
1730
+
},
1731
+
links: vec![
1732
+
CollectedLink {
1733
+
target: Link::Uri("a.com".into()),
1734
+
path: ".abc.uri".into(),
1735
+
},
1736
+
CollectedLink {
1737
+
target: Link::Uri("c.com".into()),
1738
+
path: ".def.uri".into(),
1739
+
},
1740
+
],
1741
+
},
1742
+
1,
1743
+
)?;
1744
+
storage.push(
1745
+
&ActionableEvent::CreateLinks {
1746
+
record_id: RecordId {
1747
+
did: "did:plc:user3".into(),
1748
+
collection: "app.t.c".into(),
1749
+
rkey: "post1".into(),
1750
+
},
1751
+
links: vec![
1752
+
CollectedLink {
1753
+
target: Link::Uri("a.com".into()),
1754
+
path: ".abc.uri".into(),
1755
+
},
1756
+
CollectedLink {
1757
+
target: Link::Uri("d.com".into()),
1758
+
path: ".def.uri".into(),
1759
+
},
1760
+
],
1761
+
},
1762
+
2,
1763
+
)?;
1764
+
1765
+
// Test NewestToOldest order (default order - by target ascending)
1766
+
let counts = storage.get_many_to_many_counts(
1767
+
"a.com",
1768
+
"app.t.c",
1769
+
".abc.uri",
1770
+
".def.uri",
1771
+
Order::NewestToOldest,
1772
+
10,
1773
+
None,
1774
+
&HashSet::new(),
1775
+
&HashSet::new(),
1776
+
)?;
1777
+
assert_eq!(counts.items.len(), 3);
1778
+
// Should be sorted by target in ascending order (alphabetical)
1779
+
assert_eq!(counts.items[0].0, "b.com");
1780
+
assert_eq!(counts.items[1].0, "c.com");
1781
+
assert_eq!(counts.items[2].0, "d.com");
1782
+
1783
+
// Test OldestToNewest order (descending order - by target descending)
1784
+
let counts = storage.get_many_to_many_counts(
1785
+
"a.com",
1786
+
"app.t.c",
1787
+
".abc.uri",
1788
+
".def.uri",
1789
+
Order::OldestToNewest,
1790
+
10,
1791
+
None,
1792
+
&HashSet::new(),
1793
+
&HashSet::new(),
1794
+
)?;
1795
+
assert_eq!(counts.items.len(), 3);
1796
+
// Should be sorted by target in descending order (reverse alphabetical)
1797
+
assert_eq!(counts.items[0].0, "d.com");
1798
+
assert_eq!(counts.items[1].0, "c.com");
1799
+
assert_eq!(counts.items[2].0, "b.com");
1554
1800
});
1555
1801
}
+41
-6
constellation/src/storage/rocks_store.rs
+41
-6
constellation/src/storage/rocks_store.rs
···
1
1
use super::{
2
-
ActionableEvent, LinkReader, LinkStorage, PagedAppendingCollection, PagedOrderedCollection,
3
-
StorageStats,
2
+
ActionableEvent, LinkReader, LinkStorage, Order, PagedAppendingCollection,
3
+
PagedOrderedCollection, StorageStats,
4
4
};
5
5
use crate::{CountsByCount, Did, RecordId};
6
6
use anyhow::{bail, Result};
···
941
941
collection: &str,
942
942
path: &str,
943
943
path_to_other: &str,
944
+
order: Order,
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
···
1082
1084
items.push((target.0 .0, *n, dids.len() as u64));
1083
1085
}
1084
1086
1087
+
// Sort based on order: OldestToNewest uses descending order, NewestToOldest uses ascending
1088
+
match order {
1089
+
Order::OldestToNewest => items.sort_by(|a, b| b.cmp(a)), // descending
1090
+
Order::NewestToOldest => items.sort(), // ascending
1091
+
}
1092
+
1085
1093
let next = if grouped_counts.len() as u64 >= limit {
1086
1094
// yeah.... it's a number saved as a string......sorry
1087
1095
grouped_counts
···
1127
1135
target: &str,
1128
1136
collection: &str,
1129
1137
path: &str,
1138
+
order: Order,
1130
1139
limit: u64,
1131
1140
until: Option<u64>,
1132
1141
filter_dids: &HashSet<Did>,
···
1167
1176
1168
1177
let (alive, gone) = linkers.count();
1169
1178
let total = alive + gone;
1170
-
let end = until.map(|u| std::cmp::min(u, total)).unwrap_or(total) as usize;
1171
-
let begin = end.saturating_sub(limit as usize);
1172
-
let next = if begin == 0 { None } else { Some(begin as u64) };
1179
+
1180
+
let end: usize;
1181
+
let begin: usize;
1182
+
let next: Option<u64>;
1183
+
1184
+
match order {
1185
+
// OldestToNewest: start from the beginning, paginate forward
1186
+
Order::OldestToNewest => {
1187
+
begin = until.map(|u| (u - 1) as usize).unwrap_or(0);
1188
+
end = std::cmp::min(begin + limit as usize, total as usize);
1189
+
1190
+
next = if end < total as usize {
1191
+
Some(end as u64 + 1)
1192
+
} else {
1193
+
None
1194
+
}
1195
+
}
1196
+
// NewestToOldest: start from the end, paginate backward
1197
+
Order::NewestToOldest => {
1198
+
end = until.map(|u| std::cmp::min(u, total)).unwrap_or(total) as usize;
1199
+
begin = end.saturating_sub(limit as usize);
1200
+
next = if begin == 0 { None } else { Some(begin as u64) };
1201
+
}
1202
+
}
1173
1203
1174
-
let did_id_rkeys = linkers.0[begin..end].iter().rev().collect::<Vec<_>>();
1204
+
let mut did_id_rkeys = linkers.0[begin..end].iter().rev().collect::<Vec<_>>();
1205
+
1206
+
// For OldestToNewest, reverse the items to maintain forward chronological order
1207
+
if order == Order::OldestToNewest {
1208
+
did_id_rkeys.reverse();
1209
+
}
1175
1210
1176
1211
let mut items = Vec::with_capacity(did_id_rkeys.len());
1177
1212
// 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');