From f579f9c37d0f891fe3d36c33c08bd4210da48cf7 Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Wed, 18 Dec 2024 15:54:15 +0800 Subject: [PATCH 1/9] mirage: Respect `include` query params in `GET /api/v1/crates/:id` --- mirage/route-handlers/crates.js | 13 +++++--- mirage/serializers/crate.js | 44 ++++++++++++++++++--------- tests/mirage/crates/get-by-id-test.js | 30 ++++++++++++++++++ 3 files changed, 68 insertions(+), 19 deletions(-) diff --git a/mirage/route-handlers/crates.js b/mirage/route-handlers/crates.js index 0e84d26eadc..637dbf7430a 100644 --- a/mirage/route-handlers/crates.js +++ b/mirage/route-handlers/crates.js @@ -58,12 +58,15 @@ export function register(server) { let { name } = request.params; let crate = schema.crates.findBy({ name }); if (!crate) return notFound(); - + let serialized = this.serialize(crate); return { - ...this.serialize(crate), - ...this.serialize(crate.categories), - ...this.serialize(crate.keywords), - ...this.serialize(crate.versions.sort((a, b) => Number(b.id) - Number(a.id))), + categories: null, + keywords: null, + versions: null, + ...serialized, + ...(serialized.crate.categories && this.serialize(crate.categories)), + ...(serialized.crate.keywords && this.serialize(crate.keywords)), + ...(serialized.crate.versions && this.serialize(crate.versions.sort((a, b) => Number(b.id) - Number(a.id)))), }; }); diff --git a/mirage/serializers/crate.js b/mirage/serializers/crate.js index 12a38b32918..bb9ee87f125 100644 --- a/mirage/serializers/crate.js +++ b/mirage/serializers/crate.js @@ -6,7 +6,15 @@ import semverSort from 'semver/functions/rsort'; import { compareIsoDates } from '../route-handlers/-utils'; import BaseSerializer from './application'; +const VALID_INCLUDE_MODEL = new Set(['versions', 'keywords', 'categories' /*, 'badges', 'downloads' */]); + export default BaseSerializer.extend({ + include(request) { + let include = request.queryParams.include; + return include == null || include === 'full' + ? VALID_INCLUDE_MODEL.values() + : include.split(',').filter(it => VALID_INCLUDE_MODEL.has(it)); + }, attrs: [ 'badges', 'categories', @@ -38,19 +46,20 @@ export default BaseSerializer.extend({ getHashForResource() { let [hash, addToIncludes] = BaseSerializer.prototype.getHashForResource.apply(this, arguments); + let includes = [...this.include(this.request)]; if (Array.isArray(hash)) { for (let resource of hash) { - this._adjust(resource); + this._adjust(resource, includes); } } else { - this._adjust(hash); + this._adjust(hash, includes); } return [hash, addToIncludes]; }, - _adjust(hash) { + _adjust(hash, includes) { let versions = this.schema.versions.where({ crateId: hash.id }); assert(`crate \`${hash.name}\` has no associated versions`, versions.length !== 0); @@ -63,25 +72,32 @@ export default BaseSerializer.extend({ versionNums[0]; hash.yanked = versionsByNum[hash.default_version]?.yanked ?? false; - versions = versions.filter(it => !it.yanked); - versionNums = versionNums.filter(it => !versionsByNum[it].yanked); - hash.max_version = versionNums[0] ?? '0.0.0'; - hash.max_stable_version = versionNums.find(it => !prerelease(it, { loose: true })) ?? null; + if (includes.includes('versions')) { + versions = versions.filter(it => !it.yanked); + versionNums = versionNums.filter(it => !versionsByNum[it].yanked); + hash.max_version = versionNums[0] ?? '0.0.0'; + hash.max_stable_version = versionNums.find(it => !prerelease(it, { loose: true })) ?? null; - let newestVersions = versions.models.sort((a, b) => compareIsoDates(b.updated_at, a.updated_at)); - hash.newest_version = newestVersions[0]?.num ?? '0.0.0'; + let newestVersions = versions.models.sort((a, b) => compareIsoDates(b.updated_at, a.updated_at)); + hash.newest_version = newestVersions[0]?.num ?? '0.0.0'; + + hash.versions = hash.version_ids; + } else { + hash.max_version = '0.0.0'; + hash.newest_version = '0.0.0'; + hash.max_stable_version = null; + hash.versions = null; + } + delete hash.version_ids; hash.id = hash.name; - hash.categories = hash.category_ids; + hash.categories = includes.includes('categories') ? hash.category_ids : null; delete hash.category_ids; - hash.keywords = hash.keyword_ids; + hash.keywords = includes.includes('keywords') ? hash.keyword_ids : null; delete hash.keyword_ids; - hash.versions = hash.version_ids; - delete hash.version_ids; - delete hash.team_owner_ids; delete hash.user_owner_ids; }, diff --git a/tests/mirage/crates/get-by-id-test.js b/tests/mirage/crates/get-by-id-test.js index d43075b07ff..461078b9e4d 100644 --- a/tests/mirage/crates/get-by-id-test.js +++ b/tests/mirage/crates/get-by-id-test.js @@ -193,4 +193,34 @@ module('Mirage | GET /api/v1/crates/:id', function (hooks) { }, ]); }); + + test('without versions included', async function (assert) { + this.server.create('category', { category: 'no-std' }); + this.server.create('category', { category: 'cli' }); + this.server.create('keyword', { keyword: 'no-std' }); + this.server.create('keyword', { keyword: 'cli' }); + let crate = this.server.create('crate', { name: 'rand', categoryIds: ['no-std'], keywordIds: ['no-std'] }); + this.server.create('version', { crate, num: '1.0.0' }); + this.server.create('version', { crate, num: '1.1.0' }); + this.server.create('version', { crate, num: '1.2.0' }); + + let req = await fetch('/api/v1/crates/rand'); + let expected = await req.json(); + + let response = await fetch('/api/v1/crates/rand?include=keywords,categories'); + assert.strictEqual(response.status, 200); + + let responsePayload = await response.json(); + assert.deepEqual(responsePayload, { + ...expected, + crate: { + ...expected.crate, + max_version: '0.0.0', + newest_version: '0.0.0', + max_stable_version: null, + versions: null, + }, + versions: null, + }); + }); }); From 99878764899b37f5d7554bbb75ca0f11bfdaf99b Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Wed, 18 Dec 2024 17:14:19 +0800 Subject: [PATCH 2/9] e2e/acceptance/versions: Fix potential flaky test case Wait before calling `evaluateAll()` to ensure all rows are available. --- e2e/acceptance/versions.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/acceptance/versions.spec.ts b/e2e/acceptance/versions.spec.ts index 36cc153dfe1..f3001f47a17 100644 --- a/e2e/acceptance/versions.spec.ts +++ b/e2e/acceptance/versions.spec.ts @@ -13,6 +13,7 @@ test.describe('Acceptance | crate versions page', { tag: '@acceptance' }, () => await page.goto('/crates/nanomsg/versions'); await expect(page).toHaveURL('/crates/nanomsg/versions'); + await expect(page.locator('[data-test-version]')).toHaveCount(4); let versions = await page.locator('[data-test-version]').evaluateAll(el => el.map(it => it.dataset.testVersion)); expect(versions).toEqual(['0.2.1', '0.3.0', '0.2.0', '0.1.0']); From 43bee2965a01d6dea49f78ba706d24df26c5bef6 Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Wed, 18 Dec 2024 17:20:23 +0800 Subject: [PATCH 3/9] mirage: Add `meta` field in `GET /api/v1/crates/:id/versions` This also always --- mirage/route-handlers/-utils.js | 16 ++++++++++++++ mirage/route-handlers/crates.js | 17 +++++++++++--- tests/mirage/crates/versions/list-test.js | 27 +++++++++++++++++++++++ 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/mirage/route-handlers/-utils.js b/mirage/route-handlers/-utils.js index b40b7ad4e49..cb727b41e98 100644 --- a/mirage/route-handlers/-utils.js +++ b/mirage/route-handlers/-utils.js @@ -1,4 +1,6 @@ import { Response } from 'miragejs'; +import semverParse from 'semver/functions/parse'; +import semverSort from 'semver/functions/rsort'; export function notFound() { return new Response( @@ -31,3 +33,17 @@ export function compareIsoDates(a, b) { let bDate = new Date(b); return aDate < bDate ? -1 : aDate > bDate ? 1 : 0; } + +export function releaseTracks(versions) { + let versionNums = versions.models.filter(it => !it.yanked).map(it => it.num); + semverSort(versionNums, { loose: true }); + let tracks = {}; + for (let num of versionNums) { + let semver = semverParse(num, { loose: true }); + if (!semver || semver.prerelease.length !== 0) continue; + let name = semver.major == 0 ? `0.${semver.minor}` : `${semver.major}`; + if (name in tracks) continue; + tracks[name] = { highest: num }; + } + return tracks; +} diff --git a/mirage/route-handlers/crates.js b/mirage/route-handlers/crates.js index 637dbf7430a..8633ae7f93f 100644 --- a/mirage/route-handlers/crates.js +++ b/mirage/route-handlers/crates.js @@ -1,7 +1,7 @@ import { Response } from 'miragejs'; import { getSession } from '../utils/session'; -import { compareIsoDates, compareStrings, notFound, pageParams } from './-utils'; +import { compareIsoDates, compareStrings, notFound, pageParams, releaseTracks } from './-utils'; export function list(schema, request) { const { start, end } = pageParams(request); @@ -140,12 +140,23 @@ export function register(server) { return { ok: true }; }); - server.get('/api/v1/crates/:name/versions', (schema, request) => { + server.get('/api/v1/crates/:name/versions', function (schema, request) { let { name } = request.params; let crate = schema.crates.findBy({ name }); if (!crate) return notFound(); - return crate.versions.sort((a, b) => compareIsoDates(b.created_at, a.created_at)); + let versions = crate.versions.sort((a, b) => compareIsoDates(b.created_at, a.created_at)); + let total = versions.length; + let include = request.queryParams?.include ?? ''; + let release_tracks = include.split(',').includes('release_tracks') && releaseTracks(crate.versions); + let resp = { + ...this.serialize(versions), + meta: { total, next_page: null }, + }; + if (release_tracks && Object.keys(release_tracks).length !== 0) { + resp.meta.release_tracks = release_tracks; + } + return resp; }); server.get('/api/v1/crates/:name/:version_num/authors', (schema, request) => { diff --git a/tests/mirage/crates/versions/list-test.js b/tests/mirage/crates/versions/list-test.js index 183c8dbb0b9..219607d90b7 100644 --- a/tests/mirage/crates/versions/list-test.js +++ b/tests/mirage/crates/versions/list-test.js @@ -22,6 +22,7 @@ module('Mirage | GET /api/v1/crates/:id/versions', function (hooks) { assert.strictEqual(response.status, 200); assert.deepEqual(await response.json(), { versions: [], + meta: { total: 0, next_page: null }, }); }); @@ -103,6 +104,32 @@ module('Mirage | GET /api/v1/crates/:id/versions', function (hooks) { yank_message: null, }, ], + meta: { total: 3, next_page: null }, + }); + }); + + test('include `release_tracks` meta', async function (assert) { + let user = this.server.create('user'); + let crate = this.server.create('crate', { name: 'rand' }); + this.server.create('version', { crate, num: '1.0.0' }); + this.server.create('version', { crate, num: '1.1.0', publishedBy: user }); + this.server.create('version', { crate, num: '1.2.0', rust_version: '1.69' }); + + let req = await fetch('/api/v1/crates/rand/versions'); + let expected = await req.json(); + + let response = await fetch('/api/v1/crates/rand/versions?include=release_tracks'); + assert.strictEqual(response.status, 200); + assert.deepEqual(await response.json(), { + ...expected, + meta: { + ...expected.meta, + release_tracks: { + 1: { + highest: '1.2.0', + }, + }, + }, }); }); }); From ada13ec2c4b8725acb17728812beed799f6141ea Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Thu, 19 Dec 2024 18:35:45 +0800 Subject: [PATCH 4/9] crate: load `versions` with `crate.loadVersionsTask` --- app/routes/crate/dependencies.js | 2 +- app/routes/crate/range.js | 2 +- app/routes/crate/version-dependencies.js | 2 +- app/routes/crate/version.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/routes/crate/dependencies.js b/app/routes/crate/dependencies.js index ec0a3b0c1f6..8ffa3c3aa8a 100644 --- a/app/routes/crate/dependencies.js +++ b/app/routes/crate/dependencies.js @@ -6,7 +6,7 @@ export default class VersionRoute extends Route { async model() { let crate = this.modelFor('crate'); - let versions = await crate.get('versions'); + let versions = await crate.loadVersionsTask.perform(); let { default_version } = crate; let version = versions.find(version => version.num === default_version) ?? versions.lastObject; diff --git a/app/routes/crate/range.js b/app/routes/crate/range.js index 666d35d2b3e..e33d511b7ee 100644 --- a/app/routes/crate/range.js +++ b/app/routes/crate/range.js @@ -15,7 +15,7 @@ export default class VersionRoute extends Route { let crate = this.modelFor('crate'); try { - let versions = await crate.hasMany('versions').load(); + let versions = await crate.loadVersionsTask.perform(); let allVersionNums = versions.map(it => it.num); let unyankedVersionNums = versions.filter(it => !it.yanked).map(it => it.num); diff --git a/app/routes/crate/version-dependencies.js b/app/routes/crate/version-dependencies.js index 9eb18fd4ec5..75068fd7af4 100644 --- a/app/routes/crate/version-dependencies.js +++ b/app/routes/crate/version-dependencies.js @@ -9,7 +9,7 @@ export default class VersionRoute extends Route { let versions; try { - versions = await crate.get('versions'); + versions = await crate.loadVersionsTask.perform(); } catch (error) { let title = `${crate.name}: Failed to load version data`; return this.router.replaceWith('catch-all', { transition, error, title, tryAgain: true }); diff --git a/app/routes/crate/version.js b/app/routes/crate/version.js index bc83718357c..683fbf8f3fd 100644 --- a/app/routes/crate/version.js +++ b/app/routes/crate/version.js @@ -16,7 +16,7 @@ export default class VersionRoute extends Route { let versions; try { - versions = await crate.get('versions'); + versions = await crate.loadVersionsTask.perform(); } catch (error) { let title = `${crate.name}: Failed to load version data`; return this.router.replaceWith('catch-all', { transition, error, title, tryAgain: true }); From 1801e161a5e410fa3d890659b1f6b325be6ec1b5 Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Thu, 19 Dec 2024 18:37:43 +0800 Subject: [PATCH 5/9] crate: Add reload support for `loadVersionsTask` --- app/models/crate.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/models/crate.js b/app/models/crate.js index 201be75d20c..cc73c860eb9 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -130,8 +130,10 @@ export default class Crate extends Model { return [...(teams ?? []), ...(users ?? [])]; }); - loadVersionsTask = task(async () => { - return (await this.versions) ?? []; + loadVersionsTask = task(async ({ reload = false } = {}) => { + let versionsRef = this.hasMany('versions'); + let fut = reload === true ? versionsRef.reload() : versionsRef.load(); + return (await fut) ?? []; }); } From 7cebb9132f0a1eb405a02e81352657335b066e5a Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Thu, 19 Dec 2024 18:56:14 +0800 Subject: [PATCH 6/9] crate: `findRecord` requests for `crate` without including `versions` by default This will ensure that `versions` is always fetched via a separate request, rather than being included in `crate` response. This allow us to migrate to a paginated version list in the future. --- app/adapters/crate.js | 9 +++++++++ e2e/acceptance/crate-dependencies.spec.ts | 7 ++++--- e2e/routes/crate/range.spec.ts | 7 ++++--- tests/acceptance/crate-dependencies-test.js | 6 +++--- tests/adapters/crate-test.js | 17 +++++++++++++++++ tests/models/version-test.js | 2 +- tests/routes/crate/range-test.js | 6 +++--- 7 files changed, 41 insertions(+), 13 deletions(-) diff --git a/app/adapters/crate.js b/app/adapters/crate.js index 9328d6cc674..9dc74256e29 100644 --- a/app/adapters/crate.js +++ b/app/adapters/crate.js @@ -5,6 +5,15 @@ const BULK_REQUEST_GROUP_SIZE = 10; export default class CrateAdapter extends ApplicationAdapter { coalesceFindRequests = true; + findRecord(store, type, id, snapshot) { + let { include } = snapshot; + // This ensures `crate.versions` are always fetched from another request. + if (include === undefined) { + snapshot.include = 'keywords,categories,badges,downloads'; + } + return super.findRecord(store, type, id, snapshot); + } + groupRecordsForFindMany(store, snapshots) { let result = []; for (let i = 0; i < snapshots.length; i += BULK_REQUEST_GROUP_SIZE) { diff --git a/e2e/acceptance/crate-dependencies.spec.ts b/e2e/acceptance/crate-dependencies.spec.ts index aa126869b8f..451412c9e91 100644 --- a/e2e/acceptance/crate-dependencies.spec.ts +++ b/e2e/acceptance/crate-dependencies.spec.ts @@ -76,16 +76,17 @@ test.describe('Acceptance | crate dependencies page', { tag: '@acceptance' }, () }); await ember.addHook(async owner => { - // Load `crate` and then explicitly unload the side-loaded `versions`. + // Load `crate` and ensure all `versions` are not loaded. let store = owner.lookup('service:store'); let crateRecord = await store.findRecord('crate', 'foo'); - let versions = crateRecord.hasMany('versions').value(); - versions.forEach(record => record.unloadRecord()); + let versionsRef = crateRecord.hasMany('versions'); + globalThis.version_ids = versionsRef.ids(); }); await page.goto('/crates/foo/1.0.0/dependencies'); await expect(page).toHaveURL('/crates/foo/1.0.0/dependencies'); + await page.waitForFunction(() => globalThis.version_ids?.length === 0); await expect(page.locator('[data-test-404-page]')).toBeVisible(); await expect(page.locator('[data-test-title]')).toHaveText('foo: Failed to load version data'); await expect(page.locator('[data-test-go-back]')).toHaveCount(0); diff --git a/e2e/routes/crate/range.spec.ts b/e2e/routes/crate/range.spec.ts index 3d760404f46..b6ba6744858 100644 --- a/e2e/routes/crate/range.spec.ts +++ b/e2e/routes/crate/range.spec.ts @@ -128,15 +128,16 @@ test.describe('Route | crate.range', { tag: '@routes' }, () => { }); await ember.addHook(async owner => { - // Load `crate` and then explicitly unload the side-loaded `versions`. + // Load `crate` and ensure all `versions` are not loaded. let store = owner.lookup('service:store'); let crateRecord = await store.findRecord('crate', 'foo'); - let versions = crateRecord.hasMany('versions').value(); - versions.forEach(record => record.unloadRecord()); + let versionsRef = crateRecord.hasMany('versions'); + globalThis.version_ids = versionsRef.ids(); }); await page.goto('/crates/foo/range/^3'); await expect(page).toHaveURL('/crates/foo/range/%5E3'); + await page.waitForFunction(() => globalThis.version_ids?.length === 0); await expect(page.locator('[data-test-404-page]')).toBeVisible(); await expect(page.locator('[data-test-title]')).toHaveText('foo: Failed to load version data'); await expect(page.locator('[data-test-go-back]')).toHaveCount(0); diff --git a/tests/acceptance/crate-dependencies-test.js b/tests/acceptance/crate-dependencies-test.js index 0abe94438b9..17e6a7612f7 100644 --- a/tests/acceptance/crate-dependencies-test.js +++ b/tests/acceptance/crate-dependencies-test.js @@ -78,11 +78,11 @@ module('Acceptance | crate dependencies page', function (hooks) { this.server.get('/api/v1/crates/:crate_name/versions', {}, 500); - // Load `crate` and then explicitly unload the side-loaded `versions`. + // Load `crate` and ensure all `versions` are not loaded. let store = this.owner.lookup('service:store'); let crateRecord = await store.findRecord('crate', 'foo'); - let versions = crateRecord.hasMany('versions').value(); - versions.forEach(record => record.unloadRecord()); + let versionsRef = crateRecord.hasMany('versions'); + assert.deepEqual(versionsRef.ids(), []); await visit('/crates/foo/1.0.0/dependencies'); assert.strictEqual(currentURL(), '/crates/foo/1.0.0/dependencies'); diff --git a/tests/adapters/crate-test.js b/tests/adapters/crate-test.js index 0b4a6722f27..2dcf265ce20 100644 --- a/tests/adapters/crate-test.js +++ b/tests/adapters/crate-test.js @@ -22,4 +22,21 @@ module('Adapter | crate', function (hooks) { assert.strictEqual(foo?.name, 'foo'); assert.strictEqual(bar?.name, 'bar'); }); + + test('findRecord requests do not include versions by default', async function (assert) { + let _foo = this.server.create('crate', { name: 'foo' }); + let version = this.server.create('version', { crate: _foo }); + + let store = this.owner.lookup('service:store'); + + let foo = await store.findRecord('crate', 'foo'); + assert.strictEqual(foo?.name, 'foo'); + + // versions should not be loaded yet + let versionsRef = foo.hasMany('versions'); + assert.deepEqual(versionsRef.ids(), []); + + await versionsRef.load(); + assert.deepEqual(versionsRef.ids(), [version.id]); + }); }); diff --git a/tests/models/version-test.js b/tests/models/version-test.js index 92203fc2e2c..fb92b98ff6f 100644 --- a/tests/models/version-test.js +++ b/tests/models/version-test.js @@ -229,7 +229,7 @@ module('Model | Version', function (hooks) { this.server.create('version', { crate, num: '0.4.2' }); this.server.create('version', { crate, num: '0.4.3', yanked: true }); crateRecord = await this.store.findRecord('crate', crate.name, { reload: true }); - versions = (await crateRecord.loadVersionsTask.perform()).slice(); + versions = (await crateRecord.loadVersionsTask.perform({ reload: true })).slice(); assert.deepEqual( versions.map(it => ({ num: it.num, isHighestOfReleaseTrack: it.isHighestOfReleaseTrack })), diff --git a/tests/routes/crate/range-test.js b/tests/routes/crate/range-test.js index 61474b7ad07..7ccb265136e 100644 --- a/tests/routes/crate/range-test.js +++ b/tests/routes/crate/range-test.js @@ -119,11 +119,11 @@ module('Route | crate.range', function (hooks) { this.server.get('/api/v1/crates/:crate_name/versions', {}, 500); - // Load `crate` and then explicitly unload the side-loaded `versions`. + // Load `crate` and ensure all `versions` are not loaded. let store = this.owner.lookup('service:store'); let crateRecord = await store.findRecord('crate', 'foo'); - let versions = crateRecord.hasMany('versions').value(); - versions.forEach(record => record.unloadRecord()); + let versionsRef = crateRecord.hasMany('versions'); + assert.deepEqual(versionsRef.ids(), []); await visit('/crates/foo/range/^3'); assert.strictEqual(currentURL(), '/crates/foo/range/%5E3'); From 4eac251a48b4b94a7455e423d4ada2635fe3b69a Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Tue, 17 Dec 2024 18:11:45 +0800 Subject: [PATCH 7/9] crate: Add `versions_meta` field --- app/models/crate.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/app/models/crate.js b/app/models/crate.js index cc73c860eb9..0414339f890 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -22,6 +22,22 @@ export default class Crate extends Model { @attr max_version; @attr max_stable_version; @attr newest_version; + /** + * @typedef {Object} VersionsMeta + * @property {number} total + * @property {string | null} next_page + * @property {Object.} release_tracks + * + * @typedef {Object} ReleaseTrackDetails + * @property {string} highest + **/ + /** + * This isn't an attribute in the crate response. + * It's actually the `meta` attribute that belongs to `versions` + * and needs to be assigned to `crate` manually. + * @type {VersionsMeta | null} + **/ + @attr versions_meta; @attr description; @attr homepage; From 7dc77de00a37f7876b9fec0e2f846e6e9ea7ddd8 Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Thu, 19 Dec 2024 20:31:15 +0800 Subject: [PATCH 8/9] crate: Add `withReleaseTracks` support for `findHasMany` --- app/adapters/crate.js | 28 ++++++++++++++++++++++++++++ app/models/crate.js | 13 +++++++++++-- tests/adapters/crate-test.js | 23 +++++++++++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/app/adapters/crate.js b/app/adapters/crate.js index 9dc74256e29..0328656d0b5 100644 --- a/app/adapters/crate.js +++ b/app/adapters/crate.js @@ -5,6 +5,34 @@ const BULK_REQUEST_GROUP_SIZE = 10; export default class CrateAdapter extends ApplicationAdapter { coalesceFindRequests = true; + async findHasMany(store, snapshot, url, relationship) { + if (relationship.key === 'versions') { + let { adapterOptions } = snapshot; + let data; + if (adapterOptions?.withReleaseTracks === true) { + data = { include: 'release_tracks' }; + } + return this.ajax(url, 'GET', { data }).then(resp => { + let crate = store.peekRecord('crate', snapshot.id); + if (resp.meta) { + let payload = { + crate: { + id: snapshot.id, + versions_meta: { + ...crate.versions_meta, + ...resp.meta, + }, + }, + }; + store.pushPayload(payload); + } + return resp; + }); + } + + return super.findHasMany(store, snapshot, url, relationship); + } + findRecord(store, type, id, snapshot) { let { include } = snapshot; // This ensures `crate.versions` are always fetched from another request. diff --git a/app/models/crate.js b/app/models/crate.js index 0414339f890..f580d02c418 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -146,9 +146,18 @@ export default class Crate extends Model { return [...(teams ?? []), ...(users ?? [])]; }); - loadVersionsTask = task(async ({ reload = false } = {}) => { + /** + * @param {Object} [Options] + * @param {bool} [Options.reload] + * @param {bool} [Options.withReleaseTracks] - default: true if it has not yet been loaded else false + **/ + loadVersionsTask = task(async ({ reload = false, withReleaseTracks } = {}) => { let versionsRef = this.hasMany('versions'); - let fut = reload === true ? versionsRef.reload() : versionsRef.load(); + let opts = { adapterOptions: { withReleaseTracks } }; + if (opts.adapterOptions.withReleaseTracks == null) { + opts.adapterOptions.withReleaseTracks = this?.versions_meta?.release_tracks == null; + } + let fut = reload === true ? versionsRef.reload(opts) : versionsRef.load(opts); return (await fut) ?? []; }); } diff --git a/tests/adapters/crate-test.js b/tests/adapters/crate-test.js index 2dcf265ce20..aaf90cbf752 100644 --- a/tests/adapters/crate-test.js +++ b/tests/adapters/crate-test.js @@ -39,4 +39,27 @@ module('Adapter | crate', function (hooks) { await versionsRef.load(); assert.deepEqual(versionsRef.ids(), [version.id]); }); + + test('findHasMany `versions` with `release_tracks` meta', async function (assert) { + let crate = this.server.create('crate', { name: 'foo' }); + this.server.create('version', { crate, num: '0.0.1' }); + this.server.create('version', { crate, num: '0.1.0' }); + this.server.create('version', { crate, num: '1.0.0' }); + + let store = this.owner.lookup('service:store'); + + let foo = await store.findRecord('crate', 'foo'); + assert.strictEqual(foo?.name, 'foo'); + assert.strictEqual(foo?.versions_map?.release_tracks, undefined); + + // load `versions` without `release_tracks` meta included + let versionsRef = foo.hasMany('versions'); + await versionsRef.load(); + assert.strictEqual(foo?.versions_meta?.release_tracks, undefined); + + // reload `versions` with `release_tracks` meta included + let resp = await versionsRef.reload({ adapterOptions: { withReleaseTracks: true } }); + let { meta } = resp; + assert.deepEqual(foo?.versions_meta?.release_tracks, meta.release_tracks); + }); }); From 12e91b1c218a81550caa0ef4004fcb058b9ae5ae Mon Sep 17 00:00:00 2001 From: eth3lbert Date: Thu, 19 Dec 2024 21:59:40 +0800 Subject: [PATCH 9/9] crate: Simplify `releaseTrackSet` using `release_tracks` meta --- app/models/crate.js | 19 ++++++++++--------- app/models/version.js | 2 +- tests/models/version-test.js | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/app/models/crate.js b/app/models/crate.js index f580d02c418..b02895ec50b 100644 --- a/app/models/crate.js +++ b/app/models/crate.js @@ -83,16 +83,17 @@ export default class Crate extends Model { return Object.fromEntries(versions.slice().map(v => [v.id, v])); } + /** + * @type {Set} + **/ @cached get releaseTrackSet() { - let map = new Map(); - let { versionsObj: versions, versionIdsBySemver } = this; - for (let id of versionIdsBySemver) { - let { releaseTrack, isPrerelease, yanked } = versions[id]; - if (releaseTrack && !isPrerelease && !yanked && !map.has(releaseTrack)) { - map.set(releaseTrack, id); - } - } - return new Set(map.values()); + let { release_tracks } = this.versions_meta ?? {}; + assert( + '`loadVersionsTask.perform({ withReleaseTracks: true })` must be called before calling `releaseTrackSet`', + release_tracks != null, + ); + let nums = Object.values(release_tracks ?? {}).map(it => it.highest) ?? []; + return new Set(nums); } hasOwnerUser(userId) { diff --git a/app/models/version.js b/app/models/version.js index 16171ed918f..c93d041935b 100644 --- a/app/models/version.js +++ b/app/models/version.js @@ -106,7 +106,7 @@ export default class Version extends Model { return false; } - return this.crate?.releaseTrackSet.has(this.id); + return this.crate?.releaseTrackSet.has(this.num); } get featureList() { diff --git a/tests/models/version-test.js b/tests/models/version-test.js index fb92b98ff6f..97265f8a5aa 100644 --- a/tests/models/version-test.js +++ b/tests/models/version-test.js @@ -229,7 +229,7 @@ module('Model | Version', function (hooks) { this.server.create('version', { crate, num: '0.4.2' }); this.server.create('version', { crate, num: '0.4.3', yanked: true }); crateRecord = await this.store.findRecord('crate', crate.name, { reload: true }); - versions = (await crateRecord.loadVersionsTask.perform({ reload: true })).slice(); + versions = (await crateRecord.loadVersionsTask.perform({ reload: true, withReleaseTracks: true })).slice(); assert.deepEqual( versions.map(it => ({ num: it.num, isHighestOfReleaseTrack: it.isHighestOfReleaseTrack })),