Skip to content

Commit df1bfb5

Browse files
authored
Merge pull request #10490 from eth3lbert/msw-seek-versions
msw: Add seek pagination support for `GET /api/v1/crates/:name/versions`
2 parents 010298d + 46011f0 commit df1bfb5

File tree

3 files changed

+149
-5
lines changed

3 files changed

+149
-5
lines changed

packages/crates-io-msw/handlers/versions/list.js

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { http, HttpResponse } from 'msw';
2+
import compareSemver from 'semver/functions/compare-loose.js';
23

34
import { db } from '../../index.js';
45
import { serializeVersion } from '../../serializers/version.js';
@@ -18,18 +19,43 @@ export default http.get('/api/v1/crates/:name/versions', async ({ request, param
1819
versions = versions.filter(v => nums.includes(v.num));
1920
}
2021

21-
versions.sort((a, b) => b.id - a.id);
22+
let sort = url.searchParams.get('sort');
23+
versions =
24+
sort == 'date' ? versions.sort((a, b) => b.id - a.id) : versions.sort((a, b) => compareSemver(b.num, a.num));
25+
2226
let total = versions.length;
2327

2428
let include = url.searchParams.get('include') ?? '';
2529
let includes = include ? include.split(',') : [];
26-
27-
let serializedVersions = versions.map(v => serializeVersion(v, { includePublishedBy: true }));
2830
let meta = { total, next_page: null };
2931

3032
if (includes.includes('release_tracks')) {
3133
meta.release_tracks = calculateReleaseTracks(versions);
3234
}
3335

36+
// seek pagination
37+
// A simplified seek encoding is applied here for testing purposes only. It should be opaque in
38+
// real-world scenarios.
39+
let next_seek = null;
40+
let per_page = url.searchParams.get('per_page');
41+
if (per_page != null) {
42+
let seek = url.searchParams.get('seek');
43+
if (seek != null) {
44+
let start = versions.findIndex(it => it.num === seek);
45+
versions = versions.slice(start + 1);
46+
}
47+
versions = versions.slice(0, parseInt(per_page));
48+
49+
if (versions.length == per_page) {
50+
next_seek = versions.at(-1).num;
51+
}
52+
}
53+
if (next_seek) {
54+
let next_params = new URLSearchParams(url.searchParams);
55+
next_params.set('seek', next_seek);
56+
meta.next_page = `?${next_params}`;
57+
}
58+
59+
let serializedVersions = versions.map(v => serializeVersion(v, { includePublishedBy: true }));
3460
return HttpResponse.json({ versions: serializedVersions, meta });
3561
});

packages/crates-io-msw/handlers/versions/list.test.js

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,45 @@ test('returns all versions belonging to the specified crate', async function ()
104104
});
105105
});
106106

