From 3e90b6fe6db4d42dc743c02313811cb4a61c9dd1 Mon Sep 17 00:00:00 2001 From: Quan Nguyen <86090707+qu8n@users.noreply.github.com> Date: Thu, 3 Oct 2024 13:48:23 -0400 Subject: [PATCH] Re-enable editing for the new Samples query (#165) * Create a mutation placeholder for the new samples update flow * Implement resolvers for the new sample mutation flow * Implement update flow for the new samples query * Fix bug in new samples query when filtering for Tempo data * Re-enable the higher export limit for samples * Perform various code cleanups, pruning, and refactors * Fix bug in new samples query when filtering search values * Fix bug where export >500 rows shows cols hidden by users --- frontend/src/components/RecordsList.tsx | 5 +- frontend/src/components/SamplesList.tsx | 29 +- frontend/src/components/UpdateModal.tsx | 98 +- frontend/src/generated/graphql.ts | 507 ++++------- frontend/src/index.tsx | 9 +- frontend/src/pages/cohorts/CohortsPage.tsx | 2 - frontend/src/shared/helpers.tsx | 6 - graphql-server/src/generated/graphql.ts | 437 +++------ graphql-server/src/schemas/custom.ts | 982 +++++++++++++-------- graphql-server/src/schemas/neo4j.ts | 182 +--- graphql-server/src/utils/servers.ts | 3 +- graphql.schema.json | 720 +++++++++++++-- graphql/operations.graphql | 106 +-- 13 files changed, 1635 insertions(+), 1451 deletions(-) diff --git a/frontend/src/components/RecordsList.tsx b/frontend/src/components/RecordsList.tsx index d4825e92..1fa8e5da 100644 --- a/frontend/src/components/RecordsList.tsx +++ b/frontend/src/components/RecordsList.tsx @@ -18,7 +18,7 @@ import { } from "ag-grid-community"; import { DataName, useHookLazyGeneric } from "../shared/types"; import SamplesList, { SampleContext } from "./SamplesList"; -import { Sample, SortDirection } from "../generated/graphql"; +import { SortDirection } from "../generated/graphql"; import { defaultColDef } from "../shared/helpers"; import { PatientIdsTriplet } from "../pages/patients/PatientsPage"; import { @@ -55,7 +55,6 @@ interface IRecordsListProps { handleDownload: () => void; samplesColDefs: ColDef[]; sampleContext?: SampleContext; - sampleKeyForUpdate?: keyof Sample; userEmail?: string | null; setUserEmail?: Dispatch>; setCustomSearchVals?: Dispatch>; @@ -81,7 +80,6 @@ export default function RecordsList({ handleDownload, samplesColDefs, sampleContext, - sampleKeyForUpdate, userEmail, setUserEmail, customToolbarUI, @@ -272,7 +270,6 @@ export default function RecordsList({ parentDataName={dataName} sampleContext={sampleContext} setUnsavedChanges={setUnsavedChanges} - sampleKeyForUpdate={sampleKeyForUpdate} userEmail={userEmail} setUserEmail={setUserEmail} /> diff --git a/frontend/src/components/SamplesList.tsx b/frontend/src/components/SamplesList.tsx index 5db1fb66..1842b0b8 100644 --- a/frontend/src/components/SamplesList.tsx +++ b/frontend/src/components/SamplesList.tsx @@ -1,9 +1,9 @@ -import { Sample, useDashboardSamplesQuery } from "../generated/graphql"; +import { useDashboardSamplesQuery } from "../generated/graphql"; import AutoSizer from "react-virtualized-auto-sizer"; import { Button, Col, Container } from "react-bootstrap"; import { Dispatch, SetStateAction, useRef } from "react"; import { DownloadModal } from "./DownloadModal"; -// import { UpdateModal } from "./UpdateModal"; +import { UpdateModal } from "./UpdateModal"; import { AlertModal } from "./AlertModal"; import { buildTsvString } from "../utils/stringBuilders"; import { @@ -48,7 +48,6 @@ interface ISampleListProps { setUnsavedChanges?: (unsavedChanges: boolean) => void; parentDataName?: DataName; sampleContext?: SampleContext; - sampleKeyForUpdate?: keyof Sample; userEmail?: string | null; setUserEmail?: Dispatch>; customToolbarUI?: JSX.Element; @@ -59,7 +58,6 @@ export default function SamplesList({ parentDataName, sampleContext, setUnsavedChanges, - sampleKeyForUpdate = "hasMetadataSampleMetadata", userEmail, setUserEmail, customToolbarUI, @@ -77,11 +75,12 @@ export default function SamplesList({ const params = useParams(); const hasParams = Object.keys(params).length > 0; - const { loading, error, data, startPolling, stopPolling, refetch } = + const { error, data, startPolling, stopPolling, refetch } = useDashboardSamplesQuery({ variables: { searchVals: [], sampleContext, + limit: MAX_ROWS_TABLE, }, pollInterval: POLLING_INTERVAL, }); @@ -95,6 +94,7 @@ export default function SamplesList({ refetch({ searchVals: parseUserSearchVal(userSearchVal), sampleContext, + limit: MAX_ROWS_TABLE, }).then(() => { gridRef.current?.api?.hideOverlay(); }); @@ -232,23 +232,24 @@ export default function SamplesList({ {showDownloadModal && ( { + const allColumns = gridRef.current?.columnApi?.getAllGridColumns(); return sampleCount <= MAX_ROWS_TABLE ? Promise.resolve( + buildTsvString(samples!, columnDefs, allColumns) + ) + : refetch({ limit: MAX_ROWS_EXPORT }).then((result) => buildTsvString( - samples!, + result.data.dashboardSamples!, columnDefs, - gridRef.current?.columnApi?.getAllGridColumns() + allColumns ) - ) - : refetch().then((result) => - buildTsvString(result.data.dashboardSamples!, columnDefs) ); }} onComplete={() => { setShowDownloadModal(false); // Reset the limit back to the default value of MAX_ROWS_TABLE. // Otherwise, polling will use the most recent value MAX_ROWS_EXPORT - refetch(); + refetch({ limit: MAX_ROWS_TABLE }); }} exportFileName={[ parentDataName?.slice(0, -1), @@ -260,16 +261,15 @@ export default function SamplesList({ /> )} - {/* {showUpdateModal && ( + {showUpdateModal && ( setShowUpdateModal(false)} onOpen={() => stopPolling()} - sampleKeyForUpdate={sampleKeyForUpdate} /> - )} */} + )} handleSearch(userSearchVal)} - suppressClickEdit={true} // temporarily disable cell editing /> )} diff --git a/frontend/src/components/UpdateModal.tsx b/frontend/src/components/UpdateModal.tsx index 7728c638..9ae20720 100644 --- a/frontend/src/components/UpdateModal.tsx +++ b/frontend/src/components/UpdateModal.tsx @@ -6,23 +6,32 @@ import "ag-grid-enterprise"; import styles from "./records.module.scss"; import "ag-grid-community/styles/ag-grid.css"; import "ag-grid-community/styles/ag-theme-alpine.css"; -import { ChangesByPrimaryId, SampleChange } from "../shared/helpers"; +import { SampleChange } from "../shared/helpers"; import { - DashboardSample, + DashboardSampleInput, DashboardSamplesQuery, - SampleUpdateInput, - SampleWhere, - useUpdateSamplesMutation, + useUpdateDashboardSamplesMutation, } from "../generated/graphql"; -import _ from "lodash"; + +const columnDefs = [ + { field: "primaryId", rowGroup: true, hide: true }, + { field: "fieldName" }, + { field: "oldValue" }, + { field: "newValue" }, +]; + +type ChangesByPrimaryId = { + [primaryId: string]: { + [fieldName: string]: string; + }; +}; interface UpdateModalProps { changes: SampleChange[]; onSuccess: () => void; onHide: () => void; samples: DashboardSamplesQuery["dashboardSamples"]; - onOpen?: () => void; - sampleKeyForUpdate: keyof DashboardSample; + onOpen: () => void; } export function UpdateModal({ @@ -31,21 +40,13 @@ export function UpdateModal({ onSuccess, onOpen, samples, - sampleKeyForUpdate, }: UpdateModalProps) { - const columnDefs = [ - { field: "primaryId", rowGroup: true, hide: true }, - { field: "fieldName" }, - { field: "oldValue" }, - { field: "newValue" }, - ]; - useEffect(() => { onOpen && onOpen(); // eslint-disable-next-line }, []); - const [updateSamplesMutation] = useUpdateSamplesMutation(); + const [updateDashboardSamplesMutation] = useUpdateDashboardSamplesMutation(); async function handleSubmitUpdates() { const changesByPrimaryId: ChangesByPrimaryId = {}; @@ -57,49 +58,32 @@ export function UpdateModal({ } } - const updatedSamples = _.cloneDeep(samples); - - updatedSamples?.forEach((s) => { - if (!s) return; // TODO: fix this - - const primaryId = s.primaryId as string; - if (primaryId in changesByPrimaryId) { - s.revisable = false; - - _.forEach(changesByPrimaryId[primaryId], (v, k) => { - /* @ts-ignore */ - s[sampleKeyForUpdate][0][k] = v; - }); + let newDashboardSamples: DashboardSampleInput[] = []; + samples.forEach((s) => { + if (s.primaryId in changesByPrimaryId) { + const newDashboardSample = { + ...s, + revisable: false, + changedFieldNames: Object.keys(changesByPrimaryId[s.primaryId]), + }; + for (const [fieldName, newValue] of Object.entries( + changesByPrimaryId[s.primaryId] + )) { + if (fieldName in s) { + (newDashboardSample as any)[fieldName] = newValue; + } + } + delete newDashboardSample.__typename; + newDashboardSamples.push(newDashboardSample); } }); - for (const [primaryId, changedFields] of Object.entries( - changesByPrimaryId - )) { - updateSamplesMutation({ - variables: { - where: { - hasMetadataSampleMetadata_SOME: { - primaryId: primaryId, - }, - } as SampleWhere, - update: { - [sampleKeyForUpdate]: [ - { - update: { - node: changedFields!, - }, - }, - ], - } as SampleUpdateInput, - }, - optimisticResponse: { - updateSamples: { - samples: updatedSamples as any, // TODO: fix this - }, - }, - }); - } + updateDashboardSamplesMutation({ + variables: { newDashboardSamples }, + optimisticResponse: { + updateDashboardSamples: newDashboardSamples, + }, + }); onSuccess(); onHide(); diff --git a/frontend/src/generated/graphql.ts b/frontend/src/generated/graphql.ts index 07ffed30..3f10073c 100644 --- a/frontend/src/generated/graphql.ts +++ b/frontend/src/generated/graphql.ts @@ -1765,7 +1765,7 @@ export type DashboardSample = { mafCompleteStatus?: Maybe; oncotreeCode?: Maybe; preservation?: Maybe; - primaryId?: Maybe; + primaryId: Scalars["String"]; qcCompleteDate?: Maybe; qcCompleteReason?: Maybe; qcCompleteResult?: Maybe; @@ -1776,20 +1776,63 @@ export type DashboardSample = { sampleOrigin?: Maybe; sampleType?: Maybe; sex?: Maybe; - smileSampleId?: Maybe; + smileSampleId: Scalars["String"]; species?: Maybe; tissueLocation?: Maybe; tumorOrNormal?: Maybe; validationReport?: Maybe; - validationStatus?: Maybe; + validationStatus?: Maybe; }; export type DashboardSampleCount = { __typename?: "DashboardSampleCount"; - count?: Maybe; totalCount?: Maybe; }; +export type DashboardSampleInput = { + accessLevel?: InputMaybe; + baitSet?: InputMaybe; + bamCompleteDate?: InputMaybe; + bamCompleteStatus?: InputMaybe; + billed?: InputMaybe; + billedBy?: InputMaybe; + cancerType?: InputMaybe; + cancerTypeDetailed?: InputMaybe; + changedFieldNames: Array; + cmoPatientId?: InputMaybe; + cmoSampleName?: InputMaybe; + collectionYear?: InputMaybe; + costCenter?: InputMaybe; + custodianInformation?: InputMaybe; + embargoDate?: InputMaybe; + genePanel?: InputMaybe; + importDate?: InputMaybe; + initialPipelineRunDate?: InputMaybe; + investigatorSampleId?: InputMaybe; + mafCompleteDate?: InputMaybe; + mafCompleteNormalPrimaryId?: InputMaybe; + mafCompleteStatus?: InputMaybe; + oncotreeCode?: InputMaybe; + preservation?: InputMaybe; + primaryId: Scalars["String"]; + qcCompleteDate?: InputMaybe; + qcCompleteReason?: InputMaybe; + qcCompleteResult?: InputMaybe; + qcCompleteStatus?: InputMaybe; + recipe?: InputMaybe; + revisable?: InputMaybe; + sampleClass?: InputMaybe; + sampleOrigin?: InputMaybe; + sampleType?: InputMaybe; + sex?: InputMaybe; + smileSampleId: Scalars["String"]; + species?: InputMaybe; + tissueLocation?: InputMaybe; + tumorOrNormal?: InputMaybe; + validationReport?: InputMaybe; + validationStatus?: InputMaybe; +}; + export type DeleteInfo = { __typename?: "DeleteInfo"; bookmark?: Maybe; @@ -2182,6 +2225,7 @@ export type Mutation = { updateBamCompletes: UpdateBamCompletesMutationResponse; updateCohortCompletes: UpdateCohortCompletesMutationResponse; updateCohorts: UpdateCohortsMutationResponse; + updateDashboardSamples?: Maybe>>; updateMafCompletes: UpdateMafCompletesMutationResponse; updatePatientAliases: UpdatePatientAliasesMutationResponse; updatePatients: UpdatePatientsMutationResponse; @@ -2358,6 +2402,10 @@ export type MutationUpdateCohortsArgs = { where?: InputMaybe; }; +export type MutationUpdateDashboardSamplesArgs = { + newDashboardSamples?: InputMaybe>>; +}; + export type MutationUpdateMafCompletesArgs = { connect?: InputMaybe; create?: InputMaybe; @@ -4469,8 +4517,8 @@ export type Query = { cohorts: Array; cohortsAggregate: CohortAggregateSelection; cohortsConnection: CohortsConnection; - dashboardSampleCount?: Maybe; - dashboardSamples?: Maybe>>; + dashboardSampleCount: DashboardSampleCount; + dashboardSamples: Array; mafCompletes: Array; mafCompletesAggregate: MafCompleteAggregateSelection; mafCompletesConnection: MafCompletesConnection; @@ -4560,12 +4608,13 @@ export type QueryCohortsConnectionArgs = { export type QueryDashboardSampleCountArgs = { sampleContext?: InputMaybe; - searchVals?: InputMaybe>>; + searchVals?: InputMaybe>; }; export type QueryDashboardSamplesArgs = { + limit: Scalars["Int"]; sampleContext?: InputMaybe; - searchVals?: InputMaybe>>; + searchVals?: InputMaybe>; }; export type QueryMafCompletesArgs = { @@ -12412,23 +12461,22 @@ export type PatientsListQuery = { }; export type DashboardSamplesQueryVariables = Exact<{ - searchVals?: InputMaybe< - Array> | InputMaybe - >; + searchVals?: InputMaybe | Scalars["String"]>; sampleContext?: InputMaybe; + limit: Scalars["Int"]; }>; export type DashboardSamplesQuery = { __typename?: "Query"; - dashboardSampleCount?: { + dashboardSampleCount: { __typename?: "DashboardSampleCount"; totalCount?: number | null; - } | null; - dashboardSamples?: Array<{ + }; + dashboardSamples: Array<{ __typename?: "DashboardSample"; - smileSampleId?: string | null; + smileSampleId: string; revisable?: boolean | null; - primaryId?: string | null; + primaryId: string; cmoSampleName?: string | null; importDate?: string | null; cmoPatientId?: string | null; @@ -12447,7 +12495,7 @@ export type DashboardSamplesQuery = { sex?: string | null; recipe?: string | null; validationReport?: string | null; - validationStatus?: string | null; + validationStatus?: boolean | null; cancerType?: string | null; cancerTypeDetailed?: string | null; billed?: boolean | null; @@ -12466,18 +12514,18 @@ export type DashboardSamplesQuery = { qcCompleteResult?: string | null; qcCompleteReason?: string | null; qcCompleteStatus?: string | null; - } | null> | null; + }>; }; export type DashboardSamplePartsFragment = { __typename?: "DashboardSample"; - smileSampleId?: string | null; + smileSampleId: string; revisable?: boolean | null; }; export type DashboardSampleMetadataPartsFragment = { __typename?: "DashboardSample"; - primaryId?: string | null; + primaryId: string; cmoSampleName?: string | null; importDate?: string | null; cmoPatientId?: string | null; @@ -12496,7 +12544,7 @@ export type DashboardSampleMetadataPartsFragment = { sex?: string | null; recipe?: string | null; validationReport?: string | null; - validationStatus?: string | null; + validationStatus?: boolean | null; cancerType?: string | null; cancerTypeDetailed?: string | null; }; @@ -12543,171 +12591,55 @@ export type RequestPartsFragment = { smileRequestId: string; }; -export type SamplePartsFragment = { - __typename?: "Sample"; - datasource: string; - revisable: boolean; - sampleCategory: string; - sampleClass: string; - smileSampleId: string; -}; - -export type SampleMetadataPartsFragment = { - __typename?: "SampleMetadata"; - additionalProperties: string; - baitSet?: string | null; - cfDNA2dBarcode?: string | null; - cmoInfoIgoId?: string | null; - cmoPatientId?: string | null; - cmoSampleIdFields: string; - cmoSampleName?: string | null; - collectionYear: string; - genePanel: string; - igoComplete?: boolean | null; - igoRequestId?: string | null; - importDate: string; - investigatorSampleId?: string | null; - libraries: string; - oncotreeCode?: string | null; - preservation?: string | null; - primaryId: string; - qcReports: string; - sampleClass: string; - sampleName?: string | null; - sampleOrigin?: string | null; - sampleType: string; - sex: string; - species: string; - tissueLocation?: string | null; - tubeId?: string | null; - tumorOrNormal: string; -}; - -export type TempoPartsFragment = { - __typename?: "Tempo"; - smileTempoId: string; - billed?: boolean | null; - billedBy?: string | null; - costCenter?: string | null; - custodianInformation?: string | null; - accessLevel?: string | null; -}; - -export type SamplesQueryVariables = Exact<{ - where?: InputMaybe; - hasMetadataSampleMetadataWhere2?: InputMaybe; - hasMetadataSampleMetadataOptions2?: InputMaybe; +export type UpdateDashboardSamplesMutationVariables = Exact<{ + newDashboardSamples: Array | DashboardSampleInput; }>; -export type SamplesQuery = { - __typename?: "Query"; - samples: Array<{ - __typename?: "Sample"; - smileSampleId: string; - revisable: boolean; - sampleCategory: string; - sampleClass: string; - datasource: string; - hasMetadataSampleMetadata: Array<{ - __typename?: "SampleMetadata"; - additionalProperties: string; - baitSet?: string | null; - cfDNA2dBarcode?: string | null; - cmoInfoIgoId?: string | null; - cmoPatientId?: string | null; - cmoSampleIdFields: string; - cmoSampleName?: string | null; - collectionYear: string; - genePanel: string; - igoComplete?: boolean | null; - igoRequestId?: string | null; - importDate: string; - investigatorSampleId?: string | null; - libraries: string; - oncotreeCode?: string | null; - preservation?: string | null; - primaryId: string; - qcReports: string; - sampleClass: string; - sampleName?: string | null; - sampleOrigin?: string | null; - sampleType: string; - sex: string; - species: string; - tissueLocation?: string | null; - tubeId?: string | null; - tumorOrNormal: string; - }>; - hasTempoTempos: Array<{ - __typename?: "Tempo"; - smileTempoId: string; - billed?: boolean | null; - billedBy?: string | null; - costCenter?: string | null; - custodianInformation?: string | null; - accessLevel?: string | null; - }>; - }>; -}; - -export type UpdateSamplesMutationVariables = Exact<{ - where?: InputMaybe; - update?: InputMaybe; - connect?: InputMaybe; -}>; - -export type UpdateSamplesMutation = { +export type UpdateDashboardSamplesMutation = { __typename?: "Mutation"; - updateSamples: { - __typename?: "UpdateSamplesMutationResponse"; - samples: Array<{ - __typename?: "Sample"; - smileSampleId: string; - revisable: boolean; - datasource: string; - sampleCategory: string; - sampleClass: string; - hasMetadataSampleMetadata: Array<{ - __typename?: "SampleMetadata"; - additionalProperties: string; - baitSet?: string | null; - cfDNA2dBarcode?: string | null; - cmoInfoIgoId?: string | null; - cmoPatientId?: string | null; - cmoSampleIdFields: string; - cmoSampleName?: string | null; - collectionYear: string; - genePanel: string; - igoComplete?: boolean | null; - igoRequestId?: string | null; - importDate: string; - investigatorSampleId?: string | null; - libraries: string; - oncotreeCode?: string | null; - preservation?: string | null; - primaryId: string; - qcReports: string; - sampleClass: string; - sampleName?: string | null; - sampleOrigin?: string | null; - sampleType: string; - sex: string; - species: string; - tissueLocation?: string | null; - tubeId?: string | null; - tumorOrNormal: string; - }>; - hasTempoTempos: Array<{ - __typename?: "Tempo"; - smileTempoId: string; - billed?: boolean | null; - billedBy?: string | null; - costCenter?: string | null; - custodianInformation?: string | null; - accessLevel?: string | null; - }>; - }>; - }; + updateDashboardSamples?: Array<{ + __typename?: "DashboardSample"; + smileSampleId: string; + revisable?: boolean | null; + primaryId: string; + cmoSampleName?: string | null; + importDate?: string | null; + cmoPatientId?: string | null; + investigatorSampleId?: string | null; + sampleType?: string | null; + species?: string | null; + genePanel?: string | null; + baitSet?: string | null; + preservation?: string | null; + tumorOrNormal?: string | null; + sampleClass?: string | null; + oncotreeCode?: string | null; + collectionYear?: string | null; + sampleOrigin?: string | null; + tissueLocation?: string | null; + sex?: string | null; + recipe?: string | null; + validationReport?: string | null; + validationStatus?: boolean | null; + cancerType?: string | null; + cancerTypeDetailed?: string | null; + billed?: boolean | null; + costCenter?: string | null; + billedBy?: string | null; + custodianInformation?: string | null; + accessLevel?: string | null; + initialPipelineRunDate?: string | null; + embargoDate?: string | null; + bamCompleteDate?: string | null; + bamCompleteStatus?: string | null; + mafCompleteDate?: string | null; + mafCompleteNormalPrimaryId?: string | null; + mafCompleteStatus?: string | null; + qcCompleteDate?: string | null; + qcCompleteResult?: string | null; + qcCompleteReason?: string | null; + qcCompleteStatus?: string | null; + } | null> | null; }; export type GetPatientIdsTripletsQueryVariables = Exact<{ @@ -12846,56 +12778,6 @@ export const RequestPartsFragmentDoc = gql` smileRequestId } `; -export const SamplePartsFragmentDoc = gql` - fragment SampleParts on Sample { - datasource - revisable - sampleCategory - sampleClass - smileSampleId - } -`; -export const SampleMetadataPartsFragmentDoc = gql` - fragment SampleMetadataParts on SampleMetadata { - additionalProperties - baitSet - cfDNA2dBarcode - cmoInfoIgoId - cmoPatientId - cmoSampleIdFields - cmoSampleName - collectionYear - genePanel - igoComplete - igoRequestId - importDate - investigatorSampleId - libraries - oncotreeCode - preservation - primaryId - qcReports - sampleClass - sampleName - sampleOrigin - sampleType - sex - species - tissueLocation - tubeId - tumorOrNormal - } -`; -export const TempoPartsFragmentDoc = gql` - fragment TempoParts on Tempo { - smileTempoId - billed - billedBy - costCenter - custodianInformation - accessLevel - } -`; export const RequestsListDocument = gql` query RequestsList($options: RequestOptions, $where: RequestWhere) { requestsConnection(where: $where) { @@ -13052,14 +12934,22 @@ export type PatientsListQueryResult = Apollo.QueryResult< PatientsListQueryVariables >; export const DashboardSamplesDocument = gql` - query DashboardSamples($searchVals: [String], $sampleContext: SampleContext) { + query DashboardSamples( + $searchVals: [String!] + $sampleContext: SampleContext + $limit: Int! + ) { dashboardSampleCount( searchVals: $searchVals sampleContext: $sampleContext ) { totalCount } - dashboardSamples(searchVals: $searchVals, sampleContext: $sampleContext) { + dashboardSamples( + searchVals: $searchVals + sampleContext: $sampleContext + limit: $limit + ) { ...DashboardSampleParts ...DashboardSampleMetadataParts ...DashboardTempoParts @@ -13084,11 +12974,12 @@ export const DashboardSamplesDocument = gql` * variables: { * searchVals: // value for 'searchVals' * sampleContext: // value for 'sampleContext' + * limit: // value for 'limit' * }, * }); */ export function useDashboardSamplesQuery( - baseOptions?: Apollo.QueryHookOptions< + baseOptions: Apollo.QueryHookOptions< DashboardSamplesQuery, DashboardSamplesQueryVariables > @@ -13121,144 +13012,62 @@ export type DashboardSamplesQueryResult = Apollo.QueryResult< DashboardSamplesQuery, DashboardSamplesQueryVariables >; -export const SamplesDocument = gql` - query Samples( - $where: SampleWhere - $hasMetadataSampleMetadataWhere2: SampleMetadataWhere - $hasMetadataSampleMetadataOptions2: SampleMetadataOptions - ) { - samples(where: $where) { - smileSampleId - revisable - sampleCategory - sampleClass - datasource - hasMetadataSampleMetadata( - where: $hasMetadataSampleMetadataWhere2 - options: $hasMetadataSampleMetadataOptions2 - ) { - ...SampleMetadataParts - } - hasTempoTempos { - ...TempoParts - } - } - } - ${SampleMetadataPartsFragmentDoc} - ${TempoPartsFragmentDoc} -`; - -/** - * __useSamplesQuery__ - * - * To run a query within a React component, call `useSamplesQuery` and pass it any options that fit your needs. - * When your component renders, `useSamplesQuery` returns an object from Apollo Client that contains loading, error, and data properties - * you can use to render your UI. - * - * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; - * - * @example - * const { data, loading, error } = useSamplesQuery({ - * variables: { - * where: // value for 'where' - * hasMetadataSampleMetadataWhere2: // value for 'hasMetadataSampleMetadataWhere2' - * hasMetadataSampleMetadataOptions2: // value for 'hasMetadataSampleMetadataOptions2' - * }, - * }); - */ -export function useSamplesQuery( - baseOptions?: Apollo.QueryHookOptions -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useQuery( - SamplesDocument, - options - ); -} -export function useSamplesLazyQuery( - baseOptions?: Apollo.LazyQueryHookOptions -) { - const options = { ...defaultOptions, ...baseOptions }; - return Apollo.useLazyQuery( - SamplesDocument, - options - ); -} -export type SamplesQueryHookResult = ReturnType; -export type SamplesLazyQueryHookResult = ReturnType; -export type SamplesQueryResult = Apollo.QueryResult< - SamplesQuery, - SamplesQueryVariables ->; -export const UpdateSamplesDocument = gql` - mutation UpdateSamples( - $where: SampleWhere - $update: SampleUpdateInput - $connect: SampleConnectInput +export const UpdateDashboardSamplesDocument = gql` + mutation UpdateDashboardSamples( + $newDashboardSamples: [DashboardSampleInput!]! ) { - updateSamples(where: $where, update: $update, connect: $connect) { - samples { - smileSampleId - revisable - datasource - sampleCategory - sampleClass - hasMetadataSampleMetadata { - ...SampleMetadataParts - } - hasTempoTempos { - ...TempoParts - } - } + updateDashboardSamples(newDashboardSamples: $newDashboardSamples) { + ...DashboardSampleParts + ...DashboardSampleMetadataParts + ...DashboardTempoParts } } - ${SampleMetadataPartsFragmentDoc} - ${TempoPartsFragmentDoc} + ${DashboardSamplePartsFragmentDoc} + ${DashboardSampleMetadataPartsFragmentDoc} + ${DashboardTempoPartsFragmentDoc} `; -export type UpdateSamplesMutationFn = Apollo.MutationFunction< - UpdateSamplesMutation, - UpdateSamplesMutationVariables +export type UpdateDashboardSamplesMutationFn = Apollo.MutationFunction< + UpdateDashboardSamplesMutation, + UpdateDashboardSamplesMutationVariables >; /** - * __useUpdateSamplesMutation__ + * __useUpdateDashboardSamplesMutation__ * - * To run a mutation, you first call `useUpdateSamplesMutation` within a React component and pass it any options that fit your needs. - * When your component renders, `useUpdateSamplesMutation` returns a tuple that includes: + * To run a mutation, you first call `useUpdateDashboardSamplesMutation` within a React component and pass it any options that fit your needs. + * When your component renders, `useUpdateDashboardSamplesMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example - * const [updateSamplesMutation, { data, loading, error }] = useUpdateSamplesMutation({ + * const [updateDashboardSamplesMutation, { data, loading, error }] = useUpdateDashboardSamplesMutation({ * variables: { - * where: // value for 'where' - * update: // value for 'update' - * connect: // value for 'connect' + * newDashboardSamples: // value for 'newDashboardSamples' * }, * }); */ -export function useUpdateSamplesMutation( +export function useUpdateDashboardSamplesMutation( baseOptions?: Apollo.MutationHookOptions< - UpdateSamplesMutation, - UpdateSamplesMutationVariables + UpdateDashboardSamplesMutation, + UpdateDashboardSamplesMutationVariables > ) { const options = { ...defaultOptions, ...baseOptions }; return Apollo.useMutation< - UpdateSamplesMutation, - UpdateSamplesMutationVariables - >(UpdateSamplesDocument, options); + UpdateDashboardSamplesMutation, + UpdateDashboardSamplesMutationVariables + >(UpdateDashboardSamplesDocument, options); } -export type UpdateSamplesMutationHookResult = ReturnType< - typeof useUpdateSamplesMutation +export type UpdateDashboardSamplesMutationHookResult = ReturnType< + typeof useUpdateDashboardSamplesMutation >; -export type UpdateSamplesMutationResult = - Apollo.MutationResult; -export type UpdateSamplesMutationOptions = Apollo.BaseMutationOptions< - UpdateSamplesMutation, - UpdateSamplesMutationVariables +export type UpdateDashboardSamplesMutationResult = + Apollo.MutationResult; +export type UpdateDashboardSamplesMutationOptions = Apollo.BaseMutationOptions< + UpdateDashboardSamplesMutation, + UpdateDashboardSamplesMutationVariables >; export const GetPatientIdsTripletsDocument = gql` query GetPatientIdsTriplets($patientIds: [String!]!) { diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index c5f29889..185754e8 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -8,22 +8,15 @@ import App from "./App"; import { REACT_APP_EXPRESS_SERVER_ORIGIN } from "./shared/constants"; const cache = new InMemoryCache({ - /* @ts-ignore */ typePolicies: { Query: { fields: { requests: offsetLimitPagination(), }, }, - Sample: { + DashboardSample: { keyFields: ["smileSampleId"], }, - SampleMetadata: { - keyFields: ["primaryId"], - }, - Tempo: { - keyFields: ["smileTempoId"], - }, }, }); diff --git a/frontend/src/pages/cohorts/CohortsPage.tsx b/frontend/src/pages/cohorts/CohortsPage.tsx index 4ec5adfd..88b892dd 100644 --- a/frontend/src/pages/cohorts/CohortsPage.tsx +++ b/frontend/src/pages/cohorts/CohortsPage.tsx @@ -138,7 +138,6 @@ export default function CohortsPage({ const dataName = "cohorts"; const sampleQueryParamFieldName = "cohortId"; const sampleQueryParamValue = params[sampleQueryParamFieldName]; - const sampleKeyForUpdate = "hasTempoTempos"; const defaultSort = [{ initialCohortDeliveryDate: SortDirection.Desc }]; return ( @@ -174,7 +173,6 @@ export default function CohortsPage({ } : undefined } - sampleKeyForUpdate={sampleKeyForUpdate} userEmail={userEmail} setUserEmail={setUserEmail} /> diff --git a/frontend/src/shared/helpers.tsx b/frontend/src/shared/helpers.tsx index 95facb26..eab634fd 100644 --- a/frontend/src/shared/helpers.tsx +++ b/frontend/src/shared/helpers.tsx @@ -27,12 +27,6 @@ export type SampleChange = { rowNode: RowNode; }; -export type ChangesByPrimaryId = { - [primaryId: string]: { - [fieldName: string]: string; - }; -}; - export const RequestsListColumns: ColDef[] = [ { headerName: "View Samples", diff --git a/graphql-server/src/generated/graphql.ts b/graphql-server/src/generated/graphql.ts index de546e39..86533e23 100644 --- a/graphql-server/src/generated/graphql.ts +++ b/graphql-server/src/generated/graphql.ts @@ -1764,7 +1764,7 @@ export type DashboardSample = { mafCompleteStatus?: Maybe; oncotreeCode?: Maybe; preservation?: Maybe; - primaryId?: Maybe; + primaryId: Scalars["String"]; qcCompleteDate?: Maybe; qcCompleteReason?: Maybe; qcCompleteResult?: Maybe; @@ -1775,20 +1775,63 @@ export type DashboardSample = { sampleOrigin?: Maybe; sampleType?: Maybe; sex?: Maybe; - smileSampleId?: Maybe; + smileSampleId: Scalars["String"]; species?: Maybe; tissueLocation?: Maybe; tumorOrNormal?: Maybe; validationReport?: Maybe; - validationStatus?: Maybe; + validationStatus?: Maybe; }; export type DashboardSampleCount = { __typename?: "DashboardSampleCount"; - count?: Maybe; totalCount?: Maybe; }; +export type DashboardSampleInput = { + accessLevel?: InputMaybe; + baitSet?: InputMaybe; + bamCompleteDate?: InputMaybe; + bamCompleteStatus?: InputMaybe; + billed?: InputMaybe; + billedBy?: InputMaybe; + cancerType?: InputMaybe; + cancerTypeDetailed?: InputMaybe; + changedFieldNames: Array; + cmoPatientId?: InputMaybe; + cmoSampleName?: InputMaybe; + collectionYear?: InputMaybe; + costCenter?: InputMaybe; + custodianInformation?: InputMaybe; + embargoDate?: InputMaybe; + genePanel?: InputMaybe; + importDate?: InputMaybe; + initialPipelineRunDate?: InputMaybe; + investigatorSampleId?: InputMaybe; + mafCompleteDate?: InputMaybe; + mafCompleteNormalPrimaryId?: InputMaybe; + mafCompleteStatus?: InputMaybe; + oncotreeCode?: InputMaybe; + preservation?: InputMaybe; + primaryId: Scalars["String"]; + qcCompleteDate?: InputMaybe; + qcCompleteReason?: InputMaybe; + qcCompleteResult?: InputMaybe; + qcCompleteStatus?: InputMaybe; + recipe?: InputMaybe; + revisable?: InputMaybe; + sampleClass?: InputMaybe; + sampleOrigin?: InputMaybe; + sampleType?: InputMaybe; + sex?: InputMaybe; + smileSampleId: Scalars["String"]; + species?: InputMaybe; + tissueLocation?: InputMaybe; + tumorOrNormal?: InputMaybe; + validationReport?: InputMaybe; + validationStatus?: InputMaybe; +}; + export type DeleteInfo = { __typename?: "DeleteInfo"; bookmark?: Maybe; @@ -2181,6 +2224,7 @@ export type Mutation = { updateBamCompletes: UpdateBamCompletesMutationResponse; updateCohortCompletes: UpdateCohortCompletesMutationResponse; updateCohorts: UpdateCohortsMutationResponse; + updateDashboardSamples?: Maybe>>; updateMafCompletes: UpdateMafCompletesMutationResponse; updatePatientAliases: UpdatePatientAliasesMutationResponse; updatePatients: UpdatePatientsMutationResponse; @@ -2357,6 +2401,10 @@ export type MutationUpdateCohortsArgs = { where?: InputMaybe; }; +export type MutationUpdateDashboardSamplesArgs = { + newDashboardSamples?: InputMaybe>>; +}; + export type MutationUpdateMafCompletesArgs = { connect?: InputMaybe; create?: InputMaybe; @@ -4468,8 +4516,8 @@ export type Query = { cohorts: Array; cohortsAggregate: CohortAggregateSelection; cohortsConnection: CohortsConnection; - dashboardSampleCount?: Maybe; - dashboardSamples?: Maybe>>; + dashboardSampleCount: DashboardSampleCount; + dashboardSamples: Array; mafCompletes: Array; mafCompletesAggregate: MafCompleteAggregateSelection; mafCompletesConnection: MafCompletesConnection; @@ -4559,12 +4607,13 @@ export type QueryCohortsConnectionArgs = { export type QueryDashboardSampleCountArgs = { sampleContext?: InputMaybe; - searchVals?: InputMaybe>>; + searchVals?: InputMaybe>; }; export type QueryDashboardSamplesArgs = { + limit: Scalars["Int"]; sampleContext?: InputMaybe; - searchVals?: InputMaybe>>; + searchVals?: InputMaybe>; }; export type QueryMafCompletesArgs = { @@ -12411,23 +12460,22 @@ export type PatientsListQuery = { }; export type DashboardSamplesQueryVariables = Exact<{ - searchVals?: InputMaybe< - Array> | InputMaybe - >; + searchVals?: InputMaybe | Scalars["String"]>; sampleContext?: InputMaybe; + limit: Scalars["Int"]; }>; export type DashboardSamplesQuery = { __typename?: "Query"; - dashboardSampleCount?: { + dashboardSampleCount: { __typename?: "DashboardSampleCount"; totalCount?: number | null; - } | null; - dashboardSamples?: Array<{ + }; + dashboardSamples: Array<{ __typename?: "DashboardSample"; - smileSampleId?: string | null; + smileSampleId: string; revisable?: boolean | null; - primaryId?: string | null; + primaryId: string; cmoSampleName?: string | null; importDate?: string | null; cmoPatientId?: string | null; @@ -12446,7 +12494,7 @@ export type DashboardSamplesQuery = { sex?: string | null; recipe?: string | null; validationReport?: string | null; - validationStatus?: string | null; + validationStatus?: boolean | null; cancerType?: string | null; cancerTypeDetailed?: string | null; billed?: boolean | null; @@ -12465,18 +12513,18 @@ export type DashboardSamplesQuery = { qcCompleteResult?: string | null; qcCompleteReason?: string | null; qcCompleteStatus?: string | null; - } | null> | null; + }>; }; export type DashboardSamplePartsFragment = { __typename?: "DashboardSample"; - smileSampleId?: string | null; + smileSampleId: string; revisable?: boolean | null; }; export type DashboardSampleMetadataPartsFragment = { __typename?: "DashboardSample"; - primaryId?: string | null; + primaryId: string; cmoSampleName?: string | null; importDate?: string | null; cmoPatientId?: string | null; @@ -12495,7 +12543,7 @@ export type DashboardSampleMetadataPartsFragment = { sex?: string | null; recipe?: string | null; validationReport?: string | null; - validationStatus?: string | null; + validationStatus?: boolean | null; cancerType?: string | null; cancerTypeDetailed?: string | null; }; @@ -12542,171 +12590,55 @@ export type RequestPartsFragment = { smileRequestId: string; }; -export type SamplePartsFragment = { - __typename?: "Sample"; - datasource: string; - revisable: boolean; - sampleCategory: string; - sampleClass: string; - smileSampleId: string; -}; - -export type SampleMetadataPartsFragment = { - __typename?: "SampleMetadata"; - additionalProperties: string; - baitSet?: string | null; - cfDNA2dBarcode?: string | null; - cmoInfoIgoId?: string | null; - cmoPatientId?: string | null; - cmoSampleIdFields: string; - cmoSampleName?: string | null; - collectionYear: string; - genePanel: string; - igoComplete?: boolean | null; - igoRequestId?: string | null; - importDate: string; - investigatorSampleId?: string | null; - libraries: string; - oncotreeCode?: string | null; - preservation?: string | null; - primaryId: string; - qcReports: string; - sampleClass: string; - sampleName?: string | null; - sampleOrigin?: string | null; - sampleType: string; - sex: string; - species: string; - tissueLocation?: string | null; - tubeId?: string | null; - tumorOrNormal: string; -}; - -export type TempoPartsFragment = { - __typename?: "Tempo"; - smileTempoId: string; - billed?: boolean | null; - billedBy?: string | null; - costCenter?: string | null; - custodianInformation?: string | null; - accessLevel?: string | null; -}; - -export type SamplesQueryVariables = Exact<{ - where?: InputMaybe; - hasMetadataSampleMetadataWhere2?: InputMaybe; - hasMetadataSampleMetadataOptions2?: InputMaybe; +export type UpdateDashboardSamplesMutationVariables = Exact<{ + newDashboardSamples: Array | DashboardSampleInput; }>; -export type SamplesQuery = { - __typename?: "Query"; - samples: Array<{ - __typename?: "Sample"; - smileSampleId: string; - revisable: boolean; - sampleCategory: string; - sampleClass: string; - datasource: string; - hasMetadataSampleMetadata: Array<{ - __typename?: "SampleMetadata"; - additionalProperties: string; - baitSet?: string | null; - cfDNA2dBarcode?: string | null; - cmoInfoIgoId?: string | null; - cmoPatientId?: string | null; - cmoSampleIdFields: string; - cmoSampleName?: string | null; - collectionYear: string; - genePanel: string; - igoComplete?: boolean | null; - igoRequestId?: string | null; - importDate: string; - investigatorSampleId?: string | null; - libraries: string; - oncotreeCode?: string | null; - preservation?: string | null; - primaryId: string; - qcReports: string; - sampleClass: string; - sampleName?: string | null; - sampleOrigin?: string | null; - sampleType: string; - sex: string; - species: string; - tissueLocation?: string | null; - tubeId?: string | null; - tumorOrNormal: string; - }>; - hasTempoTempos: Array<{ - __typename?: "Tempo"; - smileTempoId: string; - billed?: boolean | null; - billedBy?: string | null; - costCenter?: string | null; - custodianInformation?: string | null; - accessLevel?: string | null; - }>; - }>; -}; - -export type UpdateSamplesMutationVariables = Exact<{ - where?: InputMaybe; - update?: InputMaybe; - connect?: InputMaybe; -}>; - -export type UpdateSamplesMutation = { +export type UpdateDashboardSamplesMutation = { __typename?: "Mutation"; - updateSamples: { - __typename?: "UpdateSamplesMutationResponse"; - samples: Array<{ - __typename?: "Sample"; - smileSampleId: string; - revisable: boolean; - datasource: string; - sampleCategory: string; - sampleClass: string; - hasMetadataSampleMetadata: Array<{ - __typename?: "SampleMetadata"; - additionalProperties: string; - baitSet?: string | null; - cfDNA2dBarcode?: string | null; - cmoInfoIgoId?: string | null; - cmoPatientId?: string | null; - cmoSampleIdFields: string; - cmoSampleName?: string | null; - collectionYear: string; - genePanel: string; - igoComplete?: boolean | null; - igoRequestId?: string | null; - importDate: string; - investigatorSampleId?: string | null; - libraries: string; - oncotreeCode?: string | null; - preservation?: string | null; - primaryId: string; - qcReports: string; - sampleClass: string; - sampleName?: string | null; - sampleOrigin?: string | null; - sampleType: string; - sex: string; - species: string; - tissueLocation?: string | null; - tubeId?: string | null; - tumorOrNormal: string; - }>; - hasTempoTempos: Array<{ - __typename?: "Tempo"; - smileTempoId: string; - billed?: boolean | null; - billedBy?: string | null; - costCenter?: string | null; - custodianInformation?: string | null; - accessLevel?: string | null; - }>; - }>; - }; + updateDashboardSamples?: Array<{ + __typename?: "DashboardSample"; + smileSampleId: string; + revisable?: boolean | null; + primaryId: string; + cmoSampleName?: string | null; + importDate?: string | null; + cmoPatientId?: string | null; + investigatorSampleId?: string | null; + sampleType?: string | null; + species?: string | null; + genePanel?: string | null; + baitSet?: string | null; + preservation?: string | null; + tumorOrNormal?: string | null; + sampleClass?: string | null; + oncotreeCode?: string | null; + collectionYear?: string | null; + sampleOrigin?: string | null; + tissueLocation?: string | null; + sex?: string | null; + recipe?: string | null; + validationReport?: string | null; + validationStatus?: boolean | null; + cancerType?: string | null; + cancerTypeDetailed?: string | null; + billed?: boolean | null; + costCenter?: string | null; + billedBy?: string | null; + custodianInformation?: string | null; + accessLevel?: string | null; + initialPipelineRunDate?: string | null; + embargoDate?: string | null; + bamCompleteDate?: string | null; + bamCompleteStatus?: string | null; + mafCompleteDate?: string | null; + mafCompleteNormalPrimaryId?: string | null; + mafCompleteStatus?: string | null; + qcCompleteDate?: string | null; + qcCompleteResult?: string | null; + qcCompleteReason?: string | null; + qcCompleteStatus?: string | null; + } | null> | null; }; export type GetPatientIdsTripletsQueryVariables = Exact<{ @@ -12845,56 +12777,6 @@ export const RequestPartsFragmentDoc = gql` smileRequestId } `; -export const SamplePartsFragmentDoc = gql` - fragment SampleParts on Sample { - datasource - revisable - sampleCategory - sampleClass - smileSampleId - } -`; -export const SampleMetadataPartsFragmentDoc = gql` - fragment SampleMetadataParts on SampleMetadata { - additionalProperties - baitSet - cfDNA2dBarcode - cmoInfoIgoId - cmoPatientId - cmoSampleIdFields - cmoSampleName - collectionYear - genePanel - igoComplete - igoRequestId - importDate - investigatorSampleId - libraries - oncotreeCode - preservation - primaryId - qcReports - sampleClass - sampleName - sampleOrigin - sampleType - sex - species - tissueLocation - tubeId - tumorOrNormal - } -`; -export const TempoPartsFragmentDoc = gql` - fragment TempoParts on Tempo { - smileTempoId - billed - billedBy - costCenter - custodianInformation - accessLevel - } -`; export const RequestsListDocument = gql` query RequestsList($options: RequestOptions, $where: RequestWhere) { requestsConnection(where: $where) { @@ -12955,14 +12837,22 @@ export type PatientsListQueryResult = Apollo.QueryResult< PatientsListQueryVariables >; export const DashboardSamplesDocument = gql` - query DashboardSamples($searchVals: [String], $sampleContext: SampleContext) { + query DashboardSamples( + $searchVals: [String!] + $sampleContext: SampleContext + $limit: Int! + ) { dashboardSampleCount( searchVals: $searchVals sampleContext: $sampleContext ) { totalCount } - dashboardSamples(searchVals: $searchVals, sampleContext: $sampleContext) { + dashboardSamples( + searchVals: $searchVals + sampleContext: $sampleContext + limit: $limit + ) { ...DashboardSampleParts ...DashboardSampleMetadataParts ...DashboardTempoParts @@ -12976,70 +12866,29 @@ export type DashboardSamplesQueryResult = Apollo.QueryResult< DashboardSamplesQuery, DashboardSamplesQueryVariables >; -export const SamplesDocument = gql` - query Samples( - $where: SampleWhere - $hasMetadataSampleMetadataWhere2: SampleMetadataWhere - $hasMetadataSampleMetadataOptions2: SampleMetadataOptions - ) { - samples(where: $where) { - smileSampleId - revisable - sampleCategory - sampleClass - datasource - hasMetadataSampleMetadata( - where: $hasMetadataSampleMetadataWhere2 - options: $hasMetadataSampleMetadataOptions2 - ) { - ...SampleMetadataParts - } - hasTempoTempos { - ...TempoParts - } - } - } - ${SampleMetadataPartsFragmentDoc} - ${TempoPartsFragmentDoc} -`; -export type SamplesQueryResult = Apollo.QueryResult< - SamplesQuery, - SamplesQueryVariables ->; -export const UpdateSamplesDocument = gql` - mutation UpdateSamples( - $where: SampleWhere - $update: SampleUpdateInput - $connect: SampleConnectInput +export const UpdateDashboardSamplesDocument = gql` + mutation UpdateDashboardSamples( + $newDashboardSamples: [DashboardSampleInput!]! ) { - updateSamples(where: $where, update: $update, connect: $connect) { - samples { - smileSampleId - revisable - datasource - sampleCategory - sampleClass - hasMetadataSampleMetadata { - ...SampleMetadataParts - } - hasTempoTempos { - ...TempoParts - } - } + updateDashboardSamples(newDashboardSamples: $newDashboardSamples) { + ...DashboardSampleParts + ...DashboardSampleMetadataParts + ...DashboardTempoParts } } - ${SampleMetadataPartsFragmentDoc} - ${TempoPartsFragmentDoc} + ${DashboardSamplePartsFragmentDoc} + ${DashboardSampleMetadataPartsFragmentDoc} + ${DashboardTempoPartsFragmentDoc} `; -export type UpdateSamplesMutationFn = Apollo.MutationFunction< - UpdateSamplesMutation, - UpdateSamplesMutationVariables +export type UpdateDashboardSamplesMutationFn = Apollo.MutationFunction< + UpdateDashboardSamplesMutation, + UpdateDashboardSamplesMutationVariables >; -export type UpdateSamplesMutationResult = - Apollo.MutationResult; -export type UpdateSamplesMutationOptions = Apollo.BaseMutationOptions< - UpdateSamplesMutation, - UpdateSamplesMutationVariables +export type UpdateDashboardSamplesMutationResult = + Apollo.MutationResult; +export type UpdateDashboardSamplesMutationOptions = Apollo.BaseMutationOptions< + UpdateDashboardSamplesMutation, + UpdateDashboardSamplesMutationVariables >; export const GetPatientIdsTripletsDocument = gql` query GetPatientIdsTriplets($patientIds: [String!]!) { diff --git a/graphql-server/src/schemas/custom.ts b/graphql-server/src/schemas/custom.ts index 8cdf9a74..4a096501 100644 --- a/graphql-server/src/schemas/custom.ts +++ b/graphql-server/src/schemas/custom.ts @@ -4,215 +4,303 @@ import { CachedOncotreeData } from "../utils/oncotree"; import NodeCache from "node-cache"; import { parseJsonSafely } from "../utils/json"; import { gql } from "apollo-server"; -import { SampleContext } from "../generated/graphql"; - -const resolvers = { - Query: { - async dashboardSamples( - _source: undefined, - { - searchVals, - sampleContext, - }: { searchVals: string[]; sampleContext: SampleContext }, - { oncotreeCache }: ApolloServerContext - ) { - const addlOncotreeCodes = getAddlOtCodesMatchingCtOrCtdVals({ - searchVals, - oncotreeCache, - }); - - return await queryDashboardSamples({ - searchVals, - sampleContext, - oncotreeCache, - addlOncotreeCodes: Array.from(addlOncotreeCodes), - }); +import { + DashboardSampleInput, + QueryDashboardSampleCountArgs, + QueryDashboardSamplesArgs, +} from "../generated/graphql"; +import { props } from "../utils/constants"; +import { connect, headers, StringCodec } from "nats"; +import { OGM } from "@neo4j/graphql-ogm"; +const request = require("request-promise-native"); + +export async function buildCustomSchema(ogm: OGM) { + const resolvers = { + Query: { + async dashboardSamples( + _source: undefined, + { searchVals, sampleContext, limit }: QueryDashboardSamplesArgs, + { oncotreeCache }: ApolloServerContext + ) { + const addlOncotreeCodes = getAddlOtCodesMatchingCtOrCtdVals({ + searchVals, + oncotreeCache, + }); + + const partialCypherQuery = buildPartialCypherQuery({ + searchVals, + sampleContext, + addlOncotreeCodes, + }); + + return await queryDashboardSamples({ + partialCypherQuery, + limit, + oncotreeCache, + }); + }, + async dashboardSampleCount( + _source: undefined, + { searchVals, sampleContext }: QueryDashboardSampleCountArgs, + { oncotreeCache }: ApolloServerContext + ) { + const addlOncotreeCodes = getAddlOtCodesMatchingCtOrCtdVals({ + searchVals, + oncotreeCache, + }); + + const partialCypherQuery = buildPartialCypherQuery({ + searchVals, + sampleContext, + addlOncotreeCodes, + }); + + return await queryDashboardSampleCount({ + partialCypherQuery, + }); + }, }, - async dashboardSampleCount( - _source: undefined, - { - searchVals, - sampleContext, - }: { searchVals: string[]; sampleContext: SampleContext }, - { oncotreeCache }: ApolloServerContext - ) { - const addlOncotreeCodes = getAddlOtCodesMatchingCtOrCtdVals({ - searchVals, - oncotreeCache, - }); - - return await queryDashboardSampleCount({ - searchVals, - sampleContext, - addlOncotreeCodes: Array.from(addlOncotreeCodes), - }); + Mutation: { + async updateDashboardSamples( + _source: undefined, + { newDashboardSamples }: { newDashboardSamples: DashboardSampleInput[] } + ) { + updateAllSamplesConcurrently(newDashboardSamples, ogm); + + // Here, we're returning newDashboardSamples for simplicity. However, if we were to follow + // GraphQL's convention, we'd return the actual resulting data from the database update. This + // means we'd wait for SMILE services to finish processing the data changes, then query that + // data to return it to the frontend. For more context, see: + // https://www.apollographql.com/docs/react/performance/optimistic-ui/#optimistic-mutation-lifecycle + return newDashboardSamples; + }, }, - }, -}; - -const typeDefs = gql` - type DashboardSampleCount { - totalCount: Int - } - - type DashboardSample { - # (s:Sample) - smileSampleId: String - revisable: Boolean - - # (s:Sample)-[:HAS_METADATA]->(sm:SampleMetadata) - ## Root-level fields - primaryId: String - cmoSampleName: String - importDate: String - cmoPatientId: String - investigatorSampleId: String - sampleType: String - species: String - genePanel: String - baitSet: String - preservation: String - tumorOrNormal: String - sampleClass: String - oncotreeCode: String - collectionYear: String - sampleOrigin: String - tissueLocation: String - sex: String - ## Custom fields - recipe: String - ## (sm:SampleMetadata)-[:HAS_STATUS]->(s:Status) - validationReport: String - validationStatus: String - - # Oncotree API - cancerType: String - cancerTypeDetailed: String - - ## (s:Sample)-[:HAS_TEMPO]->(t:Tempo) - ## Root-level fields - billed: Boolean - costCenter: String - billedBy: String - custodianInformation: String - accessLevel: String - ## Custom fields - initialPipelineRunDate: String - embargoDate: String - ## (t:Tempo)-[:HAS_EVENT]->(bc:BamComplete) - bamCompleteDate: String - bamCompleteStatus: String - ## (t:Tempo)-[:HAS_EVENT]->(mc:MafComplete) - mafCompleteDate: String - mafCompleteNormalPrimaryId: String - mafCompleteStatus: String - # (t:Tempo)-[:HAS_EVENT]->(qc:QcComplete) - qcCompleteDate: String - qcCompleteResult: String - qcCompleteReason: String - qcCompleteStatus: String - } - - type DashboardSampleCount { - count: Int - } - - input SampleContext { - fieldName: String - values: [String!]! - } - - type Query { - dashboardSamples( - searchVals: [String] - sampleContext: SampleContext - ): [DashboardSample] - dashboardSampleCount( - searchVals: [String] - sampleContext: SampleContext - ): DashboardSampleCount - } -`; + }; + + const typeDefs = gql` + type DashboardSampleCount { + totalCount: Int + } + + type DashboardSample { + # (s:Sample) + smileSampleId: String! + revisable: Boolean + + # (s:Sample)-[:HAS_METADATA]->(sm:SampleMetadata) + ## Root-level fields + primaryId: String! + cmoSampleName: String + importDate: String + cmoPatientId: String + investigatorSampleId: String + sampleType: String + species: String + genePanel: String + baitSet: String + preservation: String + tumorOrNormal: String + sampleClass: String + oncotreeCode: String + collectionYear: String + sampleOrigin: String + tissueLocation: String + sex: String + ## Custom fields + recipe: String + ## (sm:SampleMetadata)-[:HAS_STATUS]->(s:Status) + validationReport: String + validationStatus: Boolean + + # Oncotree API + cancerType: String + cancerTypeDetailed: String + + ## (s:Sample)-[:HAS_TEMPO]->(t:Tempo) + ## Root-level fields + billed: Boolean + costCenter: String + billedBy: String + custodianInformation: String + accessLevel: String + ## Custom fields + initialPipelineRunDate: String + embargoDate: String + ## (t:Tempo)-[:HAS_EVENT]->(bc:BamComplete) + bamCompleteDate: String + bamCompleteStatus: String + ## (t:Tempo)-[:HAS_EVENT]->(mc:MafComplete) + mafCompleteDate: String + mafCompleteNormalPrimaryId: String + mafCompleteStatus: String + # (t:Tempo)-[:HAS_EVENT]->(qc:QcComplete) + qcCompleteDate: String + qcCompleteResult: String + qcCompleteReason: String + qcCompleteStatus: String + } + + input SampleContext { + fieldName: String + values: [String!]! + } + + type Query { + dashboardSamples( + searchVals: [String!] + sampleContext: SampleContext + limit: Int! + ): [DashboardSample!]! + dashboardSampleCount( + searchVals: [String!] + sampleContext: SampleContext + ): DashboardSampleCount! + } + + # We have to define a separate "input" type and can't reuse DashboardSample. + # For more context, see: https://stackoverflow.com/q/41743253 + input DashboardSampleInput { + changedFieldNames: [String!]! + + # (s:Sample) + smileSampleId: String! + revisable: Boolean + + # (s:Sample)-[:HAS_METADATA]->(sm:SampleMetadata) + ## Root-level fields + primaryId: String! + cmoSampleName: String + importDate: String + cmoPatientId: String + investigatorSampleId: String + sampleType: String + species: String + genePanel: String + baitSet: String + preservation: String + tumorOrNormal: String + sampleClass: String + oncotreeCode: String + collectionYear: String + sampleOrigin: String + tissueLocation: String + sex: String + ## Custom fields + recipe: String + ## (sm:SampleMetadata)-[:HAS_STATUS]->(s:Status) + validationReport: String + validationStatus: Boolean + + # Oncotree API + cancerType: String + cancerTypeDetailed: String + + ## (s:Sample)-[:HAS_TEMPO]->(t:Tempo) + ## Root-level fields + billed: Boolean + costCenter: String + billedBy: String + custodianInformation: String + accessLevel: String + ## Custom fields + initialPipelineRunDate: String + embargoDate: String + ## (t:Tempo)-[:HAS_EVENT]->(bc:BamComplete) + bamCompleteDate: String + bamCompleteStatus: String + ## (t:Tempo)-[:HAS_EVENT]->(mc:MafComplete) + mafCompleteDate: String + mafCompleteNormalPrimaryId: String + mafCompleteStatus: String + # (t:Tempo)-[:HAS_EVENT]->(qc:QcComplete) + qcCompleteDate: String + qcCompleteResult: String + qcCompleteReason: String + qcCompleteStatus: String + } + + type Mutation { + updateDashboardSamples( + newDashboardSamples: [DashboardSampleInput] + ): [DashboardSample] + } + `; -export const customSchema = makeExecutableSchema({ - typeDefs: typeDefs, - resolvers: resolvers, -}); + return makeExecutableSchema({ + typeDefs: typeDefs, + resolvers: resolvers, + }); +} async function queryDashboardSamples({ - searchVals, - sampleContext, + partialCypherQuery, + limit, oncotreeCache, - addlOncotreeCodes, }: { - searchVals: string[]; - sampleContext?: SampleContext; + partialCypherQuery: string; + limit: QueryDashboardSamplesArgs["limit"]; oncotreeCache: NodeCache; - addlOncotreeCodes: string[]; }) { - const partialCypherQuery = buildPartialCypherQuery({ - searchVals, - sampleContext, - addlOncotreeCodes, - }); - const cypherQuery = ` - ${partialCypherQuery} - RETURN - sample.revisable AS revisable, - - latestSm.primaryId AS primaryId, - latestSm.cmoSampleName AS cmoSampleName, - latestSm.importDate AS importDate, - latestSm.cmoPatientId AS cmoPatientId, - latestSm.investigatorSampleId AS investigatorSampleId, - latestSm.sampleType AS sampleType, - latestSm.species AS species, - latestSm.genePanel AS genePanel, - latestSm.baitSet AS baitSet, - latestSm.preservation AS preservation, - latestSm.tumorOrNormal AS tumorOrNormal, - latestSm.sampleClass AS sampleClass, - latestSm.oncotreeCode AS oncotreeCode, - latestSm.collectionYear AS collectionYear, - latestSm.sampleOrigin AS sampleOrigin, - latestSm.tissueLocation AS tissueLocation, - latestSm.sex AS sex, - latestSm.cmoSampleIdFields AS cmoSampleIdFields, - - oldestCC.date AS initialPipelineRunDate, - - latestT.smileTempoId AS smileTempoId, - latestT.billed AS billed, - latestT.costCenter AS costCenter, - latestT.billedBy AS billedBy, - latestT.custodianInformation AS custodianInformation, - latestT.accessLevel AS accessLevel, - - latestBC.date AS bamCompleteDate, - latestBC.status AS bamCompleteStatus, - - latestMC.date AS mafCompleteDate, - latestMC.normalPrimaryId AS mafCompleteNormalPrimaryId, - latestMC.status AS mafCompleteStatus, - - latestQC.date AS qcCompleteDate, - latestQC.result AS qcCompleteResult, - latestQC.reason AS qcCompleteReason, - latestQC.status AS qcCompleteStatus - - ORDER BY importDate DESC - LIMIT 500 + ${partialCypherQuery} + RETURN + s.smileSampleId AS smileSampleId, + s.revisable AS revisable, + + latestSm.primaryId AS primaryId, + latestSm.cmoSampleName AS cmoSampleName, + latestSm.importDate AS importDate, + latestSm.cmoPatientId AS cmoPatientId, + latestSm.investigatorSampleId AS investigatorSampleId, + latestSm.sampleType AS sampleType, + latestSm.species AS species, + latestSm.genePanel AS genePanel, + latestSm.baitSet AS baitSet, + latestSm.preservation AS preservation, + latestSm.tumorOrNormal AS tumorOrNormal, + latestSm.sampleClass AS sampleClass, + latestSm.oncotreeCode AS oncotreeCode, + latestSm.collectionYear AS collectionYear, + latestSm.sampleOrigin AS sampleOrigin, + latestSm.tissueLocation AS tissueLocation, + latestSm.sex AS sex, + latestSm.cmoSampleIdFields AS cmoSampleIdFields, + + oldestCCDate AS initialPipelineRunDate, + + t.smileTempoId AS smileTempoId, + t.billed AS billed, + t.costCenter AS costCenter, + t.billedBy AS billedBy, + t.custodianInformation AS custodianInformation, + t.accessLevel AS accessLevel, + + latestBC.date AS bamCompleteDate, + latestBC.status AS bamCompleteStatus, + + latestMC.date AS mafCompleteDate, + latestMC.normalPrimaryId AS mafCompleteNormalPrimaryId, + latestMC.status AS mafCompleteStatus, + + latestQC.date AS qcCompleteDate, + latestQC.result AS qcCompleteResult, + latestQC.reason AS qcCompleteReason, + latestQC.status AS qcCompleteStatus + + ORDER BY importDate DESC + LIMIT ${limit} `; const session = neo4jDriver.session(); try { const result = await session.run(cypherQuery); + return result.records.map((record) => { const recordObject = record.toObject(); const otCache = recordObject.oncotreeCode ? (oncotreeCache.get(recordObject.oncotreeCode) as CachedOncotreeData) : null; + return { ...recordObject, recipe: parseJsonSafely(recordObject.cmoSampleIdFields)?.recipe, @@ -228,29 +316,19 @@ async function queryDashboardSamples({ }; }); } catch (error) { - console.error("Error running query:", error); + console.error("Error with queryDashboardSamples:", error); } } async function queryDashboardSampleCount({ - searchVals, - sampleContext, - addlOncotreeCodes, + partialCypherQuery, }: { - searchVals: string[]; - sampleContext?: SampleContext; - addlOncotreeCodes: string[]; + partialCypherQuery: string; }) { - const partialCypherQuery = buildPartialCypherQuery({ - searchVals, - sampleContext, - addlOncotreeCodes, - }); - const cypherQuery = ` - ${partialCypherQuery} - RETURN - count(sample) AS totalCount + ${partialCypherQuery} + RETURN + count(s) AS totalCount `; const session = neo4jDriver.session(); @@ -258,207 +336,195 @@ async function queryDashboardSampleCount({ const result = await session.run(cypherQuery); return result.records[0].toObject(); } catch (error) { - console.error("Error running query:", error); + console.error("Error with queryDashboardSampleCount:", error); } } -function buildPartialCypherQuery({ +function buildSearchFilters({ + variable, + fields, searchVals, - sampleContext, - addlOncotreeCodes, + useFuzzyMatch = true, }: { + variable: string; + fields: string[]; searchVals: string[]; - sampleContext?: SampleContext; - addlOncotreeCodes: string[]; -}) { - function buildSearchFilter( - variable: string, - fields: string[], - searchVals: string[], - useFuzzyMatch: boolean = true - ): string { - const regexPattern = useFuzzyMatch - ? `(?i).*(${searchVals.join("|")}).*` - : `${searchVals.join("|")}`; + useFuzzyMatch?: boolean; +}): string { + if (useFuzzyMatch) { return fields .map( - (field) => - `${variable}.${field} =${useFuzzyMatch ? "~" : ""} '${regexPattern}'` + (field) => `${variable}.${field} =~ '(?i).*(${searchVals.join("|")}).*'` + ) + .join(" OR "); + } else { + return fields + .flatMap((field) => + searchVals.map((val) => `${variable}.${field} = '${val}'`) ) .join(" OR "); } +} - const searchFiltersConfig = [ - { - variable: "sm", - fields: [ - "primaryId", - "cmoSampleName", - "importDate", - "cmoPatientId", - "investigatorSampleId", - "sampleType", - "species", - "genePanel", - "baitSet", - "preservation", - "tumorOrNormal", - "sampleClass", - "oncotreeCode", - "collectionYear", - "sampleOrigin", - "tissueLocation", - "sex", - "cmoSampleIdFields", - ], - }, - { - variable: "t", - fields: ["costCenter", "billedBy", "custodianInformation", "accessLevel"], - }, - { variable: "bc", fields: ["date", "status"] }, - { variable: "mc", fields: ["date", "normalPrimaryId", "status"] }, - { variable: "qc", fields: ["date", "result", "reason", "status"] }, - ]; - - const searchFilters = - searchVals.length > 0 - ? searchFiltersConfig.map( - (config) => - `${buildSearchFilter(config.variable, config.fields, searchVals)}` - ) - : ["", "", "", "", ""]; +const searchFiltersConfig = [ + { + variable: "latestSm", + fields: [ + "primaryId", + "cmoSampleName", + "importDate", + "cmoPatientId", + "investigatorSampleId", + "sampleType", + "species", + "genePanel", + "baitSet", + "preservation", + "tumorOrNormal", + "sampleClass", + "oncotreeCode", + "collectionYear", + "sampleOrigin", + "tissueLocation", + "sex", + "cmoSampleIdFields", // for searching recipe + ], + }, + { + variable: "t", + fields: ["costCenter", "billedBy", "custodianInformation", "accessLevel"], + }, + { variable: "latestBC", fields: ["date", "status"] }, + { variable: "latestMC", fields: ["date", "normalPrimaryId", "status"] }, + { variable: "latestQC", fields: ["date", "result", "reason", "status"] }, +]; - const [smFilters, tFilters, bcFilters, mcFilters, qcFilters] = searchFilters; +function buildPartialCypherQuery({ + searchVals, + sampleContext, + addlOncotreeCodes, +}: { + searchVals: QueryDashboardSampleCountArgs["searchVals"]; + sampleContext?: QueryDashboardSampleCountArgs["sampleContext"]; + addlOncotreeCodes: string[]; +}) { + // Build search filters given user's search values input. For example: + // latestSm.primaryId =~ '(?i).*(someInput).*' OR latestSm.cmoSampleName =~ '(?i).*(someInput).* OR ... + const searchFilters = searchVals?.length + ? searchFiltersConfig + .map((c) => + buildSearchFilters({ + variable: c.variable, + fields: c.fields, + searchVals, + }) + ) + .join(" OR ") + : ""; - const addlOncotreeCodeFilters = - addlOncotreeCodes.length > 0 - ? ` OR ${buildSearchFilter("sm", ["oncotreeCode"], addlOncotreeCodes)}` - : ""; + // Add add'l Oncotree codes to search if user inputted "cancerTypeDetailed" or "cancerType" values + const addlOncotreeCodeFilters = addlOncotreeCodes.length + ? ` OR ${buildSearchFilters({ + variable: "latestSm", + fields: ["oncotreeCode"], + searchVals: addlOncotreeCodes, + useFuzzyMatch: false, + })}` + : ""; - const smOrFilters = smFilters - ? `${"(" + smFilters + addlOncotreeCodeFilters + ")"}` + const fullSearchFilters = searchFilters + ? `(${searchFilters}${addlOncotreeCodeFilters})` : ""; - const wesFilters = + // Filters for the WES Samples view on Samples page + const wesContext = sampleContext?.fieldName === "genePanel" - ? `${smFilters && " AND "}${buildSearchFilter( - "sm", - ["genePanel"], - sampleContext.values - )}` + ? `${fullSearchFilters && " AND "}${buildSearchFilters({ + variable: "latestSm", + fields: ["genePanel"], + searchVals: sampleContext.values, + })}` : ""; - const requestFilters = + // Filters for the Request Samples view + const requestContext = sampleContext?.fieldName === "igoRequestId" - ? `${smOrFilters && " AND "}${buildSearchFilter( - "sm", - ["igoRequestId"], - sampleContext.values, - false - )}` + ? `${fullSearchFilters && " AND "}latestSm.igoRequestId = '${ + sampleContext.values[0] + }'` : ""; - const patientFilters = + // Filters for the Patient Samples view + const patientContext = sampleContext?.fieldName === "patientId" - ? `${buildSearchFilter("pa", ["value"], sampleContext.values, false)}` + ? `pa.value = '${sampleContext.values[0]}'` : ""; - const cohortFilters = + // Filters for the Cohort Samples view + const cohortContext = sampleContext?.fieldName === "cohortId" - ? `${buildSearchFilter("c", ["cohortId"], sampleContext.values, false)}` + ? `c.cohortId = '${sampleContext.values[0]}'` : ""; - let allSmFilters = ""; - if (smOrFilters || wesFilters || requestFilters) { - allSmFilters = "WHERE " + smOrFilters + wesFilters + requestFilters; - } - const partialCypherQuery = ` - // all Samples have at least one SampleMetadata (SampleMetadata is required) - MATCH (s:Sample)-[:HAS_METADATA]->(sm:SampleMetadata) - - ${allSmFilters} - - // now get the most recent import date for each Sample from the SampleMetadata (we still have all the SampleMetadata for each Sample) - WITH s, collect(sm) AS allSampleMetadata, max(sm.importDate) AS latestImportDate - - // now only keep one of the SampleMetadata that has the most recent importDate (if there is more than one we take the first) - WITH s, [sm IN allSampleMetadata WHERE sm.importDate = latestImportDate][0] AS latestSm - - // if the most recent SampleMetadata for a Sample has a Status attached to it - OPTIONAL MATCH (latestSm)-[:HAS_STATUS]->(st:Status) - WITH s, latestSm, st AS latestSt - - MATCH (s)<-[:HAS_SAMPLE]-(p:Patient)<-[:IS_ALIAS]-(pa:PatientAlias) - ${patientFilters && `WHERE ${patientFilters}`} - - // if the Sample belongs to any Cohorts, get them - the Cohort will have a CohortComplete so get that too - ${ - cohortFilters ? "" : "OPTIONAL " - }MATCH (s:Sample)<-[:HAS_COHORT_SAMPLE]-(c:Cohort)-[:HAS_COHORT_COMPLETE]->(cc:CohortComplete) - - ${cohortFilters && `WHERE ${cohortFilters}`} - - // we then collect all the CohortCompletes for each Sample and get the most recent CohortComplete.date - WITH s, latestSm, latestSt, collect(cc) AS allCohortComplete, min(cc.date) AS oldestCCDate - - // now only keep one of the CohortCompletes that has the most recent CohortComplete date (if there is more than one take the first) - WITH s, latestSm, latestSt, [cc IN allCohortComplete WHERE cc.date = oldestCCDate][0] AS oldestCC - - // if the Sample has Tempos get them - OPTIONAL MATCH (s:Sample)-[:HAS_TEMPO]->(t:Tempo) - - ${tFilters && `WHERE ${tFilters}`} - - // now get the most recent date for each Sample from the Tempos (we still have all the Tempos for each Sample) - WITH s, latestSm, latestSt, oldestCC, collect(t) AS allTempos, max(t.date) AS latestTDate - - // now only keep one of the Tempos that has the most recent date (if there is more than one we take the first) - WITH s, latestSm, latestSt, oldestCC, [t IN allTempos WHERE t.date = latestTDate][0] AS latestT - - // if the Tempo has any BamCompletes, get them - OPTIONAL MATCH (latestT)-[:HAS_EVENT]->(bc:BamComplete) - - ${bcFilters && `WHERE ${bcFilters}`} - - // now get the most recent date for each BamComplete (we still have all the BamCompletes for each Tempo) - WITH s, latestSm, latestSt, oldestCC, latestT, collect(bc) AS allBamCompletes, max(bc.date) AS latestBCDate - - // now only keep one of the BamCompletes that has the most recent date (if there is more than one we take the first) - WITH s, latestSm, latestSt, oldestCC, latestT, [bc IN allBamCompletes WHERE bc.date = latestBCDate][0] AS latestBC - - // if the Tempo has any MafCompletes, get them - OPTIONAL MATCH (latestT)-[:HAS_EVENT]->(mc:MafComplete) - - ${mcFilters && `WHERE ${mcFilters}`} - - // now get the most recent date for each MafComplete (we still have all the MafCompletes for each Tempo) - WITH s, latestSm, latestSt, oldestCC, latestT, latestBC, collect(mc) AS allMafCompletes, max(mc.date) AS latestMCDate - - // now only keep one of the MafCompletes that has the most recent date (if there is more than one we take the first) - WITH s, latestSm, latestSt, oldestCC, latestT, latestBC, [mc IN allMafCompletes WHERE mc.date = latestMCDate][0] AS latestMC - - // if the Tempo has any QcCompletes, get them - OPTIONAL MATCH (latestT)-[:HAS_EVENT]->(qc:QcComplete) - - ${qcFilters && `WHERE ${qcFilters}`} - - // now get the most recent date for each QcComplete (we still have all the QcCompletes for each Tempo) - WITH s, latestSm, latestSt, oldestCC, latestT, latestBC, latestMC, collect(qc) AS allQcCompletes, max(qc.date) AS latestQCDate - - // now only keep one of the QcCompletes that has the most recent date (if there is more than one we take the first) - WITH s, latestSm, latestSt, oldestCC, latestT, latestBC, latestMC, [qc IN allQcCompletes WHERE qc.date = latestQCDate][0] AS latestQC - - // return whatever we need (TODO would it be faster if we only return the fields we need? should we be filtering those from the start of the query?) - WITH s AS sample, - latestSm, - latestSt, - oldestCC, - latestT, - latestBC, - latestMC, - latestQC + // Get Sample and the most recent SampleMetadata + MATCH (s:Sample)-[:HAS_METADATA]->(sm:SampleMetadata) + WITH s, collect(sm) AS allSampleMetadata, max(sm.importDate) AS latestImportDate + WITH s, [sm IN allSampleMetadata WHERE sm.importDate = latestImportDate][0] AS latestSm + + // Get SampleMetadata's Status + OPTIONAL MATCH (latestSm)-[:HAS_STATUS]->(st:Status) + WITH s, latestSm, st AS latestSt + + // Filters for Patient Samples view, if applicable + ${ + patientContext && + `MATCH (s)<-[:HAS_SAMPLE]-(p:Patient)<-[:IS_ALIAS]-(pa:PatientAlias) WHERE ${patientContext}` + } + + // Filters for Cohort Samples view, if applicable + ${ + cohortContext ? "" : "OPTIONAL " + }MATCH (s:Sample)<-[:HAS_COHORT_SAMPLE]-(c:Cohort)-[:HAS_COHORT_COMPLETE]->(cc:CohortComplete) + ${cohortContext && `WHERE ${cohortContext}`} + + // Get the oldest CohortComplete date ("Initial Pipeline Run Date" in Cohort Samples view) + WITH s, latestSm, latestSt, min(cc.date) AS oldestCCDate + + // Get Tempo data + OPTIONAL MATCH (s:Sample)-[:HAS_TEMPO]->(t:Tempo) + WITH s, latestSm, latestSt, oldestCCDate, t + + // Get the most recent BamComplete event + OPTIONAL MATCH (t)-[:HAS_EVENT]->(bc:BamComplete) + WITH s, latestSm, latestSt, oldestCCDate, t, collect(bc) AS allBamCompletes, max(bc.date) AS latestBCDate + WITH s, latestSm, latestSt, oldestCCDate, t, [bc IN allBamCompletes WHERE bc.date = latestBCDate][0] AS latestBC + + // Get the most recent MafComplete event + OPTIONAL MATCH (t)-[:HAS_EVENT]->(mc:MafComplete) + WITH s, latestSm, latestSt, oldestCCDate, t, latestBC, collect(mc) AS allMafCompletes, max(mc.date) AS latestMCDate + WITH s, latestSm, latestSt, oldestCCDate, t, latestBC, [mc IN allMafCompletes WHERE mc.date = latestMCDate][0] AS latestMC + + // Get the most recent QcComplete event + OPTIONAL MATCH (t)-[:HAS_EVENT]->(qc:QcComplete) + WITH s, latestSm, latestSt, oldestCCDate, t, latestBC, latestMC, collect(qc) AS allQcCompletes, max(qc.date) AS latestQCDate + WITH s, latestSm, latestSt, oldestCCDate, t, latestBC, latestMC, [qc IN allQcCompletes WHERE qc.date = latestQCDate][0] AS latestQC + + WITH + s, + latestSm, + latestSt, + oldestCCDate, + t, + latestBC, + latestMC, + latestQC + + ${ + fullSearchFilters || wesContext || requestContext + ? `WHERE ${fullSearchFilters}${wesContext}${requestContext}` + : "" + } `; return partialCypherQuery; @@ -468,11 +534,11 @@ function getAddlOtCodesMatchingCtOrCtdVals({ searchVals, oncotreeCache, }: { - searchVals: string[]; + searchVals: QueryDashboardSamplesArgs["searchVals"]; oncotreeCache: NodeCache; }) { let addlOncotreeCodes: Set = new Set(); - if (searchVals.length > 0) { + if (searchVals?.length) { oncotreeCache.keys().forEach((code) => { const { name, mainType } = (oncotreeCache.get( code @@ -487,5 +553,163 @@ function getAddlOtCodesMatchingCtOrCtdVals({ }); }); } - return addlOncotreeCodes; + return Array.from(addlOncotreeCodes); +} + +async function updateSampleMetadata( + newDashboardSample: DashboardSampleInput, + ogm: OGM +) { + return new Promise(async (resolve) => { + const sampleManifest = await request( + props.smile_sample_endpoint + newDashboardSample.primaryId, + { + json: true, + } + ); + + Object.keys(newDashboardSample).forEach((key) => { + if (key in sampleManifest) { + sampleManifest[key] = + newDashboardSample[key as keyof DashboardSampleInput]; + } + }); + + // Ensure validator and label generator use latest status data added during validation + delete sampleManifest["status"]; + + // Ensure isCmoSample is set in sample's 'additionalProperties' if not already present. + // This ensures that cmo samples get sent to the label generator after validation as + // some of the older SMILE samples do not have this additionalProperty set + if (sampleManifest["additionalProperties"]["isCmoSample"] == null) { + const requestId = sampleManifest["additionalProperties"]["igoRequestId"]; + let req = ogm.model("Request"); + const rd = await req.find({ + where: { igoRequestId: requestId }, + }); + sampleManifest["additionalProperties"]["isCmoSample"] = + rd[0]["isCmoRequest"].toString(); + } + + publishNatsMessage( + props.pub_validate_sample_update, + JSON.stringify(sampleManifest) + ); + + await ogm.model("Sample").update({ + where: { smileSampleId: sampleManifest.smileSampleId }, + update: { revisable: false }, + }); + + resolve(null); + }); +} + +async function updateTempo(newDashboardSample: DashboardSampleInput) { + return new Promise((resolve) => { + const dataForTempoBillingUpdate = { + primaryId: newDashboardSample.primaryId, + billed: newDashboardSample.billed, + billedBy: newDashboardSample.billedBy, + costCenter: newDashboardSample.costCenter, + accessLevel: newDashboardSample.accessLevel, + custodianInformation: newDashboardSample.custodianInformation, + }; + + publishNatsMessage( + props.pub_tempo_sample_billing, + JSON.stringify(dataForTempoBillingUpdate) + ); + + resolve(null); + }); +} + +const editableSampleMetadataFields = new Set([ + "cmoPatientId", + "investigatorSampleId", + "sampleType", + "preservation", + "tumorOrNormal", + "sampleClass", + "oncotreeCode", + "collectionYear", + "sampleOrigin", + "tissueLocation", +]); + +const editableTempoFields = new Set([ + "billed", + "costCenter", + "billedBy", + "custodianInformation", + "accessLevel", +]); + +async function updateAllSamplesConcurrently( + newDashboardSamples: DashboardSampleInput[], + ogm: OGM +) { + const allPromises = newDashboardSamples.map(async (dashboardSample) => { + try { + const metadataChanged = dashboardSample.changedFieldNames.some((field) => + editableSampleMetadataFields.has(field) + ); + const tempoChanged = dashboardSample.changedFieldNames.some((field) => + editableTempoFields.has(field) + ); + + const promises = []; + if (metadataChanged) { + promises.push(updateSampleMetadata(dashboardSample, ogm)); + } + if (tempoChanged) { + promises.push(updateTempo(dashboardSample)); + } + + return Promise.all(promises); + } catch (error) { + console.error( + `Failed to update sample with primaryId ${dashboardSample.primaryId}. Error:`, + error + ); + throw error; // ensure Promise.allSettled captures the error + } + }); + + await Promise.allSettled(allPromises); +} + +async function publishNatsMessage(topic: string, message: string) { + const sc = StringCodec(); + + const tlsOptions = { + keyFile: props.nats_key_pem, + certFile: props.nats_cert_pem, + caFile: props.nats_ca_pem, + rejectUnauthorized: false, + }; + + const natsConnProperties = { + servers: [props.nats_url], + user: props.nats_username, + pass: props.nats_password, + tls: tlsOptions, + }; + + try { + const natsConn = await connect(natsConnProperties); + console.log( + `Publishing message to NATS server at ${natsConn.getServer()} under topic ${topic}: `, + message + ); + const h = headers(); + h.append("Nats-Msg-Subject", topic); + natsConn.publish(topic, sc.encode(JSON.stringify(message)), { headers: h }); + } catch (err) { + console.error( + `error connecting to ${JSON.stringify(natsConnProperties)}`, + err + ); + } } diff --git a/graphql-server/src/schemas/neo4j.ts b/graphql-server/src/schemas/neo4j.ts index bb262757..f2d89e6d 100644 --- a/graphql-server/src/schemas/neo4j.ts +++ b/graphql-server/src/schemas/neo4j.ts @@ -4,23 +4,10 @@ import { OGM } from "@neo4j/graphql-ogm"; import { toGraphQLTypeDefs } from "@neo4j/introspector"; import { createHttpLink } from "apollo-link-http"; import { InMemoryCache, NormalizedCacheObject } from "apollo-cache-inmemory"; -import { props } from "../utils/constants"; -import { - SampleHasMetadataSampleMetadataUpdateFieldInput, - SampleHasTempoTemposUpdateFieldInput, - SampleMetadata, - SampleMetadataUpdateInput, - SampleUpdateInput, - SampleWhere, - SamplesDocument, - SortDirection, - Tempo, - UpdateSamplesMutationResponse, -} from "../generated/graphql"; -import { connect, headers, StringCodec } from "nats"; +import { SortDirection } from "../generated/graphql"; const fetch = require("node-fetch"); const request = require("request-promise-native"); -import { ApolloClient, ApolloQueryResult } from "apollo-client"; +import { ApolloClient } from "apollo-client"; import { gql } from "apollo-server"; import { flattenedCohortFields, @@ -102,84 +89,6 @@ function buildResolvers( apolloClient: ApolloClient ) { return { - Mutation: { - async updateSamples( - _source: any, - { where, update }: { where: SampleWhere; update: SampleUpdateInput } - ) { - // Grab data passed in from the frontend - const primaryId = where.hasMetadataSampleMetadata_SOME!.primaryId!; - - const sampleKeyForUpdate = Object.keys( - update - )[0] as keyof SampleUpdateInput; - - const changedFields = ( - update[sampleKeyForUpdate] as Array< - | SampleHasMetadataSampleMetadataUpdateFieldInput - | SampleHasTempoTemposUpdateFieldInput - > - )[0].update!.node!; - - // Get sample manifest from SMILE API /sampleById and update fields that were changed - const sampleManifest = await request( - props.smile_sample_endpoint + primaryId, - { - json: true, - } - ); - - Object.keys(changedFields).forEach((changedField) => { - const key = changedField as keyof SampleMetadataUpdateInput; - sampleManifest[key] = - changedFields[key as keyof typeof changedFields]; - }); - - // Get the sample data from the database and update the fields that were changed - const updatedSamples: ApolloQueryResult = - await apolloClient.query({ - query: SamplesDocument, - variables: { - where: { - smileSampleId: sampleManifest.smileSampleId, - }, - hasMetadataSampleMetadataOptions2: { - sort: [{ importDate: SortDirection.Desc }], - limit: 1, - }, - }, - }); - - Object.keys(changedFields).forEach((key) => { - const sample = updatedSamples.data.samples[0][sampleKeyForUpdate] as - | SampleMetadata[] - | Tempo[]; - if (Array.isArray(sample) && sample.length > 0) { - sample[0][key as keyof typeof sample[0]] = - changedFields[key as keyof typeof changedFields]; - } - }); - - // Publish the data updates to NATS - if ("hasMetadataSampleMetadata" in update) { - await publishNatsMessageForSampleMetadataUpdates(sampleManifest, ogm); - } else if ("hasTempoTempos" in update) { - await publishNatsMessageForSampleBillingUpdates( - primaryId, - updatedSamples - ); - } else { - throw new Error("Unknown update field"); - } - - // Return the updated samples data to enable optimistic UI updates. - // The shape of the data returned here doesn't fully match the shape of the data - // in the frontend, but it has all the fields being updated - return { - samples: updatedSamples.data.samples, - }; - }, - }, Query: { async requests(_source: undefined, args: any) { const requests = await ogm.model("Request").find({ @@ -335,90 +244,3 @@ function buildResolvers( Cohort: generateFieldResolvers(flattenedCohortFields, "Cohort"), }; } - -async function publishNatsMessageForSampleMetadataUpdates( - sampleManifest: any, - ogm: OGM -) { - // remove 'status' from sample metadata to ensure validator and label - // generator use latest status data added during validation process - delete sampleManifest["status"]; - - // add isCmoSample to sample's 'additionalProperties' if not already present - // this is to ensure that cmo samples get sent to the label generator after validation - // since some of the older SMILE samples do not have this additionalProperty set - if (sampleManifest["additionalProperties"]["isCmoSample"] == null) { - const requestId = sampleManifest["additionalProperties"]["igoRequestId"]; - let req = ogm.model("Request"); - const rd = await req.find({ - where: { igoRequestId: requestId }, - }); - sampleManifest["additionalProperties"]["isCmoSample"] = - rd[0]["isCmoRequest"].toString(); - } - - publishNatsMessage( - props.pub_validate_sample_update, - JSON.stringify(sampleManifest) - ); - - await ogm.model("Sample").update({ - where: { smileSampleId: sampleManifest.smileSampleId }, - update: { revisable: false }, - }); -} - -async function publishNatsMessageForSampleBillingUpdates( - primaryId: string, - updatedSamples: ApolloQueryResult -) { - const { billed, billedBy, costCenter, accessLevel, custodianInformation } = - updatedSamples.data.samples[0].hasTempoTempos[0]; - - const dataForTempoBillingUpdate = { - primaryId, - billed, - billedBy, - costCenter, - accessLevel, - custodianInformation, - }; - - publishNatsMessage( - props.pub_tempo_sample_billing, - JSON.stringify(dataForTempoBillingUpdate) - ); -} - -async function publishNatsMessage(topic: string, message: string) { - const sc = StringCodec(); - - const tlsOptions = { - keyFile: props.nats_key_pem, - certFile: props.nats_cert_pem, - caFile: props.nats_ca_pem, - rejectUnauthorized: false, - }; - - const natsConnProperties = { - servers: [props.nats_url], - user: props.nats_username, - pass: props.nats_password, - tls: tlsOptions, - }; - - try { - const natsConn = await connect(natsConnProperties); - console.log("Connected to server: "); - console.log(natsConn.getServer()); - console.log("publishing message: ", message, "\nto topic", topic); - const h = headers(); - h.append("Nats-Msg-Subject", topic); - natsConn.publish(topic, sc.encode(JSON.stringify(message)), { headers: h }); - } catch (err) { - console.log( - `error connecting to ${JSON.stringify(natsConnProperties)}`, - err - ); - } -} diff --git a/graphql-server/src/utils/servers.ts b/graphql-server/src/utils/servers.ts index e9af9a0d..91995187 100644 --- a/graphql-server/src/utils/servers.ts +++ b/graphql-server/src/utils/servers.ts @@ -3,6 +3,7 @@ import fs from "fs"; import https from "https"; import { props } from "./constants"; import { buildNeo4jDbSchema } from "../schemas/neo4j"; +import { buildCustomSchema } from "../schemas/custom"; import { mergeSchemas } from "@graphql-tools/schema"; import { oracleDbSchema } from "../schemas/oracle"; import { ApolloServer } from "apollo-server-express"; @@ -14,7 +15,6 @@ import { updateActiveUserSessions } from "./session"; import { corsOptions } from "./constants"; import NodeCache from "node-cache"; import { fetchAndCacheOncotreeData } from "./oncotree"; -import { customSchema } from "../schemas/custom"; import neo4j from "neo4j-driver"; export function initializeHttpsServer(app: Express) { @@ -48,6 +48,7 @@ export async function initializeApolloServer( app: Express ) { const { neo4jDbSchema, ogm } = await buildNeo4jDbSchema(); + const customSchema = await buildCustomSchema(ogm); const mergedSchema = mergeSchemas({ schemas: [neo4jDbSchema, oracleDbSchema, customSchema], }); diff --git a/graphql.schema.json b/graphql.schema.json index 563e045c..ec6ce5d4 100644 --- a/graphql.schema.json +++ b/graphql.schema.json @@ -17509,244 +17509,763 @@ "name": "primaryId", "description": null, "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "qcCompleteDate", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "qcCompleteReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "qcCompleteResult", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "qcCompleteStatus", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "recipe", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "revisable", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sampleClass", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sampleOrigin", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sampleType", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sex", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "smileSampleId", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "species", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tissueLocation", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "tumorOrNormal", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "validationReport", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "validationStatus", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "DashboardSampleCount", + "description": null, + "fields": [ + { + "name": "totalCount", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INPUT_OBJECT", + "name": "DashboardSampleInput", + "description": null, + "fields": null, + "inputFields": [ + { + "name": "accessLevel", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "baitSet", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bamCompleteDate", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "bamCompleteStatus", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "billed", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "billedBy", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cancerType", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cancerTypeDetailed", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "changedFieldNames", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + } + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cmoPatientId", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "cmoSampleName", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "collectionYear", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "costCenter", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "custodianInformation", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "embargoDate", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "genePanel", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "importDate", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "initialPipelineRunDate", + "description": null, "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "qcCompleteDate", + "name": "investigatorSampleId", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "qcCompleteReason", + "name": "mafCompleteDate", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "qcCompleteResult", + "name": "mafCompleteNormalPrimaryId", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "qcCompleteStatus", + "name": "mafCompleteStatus", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "recipe", + "name": "oncotreeCode", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "revisable", + "name": "preservation", "description": null, - "args": [], "type": { "kind": "SCALAR", - "name": "Boolean", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "sampleClass", + "name": "primaryId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "qcCompleteDate", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "sampleOrigin", + "name": "qcCompleteReason", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "sampleType", + "name": "qcCompleteResult", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "sex", + "name": "qcCompleteStatus", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "smileSampleId", + "name": "recipe", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "species", + "name": "revisable", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "sampleClass", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "tissueLocation", + "name": "sampleOrigin", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "tumorOrNormal", + "name": "sampleType", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "validationReport", + "name": "sex", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "validationStatus", + "name": "smileSampleId", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "species", "description": null, - "args": [], "type": { "kind": "SCALAR", "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null - } - ], - "inputFields": null, - "interfaces": [], - "enumValues": null, - "possibleTypes": null - }, - { - "kind": "OBJECT", - "name": "DashboardSampleCount", - "description": null, - "fields": [ + }, { - "name": "count", + "name": "tissueLocation", "description": null, - "args": [], "type": { "kind": "SCALAR", - "name": "Int", + "name": "String", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null }, { - "name": "totalCount", + "name": "tumorOrNormal", "description": null, - "args": [], "type": { "kind": "SCALAR", - "name": "Int", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "validationReport", + "description": null, + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "validationStatus", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", "ofType": null }, + "defaultValue": null, "isDeprecated": false, "deprecationReason": null } ], - "inputFields": null, - "interfaces": [], + "interfaces": null, "enumValues": null, "possibleTypes": null }, @@ -22812,6 +23331,39 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "updateDashboardSamples", + "description": null, + "args": [ + { + "name": "newDashboardSamples", + "description": null, + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "INPUT_OBJECT", + "name": "DashboardSampleInput", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DashboardSample", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "updateMafCompletes", "description": null, @@ -44849,9 +45401,13 @@ "kind": "LIST", "name": null, "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } } }, "defaultValue": null, @@ -44860,9 +45416,13 @@ } ], "type": { - "kind": "OBJECT", - "name": "DashboardSampleCount", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DashboardSampleCount", + "ofType": null + } }, "isDeprecated": false, "deprecationReason": null @@ -44871,6 +45431,22 @@ "name": "dashboardSamples", "description": null, "args": [ + { + "name": "limit", + "description": null, + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "sampleContext", "description": null, @@ -44890,9 +45466,13 @@ "kind": "LIST", "name": null, "ofType": { - "kind": "SCALAR", - "name": "String", - "ofType": null + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } } }, "defaultValue": null, @@ -44901,12 +45481,20 @@ } ], "type": { - "kind": "LIST", + "kind": "NON_NULL", "name": null, "ofType": { - "kind": "OBJECT", - "name": "DashboardSample", - "ofType": null + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "DashboardSample", + "ofType": null + } + } } }, "isDeprecated": false, diff --git a/graphql/operations.graphql b/graphql/operations.graphql index d5142d53..169f4daf 100644 --- a/graphql/operations.graphql +++ b/graphql/operations.graphql @@ -46,11 +46,19 @@ query PatientsList($options: PatientOptions, $where: PatientWhere) { } } -query DashboardSamples($searchVals: [String], $sampleContext: SampleContext) { +query DashboardSamples( + $searchVals: [String!] + $sampleContext: SampleContext + $limit: Int! +) { dashboardSampleCount(searchVals: $searchVals, sampleContext: $sampleContext) { totalCount } - dashboardSamples(searchVals: $searchVals, sampleContext: $sampleContext) { + dashboardSamples( + searchVals: $searchVals + sampleContext: $sampleContext + limit: $limit + ) { ...DashboardSampleParts ...DashboardSampleMetadataParts ...DashboardTempoParts @@ -140,95 +148,13 @@ fragment RequestParts on Request { smileRequestId } -fragment SampleParts on Sample { - datasource - revisable - sampleCategory - sampleClass - smileSampleId -} - -fragment SampleMetadataParts on SampleMetadata { - additionalProperties - baitSet - cfDNA2dBarcode - cmoInfoIgoId - cmoPatientId - cmoSampleIdFields - cmoSampleName - collectionYear - genePanel - igoComplete - igoRequestId - importDate - investigatorSampleId - libraries - oncotreeCode - preservation - primaryId - qcReports - sampleClass - sampleName - sampleOrigin - sampleType - sex - species - tissueLocation - tubeId - tumorOrNormal -} - -fragment TempoParts on Tempo { - smileTempoId - billed - billedBy - costCenter - custodianInformation - accessLevel -} - -query Samples( - $where: SampleWhere - $hasMetadataSampleMetadataWhere2: SampleMetadataWhere - $hasMetadataSampleMetadataOptions2: SampleMetadataOptions -) { - samples(where: $where) { - smileSampleId - revisable - sampleCategory - sampleClass - datasource - hasMetadataSampleMetadata( - where: $hasMetadataSampleMetadataWhere2 - options: $hasMetadataSampleMetadataOptions2 - ) { - ...SampleMetadataParts - } - hasTempoTempos { - ...TempoParts - } - } -} - -mutation UpdateSamples( - $where: SampleWhere - $update: SampleUpdateInput - $connect: SampleConnectInput +mutation UpdateDashboardSamples( + $newDashboardSamples: [DashboardSampleInput!]! ) { - updateSamples(where: $where, update: $update, connect: $connect) { - samples { - smileSampleId - revisable - datasource - sampleCategory - sampleClass - hasMetadataSampleMetadata { - ...SampleMetadataParts - } - hasTempoTempos { - ...TempoParts - } - } + updateDashboardSamples(newDashboardSamples: $newDashboardSamples) { + ...DashboardSampleParts + ...DashboardSampleMetadataParts + ...DashboardTempoParts } }