···11use crate::index_html::INDEX_HTML;
22use crate::storage::StoreReader;
33use crate::store_types::{HourTruncatedCursor, WeekTruncatedCursor};
44-use crate::{ConsumerInfo, Cursor, Nsid, NsidCount, UFOsRecord};
44+use crate::{ConsumerInfo, Cursor, Nsid, NsidCount, OrderCollectionsBy, UFOsRecord};
55use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
66use chrono::{DateTime, Utc};
77use dropshot::endpoint;
···166166 let min_time_ago = SystemTime::now() - Duration::from_secs(86_400 * 3); // we want at least 3 days of data
167167 let since: WeekTruncatedCursor = Cursor::at(min_time_ago).into();
168168 let (collections, _) = storage
169169- .get_all_collections(1000, None, Some(since.try_as().unwrap()), None)
169169+ .get_collections(
170170+ 1000,
171171+ Default::default(),
172172+ Some(since.try_as().unwrap()),
173173+ None,
174174+ )
170175 .await
171176 .map_err(|e| HttpError::for_internal_error(e.to_string()))?;
172177 collections
···240245 cursor: Option<String>,
241246}
242247#[derive(Debug, Deserialize, JsonSchema)]
243243-struct AllCollectionsQuery {
248248+#[serde(rename_all = "kebab-case")]
249249+pub enum CollectionsQueryOrder {
250250+ RecordsCreated,
251251+ DidsEstimate,
252252+}
253253+impl From<&CollectionsQueryOrder> for OrderCollectionsBy {
254254+ fn from(q: &CollectionsQueryOrder) -> Self {
255255+ match q {
256256+ CollectionsQueryOrder::RecordsCreated => OrderCollectionsBy::RecordsCreated,
257257+ CollectionsQueryOrder::DidsEstimate => OrderCollectionsBy::DidsEstimate,
258258+ }
259259+ }
260260+}
261261+#[derive(Debug, Deserialize, JsonSchema)]
262262+struct CollectionsQuery {
244263 /// The maximum number of collections to return in one request.
245264 ///
246246- /// Default: 100
247247- #[schemars(range(min = 1, max = 200), default = "all_collections_default_limit")]
248248- #[serde(default = "all_collections_default_limit")]
249249- limit: usize,
265265+ /// Default: `100` normally, `32` if `order` is specified.
266266+ #[schemars(range(min = 1, max = 200))]
267267+ limit: Option<usize>,
268268+ /// Get a paginated response with more collections.
269269+ ///
250270 /// Always omit the cursor for the first request. If more collections than the limit are available, the response will contain a non-null `cursor` to include with the next request.
271271+ ///
272272+ /// `cursor` is mutually exclusive with `order`.
251273 cursor: Option<String>,
252274 /// Limit collections and statistics to those seen after this UTC datetime
253275 since: Option<DateTime<Utc>>,
254276 /// Limit collections and statistics to those seen before this UTC datetime
255277 until: Option<DateTime<Utc>>,
256256-}
257257-fn all_collections_default_limit() -> usize {
258258- 100
278278+ /// Get a limited, sorted list
279279+ ///
280280+ /// Mutually exclusive with `cursor` -- sorted results cannot be paged.
281281+ order: Option<CollectionsQueryOrder>,
259282}
260283#[endpoint {
261284 method = GET,
262262- path = "/collections/all"
285285+ path = "/collections"
263286}]
264264-/// Get all collections
287287+/// Get a list of collection NSIDs with statistics
288288+///
289289+/// ## To fetch a full list:
265290///
266266-/// There have been a lot of collections seen in the ATmosphere, well over 400 at time of writing, so you *will* need to make a series of paginaged requests with `cursor`s to get them all.
291291+/// Omit the `order` parameter and page through the results using the `cursor`. There have been a lot of collections seen in the ATmosphere, well over 400 at time of writing, so you *will* need to make a series of paginaged requests with `cursor`s to get them all.
267292///
268293/// The set of collections across multiple requests is not guaranteed to be a perfectly consistent snapshot:
269294///
···275300///
276301/// In practice this is close enough for most use-cases to not worry about.
277302///
278278-/// Statistics are bucketed hourly, so the most granular effecitve time boundary for `since` and `until` is one hour.
279279-async fn get_all_collections(
303303+/// ## To fetch the top collection NSIDs:
304304+///
305305+/// Specify the `order` parameter (must be either `records-created` or `did-estimate`). Note that ordered results cannot be paged.
306306+///
307307+/// All statistics are bucketed hourly, so the most granular effecitve time boundary for `since` and `until` is one hour.
308308+async fn get_collections(
280309 ctx: RequestContext<Context>,
281281- query: Query<AllCollectionsQuery>,
310310+ query: Query<CollectionsQuery>,
282311) -> OkCorsResponse<CollectionsResponse> {
283312 let Context { storage, .. } = ctx.context();
284313 let q = query.into_inner();
285314286286- if !(1..=200).contains(&q.limit) {
287287- let msg = format!("limit not in 1..=200: {}", q.limit);
288288- return Err(HttpError::for_bad_request(None, msg));
315315+ if q.cursor.is_some() && q.order.is_some() {
316316+ let msg = "`cursor` is mutually exclusive with `order`. ordered results cannot be paged.";
317317+ return Err(HttpError::for_bad_request(None, msg.to_string()));
289318 }
290319291291- let cursor = q
292292- .cursor
293293- .and_then(|c| if c.is_empty() { None } else { Some(c) })
294294- .map(|c| URL_SAFE_NO_PAD.decode(&c))
295295- .transpose()
296296- .map_err(|e| HttpError::for_bad_request(None, format!("invalid cursor: {e:?}")))?;
320320+ let order = if let Some(ref o) = q.order {
321321+ o.into()
322322+ } else {
323323+ let cursor = q
324324+ .cursor
325325+ .and_then(|c| if c.is_empty() { None } else { Some(c) })
326326+ .map(|c| URL_SAFE_NO_PAD.decode(&c))
327327+ .transpose()
328328+ .map_err(|e| HttpError::for_bad_request(None, format!("invalid cursor: {e:?}")))?;
329329+ OrderCollectionsBy::Lexi { cursor }
330330+ };
331331+332332+ let limit = match (q.limit, q.order) {
333333+ (Some(limit), _) => limit,
334334+ (None, Some(_)) => 32,
335335+ (None, None) => 100,
336336+ };
337337+338338+ if !(1..=200).contains(&limit) {
339339+ let msg = format!("limit not in 1..=200: {}", limit);
340340+ return Err(HttpError::for_bad_request(None, msg));
341341+ }
297342298343 let since = q.since.map(dt_to_cursor).transpose()?;
299344 let until = q.until.map(dt_to_cursor).transpose()?;
300345301346 let (collections, next_cursor) = storage
302302- .get_all_collections(q.limit, cursor, since, until)
347347+ .get_collections(limit, order, since, until)
303348 .await
304349 .map_err(|e| HttpError::for_internal_error(format!("oh shoot: {e:?}")))?;
305350···311356 })
312357}
313358359359+#[derive(Debug, Deserialize, JsonSchema)]
360360+struct TopByQuery {
361361+ /// The maximum number of collections to return in one request.
362362+ ///
363363+ /// Default: 32
364364+ #[schemars(range(min = 1, max = 200), default = "top_collections_default_limit")]
365365+ #[serde(default = "top_collections_default_limit")]
366366+ limit: usize,
367367+ /// Limit collections and statistics to those seen after this UTC datetime
368368+ since: Option<DateTime<Utc>>,
369369+ /// Limit collections and statistics to those seen before this UTC datetime
370370+ until: Option<DateTime<Utc>>,
371371+}
372372+fn top_collections_default_limit() -> usize {
373373+ 32
374374+}
375375+314376/// Get top collections by record count
315377#[endpoint {
316378 method = GET,
···318380}]
319381async fn get_top_collections_by_count(
320382 ctx: RequestContext<Context>,
383383+ query: Query<TopByQuery>,
321384) -> OkCorsResponse<Vec<NsidCount>> {
322385 let Context { storage, .. } = ctx.context();
386386+ let q = query.into_inner();
387387+388388+ if !(1..=200).contains(&q.limit) {
389389+ let msg = format!("limit not in 1..=200: {}", q.limit);
390390+ return Err(HttpError::for_bad_request(None, msg));
391391+ }
392392+393393+ let since = q.since.map(dt_to_cursor).transpose()?;
394394+ let until = q.until.map(dt_to_cursor).transpose()?;
395395+323396 let collections = storage
324324- .get_top_collections_by_count(100, None, None)
397397+ .get_top_collections_by_count(100, since, until)
325398 .await
326399 .map_err(|e| HttpError::for_internal_error(format!("oh shoot: {e:?}")))?;
327400···335408}]
336409async fn get_top_collections_by_dids(
337410 ctx: RequestContext<Context>,
411411+ query: Query<TopByQuery>,
338412) -> OkCorsResponse<Vec<NsidCount>> {
339413 let Context { storage, .. } = ctx.context();
414414+ let q = query.into_inner();
415415+416416+ if !(1..=200).contains(&q.limit) {
417417+ let msg = format!("limit not in 1..=200: {}", q.limit);
418418+ return Err(HttpError::for_bad_request(None, msg));
419419+ }
420420+421421+ let since = q.since.map(dt_to_cursor).transpose()?;
422422+ let until = q.until.map(dt_to_cursor).transpose()?;
423423+340424 let collections = storage
341341- .get_top_collections_by_dids(100, None, None)
425425+ .get_top_collections_by_dids(100, since, until)
342426 .await
343427 .map_err(|e| HttpError::for_internal_error(format!("oh shoot: {e:?}")))?;
344428···359443 api.register(get_meta_info).unwrap();
360444 api.register(get_records_by_collections).unwrap();
361445 api.register(get_records_total_seen).unwrap();
362362- api.register(get_all_collections).unwrap();
446446+ api.register(get_collections).unwrap();
363447 api.register(get_top_collections_by_count).unwrap();
364448 api.register(get_top_collections_by_dids).unwrap();
365449