107+
test('supports `sort` parameters', async function () {
108+
let user = db.user.create();
109+
let crate = db.crate.create({ name: 'rand' });
110+
db.version.create({ crate, num: '1.0.0' });
111+
db.version.create({ crate, num: '2.0.0-alpha', publishedBy: user });
112+
db.version.create({ crate, num: '1.1.0', rust_version: '1.69' });
113+
114+
// sort by `semver` by default
115+
{
116+
let response = await fetch('/api/v1/crates/rand/versions');
117+
assert.strictEqual(response.status, 200);
118+
let json = await response.json();
119+
assert.deepEqual(
120+
json.versions.map(it => it.num),
121+
['2.0.0-alpha', '1.1.0', '1.0.0'],
122+
);
123+
}
124+
125+
{
126+
let response = await fetch('/api/v1/crates/rand/versions?sort=semver');
127+
assert.strictEqual(response.status, 200);
128+
let json = await response.json();
129+
assert.deepEqual(
130+
json.versions.map(it => it.num),
131+
['2.0.0-alpha', '1.1.0', '1.0.0'],
132+
);
133+
}
134+
135+
{
136+
let response = await fetch('/api/v1/crates/rand/versions?sort=date');
137+
assert.strictEqual(response.status, 200);
138+
let json = await response.json();
139+
assert.deepEqual(
140+
json.versions.map(it => it.num),
141+
['1.1.0', '2.0.0-alpha', '1.0.0'],
142+
);
143+
}
144+
});
145+
107146
test('supports multiple `ids[]` parameters', async function () {
108147
let user = db.user.create();
109148
let crate = db.crate.create({ name: 'rand' });
@@ -119,6 +158,85 @@ test('supports multiple `ids[]` parameters', async function () {
119158
);
120159
});
121160

161+
test('supports seek pagination', async function () {
162+
let user = db.user.create();
163+
let crate = db.crate.create({ name: 'rand' });
164+
db.version.create({ crate, num: '1.0.0' });
165+
db.version.create({ crate, num: '2.0.0-alpha', publishedBy: user });
166+
db.version.create({ crate, num: '1.1.0', rust_version: '1.69' });
167+
168+
async function seek_forwards(queryParams) {
169+
let calls = 0;
170+
let next_page;
171+
let responses = [];
172+
let base_url = '/api/v1/crates/rand/versions';
173+
let params = new URLSearchParams(queryParams);
174+
let url = `${base_url}?${params}`;
175+
while ((calls == 0 || next_page) && calls < 50) {
176+
if (next_page) {
177+
url = `${base_url}${next_page}`;
178+
}
179+
let response = await fetch(url);
180+
calls += 1;
181+
assert.strictEqual(response.status, 200);
182+
let json = await response.json();
183+
responses.push(json);
184+
next_page = json.meta.next_page;
185+
if (next_page == null) {
186+
break;
187+
}
188+
}
189+
return responses;
190+
}
191+
192+
// sort by `semver` by default
193+
{
194+
let responses = await seek_forwards({ per_page: 1 });
195+
assert.deepEqual(
196+
responses.map(it => it.versions.map(v => v.num)),
197+
[['2.0.0-alpha'], ['1.1.0'], ['1.0.0'], []],
198+
);
199+
assert.deepEqual(
200+
responses.map(it => it.meta.next_page),
201+
['?per_page=1&seek=2.0.0-alpha', '?per_page=1&seek=1.1.0', '?per_page=1&seek=1.0.0', null],
202+
);
203+
}
204+
205+
{
206+
let responses = await seek_forwards({ per_page: 1, sort: 'semver' });
207+
assert.deepEqual(
208+
responses.map(it => it.versions.map(v => v.num)),
209+
[['2.0.0-alpha'], ['1.1.0'], ['1.0.0'], []],
210+
);
211+
assert.deepEqual(
212+
responses.map(it => it.meta.next_page),
213+
[
214+
'?per_page=1&sort=semver&seek=2.0.0-alpha',
215+
'?per_page=1&sort=semver&seek=1.1.0',
216+
'?per_page=1&sort=semver&seek=1.0.0',
217+
null,
218+
],
219+
);
220+
}
221+
222+
{
223+
let responses = await seek_forwards({ per_page: 1, sort: 'date' });
224+
assert.deepEqual(
225+
responses.map(it => it.versions.map(v => v.num)),
226+
[['1.1.0'], ['2.0.0-alpha'], ['1.0.0'], []],
227+
);
228+
assert.deepEqual(
229+
responses.map(it => it.meta.next_page),
230+
[
231+
'?per_page=1&sort=date&seek=1.1.0',
232+
'?per_page=1&sort=date&seek=2.0.0-alpha',
233+
'?per_page=1&sort=date&seek=1.0.0',
234+
null,
235+
],
236+
);
237+
}
238+
});
239+
122240
test('include `release_tracks` meta', async function () {
123241
let user = db.user.create();
124242
let crate = db.crate.create({ name: 'rand' });

tests/models/version-test.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ module('Model | Version', function (hooks) {
154154
'0.3.3',
155155
'0.3.2',
156156
'0.3.1',
157-
'0.3.0-alpha.01',
158157
'0.3.0',
158+
'0.3.0-alpha.01',
159159
'0.2.1',
160160
'0.2.0',
161161
'0.1.2',
@@ -182,8 +182,8 @@ module('Model | Version', function (hooks) {
182182
{ num: '0.3.3', isHighestOfReleaseTrack: false },
183183
{ num: '0.3.2', isHighestOfReleaseTrack: false },
184184
{ num: '0.3.1', isHighestOfReleaseTrack: false },
185-
{ num: '0.3.0-alpha.01', isHighestOfReleaseTrack: false },
186185
{ num: '0.3.0', isHighestOfReleaseTrack: false },
186+
{ num: '0.3.0-alpha.01', isHighestOfReleaseTrack: false },
187187
{ num: '0.2.1', isHighestOfReleaseTrack: true },
188188
{ num: '0.2.0', isHighestOfReleaseTrack: false },
189189
{ num: '0.1.2', isHighestOfReleaseTrack: true },

0 commit comments

Comments
 (0)