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
12 changes: 7 additions & 5 deletions deploy/nginx/common/routes-core.conf
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,13 @@ location ~ ^/ContentService/data/participants/[^/]+/?$ {
}

# The other forms of a physical entity -- the same reference entity, modified,
# elsewhere or in another species. Java's query kept as written; nothing found
# is Java's 404. Byte-identical to Java over 25 ids covering every class that
# appears (EWAS by reference type, simple entities, drugs), bar a 404 body's
# url, as above. /entity/{id}/in-depth and the rest stay with Java.
location ~ ^/ContentService/data/entity/[^/]+/otherForms/?$ {
# elsewhere or in another species -- and what an entity or event is part of
# (/componentOf). Java's queries kept as written; nothing found is Java's 404.
# otherForms byte-identical to Java over 25 ids covering every class that
# appears (EWAS by reference type, simple entities, drugs); componentOf over
# 21, ATP's megabyte, every id form, events and every class of entity among
# them; both bar a 404 body's url, as above. /entity/{id}/in-depth is above.
location ~ ^/ContentService/data/entity/[^/]+/(otherForms|componentOf)/?$ {
include /etc/nginx/common/content-node-proxy.conf;
}

Expand Down
16 changes: 11 additions & 5 deletions projects/website-angular/src/scripts/proxy-order.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,15 @@ describe('the dev server proxy', () => {
});
}

it('sends the other forms of an entity to the content service', async () => {
await expectContentNode('/ContentService/data/entity/R-HSA-199420/otherForms');
});
for (const url of [
'/ContentService/data/entity/R-HSA-199420/otherForms',
'/ContentService/data/entity/R-HSA-199420/componentOf',
'/ContentService/data/entity/199420/componentOf/',
]) {
it(`sends ${url} to the content service`, async () => {
await expectContentNode(url);
});
}

