= ({
{isRestricted && }
- {schema.submissionDataTypes.consensusSequences && suborganism !== null && (
+ {schema.submissionDataTypes.consensusSequences && segmentReferences !== null && (
({
@@ -35,7 +34,7 @@ const BUTTON_ROLE = 'button';
function renderSequenceViewer(
referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema,
- suborganism: string,
+ segmentReferences: Record,
) {
render(
@@ -45,7 +44,7 @@ function renderSequenceViewer(
clientConfig={testConfig.public}
referenceGenomeLightweightSchema={referenceGenomeLightweightSchema}
loadSequencesAutomatically={false}
- suborganism={suborganism}
+ segmentReferences={segmentReferences}
/>
,
);
@@ -55,19 +54,29 @@ function renderSingleReferenceSequenceViewer({
nucleotideSegmentNames,
genes,
}: {
- nucleotideSegmentNames: NucleotideSegmentNames;
+ nucleotideSegmentNames: string[];
genes: string[];
}) {
- renderSequenceViewer(
+ const segments: Record<
+ string,
{
- [SINGLE_REFERENCE]: {
- geneNames: genes,
- nucleotideSegmentNames,
- insdcAccessionFull: [],
- },
- },
- SINGLE_REFERENCE,
- );
+ references: string[];
+ insdcAccessions: Record;
+ genesByReference: Record;
+ }
+ > = {};
+ const segmentReferences: Record = {};
+
+ for (const segmentName of nucleotideSegmentNames) {
+ segments[segmentName] = {
+ references: ['ref1'],
+ insdcAccessions: {},
+ genesByReference: { ref1: genes },
+ };
+ segmentReferences[segmentName] = 'ref1';
+ }
+
+ renderSequenceViewer({ segments }, segmentReferences);
}
const multiSegmentName = 'main2';
@@ -166,23 +175,24 @@ describe('SequencesContainer', () => {
test('should render single segmented sequences', async () => {
const alignedSequence = `${suborganism1}AlignedSequence`;
const sequence = `${suborganism1}Sequence`;
- mockRequest.lapis.alignedNucleotideSequencesMultiSegment(200, `>some\n${alignedSequence}`, suborganism1);
- mockRequest.lapis.unalignedNucleotideSequencesMultiSegment(200, `>some\n${sequence}`, suborganism1);
+ // Single segment uses non-segmented endpoints even in multi-reference mode
+ mockRequest.lapis.alignedNucleotideSequences(200, `>some\n${alignedSequence}`);
+ mockRequest.lapis.unalignedNucleotideSequences(200, `>some\n${sequence}`);
renderSequenceViewer(
{
- [suborganism1]: {
- nucleotideSegmentNames: ['main'],
- geneNames: [],
- insdcAccessionFull: [],
- },
- [suborganism2]: {
- nucleotideSegmentNames: ['main'],
- geneNames: [],
- insdcAccessionFull: [],
+ segments: {
+ main: {
+ references: [suborganism1, suborganism2],
+ insdcAccessions: {},
+ genesByReference: {
+ [suborganism1]: [],
+ [suborganism2]: [],
+ },
+ },
},
},
- suborganism1,
+ { main: suborganism1 },
);
click(LOAD_SEQUENCES_BUTTON);
@@ -219,18 +229,26 @@ describe('SequencesContainer', () => {
renderSequenceViewer(
{
- [suborganism1]: {
- nucleotideSegmentNames: ['main'],
- geneNames: [],
- insdcAccessionFull: [],
- },
- [suborganism2]: {
- nucleotideSegmentNames: ['segment1', 'segment2'],
- geneNames: [],
- insdcAccessionFull: [],
+ segments: {
+ segment1: {
+ references: [suborganism1, suborganism2],
+ insdcAccessions: {},
+ genesByReference: {
+ [suborganism1]: [],
+ [suborganism2]: [],
+ },
+ },
+ segment2: {
+ references: [suborganism1, suborganism2],
+ insdcAccessions: {},
+ genesByReference: {
+ [suborganism1]: [],
+ [suborganism2]: [],
+ },
+ },
},
},
- suborganism2,
+ { segment1: suborganism2, segment2: suborganism2 },
);
click(LOAD_SEQUENCES_BUTTON);
diff --git a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx
index 1d614b417e..51bf035a4a 100644
--- a/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx
+++ b/website/src/components/SequenceDetailsPage/SequencesDisplay/SequencesContainer.tsx
@@ -1,9 +1,9 @@
import { type Dispatch, type FC, type SetStateAction, useEffect, useState } from 'react';
import { SequencesViewer } from './SequenceViewer.tsx';
-import { type ReferenceGenomesLightweightSchema, type Suborganism } from '../../../types/referencesGenomes.ts';
+import { type ReferenceGenomesLightweightSchema } from '../../../types/referencesGenomes.ts';
import type { ClientConfig } from '../../../types/runtimeConfig.ts';
-import { getSuborganismSegmentAndGeneInfo } from '../../../utils/getSuborganismSegmentAndGeneInfo.tsx';
+import { getSegmentAndGeneInfo } from '../../../utils/getSegmentAndGeneInfo.tsx';
import {
alignedSequenceSegment,
type GeneInfo,
@@ -22,7 +22,7 @@ import { withQueryProvider } from '../../common/withQueryProvider.tsx';
type SequenceContainerProps = {
organism: string;
- suborganism: Suborganism;
+ segmentReferences: Record;
accessionVersion: string;
clientConfig: ClientConfig;
referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema;
@@ -31,15 +31,15 @@ type SequenceContainerProps = {
export const InnerSequencesContainer: FC = ({
organism,
- suborganism,
+ segmentReferences,
accessionVersion,
clientConfig,
referenceGenomeLightweightSchema,
loadSequencesAutomatically,
}) => {
- const { nucleotideSegmentInfos, geneInfos, isMultiSegmented } = getSuborganismSegmentAndGeneInfo(
+ const { nucleotideSegmentInfos, geneInfos, isMultiSegmented } = getSegmentAndGeneInfo(
referenceGenomeLightweightSchema,
- suborganism,
+ segmentReferences,
);
const [loadSequences, setLoadSequences] = useState(() => loadSequencesAutomatically);
diff --git a/website/src/components/SequenceDetailsPage/getTableData.spec.ts b/website/src/components/SequenceDetailsPage/getTableData.spec.ts
index 329dfd1085..e8464881d0 100644
--- a/website/src/components/SequenceDetailsPage/getTableData.spec.ts
+++ b/website/src/components/SequenceDetailsPage/getTableData.spec.ts
@@ -8,7 +8,7 @@ import { LapisClient } from '../../services/lapisClient.ts';
import type { ProblemDetail } from '../../types/backend.ts';
import type { Schema } from '../../types/config.ts';
import type { MutationProportionCount } from '../../types/lapis.ts';
-import { type ReferenceGenomes, SINGLE_REFERENCE } from '../../types/referencesGenomes.ts';
+import type { ReferenceGenomes } from '../../types/referencesGenomes.ts';
const schema: Schema = {
organismName: 'instance name',
@@ -29,22 +29,26 @@ const schema: Schema = {
};
const singleReferenceGenomes: ReferenceGenomes = {
- [SINGLE_REFERENCE]: {
- nucleotideSequences: [],
- genes: [],
+ main: {
+ ref1: {
+ sequence: 'ATCG',
+ genes: {},
+ },
},
};
const genome1 = 'genome1';
const genome2 = 'genome2';
const multipleReferenceGenomes: ReferenceGenomes = {
- [genome1]: {
- nucleotideSequences: [],
- genes: [],
- },
- [genome2]: {
- nucleotideSequences: [],
- genes: [],
+ main: {
+ [genome1]: {
+ sequence: 'ATCG',
+ genes: {},
+ },
+ [genome2]: {
+ sequence: 'ATCG',
+ genes: {},
+ },
},
};
@@ -326,22 +330,22 @@ describe('getTableData', () => {
expect(mutationTableEntries).toStrictEqual([]);
});
- test('should return the suborganism name for a single reference genome', async () => {
+ test('should return the segmentReferences for a single reference genome', async () => {
const result = await getTableData(accessionVersion, schema, singleReferenceGenomes, lapisClient);
- const suborganism = result._unsafeUnwrap().suborganism;
+ const segmentReferences = result._unsafeUnwrap().segmentReferences;
- expect(suborganism).equals(SINGLE_REFERENCE);
+ expect(segmentReferences).toEqual({ main: 'ref1' });
});
- test('should return the suborganism name for multiple reference genomes', async () => {
+ test('should return the segmentReferences for multiple reference genomes', async () => {
mockRequest.lapis.details(200, { info, data: [{ genotype: genome2 }] });
const result = await getTableData(accessionVersion, schema, multipleReferenceGenomes, lapisClient);
- const suborganism = result._unsafeUnwrap().suborganism;
+ const segmentReferences = result._unsafeUnwrap().segmentReferences;
- expect(suborganism).equals(genome2);
+ expect(segmentReferences).toEqual({ main: genome2 });
});
test('should throw when the suborganism name is not in multiple reference genomes', async () => {
@@ -360,14 +364,14 @@ describe('getTableData', () => {
);
});
- test('should tolerate when suborganism is null (as e.g. for revocation entries)', async () => {
+ test('should tolerate when genotype is null (as e.g. for revocation entries)', async () => {
mockRequest.lapis.details(200, { info, data: [{ genotype: null }] });
const result = await getTableData(accessionVersion, schema, multipleReferenceGenomes, lapisClient);
- const suborganism = result._unsafeUnwrap().suborganism;
+ const segmentReferences = result._unsafeUnwrap().segmentReferences;
- expect(suborganism).equals(null);
+ expect(segmentReferences).equals(null);
});
test('should throw when the suborganism name is not in multiple reference genomes', async () => {
@@ -377,7 +381,7 @@ describe('getTableData', () => {
expect(result).toStrictEqual(
err({
- detail: "Suborganism 'unknown suborganism' (value of field 'genotype') not found in reference genomes.",
+ detail: "ReferenceName 'unknown suborganism' (value of field 'genotype') not found in reference genomes.",
instance: '/seq/' + accessionVersion,
status: 0,
title: 'Invalid suborganism',
diff --git a/website/src/components/SequenceDetailsPage/getTableData.ts b/website/src/components/SequenceDetailsPage/getTableData.ts
index 87cee3ac43..2c0debc31a 100644
--- a/website/src/components/SequenceDetailsPage/getTableData.ts
+++ b/website/src/components/SequenceDetailsPage/getTableData.ts
@@ -12,12 +12,12 @@ import {
type InsertionCount,
type MutationProportionCount,
} from '../../types/lapis.ts';
-import { type ReferenceGenomes, SINGLE_REFERENCE, type Suborganism } from '../../types/referencesGenomes.ts';
+import { type ReferenceGenomes } from '../../types/referencesGenomes.ts';
import { parseUnixTimestamp } from '../../utils/parseUnixTimestamp.ts';
export type GetTableDataResult = {
data: TableDataEntry[];
- suborganism: Suborganism | null;
+ segmentReferences: Record | null;
isRevocation: boolean;
};
@@ -54,31 +54,53 @@ export async function getTableData(
}),
)
.andThen((data) => {
- const suborganismResult = getSuborganism(data.details, schema, referenceGenomes, accessionVersion);
- if (suborganismResult.isErr()) {
- return err(suborganismResult.error);
+ const segmentReferencesResult = getSegmentReferences(
+ data.details,
+ schema,
+ referenceGenomes,
+ accessionVersion,
+ );
+ if (segmentReferencesResult.isErr()) {
+ return err(segmentReferencesResult.error);
}
- const suborganism = suborganismResult.value;
+ const segmentReferences = segmentReferencesResult.value;
return ok({
- data: toTableData(schema, suborganism, data),
- suborganism,
+ data: toTableData(schema, segmentReferences, data),
+ segmentReferences,
isRevocation: isRevocationEntry(data.details),
});
}),
);
}
-function getSuborganism(
+function getSegmentReferences(
details: Details,
schema: Schema,
referenceGenomes: ReferenceGenomes,
accessionVersion: string,
-): Result {
- if (SINGLE_REFERENCE in referenceGenomes) {
- return ok(SINGLE_REFERENCE);
+): Result | null, ProblemDetail> {
+ const segments = Object.keys(referenceGenomes);
+
+ // Check if single reference mode (only one reference per segment)
+ const firstSegment = segments[0];
+ const firstSegmentRefs = firstSegment ? Object.keys(referenceGenomes[firstSegment] ?? {}) : [];
+ const isSingleReference = firstSegmentRefs.length === 1;
+
+ if (isSingleReference) {
+ // Build segment references from the single reference
+ const segmentReferences: Record = {};
+ for (const segmentName of segments) {
+ const refs = Object.keys(referenceGenomes[segmentName] ?? {});
+ if (refs.length > 0) {
+ segmentReferences[segmentName] = refs[0];
+ }
+ }
+ return ok(segmentReferences);
}
+
+ // Multiple references mode - get from metadata field
const suborganismField = schema.suborganismIdentifierField;
if (suborganismField === undefined) {
return err({
@@ -89,6 +111,7 @@ function getSuborganism(
instance: '/seq/' + accessionVersion,
});
}
+
const value = details[suborganismField];
const suborganismResult = z.string().nullable().safeParse(value);
if (!suborganismResult.success) {
@@ -100,17 +123,38 @@ function getSuborganism(
instance: '/seq/' + accessionVersion,
});
}
- const suborganism = suborganismResult.data;
- if (suborganism !== null && !(suborganism in referenceGenomes)) {
+
+ const referenceName = suborganismResult.data;
+ if (referenceName === null) {
+ return ok(null);
+ }
+
+ // Validate that the reference exists in at least one segment
+ let foundInAnySegment = false;
+ for (const segmentName of segments) {
+ if (referenceName in (referenceGenomes[segmentName] ?? {})) {
+ foundInAnySegment = true;
+ break;
+ }
+ }
+
+ if (!foundInAnySegment) {
return err({
type: 'about:blank',
title: 'Invalid suborganism',
status: 0,
- detail: `Suborganism '${suborganism}' (value of field '${suborganismField}') not found in reference genomes.`,
+ detail: `ReferenceName '${referenceName}' (value of field '${suborganismField}') not found in reference genomes.`,
instance: '/seq/' + accessionVersion,
});
}
- return ok(suborganism);
+
+ // Build segment references - all segments use the same reference
+ const segmentReferences: Record = {};
+ for (const segmentName of segments) {
+ segmentReferences[segmentName] = referenceName;
+ }
+
+ return ok(segmentReferences);
}
function isRevocationEntry(details: Details): boolean {
@@ -140,7 +184,7 @@ function mutationDetails(
aminoAcidMutations: MutationProportionCount[],
nucleotideInsertions: InsertionCount[],
aminoAcidInsertions: InsertionCount[],
- suborganism: Suborganism | null,
+ segmentReferences: Record | null,
): TableDataEntry[] {
const data: TableDataEntry[] = [
{
@@ -150,21 +194,21 @@ function mutationDetails(
header: 'Nucleotide mutations',
customDisplay: {
type: 'badge',
- value: substitutionsMap(nucleotideMutations, suborganism),
+ value: substitutionsMap(nucleotideMutations, segmentReferences),
},
type: { kind: 'mutation' },
},
{
label: 'Deletions',
name: 'nucleotideDeletions',
- value: deletionsToCommaSeparatedString(nucleotideMutations, suborganism),
+ value: deletionsToCommaSeparatedString(nucleotideMutations, segmentReferences),
header: 'Nucleotide mutations',
type: { kind: 'mutation' },
},
{
label: 'Insertions',
name: 'nucleotideInsertions',
- value: insertionsToCommaSeparatedString(nucleotideInsertions, suborganism),
+ value: insertionsToCommaSeparatedString(nucleotideInsertions, segmentReferences),
header: 'Nucleotide mutations',
type: { kind: 'mutation' },
},
@@ -175,21 +219,21 @@ function mutationDetails(
header: 'Amino acid mutations',
customDisplay: {
type: 'badge',
- value: substitutionsMap(aminoAcidMutations, suborganism),
+ value: substitutionsMap(aminoAcidMutations, segmentReferences),
},
type: { kind: 'mutation' },
},
{
label: 'Deletions',
name: 'aminoAcidDeletions',
- value: deletionsToCommaSeparatedString(aminoAcidMutations, suborganism),
+ value: deletionsToCommaSeparatedString(aminoAcidMutations, segmentReferences),
header: 'Amino acid mutations',
type: { kind: 'mutation' },
},
{
label: 'Insertions',
name: 'aminoAcidInsertions',
- value: insertionsToCommaSeparatedString(aminoAcidInsertions, suborganism),
+ value: insertionsToCommaSeparatedString(aminoAcidInsertions, segmentReferences),
header: 'Amino acid mutations',
type: { kind: 'mutation' },
},
@@ -199,7 +243,7 @@ function mutationDetails(
function toTableData(
config: Schema,
- suborganism: Suborganism | null,
+ segmentReferences: Record | null,
{
details,
nucleotideMutations,
@@ -233,7 +277,7 @@ function toTableData(
aminoAcidMutations,
nucleotideInsertions,
aminoAcidInsertions,
- suborganism,
+ segmentReferences,
);
data.push(...mutations);
}
@@ -255,7 +299,7 @@ function mapValueToDisplayedValue(value: undefined | null | string | number | bo
export function substitutionsMap(
mutationData: MutationProportionCount[],
- suborganism: Suborganism | null,
+ segmentReferences: Record | null,
): SegmentedMutations[] {
const result: SegmentedMutations[] = [];
const substitutionData = mutationData.filter((m) => m.mutationTo !== '-');
@@ -263,7 +307,7 @@ export function substitutionsMap(
const segmentMutationsMap = new Map();
for (const entry of substitutionData) {
const { sequenceName, mutationFrom, position, mutationTo } = entry;
- const sequenceDisplayName = computeSequenceDisplayName(sequenceName, suborganism);
+ const sequenceDisplayName = computeSequenceDisplayName(sequenceName, segmentReferences);
const sequenceKey = sequenceDisplayName ?? '';
if (!segmentMutationsMap.has(sequenceKey)) {
@@ -282,29 +326,38 @@ export function substitutionsMap(
function computeSequenceDisplayName(
originalSequenceName: string | null,
- suborganism: Suborganism | null,
+ segmentReferences: Record | null,
): string | null {
- if (originalSequenceName === null || suborganism === SINGLE_REFERENCE || suborganism === null) {
+ if (originalSequenceName === null || segmentReferences === null) {
return originalSequenceName;
}
- if (originalSequenceName === suborganism) {
- // there is only one segment in which case the name should be null
- return null;
+ // Try to strip any reference prefix from the sequence name
+ for (const referenceName of Object.values(segmentReferences)) {
+ // Check if the sequence name is just the reference (single segment case)
+ if (originalSequenceName === referenceName) {
+ return null;
+ }
+
+ // Try to strip the reference prefix
+ const prefixToTrim = `${referenceName}-`;
+ if (originalSequenceName.startsWith(prefixToTrim)) {
+ return originalSequenceName.substring(prefixToTrim.length);
+ }
}
- const prefixToTrim = `${suborganism}-`;
- return originalSequenceName.startsWith(prefixToTrim)
- ? originalSequenceName.substring(prefixToTrim.length)
- : originalSequenceName;
+ return originalSequenceName;
}
-function deletionsToCommaSeparatedString(mutationData: MutationProportionCount[], suborganism: Suborganism | null) {
+function deletionsToCommaSeparatedString(
+ mutationData: MutationProportionCount[],
+ segmentReferences: Record | null,
+) {
const segmentPositions = new Map();
mutationData
.filter((m) => m.mutationTo === '-')
.forEach((m) => {
- const segment = computeSequenceDisplayName(m.sequenceName, suborganism);
+ const segment = computeSequenceDisplayName(m.sequenceName, segmentReferences);
const position = m.position;
if (!segmentPositions.has(segment)) {
segmentPositions.set(segment, []);
@@ -348,10 +401,13 @@ function deletionsToCommaSeparatedString(mutationData: MutationProportionCount[]
.join(', ');
}
-function insertionsToCommaSeparatedString(insertionData: InsertionCount[], suborganism: Suborganism | null) {
+function insertionsToCommaSeparatedString(
+ insertionData: InsertionCount[],
+ segmentReferences: Record | null,
+) {
return insertionData
.map((insertion) => {
- const sequenceDisplayName = computeSequenceDisplayName(insertion.sequenceName, suborganism);
+ const sequenceDisplayName = computeSequenceDisplayName(insertion.sequenceName, segmentReferences);
const sequenceNamePart = sequenceDisplayName !== null ? sequenceDisplayName + ':' : '';
return `ins_${sequenceNamePart}${insertion.position}:${insertion.insertedSymbols}`;
diff --git a/website/src/config.spec.ts b/website/src/config.spec.ts
index 79ca340c00..0045260fda 100644
--- a/website/src/config.spec.ts
+++ b/website/src/config.spec.ts
@@ -16,7 +16,7 @@ const defaultConfig: WebsiteConfig = {
};
describe('validateWebsiteConfig', () => {
- it('should fail when "onlyForSuborganism" is not a valid organism', () => {
+ it('should fail when "onlyForReferenceName" is not a valid organism', () => {
const errors = validateWebsiteConfig({
...defaultConfig,
organisms: {
@@ -27,7 +27,7 @@ describe('validateWebsiteConfig', () => {
{
type: 'string',
name: 'test field',
- onlyForSuborganism: 'nonExistentSuborganism',
+ onlyForReferenceName: 'nonExistentReferenceName',
},
],
inputFields: [],
@@ -44,7 +44,7 @@ describe('validateWebsiteConfig', () => {
expect(errors).toHaveLength(1);
expect(errors[0].message).contains(
- `Metadata field 'test field' in organism 'dummyOrganism' references unknown suborganism 'nonExistentSuborganism' in 'onlyForSuborganism'.`,
+ `Metadata field 'test field' in organism 'dummyOrganism' references unknown suborganism 'nonExistentReferenceName' in 'onlyForReferenceName'.`,
);
});
diff --git a/website/src/config.ts b/website/src/config.ts
index 60a9fa718b..63597a0161 100644
--- a/website/src/config.ts
+++ b/website/src/config.ts
@@ -13,9 +13,8 @@ import {
websiteConfig,
} from './types/config.ts';
import {
- type NamedSequence,
type ReferenceAccession,
- type ReferenceGenomes,
+ type SegmentFirstReferenceGenomes,
type ReferenceGenomesLightweightSchema,
} from './types/referencesGenomes.ts';
import { runtimeConfig, type RuntimeConfig, type ServiceUrls } from './types/runtimeConfig.ts';
@@ -47,14 +46,14 @@ export function validateWebsiteConfig(config: WebsiteConfig): Error[] {
});
}
- const knownSuborganisms = Object.keys(schema.referenceGenomes);
+ const knownReferenceNames = Object.keys(schema.referenceGenomes);
schema.schema.metadata.forEach((metadatum) => {
- const onlyForSuborganism = metadatum.onlyForSuborganism;
- if (onlyForSuborganism !== undefined && !knownSuborganisms.includes(onlyForSuborganism)) {
+ const onlyForReferenceName = metadatum.onlyForReferenceName;
+ if (onlyForReferenceName !== undefined && !knownReferenceNames.includes(onlyForReferenceName)) {
errors.push(
new Error(
- `Metadata field '${metadatum.name}' in organism '${organism}' references unknown suborganism '${onlyForSuborganism}' in 'onlyForSuborganism'.`,
+ `Metadata field '${metadatum.name}' in organism '${organism}' references unknown suborganism '${onlyForReferenceName}' in 'onlyForReferenceName'.`,
),
);
}
@@ -278,29 +277,48 @@ export function getLapisUrl(serviceConfig: ServiceUrls, organism: string): strin
return serviceConfig.lapisUrls[organism];
}
-export function getReferenceGenomes(organism: string): ReferenceGenomes {
+export function getReferenceGenomes(organism: string): SegmentFirstReferenceGenomes {
return getConfig(organism).referenceGenomes;
}
-const getAccession = (n: NamedSequence): ReferenceAccession => {
- return {
- name: n.name,
- insdcAccessionFull: n.insdcAccessionFull,
- };
-};
-
export const getReferenceGenomeLightweightSchema = (organism: string): ReferenceGenomesLightweightSchema => {
const referenceGenomes = getReferenceGenomes(organism);
- return Object.fromEntries(
- Object.entries(referenceGenomes).map(([suborganism, referenceGenome]) => [
- suborganism,
- {
- nucleotideSegmentNames: referenceGenome.nucleotideSequences.map((n) => n.name),
- geneNames: referenceGenome.genes.map((n) => n.name),
- insdcAccessionFull: referenceGenome.nucleotideSequences.map((n) => getAccession(n)),
- },
- ]),
- );
+ const segments: Record<
+ string,
+ {
+ references: string[];
+ insdcAccessions: Record;
+ genesByReference: Record;
+ }
+ > = {};
+
+ // Transform segment-first structure to lightweight schema
+ for (const [segmentName, referenceMap] of Object.entries(referenceGenomes)) {
+ segments[segmentName] = {
+ references: Object.keys(referenceMap),
+ insdcAccessions: {},
+ genesByReference: {},
+ };
+
+ for (const [referenceName, referenceData] of Object.entries(referenceMap)) {
+ // Add INSDC accession
+ if (referenceData.insdcAccessionFull) {
+ segments[segmentName].insdcAccessions[referenceName] = {
+ name: referenceName,
+ insdcAccessionFull: referenceData.insdcAccessionFull,
+ };
+ }
+
+ // Add genes for this reference
+ if (referenceData.genes) {
+ segments[segmentName].genesByReference[referenceName] = Object.keys(referenceData.genes);
+ } else {
+ segments[segmentName].genesByReference[referenceName] = [];
+ }
+ }
+ }
+
+ return { segments };
};
export function seqSetsAreEnabled() {
diff --git a/website/src/hooks/useUrlParamState.ts b/website/src/hooks/useUrlParamState.ts
index 490f7d7d10..82cc248fb5 100644
--- a/website/src/hooks/useUrlParamState.ts
+++ b/website/src/hooks/useUrlParamState.ts
@@ -3,7 +3,7 @@ import { useCallback, useMemo } from 'react';
import type { QueryState } from '../components/SearchPage/useStateSyncedWithUrlQueryParams.ts';
import type { FieldValueUpdate } from '../types/config.ts';
-type ParamType = 'string' | 'boolean' | 'nullable-string';
+type ParamType = 'string' | 'boolean' | 'nullable-string' | 'json';
/**
* A hook that syncs state with URL parameters.
@@ -40,14 +40,31 @@ function useUrlParamState(
throw Error('Expected string, found array value in state.');
}
return (urlValue ?? '') as T;
+ case 'json':
+ if (typeof urlValue === 'string') {
+ try {
+ return JSON.parse(urlValue) as T;
+ } catch {
+ return defaultValue;
+ }
+ }
+ return defaultValue;
}
}
const updateUrlParam = useCallback(
(newValue: T) => {
- setSomeFieldValues([paramName, shouldRemove(newValue) ? null : String(newValue)]);
+ let serializedValue: string | null;
+ if (shouldRemove(newValue)) {
+ serializedValue = null;
+ } else if (paramType === 'json') {
+ serializedValue = JSON.stringify(newValue);
+ } else {
+ serializedValue = String(newValue);
+ }
+ setSomeFieldValues([paramName, serializedValue]);
},
- [paramName, setSomeFieldValues, shouldRemove],
+ [paramName, setSomeFieldValues, shouldRemove, paramType],
);
return [valueState, updateUrlParam];
diff --git a/website/src/pages/seq/[accessionVersion].fa/index.ts b/website/src/pages/seq/[accessionVersion].fa/index.ts
index 00b98863ac..b42c762b28 100644
--- a/website/src/pages/seq/[accessionVersion].fa/index.ts
+++ b/website/src/pages/seq/[accessionVersion].fa/index.ts
@@ -4,7 +4,6 @@ import { getReferenceGenomeLightweightSchema } from '../../../config.ts';
import { routes } from '../../../routes/routes.ts';
import { LapisClient } from '../../../services/lapisClient.ts';
import { ACCESSION_VERSION_FIELD } from '../../../settings.ts';
-import { SINGLE_REFERENCE } from '../../../types/referencesGenomes.ts';
import { createDownloadAPIRoute } from '../../../utils/createDownloadAPIRoute.ts';
export const GET: APIRoute = createDownloadAPIRoute(
@@ -16,10 +15,14 @@ export const GET: APIRoute = createDownloadAPIRoute(
const referenceGenomeLightweightSchema = getReferenceGenomeLightweightSchema(organism);
- if (SINGLE_REFERENCE in referenceGenomeLightweightSchema) {
- const { nucleotideSegmentNames } = referenceGenomeLightweightSchema[SINGLE_REFERENCE];
- if (nucleotideSegmentNames.length > 1) {
- return lapisClient.getMultiSegmentSequenceFasta(accessionVersion, nucleotideSegmentNames);
+ // Check if single reference mode (all segments have only one reference)
+ const segments = Object.entries(referenceGenomeLightweightSchema.segments);
+ const isSingleReference = segments.every(([_, segmentData]) => segmentData.references.length === 1);
+
+ if (isSingleReference) {
+ const segmentNames = Object.keys(referenceGenomeLightweightSchema.segments);
+ if (segmentNames.length > 1) {
+ return lapisClient.getMultiSegmentSequenceFasta(accessionVersion, segmentNames);
}
return lapisClient.getSequenceFasta(accessionVersion);
diff --git a/website/src/pages/seq/[accessionVersion]/details.json.ts b/website/src/pages/seq/[accessionVersion]/details.json.ts
index 4279086014..9b1b34fbeb 100644
--- a/website/src/pages/seq/[accessionVersion]/details.json.ts
+++ b/website/src/pages/seq/[accessionVersion]/details.json.ts
@@ -41,7 +41,7 @@ export const GET: APIRoute = async (req) => {
dataUseTermsHistory: result.dataUseTermsHistory,
schema,
clientConfig,
- suborganism: result.suborganism,
+ segmentReferences: result.segmentReferences,
isRevocation: result.isRevocation,
sequenceEntryHistory: result.sequenceEntryHistory,
};
diff --git a/website/src/pages/seq/[accessionVersion]/getSequenceDetailsTableData.ts b/website/src/pages/seq/[accessionVersion]/getSequenceDetailsTableData.ts
index 78c2eefcbd..462eb5d70e 100644
--- a/website/src/pages/seq/[accessionVersion]/getSequenceDetailsTableData.ts
+++ b/website/src/pages/seq/[accessionVersion]/getSequenceDetailsTableData.ts
@@ -8,7 +8,6 @@ import { createBackendClient } from '../../../services/backendClientFactory.ts';
import { LapisClient } from '../../../services/lapisClient.ts';
import type { DataUseTermsHistoryEntry, ProblemDetail } from '../../../types/backend.ts';
import type { SequenceEntryHistory } from '../../../types/lapis.ts';
-import type { Suborganism } from '../../../types/referencesGenomes.ts';
import { parseAccessionVersionFromString } from '../../../utils/extractAccessionVersion.ts';
export enum SequenceDetailsTableResultType {
@@ -21,7 +20,7 @@ export type TableData = {
tableData: TableDataEntry[];
sequenceEntryHistory: SequenceEntryHistory;
dataUseTermsHistory: DataUseTermsHistoryEntry[];
- suborganism: Suborganism | null;
+ segmentReferences: Record | null;
isRevocation: boolean;
};
@@ -64,7 +63,7 @@ export const getSequenceDetailsTableData = async (
tableData: tableData.data,
sequenceEntryHistory,
dataUseTermsHistory,
- suborganism: tableData.suborganism,
+ segmentReferences: tableData.segmentReferences,
isRevocation: tableData.isRevocation,
}),
);
diff --git a/website/src/pages/seq/[accessionVersion]/index.astro b/website/src/pages/seq/[accessionVersion]/index.astro
index 64c51c0aa9..2c3dd077f9 100644
--- a/website/src/pages/seq/[accessionVersion]/index.astro
+++ b/website/src/pages/seq/[accessionVersion]/index.astro
@@ -74,13 +74,13 @@ const sequenceFlaggingConfig = getWebsiteConfig().sequenceFlagging;
tableData={result.tableData}
dataUseTermsHistory={result.dataUseTermsHistory}
referenceGenomeLightweightSchema={getReferenceGenomeLightweightSchema(organism)}
- suborganism={result.suborganism}
+ segmentReferences={result.segmentReferences}
/>
) : (
;
export const instanceConfig = z.object({
schema,
- referenceGenomes,
+ referenceGenomes: segmentFirstReferenceGenomes,
});
export type InstanceConfig = z.infer;
diff --git a/website/src/types/detailsJson.ts b/website/src/types/detailsJson.ts
index ed4fdca86a..aa935de0f8 100644
--- a/website/src/types/detailsJson.ts
+++ b/website/src/types/detailsJson.ts
@@ -3,7 +3,6 @@ import { z } from 'zod';
import { dataUseTermsHistoryEntry } from './backend.ts';
import { schema } from './config.ts';
import { parsedSequenceEntryHistoryEntrySchema } from './lapis.ts';
-import { suborganism } from './referencesGenomes.ts';
import { serviceUrls } from './runtimeConfig.ts';
import { tableDataEntrySchema } from '../components/SequenceDetailsPage/types.ts';
@@ -14,7 +13,8 @@ export const detailsJsonSchema = z.object({
dataUseTermsHistory: z.array(dataUseTermsHistoryEntry),
schema: schema,
clientConfig: serviceUrls,
- suborganism: suborganism.nullable(),
+ // Segment-first mode: map of segment names to reference names
+ segmentReferences: z.record(z.string(), z.string()).nullable(),
isRevocation: z.boolean(),
sequenceEntryHistory: z.array(parsedSequenceEntryHistoryEntrySchema),
});
diff --git a/website/src/types/referencesGenomes.ts b/website/src/types/referencesGenomes.ts
index ba64d446d8..bfb5732927 100644
--- a/website/src/types/referencesGenomes.ts
+++ b/website/src/types/referencesGenomes.ts
@@ -5,35 +5,48 @@ export type ReferenceAccession = {
insdcAccessionFull?: string;
};
-const namedSequence = z.object({
- name: z.string(),
- sequence: z.string(),
- insdcAccessionFull: z.optional(z.string()),
-});
-export type NamedSequence = z.infer;
+// Segment-first structure types
+export type SegmentName = string;
+export type ReferenceName = string;
+export type GeneName = string;
-export const referenceGenome = z.object({
- nucleotideSequences: z.array(namedSequence),
- genes: z.array(namedSequence),
-});
-export type ReferenceGenome = z.infer;
-
-export const suborganism = z.string();
-export type Suborganism = z.infer;
-
-export const referenceGenomes = z
- .record(suborganism, referenceGenome)
- .refine((value) => Object.entries(value).length > 0, 'The reference genomes must not be empty.');
-export type ReferenceGenomes = z.infer;
-
-export type NucleotideSegmentNames = string[];
-
-export type SuborganismReferenceGenomesLightweightSchema = {
- nucleotideSegmentNames: NucleotideSegmentNames;
- geneNames: string[];
- insdcAccessionFull: ReferenceAccession[];
+export type GeneSequenceData = {
+ sequence: string;
};
-export type ReferenceGenomesLightweightSchema = Record;
+export type ReferenceSequenceData = {
+ sequence: string;
+ insdcAccessionFull?: string;
+ genes?: Record;
+};
-export const SINGLE_REFERENCE = 'singleReference';
+// Segment-first reference genomes structure (from values.yaml)
+// Structure: referenceGenomes[segmentName][referenceName] = { sequence, insdcAccessionFull?, genes? }
+export const segmentFirstReferenceGenomes = z.record(
+ z.string(), // segment name
+ z.record(
+ z.string(), // reference name
+ z.object({
+ sequence: z.string(),
+ insdcAccessionFull: z.string().optional(),
+ genes: z.record(z.string(), z.object({ sequence: z.string() })).optional(),
+ }),
+ ),
+);
+export type SegmentFirstReferenceGenomes = z.infer;
+
+// Type alias for the new segment-first structure
+export type ReferenceGenomes = SegmentFirstReferenceGenomes;
+
+// Lightweight schema for segment-first mode
+export type ReferenceGenomesLightweightSchema = {
+ segments: Record<
+ SegmentName,
+ {
+ references: ReferenceName[];
+ insdcAccessions: Record;
+ // Genes available for each reference in this segment
+ genesByReference: Record;
+ }
+ >;
+};
diff --git a/website/src/utils/getSegmentAndGeneInfo.spec.tsx b/website/src/utils/getSegmentAndGeneInfo.spec.tsx
new file mode 100644
index 0000000000..9b9d7c4b5b
--- /dev/null
+++ b/website/src/utils/getSegmentAndGeneInfo.spec.tsx
@@ -0,0 +1,185 @@
+/* eslint-disable @typescript-eslint/naming-convention */
+import { describe, expect, test } from 'vitest';
+
+import { getSegmentAndGeneInfo } from './getSegmentAndGeneInfo.tsx';
+import type { ReferenceGenomesLightweightSchema } from '../types/referencesGenomes.ts';
+
+describe('getSegmentAndGeneInfo', () => {
+ describe('with single reference per segment', () => {
+ test('should return correct names for multi-segmented organism', () => {
+ const schema: ReferenceGenomesLightweightSchema = {
+ segments: {
+ segment1: {
+ references: ['ref1'],
+ insdcAccessions: {},
+ genesByReference: {
+ ref1: ['gene1'],
+ },
+ },
+ segment2: {
+ references: ['ref1'],
+ insdcAccessions: {},
+ genesByReference: {
+ ref1: ['gene2'],
+ },
+ },
+ },
+ };
+
+ const selectedReferences = {
+ segment1: 'ref1',
+ segment2: 'ref1',
+ };
+
+ const result = getSegmentAndGeneInfo(schema, selectedReferences);
+
+ expect(result).toEqual({
+ nucleotideSegmentInfos: [
+ { lapisName: 'segment1', label: 'segment1' },
+ { lapisName: 'segment2', label: 'segment2' },
+ ],
+ geneInfos: [
+ { lapisName: 'gene1', label: 'gene1' },
+ { lapisName: 'gene2', label: 'gene2' },
+ ],
+ isMultiSegmented: true,
+ });
+ });
+
+ test('should return correct names for single-segmented organism', () => {
+ const schema: ReferenceGenomesLightweightSchema = {
+ segments: {
+ main: {
+ references: ['ref1'],
+ insdcAccessions: {},
+ genesByReference: {
+ ref1: ['gene1'],
+ },
+ },
+ },
+ };
+
+ const selectedReferences = {
+ main: 'ref1',
+ };
+
+ const result = getSegmentAndGeneInfo(schema, selectedReferences);
+
+ expect(result).toEqual({
+ nucleotideSegmentInfos: [{ lapisName: 'main', label: 'main' }],
+ geneInfos: [{ lapisName: 'gene1', label: 'gene1' }],
+ isMultiSegmented: false,
+ });
+ });
+ });
+
+ describe('with multiple references (mixed)', () => {
+ test('should handle different references for different segments', () => {
+ const schema: ReferenceGenomesLightweightSchema = {
+ segments: {
+ segment1: {
+ references: ['CV-A16', 'CV-A10'],
+ insdcAccessions: {},
+ genesByReference: {
+ 'CV-A16': ['gene1'],
+ 'CV-A10': ['gene1'],
+ },
+ },
+ segment2: {
+ references: ['CV-A16', 'CV-A10'],
+ insdcAccessions: {},
+ genesByReference: {
+ 'CV-A16': ['gene2'],
+ 'CV-A10': ['gene2'],
+ },
+ },
+ },
+ };
+
+ const selectedReferences = {
+ segment1: 'CV-A16',
+ segment2: 'CV-A10',
+ };
+
+ const result = getSegmentAndGeneInfo(schema, selectedReferences);
+
+ expect(result).toEqual({
+ nucleotideSegmentInfos: [
+ { lapisName: 'CV-A16-segment1', label: 'segment1' },
+ { lapisName: 'CV-A10-segment2', label: 'segment2' },
+ ],
+ geneInfos: [
+ { lapisName: 'CV-A16-gene1', label: 'gene1' },
+ { lapisName: 'CV-A10-gene2', label: 'gene2' },
+ ],
+ isMultiSegmented: true,
+ });
+ });
+
+ test('should handle segments without selected references', () => {
+ const schema: ReferenceGenomesLightweightSchema = {
+ segments: {
+ segment1: {
+ references: ['CV-A16', 'CV-A10'],
+ insdcAccessions: {},
+ genesByReference: {
+ 'CV-A16': ['gene1'],
+ 'CV-A10': ['gene1'],
+ },
+ },
+ segment2: {
+ references: ['CV-A16', 'CV-A10'],
+ insdcAccessions: {},
+ genesByReference: {
+ 'CV-A16': ['gene2'],
+ 'CV-A10': ['gene2'],
+ },
+ },
+ },
+ };
+
+ const selectedReferences = {
+ segment1: 'CV-A16',
+ // segment2 not selected
+ };
+
+ const result = getSegmentAndGeneInfo(schema, selectedReferences);
+
+ expect(result).toEqual({
+ nucleotideSegmentInfos: [
+ { lapisName: 'CV-A16-segment1', label: 'segment1' },
+ { lapisName: 'segment2', label: 'segment2' }, // No reference selected
+ ],
+ geneInfos: [
+ { lapisName: 'CV-A16-gene1', label: 'gene1' },
+ // gene2 not included since segment2 has no reference
+ ],
+ isMultiSegmented: true,
+ });
+ });
+
+ test('should handle empty selectedReferences', () => {
+ const schema: ReferenceGenomesLightweightSchema = {
+ segments: {
+ main: {
+ references: ['ref1'],
+ insdcAccessions: {},
+ genesByReference: {
+ ref1: ['gene1'],
+ },
+ },
+ },
+ };
+
+ const selectedReferences = {};
+
+ const result = getSegmentAndGeneInfo(schema, selectedReferences);
+
+ expect(result).toEqual({
+ nucleotideSegmentInfos: [{ lapisName: 'main', label: 'main' }],
+ geneInfos: [],
+ isMultiSegmented: false,
+ });
+ });
+ });
+});
diff --git a/website/src/utils/getSegmentAndGeneInfo.tsx b/website/src/utils/getSegmentAndGeneInfo.tsx
new file mode 100644
index 0000000000..8fb548b2ec
--- /dev/null
+++ b/website/src/utils/getSegmentAndGeneInfo.tsx
@@ -0,0 +1,57 @@
+import {
+ type GeneInfo,
+ type SegmentInfo,
+ getSegmentInfoWithReference,
+ getGeneInfoWithReference,
+ type SegmentReferenceSelections,
+} from './sequenceTypeHelpers.ts';
+import { type ReferenceGenomesLightweightSchema } from '../types/referencesGenomes.ts';
+
+export type SegmentAndGeneInfo = {
+ nucleotideSegmentInfos: SegmentInfo[];
+ geneInfos: GeneInfo[];
+ isMultiSegmented: boolean;
+};
+
+/**
+ * Get segment and gene info where each segment can have its own reference.
+ * @param schema - The reference genome lightweight schema
+ * @param selectedReferences - Map of segment names to selected references
+ * @returns SegmentAndGeneInfo with all segments and their genes
+ */
+export function getSegmentAndGeneInfo(
+ schema: ReferenceGenomesLightweightSchema,
+ selectedReferences: SegmentReferenceSelections,
+): SegmentAndGeneInfo {
+ const nucleotideSegmentInfos: SegmentInfo[] = [];
+ const geneInfos: GeneInfo[] = [];
+
+ // Check if this is single-reference mode (all segments have only one reference)
+ const segments = Object.values(schema.segments);
+ const isSingleReference = segments.every((segmentData) => segmentData.references.length === 1);
+
+ // Process each segment
+ for (const [segmentName, segmentData] of Object.entries(schema.segments)) {
+ const selectedRef = selectedReferences[segmentName] ?? null;
+
+ // In single-reference mode, don't prefix segment names
+ const refForNaming = isSingleReference ? null : selectedRef;
+
+ // Add nucleotide sequence info for this segment
+ nucleotideSegmentInfos.push(getSegmentInfoWithReference(segmentName, refForNaming));
+
+ // Add gene info if reference is selected
+ if (selectedRef) {
+ const geneNames = segmentData.genesByReference[selectedRef];
+ for (const geneName of geneNames) {
+ geneInfos.push(getGeneInfoWithReference(geneName, refForNaming));
+ }
+ }
+ }
+
+ return {
+ nucleotideSegmentInfos,
+ geneInfos,
+ isMultiSegmented: Object.keys(schema.segments).length > 1,
+ };
+}
diff --git a/website/src/utils/getSuborganismSegmentAndGeneInfo.spec.tsx b/website/src/utils/getSuborganismSegmentAndGeneInfo.spec.tsx
deleted file mode 100644
index 905274c76e..0000000000
--- a/website/src/utils/getSuborganismSegmentAndGeneInfo.spec.tsx
+++ /dev/null
@@ -1,144 +0,0 @@
-import { describe, expect, test } from 'vitest';
-
-import { getSuborganismSegmentAndGeneInfo } from './getSuborganismSegmentAndGeneInfo.tsx';
-import { SINGLE_REFERENCE } from '../types/referencesGenomes.ts';
-
-describe('getSuborganismSegmentAndGeneInfo', () => {
- describe('with single reference', () => {
- test('should return correct names for multi-segmented organism', () => {
- const referenceGenomeSequenceNames = {
- [SINGLE_REFERENCE]: {
- nucleotideSegmentNames: ['segment1', 'segment2'],
- geneNames: ['gene1', 'gene2'],
- insdcAccessionFull: [],
- },
- };
-
- const result = getSuborganismSegmentAndGeneInfo(referenceGenomeSequenceNames, SINGLE_REFERENCE);
-
- expect(result).to.deep.equal({
- nucleotideSegmentInfos: [
- { lapisName: 'segment1', label: 'segment1' },
- { lapisName: 'segment2', label: 'segment2' },
- ],
- geneInfos: [
- { lapisName: 'gene1', label: 'gene1' },
- { lapisName: 'gene2', label: 'gene2' },
- ],
- isMultiSegmented: true,
- });
- });
-
- test('should return correct names for single-segmented organism', () => {
- const referenceGenomeSequenceNames = {
- [SINGLE_REFERENCE]: {
- nucleotideSegmentNames: ['main'],
- geneNames: ['gene1'],
- insdcAccessionFull: [],
- },
- };
-
- const result = getSuborganismSegmentAndGeneInfo(referenceGenomeSequenceNames, SINGLE_REFERENCE);
-
- expect(result).to.deep.equal({
- nucleotideSegmentInfos: [{ lapisName: 'main', label: 'main' }],
- geneInfos: [{ lapisName: 'gene1', label: 'gene1' }],
- isMultiSegmented: false,
- });
- });
- });
-
- describe('with multiple references', () => {
- const suborganism = 'sub1';
-
- test('should return correct names for multi-segmented suborganism', () => {
- const referenceGenomeSequenceNames = {
- [suborganism]: {
- nucleotideSegmentNames: ['segment1', 'segment2'],
- geneNames: ['gene1', 'gene2'],
- insdcAccessionFull: [],
- },
- anotherSuborganism: {
- nucleotideSegmentNames: ['segmentA', 'segmentB'],
- geneNames: ['geneA'],
- insdcAccessionFull: [],
- },
- };
-
- const result = getSuborganismSegmentAndGeneInfo(referenceGenomeSequenceNames, suborganism);
-
- expect(result).to.deep.equal({
- nucleotideSegmentInfos: [
- { lapisName: 'sub1-segment1', label: 'segment1' },
- { lapisName: 'sub1-segment2', label: 'segment2' },
- ],
- geneInfos: [
- { lapisName: 'sub1-gene1', label: 'gene1' },
- { lapisName: 'sub1-gene2', label: 'gene2' },
- ],
- isMultiSegmented: true,
- });
- });
-
- test('should return correct names for single-segmented suborganism', () => {
- const referenceGenomeSequenceNames = {
- [suborganism]: {
- nucleotideSegmentNames: ['main'],
- geneNames: ['gene1'],
- insdcAccessionFull: [],
- },
- anotherSuborganism: {
- nucleotideSegmentNames: ['segmentA', 'segmentB'],
- geneNames: ['geneA', 'geneB'],
- insdcAccessionFull: [],
- },
- };
-
- const result = getSuborganismSegmentAndGeneInfo(referenceGenomeSequenceNames, suborganism);
-
- expect(result).to.deep.equal({
- nucleotideSegmentInfos: [{ lapisName: 'sub1', label: 'main' }],
- geneInfos: [{ lapisName: 'sub1-gene1', label: 'gene1' }],
- isMultiSegmented: true,
- });
- });
-
- test('should return null when no suborganism is selected', () => {
- const referenceGenomeSequenceNames = {
- [suborganism]: {
- nucleotideSegmentNames: ['main'],
- geneNames: ['gene1'],
- insdcAccessionFull: [],
- },
- anotherSuborganism: {
- nucleotideSegmentNames: ['segmentA', 'segmentB'],
- geneNames: ['geneA', 'geneB'],
- insdcAccessionFull: [],
- },
- };
-
- const result = getSuborganismSegmentAndGeneInfo(referenceGenomeSequenceNames, null);
-
- expect(result).toBeNull();
- });
-
- test('should return null when unknown suborganism is selected', () => {
- const referenceGenomeSequenceNames = {
- [suborganism]: {
- nucleotideSegmentNames: ['main'],
- geneNames: ['gene1'],
- insdcAccessionFull: [],
- },
- anotherSuborganism: {
- nucleotideSegmentNames: ['segmentA', 'segmentB'],
- geneNames: ['geneA', 'geneB'],
- insdcAccessionFull: [],
- },
- };
-
- const result = getSuborganismSegmentAndGeneInfo(referenceGenomeSequenceNames, 'unknownSuborganism');
-
- expect(result).toBeNull();
- });
- });
-});
diff --git a/website/src/utils/getSuborganismSegmentAndGeneInfo.tsx b/website/src/utils/getSuborganismSegmentAndGeneInfo.tsx
deleted file mode 100644
index 4a2dcfd9dc..0000000000
--- a/website/src/utils/getSuborganismSegmentAndGeneInfo.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import {
- type GeneInfo,
- getMultiPathogenNucleotideSequenceNames,
- getMultiPathogenSequenceName,
- getSinglePathogenSequenceName,
- isMultiSegmented,
- type SegmentInfo,
-} from './sequenceTypeHelpers.ts';
-import { type ReferenceGenomesLightweightSchema, SINGLE_REFERENCE } from '../types/referencesGenomes.ts';
-
-export type SuborganismSegmentAndGeneInfo = {
- nucleotideSegmentInfos: SegmentInfo[];
- geneInfos: GeneInfo[];
- isMultiSegmented: boolean;
-};
-
-/**
- * If we know that the suborganism is not null, then the result will also be non-null.
- */
-export function getSuborganismSegmentAndGeneInfo(
- referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema,
- suborganism: string,
-): SuborganismSegmentAndGeneInfo;
-
-export function getSuborganismSegmentAndGeneInfo(
- referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema,
- suborganism: string | null,
-): SuborganismSegmentAndGeneInfo | null;
-
-export function getSuborganismSegmentAndGeneInfo(
- referenceGenomeLightweightSchema: ReferenceGenomesLightweightSchema,
- suborganism: string | null,
-): SuborganismSegmentAndGeneInfo | null {
- if (SINGLE_REFERENCE in referenceGenomeLightweightSchema) {
- const { nucleotideSegmentNames, geneNames } = referenceGenomeLightweightSchema[SINGLE_REFERENCE];
- return {
- nucleotideSegmentInfos: nucleotideSegmentNames.map(getSinglePathogenSequenceName),
- geneInfos: geneNames.map(getSinglePathogenSequenceName),
- isMultiSegmented: isMultiSegmented(nucleotideSegmentNames),
- };
- }
-
- if (suborganism === null || !(suborganism in referenceGenomeLightweightSchema)) {
- return null;
- }
-
- const { nucleotideSegmentNames, geneNames } = referenceGenomeLightweightSchema[suborganism];
-
- return {
- nucleotideSegmentInfos: getMultiPathogenNucleotideSequenceNames(nucleotideSegmentNames, suborganism),
- geneInfos: geneNames.map((name) => getMultiPathogenSequenceName(name, suborganism)),
- isMultiSegmented: true, // LAPIS treats the suborganisms as multiple nucleotide segments -> always true
- };
-}
diff --git a/website/src/utils/mutation.spec.ts b/website/src/utils/mutation.spec.ts
index d5ebaa9104..9d09fe6c6c 100644
--- a/website/src/utils/mutation.spec.ts
+++ b/website/src/utils/mutation.spec.ts
@@ -1,6 +1,6 @@
import { describe, expect, it } from 'vitest';
-import type { SuborganismSegmentAndGeneInfo } from './getSuborganismSegmentAndGeneInfo.tsx';
+import type { SegmentAndGeneInfo } from './getSegmentAndGeneInfo.tsx';
import {
intoMutationSearchParams,
type MutationQuery,
@@ -12,7 +12,7 @@ import {
describe('mutation', () => {
describe('single segment', () => {
- const mockSuborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo = {
+ const mockSegmentAndGeneInfo: SegmentAndGeneInfo = {
nucleotideSegmentInfos: [
{
lapisName: 'lapisName-main',
@@ -51,7 +51,7 @@ describe('mutation', () => {
],
...aminoAcidInsertionCases,
])('parses the valid mutation string "%s"', (input, expected) => {
- const result = parseMutationString(input, mockSuborganismSegmentAndGeneInfo);
+ const result = parseMutationString(input, mockSegmentAndGeneInfo);
expect(result).toEqual(expected);
});
@@ -74,13 +74,13 @@ describe('mutation', () => {
'INS_label-GENE1:23:TTT:',
'INS_label-GENE1:23:TTT:INVALID',
])('returns undefined for invalid mutation string %s', (input) => {
- const result = parseMutationString(input, mockSuborganismSegmentAndGeneInfo);
+ const result = parseMutationString(input, mockSegmentAndGeneInfo);
expect(result).toBeUndefined();
});
});
describe('single segmented case with multiple suborganism', () => {
- const mockSuborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo = {
+ const mockSegmentAndGeneInfo: SegmentAndGeneInfo = {
nucleotideSegmentInfos: [
{
lapisName: 'lapisName-main',
@@ -153,21 +153,21 @@ describe('mutation', () => {
],
...aminoAcidInsertionCases,
])('parses the valid mutation string "%s"', (input, expected) => {
- const result = parseMutationString(input, mockSuborganismSegmentAndGeneInfo);
+ const result = parseMutationString(input, mockSegmentAndGeneInfo);
expect(result).toEqual(expected);
});
it.each(['lapisName-main:A123T', 'label-main:A123T'])(
'returns undefined for invalid mutation string %s',
(input) => {
- const result = parseMutationString(input, mockSuborganismSegmentAndGeneInfo);
+ const result = parseMutationString(input, mockSegmentAndGeneInfo);
expect(result).toBeUndefined();
},
);
});
describe('multi-segment', () => {
- const mockSuborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo = {
+ const mockSegmentAndGeneInfo: SegmentAndGeneInfo = {
nucleotideSegmentInfos: [
{
lapisName: 'lapisName-SEQ1',
@@ -244,7 +244,7 @@ describe('mutation', () => {
],
...aminoAcidInsertionCases,
])('parses the valid mutation string "%s"', (input, expected) => {
- const result = parseMutationString(input, mockSuborganismSegmentAndGeneInfo);
+ const result = parseMutationString(input, mockSegmentAndGeneInfo);
expect(result).toEqual(expected);
});
@@ -262,12 +262,12 @@ describe('mutation', () => {
'ins_23:A:T',
'INS_4:G:T',
])('returns undefined for invalid mutation string %s', (input) => {
- const result = parseMutationString(input, mockSuborganismSegmentAndGeneInfo);
+ const result = parseMutationString(input, mockSegmentAndGeneInfo);
expect(result).toBeUndefined();
});
it('parses a comma-separated mutation string', () => {
- const result = parseMutationsString('label-GENE1:A23T, label-SEQ1:123C', mockSuborganismSegmentAndGeneInfo);
+ const result = parseMutationsString('label-GENE1:A23T, label-SEQ1:123C', mockSegmentAndGeneInfo);
expect(result).toEqual([
{
baseType: 'aminoAcid',
@@ -305,7 +305,7 @@ describe('mutation', () => {
it('removes specified mutation queries', () => {
const result = removeMutationQueries(
'label-GENE1:A23T, label-SEQ1:123C',
- mockSuborganismSegmentAndGeneInfo,
+ mockSegmentAndGeneInfo,
'aminoAcid',
'substitutionOrDeletion',
);
@@ -315,7 +315,7 @@ describe('mutation', () => {
it('converts mutations to search params', () => {
const params = intoMutationSearchParams(
'label-GENE1:A23T, label-SEQ1:123C, INS_label-SEQ1:100:G',
- mockSuborganismSegmentAndGeneInfo,
+ mockSegmentAndGeneInfo,
);
expect(params).toEqual({
nucleotideMutations: ['lapisName-SEQ1:123C'],
diff --git a/website/src/utils/mutation.ts b/website/src/utils/mutation.ts
index 61c369fdcb..a7720b34a4 100644
--- a/website/src/utils/mutation.ts
+++ b/website/src/utils/mutation.ts
@@ -1,4 +1,4 @@
-import type { SuborganismSegmentAndGeneInfo } from './getSuborganismSegmentAndGeneInfo.tsx';
+import type { SegmentAndGeneInfo } from './getSegmentAndGeneInfo.tsx';
import { type BaseType, isMultiSegmented, type SegmentInfo } from './sequenceTypeHelpers';
export type MutationType = 'substitutionOrDeletion' | 'insertion';
@@ -26,7 +26,7 @@ export type MutationSearchParams = {
export const removeMutationQueries = (
mutations: string,
- suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo,
+ suborganismSegmentAndGeneInfo: SegmentAndGeneInfo,
baseType: BaseType,
mutationType: MutationType,
): string => {
@@ -39,7 +39,7 @@ export const removeMutationQueries = (
export const parseMutationsString = (
value: string,
- suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo,
+ suborganismSegmentAndGeneInfo: SegmentAndGeneInfo,
): MutationQuery[] => {
return value
.split(',')
@@ -53,7 +53,7 @@ export const parseMutationsString = (
*/
export const parseMutationString = (
mutation: string,
- suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo,
+ suborganismSegmentAndGeneInfo: SegmentAndGeneInfo,
): MutationQuery | undefined => {
const tests = [
{ baseType: 'nucleotide', mutationType: 'substitutionOrDeletion', test: isValidNucleotideMutationQuery },
@@ -76,7 +76,7 @@ export const serializeMutationQueries = (selectedOptions: MutationQuery[]): stri
export const intoMutationSearchParams = (
mutation: string | undefined,
- suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo,
+ suborganismSegmentAndGeneInfo: SegmentAndGeneInfo,
): MutationSearchParams => {
const mutationFilter = parseMutationsString(mutation ?? '', suborganismSegmentAndGeneInfo);
@@ -102,7 +102,7 @@ const INVALID: MutationTestResult = { valid: false };
const isValidAminoAcidInsertionQuery = (
text: string,
- suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo,
+ suborganismSegmentAndGeneInfo: SegmentAndGeneInfo,
): MutationTestResult => {
try {
const textUpper = text.toUpperCase();
@@ -136,7 +136,7 @@ const isValidAminoAcidInsertionQuery = (
const isValidAminoAcidMutationQuery = (
text: string,
- suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo,
+ suborganismSegmentAndGeneInfo: SegmentAndGeneInfo,
): MutationTestResult => {
try {
const textUpper = text.toUpperCase();
@@ -169,7 +169,7 @@ const isValidAminoAcidMutationQuery = (
const isValidNucleotideInsertionQuery = (
text: string,
- suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo,
+ suborganismSegmentAndGeneInfo: SegmentAndGeneInfo,
): MutationTestResult => {
try {
const multiSegmented = isMultiSegmented(suborganismSegmentAndGeneInfo.nucleotideSegmentInfos);
@@ -210,7 +210,7 @@ const isValidNucleotideInsertionQuery = (
const isValidNucleotideMutationQuery = (
text: string,
- suborganismSegmentAndGeneInfo: SuborganismSegmentAndGeneInfo,
+ suborganismSegmentAndGeneInfo: SegmentAndGeneInfo,
): MutationTestResult => {
try {
const multiSegmented = isMultiSegmented(suborganismSegmentAndGeneInfo.nucleotideSegmentInfos);
diff --git a/website/src/utils/search.spec.ts b/website/src/utils/search.spec.ts
index c33ee43452..86e2109dc4 100644
--- a/website/src/utils/search.spec.ts
+++ b/website/src/utils/search.spec.ts
@@ -36,7 +36,7 @@ describe('MetadataVisibility', () => {
expect(visibility.isVisible('suborganism1')).toBe(false);
});
- it('should return true when isChecked is true and onlyForSuborganism is undefined', () => {
+ it('should return true when isChecked is true and onlyForReferenceName is undefined', () => {
const visibility = new MetadataVisibility(true, undefined);
expect(visibility.isVisible(null)).toBe(true);
diff --git a/website/src/utils/search.ts b/website/src/utils/search.ts
index abf3910bfc..a9a887535e 100644
--- a/website/src/utils/search.ts
+++ b/website/src/utils/search.ts
@@ -33,23 +33,23 @@ type VisiblitySelectableAccessor = (field: MetadataFilter) => boolean;
export class MetadataVisibility {
public readonly isChecked: boolean;
- private readonly onlyForSuborganism: string | undefined;
+ private readonly onlyForReferenceName: string | undefined;
- constructor(isChecked: boolean, onlyForSuborganism: string | undefined) {
+ constructor(isChecked: boolean, onlyForReferenceName: string | undefined) {
this.isChecked = isChecked;
- this.onlyForSuborganism = onlyForSuborganism;
+ this.onlyForReferenceName = onlyForReferenceName;
}
- public isVisible(selectedSuborganism: string | null) {
+ public isVisible(selectedReferenceName: string | null) {
if (!this.isChecked) {
return false;
}
- if (this.onlyForSuborganism === undefined || selectedSuborganism === null) {
+ if (this.onlyForReferenceName === undefined || selectedReferenceName === null) {
return true;
}
- return this.onlyForSuborganism === selectedSuborganism;
+ return this.onlyForReferenceName === selectedReferenceName;
}
}
@@ -80,7 +80,7 @@ const getFieldOrColumnVisibilitiesFromQuery = (
const visibility = new MetadataVisibility(
explicitVisibilitiesInUrlByFieldName.get(fieldName) ?? initiallyVisibleAccessor(field),
- field.onlyForSuborganism,
+ field.onlyForReferenceName,
);
visibilities.set(fieldName, visibility);
diff --git a/website/src/utils/sequenceTypeHelpers.ts b/website/src/utils/sequenceTypeHelpers.ts
index 36510c597d..7a3616fa81 100644
--- a/website/src/utils/sequenceTypeHelpers.ts
+++ b/website/src/utils/sequenceTypeHelpers.ts
@@ -62,3 +62,48 @@ export const isUnalignedSequence = (type: SequenceType): boolean => type.type ==
export const isAlignedSequence = (type: SequenceType): boolean => type.type === 'nucleotide' && type.aligned;
export const isGeneSequence = (segmentOrGeneInfo: SegmentInfo | GeneInfo, type: SequenceType): boolean =>
type.type === 'aminoAcid' && type.name.lapisName === segmentOrGeneInfo.lapisName;
+
+// NEW: Segment-first mode helpers
+export type SegmentReferenceSelections = Record;
+
+/**
+ * Get segment info for segment-first mode where each segment can have its own reference.
+ * @param segmentName - The segment name (e.g., "main", "VP4")
+ * @param referenceName - The selected reference for this segment (e.g., "CV-A16"), or null
+ * @returns SegmentInfo with appropriate LAPIS naming
+ */
+export function getSegmentInfoWithReference(segmentName: string, referenceName: string | null): SegmentInfo {
+ if (referenceName === null) {
+ // No reference selected - use segment name as-is
+ return {
+ lapisName: segmentName,
+ label: segmentName,
+ };
+ }
+ // Reference selected - prefix with reference name for LAPIS
+ return {
+ lapisName: `${referenceName}-${segmentName}`,
+ label: segmentName,
+ };
+}
+
+/**
+ * Get gene info for segment-first mode.
+ * @param geneName - The gene name (e.g., "VP4")
+ * @param referenceName - The reference name (e.g., "CV-A16")
+ * @returns GeneInfo with appropriate LAPIS naming
+ */
+export function getGeneInfoWithReference(geneName: string, referenceName: string | null): GeneInfo {
+ if (referenceName === null) {
+ // No reference selected - use gene name as-is
+ return {
+ lapisName: geneName,
+ label: geneName,
+ };
+ }
+ // Reference selected - prefix with reference name for LAPIS
+ return {
+ lapisName: `${referenceName}-${geneName}`,
+ label: geneName,
+ };
+}
diff --git a/website/src/utils/serversideSearch.ts b/website/src/utils/serversideSearch.ts
index 08f5352b4b..e2f51a049d 100644
--- a/website/src/utils/serversideSearch.ts
+++ b/website/src/utils/serversideSearch.ts
@@ -1,5 +1,5 @@
import { validateSingleValue } from './extractFieldValue';
-import { getSuborganismSegmentAndGeneInfo } from './getSuborganismSegmentAndGeneInfo.tsx';
+import { getSegmentAndGeneInfo } from './getSegmentAndGeneInfo.tsx';
import {
getColumnVisibilitiesFromQuery,
MetadataFilterSchema,
@@ -23,11 +23,19 @@ export const performLapisSearchQueries = async (
hiddenFieldValues: FieldValues,
organism: string,
): Promise => {
- const suborganism = extractSuborganism(schema, state);
+ const suborganism = extractReferenceName(schema, state);
- const suborganismSegmentAndGeneInfo = getSuborganismSegmentAndGeneInfo(
+ // Build segment references - all segments use the same reference
+ const segmentReferences: Record = {};
+ if (suborganism !== null) {
+ for (const segmentName of Object.keys(referenceGenomeLightweightSchema.segments)) {
+ segmentReferences[segmentName] = suborganism;
+ }
+ }
+
+ const suborganismSegmentAndGeneInfo = getSegmentAndGeneInfo(
referenceGenomeLightweightSchema,
- suborganism,
+ Object.keys(segmentReferences).length > 0 ? segmentReferences : {},
);
const filterSchema = new MetadataFilterSchema(schema.metadata);
@@ -78,7 +86,7 @@ export const performLapisSearchQueries = async (
};
};
-function extractSuborganism(schema: Schema, state: QueryState): string | null {
+function extractReferenceName(schema: Schema, state: QueryState): string | null {
if (schema.suborganismIdentifierField === undefined) {
return null;
}