Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions app/(reference-data)/biochem/compounds/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -467,9 +467,6 @@ export default function CompoundDetailPage() {
<DetailRow label="Synonyms">
<SynonymsDisplay synonyms={synonyms} />
</DetailRow>
{cpd.ontology && cpd.ontology !== 'class:null|context:null' && (
<DetailRow label="Ontology"><Typography variant="body2">{cpd.ontology}</Typography></DetailRow>
)}
<DetailRow label="Source"><Typography variant="body2">{cpd.source ?? 'N/A'}</Typography></DetailRow>
</Box>
</Box>
Expand Down
7 changes: 0 additions & 7 deletions app/(reference-data)/biochem/reactions/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -426,13 +426,6 @@ export default function ReactionDetailPage() {
<PathwaysDisplay pathways={pathways} />
</DetailRow>

{rxn.ontology && rxn.ontology !== 'class:null|context:null|step:null' && (
<DetailRow label="Ontology">
<Typography variant="body2" sx={{ wordBreak: 'break-word' }}>
{rxn.ontology}
</Typography>
</DetailRow>
)}

</CardContent>
</Card>
Expand Down
29 changes: 12 additions & 17 deletions lib/api/biochem.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import {
CPD_IMG_BASE,
MODELSEED_API_URL,
SOLR_COMPOUNDS_COLLECTION,
solrCorpusEndpoint,
} from './config';
import { hasNestedSchema, parentDocTypeFilter } from './solrSchema';
Expand Down Expand Up @@ -114,7 +115,6 @@
ec_numbers: string[];
is_obsolete: string;
is_transport: boolean;
ontology: string;
pathways: string[];
notes: string[];
abbreviation?: string;
Expand Down Expand Up @@ -146,7 +146,6 @@
deltagerr: number;
abbreviation: string;
aliases: string[];
ontology: string;
inchikey?: string;
smiles?: string;
is_cofactor?: boolean;
Expand Down Expand Up @@ -461,7 +460,13 @@
if (nestedStoichiometryQuickSearch) {
const wildcard = usePrefixOnly ? `${token}*` : `*${token}*`;
fieldClauses.push(
`({!parent which="${parentDocTypeFilter('reactions')}" v="doc_type:stoichiometry AND (compound:${wildcard} OR participant_name:${wildcard} OR participant_aliases:${wildcard} OR aliases:${wildcard})"})`,
// Participant aliases are NOT denormalised onto the stoichiometry children.
// They are read from the compounds core at query time with a cross-core
// join, so editing a compound's aliases takes effect without reposting
// every reaction that uses it. fromIndex must name the compounds core for
// THIS environment -- compounds_staging from reactions_staging -- or the
// clause silently resolves against the wrong dataset instead of erroring.
`({!parent which="${parentDocTypeFilter('reactions')}" v="doc_type:stoichiometry AND (compound:${wildcard} OR participant_name:${wildcard} OR {!join from=id to=compound fromIndex=${SOLR_COMPOUNDS_COLLECTION}}aliases:${wildcard})"})`,
);
}
if (nestedThermoEvidenceQuickSearch) {
Expand Down Expand Up @@ -519,11 +524,7 @@
url += `&fq=${encodeURIComponent(fq)}`;
}

// Filter out ontology field for compounds (Solr compounds_staging has no ontology field)
const filterItems = (filterModel?.items ?? []).filter(item => {
const field = toSolrField(String(item.field ?? ''));
return !(collection === 'compounds' && field === 'ontology');
});
const filterItems = filterModel?.items ?? [];

const filterClauses = filterItems
.map((item) => buildFilterClause(item))
Expand Down Expand Up @@ -793,14 +794,12 @@
function matchesFilterItem(
doc: Record<string, unknown>,
item: GridFilterItem,
endpoint?: string,

Check warning on line 797 in lib/api/biochem.ts

View workflow job for this annotation

GitHub Actions / Verify (lint, typecheck, test, build, audit)

'endpoint' is defined but never used
): boolean {
const operator = String(item.operator ?? '');
const field = toSolrField(String(item.field ?? ''));
if (!field || !operator) return true;

/* Solr compounds_staging has no ontology query field — REST payloads typically omit it too. */
if (endpoint === 'compounds' && field === 'ontology') return true;

const rawField = doc[field];
const fieldValue = normalizeFieldValue(rawField);
Expand Down Expand Up @@ -1157,23 +1156,19 @@
const RXN_VISIBLE = [
'name', 'id', 'definition', 'reversibility', 'thermo_evidence',
'stoichiometry', 'status', 'aliases', 'ec_numbers', 'is_obsolete',
'is_transport', 'ontology', 'pathways', 'notes',
'is_transport', 'pathways', 'notes',
];
// Solr child transformers only return fields also present in the parent `fl` list.
// Include the stored stoichiometry fields needed by Equation highlighting before
// requesting the matching child documents.
const RXN_VISIBLE_NESTED = [
...RXN_VISIBLE,
'compound', 'coefficient', 'compartment', 'is_reactant', 'participant_name',
'participant_aliases', 'aliases', 'grade', 'doc_type', '_nest_path_',
'grade', 'doc_type', '_nest_path_',
'[child childFilter="doc_type:stoichiometry OR doc_type:thermo_evidence OR doc_type:thermo-evidence" limit=200]',
];

/**
* Compound quick-search fields — must exist on Solr `compounds_staging`.
* Note: Solr does not expose an `ontology` field on compounds; querying it yields 400
* ("undefined field ontology") and breaks the whole quick-search clause.
*/
/** Compound quick-search fields — every entry must exist on the Solr core. */
const CPD_SEARCH_FIELDS = ['id', 'name', 'formula', 'synonyms', 'aliases'];

/** Compound visible fields matching Solr `compounds_staging` stored fields (see fl=). */
Expand Down
18 changes: 0 additions & 18 deletions tests/unit/api/biochem-rest-filtering.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,24 +98,6 @@ describe('biochem REST path local filter/sort/pagination', () => {
expect(String(fetchMock.mock.calls[0]?.[0] ?? '')).toContain('limit=5000');
});

it('ignores ontology column filters on compound REST payloads (Solr has no ontology field)', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
json: async () => [{ id: 'cpd00001', name: 'ATP', formula: '', aliases: [] }],
});
vi.stubGlobal('fetch', fetchMock);

const biochem = await import('@/lib/api/biochem');
const res = await biochem.getCompoundsFromModelseedApi({
limit: 10,
filterModel: {
items: [{ field: 'ontology', operator: 'contains', value: 'nope-miss' }],
quickFilterValues: [],
},
});
expect(res.docs).toHaveLength(1);
});

