2
2
3
3
use std:: cmp:: Reverse ;
4
4
5
+ use diesel:: connection:: DefaultLoadingMode ;
6
+ use indexmap:: IndexMap ;
7
+
5
8
use crate :: controllers:: frontend_prelude:: * ;
9
+ use crate :: controllers:: helpers:: pagination:: { encode_seek, Page , PaginationOptions } ;
6
10
7
11
use crate :: models:: { Crate , CrateVersions , User , Version , VersionOwnerAction } ;
8
12
use crate :: schema:: { users, versions} ;
9
13
use crate :: util:: errors:: crate_not_found;
10
14
use crate :: views:: EncodableVersion ;
11
15
12
16
/// 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 > > {
16
22
spawn_blocking ( move || {
17
23
let conn = & mut * state. db_read ( ) ?;
18
24
@@ -21,27 +27,256 @@ pub async fn versions(state: AppState, Path(crate_name): Path<String>) -> AppRes
21
27
. optional ( ) ?
22
28
. ok_or_else ( || crate_not_found ( & crate_name) ) ?;
23
29
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
+ }
29
41
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
+ } ;
32
48
33
49
let versions = versions_and_publishers
50
+ . data
34
51
. iter ( )
35
52
. map ( |( v, _) | v)
36
53
. cloned ( )
37
54
. collect :: < Vec < _ > > ( ) ;
38
55
let versions = versions_and_publishers
56
+ . data
39
57
. into_iter ( )
40
58
. zip ( VersionOwnerAction :: for_versions ( conn, & versions) ?)
41
59
. map ( |( ( v, pb) , aas) | EncodableVersion :: from ( v, & crate_name, pb, aas) )
42
60
. collect :: < Vec < _ > > ( ) ;
43
61
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
+ } ) )
45
66
} )
46
67
. await
47
68
}
69
+
70
+ /// Seek-based pagination of versions by date
71
+ ///
72
+ /// # Panics
73
+ ///
74
+ /// This function will panic if `option` is built with `enable_pages` set to true.
75
+ fn list_by_date (
76
+ krate : & Crate ,
77
+ options : Option < & PaginationOptions > ,
78
+ req : & Parts ,
79
+ conn : & mut PgConnection ,
80
+ ) -> AppResult < PaginatedVersionsAndPublishers > {
81
+ use seek:: * ;
82
+
83
+ let mut query = krate
84
+ . all_versions ( )
85
+ . left_outer_join ( users:: table)
86
+ . select ( ( versions:: all_columns, users:: all_columns. nullable ( ) ) ) ;
87
+
88
+ if let Some ( options) = options {
89
+ assert ! (
90
+ !matches!( & options. page, Page :: Numeric ( _) ) ,
91
+ "?page= is not supported"
92
+ ) ;
93
+ if let Some ( SeekPayload :: Date ( Date ( created_at, id) ) ) = Seek :: Date . after ( & options. page ) ? {
94
+ query = query. filter (
95
+ versions:: created_at
96
+ . eq ( created_at)
97
+ . and ( versions:: id. lt ( id) )
98
+ . or ( versions:: created_at. lt ( created_at) ) ,
99
+ )
100
+ }
101
+ query = query. limit ( options. per_page ) ;
102
+ }
103
+
104
+ query = query. order ( ( versions:: created_at. desc ( ) , versions:: id. desc ( ) ) ) ;
105
+
106
+ let data: Vec < ( Version , Option < User > ) > = query. load ( conn) ?;
107
+ let mut next_page = None ;
108
+ if let Some ( options) = options {
109
+ next_page = next_seek_params ( & data, options, |last| Seek :: Date . to_payload ( last) ) ?
110
+ . map ( |p| req. query_with_params ( p) ) ;
111
+ } ;
112
+
113
+ // Since the total count is retrieved through an additional query, to maintain consistency
114
+ // with other pagination methods, we only make a count query while data is not empty.
115
+ let total = if !data. is_empty ( ) {
116
+ krate. all_versions ( ) . count ( ) . get_result ( conn) ?
117
+ } else {
118
+ 0
119
+ } ;
120
+
121
+ Ok ( PaginatedVersionsAndPublishers {
122
+ data,
123
+ meta : ResponseMeta { total, next_page } ,
124
+ } )
125
+ }
126
+
127
+ /// Seek-based pagination of versions by semver
128
+ ///
129
+ /// # Panics
130
+ ///
131
+ /// This function will panic if `option` is built with `enable_pages` set to true.
132
+
133
+ // Unfortunately, Heroku Postgres has no support for the semver PG extension.
134
+ // Therefore, we need to perform both sorting and pagination manually on the server.
135
+ fn list_by_semver (
136
+ krate : & Crate ,
137
+ options : Option < & PaginationOptions > ,
138
+ req : & Parts ,
139
+ conn : & mut PgConnection ,
140
+ ) -> AppResult < PaginatedVersionsAndPublishers > {
141
+ use seek:: * ;
142
+
143
+ let ( data, total) = if let Some ( options) = options {
144
+ // Since versions will only increase in the future and both sorting and pagination need to
145
+ // happen on the app server, implementing it with fetching only the data needed for sorting
146
+ // and pagination, then making another query for the data to respond with, would minimize
147
+ // payload and memory usage. This way, we can utilize the sorted map and enrich it later
148
+ // without sorting twice.
149
+ // Sorting by semver but opted for id as the seek key because num can be quite lengthy,
150
+ // while id values are significantly smaller.
151
+ let mut sorted_versions = IndexMap :: new ( ) ;
152
+ for result in krate
153
+ . all_versions ( )
154
+ . select ( ( versions:: id, versions:: num) )
155
+ . load_iter :: < ( i32 , String ) , DefaultLoadingMode > ( conn) ?
156
+ {
157
+ let ( id, num) = result?;
158
+ sorted_versions. insert ( id, ( num, None ) ) ;
159
+ }
160
+ sorted_versions. sort_by_cached_key ( |_, ( num, _) | Reverse ( semver:: Version :: parse ( num) . ok ( ) ) ) ;
161
+
162
+ assert ! (
163
+ !matches!( & options. page, Page :: Numeric ( _) ) ,
164
+ "?page= is not supported"
165
+ ) ;
166
+ let mut idx = Some ( 0 ) ;
167
+ if let Some ( SeekPayload :: Semver ( Semver ( id) ) ) = Seek :: Semver . after ( & options. page ) ? {
168
+ idx = sorted_versions
169
+ . get_index_of ( & id)
170
+ . filter ( |i| i + 1 < sorted_versions. len ( ) )
171
+ . map ( |i| i + 1 ) ;
172
+ }
173
+ if let Some ( start) = idx {
174
+ let end = ( start + options. per_page as usize ) . min ( sorted_versions. len ( ) ) ;
175
+ let ids = sorted_versions[ start..end] . keys ( ) . collect :: < Vec < _ > > ( ) ;
176
+ for result in krate
177
+ . all_versions ( )
178
+ . left_outer_join ( users:: table)
179
+ . select ( ( versions:: all_columns, users:: all_columns. nullable ( ) ) )
180
+ . filter ( versions:: id. eq_any ( ids) )
181
+ . load_iter :: < ( Version , Option < User > ) , DefaultLoadingMode > ( conn) ?
182
+ {
183
+ let row = result?;
184
+ sorted_versions. insert ( row. 0 . id , ( row. 0 . num . to_owned ( ) , Some ( row) ) ) ;
185
+ }
186
+
187
+ let len = sorted_versions. len ( ) ;
188
+ (
189
+ sorted_versions
190
+ . into_values ( )
191
+ . filter_map ( |( _, v) | v)
192
+ . collect ( ) ,
193
+ len,
194
+ )
195
+ } else {
196
+ ( vec ! [ ] , 0 )
197
+ }
198
+ } else {
199
+ let mut data: Vec < ( Version , Option < User > ) > = krate
200
+ . all_versions ( )
201
+ . left_outer_join ( users:: table)
202
+ . select ( ( versions:: all_columns, users:: all_columns. nullable ( ) ) )
203
+ . load ( conn) ?;
204
+ data. sort_by_cached_key ( |( version, _) | Reverse ( semver:: Version :: parse ( & version. num ) . ok ( ) ) ) ;
205
+ let total = data. len ( ) ;
206
+ ( data, total)
207
+ } ;
208
+
209
+ let mut next_page = None ;
210
+ if let Some ( options) = options {
211
+ next_page = next_seek_params ( & data, options, |last| Seek :: Semver . to_payload ( last) ) ?
212
+ . map ( |p| req. query_with_params ( p) )
213
+ } ;
214
+
215
+ Ok ( PaginatedVersionsAndPublishers {
216
+ data,
217
+ meta : ResponseMeta {
218
+ total : total as i64 ,
219
+ next_page,
220
+ } ,
221
+ } )
222
+ }
223
+
224
+ mod seek {
225
+ use crate :: controllers:: helpers:: pagination:: seek;
226
+ use crate :: models:: { User , Version } ;
227
+ use chrono:: naive:: serde:: ts_microseconds;
228
+
229
+ // We might consider refactoring this to use named fields, which would be clearer and more
230
+ // flexible. It's also worth noting that we currently encode seek compactly as a Vec, which
231
+ // doesn't include field names.
232
+ seek ! {
233
+ pub enum Seek {
234
+ Semver ( i32 )
235
+ Date ( #[ serde( with="ts_microseconds" ) ] chrono:: NaiveDateTime , i32 )
236
+ }
237
+ }
238
+
239
+ impl Seek {
240
+ pub ( crate ) fn to_payload ( & self , record : & ( Version , Option < User > ) ) -> SeekPayload {
241
+ match * self {
242
+ Seek :: Semver => SeekPayload :: Semver ( Semver ( record. 0 . id ) ) ,
243
+ Seek :: Date => SeekPayload :: Date ( Date ( record. 0 . created_at , record. 0 . id ) ) ,
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ fn next_seek_params < T , S , F > (
250
+ records : & [ T ] ,
251
+ options : & PaginationOptions ,
252
+ f : F ,
253
+ ) -> AppResult < Option < IndexMap < String , String > > >
254
+ where
255
+ F : Fn ( & T ) -> S ,
256
+ S : serde:: Serialize ,
257
+ {
258
+ if matches ! ( options. page, Page :: Numeric ( _) ) || records. len ( ) < options. per_page as usize {
259
+ return Ok ( None ) ;
260
+ }
261
+
262
+ let mut opts = IndexMap :: new ( ) ;
263
+ match options. page {
264
+ Page :: Unspecified | Page :: Seek ( _) => {
265
+ let seek = f ( records. last ( ) . unwrap ( ) ) ;
266
+ opts. insert ( "seek" . into ( ) , encode_seek ( seek) ?) ;
267
+ }
268
+ Page :: Numeric ( _) => unreachable ! ( ) ,
269
+ } ;
270
+ Ok ( Some ( opts) )
271
+ }
272
+
273
+ struct PaginatedVersionsAndPublishers {
274
+ data : Vec < ( Version , Option < User > ) > ,
275
+ meta : ResponseMeta ,
276
+ }
277
+
278
+ #[ derive( Serialize ) ]
279
+ struct ResponseMeta {
280
+ total : i64 ,
281
+ next_page : Option < String > ,
282
+ }
0 commit comments