for (const url of [
'/ContentService/data/database/version',
Expand Down Expand Up @@ -321,8 +327,8 @@ describe('the dev server proxy', () => {
'/ContentService/data/complexes/UniProt/P00533/x',
// Only the participants and their reference entities are ported.
'/ContentService/data/participants/R-HSA-5672965/participatingPhysicalEntities',
// Only the other forms and in-depth of an entity are ported.
'/ContentService/data/entity/R-HSA-199420/componentOf',
// Only these of an entity are ported: not a path below them.
'/ContentService/data/entity/R-HSA-199420/componentOf/x',
// Only the locations are ported, not their neighbours under /detail.
'/ContentService/data/detail/R-HSA-199420/getReverseGraph',
'/ContentService/data/detail/R-HSA-109581/getSubGraph',
Expand Down
3 changes: 2 additions & 1 deletion proxy.conf.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ module.exports = {
'^/ContentService/data/database/(version|name)/?(\\?|$)': contentNode,
'^/ContentService/data/pathways/top/[^/?]+/?(\\?|$)': contentNode,
// The other forms of a physical entity: the details panel and the popup.
'^/ContentService/data/entity/[^/?]+/otherForms/?(\\?|$)': contentNode,
// What an entity or event is part of: the details page's table.
'^/ContentService/data/entity/[^/?]+/(otherForms|componentOf)/?(\\?|$)': contentNode,
// Every place an object appears in the Pathway Browser: the details page's
// locations tree. Only this one of Java's /data/detail/{id}/... paths.
'^/ContentService/data/detail/[^/?]+/locationsInPWB/?(\\?|$)': contentNode,
Expand Down
2 changes: 1 addition & 1 deletion specs/006-content-service-to-node/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Each port follows the eight steps in [plan.md](plan.md#how-one-endpoint-is-porte
- [x] T074 `GET /data/schema/{className}` — #511
- [x] T074b `GET /data/schema/{className}/reference` — #512
- [x] T075 `GET /data/complex/{id}/subunits`, `GET /data/complexes/{resource}/{identifier}` — #514
- [ ] T076 `GET /data/entity/{id}/componentOf`
- [x] T076 `GET /data/entity/{id}/componentOf` — #515
- [ ] T077 `GET /data/participants/{id}/participatingPhysicalEntities`
- [ ] T078 `GET /data/pathway/{id}/containedEvents`, `GET /data/pathway/{id}/containedEvents/{attributeName}` — Java maps the whole hasEvent subgraph into Events (Neo4jTemplate.findAll over `RETURN e, COLLECT(r), COLLECT(p)`), and `{attributeName}` reads a getter by reflection: do with T070/T072's getter table
- [x] T079 `GET /data/pathways/low/entity/{id}/allForms`, `GET /data/pathways/low/diagram/entity/{id}`, `GET /data/pathways/low/diagram/entity/{id}/allForms` — #513
Expand Down
2 changes: 1 addition & 1 deletion specs/010-java-services/contracts/operation-homes.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ The decision record of FR-001 and FR-002. It covers every operation in the two s
| GET | `/data/discover/{identifier}` | node — to port | graph or search index |
| GET | `/data/diseases` | node — to port | graph or search index |
| GET | `/data/diseases/doid` | node — to port | graph or search index |
| GET | `/data/entity/{id}/componentOf` | node — to port | graph or search index |
| GET | `/data/entity/{id}/componentOf` | node — ported | graph or search index |
| GET | `/data/entity/{id}/in-depth` | node — ported | graph or search index |
| GET | `/data/entity/{id}/otherForms` | node — ported | graph or search index |
| GET | `/data/event/{id}/ancestors` | node — ported | graph or search index |
Expand Down
116 changes: 116 additions & 0 deletions tools/content-node/component-of.spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
// @vitest-environment node
//
// /data/entity/{id}/componentOf -- what an entity or event is part of: the
// complexes, sets and reactions above it, grouped by the relationship -- with
// a stand-in graph. PhysicalEntityController over graph-core's
// ComponentOfLinkageRepository, whose query is kept as written; rows come in
// the order Neo4j gives them, unsorted, as in Java. The rows here are invented.
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';

const asked = [];
let rows = [];
vi.mock('./graph.mjs', () => ({
read: async (cypher, params) => {
asked.push([cypher, params]);
return rows;
},
configured: () => true,
close: async () => {},
envSetting: () => '',
}));

const { app } = await import('./service.mjs');

let base;
let server;
beforeAll(async () => {
await new Promise((resolve) => {
server = app().listen(0, '127.0.0.1', resolve);
});
base = `http://127.0.0.1:${server.address().port}`;
});
afterAll(() => new Promise((resolve) => server.close(resolve)));

const get = (id) => fetch(`${base}/ContentService/data/entity/${id}/componentOf`);
const last = () => asked[asked.length - 1];
const oneLine = (cypher) => cypher.replace(/\s+/g, ' ').trim();

describe('/data/entity/{id}/componentOf', () => {
it("runs graph-core's query as written and writes each group as Java does", async () => {
rows = [
{
type: 'output',
schemaClasses: ['Reaction', ''],
names: ['B', 'A'],
stIds: ['R-HSA-2', ''],
species: ['Homo sapiens', ''],
},
{
type: 'hasComponent',
schemaClasses: ['Complex'],
names: ['C'],
stIds: ['R-HSA-3'],
species: ['Homo sapiens'],
},
];
asked.length = 0;
const response = await get('R-HSA-1');
expect(await response.text()).toBe(
'[{"type":"output","names":["B","A"],"stIds":["R-HSA-2",""],"schemaClasses":["Reaction",""],"species":["Homo sapiens",""]},' +
'{"type":"hasComponent","names":["C"],"stIds":["R-HSA-3"],"schemaClasses":["Complex"],"species":["Homo sapiens"]}]'
);
expect(oneLine(last()[0])).toBe(
"MATCH (n:DatabaseObject{stId: $id})<-[r:hasEvent|input|output|hasComponent|hasMember|hasCandidate|repeatedUnit|proteinMarker|RNAMarker]-(m) RETURN DISTINCT(TYPE(r)) AS type, COLLECT(COALESCE(m.schemaClass, '')) AS schemaClasses, COLLECT(COALESCE(m.displayName, '')) AS names, COLLECT(COALESCE(m.stId, '')) AS stIds, COLLECT(COALESCE(m.speciesName, '')) as species"
);
expect(last()[1]).toEqual({ id: 'R-HSA-1' });
});

it('reads the id as Spring reads a path variable: decoded, a matrix parameter cut', async () => {
rows = [
{
type: 'input',
schemaClasses: ['Reaction'],
names: ['A'],
stIds: ['R-HSA-2'],
species: ['H'],
},
];
asked.length = 0;
await get('R-HSA-69620;x=1');
expect(last()[1]).toEqual({ id: 'R-HSA-69620' });
});

it('matches a dbId as a number', async () => {
rows = [
{
type: 'input',
schemaClasses: ['Reaction'],
names: ['A'],
stIds: ['R-HSA-2'],
species: ['H'],
},
];
asked.length = 0;
await get('199420');
expect(oneLine(last()[0])).toMatch(/^MATCH \(n:DatabaseObject\{dbId: toInteger\(\$id\)\}\)<-/);
});

it('is a 404 naming the id, for one that is neither or is part of nothing', async () => {
rows = [];
for (const id of ['xyz', 'R-HSA-162582']) {
const response = await get(id);
const body = await response.json();
expect([id, response.status, body]).toEqual([
id,
404,
{
code: 404,
reason: 'NOT_FOUND',
url: expect.stringMatching(/\/componentOf$/),
messages: [`Id: ${id} has not been found in the System`],
targets: null,
},
]);
}
});
});
46 changes: 46 additions & 0 deletions tools/content-node/service.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1310,6 +1310,52 @@ export const endpoints = [
};
},
},
{
path: '/ContentService/data/entity/{id}/componentOf',
// PTEN: the reactions it comes out of and goes into, and the complexes and
// sets it is part of.
sample: 'R-HSA-199420',
differs: [/^\/ContentService\/data\/entity\/[^/]+\/componentOf: \.url: /],
/**
* What an entity or event is directly part of -- the reactions, complexes,
* sets and pathways above it -- one group per relationship, each with the
* names, stable ids, classes and species of its members, '' for what one
* lacks. The details page's "Component of" table.
*
* graph-core's ComponentOfLinkageRepository.getComponentsOf, kept as
* written: the groups and their members come in the order Neo4j gives
* them, unsorted (ComponentOf.sortByName is never called). The id is read as graph-core reads it
* (graphCoreId); none found, or part of nothing, is Java's 404.
*/
handler: async (request) => {
const raw = pathId(rawSegment(request, 4));
const missing = () => notFound(request, `Id: ${raw} has not been found in the System`);
const id = await graphCoreId(raw);
if (!id) return missing();
const rows = await read(
`MATCH (n:DatabaseObject${id.object})<-[r:hasEvent|input|output|hasComponent|hasMember|hasCandidate|repeatedUnit|proteinMarker|RNAMarker]-(m)
RETURN DISTINCT(TYPE(r)) AS type,
COLLECT(COALESCE(m.schemaClass, '')) AS schemaClasses,
COLLECT(COALESCE(m.displayName, '')) AS names,
COLLECT(COALESCE(m.stId, '')) AS stIds,
COLLECT(COALESCE(m.speciesName, '')) as species`,
id.params
);
if (rows.length === 0) return missing();
return {
status: 200,
// ComponentOf's fields in declaration order; none is ever empty.
body: rows.map(({ type, names, stIds, schemaClasses, species }) => ({
type,
names,
stIds,
schemaClasses,
species,
})),
type: 'application/json',
};
},
},
{
path: '/ContentService/data/complex/{id}/subunits',
// A set: 354 entities under it -- complexes, sets, proteins and small molecules.
Expand Down
Loading