it('refines quick search across compound fields locally (OR across searchFields)', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
Expand Down
11 changes: 9 additions & 2 deletions tests/unit/api/biochem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ describe('getCompounds Solr query shape', () => {
vi.restoreAllMocks();
});

it('quick search must not reference ontology (undefined field on compounds_staging)', async () => {
it('quick search only references fields that exist on the compounds core', async () => {
const biochemApi = await loadBiochemApi();
const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(() =>
Promise.resolve(
Expand All @@ -143,7 +143,14 @@ describe('getCompounds Solr query shape', () => {
const qRaw = u.searchParams.get('q');
expect(qRaw).toBeTruthy();
const q = decodeURIComponent(qRaw ?? '');
expect(q).not.toMatch(/\bontology\b/i);
// Every field: token in q must be one the core actually defines. Solr answers an
// undefined field in q with a hard 400, and a 400 carries no CORS header, so the
// browser reports a CORS failure and points at the wrong layer. Three fields have
// reached production this way.
const COMPOUND_CORE_FIELDS = ['id', 'name', 'formula', 'synonyms', 'aliases'];
const queried = [...q.matchAll(/(\w+):/g)].map((mm) => mm[1]);
const unknown = [...new Set(queried)].filter((f) => !COMPOUND_CORE_FIELDS.includes(f));
expect(unknown).toEqual([]);
expect(q).toContain('cpd05323');
expect(q).toMatch(/formula|aliases|name|id/);
});
Expand Down
11 changes: 5 additions & 6 deletions tests/unit/api/biochemStoichiometry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,9 +124,9 @@ describe('Solr stoichiometry support', () => {
expect(nestedUrl.searchParams.get('fl')).toBe([
'name', 'id', 'definition', 'reversibility', 'thermo_evidence',
'stoichiometry', 'status', 'aliases', 'ec_numbers', 'is_obsolete',
'is_transport', 'ontology', 'pathways', 'notes',
'is_transport', 'pathways', 'notes',
'compound', 'coefficient', 'compartment', 'is_reactant', 'participant_name',
'participant_aliases', 'aliases', 'grade', 'doc_type', '_nest_path_',
'grade', 'doc_type', '_nest_path_',
'[child childFilter="doc_type:stoichiometry OR doc_type:thermo_evidence OR doc_type:thermo-evidence" limit=200]',
].join(','));
expect(nested.docs[0].participants).toEqual([{
Expand All @@ -150,7 +150,7 @@ describe('Solr stoichiometry support', () => {
expect(reaction.participants).toHaveLength(1);
await api.getReactions({ filterModel: { items: [], quickFilterValues: ['cpd05331'] } });
const legacyListUrl = new URL(dataUrl(fetchMock));
expect(legacyListUrl.searchParams.get('fl')).toBe('name,id,definition,reversibility,thermo_evidence,stoichiometry,status,aliases,ec_numbers,is_obsolete,is_transport,ontology,pathways,notes');
expect(legacyListUrl.searchParams.get('fl')).toBe('name,id,definition,reversibility,thermo_evidence,stoichiometry,status,aliases,ec_numbers,is_obsolete,is_transport,pathways,notes');
expect(legacyListUrl.searchParams.get('q')).toBe('(id:*cpd05331* OR name:*cpd05331* OR definition:*cpd05331* OR reversibility:*cpd05331* OR status:*cpd05331* OR ec_numbers:*cpd05331* OR aliases:*cpd05331* OR pathways:*cpd05331* OR stoichiometry:*cpd05331* OR notes:*cpd05331*)');
expect(legacyListUrl.searchParams.get('sort')).toBe('id asc');
await api.getReactions({ filterModel: { items: [], quickFilterValues: ['Glucoraphanin'] } });
Expand All @@ -176,8 +176,8 @@ describe('Solr stoichiometry support', () => {
expect(nestedQuery).toContain('name:*Glucoraphanin*');
expect(nestedQuery).toContain('definition:*Glucoraphanin*');
expect(nestedQuery).not.toContain('stoichiometry:*');
expect(nestedQuery).toContain('({!parent which="doc_type:reaction" v="doc_type:stoichiometry AND (compound:*Glucoraphanin* OR participant_name:*Glucoraphanin* OR participant_aliases:*Glucoraphanin* OR aliases:*Glucoraphanin*)"})');
expect(nestedQuery).toContain('({!parent which="doc_type:reaction" v="doc_type:stoichiometry AND (compound:*cpd05331* OR participant_name:*cpd05331* OR participant_aliases:*cpd05331* OR aliases:*cpd05331*)"})');
expect(nestedQuery).toContain('({!parent which="doc_type:reaction" v="doc_type:stoichiometry AND (compound:*Glucoraphanin* OR participant_name:*Glucoraphanin* OR {!join from=id to=compound fromIndex=compounds_staging}aliases:*Glucoraphanin*)"})');
expect(nestedQuery).toContain('({!parent which="doc_type:reaction" v="doc_type:stoichiometry AND (compound:*cpd05331* OR participant_name:*cpd05331* OR {!join from=id to=compound fromIndex=compounds_staging}aliases:*cpd05331*)"})');
expect(nestedQuery).toContain(') AND (');
await api.findReactionsForCompound('cpd00002');
expect(new URL(dataUrl(fetchMock)).searchParams.get('q')).toContain('{!parent which="doc_type:reaction" v="doc_type:stoichiometry AND compound:cpd00002"}');
Expand Down Expand Up @@ -237,7 +237,6 @@ describe('reaction request regression matrix', () => {
await api.getReactions({ filterModel: { items: [], quickFilterValues: ['RegistryAlias'] } });

const query = new URL(dataUrl(fetchMock)).searchParams.get('q') ?? '';
expect(query).toContain('participant_aliases:*RegistryAlias*');
expect(query).toContain('aliases:*RegistryAlias*');
expect(query).toContain('participant_name:*RegistryAlias*');
expect(query).toContain('compound:*RegistryAlias*');
Expand Down
Loading