diff --git a/website/src/components/Edit/EditPage.tsx b/website/src/components/Edit/EditPage.tsx index 4f2251065..0e42c2057 100644 --- a/website/src/components/Edit/EditPage.tsx +++ b/website/src/components/Edit/EditPage.tsx @@ -6,6 +6,7 @@ import type { Row } from './InputField.tsx'; import { getClientLogger } from '../../clientLogger.ts'; import { routes } from '../../routes.ts'; import { backendClientHooks } from '../../services/serviceHooks.ts'; +import { ACCESSION_FIELD } from '../../settings.ts'; import type { MetadataRecord, ProcessingAnnotationSourceType, SequenceEntryToEdit } from '../../types/backend.ts'; import type { ClientConfig } from '../../types/runtimeConfig.ts'; import { createAuthorizationHeader } from '../../utils/createAuthorizationHeader.ts'; @@ -91,7 +92,7 @@ const InnerEditPage: FC = ({ organism, dataToEdit, clientConfig, key !== 'accession')} + editedMetadata={editedMetadata.filter(({ key }) => key !== ACCESSION_FIELD)} setEditedMetadata={setEditedMetadata} /> { }); }); +describe('getVersionStatus', () => { + test('should return status for correct status', () => { + const versionStatus = getVersionStatus([ + { + label: 'does not matter', + name: VERSION_STATUS_FIELD, + value: siloVersionStatuses.latestVersion, + }, + ]); + + expect(versionStatus).toStrictEqual(siloVersionStatuses.latestVersion); + }); + + test('should throw error for unknown status', () => { + expect(() => + getVersionStatus([ + { + label: 'does not matter', + name: VERSION_STATUS_FIELD, + value: 'unknown status', + }, + ]), + ).toThrowError(/Invalid version status: "unknown status"/); + }); +}); + const nucleotideMutations = [ { count: 0, proportion: 0, mutation: 'nucleotideMutation1' }, { count: 0, proportion: 0, mutation: 'nucleotideDeletion1-' }, diff --git a/website/src/components/SequenceDetailsPage/getTableData.ts b/website/src/components/SequenceDetailsPage/getTableData.ts index 0c83403a7..39f4c7837 100644 --- a/website/src/components/SequenceDetailsPage/getTableData.ts +++ b/website/src/components/SequenceDetailsPage/getTableData.ts @@ -1,16 +1,19 @@ import { sentenceCase } from 'change-case'; import { err, Result } from 'neverthrow'; +import { type LapisClient } from '../../services/lapisClient.ts'; +import { VERSION_STATUS_FIELD } from '../../settings.ts'; +import type { AccessionVersion, ProblemDetail } from '../../types/backend.ts'; +import type { Schema } from '../../types/config.ts'; import { - isSiloVersionStatus, - type LapisClient, + type Details, + type DetailsResponse, + type InsertionCount, + type MutationProportionCount, type SequenceEntryHistory, type SiloVersionStatus, - siloVersionStatuses, -} from '../../services/lapisClient.ts'; -import type { AccessionVersion, ProblemDetail } from '../../types/backend.ts'; -import type { Schema } from '../../types/config.ts'; -import type { Details, DetailsResponse, InsertionCount, MutationProportionCount } from '../../types/lapis.ts'; + siloVersionStatusSchema, +} from '../../types/lapis.ts'; export type TableDataEntry = { label: string; name: string; value: string | number }; @@ -50,18 +53,15 @@ export async function getTableData( } export function getVersionStatus(tableData: TableDataEntry[]): SiloVersionStatus { - const versionStatus = tableData.find((pred) => pred.name === 'versionStatus')?.value.toString() ?? undefined; + const versionStatus = tableData.find((pred) => pred.name === VERSION_STATUS_FIELD)?.value.toString() ?? undefined; - if (isSiloVersionStatus(versionStatus) === false) { - throw new Error( - 'Invalid version status: ' + - JSON.stringify(versionStatus) + - ' not in ' + - JSON.stringify(Object.values(siloVersionStatuses)), - ); + const parsedStatus = siloVersionStatusSchema.safeParse(versionStatus); + + if (parsedStatus.success) { + return parsedStatus.data; } - return versionStatus as SiloVersionStatus; + throw new Error('Invalid version status: ' + JSON.stringify(versionStatus) + ': ' + parsedStatus.error.toString()); } export function getLatestAccessionVersion(sequenceEntryHistory: SequenceEntryHistory): AccessionVersion | undefined { diff --git a/website/src/pages/[organism]/sequences/[accessionVersion]/getSequenceDetailsTableData.ts b/website/src/pages/[organism]/sequences/[accessionVersion]/getSequenceDetailsTableData.ts index c08b290bd..dbc8a3c07 100644 --- a/website/src/pages/[organism]/sequences/[accessionVersion]/getSequenceDetailsTableData.ts +++ b/website/src/pages/[organism]/sequences/[accessionVersion]/getSequenceDetailsTableData.ts @@ -7,8 +7,9 @@ import { } from '../../../../components/SequenceDetailsPage/getTableData.ts'; import { getSchema } from '../../../../config.ts'; import { routes } from '../../../../routes.ts'; -import { LapisClient, type SequenceEntryHistory, type SiloVersionStatus } from '../../../../services/lapisClient.ts'; +import { LapisClient } from '../../../../services/lapisClient.ts'; import type { ProblemDetail } from '../../../../types/backend.ts'; +import type { SequenceEntryHistory, SiloVersionStatus } from '../../../../types/lapis.ts'; import { parseAccessionVersionFromString } from '../../../../utils/extractAccessionVersion.ts'; export enum SequenceDetailsTableResultType { diff --git a/website/src/services/lapisClient.ts b/website/src/services/lapisClient.ts index 86e1ba21f..dac103360 100644 --- a/website/src/services/lapisClient.ts +++ b/website/src/services/lapisClient.ts @@ -1,41 +1,16 @@ import type { Narrow } from '@zodios/core/lib/utils.types'; -import { err, ok, Result } from 'neverthrow'; -import { z } from 'zod'; +import { err, ok, type Result } from 'neverthrow'; import { lapisApi } from './lapisApi.ts'; import { ZodiosWrapperClient } from './zodiosWrapperClient.ts'; import { getLapisUrl, getRuntimeConfig, getSchema } from '../config.ts'; import { getInstanceLogger, type InstanceLogger } from '../logger.ts'; +import { ACCESSION_FIELD, VERSION_FIELD, VERSION_STATUS_FIELD } from '../settings.ts'; import { accessionVersion, type AccessionVersion, type ProblemDetail } from '../types/backend.ts'; import type { Schema } from '../types/config.ts'; +import { sequenceEntryHistory, type SequenceEntryHistory, siloVersionStatuses } from '../types/lapis.ts'; import type { BaseType } from '../utils/sequenceTypeHelpers.ts'; -export const siloVersionStatuses = { - revoked: 'REVOKED', - revised: 'REVISED', - latestVersion: 'LATEST_VERSION', -} as const; - -export type SiloVersionStatus = (typeof siloVersionStatuses)[keyof typeof siloVersionStatuses]; -export function isSiloVersionStatus(status: string | undefined | null): status is SiloVersionStatus { - if (status === undefined || status === null) { - return false; - } - return Object.values(siloVersionStatuses).includes(status as SiloVersionStatus); -} - -export type SequenceEntryHistory = SequenceEntryHistoryEntry[]; - -const sequenceEntryHistoryEntry = accessionVersion.merge( - z.object({ - versionStatus: z.string().refine((status) => isSiloVersionStatus(status), { - message: `Invalid version status`, - }) as z.ZodType, - }), -); - -type SequenceEntryHistoryEntry = z.infer; - export class LapisClient extends ZodiosWrapperClient { constructor( url: string, @@ -71,35 +46,31 @@ export class LapisClient extends ZodiosWrapperClient { const result = await this.call('details', { accession, versionStatus: siloVersionStatuses.latestVersion, - fields: ['accession', 'version'], + fields: [ACCESSION_FIELD, VERSION_FIELD], }); - if (result.isOk()) { - const data = result.value.data; + return result.andThen(({ data }) => { if (data.length !== 1) { - const problemDetail: ProblemDetail = { + return err({ type: 'about:blank', title: 'Unexpected number of results', detail: `Expected 1 result, got ${data.length}`, status: 500, instance: 'LapisClient/getLatestAccessionVersion', - }; - return err(problemDetail); + }); } const parsedAccessionversion = accessionVersion.safeParse(data[0]); if (!parsedAccessionversion.success) { - const problemDetail: ProblemDetail = { + return err({ type: 'about:blank', title: 'Could not parse accession version', detail: `Expected 1 result, got ${data.length}`, status: 500, instance: 'LapisClient/getLatestAccessionVersion', - }; - return err(problemDetail); + }); } return ok(parsedAccessionversion.data); - } - return result; + }); } public async getAllSequenceEntryHistoryForAccession( @@ -107,8 +78,8 @@ export class LapisClient extends ZodiosWrapperClient { ): Promise> { const result = await this.call('details', { accession, - fields: ['accession', 'version', 'versionStatus'], - orderBy: ['version'], + fields: [ACCESSION_FIELD, VERSION_FIELD, VERSION_STATUS_FIELD], + orderBy: [VERSION_FIELD], }); const createSequenceHistoryProblemDetail = (detail: string): ProblemDetail => ({ @@ -119,22 +90,16 @@ export class LapisClient extends ZodiosWrapperClient { detail, }); - return result.match( - (data) => - Result.combine( - data.data.map((entry) => { - const parsedHistory = sequenceEntryHistoryEntry.safeParse(entry); - return parsedHistory.success - ? ok(parsedHistory.data) - : err( - createSequenceHistoryProblemDetail( - `Validation error for ${accession}: ${parsedHistory.error.errors}`, - ), - ); - }), - ), - (error) => err(error), - ); + return result.andThen(({ data }) => { + const parseResult = sequenceEntryHistory.safeParse(data); + return parseResult.success + ? ok(parseResult.data) + : err( + createSequenceHistoryProblemDetail( + `Validation error for ${accession}: ${parseResult.error.toString()}`, + ), + ); + }); } public getSequenceMutations(accessionVersion: string, type: BaseType) { diff --git a/website/src/services/zodiosWrapperClient.ts b/website/src/services/zodiosWrapperClient.ts index 3578f683c..3a8b958a4 100644 --- a/website/src/services/zodiosWrapperClient.ts +++ b/website/src/services/zodiosWrapperClient.ts @@ -2,7 +2,7 @@ import { Zodios, type ZodiosEndpointDefinitions, type ZodiosInstance } from '@zo import type { Narrow } from '@zodios/core/lib/utils.types'; import type { Aliases, ZodiosAliases } from '@zodios/core/lib/zodios.types'; import type { AxiosError, AxiosResponse } from 'axios'; -import { type Err, err, ok } from 'neverthrow'; +import { type Err, err, ok, type Result } from 'neverthrow'; import { type InstanceLogger } from '../logger.ts'; import { problemDetail, type ProblemDetail } from '../types/backend.ts'; @@ -29,7 +29,10 @@ export class ZodiosWrapperClient { this.zodios = new Zodios(url, api); } - public call>(method: Method, ...args: ZodiosMethod['parameters']) { + public call>( + method: Method, + ...args: ZodiosMethod['parameters'] + ): Promise['response']>, ProblemDetail>> { const zodiosMethod = this.zodios[method] as ZodiosAliases[Method]; const zodiosResponse = zodiosMethod(...(args as TypeThatCanBeUsedAsArgs)) as ZodiosMethod< Api, diff --git a/website/src/settings.ts b/website/src/settings.ts index 0ee2e0976..de9ef3c40 100644 --- a/website/src/settings.ts +++ b/website/src/settings.ts @@ -1,6 +1,13 @@ +import { siloVersionStatuses } from './types/lapis.ts'; + export const pageSize = 100; +export const ACCESSION_FIELD = 'accession'; +export const VERSION_FIELD = 'version'; +export const VERSION_STATUS_FIELD = 'versionStatus'; +export const IS_REVOCATION_FIELD = 'isRevocation'; + export const hiddenDefaultSearchFilters = [ - { name: 'versionStatus', filterValue: 'LATEST_VERSION', type: 'string' as const }, - { name: 'isRevocation', filterValue: 'false', type: 'string' as const }, + { name: VERSION_STATUS_FIELD, filterValue: siloVersionStatuses.latestVersion, type: 'string' as const }, + { name: IS_REVOCATION_FIELD, filterValue: 'false', type: 'string' as const }, ]; diff --git a/website/src/types/lapis.ts b/website/src/types/lapis.ts index 60b34c681..71162311a 100644 --- a/website/src/types/lapis.ts +++ b/website/src/types/lapis.ts @@ -1,6 +1,6 @@ import z, { type ZodTypeAny } from 'zod'; -import type { ProblemDetail } from './backend.ts'; +import { accessionVersion, type ProblemDetail } from './backend.ts'; export const lapisBaseRequest = z .object({ @@ -50,3 +50,27 @@ function makeLapisResponse(data: T) { export type LapisError = { error: ProblemDetail; }; + +export const siloVersionStatuses = { + revoked: 'REVOKED', + revised: 'REVISED', + latestVersion: 'LATEST_VERSION', +} as const; + +export const siloVersionStatusSchema = z.enum([ + siloVersionStatuses.revoked, + siloVersionStatuses.revised, + siloVersionStatuses.latestVersion, +]); + +export type SiloVersionStatus = z.infer; + +export const sequenceEntryHistory = z.array( + accessionVersion.merge( + z.object({ + versionStatus: siloVersionStatusSchema, + }), + ), +); + +export type SequenceEntryHistory = z.infer; diff --git a/website/src/utils/getVersionStatusColor.ts b/website/src/utils/getVersionStatusColor.ts index a38f8477f..05dc33b2a 100644 --- a/website/src/utils/getVersionStatusColor.ts +++ b/website/src/utils/getVersionStatusColor.ts @@ -1,8 +1,8 @@ -import { type SiloVersionStatus } from '../services/lapisClient.ts'; +import { type SiloVersionStatus, siloVersionStatuses } from '../types/lapis.ts'; export const getVersionStatusColor = (versionStatus: SiloVersionStatus) => { switch (versionStatus) { - case 'LATEST_VERSION': + case siloVersionStatuses.latestVersion: return 'text-green-500'; default: return 'text-red-500'; diff --git a/website/tests/playwrightSetup.ts b/website/tests/playwrightSetup.ts index 4c120acff..07bd99b86 100644 --- a/website/tests/playwrightSetup.ts +++ b/website/tests/playwrightSetup.ts @@ -3,7 +3,9 @@ import sortBy from 'lodash/sortBy.js'; import { e2eLogger, getToken, lapisUrl, testUser, testUserPassword } from './e2e.fixture.ts'; import { prepareDataToBe } from './util/prepareDataToBe.ts'; -import { LapisClient, siloVersionStatuses } from '../src/services/lapisClient.ts'; +import { LapisClient } from '../src/services/lapisClient.ts'; +import { ACCESSION_FIELD, IS_REVOCATION_FIELD, VERSION_FIELD, VERSION_STATUS_FIELD } from '../src/settings.ts'; +import { siloVersionStatuses } from '../src/types/lapis.ts'; enum LapisStateBeforeTests { NoSequencesInLapis = 'NoSequencesInLapis', @@ -73,7 +75,7 @@ async function checkLapisState(lapisClient: LapisClient): Promise