Skip to content

Commit

Permalink
Automatically sort samples query results by importDate (#152)
Browse files Browse the repository at this point in the history
* Prevent SamplesList from being re-rendered needlessly

The previous implementation of refetchWhereVariables in SamplesPage was
causing double rendering because it was defined inline within the
SamplesList component's props. This meant that this function was
re-created on every render of SamplesPage.

Since SamplesList uses this function in a useEffect hook's dependency
array, each time SamplesPage rendered, it triggered the useEffect in
SamplesList to run again, causing an additional render.

* Sort samples query result by import date

* Implement DataLoader to prevent the samples query from calling the db 2x
  • Loading branch information
qu8n authored Sep 5, 2024
1 parent 7b10e81 commit 845d7f2
Show file tree
Hide file tree
Showing 8 changed files with 109 additions and 56 deletions.
3 changes: 0 additions & 3 deletions frontend/src/components/SamplesList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,6 @@ export default function SamplesList({
useSamplesListQuery({
variables: {
where: parentWhereVariables || {},
options: {
limit: MAX_ROWS,
},
sampleMetadataOptions: {
sort: [{ importDate: SortDirection.Desc }],
limit: 1,
Expand Down
38 changes: 20 additions & 18 deletions frontend/src/pages/samples/SamplesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,29 @@ const WES_SAMPLE_FILTERS = [
export default function SamplesPage() {
const [columnDefs, setColumnDefs] = useState(combinedSampleDetailsColumns);

const refetchWhereVariables = (parsedSearchVals: string[]) => {
const cohortSampleFilters = cohortSampleFilterWhereVariables(
parsedSearchVals
).filter((filter) => filter.hasTempoTempos_SOME);
const sampleMetadataFilters = {
hasMetadataSampleMetadata_SOME: {
OR: sampleFilterWhereVariables(parsedSearchVals),
},
};
return {
OR: cohortSampleFilters.concat(sampleMetadataFilters),
...(_.isEqual(columnDefs, ReadOnlyCohortSampleDetailsColumns) && {
hasMetadataSampleMetadata_SOME: {
OR: sampleFilterWhereVariables(WES_SAMPLE_FILTERS),
},
}),
};
};

return (
<SamplesList
columnDefs={columnDefs}
refetchWhereVariables={(parsedSearchVals) => {
const cohortSampleFilters = cohortSampleFilterWhereVariables(
parsedSearchVals
).filter((filter) => filter.hasTempoTempos_SOME);
const sampleMetadataFilters = {
hasMetadataSampleMetadata_SOME: {
OR: sampleFilterWhereVariables(parsedSearchVals),
},
};
return {
OR: cohortSampleFilters.concat(sampleMetadataFilters),
...(_.isEqual(columnDefs, ReadOnlyCohortSampleDetailsColumns) && {
hasMetadataSampleMetadata_SOME: {
OR: sampleFilterWhereVariables(WES_SAMPLE_FILTERS),
},
}),
};
}}
refetchWhereVariables={refetchWhereVariables}
customToolbarUI={
<>
<InfoToolTip>
Expand Down
1 change: 1 addition & 0 deletions graphql-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"apollo-server": "^3.10.0",
"apollo-server-core": "^3.10.2",
"apollo-server-express": "^3.10.2",
"dataloader": "^2.2.2",
"express": "^4.18.2",
"express-session": "^1.17.3",
"graphql": "^16.5.0",
Expand Down
49 changes: 26 additions & 23 deletions graphql-server/src/schemas/neo4j.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,7 @@ import {
sortArrayByNestedField,
} from "../utils/flattening";
import { ApolloServerContext } from "../utils/servers";
import {
CachedOncotreeData,
includeCancerTypeFieldsInSearch,
} from "../utils/oncotree";
import { includeCancerTypeFieldsInSearch } from "../utils/oncotree";
import { querySamplesList } from "../utils/ogm";

type SortOptions = { [key: string]: SortDirection }[];
Expand Down Expand Up @@ -163,7 +160,10 @@ export async function buildNeo4jDbSchema() {
await ogm.init();
const neo4jDbSchema = await neoSchema.getSchema();

return neo4jDbSchema;
return {
neo4jDbSchema,
ogm,
};
}

function buildResolvers(
Expand Down Expand Up @@ -290,7 +290,12 @@ function buildResolvers(
const sortOrder = Object.values(sortOptions[0])[0];

if (flattenedRequestFields.includes(sortField)) {
sortArrayByNestedField(requests, "Request", sortField, sortOrder);
await sortArrayByNestedField(
requests,
"Request",
sortField,
sortOrder
);
}
}

Expand Down Expand Up @@ -332,7 +337,12 @@ function buildResolvers(
const sortOrder = Object.values(sortOptions[0])[0];

if (flattenedPatientFields.includes(sortField)) {
sortArrayByNestedField(patients, "Patient", sortField, sortOrder);
await sortArrayByNestedField(
patients,
"Patient",
sortField,
sortOrder
);
}
}

Expand All @@ -343,27 +353,20 @@ function buildResolvers(
},
async samples(
_source: undefined,
args: any,
{ oncotreeCache }: ApolloServerContext
{ where }: any,
{ samplesLoader }: ApolloServerContext
) {
let customWhere = includeCancerTypeFieldsInSearch(
args.where,
oncotreeCache
);
return await querySamplesList(ogm, customWhere, args.options);
const result = await samplesLoader.load(where);
return result.data;
},
async samplesConnection(
_source: undefined,
args: any,
{ oncotreeCache }: ApolloServerContext
{ where }: any,
{ samplesLoader }: ApolloServerContext
) {
let customWhere = includeCancerTypeFieldsInSearch(
args.where,
oncotreeCache
);
const samples = await querySamplesList(ogm, customWhere, args.options);
const result = await samplesLoader.load(where);
return {
totalCount: samples.length,
totalCount: result.totalCount,
};
},
async cohorts(_source: undefined, args: any) {
Expand Down Expand Up @@ -402,7 +405,7 @@ function buildResolvers(

// We don't check for other flattened fields here because the Cohorts AG Grid opts out of the Server-Side Row Model. We only need to sort by initialCohortDeliveryDate for the initial load
if (sortField === "initialCohortDeliveryDate") {
sortArrayByNestedField(
await sortArrayByNestedField(
cohorts,
"Cohort",
"initialCohortDeliveryDate",
Expand Down
28 changes: 28 additions & 0 deletions graphql-server/src/utils/dataloader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { OGM } from "@neo4j/graphql-ogm";
import { includeCancerTypeFieldsInSearch } from "./oncotree";
import { querySamplesList } from "./ogm";
import DataLoader from "dataloader";
import { SamplesListQuery, SampleWhere } from "../generated/graphql";
import NodeCache from "node-cache";

type SamplesQueryResult = {
totalCount: number;
data: SamplesListQuery["samples"];
};

/**
* Create a DataLoader instance that batches the child queries (e.g. `samples` and
* `samplesConnection` child queries of SamplesList query) and then caches the results
* of those child queries within the same frontend request.
*
* This enables the SamplesList query to make a single trip to the database.
* Without this, we would make two trips, one via the `samples` child query and one
* via the `samplesConnection` child query.
*/
export function createSamplesLoader(ogm: OGM, oncotreeCache: NodeCache) {
return new DataLoader<SampleWhere, SamplesQueryResult>(async (keys) => {
const customWhere = includeCancerTypeFieldsInSearch(keys[0], oncotreeCache);
const result = await querySamplesList(ogm, customWhere);
return keys.map(() => result);
});
}
14 changes: 11 additions & 3 deletions graphql-server/src/utils/flattening.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,16 +307,24 @@ const nestedValueGetters: NestedValueGetters = {
},
};

export function sortArrayByNestedField(
export async function sortArrayByNestedField(
arr: any[],
nodeLabel: keyof typeof nestedValueGetters,
fieldName: string,
sortOrder: SortDirection,
context?: ApolloServerContext
) {
// Although the parts of nestedValueGetters that we use are all synchronous,
// it's still an async function and could return a promise
const resolvedValues = await Promise.all(
arr.map((obj) => nestedValueGetters[nodeLabel](obj, fieldName, context))
);

arr.sort((objA, objB) => {
let a = nestedValueGetters[nodeLabel](objA, fieldName, context);
let b = nestedValueGetters[nodeLabel](objB, fieldName, context);
const indexA = arr.indexOf(objA);
const indexB = arr.indexOf(objB);
let a = resolvedValues[indexA];
let b = resolvedValues[indexB];

if (a === null || a === undefined) return 1;
if (b === null || b === undefined) return -1;
Expand Down
27 changes: 19 additions & 8 deletions graphql-server/src/utils/ogm.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { GraphQLOptionsArg, GraphQLWhereArg, OGM } from "@neo4j/graphql-ogm";
import { GraphQLWhereArg, OGM } from "@neo4j/graphql-ogm";
import { sortArrayByNestedField } from "./flattening";
import { SortDirection } from "../generated/graphql";

export async function querySamplesList(
ogm: OGM,
where: GraphQLWhereArg,
options: GraphQLOptionsArg
) {
return await ogm.model("Sample").find({
const MAX_ROWS = 500;

export async function querySamplesList(ogm: OGM, where: GraphQLWhereArg) {
const samples = await ogm.model("Sample").find({
where: where,
options: options,
selectionSet: `{
datasource
revisable
Expand Down Expand Up @@ -105,4 +104,16 @@ export async function querySamplesList(
}
}`,
});

await sortArrayByNestedField(
samples,
"Sample",
"importDate",
SortDirection.Desc
);

return {
totalCount: samples.length,
data: samples.slice(0, MAX_ROWS),
};
}
5 changes: 4 additions & 1 deletion graphql-server/src/utils/servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { updateActiveUserSessions } from "./session";
import { corsOptions } from "./constants";
import NodeCache from "node-cache";
import { fetchAndCacheOncotreeData } from "./oncotree";
import { createSamplesLoader } from "./dataloader";

export function initializeHttpsServer(app: Express) {
const httpsServer = https.createServer(
Expand All @@ -33,13 +34,14 @@ export interface ApolloServerContext {
isAuthenticated: boolean;
};
oncotreeCache: NodeCache;
samplesLoader: ReturnType<typeof createSamplesLoader>;
}

export async function initializeApolloServer(
httpsServer: https.Server,
app: Express
) {
const neo4jDbSchema = await buildNeo4jDbSchema();
const { neo4jDbSchema, ogm } = await buildNeo4jDbSchema();
const mergedSchema = mergeSchemas({
schemas: [neo4jDbSchema, oracleDbSchema],
});
Expand All @@ -58,6 +60,7 @@ export async function initializeApolloServer(
isAuthenticated: req.isAuthenticated,
},
oncotreeCache: oncotreeCache,
samplesLoader: createSamplesLoader(ogm, oncotreeCache),
};
},
plugins: [
Expand Down

0 comments on commit 845d7f2

Please sign in to comment.