Skip to content

Commit 3f1a04e

Browse files
eth3lbertTurbo87
authored andcommitted
controllers/krate/versions: Add basic support for seek-based pagination to versions endpoint
1 parent 09a29ce commit 3f1a04e

File tree

4 files changed

+443
-11
lines changed

4 files changed

+443
-11
lines changed

src/controllers/krate/versions.rs

Lines changed: 207 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22
33
use std::cmp::Reverse;
44

5+
use diesel::connection::DefaultLoadingMode;
6+
use indexmap::IndexMap;
7+
58
use crate::controllers::frontend_prelude::*;
9+
use crate::controllers::helpers::pagination::{encode_seek, Page, PaginationOptions};
610

711
use crate::models::{Crate, CrateVersions, User, Version, VersionOwnerAction};
812
use crate::schema::{users, versions};
913
use crate::util::errors::crate_not_found;
1014
use crate::views::EncodableVersion;
1115

1216
/// Handles the `GET /crates/:crate_id/versions` route.
13-
// FIXME: Not sure why this is necessary since /crates/:crate_id returns
14-
// this information already, but ember is definitely requesting it
15-
pub async fn versions(state: AppState, Path(crate_name): Path<String>) -> AppResult<Json<Value>> {
17+
pub async fn versions(
18+
state: AppState,
19+
Path(crate_name): Path<String>,
20+
req: Parts,
21+
) -> AppResult<Json<Value>> {
1622
spawn_blocking(move || {
1723
let conn = &mut *state.db_read()?;
1824

@@ -21,27 +27,217 @@ pub async fn versions(state: AppState, Path(crate_name): Path<String>) -> AppRes
2127
.optional()?
2228
.ok_or_else(|| crate_not_found(&crate_name))?;
2329

24-
let mut versions_and_publishers: Vec<(Version, Option<User>)> = krate
25-
.all_versions()
26-
.left_outer_join(users::table)
27-
.select((versions::all_columns, users::all_columns.nullable()))
28-
.load(conn)?;
30+
let mut pagination = None;
31+
let params = req.query();
32+
// To keep backward compatibility, we paginate only if per_page is provided
33+
if params.get("per_page").is_some() {
34+
pagination = Some(
35+
PaginationOptions::builder()
36+
.enable_seek(true)
37+
.enable_pages(false)
38+
.gather(&req)?,
39+
);
40+
}
2941

30-
versions_and_publishers
31-
.sort_by_cached_key(|(version, _)| Reverse(semver::Version::parse(&version.num).ok()));
42+
// Sort by semver by default
43+
let versions_and_publishers = match params.get("sort").map(|s| s.to_lowercase()).as_deref()
44+
{
45+
Some("date") => list_by_date(&krate, pagination.as_ref(), &req, conn)?,
46+
_ => list_by_semver(&krate, pagination.as_ref(), &req, conn)?,
47+
};
3248

3349
let versions = versions_and_publishers
50+
.data
3451
.iter()
3552
.map(|(v, _)| v)
3653
.cloned()
3754
.collect::<Vec<_>>();
3855
let versions = versions_and_publishers
56+
.data
3957
.into_iter()
4058
.zip(VersionOwnerAction::for_versions(conn, &versions)?)
4159
.map(|((v, pb), aas)| EncodableVersion::from(v, &crate_name, pb, aas))
4260
.collect::<Vec<_>>();
4361

44-
Ok(Json(json!({ "versions": versions })))
62+
Ok(Json(match pagination {
63+
Some(_) => json!({ "versions": versions, "meta": versions_and_publishers.meta }),
64+
None => json!({ "versions": versions }),
65+
}))
4566
})
4667
.await
4768
}
69+
70+
fn list_by_date(
71+
krate: &Crate,
72+
options: Option<&PaginationOptions>,
73+
req: &Parts,
74+
conn: &mut PgConnection,
75+
) -> AppResult<PaginatedVersionsAndPublishers> {
76+
let mut query = krate
77+
.all_versions()
78+
.left_outer_join(users::table)
79+
.select((versions::all_columns, users::all_columns.nullable()));
80+
81+
if let Some(options) = options {
82+
if let Page::Seek(ref seek) = options.page {
83+
let (created_at, id) = seek.decode::<seek::Date>().map(|s| (s.0, s.1))?;
84+
query = query.filter(
85+
versions::created_at
86+
.eq(created_at)
87+
.and(versions::id.lt(id))
88+
.or(versions::created_at.lt(created_at)),
89+
)
90+
}
91+
query = query.limit(options.per_page);
92+
}
93+
94+
query = query.order((versions::created_at.desc(), versions::id.desc()));
95+
96+
let data: Vec<(Version, Option<User>)> = query.load(conn)?;
97+
let mut next_page = None;
98+
if let Some(options) = options {
99+
next_page = next_seek_params(&data, options, |last| {
100+
seek::Date(last.0.created_at, last.0.id)
101+
})?
102+
.map(|p| req.query_with_params(p));
103+
};
104+
105+
// Since the total count is retrieved through an additional query, to maintain consistency
106+
// with other pagination methods, we only make a count query while data is not empty.
107+
let total = if !data.is_empty() {
108+
versions::table.count().get_result(conn)?
109+
} else {
110+
0
111+
};
112+
113+
Ok(PaginatedVersionsAndPublishers {
114+
data,
115+
meta: ResponseMeta { total, next_page },
116+
})
117+
}
118+
119+
// Unfortunately, Heroku Postgres has no support for the semver PG extension.
120+
// Therefore, we need to perform both sorting and pagination manually on the server.
121+
fn list_by_semver(
122+
krate: &Crate,
123+
options: Option<&PaginationOptions>,
124+
req: &Parts,
125+
conn: &mut PgConnection,
126+
) -> AppResult<PaginatedVersionsAndPublishers> {
127+
let (data, total) = if let Some(options) = options {
128+
// Sorting by semver but opted for id as the seek key because num can be quite lengthy,
129+
// while id values are significantly smaller.
130+
let mut sorted_versions = IndexMap::new();
131+
for result in krate
132+
.all_versions()
133+
.select((versions::id, versions::num))
134+
.load_iter::<(i32, String), DefaultLoadingMode>(conn)?
135+
{
136+
let (id, num) = result?;
137+
sorted_versions.insert(id, (num, None));
138+
}
139+
sorted_versions.sort_by_cached_key(|_, (num, _)| Reverse(semver::Version::parse(num).ok()));
140+
141+
let mut idx = Some(0);
142+
if let Page::Seek(ref seek) = options.page {
143+
idx = seek
144+
.decode::<i32>()
145+
.map(|id| sorted_versions.get_index_of(&id))?
146+
.filter(|i| i + 1 < sorted_versions.len())
147+
.map(|i| i + 1);
148+
}
149+
if let Some(start) = idx {
150+
let end = (start + options.per_page as usize).min(sorted_versions.len());
151+
let ids = sorted_versions[start..end].keys().collect::<Vec<_>>();
152+
for result in krate
153+
.all_versions()
154+
.left_outer_join(users::table)
155+
.select((versions::all_columns, users::all_columns.nullable()))
156+
.filter(versions::id.eq_any(ids))
157+
.load_iter::<(Version, Option<User>), DefaultLoadingMode>(conn)?
158+
{
159+
let row = result?;
160+
sorted_versions.insert(row.0.id, (row.0.num.to_owned(), Some(row)));
161+
}
162+
(
163+
sorted_versions
164+
.values()
165+
.flat_map(|(_, v)| v)
166+
.cloned()
167+
.collect(),
168+
sorted_versions.len(),
169+
)
170+
} else {
171+
(vec![], 0)
172+
}
173+
} else {
174+
let mut data: Vec<(Version, Option<User>)> = krate
175+
.all_versions()
176+
.left_outer_join(users::table)
177+
.select((versions::all_columns, users::all_columns.nullable()))
178+
.load(conn)?;
179+
data.sort_by_cached_key(|(version, _)| Reverse(semver::Version::parse(&version.num).ok()));
180+
let total = data.len();
181+
(data, total)
182+
};
183+
184+
let mut next_page = None;
185+
if let Some(options) = options {
186+
next_page =
187+
next_seek_params(&data, options, |last| last.0.id)?.map(|p| req.query_with_params(p))
188+
};
189+
190+
Ok(PaginatedVersionsAndPublishers {
191+
data,
192+
meta: ResponseMeta {
193+
total: total as i64,
194+
next_page,
195+
},
196+
})
197+
}
198+
199+
mod seek {
200+
use chrono::naive::serde::ts_microseconds;
201+
use serde::{Deserialize, Serialize};
202+
203+
#[derive(Deserialize, Serialize)]
204+
pub(super) struct Date(
205+
#[serde(with = "ts_microseconds")] pub(super) chrono::NaiveDateTime,
206+
pub(super) i32,
207+
);
208+
}
209+
210+
fn next_seek_params<T, S, F>(
211+
records: &[T],
212+
options: &PaginationOptions,
213+
f: F,
214+
) -> AppResult<Option<IndexMap<String, String>>>
215+
where
216+
F: Fn(&T) -> S,
217+
S: serde::Serialize,
218+
{
219+
if matches!(options.page, Page::Numeric(_)) || records.len() < options.per_page as usize {
220+
return Ok(None);
221+
}
222+
223+
let mut opts = IndexMap::new();
224+
match options.page {
225+
Page::Unspecified | Page::Seek(_) => {
226+
let seek = f(records.last().unwrap());
227+
opts.insert("seek".into(), encode_seek(seek)?);
228+
}
229+
Page::Numeric(_) => unreachable!(),
230+
};
231+
Ok(Some(opts))
232+
}
233+
234+
struct PaginatedVersionsAndPublishers {
235+
data: Vec<(Version, Option<User>)>,
236+
meta: ResponseMeta,
237+
}
238+
239+
#[derive(Serialize)]
240+
struct ResponseMeta {
241+
total: i64,
242+
next_page: Option<String>,
243+
}

0 commit comments

Comments
 (0)